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    }