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
19 import java.beans.IntrospectionException;
20 import java.beans.Introspector;
21 import java.beans.PropertyDescriptor;
22 import java.lang.reflect.Method;
23 import java.util.Locale;
24 import java.util.Objects;
25
26 import org.apache.commons.logging.Log;
27 import org.apache.commons.logging.LogFactory;
28
29 /**
30 * <p>
31 * An implementation of the {@code BeanIntrospector} interface which can detect write methods for properties used in fluent API scenario.
32 * </p>
33 * <p>
34 * 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
35 * 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
36 * look as follows:
37 * </p>
38 *
39 * <pre>
40 * public class FooBuilder {
41 * public FooBuilder setFooProperty1(String value) {
42 * ...
43 * return this;
44 * }
45 *
46 * public FooBuilder setFooProperty2(int value) {
47 * ...
48 * return this;
49 * }
50 * }
51 * </pre>
52 *
53 * <p>
54 * Per default, {@code PropertyUtils} does not detect methods like this because, having a non-<strong>void</strong> return type, they violate the Java Beans
55 * specification.
56 * </p>
57 * <p>
58 * 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
59 * configurable prefix (the default prefix is {@code set}). It then generates corresponding {@code PropertyDescriptor} objects for the methods found which use
60 * these methods as write methods.
61 * </p>
62 * <p>
63 * An instance of this class is intended to collaborate with a {@link DefaultBeanIntrospector} object. So best results are achieved by adding this instance as
64 * custom {@code BeanIntrospector} after the {@code DefaultBeanIntrospector} object. Then default introspection finds read-only properties because it does not
65 * detect the write methods with a non-<strong>void</strong> return type. {@code FluentPropertyBeanIntrospector} completes the descriptors for these properties
66 * by setting the correct write method.
67 * </p>
68 *
69 * @since 1.9
70 */
71 public class FluentPropertyBeanIntrospector implements BeanIntrospector {
72 /** The default prefix for write methods. */
73 public static final String DEFAULT_WRITE_METHOD_PREFIX = "set";
74
75 /** The logger. */
76 private final Log log = LogFactory.getLog(getClass());
77
78 /** The prefix of write methods to search for. */
79 private final String writeMethodPrefix;
80
81 /**
82 *
83 * Creates a new instance of {@code FluentPropertyBeanIntrospector} and sets the default prefix for write methods.
84 */
85 public FluentPropertyBeanIntrospector() {
86 this(DEFAULT_WRITE_METHOD_PREFIX);
87 }
88
89 /**
90 *
91 * Creates a new instance of {@code FluentPropertyBeanIntrospector} and initializes it with the prefix for write methods used by the classes to be
92 * inspected.
93 *
94 * @param writePrefix the prefix for write methods (must not be <strong>null</strong>)
95 * @throws IllegalArgumentException if the prefix is <strong>null</strong>
96 */
97 public FluentPropertyBeanIntrospector(final String writePrefix) {
98 writeMethodPrefix = Objects.requireNonNull(writePrefix, "writePrefix");
99 }
100
101 /**
102 * Creates a property descriptor for a fluent API property.
103 *
104 * @param m the set method for the fluent API property
105 * @param propertyName the name of the corresponding property
106 * @return the descriptor
107 * @throws IntrospectionException if an error occurs
108 */
109 private PropertyDescriptor createFluentPropertyDescritor(final Method m, final String propertyName) throws IntrospectionException {
110 return new PropertyDescriptor(propertyName(m), null, m);
111 }
112
113 /**
114 * Returns the prefix for write methods this instance scans for.
115 *
116 * @return the prefix for write methods
117 */
118 public String getWriteMethodPrefix() {
119 return writeMethodPrefix;
120 }
121
122 /**
123 * Performs introspection. This method scans the current class's methods for property write methods which have not been discovered by default introspection.
124 *
125 * @param icontext the introspection context
126 * @throws IntrospectionException if an error occurs
127 */
128 @Override
129 public void introspect(final IntrospectionContext icontext) throws IntrospectionException {
130 for (final Method m : icontext.getTargetClass().getMethods()) {
131 if (m.getName().startsWith(getWriteMethodPrefix())) {
132 final String propertyName = propertyName(m);
133 final PropertyDescriptor pd = icontext.getPropertyDescriptor(propertyName);
134 try {
135 if (pd == null) {
136 icontext.addPropertyDescriptor(createFluentPropertyDescritor(m, propertyName));
137 } else if (pd.getWriteMethod() == null) {
138 // We should not change statically cached PropertyDescriptor as it can be from super-type,
139 // it may affect other subclasses of targetClass supertype.
140 // See BEANUTILS-541 for more details.
141 final PropertyDescriptor fluentPropertyDescriptor = new PropertyDescriptor(pd.getName(), pd.getReadMethod(), m);
142 // replace existing (possibly inherited from super-class) to one specific to current class
143 icontext.addPropertyDescriptor(fluentPropertyDescriptor);
144 }
145 } catch (final IntrospectionException e) {
146 if (log.isDebugEnabled()) {
147 log.debug("Error when creating PropertyDescriptor for " + m + "! Ignoring this property.", e);
148 }
149 }
150 }
151 }
152 }
153
154 /**
155 * Derives the name of a property from the given set method.
156 *
157 * @param m the method
158 * @return the corresponding property name
159 */
160 private String propertyName(final Method m) {
161 final String methodName = m.getName().substring(getWriteMethodPrefix().length());
162 return methodName.length() > 1 ? Introspector.decapitalize(methodName) : methodName.toLowerCase(Locale.ROOT);
163 }
164 }