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 */
016package org.joda.beans;
017
018import java.lang.reflect.Array;
019import java.lang.reflect.GenericArrayType;
020import java.lang.reflect.ParameterizedType;
021import java.lang.reflect.Type;
022import java.lang.reflect.TypeVariable;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.Comparator;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Set;
030import java.util.concurrent.ConcurrentHashMap;
031
032import org.joda.beans.impl.direct.DirectBean;
033import org.joda.beans.impl.flexi.FlexiBean;
034import 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 */
041public 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 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}