001 /* ====================================================================
002 * The Apache Software License, Version 1.1
003 *
004 * Copyright (c) 2003 The Apache Software Foundation. All rights
005 * reserved.
006 *
007 * Redistribution and use in source and binary forms, with or without
008 * modification, are permitted provided that the following conditions
009 * are met:
010 *
011 * 1. Redistributions of source code must retain the above copyright
012 * notice, this list of conditions and the following disclaimer.
013 *
014 * 2. Redistributions in binary form must reproduce the above copyright
015 * notice, this list of conditions and the following disclaimer in
016 * the documentation and/or other materials provided with the
017 * distribution.
018 *
019 * 3. The end-user documentation included with the redistribution,
020 * if any, must include the following acknowledgment:
021 * "This product includes software developed by the
022 * Apache Software Foundation (http://www.apache.org/)."
023 * Alternately, this acknowledgment may appear in the software itself,
024 * if and wherever such third-party acknowledgments normally appear.
025 *
026 * 4. The names "The Jakarta Project", "Commons", and "Apache Software
027 * Foundation" must not be used to endorse or promote products derived
028 * from this software without prior written permission. For written
029 * permission, please contact apache@apache.org.
030 *
031 * 5. Products derived from this software may not be called "Apache",
032 * nor may "Apache" appear in their name, without prior written
033 * permission of the Apache Software Foundation.
034 *
035 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
036 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
037 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
038 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
039 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
040 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
041 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
042 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
043 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
044 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
045 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
046 * SUCH DAMAGE.
047 * ====================================================================
048 *
049 * This software consists of voluntary contributions made by many
050 * individuals on behalf of the Apache Software Foundation. For more
051 * information on the Apache Software Foundation, please see
052 * <http://www.apache.org/>.
053 *
054 */
055 package org.jpu.patterns.common;
056
057 import java.lang.reflect.Method;
058 import java.util.ArrayList;
059 import java.util.Arrays;
060 import java.util.HashMap;
061 import java.util.Iterator;
062 import java.util.List;
063 import java.util.ListIterator;
064 import java.util.Map;
065 import java.util.WeakHashMap;
066
067 import org.apache.commons.collections.IteratorUtils;
068 import org.apache.commons.collections.iterators.IteratorChain;
069 import org.apache.commons.lang.ClassUtils;
070 import org.apache.commons.lang.exception.NestableRuntimeException;
071 import org.apache.commons.logging.Log;
072 import org.apache.commons.logging.LogFactory;
073
074 /**
075 * Public static methods for manipulating beans. Unless otherwise
076 * indicated, these methods are all standalone and usable with our without any of the other
077 * classes provided by the {@link org.jpu.patterns.proxy} package or any other portions
078 * of the JPatternUtils library. None of these methods requires that any of the objects passed
079 * in be backed by {@link org.jpu.patterns.proxy.ProxyBean}.
080 * <p>
081 * Some of the methods of this class perform similar functions to
082 * <a href="http://jakarta.apache.org/commons/beanutils/api/org/apache/commons/beanutils/PropertyUtils.html">org.apache.commons.beanutils.PropertyUtils</a>,
083 * and you may use <code>PropertyUtils</code> to manipulate objects backed by <code>ProxyBean</code>.
084 * However, the methods here are optimized to give the best performance for the operations
085 * needed by {@link org.jpu.patterns.proxy.ProxyBean}, and provide a finer level of control.
086 */
087 public class JPUBeanUtils {
088
089 public static class MethodDescriptor implements Cloneable {
090 public static final int GETTER = 0;
091 public static final int SETTER = 1;
092 public static final int OTHER = 2;
093
094 public Object clone() {
095 try {
096 return super.clone();
097 }
098 catch( CloneNotSupportedException e ) {
099 throw new NestableRuntimeException(e);
100 }
101 }
102
103 public String attributeName = "";
104 public int type = OTHER;
105 public boolean isBooleanGetter = false;
106 }
107
108 /**
109 * Convenience alias for "<code>copy(from, to, null)</code>".
110 */
111 public static Map copy(
112 Object from,
113 Object to ) throws CopyException {
114 return copy(from, to, null);
115 }
116
117 /**
118 * Copies specified attributes from "<code>from</code>" to "<code>to</code>"
119 * giving the caller very precise control, via the "<code>options</code>" parameter,
120 * over how the copying is to take place. Copying is done via accessible
121 * getters of "<code>from</code>" and compatible accessible setters of
122 * "<code>to</code>".
123 * <p>
124 * This class <i>does not</i> perform any kind of attribute conversion.
125 * You can use
126 * <a href="http://jakarta.apache.org/commons/beanutils/api/org/apache/commons/beanutils/BeanUtils.html">org.apache.commons.beanutils.BeanUtils</a>,
127 * for this type of copying.
128 * <p>
129 * For purposes of this method, a getter is a method whose name starts with
130 * "get", takes no arguments, and returns non-void; or starts with the word
131 * "is", takes no arguents, and returns a <code>Boolean</code> or <code>boolean</code>.
132 * A setter is a method whose name starts with "set" and takes one argument.
133 * <p>
134 * Options affecting this method's behavior are described below:
135 * <ul>
136 * <li>
137 * <code>options.ignoreNulls</code>. If the getter returns <code>null</code>, don't call
138 * the corresponding setter.
139 * </li>
140 * <li>
141 * <code>options.sourceClasses</code>. If non-<code>null</code>, copy only attributes whose getters are
142 * declared in one or more of these classes or interfaces (or their superclasses or superinterfaces), ignoring all others.
143 * Any classes or interfaces that "<code>from</code>" is not an instance of
144 * are tacitly ignored.
145 * </li>
146 * <li>
147 * <code>options.destClasses</code>. If non-<code>null</code>, restrict the search for appropriate setters
148 * to methods declared in at least one of these classes or interfaces (or their superclasses or superinterfaces).
149 * Any classes or interfaces that "<code>to</code>" is not an instance of
150 * are tacitly ignored.
151 * </li>
152 * <li>
153 * <code>options.attributesToCopy</code>. If non-<code>null</code>, only copy the attributes whose
154 * names are contained in this set, ignoring all others. Any named attribute
155 * that has no corresponding getter in the source or setter in the destination
156 * is ignored.
157 * </li>
158 * <li>
159 * <code>options.attributesToIgnore</code>. If non-<code>null</code>, ignore any attributes whose
160 * names are contained in this set.
161 * </li>
162 * <li>
163 * <code>options.unmatchedAttributes</code>. If non-<code>null</code>, this
164 * <code>Map</code> is populated with all attributes which were obtained
165 * from "<code>from</code>" but for which no matching setter was found
166 * in "<code>to</code>". The map's keys will be attribute names, while
167 * the corresponding values will be the attributes' values as returned
168 * by the getter.
169 * </li>
170 * </ul>
171 * If for a given attribute no appropriate setter can be found, that
172 * attribute is tacitly ignored.
173 */
174 public static Map copy(
175 Object from,
176 Object to,
177 CopyOptions options ) throws CopyException {
178 try {
179 if ( options == null ) {
180 options = new CopyOptions();
181 }
182 Class[] fromClasses = options.sourceClasses;
183 Class[] toClasses = options.destClasses;
184 Map copiedAttributes = options.dontPopulateMap ? null : new HashMap();
185 if ( fromClasses == null ) {
186 fromClasses = (Class[])IteratorUtils.toArray( getClassIterator( from.getClass() ), Class.class );
187 }
188 if ( toClasses == null ) {
189 toClasses = (Class[])IteratorUtils.toArray( getClassIterator( to.getClass() ), Class.class );
190 }
191 fromClasses = trimClasses(fromClasses, from);
192 toClasses = trimClasses(toClasses, to);
193
194 Map getters = getMethods(fromClasses, MethodDescriptor.GETTER, false, options.includeSourceSuperclassAttributes);
195 if ( _log.isDebugEnabled() ) {
196 _log.debug( "Here are the getters to use in the copy:" + getters );
197 }
198 Map setters = getMethods(toClasses, MethodDescriptor.SETTER, false, options.includeDestSuperclassAttributes);
199 if ( _log.isDebugEnabled() ) {
200 _log.debug( "Here are the setters to use in the copy:" + setters );
201 }
202 boolean debugEnabled = _log.isDebugEnabled();
203
204 Iterator it = getters.entrySet().iterator();
205 while ( it.hasNext() ) {
206 Map.Entry entry = (Map.Entry)it.next();
207 String attName = (String)entry.getKey();
208 if ( options.attributesToCopy != null && ! options.attributesToCopy.contains(attName) ) {
209 // Skip this one.
210 }
211 else if ( options.attributesToIgnore != null && options.attributesToIgnore.contains(attName) ) {
212 // Skip this one.
213 }
214 else {
215 Method get = (Method)entry.getValue();
216 if ( debugEnabled ) {
217 _log.debug( "Calling " + get + " on a '" + from.getClass().getName() + "'." );
218 }
219 Object value = get.invoke(from, new Object[0]);
220 if ( value == null && options.ignoreNulls ) {
221 // Don't copy this one.
222 }
223 else {
224 Method set = (Method)setters.get(attName);
225 if ( set != null && set.getParameterTypes()[0].isInstance(value) ) {
226 if ( debugEnabled ) {
227 _log.debug( "Calling " + set + " on a '" + to.getClass().getName() + "' passing a '" + ClassUtils.getShortClassName(value, "null") + "'." );
228 }
229 set.invoke( to, new Object[] { value } );
230 if ( ! options.dontPopulateMap ) {
231 copiedAttributes.put(attName, set);
232 }
233 }
234 else {
235 if ( options.unmatchedAttributes != null ) {
236 options.unmatchedAttributes.put(attName, value);
237 }
238 }
239 }
240 }
241 }
242 return copiedAttributes;
243 }
244 catch( Exception e ) {
245 throw new CopyException(e);
246 }
247 }
248
249 public static void resetCacheCounters() {
250 _misses = 0;
251 _hits = 0;
252 }
253
254 public static long getCacheMisses() {
255 if ( LOCK != null ) {
256 LOCK.lock();
257 }
258 try {
259 return _misses;
260 }
261 finally {
262 if ( LOCK != null ) {
263 LOCK.unlock();
264 }
265 }
266 }
267
268 public static long getCacheHits() {
269 if ( LOCK != null ) {
270 LOCK.lock();
271 }
272 try {
273 return _hits;
274 }
275 finally {
276 if ( LOCK != null ) {
277 LOCK.unlock();
278 }
279 }
280 }
281
282 public static MethodDescriptor getDescriptor(Method method) {
283 if ( LOCK != null ) {
284 LOCK.lock();
285 }
286 MethodDescriptor desc = null;
287 if ( CACHING_ENABLED ) {
288 desc = (MethodDescriptor)_descriptors.get(method);
289 }
290 if ( desc == null ) {
291 _misses++;
292 if ( _log.isDebugEnabled() ) {
293 _log.debug( "Cache miss for method '" + method + "'" );
294 }
295 desc = new MethodDescriptor();
296 String name = method.getName();
297 Class[] parameterTypes = method.getParameterTypes();
298 if ( parameterTypes.length == 0 && name.startsWith("get") && name.length() > 3 ) {
299 desc.type = desc.GETTER;
300 desc.attributeName = name.substring(3, 4).toLowerCase() + name.substring(4);
301 }
302 else if ( parameterTypes.length == 0 && name.startsWith("is") && name.length() > 2 &&
303 Boolean.class.isAssignableFrom( method.getReturnType() ) ) {
304 desc.type = desc.GETTER;
305 desc.isBooleanGetter = true;
306 desc.attributeName = name.substring(2, 3).toLowerCase() + name.substring(3);
307 }
308 else if ( parameterTypes.length == 1 && name.startsWith("set") && name.length() > 3 ) {
309 desc.type = desc.SETTER;
310 desc.attributeName = name.substring(3, 4).toLowerCase() + name.substring(4);
311 }
312 else {
313 desc.type = desc.OTHER;
314 }
315 if ( CACHING_ENABLED ) {
316 _descriptors.put(method, desc);
317 }
318 }
319 else {
320 _hits++;
321 }
322 if ( LOCK != null ) {
323 LOCK.unlock();
324 }
325 return desc;
326 }
327
328 /**
329 * Returns all methods of the given type declared in any classes in the "<code>classes</code>"
330 * array (or their superclasses or superinterfaces), keyed by corresponding attribute
331 * name. "<code>type</code>" should be one of the public static constants of
332 * {@link MethodDescriptor}. If <code>descs</code> is true, the returned <code>Map</code>
333 * will contain {@link MethodDescriptor}'s, else it will contain <code>Method</code>'s.
334 */
335 public static Map getMethods(Class[] classes, int type, boolean descs, boolean includeSuperclassAttributes) {
336 Map result = new HashMap();
337 for( int i = 0; i < classes.length; i++ ) {
338 Class c = classes[i];
339 Method[] methods = c.getMethods();
340 for( int j = 0; j < methods.length; j++ ) {
341 Method method = methods[j];
342 if ( includeSuperclassAttributes || method.getDeclaringClass().equals(c) ) {
343 MethodDescriptor desc = getDescriptor(method);
344 if ( desc.type == type ) {
345 result.put( desc.attributeName, descs ? (Object)desc : (Object)method );
346 }
347 }
348 }
349 }
350 return result;
351 }
352
353 protected static Iterator getClassIterator(Class c) {
354 IteratorChain it = new IteratorChain();
355 it.addIterator( IteratorUtils.singletonIterator(c) );
356 it.addIterator( ClassUtils.getAllInterfaces(c).iterator() );
357 it.addIterator( ClassUtils.getAllSuperclasses(c).iterator() );
358 return it;
359 }
360
361 protected static Class[] trimClasses(Class[] classes, Object o) {
362 List l = new ArrayList();
363 l.addAll( Arrays.asList(classes) );
364 ListIterator it = l.listIterator();
365 while ( it.hasNext() ) {
366 Class c = (Class)it.next();
367 if ( ! c.isInstance(o) ) {
368 it.remove();
369 }
370 }
371 if ( l.size() < classes.length ) {
372 classes = (Class[])l.toArray( new Class[0] );
373 }
374 return classes;
375 }
376
377 protected static ClassDescriptor getClassDescriptor(Class c) {
378 synchronized(JPUBeanUtils.class) {
379 ClassDescriptor desc = (ClassDescriptor)_classes.get(c);
380 if ( desc == null ) {
381 desc = new ClassDescriptor();
382 _classes.put(c, desc);
383 }
384 return desc;
385 }
386 }
387
388 private static class ClassDescriptor {
389 }
390
391 private static Map _descriptors = new WeakHashMap();
392 private static Map _classes = new WeakHashMap();
393 private static Log _log = LogFactory.getLog(JPUBeanUtils.class);
394 private static final IReentrantLock LOCK = new ReentrantLock();
395 private static final boolean CACHING_ENABLED = true;
396 private static long _hits = 0;
397 private static long _misses = 0;
398 }