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 }