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            }
064        }
065    
066        //-----------------------------------------------------------------------
067        /**
068         * Converts the specified object to a {@code String}.
069         * <p>
070         * This uses {@link #findConverter} to provide the converter.
071         * 
072         * @param <T>  the type to convert from
073         * @param object  the object to convert, null returns null
074         * @return the converted string, may be null
075         * @throws RuntimeException (or subclass) if unable to convert
076         */
077        @SuppressWarnings("unchecked")
078        public <T> String convertToString(T object) {
079            if (object == null) {
080                return null;
081            }
082            Class<T> cls = (Class<T>) object.getClass();
083            StringConverter<T> conv = findConverter(cls);
084            return conv.convertToString(object);
085        }
086    
087        /**
088         * Converts the specified object from a {@code String}.
089         * <p>
090         * This uses {@link #findConverter} to provide the converter.
091         * 
092         * @param <T>  the type to convert to
093         * @param cls  the class to convert to, not null
094         * @param str  the string to convert, null returns null
095         * @return the converted object, may be null
096         * @throws RuntimeException (or subclass) if unable to convert
097         */
098        public <T> T convertFromString(Class<T> cls, String str) {
099            if (str == null) {
100                return null;
101            }
102            StringConverter<T> conv = findConverter(cls);
103            return conv.convertFromString(cls, str);
104        }
105    
106        /**
107         * Finds a suitable converter for the type.
108         * <p>
109         * This returns an instance of {@code StringConverter} for the specified class.
110         * This could be useful in other frameworks.
111         * <p>
112         * The search algorithm first searches the registered converters.
113         * It then searches for {@code ToString} and {@code FromString} annotations on the specified class.
114         * Both searches consider superclasses.
115         * 
116         * @param <T>  the type of the converter
117         * @param cls  the class to find a converter for, not null
118         * @return the converter, not null
119         * @throws RuntimeException (or subclass) if no converter found
120         */
121        @SuppressWarnings("unchecked")
122        public <T> StringConverter<T> findConverter(final Class<T> cls) {
123            if (cls == null) {
124                throw new IllegalArgumentException("Class must not be null");
125            }
126            StringConverter<T> conv = (StringConverter<T>) registered.get(cls);
127            if (conv == null) {
128                if (cls == Object.class) {
129                    throw new IllegalStateException("No registered converter found: " + cls);
130                }
131                Class<?> loopCls = cls.getSuperclass();
132                while (loopCls != Object.class && conv == null) {
133                    conv = (StringConverter<T>) registered.get(loopCls);
134                    loopCls = loopCls.getSuperclass();
135                }
136                if (conv == null) {
137                    conv = findAnnotationConverter(cls);
138                    if (conv == null) {
139                        throw new IllegalStateException("No registered converter found: " + cls);
140                    }
141                }
142                registered.putIfAbsent(cls, conv);
143            }
144            return conv;
145        }
146    
147        /**
148         * Finds the conversion method.
149         * 
150         * @param <T>  the type of the converter
151         * @param cls  the class to find a method for, not null
152         * @return the method to call, null means use {@code toString}
153         */
154        private <T> StringConverter<T> findAnnotationConverter(final Class<T> cls) {
155            Method toString = findToStringMethod(cls);
156            if (toString == null) {
157                return null;
158            }
159            Constructor<T> con = findFromStringConstructor(cls);
160            Method fromString = findFromStringMethod(cls, con == null);
161            if (con == null && fromString == null) {
162                throw new IllegalStateException("Class annotated with @ToString but not with @FromString");
163            }
164            if (con != null && fromString != null) {
165                throw new IllegalStateException("Both method and constructor are annotated with @FromString");
166            }
167            if (con != null) {
168                return new MethodConstructorStringConverter<T>(cls, toString, con);
169            } else {
170                return new MethodsStringConverter<T>(cls, toString, fromString);
171            }
172        }
173    
174        /**
175         * Finds the conversion method.
176         * 
177         * @param cls  the class to find a method for, not null
178         * @return the method to call, null means use {@code toString}
179         */
180        private Method findToStringMethod(Class<?> cls) {
181            Method matched = null;
182            Class<?> loopCls = cls;
183            while (loopCls != Object.class && matched == null) {
184                Method[] methods = loopCls.getDeclaredMethods();
185                for (Method method : methods) {
186                    ToString toString = method.getAnnotation(ToString.class);
187                    if (toString != null) {
188                        if (matched != null) {
189                            throw new IllegalStateException("Two methods are annotated with @ToString");
190                        }
191                        matched = method;
192                    }
193                }
194                loopCls = loopCls.getSuperclass();
195            }
196            return matched;
197        }
198    
199        /**
200         * Finds the conversion method.
201         * 
202         * @param <T>  the type of the converter
203         * @param cls  the class to find a method for, not null
204         * @return the method to call, null means use {@code toString}
205         */
206        private <T> Constructor<T> findFromStringConstructor(Class<T> cls) {
207            try {
208                Constructor<T> con = cls.getDeclaredConstructor(String.class);
209                FromString fromString = con.getAnnotation(FromString.class);
210                return fromString != null ? con : null;
211            } catch (NoSuchMethodException ex) {
212                return null;
213            }
214        }
215    
216        /**
217         * Finds the conversion method.
218         * 
219         * @param cls  the class to find a method for, not null
220         * @return the method to call, null means use {@code toString}
221         */
222        private Method findFromStringMethod(Class<?> cls, boolean searchSuperclasses) {
223            Method matched = null;
224            Class<?> loopCls = cls;
225            while (loopCls != Object.class && matched == null) {
226                Method[] methods = loopCls.getDeclaredMethods();
227                for (Method method : methods) {
228                    FromString fromString = method.getAnnotation(FromString.class);
229                    if (fromString != null) {
230                        if (matched != null) {
231                            throw new IllegalStateException("Two methods are annotated with @ToString");
232                        }
233                        matched = method;
234                    }
235                }
236                if (searchSuperclasses == false) {
237                    break;
238                }
239                loopCls = loopCls.getSuperclass();
240            }
241            return matched;
242        }
243    
244        //-----------------------------------------------------------------------
245        /**
246         * Registers a converter for a specific type.
247         * <p>
248         * The converter will be used for subclasses unless overidden.
249         * <p>
250         * No new converters may be registered for the global singleton.
251         * 
252         * @param <T>  the type of the converter
253         * @param cls  the class to register a converter for, not null
254         * @param converter  the String converter, not null
255         */
256        public <T> void register(final Class<T> cls, StringConverter<T> converter) {
257            if (cls == null ) {
258                throw new IllegalArgumentException("Class must not be null");
259            }
260            if (converter == null) {
261                throw new IllegalArgumentException("StringConverter must not be null");
262            }
263            if (this == INSTANCE) {
264                throw new IllegalStateException("Global singleton cannot be extended");
265            }
266            StringConverter<?> old = registered.putIfAbsent(cls, converter);
267            if (old != null) {
268                throw new IllegalStateException("Converter already registered for class: " + cls);
269            }
270        }
271    
272        //-----------------------------------------------------------------------
273        /**
274         * Returns a simple string representation of the object.
275         * 
276         * @return the string representation, never null
277         */
278        @Override
279        public String toString() {
280            return getClass().getSimpleName();
281        }
282    
283    }