001    /*
002     *  Copyright 2010 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.util.concurrent.ConcurrentHashMap;
021    import java.util.concurrent.ConcurrentMap;
022    
023    /**
024     * Manager for conversion to and from a {@code String}, acting as the main client interface.
025     * <p>
026     * Support is provided for conversions based on the {@link StringConverter} interface
027     * or the {@link ToString} and {@link FromString} annotations.
028     * <p>
029     * StringConvert is thread-safe with concurrent caches.
030     */
031    public final class StringConvert {
032    
033        /**
034         * An immutable global instance.
035         * <p>
036         * This instance cannot be added to using {@link #register}, however annotated classes
037         * are picked up. To register your own converters, simply create an instance of this class.
038         */
039        public static final StringConvert INSTANCE = new StringConvert();
040    
041        /**
042         * The cache of converters.
043         */
044        private final ConcurrentMap<Class<?>, StringConverter<?>> registered = new ConcurrentHashMap<Class<?>, StringConverter<?>>();
045    
046        /**
047         * Creates a new conversion manager including the JDK converters.
048         */
049        public StringConvert() {
050            this(true);
051        }
052    
053        /**
054         * Creates a new conversion manager.
055         * 
056         * @param includeJdkConverters  true to include the JDK converters
057         */
058        public StringConvert(boolean includeJdkConverters) {
059            if (includeJdkConverters) {
060                for (JDKStringConverter conv : JDKStringConverter.values()) {
061                    registered.put(conv.getType(), conv);
062                }
063                registered.put(Boolean.TYPE, JDKStringConverter.BOOLEAN);
064                registered.put(Byte.TYPE, JDKStringConverter.BYTE);
065                registered.put(Short.TYPE, JDKStringConverter.SHORT);
066                registered.put(Integer.TYPE, JDKStringConverter.INTEGER);
067                registered.put(Long.TYPE, JDKStringConverter.LONG);
068                registered.put(Float.TYPE, JDKStringConverter.FLOAT);
069                registered.put(Double.TYPE, JDKStringConverter.DOUBLE);
070                registered.put(Character.TYPE, JDKStringConverter.CHARACTER);
071            }
072        }
073    
074        //-----------------------------------------------------------------------
075        /**
076         * Converts the specified object to a {@code String}.
077         * <p>
078         * This uses {@link #findConverter} to provide the converter.
079         * 
080         * @param <T>  the type to convert from
081         * @param object  the object to convert, null returns null
082         * @return the converted string, may be null
083         * @throws RuntimeException (or subclass) if unable to convert
084         */
085        @SuppressWarnings("unchecked")
086        public <T> String convertToString(T object) {
087            if (object == null) {
088                return null;
089            }
090            Class<T> cls = (Class<T>) object.getClass();
091            StringConverter<T> conv = findConverter(cls);
092            return conv.convertToString(object);
093        }
094    
095        /**
096         * Converts the specified object from a {@code String}.
097         * <p>
098         * This uses {@link #findConverter} to provide the converter.
099         * 
100         * @param <T>  the type to convert to
101         * @param cls  the class to convert to, not null
102         * @param str  the string to convert, null returns null
103         * @return the converted object, may be null
104         * @throws RuntimeException (or subclass) if unable to convert
105         */
106        public <T> T convertFromString(Class<T> cls, String str) {
107            if (str == null) {
108                return null;
109            }
110            StringConverter<T> conv = findConverter(cls);
111            return conv.convertFromString(cls, str);
112        }
113    
114        /**
115         * Finds a suitable converter for the type.
116         * <p>
117         * This returns an instance of {@code StringConverter} for the specified class.
118         * This could be useful in other frameworks.
119         * <p>
120         * The search algorithm first searches the registered converters.
121         * It then searches for {@code ToString} and {@code FromString} annotations on the specified class.
122         * Both searches consider superclasses.
123         * 
124         * @param <T>  the type of the converter
125         * @param cls  the class to find a converter for, not null
126         * @return the converter, not null
127         * @throws RuntimeException (or subclass) if no converter found
128         */
129        @SuppressWarnings("unchecked")
130        public <T> StringConverter<T> findConverter(final Class<T> cls) {
131            if (cls == null) {
132                throw new IllegalArgumentException("Class must not be null");
133            }
134            StringConverter<T> conv = (StringConverter<T>) registered.get(cls);
135            if (conv == null) {
136                if (cls == Object.class) {
137                    throw new IllegalStateException("No registered converter found: " + cls);
138                }
139                Class<?> loopCls = cls.getSuperclass();
140                while (loopCls != null && conv == null) {
141                    conv = (StringConverter<T>) registered.get(loopCls);
142                    loopCls = loopCls.getSuperclass();
143                }
144                if (conv == null) {
145                    conv = findAnnotationConverter(cls);
146                    if (conv == null) {
147                        throw new IllegalStateException("No registered converter found: " + cls);
148                    }
149                }
150                registered.putIfAbsent(cls, conv);
151            }
152            return conv;
153        }
154    
155        /**
156         * Finds the conversion method.
157         * 
158         * @param <T>  the type of the converter
159         * @param cls  the class to find a method for, not null
160         * @return the method to call, null means use {@code toString}
161         */
162        private <T> StringConverter<T> findAnnotationConverter(final Class<T> cls) {
163            Method toString = findToStringMethod(cls);
164            if (toString == null) {
165                return null;
166            }
167            Constructor<T> con = findFromStringConstructor(cls);
168            Method fromString = findFromStringMethod(cls, con == null);
169            if (con == null && fromString == null) {
170                throw new IllegalStateException("Class annotated with @ToString but not with @FromString");
171            }
172            if (con != null && fromString != null) {
173                throw new IllegalStateException("Both method and constructor are annotated with @FromString");
174            }
175            if (con != null) {
176                return new MethodConstructorStringConverter<T>(cls, toString, con);
177            } else {
178                return new MethodsStringConverter<T>(cls, toString, fromString);
179            }
180        }
181    
182        /**
183         * Finds the conversion method.
184         * 
185         * @param cls  the class to find a method for, not null
186         * @return the method to call, null means use {@code toString}
187         */
188        private Method findToStringMethod(Class<?> cls) {
189            Method matched = null;
190            Class<?> loopCls = cls;
191            while (loopCls != null && matched == null) {
192                Method[] methods = loopCls.getDeclaredMethods();
193                for (Method method : methods) {
194                    ToString toString = method.getAnnotation(ToString.class);
195                    if (toString != null) {
196                        if (matched != null) {
197                            throw new IllegalStateException("Two methods are annotated with @ToString");
198                        }
199                        matched = method;
200                    }
201                }
202                loopCls = loopCls.getSuperclass();
203            }
204            return matched;
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> Constructor<T> findFromStringConstructor(Class<T> cls) {
215            try {
216                Constructor<T> con = cls.getDeclaredConstructor(String.class);
217                FromString fromString = con.getAnnotation(FromString.class);
218                return fromString != null ? con : null;
219            } catch (NoSuchMethodException ex) {
220                return null;
221            }
222        }
223    
224        /**
225         * Finds the conversion method.
226         * 
227         * @param cls  the class to find a method for, not null
228         * @return the method to call, null means use {@code toString}
229         */
230        private Method findFromStringMethod(Class<?> cls, boolean searchSuperclasses) {
231            Method matched = null;
232            Class<?> loopCls = cls;
233            while (loopCls != null && matched == null) {
234                Method[] methods = loopCls.getDeclaredMethods();
235                for (Method method : methods) {
236                    FromString fromString = method.getAnnotation(FromString.class);
237                    if (fromString != null) {
238                        if (matched != null) {
239                            throw new IllegalStateException("Two methods are annotated with @ToString");
240                        }
241                        matched = method;
242                    }
243                }
244                if (searchSuperclasses == false) {
245                    break;
246                }
247                loopCls = loopCls.getSuperclass();
248            }
249            return matched;
250        }
251    
252        //-----------------------------------------------------------------------
253        /**
254         * Registers a converter for a specific type.
255         * <p>
256         * The converter will be used for subclasses unless overidden.
257         * <p>
258         * No new converters may be registered for the global singleton.
259         * 
260         * @param <T>  the type of the converter
261         * @param cls  the class to register a converter for, not null
262         * @param converter  the String converter, not null
263         */
264        public <T> void register(final Class<T> cls, StringConverter<T> converter) {
265            if (cls == null ) {
266                throw new IllegalArgumentException("Class must not be null");
267            }
268            if (converter == null) {
269                throw new IllegalArgumentException("StringConverter must not be null");
270            }
271            if (this == INSTANCE) {
272                throw new IllegalStateException("Global singleton cannot be extended");
273            }
274            StringConverter<?> old = registered.putIfAbsent(cls, converter);
275            if (old != null) {
276                throw new IllegalStateException("Converter already registered for class: " + cls);
277            }
278        }
279    
280        //-----------------------------------------------------------------------
281        /**
282         * Returns a simple string representation of the object.
283         * 
284         * @return the string representation, never null
285         */
286        @Override
287        public String toString() {
288            return getClass().getSimpleName();
289        }
290    
291    }