View Javadoc

1   /*
2    *  Copyright 2010-2011 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.convert;
17  
18  import java.lang.reflect.Constructor;
19  import java.lang.reflect.Method;
20  import java.lang.reflect.Modifier;
21  import java.util.concurrent.ConcurrentHashMap;
22  import java.util.concurrent.ConcurrentMap;
23  
24  /**
25   * Manager for conversion to and from a {@code String}, acting as the main client interface.
26   * <p>
27   * Support is provided for conversions based on the {@link StringConverter} interface
28   * or the {@link ToString} and {@link FromString} annotations.
29   * <p>
30   * StringConvert is thread-safe with concurrent caches.
31   */
32  public final class StringConvert {
33  
34      /**
35       * An immutable global instance.
36       * <p>
37       * This instance cannot be added to using {@link #register}, however annotated classes
38       * are picked up. To register your own converters, simply create an instance of this class.
39       */
40      public static final StringConvert INSTANCE = new StringConvert();
41  
42      /**
43       * The cache of converters.
44       */
45      private final ConcurrentMap<Class<?>, StringConverter<?>> registered = new ConcurrentHashMap<Class<?>, StringConverter<?>>();
46  
47      /**
48       * Creates a new conversion manager including the JDK converters.
49       * <p>
50       * The convert instance is mutable in a thread-safe manner.
51       * Converters may be altered at any time, including the JDK converters.
52       * It is strongly recommended to only alter the converters before performing
53       * actual conversions.
54       */
55      public StringConvert() {
56          this(true);
57      }
58  
59      /**
60       * Creates a new conversion manager.
61       * <p>
62       * The convert instance is mutable in a thread-safe manner.
63       * Converters may be altered at any time, including the JDK converters.
64       * It is strongly recommended to only alter the converters before performing
65       * actual conversions.
66       * 
67       * @param includeJdkConverters  true to include the JDK converters
68       */
69      public StringConvert(boolean includeJdkConverters) {
70          if (includeJdkConverters) {
71              for (JDKStringConverter conv : JDKStringConverter.values()) {
72                  registered.put(conv.getType(), conv);
73              }
74              registered.put(Boolean.TYPE, JDKStringConverter.BOOLEAN);
75              registered.put(Byte.TYPE, JDKStringConverter.BYTE);
76              registered.put(Short.TYPE, JDKStringConverter.SHORT);
77              registered.put(Integer.TYPE, JDKStringConverter.INTEGER);
78              registered.put(Long.TYPE, JDKStringConverter.LONG);
79              registered.put(Float.TYPE, JDKStringConverter.FLOAT);
80              registered.put(Double.TYPE, JDKStringConverter.DOUBLE);
81              registered.put(Character.TYPE, JDKStringConverter.CHARACTER);
82              // JDK 1.8 classes
83              tryRegister("java.time.Instant", "parse");
84              tryRegister("java.time.Duration", "parse");
85              tryRegister("java.time.LocalDate", "parse");
86              tryRegister("java.time.LocalTime", "parse");
87              tryRegister("java.time.LocalDateTime", "parse");
88              tryRegister("java.time.OffsetTime", "parse");
89              tryRegister("java.time.OffsetDateTime", "parse");
90              tryRegister("java.time.ZonedDateTime", "parse");
91              tryRegister("java.time.Year", "parse");
92              tryRegister("java.time.YearMonth", "parse");
93              tryRegister("java.time.MonthDay", "parse");
94              tryRegister("java.time.Period", "parse");
95              tryRegister("java.time.ZoneOffset", "of");
96              tryRegister("java.time.ZoneId", "of");
97              // ThreeTen backport classes
98              tryRegister("org.threeten.bp.Instant", "parse");
99              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 }