FluentPropertyBeanIntrospector.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.Introspector;
  20. import java.beans.PropertyDescriptor;
  21. import java.lang.reflect.Method;
  22. import java.util.Locale;
  23. import java.util.Objects;

  24. import org.apache.commons.logging.Log;
  25. import org.apache.commons.logging.LogFactory;

  26. /**
  27.  * <p>
  28.  * An implementation of the {@code BeanIntrospector} interface which can detect write methods for properties used in fluent API scenario.
  29.  * </p>
  30.  * <p>
  31.  * A <em>fluent API</em> allows setting multiple properties using a single statement by supporting so-called <em>method chaining</em>: Methods for setting a
  32.  * property value do not return <strong>void</strong>, but an object which can be called for setting another property. An example of such a fluent API could
  33.  * look as follows:
  34.  * </p>
  35.  *
  36.  * <pre>
  37.  * public class FooBuilder {
  38.  *     public FooBuilder setFooProperty1(String value) {
  39.  *        ...
  40.  *        return this;
  41.  *    }
  42.  *
  43.  *     public FooBuilder setFooProperty2(int value) {
  44.  *        ...
  45.  *        return this;
  46.  *    }
  47.  * }
  48.  * </pre>
  49.  *
  50.  * <p>
  51.  * Per default, {@code PropertyUtils} does not detect methods like this because, having a non-<strong>void</strong> return type, they violate the Java Beans
  52.  * specification.
  53.  * </p>
  54.  * <p>
  55.  * This class is more tolerant with regards to the return type of a set method. It basically iterates over all methods of a class and filters them for a
  56.  * configurable prefix (the default prefix is {@code set}). It then generates corresponding {@code PropertyDescriptor} objects for the methods found which use
  57.  * these methods as write methods.
  58.  * </p>
  59.  * <p>
  60.  * An instance of this class is intended to collaborate with a {@link DefaultBeanIntrospector} object. So best results are achieved by adding this instance as
  61.  * custom {@code BeanIntrospector} after the {@code DefaultBeanIntrospector} object. Then default introspection finds read-only properties because it does not
  62.  * detect the write methods with a non-<strong>void</strong> return type. {@code FluentPropertyBeanIntrospector} completes the descriptors for these properties
  63.  * by setting the correct write method.
  64.  * </p>
  65.  *
  66.  * @since 1.9
  67.  */
  68. public class FluentPropertyBeanIntrospector implements BeanIntrospector {
  69.     /** The default prefix for write methods. */
  70.     public static final String DEFAULT_WRITE_METHOD_PREFIX = "set";

  71.     /** The logger. */
  72.     private final Log log = LogFactory.getLog(getClass());

  73.     /** The prefix of write methods to search for. */
  74.     private final String writeMethodPrefix;

  75.     /**
  76.      *
  77.      * Creates a new instance of {@code FluentPropertyBeanIntrospector} and sets the default prefix for write methods.
  78.      */
  79.     public FluentPropertyBeanIntrospector() {
  80.         this(DEFAULT_WRITE_METHOD_PREFIX);
  81.     }

  82.     /**
  83.      *
  84.      * Creates a new instance of {@code FluentPropertyBeanIntrospector} and initializes it with the prefix for write methods used by the classes to be
  85.      * inspected.
  86.      *
  87.      * @param writePrefix the prefix for write methods (must not be <strong>null</strong>)
  88.      * @throws IllegalArgumentException if the prefix is <strong>null</strong>
  89.      */
  90.     public FluentPropertyBeanIntrospector(final String writePrefix) {
  91.         writeMethodPrefix = Objects.requireNonNull(writePrefix, "writePrefix");
  92.     }

  93.     /**
  94.      * Creates a property descriptor for a fluent API property.
  95.      *
  96.      * @param m            the set method for the fluent API property
  97.      * @param propertyName the name of the corresponding property
  98.      * @return the descriptor
  99.      * @throws IntrospectionException if an error occurs
  100.      */
  101.     private PropertyDescriptor createFluentPropertyDescritor(final Method m, final String propertyName) throws IntrospectionException {
  102.         return new PropertyDescriptor(propertyName(m), null, m);
  103.     }

  104.     /**
  105.      * Returns the prefix for write methods this instance scans for.
  106.      *
  107.      * @return the prefix for write methods
  108.      */
  109.     public String getWriteMethodPrefix() {
  110.         return writeMethodPrefix;
  111.     }

  112.     /**
  113.      * Performs introspection. This method scans the current class's methods for property write methods which have not been discovered by default introspection.
  114.      *
  115.      * @param icontext the introspection context
  116.      * @throws IntrospectionException if an error occurs
  117.      */
  118.     @Override
  119.     public void introspect(final IntrospectionContext icontext) throws IntrospectionException {
  120.         for (final Method m : icontext.getTargetClass().getMethods()) {
  121.             if (m.getName().startsWith(getWriteMethodPrefix())) {
  122.                 final String propertyName = propertyName(m);
  123.                 final PropertyDescriptor pd = icontext.getPropertyDescriptor(propertyName);
  124.                 try {
  125.                     if (pd == null) {
  126.                         icontext.addPropertyDescriptor(createFluentPropertyDescritor(m, propertyName));
  127.                     } else if (pd.getWriteMethod() == null) {
  128.                         // We should not change statically cached PropertyDescriptor as it can be from super-type,
  129.                         // it may affect other subclasses of targetClass supertype.
  130.                         // See BEANUTILS-541 for more details.
  131.                         final PropertyDescriptor fluentPropertyDescriptor = new PropertyDescriptor(pd.getName(), pd.getReadMethod(), m);
  132.                         // replace existing (possibly inherited from super-class) to one specific to current class
  133.                         icontext.addPropertyDescriptor(fluentPropertyDescriptor);
  134.                     }
  135.                 } catch (final IntrospectionException e) {
  136.                     if (log.isDebugEnabled()) {
  137.                         log.debug("Error when creating PropertyDescriptor for " + m + "! Ignoring this property.", e);
  138.                     }
  139.                 }
  140.             }
  141.         }
  142.     }

  143.     /**
  144.      * Derives the name of a property from the given set method.
  145.      *
  146.      * @param m the method
  147.      * @return the corresponding property name
  148.      */
  149.     private String propertyName(final Method m) {
  150.         final String methodName = m.getName().substring(getWriteMethodPrefix().length());
  151.         return methodName.length() > 1 ? Introspector.decapitalize(methodName) : methodName.toLowerCase(Locale.ROOT);
  152.     }
  153. }