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     */
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}