001 /* 002 * Copyright 2010-2011 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.convert; 017 018 import java.lang.reflect.Constructor; 019 import java.lang.reflect.Method; 020 import java.lang.reflect.Modifier; 021 import java.util.concurrent.ConcurrentHashMap; 022 import java.util.concurrent.ConcurrentMap; 023 024 /** 025 * Manager for conversion to and from a {@code String}, acting as the main client interface. 026 * <p> 027 * Support is provided for conversions based on the {@link StringConverter} interface 028 * or the {@link ToString} and {@link FromString} annotations. 029 * <p> 030 * StringConvert is thread-safe with concurrent caches. 031 */ 032 public final class StringConvert { 033 034 /** 035 * An immutable global instance. 036 * <p> 037 * This instance cannot be added to using {@link #register}, however annotated classes 038 * are picked up. To register your own converters, simply create an instance of this class. 039 */ 040 public static final StringConvert INSTANCE = new StringConvert(); 041 042 /** 043 * The cache of converters. 044 */ 045 private final ConcurrentMap<Class<?>, StringConverter<?>> registered = new ConcurrentHashMap<Class<?>, StringConverter<?>>(); 046 047 /** 048 * Creates a new conversion manager including the JDK converters. 049 * <p> 050 * The convert instance is mutable in a thread-safe manner. 051 * Converters may be altered at any time, including the JDK converters. 052 * It is strongly recommended to only alter the converters before performing 053 * actual conversions. 054 */ 055 public StringConvert() { 056 this(true); 057 } 058 059 /** 060 * Creates a new conversion manager. 061 * <p> 062 * The convert instance is mutable in a thread-safe manner. 063 * Converters may be altered at any time, including the JDK converters. 064 * It is strongly recommended to only alter the converters before performing 065 * actual conversions. 066 * 067 * @param includeJdkConverters true to include the JDK converters 068 */ 069 public StringConvert(boolean includeJdkConverters) { 070 if (includeJdkConverters) { 071 for (JDKStringConverter conv : JDKStringConverter.values()) { 072 registered.put(conv.getType(), conv); 073 } 074 registered.put(Boolean.TYPE, JDKStringConverter.BOOLEAN); 075 registered.put(Byte.TYPE, JDKStringConverter.BYTE); 076 registered.put(Short.TYPE, JDKStringConverter.SHORT); 077 registered.put(Integer.TYPE, JDKStringConverter.INTEGER); 078 registered.put(Long.TYPE, JDKStringConverter.LONG); 079 registered.put(Float.TYPE, JDKStringConverter.FLOAT); 080 registered.put(Double.TYPE, JDKStringConverter.DOUBLE); 081 registered.put(Character.TYPE, JDKStringConverter.CHARACTER); 082 // JDK 1.8 classes 083 tryRegister("java.time.Instant", "parse"); 084 tryRegister("java.time.Duration", "parse"); 085 tryRegister("java.time.LocalDate", "parse"); 086 tryRegister("java.time.LocalTime", "parse"); 087 tryRegister("java.time.LocalDateTime", "parse"); 088 tryRegister("java.time.OffsetTime", "parse"); 089 tryRegister("java.time.OffsetDateTime", "parse"); 090 tryRegister("java.time.ZonedDateTime", "parse"); 091 tryRegister("java.time.Year", "parse"); 092 tryRegister("java.time.YearMonth", "parse"); 093 tryRegister("java.time.MonthDay", "parse"); 094 tryRegister("java.time.Period", "parse"); 095 tryRegister("java.time.ZoneOffset", "of"); 096 tryRegister("java.time.ZoneId", "of"); 097 // ThreeTen backport classes 098 tryRegister("org.threeten.bp.Instant", "parse"); 099 tryRegister("org.threeten.bp.Duration", "parse"); 100 tryRegister("org.threeten.bp.LocalDate", "parse"); 101 tryRegister("org.threeten.bp.LocalTime", "parse"); 102 tryRegister("org.threeten.bp.LocalDateTime", "parse"); 103 tryRegister("org.threeten.bp.OffsetTime", "parse"); 104 tryRegister("org.threeten.bp.OffsetDateTime", "parse"); 105 tryRegister("org.threeten.bp.ZonedDateTime", "parse"); 106 tryRegister("org.threeten.bp.Year", "parse"); 107 tryRegister("org.threeten.bp.YearMonth", "parse"); 108 tryRegister("org.threeten.bp.MonthDay", "parse"); 109 tryRegister("org.threeten.bp.Period", "parse"); 110 tryRegister("org.threeten.bp.ZoneOffset", "of"); 111 tryRegister("org.threeten.bp.ZoneId", "of"); 112 // Old ThreeTen/JSR-310 classes v0.6.3 and beyond 113 tryRegister("javax.time.Instant", "parse"); 114 tryRegister("javax.time.Duration", "parse"); 115 tryRegister("javax.time.calendar.LocalDate", "parse"); 116 tryRegister("javax.time.calendar.LocalTime", "parse"); 117 tryRegister("javax.time.calendar.LocalDateTime", "parse"); 118 tryRegister("javax.time.calendar.OffsetDate", "parse"); 119 tryRegister("javax.time.calendar.OffsetTime", "parse"); 120 tryRegister("javax.time.calendar.OffsetDateTime", "parse"); 121 tryRegister("javax.time.calendar.ZonedDateTime", "parse"); 122 tryRegister("javax.time.calendar.Year", "parse"); 123 tryRegister("javax.time.calendar.YearMonth", "parse"); 124 tryRegister("javax.time.calendar.MonthDay", "parse"); 125 tryRegister("javax.time.calendar.Period", "parse"); 126 tryRegister("javax.time.calendar.ZoneOffset", "of"); 127 tryRegister("javax.time.calendar.ZoneId", "of"); 128 tryRegister("javax.time.calendar.TimeZone", "of"); 129 } 130 } 131 132 /** 133 * Tries to register a class using the standard toString/parse pattern. 134 * 135 * @param className the class name, not null 136 */ 137 private void tryRegister(String className, String fromStringMethodName) { 138 try { 139 Class<?> cls = getClass().getClassLoader().loadClass(className); 140 registerMethods(cls, "toString", fromStringMethodName); 141 } catch (Exception ex) { 142 // ignore 143 } 144 } 145 146 //----------------------------------------------------------------------- 147 /** 148 * Converts the specified object to a {@code String}. 149 * <p> 150 * This uses {@link #findConverter} to provide the converter. 151 * 152 * @param <T> the type to convert from 153 * @param object the object to convert, null returns null 154 * @return the converted string, may be null 155 * @throws RuntimeException (or subclass) if unable to convert 156 */ 157 @SuppressWarnings("unchecked") 158 public <T> String convertToString(T object) { 159 if (object == null) { 160 return null; 161 } 162 Class<T> cls = (Class<T>) object.getClass(); 163 StringConverter<T> conv = findConverter(cls); 164 return conv.convertToString(object); 165 } 166 167 /** 168 * Converts the specified object to a {@code String}. 169 * <p> 170 * This uses {@link #findConverter} to provide the converter. 171 * The class can be provided to select a more specific converter. 172 * 173 * @param <T> the type to convert from 174 * @param cls the class to convert from, not null 175 * @param object the object to convert, null returns null 176 * @return the converted string, may be null 177 * @throws RuntimeException (or subclass) if unable to convert 178 */ 179 public <T> String convertToString(Class<T> cls, T object) { 180 if (object == null) { 181 return null; 182 } 183 StringConverter<T> conv = findConverter(cls); 184 return conv.convertToString(object); 185 } 186 187 /** 188 * Converts the specified object from a {@code String}. 189 * <p> 190 * This uses {@link #findConverter} to provide the converter. 191 * 192 * @param <T> the type to convert to 193 * @param cls the class to convert to, not null 194 * @param str the string to convert, null returns null 195 * @return the converted object, may be null 196 * @throws RuntimeException (or subclass) if unable to convert 197 */ 198 public <T> T convertFromString(Class<T> cls, String str) { 199 if (str == null) { 200 return null; 201 } 202 StringConverter<T> conv = findConverter(cls); 203 return conv.convertFromString(cls, str); 204 } 205 206 /** 207 * Finds a suitable converter for the type. 208 * <p> 209 * This returns an instance of {@code StringConverter} for the specified class. 210 * This could be useful in other frameworks. 211 * <p> 212 * The search algorithm first searches the registered converters. 213 * It then searches for {@code ToString} and {@code FromString} annotations on the specified class. 214 * Both searches consider superclasses, but not interfaces. 215 * 216 * @param <T> the type of the converter 217 * @param cls the class to find a converter for, not null 218 * @return the converter, not null 219 * @throws RuntimeException (or subclass) if no converter found 220 */ 221 @SuppressWarnings("unchecked") 222 public <T> StringConverter<T> findConverter(final Class<T> cls) { 223 if (cls == null) { 224 throw new IllegalArgumentException("Class must not be null"); 225 } 226 StringConverter<T> conv = (StringConverter<T>) registered.get(cls); 227 if (conv == null) { 228 if (cls == Object.class) { 229 throw new IllegalStateException("No registered converter found: " + cls); 230 } 231 Class<?> loopCls = cls.getSuperclass(); 232 while (loopCls != null && conv == null) { 233 conv = (StringConverter<T>) registered.get(loopCls); 234 loopCls = loopCls.getSuperclass(); 235 } 236 if (conv == null) { 237 conv = findAnnotationConverter(cls); 238 if (conv == null) { 239 throw new IllegalStateException("No registered converter found: " + cls); 240 } 241 } 242 registered.putIfAbsent(cls, conv); 243 } 244 return conv; 245 } 246 247 /** 248 * Finds the conversion method. 249 * 250 * @param <T> the type of the converter 251 * @param cls the class to find a method for, not null 252 * @return the method to call, null means use {@code toString} 253 */ 254 private <T> StringConverter<T> findAnnotationConverter(final Class<T> cls) { 255 Method toString = findToStringMethod(cls); 256 if (toString == null) { 257 return null; 258 } 259 Constructor<T> con = findFromStringConstructor(cls); 260 Method fromString = findFromStringMethod(cls, con == null); 261 if (con == null && fromString == null) { 262 throw new IllegalStateException("Class annotated with @ToString but not with @FromString"); 263 } 264 if (con != null && fromString != null) { 265 throw new IllegalStateException("Both method and constructor are annotated with @FromString"); 266 } 267 if (con != null) { 268 return new MethodConstructorStringConverter<T>(cls, toString, con); 269 } else { 270 return new MethodsStringConverter<T>(cls, toString, fromString); 271 } 272 } 273 274 /** 275 * Finds the conversion method. 276 * 277 * @param cls the class to find a method for, not null 278 * @return the method to call, null means use {@code toString} 279 */ 280 private Method findToStringMethod(Class<?> cls) { 281 Method matched = null; 282 Class<?> loopCls = cls; 283 while (loopCls != null && matched == null) { 284 Method[] methods = loopCls.getDeclaredMethods(); 285 for (Method method : methods) { 286 ToString toString = method.getAnnotation(ToString.class); 287 if (toString != null) { 288 if (matched != null) { 289 throw new IllegalStateException("Two methods are annotated with @ToString"); 290 } 291 matched = method; 292 } 293 } 294 loopCls = loopCls.getSuperclass(); 295 } 296 return matched; 297 } 298 299 /** 300 * Finds the conversion method. 301 * 302 * @param <T> the type of the converter 303 * @param cls the class to find a method for, not null 304 * @return the method to call, null means use {@code toString} 305 */ 306 private <T> Constructor<T> findFromStringConstructor(Class<T> cls) { 307 Constructor<T> con; 308 try { 309 con = cls.getDeclaredConstructor(String.class); 310 } catch (NoSuchMethodException ex) { 311 try { 312 con = cls.getDeclaredConstructor(CharSequence.class); 313 } catch (NoSuchMethodException ex2) { 314 return null; 315 } 316 } 317 FromString fromString = con.getAnnotation(FromString.class); 318 return fromString != null ? con : null; 319 } 320 321 /** 322 * Finds the conversion method. 323 * 324 * @param cls the class to find a method for, not null 325 * @return the method to call, null means use {@code toString} 326 */ 327 private Method findFromStringMethod(Class<?> cls, boolean searchSuperclasses) { 328 Method matched = null; 329 Class<?> loopCls = cls; 330 while (loopCls != null && matched == null) { 331 Method[] methods = loopCls.getDeclaredMethods(); 332 for (Method method : methods) { 333 FromString fromString = method.getAnnotation(FromString.class); 334 if (fromString != null) { 335 if (matched != null) { 336 throw new IllegalStateException("Two methods are annotated with @ToString"); 337 } 338 matched = method; 339 } 340 } 341 if (searchSuperclasses == false) { 342 break; 343 } 344 loopCls = loopCls.getSuperclass(); 345 } 346 return matched; 347 } 348 349 //----------------------------------------------------------------------- 350 /** 351 * Registers a converter for a specific type. 352 * <p> 353 * The converter will be used for subclasses unless overidden. 354 * <p> 355 * No new converters may be registered for the global singleton. 356 * 357 * @param <T> the type of the converter 358 * @param cls the class to register a converter for, not null 359 * @param converter the String converter, not null 360 * @throws IllegalArgumentException if the class or converter are null 361 * @throws IllegalStateException if trying to alter the global singleton 362 */ 363 public <T> void register(final Class<T> cls, StringConverter<T> converter) { 364 if (cls == null ) { 365 throw new IllegalArgumentException("Class must not be null"); 366 } 367 if (converter == null) { 368 throw new IllegalArgumentException("StringConverter must not be null"); 369 } 370 if (this == INSTANCE) { 371 throw new IllegalStateException("Global singleton cannot be extended"); 372 } 373 registered.put(cls, converter); 374 } 375 376 /** 377 * Registers a converter for a specific type using two separate converters. 378 * <p> 379 * This method registers a converter for the specified class. 380 * It is primarily intended for use with JDK 1.8 method references or lambdas: 381 * <pre> 382 * sc.register(Distance.class, Distance::toString, Distance::parse); 383 * </pre> 384 * The converter will be used for subclasses unless overidden. 385 * <p> 386 * No new converters may be registered for the global singleton. 387 * 388 * @param <T> the type of the converter 389 * @param cls the class to register a converter for, not null 390 * @param toString the to String converter, typically a method reference, not null 391 * @param fromString the from String converter, typically a method reference, not null 392 * @throws IllegalArgumentException if the class or converter are null 393 * @throws IllegalStateException if trying to alter the global singleton 394 * @since 1.3 395 */ 396 public <T> void register(final Class<T> cls, final ToStringConverter<T> toString, final FromStringConverter<T> fromString) { 397 if (fromString == null || toString == null) { 398 throw new IllegalArgumentException("Converters must not be null"); 399 } 400 register(cls, new StringConverter<T>() { 401 public String convertToString(T object) { 402 return toString.convertToString(object); 403 } 404 public T convertFromString(Class<? extends T> cls, String str) { 405 return fromString.convertFromString(cls, str); 406 } 407 }); 408 } 409 410 /** 411 * Registers a converter for a specific type by method names. 412 * <p> 413 * This method allows the converter to be used when the target class cannot have annotations added. 414 * The two method names must obey the same rules as defined by the annotations 415 * {@link ToString} and {@link FromString}. 416 * The converter will be used for subclasses unless overidden. 417 * <p> 418 * No new converters may be registered for the global singleton. 419 * <p> 420 * For example, {@code convert.registerMethods(Distance.class, "toString", "parse");} 421 * 422 * @param <T> the type of the converter 423 * @param cls the class to register a converter for, not null 424 * @param toStringMethodName the name of the method converting to a string, not null 425 * @param fromStringMethodName the name of the method converting from a string, not null 426 * @throws IllegalArgumentException if the class or method name are null or invalid 427 * @throws IllegalStateException if trying to alter the global singleton 428 */ 429 public <T> void registerMethods(final Class<T> cls, String toStringMethodName, String fromStringMethodName) { 430 if (cls == null ) { 431 throw new IllegalArgumentException("Class must not be null"); 432 } 433 if (toStringMethodName == null || fromStringMethodName == null) { 434 throw new IllegalArgumentException("Method names must not be null"); 435 } 436 if (this == INSTANCE) { 437 throw new IllegalStateException("Global singleton cannot be extended"); 438 } 439 Method toString = findToStringMethod(cls, toStringMethodName); 440 Method fromString = findFromStringMethod(cls, fromStringMethodName); 441 MethodsStringConverter<T> converter = new MethodsStringConverter<T>(cls, toString, fromString); 442 registered.putIfAbsent(cls, converter); 443 } 444 445 /** 446 * Registers a converter for a specific type by method and constructor. 447 * <p> 448 * This method allows the converter to be used when the target class cannot have annotations added. 449 * The two method name and constructor must obey the same rules as defined by the annotations 450 * {@link ToString} and {@link FromString}. 451 * The converter will be used for subclasses unless overidden. 452 * <p> 453 * No new converters may be registered for the global singleton. 454 * <p> 455 * For example, {@code convert.registerMethodConstructor(Distance.class, "toString");} 456 * 457 * @param <T> the type of the converter 458 * @param cls the class to register a converter for, not null 459 * @param toStringMethodName the name of the method converting to a string, not null 460 * @throws IllegalArgumentException if the class or method name are null or invalid 461 * @throws IllegalStateException if trying to alter the global singleton 462 */ 463 public <T> void registerMethodConstructor(final Class<T> cls, String toStringMethodName) { 464 if (cls == null ) { 465 throw new IllegalArgumentException("Class must not be null"); 466 } 467 if (toStringMethodName == null) { 468 throw new IllegalArgumentException("Method name must not be null"); 469 } 470 if (this == INSTANCE) { 471 throw new IllegalStateException("Global singleton cannot be extended"); 472 } 473 Method toString = findToStringMethod(cls, toStringMethodName); 474 Constructor<T> fromString = findFromStringConstructorByType(cls); 475 MethodConstructorStringConverter<T> converter = new MethodConstructorStringConverter<T>(cls, toString, fromString); 476 registered.putIfAbsent(cls, converter); 477 } 478 479 /** 480 * Finds the conversion method. 481 * 482 * @param cls the class to find a method for, not null 483 * @param methodName the name of the method to find, not null 484 * @return the method to call, null means use {@code toString} 485 */ 486 private Method findToStringMethod(Class<?> cls, String methodName) { 487 Method m; 488 try { 489 m = cls.getMethod(methodName); 490 } catch (NoSuchMethodException ex) { 491 throw new IllegalArgumentException(ex); 492 } 493 if (Modifier.isStatic(m.getModifiers())) { 494 throw new IllegalArgumentException("Method must not be static: " + methodName); 495 } 496 return m; 497 } 498 499 /** 500 * Finds the conversion method. 501 * 502 * @param cls the class to find a method for, not null 503 * @param methodName the name of the method to find, not null 504 * @return the method to call, null means use {@code toString} 505 */ 506 private Method findFromStringMethod(Class<?> cls, String methodName) { 507 Method m; 508 try { 509 m = cls.getMethod(methodName, String.class); 510 } catch (NoSuchMethodException ex) { 511 try { 512 m = cls.getMethod(methodName, CharSequence.class); 513 } catch (NoSuchMethodException ex2) { 514 throw new IllegalArgumentException("Method not found", ex2); 515 } 516 } 517 if (Modifier.isStatic(m.getModifiers()) == false) { 518 throw new IllegalArgumentException("Method must be static: " + methodName); 519 } 520 return m; 521 } 522 523 /** 524 * Finds the conversion method. 525 * 526 * @param <T> the type of the converter 527 * @param cls the class to find a method for, not null 528 * @return the method to call, null means use {@code toString} 529 */ 530 private <T> Constructor<T> findFromStringConstructorByType(Class<T> cls) { 531 try { 532 return cls.getDeclaredConstructor(String.class); 533 } catch (NoSuchMethodException ex) { 534 try { 535 return cls.getDeclaredConstructor(CharSequence.class); 536 } catch (NoSuchMethodException ex2) { 537 throw new IllegalArgumentException("Constructor not found", ex2); 538 } 539 } 540 } 541 542 //----------------------------------------------------------------------- 543 /** 544 * Returns a simple string representation of the object. 545 * 546 * @return the string representation, never null 547 */ 548 @Override 549 public String toString() { 550 return getClass().getSimpleName(); 551 } 552 553 }