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    }