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}