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 private static void clearDescriptorsCacheHierarchy(final Class<?> cls) { 084 if (cls != null && cls != Object.class) { 085 Introspector.flushFromCaches(cls); 086 clearDescriptorsCacheHierarchy(cls.getSuperclass()); 087 } 088 } 089 090 /** The logger. */ 091 private final Log log = LogFactory.getLog(getClass()); 092 093 /** The prefix of write methods to search for. */ 094 private final String writeMethodPrefix; 095 096 /** 097 * 098 * Creates a new instance of <code>FluentPropertyBeanIntrospector</code> and 099 * sets the default prefix for write methods. 100 */ 101 public FluentPropertyBeanIntrospector() { 102 this(DEFAULT_WRITE_METHOD_PREFIX); 103 } 104 105 /** 106 * 107 * Creates a new instance of <code>FluentPropertyBeanIntrospector</code> and 108 * initializes it with the prefix for write methods used by the classes to 109 * be inspected. 110 * 111 * @param writePrefix the prefix for write methods (must not be <strong>null</strong>) 112 * @throws IllegalArgumentException if the prefix is <strong>null</strong> 113 */ 114 public FluentPropertyBeanIntrospector(final String writePrefix) { 115 if (writePrefix == null) { 116 throw new IllegalArgumentException( 117 "Prefix for write methods must not be null!"); 118 } 119 writeMethodPrefix = writePrefix; 120 } 121 122 /** 123 * Creates a property descriptor for a fluent API property. 124 * 125 * @param m the set method for the fluent API property 126 * @param propertyName the name of the corresponding property 127 * @return the descriptor 128 * @throws IntrospectionException if an error occurs 129 */ 130 private PropertyDescriptor createFluentPropertyDescritor(final Method m, 131 final String propertyName) throws IntrospectionException { 132 return new PropertyDescriptor(propertyName(m), null, m); 133 } 134 135 /** 136 * Returns the prefix for write methods this instance scans for. 137 * 138 * @return the prefix for write methods 139 */ 140 public String getWriteMethodPrefix() { 141 return writeMethodPrefix; 142 } 143 144 /** 145 * Performs introspection. This method scans the current class's methods for 146 * property write methods which have not been discovered by default 147 * introspection. 148 * 149 * @param icontext the introspection context 150 * @throws IntrospectionException if an error occurs 151 */ 152 @Override 153 public void introspect(final IntrospectionContext icontext) 154 throws IntrospectionException { 155 for (final Method m : icontext.getTargetClass().getMethods()) { 156 if (m.getName().startsWith(getWriteMethodPrefix())) { 157 final String propertyName = propertyName(m); 158 final PropertyDescriptor pd = icontext 159 .getPropertyDescriptor(propertyName); 160 try { 161 if (pd == null) { 162 icontext.addPropertyDescriptor(createFluentPropertyDescritor( 163 m, propertyName)); 164 } else if (pd.getWriteMethod() == null) { 165 // We change statically cached PropertyDescriptor, it may affect 166 // other subclasses of targetClass supertype. 167 // See BEANUTILS-541 for more details. 168 clearDescriptorsCacheHierarchy(icontext.getTargetClass().getSuperclass()); 169 pd.setWriteMethod(m); 170 } 171 } catch (final IntrospectionException e) { 172 log.info("Error when creating PropertyDescriptor for " + m 173 + "! Ignoring this property."); 174 log.debug("Exception is:", e); 175 } 176 } 177 } 178 } 179 180 /** 181 * Derives the name of a property from the given set method. 182 * 183 * @param m the method 184 * @return the corresponding property name 185 */ 186 private String propertyName(final Method m) { 187 final String methodName = m.getName().substring( 188 getWriteMethodPrefix().length()); 189 return methodName.length() > 1 ? Introspector.decapitalize(methodName) : methodName 190 .toLowerCase(Locale.ROOT); 191 } 192}