| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * https://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.beanutils2; |
| |
| import java.beans.IntrospectionException; |
| import java.beans.Introspector; |
| import java.beans.PropertyDescriptor; |
| import java.lang.reflect.Method; |
| import java.util.Objects; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| /** |
| * <p> |
| * An implementation of the {@code BeanIntrospector} interface which can detect write methods for properties used in fluent API scenario. |
| * </p> |
| * <p> |
| * 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 |
| * 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 |
| * look as follows: |
| * </p> |
| * |
| * <pre> |
| * public class FooBuilder { |
| * public FooBuilder setFooProperty1(String value) { |
| * ... |
| * return this; |
| * } |
| * |
| * public FooBuilder setFooProperty2(int value) { |
| * ... |
| * return this; |
| * } |
| * } |
| * </pre> |
| * |
| * <p> |
| * Per default, {@code PropertyUtils} does not detect methods like this because, having a non-<strong>void</strong> return type, they violate the Java Beans |
| * specification. |
| * </p> |
| * <p> |
| * 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 |
| * configurable prefix (the default prefix is {@code set}). It then generates corresponding {@code PropertyDescriptor} objects for the methods found which use |
| * these methods as write methods. |
| * </p> |
| * <p> |
| * An instance of this class is intended to collaborate with a {@link DefaultBeanIntrospector} object. So best results are achieved by adding this instance as |
| * custom {@code BeanIntrospector} after the {@code DefaultBeanIntrospector} object. Then default introspection finds read-only properties because it does not |
| * detect the write methods with a non-<strong>void</strong> return type. {@code FluentPropertyBeanIntrospector} completes the descriptors for these properties |
| * by setting the correct write method. |
| * </p> |
| * |
| * @since 1.9 |
| */ |
| public class FluentPropertyBeanIntrospector implements BeanIntrospector { |
| |
| /** The default prefix for write methods. */ |
| public static final String DEFAULT_WRITE_METHOD_PREFIX = "set"; |
| |
| /** The logger. */ |
| private final Log log = LogFactory.getLog(getClass()); |
| |
| /** The prefix of write methods to search for. */ |
| private final String writeMethodPrefix; |
| |
| /** |
| * |
| * Creates a new instance of {@code FluentPropertyBeanIntrospector} and sets the default prefix for write methods. |
| */ |
| public FluentPropertyBeanIntrospector() { |
| this(DEFAULT_WRITE_METHOD_PREFIX); |
| } |
| |
| /** |
| * |
| * Creates a new instance of {@code FluentPropertyBeanIntrospector} and initializes it with the prefix for write methods used by the classes to be |
| * inspected. |
| * |
| * @param writePrefix The prefix for write methods (must not be {@code null}) |
| * @throws IllegalArgumentException if the prefix is {@code null} |
| */ |
| public FluentPropertyBeanIntrospector(final String writePrefix) { |
| writeMethodPrefix = Objects.requireNonNull(writePrefix, "writePrefix"); |
| } |
| |
| /** |
| * Creates a property descriptor for a fluent API property. |
| * |
| * @param m The set method for the fluent API property |
| * @param propertyName The name of the corresponding property |
| * @return The descriptor |
| * @throws IntrospectionException if an error occurs |
| */ |
| private PropertyDescriptor createFluentPropertyDescritor(final Method m, final String propertyName) throws IntrospectionException { |
| return new PropertyDescriptor(propertyName(m), null, m); |
| } |
| |
| /** |
| * Returns the prefix for write methods this instance scans for. |
| * |
| * @return The prefix for write methods |
| */ |
| public String getWriteMethodPrefix() { |
| return writeMethodPrefix; |
| } |
| |
| /** |
| * Performs introspection. This method scans the current class's methods for property write methods which have not been discovered by default introspection. |
| * |
| * @param icontext The introspection context |
| * @throws IntrospectionException if an error occurs |
| */ |
| @Override |
| public void introspect(final IntrospectionContext icontext) throws IntrospectionException { |
| for (final Method m : icontext.getTargetClass().getMethods()) { |
| if (m.getName().startsWith(getWriteMethodPrefix())) { |
| final String propertyName = propertyName(m); |
| final PropertyDescriptor pd = icontext.getPropertyDescriptor(propertyName); |
| try { |
| if (pd == null) { |
| icontext.addPropertyDescriptor(createFluentPropertyDescritor(m, propertyName)); |
| } else if (pd.getWriteMethod() == null) { |
| // We should not change statically cached PropertyDescriptor as it can be from super-type, |
| // it may affect other subclasses of targetClass supertype. |
| // See BEANUTILS-541 for more details. |
| final PropertyDescriptor fluentPropertyDescriptor = new PropertyDescriptor(pd.getName(), pd.getReadMethod(), m); |
| // replace existing (possibly inherited from super-class) to one specific to current class |
| icontext.addPropertyDescriptor(fluentPropertyDescriptor); |
| } |
| } catch (final IntrospectionException e) { |
| if (log.isDebugEnabled()) { |
| log.debug("Error when creating PropertyDescriptor for " + m + "! Ignoring this property.", e); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Derives the name of a property from the given set method. |
| * |
| * @param m The method |
| * @return The corresponding property name |
| */ |
| private String propertyName(final Method m) { |
| final String methodName = m.getName().substring(getWriteMethodPrefix().length()); |
| return Introspector.decapitalize(methodName); |
| } |
| } |