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