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.beanutils; 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 25 import org.apache.commons.logging.Log; 26 import org.apache.commons.logging.LogFactory; 27 28 /** 29 * <p> 30 * An implementation of the <code>BeanIntrospector</code> interface which can 31 * 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 35 * statement by supporting so-called <em>method chaining</em>: Methods for 36 * setting a property value do not return <b>void</b>, but an object which can 37 * be called for setting another property. An example of such a fluent API could 38 * look as follows: 39 * 40 * <pre> 41 * public class FooBuilder { 42 * public FooBuilder setFooProperty1(String value) { 43 * ... 44 * return this; 45 * } 46 * 47 * public FooBuilder setFooProperty2(int value) { 48 * ... 49 * return this; 50 * } 51 * } 52 * </pre> 53 * 54 * Per default, <code>PropertyUtils</code> does not detect methods like this 55 * because, having a non-<b>void</b> return type, they violate the Java Beans 56 * specification. 57 * </p> 58 * <p> 59 * This class is more tolerant with regards to the return type of a set method. 60 * It basically iterates over all methods of a class and filters them for a 61 * configurable prefix (the default prefix is <code>set</code>). It then 62 * generates corresponding <code>PropertyDescriptor</code> objects for the 63 * methods found which use these methods as write methods. 64 * </p> 65 * <p> 66 * An instance of this class is intended to collaborate with a 67 * {@link DefaultBeanIntrospector} object. So best results are achieved by 68 * adding this instance as custom {@code BeanIntrospector} after the 69 * <code>DefaultBeanIntrospector</code> object. Then default introspection finds 70 * read-only properties because it does not detect the write methods with a 71 * non-<b>void</b> return type. {@code FluentPropertyBeanIntrospector} 72 * completes the descriptors for these properties by setting the correct write 73 * method. 74 * </p> 75 * 76 * @version $Id$ 77 * @since 1.9 78 */ 79 public class FluentPropertyBeanIntrospector implements BeanIntrospector { 80 /** The default prefix for write methods. */ 81 public static final String DEFAULT_WRITE_METHOD_PREFIX = "set"; 82 83 /** The logger. */ 84 private final Log log = LogFactory.getLog(getClass()); 85 86 /** The prefix of write methods to search for. */ 87 private final String writeMethodPrefix; 88 89 /** 90 * 91 * Creates a new instance of <code>FluentPropertyBeanIntrospector</code> and 92 * initializes it with the prefix for write methods used by the classes to 93 * be inspected. 94 * 95 * @param writePrefix the prefix for write methods (must not be <b>null</b>) 96 * @throws IllegalArgumentException if the prefix is <b>null</b> 97 */ 98 public FluentPropertyBeanIntrospector(final String writePrefix) { 99 if (writePrefix == null) { 100 throw new IllegalArgumentException( 101 "Prefix for write methods must not be null!"); 102 } 103 writeMethodPrefix = writePrefix; 104 } 105 106 /** 107 * 108 * Creates a new instance of <code>FluentPropertyBeanIntrospector</code> and 109 * sets the default prefix for write methods. 110 */ 111 public FluentPropertyBeanIntrospector() { 112 this(DEFAULT_WRITE_METHOD_PREFIX); 113 } 114 115 /** 116 * Returns the prefix for write methods this instance scans for. 117 * 118 * @return the prefix for write methods 119 */ 120 public String getWriteMethodPrefix() { 121 return writeMethodPrefix; 122 } 123 124 /** 125 * Performs introspection. This method scans the current class's methods for 126 * property write methods which have not been discovered by default 127 * introspection. 128 * 129 * @param icontext the introspection context 130 * @throws IntrospectionException if an error occurs 131 */ 132 public void introspect(final IntrospectionContext icontext) 133 throws IntrospectionException { 134 for (final Method m : icontext.getTargetClass().getMethods()) { 135 if (m.getName().startsWith(getWriteMethodPrefix())) { 136 final String propertyName = propertyName(m); 137 final PropertyDescriptor pd = icontext 138 .getPropertyDescriptor(propertyName); 139 try { 140 if (pd == null) { 141 icontext.addPropertyDescriptor(createFluentPropertyDescritor( 142 m, propertyName)); 143 } else if (pd.getWriteMethod() == null) { 144 pd.setWriteMethod(m); 145 } 146 } catch (final IntrospectionException e) { 147 log.info("Error when creating PropertyDescriptor for " + m 148 + "! Ignoring this property."); 149 log.debug("Exception is:", e); 150 } 151 } 152 } 153 } 154 155 /** 156 * Derives the name of a property from the given set method. 157 * 158 * @param m the method 159 * @return the corresponding property name 160 */ 161 private String propertyName(final Method m) { 162 final String methodName = m.getName().substring( 163 getWriteMethodPrefix().length()); 164 return (methodName.length() > 1) ? Introspector.decapitalize(methodName) : methodName 165 .toLowerCase(Locale.ENGLISH); 166 } 167 168 /** 169 * Creates a property descriptor for a fluent API property. 170 * 171 * @param m the set method for the fluent API property 172 * @param propertyName the name of the corresponding property 173 * @return the descriptor 174 * @throws IntrospectionException if an error occurs 175 */ 176 private PropertyDescriptor createFluentPropertyDescritor(final Method m, 177 final String propertyName) throws IntrospectionException { 178 return new PropertyDescriptor(propertyName(m), null, m); 179 } 180 }