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    }