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.  *      https://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. import java.util.Objects;

  26. /**
  27.  * A MappedPropertyDescriptor describes one mapped property. Mapped properties are multivalued properties like indexed properties but that are accessed with a
  28.  * 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
  29.  * getter and setter methods of the form
  30.  * <p>
  31.  * {@code get<strong>Property</strong>(String key)} and
  32.  * <p>
  33.  * {@code set<strong>Property</strong>(String key, Object value)},
  34.  * <p>
  35.  * where {@code <strong>Property</strong>} must be replaced by the name of the property.
  36.  *
  37.  * @see java.beans.PropertyDescriptor
  38.  */
  39. public class MappedPropertyDescriptor extends PropertyDescriptor {

  40.     /**
  41.      * 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
  42.      * method if the method reference has been released.
  43.      *
  44.      * See https://issues.apache.org/jira/browse/BEANUTILS-291
  45.      */
  46.     private static final class MappedMethodReference {
  47.         private String className;
  48.         private String methodName;
  49.         private Reference<Method> methodRef;
  50.         private Reference<Class<?>> classRef;
  51.         private Reference<Class<?>> writeParamTypeRef0;
  52.         private Reference<Class<?>> writeParamTypeRef1;
  53.         private String[] writeParamClassNames;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  221.         return null;
  222.     }

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

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

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

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

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

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

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

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

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

  276.         findMappedPropertyType();
  277.     }

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

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

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

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

  306.         findMappedPropertyType();
  307.     }

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

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

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

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

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

  354.     /**
  355.      * Gets the Class object for the property values.
  356.      *
  357.      * @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
  358.      *         "null" if this is a mapped property that does not support non-keyed access.
  359.      *         <p>
  360.      *         This is the type that will be returned by the mappedReadMethod.
  361.      */
  362.     public Class<?> getMappedPropertyType() {
  363.         return mappedPropertyTypeRef.get();
  364.     }

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

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

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

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