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 }