1   /*
2    * IntrospectionPerformanceTest - Tests introspection on JavaBeans
3    * Copyright (C) 2007 Christian Schenk
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    * 
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   * 
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
18   */
19  package org.christianschenk.beanintrospect;
20  
21  import java.beans.BeanInfo;
22  import java.beans.IntrospectionException;
23  import java.beans.Introspector;
24  import java.beans.PropertyDescriptor;
25  import java.lang.reflect.Method;
26  import java.util.ArrayList;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  
31  import org.apache.log4j.Logger;
32  
33  /**
34   * This class uses introspection to gather information about JavaBeans and can then be used for
35   * prototyping, i.e. copying properties from one bean to another.
36   * 
37   * @author Christian Schenk
38   */
39  public class BeanIntrospector {
40  
41  	private static final Logger log = Logger.getLogger(BeanIntrospector.class);
42  	/**
43  	 * This is a lookup table that lists classes with their property names and the corresponding
44  	 * getter/setter methods.<br/> This might look like:
45  	 * 
46  	 * <pre>
47  	 * + Foo.class
48  	 *   - bar
49  	 *     - getBar()
50  	 *     - setBar()
51  	 * + Tee.class
52  	 *   - tee
53  	 *     - getTee()
54  	 *     - setTee()
55  	 *   - ...
56  	 * + ...
57  	 * </pre>
58  	 */
59  	private final Map<Class<?>, Map<String, List<Method>>> methods;
60  	/**
61  	 * Getter/Setter methods for a property of a class are stored in a list and this index
62  	 * represents the position of getter methods in that list.
63  	 */
64  	private final int GETTER_INDEX = 0;
65  	/**
66  	 * Getter/Setter methods for a property of a class are stored in a list and this index
67  	 * represents the position of setter methods in that list.
68  	 */
69  	private final int SETTER_INDEX = 1;
70  	/** Certain methods of a class can be ignored when copying properties from one bean to another. */
71  	private final Map<Class<?>, List<String>> ignore;
72  	/** Holds semantically equivalent methods of different classes that were configured by the user. */
73  	private final Map<String, String> methodNameMapping;
74  
75  	public BeanIntrospector() {
76  		this.methods = new HashMap<Class<?>, Map<String, List<Method>>>();
77  		this.ignore = new HashMap<Class<?>, List<String>>();
78  		this.methodNameMapping = new HashMap<String, String>();
79  	}
80  
81  	/**
82  	 * Fills the bean with the given prototype bean.
83  	 */
84  	public void fill(final Object bean, final Object prototype) {
85  		try {
86  			// just for optimization
87  			final Class<?> beanClass = bean.getClass();
88  			final Class<?> prototypeClass = prototype.getClass();
89  
90  			this.registerBean(beanClass);
91  			this.registerBean(prototypeClass);
92  
93  			for (final String methodName : this.methods.get(beanClass).keySet()) {
94  				log.debug("Setting method: " + methodName);
95  				if (this.ignore.containsKey(beanClass) && this.ignore.get(beanClass).contains(methodName)) {
96  					log.debug("Ignoring: " + methodName);
97  					continue;
98  				}
99  				// retrieve the corresponding getter/setter methods and copy the property from the
100 				// prototype-bean to the other bean
101 				final List<Method> prototypeGetSetMethods = this.getGetSetMethodsForClass(prototypeClass, beanClass, methodName);
102 				final List<Method> beanGetSetMethods = this.getGetSetMethodsForClass(beanClass, null, methodName);
103 				final Method prototypeGetter = prototypeGetSetMethods.get(this.GETTER_INDEX);
104 				final Method beanSetter = beanGetSetMethods.get(this.SETTER_INDEX);
105 				// would be: bean.setX(prototype.getX())
106 				beanSetter.invoke(bean, new Object[] { prototypeGetter.invoke(prototype, (Object[]) null) });
107 			}
108 		} catch (final Exception ex) {
109 			throw new RuntimeException("Could not introspect object of class '" + bean.getClass().getName() + "'", ex);
110 		}
111 	}
112 
113 	/**
114 	 * Looks up the getter/setter methods for a class in the global map of methods. If it can't find
115 	 * these methods it'll try to look them up in the map with user supplied mappings between
116 	 * methods.
117 	 */
118 	private List<Method> getGetSetMethodsForClass(final Class<?> prototypeClass, final Class<?> beanClass, final String methodName) {
119 		List<Method> prototypeGetSetMethods = this.methods.get(prototypeClass).get(methodName);
120 		// if there are no getter/setter methods for the prototype bean we'll try to get a
121 		// compatible method that has been mapped by the user
122 		if (prototypeGetSetMethods == null) {
123 			final String beanClassCanonicalNameAndMethodName = beanClass.getCanonicalName() + methodName;
124 			if (this.methodNameMapping.containsKey(beanClassCanonicalNameAndMethodName)) {
125 				final String compatibleMethodName = this.methodNameMapping.get(beanClassCanonicalNameAndMethodName);
126 				log.debug("Using mapped method: " + compatibleMethodName);
127 				prototypeGetSetMethods = this.methods.get(prototypeClass).get(compatibleMethodName);
128 			} else {
129 				throw new RuntimeException("Could not set '" + methodName + "' because class '" + prototypeClass.getCanonicalName() + "' has no compatible property");
130 			}
131 		}
132 		return prototypeGetSetMethods;
133 	}
134 
135 	/**
136 	 * Puts the getter and setter methods of the given class into a map, so they can be looked up
137 	 * faster.
138 	 */
139 	private void registerBean(final Class<?> beanClass) throws IntrospectionException {
140 		if (this.methods.containsKey(beanClass)) return;
141 
142 		final Map<String, List<Method>> beanMethods = new HashMap<String, List<Method>>();
143 		final BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
144 		for (final PropertyDescriptor propDesc : beanInfo.getPropertyDescriptors()) {
145 			final Method getter = propDesc.getWriteMethod();
146 			final Method setter = propDesc.getReadMethod();
147 
148 			if (getter == null || setter == null) continue;
149 
150 			log.debug("Adding setter/getter for '" + propDesc.getName() + "' from " + beanClass.getCanonicalName());
151 			final List<Method> methods = new ArrayList<Method>(2);
152 			methods.add(setter);
153 			methods.add(getter);
154 			beanMethods.put(propDesc.getName(), methods);
155 		}
156 		this.methods.put(beanClass, beanMethods);
157 	}
158 
159 	/**
160 	 * Getter and setter methods of this class with the given name will be ignored.
161 	 */
162 	public void ignoreMethodName(final Class<?> clazz, final String methodName) {
163 		List<String> methods = this.ignore.get(clazz);
164 		if (methods == null) methods = new ArrayList<String>();
165 		methods.add(methodName.toLowerCase());
166 		this.ignore.put(clazz, methods);
167 	}
168 
169 	/**
170 	 * Maps a method from one class to a method of another class. Useful if the property names are
171 	 * different in two classes but semantically equivalent.
172 	 */
173 	public void addMethodNameMapping(final Class<?> fromClass, final String fromMethod, final Class<?> toClass, final String toMethod) {
174 		final String fromSignature = fromClass.getCanonicalName() + fromMethod.toLowerCase();
175 		final String toSignature = toClass.getCanonicalName() + toMethod.toLowerCase();
176 		this.methodNameMapping.put(fromSignature, toMethod.toLowerCase());
177 		this.methodNameMapping.put(toSignature, fromMethod.toLowerCase());
178 	}
179 }