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 }