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    }