001 /*
002 * Copyright 2007 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005 * use this file except in compliance with the License. You may obtain a copy of
006 * the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013 * License for the specific language governing permissions and limitations under
014 * the License.
015 */
016 package org.springframework.richclient.beans;
017
018 import java.beans.PropertyEditor;
019 import java.io.ByteArrayOutputStream;
020 import java.io.PrintWriter;
021 import java.lang.reflect.Array;
022 import java.lang.reflect.Field;
023 import java.lang.reflect.Member;
024 import java.lang.reflect.Method;
025 import java.lang.reflect.Modifier;
026 import java.util.ArrayList;
027 import java.util.Collection;
028 import java.util.HashMap;
029 import java.util.HashSet;
030 import java.util.List;
031 import java.util.Map;
032 import java.util.Set;
033
034 import org.springframework.beans.AbstractPropertyAccessor;
035 import org.springframework.beans.BeansException;
036 import org.springframework.beans.InvalidPropertyException;
037 import org.springframework.beans.NotReadablePropertyException;
038 import org.springframework.core.JdkVersion;
039 import org.springframework.core.MethodParameter;
040 import org.springframework.richclient.core.GenericCollectionTypeResolver;
041 import org.springframework.util.Assert;
042
043 /**
044 * PropertyAccessor implementation that determines property types by field, if
045 * available. Otherwise methods are used.
046 *
047 * Actual access to properties is left for implementation for subclasses.
048 *
049 * This implementation does not support nested properties. Use
050 * {@link AbstractNestedMemberPropertyAccessor}, if you need nested
051 * property-support.
052 *
053 * @author Arne Limburg
054 *
055 */
056 public abstract class AbstractMemberPropertyAccessor extends AbstractPropertyAccessor {
057
058 private Class targetClass;
059
060 private final boolean fieldAccessEnabled;
061
062 private final Map readAccessors = new HashMap();
063
064 private final Map writeAccessors = new HashMap();
065
066 /**
067 * Creates a new <tt>AbstractMemberPropertyAccessor</tt>.
068 * @param targetClass the target class.
069 * @param fieldAccessEnabled whether field access should be used for
070 * property type determination.
071 */
072 protected AbstractMemberPropertyAccessor(Class targetClass, boolean fieldAccessEnabled) {
073 this.fieldAccessEnabled = fieldAccessEnabled;
074 registerDefaultEditors();
075 setTargetClass(targetClass);
076 }
077
078 /**
079 * Clears all cached members and introspect methods again. If fieldAccess is
080 * enabled introspect fields as well.
081 *
082 * @param targetClass the target class.
083 */
084 protected void setTargetClass(Class targetClass) {
085 this.targetClass = targetClass;
086 this.readAccessors.clear();
087 this.writeAccessors.clear();
088 introspectMethods(targetClass, new HashSet());
089 if (isFieldAccessEnabled()) {
090 introspectFields(targetClass, new HashSet());
091 }
092 }
093
094 /**
095 * Introspect fields of a class. This excludes static fields and handles
096 * final fields as readOnly.
097 *
098 * @param type the class to inspect.
099 * @param introspectedClasses a set of already inspected classes.
100 */
101 private void introspectFields(Class type, Set introspectedClasses) {
102 if (type == null || Object.class.equals(type) || type.isInterface() || introspectedClasses.contains(type)) {
103 return;
104 }
105 introspectedClasses.add(type);
106 introspectFields(type.getSuperclass(), introspectedClasses);
107 Field[] fields = type.getDeclaredFields();
108 for (int i = 0; i < fields.length; i++) {
109 if (!Modifier.isStatic(fields[i].getModifiers())) {
110 readAccessors.put(fields[i].getName(), fields[i]);
111 if (!Modifier.isFinal(fields[i].getModifiers())) {
112 writeAccessors.put(fields[i].getName(), fields[i]);
113 }
114 }
115 }
116 }
117
118 /**
119 * Introspect class for accessor methods. This includes methods starting
120 * with 'get', 'set' and 'is'.
121 *
122 * @param type class to introspect.
123 * @param introspectedClasses set of already inspected classes.
124 */
125 private void introspectMethods(Class type, Set introspectedClasses) {
126 if (type == null || Object.class.equals(type) || introspectedClasses.contains(type)) {
127 return;
128 }
129 introspectedClasses.add(type);
130 Class[] interfaces = type.getInterfaces();
131 for (int i = 0; i < interfaces.length; i++) {
132 introspectMethods(interfaces[i], introspectedClasses);
133 }
134 introspectMethods(type.getSuperclass(), introspectedClasses);
135 Method[] methods = type.getDeclaredMethods();
136 for (int i = 0; i < methods.length; i++) {
137 String methodName = methods[i].getName();
138 if (methodName.startsWith("get") && methods[i].getParameterTypes().length == 0) {
139 readAccessors.put(getPropertyName(methodName, 3), methods[i]);
140 }
141 else if (methodName.startsWith("is") && methods[i].getParameterTypes().length == 0) {
142 readAccessors.put(getPropertyName(methodName, 2), methods[i]);
143 }
144 else if (methodName.startsWith("set") && methods[i].getParameterTypes().length == 1) {
145 writeAccessors.put(getPropertyName(methodName, 3), methods[i]);
146 }
147 }
148 }
149
150 /**
151 * Returns whether this PropertyAccessor should inspect fields.
152 */
153 public boolean isFieldAccessEnabled() {
154 return fieldAccessEnabled;
155 }
156
157 /**
158 * Returns the class used to introspect members.
159 */
160 public Class getTargetClass() {
161 return targetClass;
162 }
163
164 /**
165 * Return the read accessor for the given property.
166 *
167 * @param propertyName name of the property.
168 * @return a Member to read the property or <code>null</code>.
169 */
170 protected Member getReadPropertyAccessor(String propertyName) {
171 return (Member) readAccessors.get(propertyName);
172 }
173
174 /**
175 * Return the write accessor for the given property.
176 *
177 * @param propertyName name of the property.
178 * @return a Member to write the property or <code>null</code>.
179 */
180 protected Member getWritePropertyAccessor(String propertyName) {
181 return (Member) writeAccessors.get(propertyName);
182 }
183
184 /**
185 * Return any accessor, be it read or write, for the given property.
186 *
187 * @param propertyName name of the property.
188 * @return an accessor for the property or <code>null</code>
189 */
190 protected Member getPropertyAccessor(String propertyName) {
191 if (readAccessors.containsKey(propertyName)) {
192 return (Member) readAccessors.get(propertyName);
193 }
194 else {
195 return (Member) writeAccessors.get(propertyName);
196 }
197 }
198
199 /**
200 * {@inheritDoc}
201 */
202 public boolean isReadableProperty(String propertyName) {
203 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) {
204 String rootProperty = getRootPropertyName(propertyName);
205 String parentProperty = getParentPropertyName(propertyName);
206 return isReadableProperty(rootProperty)
207 && checkKeyTypes(propertyName)
208 && (!getPropertyType(parentProperty).isArray() || checkSize(propertyName) || isWritableProperty(parentProperty))
209 && ((isReadableProperty(parentProperty) && getPropertyValue(parentProperty) != null) || isWritableProperty(parentProperty));
210 }
211 else {
212 return readAccessors.containsKey(propertyName);
213 }
214 }
215
216 /**
217 * {@inheritDoc}
218 */
219 public boolean isWritableProperty(String propertyName) {
220 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) {
221 // if an indexed property is readable it is writable, too
222 return isReadableProperty(propertyName);
223 }
224 else {
225 return writeAccessors.containsKey(propertyName);
226 }
227 }
228
229 /**
230 * {@inheritDoc}
231 */
232 public Class getPropertyType(String propertyName) {
233 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) {
234 int nestingLevel = PropertyAccessorUtils.getNestingLevel(propertyName);
235 if (JdkVersion.isAtLeastJava15()) {
236 Member accessor = getPropertyAccessor(getRootPropertyName(propertyName));
237 if (accessor instanceof Field) {
238 return GenericCollectionTypeResolver.getIndexedValueFieldType((Field) accessor, nestingLevel);
239 }
240 else {
241 Method accessorMethod = (Method) accessor;
242 MethodParameter parameter = new MethodParameter(accessorMethod,
243 accessorMethod.getParameterTypes().length - 1);
244 return GenericCollectionTypeResolver.getIndexedValueMethodType(parameter, nestingLevel);
245 }
246 }
247 else {
248 // we can only resolve array types in Java 1.4
249 Class type = getPropertyType(getRootPropertyName(propertyName));
250 for (int i = 0; i < nestingLevel; i++) {
251 if (type.isArray()) {
252 type = type.getComponentType();
253 }
254 else {
255 return Object.class; // cannot resolve type
256 }
257 }
258 return type;
259 }
260 }
261 else {
262 Member readAccessor = (Member) readAccessors.get(propertyName);
263 if (readAccessor instanceof Field) {
264 return ((Field) readAccessor).getType();
265 }
266 else if (readAccessor instanceof Method) {
267 return ((Method) readAccessor).getReturnType();
268 }
269 Member writeAccessor = (Member) writeAccessors.get(propertyName);
270 if (writeAccessor instanceof Field) {
271 return ((Field) writeAccessor).getType();
272 }
273 else if (writeAccessor instanceof Method) {
274 return ((Method) writeAccessor).getParameterTypes()[0];
275 }
276 }
277 return null;
278 }
279
280 /**
281 * Determine the type of the key used to index the collection/map. When jdk
282 * is at least 1.5, maps can be specified with generics and their key type
283 * can be resolved.
284 *
285 * @param propertyName name of the property.
286 * @return the type of the key. An integer if it's not a map, {@link String}
287 * if the jdk is less than 1.5, a specific type if the map was generified.
288 */
289 public Class getIndexedPropertyKeyType(String propertyName) {
290 if (!PropertyAccessorUtils.isIndexedProperty(propertyName)) {
291 throw new IllegalArgumentException("'" + propertyName + "' is no indexed property");
292 }
293 Class type = getPropertyType(getParentPropertyName(propertyName));
294 if (!Map.class.isAssignableFrom(type)) {
295 return Integer.class;
296 }
297 if (JdkVersion.isAtLeastJava15()) {
298 int nestingLevel = PropertyAccessorUtils.getNestingLevel(propertyName) - 1;
299 Member accessor = getPropertyAccessor(getRootPropertyName(propertyName));
300 if (accessor instanceof Field) {
301 return GenericCollectionTypeResolver.getMapKeyFieldType((Field) accessor, nestingLevel);
302 }
303 else if (accessor instanceof Method) {
304 MethodParameter parameter = new MethodParameter((Method) accessor, ((Method) accessor)
305 .getParameterTypes().length - 1, nestingLevel);
306 return GenericCollectionTypeResolver.getMapKeyParameterType(parameter);
307 }
308 else {
309 throw new InvalidPropertyException(getTargetClass(), propertyName, "property not accessable");
310 }
311 }
312 else {
313 return String.class; // the default for Java 1.4
314 }
315 }
316
317 /**
318 * {@inheritDoc}
319 */
320 public Object getPropertyValue(String propertyName) throws BeansException {
321 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) {
322 return getIndexedPropertyValue(propertyName);
323 }
324 else {
325 return getSimplePropertyValue(propertyName);
326 }
327 }
328
329 /**
330 * {@inheritDoc}
331 */
332 public void setPropertyValue(String propertyName, Object value) throws BeansException {
333 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) {
334 setIndexedPropertyValue(propertyName, value);
335 }
336 else {
337 setSimplePropertyValue(propertyName, value);
338 }
339 }
340
341 /**
342 * Retrieve the value of an indexed property.
343 *
344 * @param propertyName name of the property.
345 * @return value of the property.
346 */
347 protected abstract Object getIndexedPropertyValue(String propertyName);
348
349 /**
350 * Retrieve the value of a simple property (non-indexed).
351 *
352 * @param propertyName name of the property.
353 * @return value of the property.
354 */
355 protected abstract Object getSimplePropertyValue(String propertyName);
356
357 /**
358 * Set the value of an indexed property.
359 *
360 * @param propertyName name of the property.
361 * @param value new value for the property.
362 */
363 protected abstract void setIndexedPropertyValue(String propertyName, Object value);
364
365 /**
366 * Set the value of a simple property (non-indexed).
367 *
368 * @param propertyName name of the property.
369 * @param value new value for the property.
370 */
371 protected abstract void setSimplePropertyValue(String propertyName, Object value);
372
373 /**
374 * Returns the propertyName based on the methodName. Cuts of the prefix and
375 * removes first capital.
376 *
377 * @param methodName name of method to convert.
378 * @param prefixLength length of prefix to cut of.
379 * @return property name.
380 */
381 protected String getPropertyName(String methodName, int prefixLength) {
382 return Character.toLowerCase(methodName.charAt(prefixLength)) + methodName.substring(prefixLength + 1);
383 }
384
385 /**
386 * Returns the root property of an indexed property. The root property is
387 * the property that contains no indices.
388 *
389 * @param propertyName the name of the property.
390 * @return the root property.
391 */
392 protected String getRootPropertyName(String propertyName) {
393 int location = propertyName.indexOf(PROPERTY_KEY_PREFIX);
394 return location == -1 ? propertyName : propertyName.substring(0, location);
395 }
396
397 /**
398 * Return the parent property name of an indexed property or the empty string.
399 *
400 * @param propertyName the name of the property.
401 * @return the empty string or the parent property name if it was indexed.
402 */
403 protected String getParentPropertyName(String propertyName) {
404 if (!PropertyAccessorUtils.isIndexedProperty(propertyName)) {
405 return "";
406 }
407 else {
408 return propertyName.substring(0, propertyName.lastIndexOf(PROPERTY_KEY_PREFIX_CHAR));
409 }
410 }
411
412 protected boolean checkKeyTypes(String propertyName) {
413 try {
414 getIndices(propertyName);
415 return true;
416 }
417 catch (Exception e) {
418 return false;
419 }
420 }
421
422 protected Object[] getIndices(String propertyName) {
423 int location = propertyName.indexOf(PROPERTY_KEY_PREFIX);
424 if (location == -1) {
425 return new Object[0];
426 }
427 String[] indexStrings = split(propertyName.substring(location));
428 Object[] indices = new Object[indexStrings.length];
429 String rootPropertyName = getRootPropertyName(propertyName);
430 String indexedPropertyName = rootPropertyName;
431 for (int i = 0; i < indices.length; i++) {
432 indexedPropertyName += '[' + indexStrings[i] + ']';
433 Class keyType = getIndexedPropertyKeyType(indexedPropertyName);
434 indices[i] = convert(keyType, indexStrings[i]);
435 }
436 return indices;
437 }
438
439 private Object convert(Class targetClass, String value) {
440 if (Object.class.equals(targetClass) || String.class.equals(targetClass)) {
441 return value;
442 }
443 PropertyEditor editor = getDefaultEditor(targetClass);
444 editor.setAsText(value);
445 return editor.getValue();
446 }
447
448 private String[] split(String indices) {
449 Assert.isTrue(indices.startsWith(PROPERTY_KEY_PREFIX));
450 Assert.isTrue(indices.endsWith(PROPERTY_KEY_SUFFIX));
451 List result = new ArrayList();
452 int fromIndex = 1;
453 int toIndex = -1;
454 while ((toIndex = indices.indexOf("][", fromIndex)) != -1) {
455 result.add(indices.substring(fromIndex, toIndex));
456 fromIndex = toIndex + 2;
457 }
458 result.add(indices.substring(fromIndex, indices.length() - 1));
459 return (String[]) result.toArray(new String[result.size()]);
460 }
461
462 private boolean checkSize(String propertyName) {
463 String parentPropertyName = getParentPropertyName(propertyName);
464 if (!getPropertyType(parentPropertyName).isArray()) {
465 try {
466 // collections are considered to be expandable
467 // so if it is not null, any index matches
468 return getPropertyValue(parentPropertyName) != null;
469 }
470 catch (NotReadablePropertyException e) {
471 return false;
472 }
473 }
474 int from = propertyName.lastIndexOf(PROPERTY_KEY_PREFIX_CHAR) + 1;
475 int to = propertyName.length() - 1;
476 int index = Integer.parseInt(propertyName.substring(from, to));
477 try {
478 Object parentProperty = getPropertyValue(parentPropertyName);
479 return parentProperty != null && Array.getLength(parentProperty) > index;
480 }
481 catch (NotReadablePropertyException e) {
482 return false;
483 }
484 }
485
486 protected NotReadablePropertyException createNotReadablePropertyException(String propertyName, Exception e) {
487 if (JdkVersion.isAtLeastJava14()) {
488 NotReadablePropertyException beanException = new NotReadablePropertyException(getTargetClass(),
489 propertyName);
490 beanException.initCause(e);
491 return beanException;
492 }
493 else {
494 ByteArrayOutputStream stackTrace = new ByteArrayOutputStream();
495 PrintWriter stackTraceWriter = new PrintWriter(stackTrace);
496 e.printStackTrace(stackTraceWriter);
497 stackTraceWriter.close();
498 return new NotReadablePropertyException(getTargetClass(), propertyName,
499 new String(stackTrace.toByteArray()));
500 }
501 }
502
503 /**
504 * Helper method for subclasses to set values of indexed properties, like
505 * map-values, collection-values or array-values.
506 *
507 * @param assemblageType either map or collection or array
508 * @param assemblage the assemblage to set the value on
509 * @param index the index to set the value at
510 * @param value the value to set
511 * @return the assemblage
512 */
513 protected Object setAssemblageValue(Class assemblageType, Object assemblage, Object index, Object value) {
514 if (assemblageType.isArray()) {
515 int i = ((Integer) index).intValue();
516 if (Array.getLength(assemblage) <= i) {
517 Object newAssemblage = Array.newInstance(assemblageType.getComponentType(), i + 1);
518 System.arraycopy(assemblage, 0, newAssemblage, 0, Array.getLength(assemblage));
519 assemblage = newAssemblage;
520 }
521 Array.set(assemblage, i, value);
522 }
523 else if (List.class.isAssignableFrom(assemblageType)) {
524 int i = ((Integer) index).intValue();
525 List list = (List) assemblage;
526 if (list.size() > i) {
527 list.set(i, value);
528 }
529 else {
530 while (list.size() < i) {
531 list.add(null);
532 }
533 list.add(value);
534 }
535 }
536 else if (Map.class.isAssignableFrom(assemblageType)) {
537 ((Map) assemblage).put(index, value);
538 }
539 else if (assemblage instanceof Collection) {
540 ((Collection) assemblage).add(value);
541 }
542 else {
543 throw new IllegalArgumentException("assemblage must be of type array, collection or map.");
544 }
545 return assemblage;
546 }
547 }