View Javadoc

1   /*
2    *  Copyright 2001-2013 Stephen Colebourne
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  package org.joda.beans;
17  
18  import java.lang.reflect.Array;
19  import java.lang.reflect.GenericArrayType;
20  import java.lang.reflect.ParameterizedType;
21  import java.lang.reflect.Type;
22  import java.lang.reflect.TypeVariable;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.Comparator;
27  import java.util.HashMap;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.concurrent.ConcurrentHashMap;
31  
32  import org.joda.beans.impl.direct.DirectBean;
33  import org.joda.beans.impl.flexi.FlexiBean;
34  import org.joda.convert.StringConvert;
35  
36  /**
37   * A set of utilities to assist when working with beans and properties.
38   * 
39   * @author Stephen Colebourne
40   */
41  public final class JodaBeanUtils {
42  
43      /**
44       * The cache of meta-beans.
45       */
46      private static final ConcurrentHashMap<Class<?>, MetaBean> metaBeans = new ConcurrentHashMap<Class<?>, MetaBean>();
47      /**
48       * The cache of meta-beans.
49       */
50      private static final StringConvert converter = new StringConvert();
51  
52      /**
53       * Restricted constructor.
54       */
55      private JodaBeanUtils() {
56      }
57  
58      //-----------------------------------------------------------------------
59      /**
60       * Gets the meta-bean given a class.
61       * <p>
62       * This only works for those beans that have registered their meta-beans.
63       * See {@link #registerMetaBean(MetaBean)}.
64       * 
65       * @param cls  the class to get the meta-bean for, not null
66       * @return the meta-bean, not null
67       * @throws IllegalArgumentException if unable to obtain the meta-bean
68       */
69      public static MetaBean metaBean(Class<?> cls) {
70          MetaBean meta = metaBeans.get(cls);
71          if (meta == null) {
72              throw new IllegalArgumentException("Unable to find meta-bean: " + cls.getName());
73          }
74          return meta;
75      }
76  
77      /**
78       * Registers a meta-bean.
79       * <p>
80       * This should be done for all beans in a static factory where possible.
81       * If the meta-bean is dynamic, this method should not be called.
82       * 
83       * @param metaBean  the meta-bean, not null
84       * @throws IllegalArgumentException if unable to register
85       */
86      public static void registerMetaBean(MetaBean metaBean) {
87          Class<? extends Bean> type = metaBean.beanType();
88          if (metaBeans.putIfAbsent(type, metaBean) != null) {
89              throw new IllegalArgumentException("Cannot register class twice: " + type.getName());
90          }
91      }
92  
93      //-----------------------------------------------------------------------
94      /**
95       * Gets the standard string format converter.
96       * <p>
97       * This returns a singleton that may be mutated (holds a concurrent map).
98       * New conversions should be registered at program startup.
99       * 
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 value  the value to convert to a hash code
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 value  the value to convert to a hash code
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 value  the value to convert to a hash code
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 }