001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.beanutils; 018 019import java.beans.IntrospectionException; 020import java.beans.Introspector; 021import java.beans.PropertyDescriptor; 022import java.lang.reflect.Method; 023import java.util.Locale; 024 025import org.apache.commons.logging.Log; 026import org.apache.commons.logging.LogFactory; 027 028/** 029 * <p> 030 * An implementation of the <code>BeanIntrospector</code> interface which can 031 * detect write methods for properties used in fluent API scenario. 032 * </p> 033 * <p> 034 * A <em>fluent API</em> allows setting multiple properties using a single 035 * statement by supporting so-called <em>method chaining</em>: Methods for 036 * setting a property value do not return <strong>void</strong>, but an object which can 037 * be called for setting another property. An example of such a fluent API could 038 * look as follows: 039 * </p> 040 * <pre> 041 * public class FooBuilder { 042 * public FooBuilder setFooProperty1(String value) { 043 * ... 044 * return this; 045 * } 046 * 047 * public FooBuilder setFooProperty2(int value) { 048 * ... 049 * return this; 050 * } 051 * } 052 * </pre> 053 * <p> 054 * Per default, <code>PropertyUtils</code> does not detect methods like this 055 * because, having a non-<strong>void</strong> return type, they violate the Java Beans 056 * specification. 057 * </p> 058 * <p> 059 * This class is more tolerant with regards to the return type of a set method. 060 * It basically iterates over all methods of a class and filters them for a 061 * configurable prefix (the default prefix is <code>set</code>). It then 062 * generates corresponding <code>PropertyDescriptor</code> objects for the 063 * methods found which use these methods as write methods. 064 * </p> 065 * <p> 066 * An instance of this class is intended to collaborate with a 067 * {@link DefaultBeanIntrospector} object. So best results are achieved by 068 * adding this instance as custom {@code BeanIntrospector} after the 069 * <code>DefaultBeanIntrospector</code> object. Then default introspection finds 070 * read-only properties because it does not detect the write methods with a 071 * non-<strong>void</strong> return type. {@code FluentPropertyBeanIntrospector} 072 * completes the descriptors for these properties by setting the correct write 073 * method. 074 * </p> 075 * 076 * @since 1.9 077 */ 078public class FluentPropertyBeanIntrospector implements BeanIntrospector { 079 080 /** The default prefix for write methods. */ 081 public static final String DEFAULT_WRITE_METHOD_PREFIX = "set"; 082 083 /** The logger. */ 084 private final Log log = LogFactory.getLog(getClass()); 085 086 /** The prefix of write methods to search for. */ 087 private final String writeMethodPrefix; 088 089 /** 090 * 091 * Creates a new instance of <code>FluentPropertyBeanIntrospector</code> and 092 * sets the default prefix for write methods. 093 */ 094 public FluentPropertyBeanIntrospector() { 095 this(DEFAULT_WRITE_METHOD_PREFIX); 096 } 097 098 /** 099 * 100 * Creates a new instance of <code>FluentPropertyBeanIntrospector</code> and 101 * initializes it with the prefix for write methods used by the classes to 102 * be inspected. 103 * 104 * @param writePrefix the prefix for write methods (must not be <strong>null</strong>) 105 * @throws IllegalArgumentException if the prefix is <strong>null</strong> 106 */ 107 public FluentPropertyBeanIntrospector(final String writePrefix) { 108 if (writePrefix == null) { 109 throw new IllegalArgumentException( 110 "Prefix for write methods must not be null!"); 111 } 112 writeMethodPrefix = writePrefix; 113 } 114 115 /** 116 * Creates a property descriptor for a fluent API property. 117 * 118 * @param m the set method for the fluent API property 119 * @param propertyName the name of the corresponding property 120 * @return the descriptor 121 * @throws IntrospectionException if an error occurs 122 */ 123 private PropertyDescriptor createFluentPropertyDescritor(final Method m, 124 final String propertyName) throws IntrospectionException { 125 return new PropertyDescriptor(propertyName(m), null, m); 126 } 127 128 /** 129 * Returns the prefix for write methods this instance scans for. 130 * 131 * @return the prefix for write methods 132 */ 133 public String getWriteMethodPrefix() { 134 return writeMethodPrefix; 135 } 136 137 /** 138 * Performs introspection. This method scans the current class's methods for 139 * property write methods which have not been discovered by default 140 * introspection. 141 * 142 * @param icontext the introspection context 143 * @throws IntrospectionException if an error occurs 144 */ 145 @Override 146 public void introspect(final IntrospectionContext icontext) 147 throws IntrospectionException { 148 for (final Method m : icontext.getTargetClass().getMethods()) { 149 if (m.getName().startsWith(getWriteMethodPrefix())) { 150 final String propertyName = propertyName(m); 151 final PropertyDescriptor pd = icontext 152 .getPropertyDescriptor(propertyName); 153 try { 154 if (pd == null) { 155 icontext.addPropertyDescriptor(createFluentPropertyDescritor( 156 m, propertyName)); 157 } else if (pd.getWriteMethod() == null) { 158 // We should not change statically cached PropertyDescriptor as it can be from super-type, 159 // it may affect other subclasses of targetClass supertype. 160 // See BEANUTILS-541 for more details. 161 final PropertyDescriptor fluentPropertyDescriptor = new PropertyDescriptor( 162 pd.getName(), pd.getReadMethod(), m); 163 // replace existing (possibly inherited from super-class) to one specific to current class 164 icontext.addPropertyDescriptor(fluentPropertyDescriptor); 165 } 166 } catch (final IntrospectionException e) { 167 log.info("Error when creating PropertyDescriptor for " + m 168 + "! Ignoring this property."); 169 log.debug("Exception is:", e); 170 } 171 } 172 } 173 } 174 175 /** 176 * Derives the name of a property from the given set method. 177 * 178 * @param m the method 179 * @return the corresponding property name 180 */ 181 private String propertyName(final Method m) { 182 final String methodName = m.getName().substring( 183 getWriteMethodPrefix().length()); 184 return methodName.length() > 1 ? Introspector.decapitalize(methodName) : methodName 185 .toLowerCase(Locale.ROOT); 186 } 187}