001    /*
002     *  Copyright 2001-2013 Stephen Colebourne
003     *
004     *  Licensed under the Apache License, Version 2.0 (the "License");
005     *  you may not use this file except in compliance with the License.
006     *  You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     *  Unless required by applicable law or agreed to in writing, software
011     *  distributed under the License is distributed on an "AS IS" BASIS,
012     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     *  See the License for the specific language governing permissions and
014     *  limitations under the License.
015     */
016    package org.joda.beans;
017    
018    import java.lang.reflect.Array;
019    import java.lang.reflect.GenericArrayType;
020    import java.lang.reflect.ParameterizedType;
021    import java.lang.reflect.Type;
022    import java.lang.reflect.TypeVariable;
023    import java.util.Arrays;
024    import java.util.Collection;
025    import java.util.Collections;
026    import java.util.Comparator;
027    import java.util.HashMap;
028    import java.util.Map;
029    import java.util.Set;
030    import java.util.concurrent.ConcurrentHashMap;
031    
032    import org.joda.beans.impl.direct.DirectBean;
033    import org.joda.beans.impl.flexi.FlexiBean;
034    import org.joda.convert.StringConvert;
035    
036    /**
037     * A set of utilities to assist when working with beans and properties.
038     * 
039     * @author Stephen Colebourne
040     */
041    public final class JodaBeanUtils {
042    
043        /**
044         * The cache of meta-beans.
045         */
046        private static final ConcurrentHashMap<Class<?>, MetaBean> metaBeans = new ConcurrentHashMap<Class<?>, MetaBean>();
047        /**
048         * The cache of meta-beans.
049         */
050        private static final StringConvert converter = new StringConvert();
051    
052        /**
053         * Restricted constructor.
054         */
055        private JodaBeanUtils() {
056        }
057    
058        //-----------------------------------------------------------------------
059        /**
060         * Gets the meta-bean given a class.
061         * <p>
062         * This only works for those beans that have registered their meta-beans.
063         * See {@link #registerMetaBean(MetaBean)}.
064         * 
065         * @param cls  the class to get the meta-bean for, not null
066         * @return the meta-bean, not null
067         * @throws IllegalArgumentException if unable to obtain the meta-bean
068         */
069        public static MetaBean metaBean(Class<?> cls) {
070            MetaBean meta = metaBeans.get(cls);
071            if (meta == null) {
072                throw new IllegalArgumentException("Unable to find meta-bean: " + cls.getName());
073            }
074            return meta;
075        }
076    
077        /**
078         * Registers a meta-bean.
079         * <p>
080         * This should be done for all beans in a static factory where possible.
081         * If the meta-bean is dynamic, this method should not be called.
082         * 
083         * @param metaBean  the meta-bean, not null
084         * @throws IllegalArgumentException if unable to register
085         */
086        public static void registerMetaBean(MetaBean metaBean) {
087            Class<? extends Bean> type = metaBean.beanType();
088            if (metaBeans.putIfAbsent(type, metaBean) != null) {
089                throw new IllegalArgumentException("Cannot register class twice: " + type.getName());
090            }
091        }
092    
093        //-----------------------------------------------------------------------
094        /**
095         * Gets the standard string format converter.
096         * <p>
097         * This returns a singleton that may be mutated (holds a concurrent map).
098         * New conversions should be registered at program startup.
099         * 
100         * @return the standard string converter, not null
101         */
102        public static StringConvert stringConverter() {
103            return converter;
104        }
105    
106        //-----------------------------------------------------------------------
107        /**
108         * Checks if two objects are equal handling null.
109         * 
110         * @param obj1  the first object, may be null
111         * @param obj2  the second object, may be null
112         * @return true if equal
113         */
114        public static boolean equal(Object obj1, Object obj2) {
115            if (obj1 == obj2) {
116                return true;
117            }
118            if (obj1 == null || obj2 == null) {
119                return false;
120            }
121            if (obj1.getClass().isArray() && obj1.getClass() == obj2.getClass()) {
122                if (obj1 instanceof Object[] && obj2 instanceof Object[]) {
123                    return Arrays.deepEquals((Object[]) obj1, (Object[]) obj2);
124                } else if (obj1 instanceof int[] && obj2 instanceof int[]) {
125                    return Arrays.equals((int[]) obj1, (int[]) obj2);
126                } else if (obj1 instanceof long[] && obj2 instanceof long[]) {
127                    return Arrays.equals((long[]) obj1, (long[]) obj2);
128                } else if (obj1 instanceof byte[] && obj2 instanceof byte[]) {
129                    return Arrays.equals((byte[]) obj1, (byte[]) obj2);
130                } else if (obj1 instanceof double[] && obj2 instanceof double[]) {
131                    return Arrays.equals((double[]) obj1, (double[]) obj2);
132                } else if (obj1 instanceof float[] && obj2 instanceof float[]) {
133                    return Arrays.equals((float[]) obj1, (float[]) obj2);
134                } else if (obj1 instanceof char[] && obj2 instanceof char[]) {
135                    return Arrays.equals((char[]) obj1, (char[]) obj2);
136                } else if (obj1 instanceof short[] && obj2 instanceof short[]) {
137                    return Arrays.equals((short[]) obj1, (short[]) obj2);
138                } else if (obj1 instanceof boolean[] && obj2 instanceof boolean[]) {
139                    return Arrays.equals((boolean[]) obj1, (boolean[]) obj2);
140                }
141            }
142            return obj1.equals(obj2);
143        }
144    
145        /**
146         * Returns a hash code for an object handling null.
147         * 
148         * @param obj  the object, may be null
149         * @return the hash code
150         */
151        public static int hashCode(Object obj) {
152            return obj == null ? 0 : obj.hashCode();
153        }
154    
155        /**
156         * Returns a hash code for a {@code long}.
157         * 
158         * @param obj  the object, may be null
159         * @return the hash code
160         */
161        public static int hashCode(long value) {
162            return (int) (value ^ value >>> 32);
163        }
164    
165        /**
166         * Returns a hash code for a {@code float}.
167         * 
168         * @param obj  the object, may be null
169         * @return the hash code
170         */
171        public static int hashCode(float value) {
172            return Float.floatToIntBits(value);
173        }
174    
175        /**
176         * Returns a hash code for a {@code double}.
177         * 
178         * @param obj  the object, may be null
179         * @return the hash code
180         */
181        public static int hashCode(double value) {
182            return hashCode(Double.doubleToLongBits(value));
183        }
184    
185        //-----------------------------------------------------------------------
186        /**
187         * Checks if the two beans have the same set of properties.
188         * <p>
189         * This comparison checks that both beans have the same set of property names
190         * and that the value of each property name is also equal.
191         * It does not check the bean type, thus a {@link FlexiBean} may be equal
192         * to a {@link DirectBean}.
193         * <p>
194         * This comparison is usable with the {@link #propertiesHashCode} method.
195         * The result is the same as that if each bean was converted to a {@code Map}
196         * from name to value.
197         * 
198         * @param bean1  the first bean to compare, not null
199         * @param bean2  the second bean to compare, not null
200         * @return true if equal
201         */
202        public static boolean propertiesEqual(Bean bean1, Bean bean2) {
203            Set<String> names = bean1.propertyNames();
204            if (names.equals(bean2.propertyNames()) == false) {
205                return false;
206            }
207            for (String name : names) {
208                Object value1 = bean1.property(name).get();
209                Object value2 = bean2.property(name).get();
210                if (equal(value1, value2) == false) {
211                    return false;
212                }
213            }
214            return true;
215        }
216    
217        /**
218         * Returns a hash code based on the set of properties on a bean.
219         * <p>
220         * This hash code is usable with the {@link #propertiesEqual} method.
221         * The result is the same as that if each bean was converted to a {@code Map}
222         * from name to value.
223         * 
224         * @param bean  the bean to generate a hash code for, not null
225         * @return the hash code
226         */
227        public static int propertiesHashCode(Bean bean) {
228            int hash = 7;
229            Set<String> names = bean.propertyNames();
230            for (String name : names) {
231                Object value = bean.property(name).get();
232                hash += hashCode(value);
233            }
234            return hash;
235        }
236    
237        /**
238         * Returns a string describing the set of properties on a bean.
239         * <p>
240         * The result is the same as that if the bean was converted to a {@code Map}
241         * from name to value.
242         * 
243         * @param bean  the bean to generate a string for, not null
244         * @param prefix  the prefix to use, null ignored
245         * @return the string form of the bean, not null
246         */
247        public static String propertiesToString(Bean bean, String prefix) {
248            Set<String> names = bean.propertyNames();
249            StringBuilder buf = new StringBuilder((names.size()) * 32 + prefix.length());
250            if (prefix != null) {
251                buf.append(prefix);
252            }
253            buf.append('{');
254            if (names.size() > 0) {
255                for (String name : names) {
256                    Object value = bean.property(name).get();
257                    buf.append(name).append('=').append(value).append(',').append(' ');
258                }
259                buf.setLength(buf.length() - 2);
260            }
261            buf.append('}');
262            return buf.toString();
263        }
264    
265        //-----------------------------------------------------------------------
266        @SuppressWarnings("unchecked")
267        public static <T extends Bean> T clone(T original) {
268          BeanBuilder<? extends Bean> builder = original.metaBean().builder();
269          for (MetaProperty<?> mp : original.metaBean().metaPropertyIterable()) {
270            if (mp.readWrite().isWritable()) {
271              Object value = mp.get(original);
272              if (value instanceof Bean) {
273                value = clone((Bean) value);
274              }
275              builder.set(mp.name(), value);
276            }
277          }
278          return (T) builder.build();
279        }
280    
281        //-----------------------------------------------------------------------
282        /**
283         * Checks if the value is not null, throwing an exception if it is.
284         * 
285         * @param value  the value to check, may be null
286         * @param propertyName  the property name, should not be null
287         * @throws IllegalArgumentException if the value is null
288         */
289        public static void notNull(Object value, String propertyName) {
290            if (value == null) {
291                throw new IllegalArgumentException("Argument '" + propertyName + "' must not be null");
292            }
293        }
294    
295        /**
296         * Checks if the value is not empty, throwing an exception if it is.
297         * 
298         * @param value  the value to check, may be null
299         * @param propertyName  the property name, should not be null
300         * @throws IllegalArgumentException if the value is null or empty
301         */
302        public static void notEmpty(String value, String propertyName) {
303            if (value == null || value.length() == 0) {
304                throw new IllegalArgumentException("Argument '" + propertyName + "' must not be empty");
305            }
306        }
307    
308        //-----------------------------------------------------------------------
309        /**
310         * Extracts the collection content type as a {@code Class} from a property.
311         * <p>
312         * This method allows the resolution of generics in certain cases.
313         * 
314         * @param prop  the property to examine, not null
315         * @return the collection content type, null if unable to determine
316         * @throws IllegalArgumentException if the property is not a collection
317         */
318        public static Class<?> collectionType(Property<?> prop) {
319            return collectionType(prop.metaProperty(), prop.bean().getClass());
320        }
321    
322        /**
323         * Extracts the collection content type as a {@code Class} from a meta-property.
324         * <p>
325         * The target type is the type of the object, not the declaring type of the meta-property.
326         * 
327         * @param prop  the property to examine, not null
328         * @param targetClass  the target type to evaluate against, not null
329         * @return the collection content type, null if unable to determine
330         * @throws IllegalArgumentException if the property is not a collection
331         */
332        public static Class<?> collectionType(MetaProperty<?> prop, Class<?> targetClass) {
333            if (Collection.class.isAssignableFrom(prop.propertyType()) == false) {
334                throw new IllegalArgumentException("Property is not a Collection");
335            }
336            return extractType(targetClass, prop, 1, 0);
337        }
338    
339        /**
340         * Extracts the map key type as a {@code Class} from a meta-property.
341         * 
342         * @param prop  the property to examine, not null
343         * @return the map key type, null if unable to determine
344         * @throws IllegalArgumentException if the property is not a map
345         */
346        public static Class<?> mapKeyType(Property<?> prop) {
347            return mapKeyType(prop.metaProperty(), prop.bean().getClass());
348        }
349    
350        /**
351         * Extracts the map key type as a {@code Class} from a meta-property.
352         * <p>
353         * The target type is the type of the object, not the declaring type of the meta-property.
354         * 
355         * @param prop  the property to examine, not null
356         * @param targetClass  the target type to evaluate against, not null
357         * @return the map key type, null if unable to determine
358         * @throws IllegalArgumentException if the property is not a map
359         */
360        public static Class<?> mapKeyType(MetaProperty<?> prop, Class<?> targetClass) {
361            if (Map.class.isAssignableFrom(prop.propertyType()) == false) {
362                throw new IllegalArgumentException("Property is not a Map");
363            }
364            return extractType(targetClass, prop, 2, 0);
365        }
366    
367        /**
368         * Extracts the map key type as a {@code Class} from a meta-property.
369         * 
370         * @param prop  the property to examine, not null
371         * @return the map key type, null if unable to determine
372         * @throws IllegalArgumentException if the property is not a map
373         */
374        public static Class<?> mapValueType(Property<?> prop) {
375            return mapValueType(prop.metaProperty(), prop.bean().getClass());
376        }
377    
378        /**
379         * Extracts the map key type as a {@code Class} from a meta-property.
380         * <p>
381         * The target type is the type of the object, not the declaring type of the meta-property.
382         * 
383         * @param prop  the property to examine, not null
384         * @param targetClass  the target type to evaluate against, not null
385         * @return the map key type, null if unable to determine
386         * @throws IllegalArgumentException if the property is not a map
387         */
388        public static Class<?> mapValueType(MetaProperty<?> prop, Class<?> targetClass) {
389            if (Map.class.isAssignableFrom(prop.propertyType()) == false) {
390                throw new IllegalArgumentException("Property is not a Map");
391            }
392            return extractType(targetClass, prop, 2, 1);
393        }
394    
395        private static Class<?> extractType(Class<?> targetClass, MetaProperty<?> prop, int size, int index) {
396            Type genType = prop.propertyGenericType();
397            if (genType instanceof ParameterizedType) {
398                ParameterizedType pt = (ParameterizedType) genType;
399                Type[] types = pt.getActualTypeArguments();
400                if (types.length == size) {
401                    Type type = types[index];
402                    if (type instanceof TypeVariable) {
403                        type = resolveGenerics(targetClass, (TypeVariable<?>) type);
404                    }
405                    return eraseToClass(type);
406                }
407            }
408            return null;
409        }
410    
411        private static Type resolveGenerics(Class<?> targetClass, TypeVariable<?> typevar) {
412            // looks up meaning of type variables like T
413            Map<Type, Type> resolved = new HashMap<Type, Type>();
414            Type type = targetClass;
415            while (type != null) {
416                if (type instanceof Class) {
417                    type = ((Class<?>) type).getGenericSuperclass();
418                } else if (type instanceof ParameterizedType) {
419                    // find actual types captured by subclass
420                    ParameterizedType pt = (ParameterizedType) type;
421                    Type[] actualTypeArguments = pt.getActualTypeArguments();
422                    // find type variables declared in source code
423                    Class<?> rawType = eraseToClass(pt.getRawType());
424                    TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
425                    for (int i = 0; i < actualTypeArguments.length; i++) {
426                        resolved.put(typeParameters[i], actualTypeArguments[i]);
427                    }
428                    type = rawType.getGenericSuperclass();
429                }
430            }
431            // resolve type variable to a meaningful type
432            Type result = typevar;
433            while (resolved.containsKey(result)) {
434                result = resolved.get(result);
435            }
436            return result;
437        }
438    
439        private static Class<?> eraseToClass(Type type) {
440            if (type instanceof Class) {
441                return (Class<?>) type;
442            } else if (type instanceof ParameterizedType) {
443                return eraseToClass(((ParameterizedType) type).getRawType());
444            } else if (type instanceof GenericArrayType) {
445                Type componentType = ((GenericArrayType) type).getGenericComponentType();
446                Class<?> componentClass = eraseToClass(componentType);
447                if (componentClass != null ) {
448                  return Array.newInstance(componentClass, 0).getClass();
449                }
450            } else if (type instanceof TypeVariable) {
451                Type[] bounds = ((TypeVariable<?>) type).getBounds();
452                if (bounds.length == 0) {
453                    return Object.class;
454                } else {
455                    return eraseToClass(bounds[0]);
456                }
457            }
458            return null;
459        }
460    
461        //-------------------------------------------------------------------------
462        /**
463         * Obtains a comparator for the specified bean query.
464         * <p>
465         * The result of the query must be {@link Comparable}.
466         * 
467         * @param query  the query to use, not null
468         * @param ascending  true for ascending, false for descending
469         * @return the comparator, not null
470         */
471        public static Comparator<Bean> comparator(BeanQuery<?> query, boolean ascending) {
472            return (ascending ? comparatorAscending(query) : comparatorDescending(query));
473        }
474    
475        /**
476         * Obtains an ascending comparator for the specified bean query.
477         * <p>
478         * The result of the query must be {@link Comparable}.
479         * 
480         * @param query  the query to use, not null
481         * @return the comparator, not null
482         */
483        public static Comparator<Bean> comparatorAscending(BeanQuery<?> query) {
484            if (query == null) {
485                throw new NullPointerException("BeanQuery must not be null");
486            }
487            return new Comp(query);
488        }
489    
490        /**
491         * Obtains an descending comparator for the specified bean query.
492         * <p>
493         * The result of the query must be {@link Comparable}.
494         * 
495         * @param query  the query to use, not null
496         * @return the comparator, not null
497         */
498        public static Comparator<Bean> comparatorDescending(BeanQuery<?> query) {
499            if (query == null) {
500                throw new NullPointerException("BeanQuery must not be null");
501            }
502            return Collections.reverseOrder(new Comp(query));
503        }
504    
505        //-------------------------------------------------------------------------
506        /**
507         * Compare for BeanQuery.
508         */
509        private static final class Comp implements Comparator<Bean> {
510            private final BeanQuery<?> query;
511    
512            private Comp(BeanQuery<?> query) {
513                this.query = query;
514            }
515    
516            @Override
517            public int compare(Bean bean1, Bean bean2) {
518                @SuppressWarnings("unchecked")
519                Comparable<Object> value1 = (Comparable<Object>) query.get(bean1);
520                Object value2 = query.get(bean2);
521                return value1.compareTo(value2);
522            }
523        }
524    
525    }