MappedPropertyDescriptor.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.apache.commons.beanutils2;

  18. import java.beans.IntrospectionException;
  19. import java.beans.PropertyDescriptor;
  20. import java.lang.ref.Reference;
  21. import java.lang.ref.SoftReference;
  22. import java.lang.ref.WeakReference;
  23. import java.lang.reflect.Method;
  24. import java.lang.reflect.Modifier;

  25. /**
  26.  * A MappedPropertyDescriptor describes one mapped property. Mapped properties are multivalued properties like indexed properties but that are accessed with a
  27.  * String key instead of an index. Such property values are typically stored in a Map collection. For this class to work properly, a mapped value must have
  28.  * getter and setter methods of the form
  29.  * <p>
  30.  * {@code get<strong>Property</strong>(String key)} and
  31.  * <p>
  32.  * {@code set<strong>Property</strong>(String key, Object value)},
  33.  * <p>
  34.  * where {@code <strong>Property</strong>} must be replaced by the name of the property.
  35.  *
  36.  * @see java.beans.PropertyDescriptor
  37.  */
  38. public class MappedPropertyDescriptor extends PropertyDescriptor {

  39.     /**
  40.      * Holds a {@link Method} in a {@link SoftReference} so that it it doesn't prevent any ClassLoader being garbage collected, but tries to re-create the
  41.      * method if the method reference has been released.
  42.      *
  43.      * See https://issues.apache.org/jira/browse/BEANUTILS-291
  44.      */
  45.     private static final class MappedMethodReference {
  46.         private String className;
  47.         private String methodName;
  48.         private Reference<Method> methodRef;
  49.         private Reference<Class<?>> classRef;
  50.         private Reference<Class<?>> writeParamTypeRef0;
  51.         private Reference<Class<?>> writeParamTypeRef1;
  52.         private String[] writeParamClassNames;

  53.         MappedMethodReference(final Method m) {
  54.             if (m != null) {
  55.                 className = m.getDeclaringClass().getName();
  56.                 methodName = m.getName();
  57.                 methodRef = new SoftReference<>(m);
  58.                 classRef = new WeakReference<>(m.getDeclaringClass());
  59.                 final Class<?>[] types = m.getParameterTypes();
  60.                 if (types.length == 2) {
  61.                     writeParamTypeRef0 = new WeakReference<>(types[0]);
  62.                     writeParamTypeRef1 = new WeakReference<>(types[1]);
  63.                     writeParamClassNames = new String[2];
  64.                     writeParamClassNames[0] = types[0].getName();
  65.                     writeParamClassNames[1] = types[1].getName();
  66.                 }
  67.             }
  68.         }

  69.         private Method get() {
  70.             if (methodRef == null) {
  71.                 return null;
  72.             }
  73.             Method m = methodRef.get();
  74.             if (m == null) {
  75.                 Class<?> clazz = classRef.get();
  76.                 if (clazz == null) {
  77.                     clazz = reLoadClass();
  78.                     if (clazz != null) {
  79.                         classRef = new WeakReference<>(clazz);
  80.                     }
  81.                 }
  82.                 if (clazz == null) {
  83.                     throw new RuntimeException("Method " + methodName + " for " + className + " could not be reconstructed - class reference has gone");
  84.                 }
  85.                 Class<?>[] paramTypes = null;
  86.                 if (writeParamClassNames != null) {
  87.                     paramTypes = new Class[2];
  88.                     paramTypes[0] = writeParamTypeRef0.get();
  89.                     if (paramTypes[0] == null) {
  90.                         paramTypes[0] = reLoadClass(writeParamClassNames[0]);
  91.                         if (paramTypes[0] != null) {
  92.                             writeParamTypeRef0 = new WeakReference<>(paramTypes[0]);
  93.                         }
  94.                     }
  95.                     paramTypes[1] = writeParamTypeRef1.get();
  96.                     if (paramTypes[1] == null) {
  97.                         paramTypes[1] = reLoadClass(writeParamClassNames[1]);
  98.                         if (paramTypes[1] != null) {
  99.                             writeParamTypeRef1 = new WeakReference<>(paramTypes[1]);
  100.                         }
  101.                     }
  102.                 } else {
  103.                     paramTypes = STRING_CLASS_PARAMETER;
  104.                 }
  105.                 try {
  106.                     m = clazz.getMethod(methodName, paramTypes);
  107.                     // Un-comment following line for testing
  108.                     // System.out.println("Recreated Method " + methodName + " for " + className);
  109.                 } catch (final NoSuchMethodException e) {
  110.                     throw new RuntimeException("Method " + methodName + " for " + className + " could not be reconstructed - method not found");
  111.                 }
  112.                 methodRef = new SoftReference<>(m);
  113.             }
  114.             return m;
  115.         }

  116.         /**
  117.          * Try to re-load the class
  118.          */
  119.         private Class<?> reLoadClass() {
  120.             return reLoadClass(className);
  121.         }

  122.         /**
  123.          * Try to re-load the class
  124.          */
  125.         private Class<?> reLoadClass(final String name) {

  126.             ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

  127.             // Try the context class loader
  128.             if (classLoader != null) {
  129.                 try {
  130.                     return classLoader.loadClass(name);
  131.                 } catch (final ClassNotFoundException e) {
  132.                     // ignore
  133.                 }
  134.             }

  135.             // Try this class's class loader
  136.             classLoader = MappedPropertyDescriptor.class.getClassLoader();
  137.             try {
  138.                 return classLoader.loadClass(name);
  139.             } catch (final ClassNotFoundException e) {
  140.                 return null;
  141.             }
  142.         }
  143.     }

  144.     /**
  145.      * The parameter types array for the reader method signature.
  146.      */
  147.     private static final Class<?>[] STRING_CLASS_PARAMETER = new Class[] { String.class };

  148.     /**
  149.      * Gets a capitalized version of the specified property name.
  150.      *
  151.      * @param s The property name
  152.      */
  153.     private static String capitalizePropertyName(final String s) {
  154.         if (s.isEmpty()) {
  155.             return s;
  156.         }

  157.         final char[] chars = s.toCharArray();
  158.         chars[0] = Character.toUpperCase(chars[0]);
  159.         return new String(chars);
  160.     }

  161.     /**
  162.      * Find a method on a class with a specified parameter list.
  163.      */
  164.     private static Method getMethod(final Class<?> clazz, final String methodName, final Class<?>[] parameterTypes) throws IntrospectionException {
  165.         if (methodName == null) {
  166.             return null;
  167.         }

  168.         final Method method = MethodUtils.getMatchingAccessibleMethod(clazz, methodName, parameterTypes);
  169.         if (method != null) {
  170.             return method;
  171.         }

  172.         final int parameterCount = parameterTypes == null ? 0 : parameterTypes.length;

  173.         // No Method found
  174.         throw new IntrospectionException("No method \"" + methodName + "\" with " + parameterCount + " parameter(s) of matching types.");
  175.     }

  176.     /**
  177.      * Find a method on a class with a specified number of parameters.
  178.      */
  179.     private static Method getMethod(final Class<?> clazz, final String methodName, final int parameterCount) throws IntrospectionException {
  180.         if (methodName == null) {
  181.             return null;
  182.         }

  183.         final Method method = internalGetMethod(clazz, methodName, parameterCount);
  184.         if (method != null) {
  185.             return method;
  186.         }

  187.         // No Method found
  188.         throw new IntrospectionException("No method \"" + methodName + "\" with " + parameterCount + " parameter(s)");
  189.     }

  190.     /**
  191.      * Find a method on a class with a specified number of parameters.
  192.      */
  193.     private static Method internalGetMethod(final Class<?> initial, final String methodName, final int parameterCount) {
  194.         // For overridden methods we need to find the most derived version.
  195.         // So we start with the given class and walk up the superclass chain.
  196.         for (Class<?> clazz = initial; clazz != null; clazz = clazz.getSuperclass()) {
  197.             final Method[] methods = clazz.getDeclaredMethods();
  198.             for (final Method method : methods) {
  199.                 if (method == null) {
  200.                     continue;
  201.                 }
  202.                 // skip static methods.
  203.                 final int mods = method.getModifiers();
  204.                 if (!Modifier.isPublic(mods) || Modifier.isStatic(mods)) {
  205.                     continue;
  206.                 }
  207.                 if (method.getName().equals(methodName) && method.getParameterTypes().length == parameterCount) {
  208.                     return method;
  209.                 }
  210.             }
  211.         }

  212.         // Now check any inherited interfaces. This is necessary both when
  213.         // the argument class is itself an interface, and when the argument
  214.         // class is an abstract class.
  215.         final Class<?>[] interfaces = initial.getInterfaces();
  216.         for (final Class<?> interface1 : interfaces) {
  217.             final Method method = internalGetMethod(interface1, methodName, parameterCount);
  218.             if (method != null) {
  219.                 return method;
  220.             }
  221.         }

  222.         return null;
  223.     }

  224.     /**
  225.      * The underlying data type of the property we are describing.
  226.      */
  227.     private Reference<Class<?>> mappedPropertyTypeRef;

  228.     /**
  229.      * The reader method for this property (if any).
  230.      */
  231.     private MappedMethodReference mappedReadMethodRef;

  232.     /**
  233.      * The writer method for this property (if any).
  234.      */
  235.     private MappedMethodReference mappedWriteMethodRef;

  236.     /**
  237.      * Constructs a MappedPropertyDescriptor for a property that follows the standard Java convention by having getFoo and setFoo accessor methods, with the
  238.      * addition of a String parameter (the key). Thus if the argument name is "fred", it will assume that the writer method is "setFred" and the reader method
  239.      * is "getFred". Note that the property name should start with a lower case character, which will be capitalized in the method names.
  240.      *
  241.      * @param propertyName The programmatic name of the property.
  242.      * @param beanClass    The Class object for the target bean. For example sun.beans.OurButton.class.
  243.      * @throws IntrospectionException if an exception occurs during introspection.
  244.      */
  245.     public MappedPropertyDescriptor(final String propertyName, final Class<?> beanClass) throws IntrospectionException {
  246.         super(propertyName, null, null);

  247.         if (propertyName == null || propertyName.isEmpty()) {
  248.             throw new IntrospectionException("bad property name: " + propertyName + " on class: " + beanClass.getClass().getName());
  249.         }

  250.         setName(propertyName);
  251.         final String base = capitalizePropertyName(propertyName);

  252.         // Look for mapped read method and matching write method
  253.         Method mappedReadMethod = null;
  254.         Method mappedWriteMethod = null;
  255.         try {
  256.             try {
  257.                 mappedReadMethod = getMethod(beanClass, "get" + base, STRING_CLASS_PARAMETER);
  258.             } catch (final IntrospectionException e) {
  259.                 mappedReadMethod = getMethod(beanClass, "is" + base, STRING_CLASS_PARAMETER);
  260.             }
  261.             final Class<?>[] params = { String.class, mappedReadMethod.getReturnType() };
  262.             mappedWriteMethod = getMethod(beanClass, "set" + base, params);
  263.         } catch (final IntrospectionException e) {
  264.             /*
  265.              * Swallow IntrospectionException TODO: Why?
  266.              */
  267.         }

  268.         // If there's no read method, then look for just a write method
  269.         if (mappedReadMethod == null) {
  270.             mappedWriteMethod = getMethod(beanClass, "set" + base, 2);
  271.         }

  272.         if (mappedReadMethod == null && mappedWriteMethod == null) {
  273.             throw new IntrospectionException("Property '" + propertyName + "' not found on " + beanClass.getName());
  274.         }
  275.         mappedReadMethodRef = new MappedMethodReference(mappedReadMethod);
  276.         mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod);

  277.         findMappedPropertyType();
  278.     }

  279.     /**
  280.      * This constructor takes the name of a mapped property, and method names for reading and writing the property.
  281.      *
  282.      * @param propertyName     The programmatic name of the property.
  283.      * @param beanClass        The Class object for the target bean. For example sun.beans.OurButton.class.
  284.      * @param mappedGetterName The name of the method used for reading one of the property values. May be null if the property is write-only.
  285.      * @param mappedSetterName The name of the method used for writing one of the property values. May be null if the property is read-only.
  286.      * @throws IntrospectionException if an exception occurs during introspection.
  287.      */
  288.     public MappedPropertyDescriptor(final String propertyName, final Class<?> beanClass, final String mappedGetterName, final String mappedSetterName)
  289.             throws IntrospectionException {
  290.         super(propertyName, null, null);

  291.         if (propertyName == null || propertyName.isEmpty()) {
  292.             throw new IntrospectionException("bad property name: " + propertyName);
  293.         }
  294.         setName(propertyName);

  295.         // search the mapped get and set methods
  296.         Method mappedReadMethod;
  297.         Method mappedWriteMethod = null;
  298.         mappedReadMethod = getMethod(beanClass, mappedGetterName, STRING_CLASS_PARAMETER);

  299.         if (mappedReadMethod != null) {
  300.             final Class<?>[] params = { String.class, mappedReadMethod.getReturnType() };
  301.             mappedWriteMethod = getMethod(beanClass, mappedSetterName, params);
  302.         } else {
  303.             mappedWriteMethod = getMethod(beanClass, mappedSetterName, 2);
  304.         }
  305.         mappedReadMethodRef = new MappedMethodReference(mappedReadMethod);
  306.         mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod);

  307.         findMappedPropertyType();
  308.     }

  309.     /**
  310.      * This constructor takes the name of a mapped property, and Method objects for reading and writing the property.
  311.      *
  312.      * @param propertyName The programmatic name of the property.
  313.      * @param mappedGetter The method used for reading one of the property values. May be null if the property is write-only.
  314.      * @param mappedSetter The method used for writing one the property values. May be null if the property is read-only.
  315.      * @throws IntrospectionException if an exception occurs during introspection.
  316.      */
  317.     public MappedPropertyDescriptor(final String propertyName, final Method mappedGetter, final Method mappedSetter) throws IntrospectionException {
  318.         super(propertyName, mappedGetter, mappedSetter);

  319.         if (propertyName == null || propertyName.isEmpty()) {
  320.             throw new IntrospectionException("bad property name: " + propertyName);
  321.         }

  322.         setName(propertyName);
  323.         mappedReadMethodRef = new MappedMethodReference(mappedGetter);
  324.         mappedWriteMethodRef = new MappedMethodReference(mappedSetter);
  325.         findMappedPropertyType();
  326.     }

  327.     /**
  328.      * Introspect our bean class to identify the corresponding getter and setter methods.
  329.      */
  330.     private void findMappedPropertyType() throws IntrospectionException {
  331.         final Method mappedReadMethod = getMappedReadMethod();
  332.         final Method mappedWriteMethod = getMappedWriteMethod();
  333.         Class<?> mappedPropertyType = null;
  334.         if (mappedReadMethod != null) {
  335.             if (mappedReadMethod.getParameterTypes().length != 1) {
  336.                 throw new IntrospectionException("bad mapped read method arg count");
  337.             }
  338.             mappedPropertyType = mappedReadMethod.getReturnType();
  339.             if (mappedPropertyType == Void.TYPE) {
  340.                 throw new IntrospectionException("mapped read method " + mappedReadMethod.getName() + " returns void");
  341.             }
  342.         }

  343.         if (mappedWriteMethod != null) {
  344.             final Class<?>[] params = mappedWriteMethod.getParameterTypes();
  345.             if (params.length != 2) {
  346.                 throw new IntrospectionException("bad mapped write method arg count");
  347.             }
  348.             if (mappedPropertyType != null && mappedPropertyType != params[1]) {
  349.                 throw new IntrospectionException("type mismatch between mapped read and write methods");
  350.             }
  351.             mappedPropertyType = params[1];
  352.         }
  353.         mappedPropertyTypeRef = new SoftReference<>(mappedPropertyType);
  354.     }

  355.     /**
  356.      * Gets the Class object for the property values.
  357.      *
  358.      * @return The Java type info for the property values. Note that the "Class" object may describe a built-in Java type such as "int". The result may be
  359.      *         "null" if this is a mapped property that does not support non-keyed access.
  360.      *         <p>
  361.      *         This is the type that will be returned by the mappedReadMethod.
  362.      */
  363.     public Class<?> getMappedPropertyType() {
  364.         return mappedPropertyTypeRef.get();
  365.     }

  366.     /**
  367.      * Gets the method that should be used to read one of the property value.
  368.      *
  369.      * @return The method that should be used to read the property value. May return null if the property can't be read.
  370.      */
  371.     public Method getMappedReadMethod() {
  372.         return mappedReadMethodRef.get();
  373.     }

  374.     /**
  375.      * Gets the method that should be used to write one of the property value.
  376.      *
  377.      * @return The method that should be used to write one of the property value. May return null if the property can't be written.
  378.      */
  379.     public Method getMappedWriteMethod() {
  380.         return mappedWriteMethodRef.get();
  381.     }

  382.     /**
  383.      * Sets the method that should be used to read one of the property value.
  384.      *
  385.      * @param mappedGetter The mapped getter method.
  386.      * @throws IntrospectionException If an error occurs finding the mapped property
  387.      */
  388.     public void setMappedReadMethod(final Method mappedGetter) throws IntrospectionException {
  389.         mappedReadMethodRef = new MappedMethodReference(mappedGetter);
  390.         findMappedPropertyType();
  391.     }

  392.     /**
  393.      * Sets the method that should be used to write the property value.
  394.      *
  395.      * @param mappedSetter The mapped setter method.
  396.      * @throws IntrospectionException If an error occurs finding the mapped property
  397.      */
  398.     public void setMappedWriteMethod(final Method mappedSetter) throws IntrospectionException {
  399.         mappedWriteMethodRef = new MappedMethodReference(mappedSetter);
  400.         findMappedPropertyType();
  401.     }
  402. }