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 }