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 */
016package org.joda.convert;
017
018import java.lang.reflect.Constructor;
019import java.lang.reflect.Method;
020import java.lang.reflect.Modifier;
021import java.util.concurrent.ConcurrentHashMap;
022import 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 */
032public 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}