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 * <p>
050 * The convert instance is mutable in a thread-safe manner.
051 * Converters may be altered at any time, including the JDK converters.
052 * It is strongly recommended to only alter the converters before performing
053 * actual conversions.
054 */
055 public StringConvert() {
056 this(true);
057 }
058
059 /**
060 * Creates a new conversion manager.
061 * <p>
062 * The convert instance is mutable in a thread-safe manner.
063 * Converters may be altered at any time, including the JDK converters.
064 * It is strongly recommended to only alter the converters before performing
065 * actual conversions.
066 *
067 * @param includeJdkConverters true to include the JDK converters
068 */
069 public StringConvert(boolean includeJdkConverters) {
070 if (includeJdkConverters) {
071 for (JDKStringConverter conv : JDKStringConverter.values()) {
072 registered.put(conv.getType(), conv);
073 }
074 registered.put(Boolean.TYPE, JDKStringConverter.BOOLEAN);
075 registered.put(Byte.TYPE, JDKStringConverter.BYTE);
076 registered.put(Short.TYPE, JDKStringConverter.SHORT);
077 registered.put(Integer.TYPE, JDKStringConverter.INTEGER);
078 registered.put(Long.TYPE, JDKStringConverter.LONG);
079 registered.put(Float.TYPE, JDKStringConverter.FLOAT);
080 registered.put(Double.TYPE, JDKStringConverter.DOUBLE);
081 registered.put(Character.TYPE, JDKStringConverter.CHARACTER);
082 // JDK 1.8 classes
083 tryRegister("java.time.Instant", "parse");
084 tryRegister("java.time.Duration", "parse");
085 tryRegister("java.time.LocalDate", "parse");
086 tryRegister("java.time.LocalTime", "parse");
087 tryRegister("java.time.LocalDateTime", "parse");
088 tryRegister("java.time.OffsetTime", "parse");
089 tryRegister("java.time.OffsetDateTime", "parse");
090 tryRegister("java.time.ZonedDateTime", "parse");
091 tryRegister("java.time.Year", "parse");
092 tryRegister("java.time.YearMonth", "parse");
093 tryRegister("java.time.MonthDay", "parse");
094 tryRegister("java.time.Period", "parse");
095 tryRegister("java.time.ZoneOffset", "of");
096 tryRegister("java.time.ZoneId", "of");
097 // ThreeTen backport classes
098 tryRegister("org.threeten.bp.Instant", "parse");
099 tryRegister("org.threeten.bp.Duration", "parse");
100 tryRegister("org.threeten.bp.LocalDate", "parse");
101 tryRegister("org.threeten.bp.LocalTime", "parse");
102 tryRegister("org.threeten.bp.LocalDateTime", "parse");
103 tryRegister("org.threeten.bp.OffsetTime", "parse");
104 tryRegister("org.threeten.bp.OffsetDateTime", "parse");
105 tryRegister("org.threeten.bp.ZonedDateTime", "parse");
106 tryRegister("org.threeten.bp.Year", "parse");
107 tryRegister("org.threeten.bp.YearMonth", "parse");
108 tryRegister("org.threeten.bp.MonthDay", "parse");
109 tryRegister("org.threeten.bp.Period", "parse");
110 tryRegister("org.threeten.bp.ZoneOffset", "of");
111 tryRegister("org.threeten.bp.ZoneId", "of");
112 // Old ThreeTen/JSR-310 classes v0.6.3 and beyond
113 tryRegister("javax.time.Instant", "parse");
114 tryRegister("javax.time.Duration", "parse");
115 tryRegister("javax.time.calendar.LocalDate", "parse");
116 tryRegister("javax.time.calendar.LocalTime", "parse");
117 tryRegister("javax.time.calendar.LocalDateTime", "parse");
118 tryRegister("javax.time.calendar.OffsetDate", "parse");
119 tryRegister("javax.time.calendar.OffsetTime", "parse");
120 tryRegister("javax.time.calendar.OffsetDateTime", "parse");
121 tryRegister("javax.time.calendar.ZonedDateTime", "parse");
122 tryRegister("javax.time.calendar.Year", "parse");
123 tryRegister("javax.time.calendar.YearMonth", "parse");
124 tryRegister("javax.time.calendar.MonthDay", "parse");
125 tryRegister("javax.time.calendar.Period", "parse");
126 tryRegister("javax.time.calendar.ZoneOffset", "of");
127 tryRegister("javax.time.calendar.ZoneId", "of");
128 tryRegister("javax.time.calendar.TimeZone", "of");
129 }
130 }
131
132 /**
133 * Tries to register a class using the standard toString/parse pattern.
134 *
135 * @param className the class name, not null
136 */
137 private void tryRegister(String className, String fromStringMethodName) {
138 try {
139 Class<?> cls = getClass().getClassLoader().loadClass(className);
140 registerMethods(cls, "toString", fromStringMethodName);
141 } catch (Exception ex) {
142 // ignore
143 }
144 }
145
146 //-----------------------------------------------------------------------
147 /**
148 * Converts the specified object to a {@code String}.
149 * <p>
150 * This uses {@link #findConverter} to provide the converter.
151 *
152 * @param <T> the type to convert from
153 * @param object the object to convert, null returns null
154 * @return the converted string, may be null
155 * @throws RuntimeException (or subclass) if unable to convert
156 */
157 @SuppressWarnings("unchecked")
158 public <T> String convertToString(T object) {
159 if (object == null) {
160 return null;
161 }
162 Class<T> cls = (Class<T>) object.getClass();
163 StringConverter<T> conv = findConverter(cls);
164 return conv.convertToString(object);
165 }
166
167 /**
168 * Converts the specified object to a {@code String}.
169 * <p>
170 * This uses {@link #findConverter} to provide the converter.
171 * The class can be provided to select a more specific converter.
172 *
173 * @param <T> the type to convert from
174 * @param cls the class to convert from, not null
175 * @param object the object to convert, null returns null
176 * @return the converted string, may be null
177 * @throws RuntimeException (or subclass) if unable to convert
178 */
179 public <T> String convertToString(Class<T> cls, T object) {
180 if (object == null) {
181 return null;
182 }
183 StringConverter<T> conv = findConverter(cls);
184 return conv.convertToString(object);
185 }
186
187 /**
188 * Converts the specified object from a {@code String}.
189 * <p>
190 * This uses {@link #findConverter} to provide the converter.
191 *
192 * @param <T> the type to convert to
193 * @param cls the class to convert to, not null
194 * @param str the string to convert, null returns null
195 * @return the converted object, may be null
196 * @throws RuntimeException (or subclass) if unable to convert
197 */
198 public <T> T convertFromString(Class<T> cls, String str) {
199 if (str == null) {
200 return null;
201 }
202 StringConverter<T> conv = findConverter(cls);
203 return conv.convertFromString(cls, str);
204 }
205
206 /**
207 * Finds a suitable converter for the type.
208 * <p>
209 * This returns an instance of {@code StringConverter} for the specified class.
210 * This could be useful in other frameworks.
211 * <p>
212 * The search algorithm first searches the registered converters.
213 * It then searches for {@code ToString} and {@code FromString} annotations on the specified class.
214 * Both searches consider superclasses, but not interfaces.
215 *
216 * @param <T> the type of the converter
217 * @param cls the class to find a converter for, not null
218 * @return the converter, not null
219 * @throws RuntimeException (or subclass) if no converter found
220 */
221 @SuppressWarnings("unchecked")
222 public <T> StringConverter<T> findConverter(final Class<T> cls) {
223 if (cls == null) {
224 throw new IllegalArgumentException("Class must not be null");
225 }
226 StringConverter<T> conv = (StringConverter<T>) registered.get(cls);
227 if (conv == null) {
228 if (cls == Object.class) {
229 throw new IllegalStateException("No registered converter found: " + cls);
230 }
231 Class<?> loopCls = cls.getSuperclass();
232 while (loopCls != null && conv == null) {
233 conv = (StringConverter<T>) registered.get(loopCls);
234 loopCls = loopCls.getSuperclass();
235 }
236 if (conv == null) {
237 conv = findAnnotationConverter(cls);
238 if (conv == null) {
239 throw new IllegalStateException("No registered converter found: " + cls);
240 }
241 }
242 registered.putIfAbsent(cls, conv);
243 }
244 return conv;
245 }
246
247 /**
248 * Finds the conversion method.
249 *
250 * @param <T> the type of the converter
251 * @param cls the class to find a method for, not null
252 * @return the method to call, null means use {@code toString}
253 */
254 private <T> StringConverter<T> findAnnotationConverter(final Class<T> cls) {
255 Method toString = findToStringMethod(cls);
256 if (toString == null) {
257 return null;
258 }
259 Constructor<T> con = findFromStringConstructor(cls);
260 Method fromString = findFromStringMethod(cls, con == null);
261 if (con == null && fromString == null) {
262 throw new IllegalStateException("Class annotated with @ToString but not with @FromString");
263 }
264 if (con != null && fromString != null) {
265 throw new IllegalStateException("Both method and constructor are annotated with @FromString");
266 }
267 if (con != null) {
268 return new MethodConstructorStringConverter<T>(cls, toString, con);
269 } else {
270 return new MethodsStringConverter<T>(cls, toString, fromString);
271 }
272 }
273
274 /**
275 * Finds the conversion method.
276 *
277 * @param cls the class to find a method for, not null
278 * @return the method to call, null means use {@code toString}
279 */
280 private Method findToStringMethod(Class<?> cls) {
281 Method matched = null;
282 Class<?> loopCls = cls;
283 while (loopCls != null && matched == null) {
284 Method[] methods = loopCls.getDeclaredMethods();
285 for (Method method : methods) {
286 ToString toString = method.getAnnotation(ToString.class);
287 if (toString != null) {
288 if (matched != null) {
289 throw new IllegalStateException("Two methods are annotated with @ToString");
290 }
291 matched = method;
292 }
293 }
294 loopCls = loopCls.getSuperclass();
295 }
296 return matched;
297 }
298
299 /**
300 * Finds the conversion method.
301 *
302 * @param <T> the type of the converter
303 * @param cls the class to find a method for, not null
304 * @return the method to call, null means use {@code toString}
305 */
306 private <T> Constructor<T> findFromStringConstructor(Class<T> cls) {
307 Constructor<T> con;
308 try {
309 con = cls.getDeclaredConstructor(String.class);
310 } catch (NoSuchMethodException ex) {
311 try {
312 con = cls.getDeclaredConstructor(CharSequence.class);
313 } catch (NoSuchMethodException ex2) {
314 return null;
315 }
316 }
317 FromString fromString = con.getAnnotation(FromString.class);
318 return fromString != null ? con : null;
319 }
320
321 /**
322 * Finds the conversion method.
323 *
324 * @param cls the class to find a method for, not null
325 * @return the method to call, null means use {@code toString}
326 */
327 private Method findFromStringMethod(Class<?> cls, boolean searchSuperclasses) {
328 Method matched = null;
329 Class<?> loopCls = cls;
330 while (loopCls != null && matched == null) {
331 Method[] methods = loopCls.getDeclaredMethods();
332 for (Method method : methods) {
333 FromString fromString = method.getAnnotation(FromString.class);
334 if (fromString != null) {
335 if (matched != null) {
336 throw new IllegalStateException("Two methods are annotated with @ToString");
337 }
338 matched = method;
339 }
340 }
341 if (searchSuperclasses == false) {
342 break;
343 }
344 loopCls = loopCls.getSuperclass();
345 }
346 return matched;
347 }
348
349 //-----------------------------------------------------------------------
350 /**
351 * Registers a converter for a specific type.
352 * <p>
353 * The converter will be used for subclasses unless overidden.
354 * <p>
355 * No new converters may be registered for the global singleton.
356 *
357 * @param <T> the type of the converter
358 * @param cls the class to register a converter for, not null
359 * @param converter the String converter, not null
360 * @throws IllegalArgumentException if the class or converter are null
361 * @throws IllegalStateException if trying to alter the global singleton
362 */
363 public <T> void register(final Class<T> cls, StringConverter<T> converter) {
364 if (cls == null ) {
365 throw new IllegalArgumentException("Class must not be null");
366 }
367 if (converter == null) {
368 throw new IllegalArgumentException("StringConverter must not be null");
369 }
370 if (this == INSTANCE) {
371 throw new IllegalStateException("Global singleton cannot be extended");
372 }
373 registered.put(cls, converter);
374 }
375
376 /**
377 * Registers a converter for a specific type using two separate converters.
378 * <p>
379 * This method registers a converter for the specified class.
380 * It is primarily intended for use with JDK 1.8 method references or lambdas:
381 * <pre>
382 * sc.register(Distance.class, Distance::toString, Distance::parse);
383 * </pre>
384 * The converter will be used for subclasses unless overidden.
385 * <p>
386 * No new converters may be registered for the global singleton.
387 *
388 * @param <T> the type of the converter
389 * @param cls the class to register a converter for, not null
390 * @param toString the to String converter, typically a method reference, not null
391 * @param fromString the from String converter, typically a method reference, not null
392 * @throws IllegalArgumentException if the class or converter are null
393 * @throws IllegalStateException if trying to alter the global singleton
394 * @since 1.3
395 */
396 public <T> void register(final Class<T> cls, final ToStringConverter<T> toString, final FromStringConverter<T> fromString) {
397 if (fromString == null || toString == null) {
398 throw new IllegalArgumentException("Converters must not be null");
399 }
400 register(cls, new StringConverter<T>() {
401 public String convertToString(T object) {
402 return toString.convertToString(object);
403 }
404 public T convertFromString(Class<? extends T> cls, String str) {
405 return fromString.convertFromString(cls, str);
406 }
407 });
408 }
409
410 /**
411 * Registers a converter for a specific type by method names.
412 * <p>
413 * This method allows the converter to be used when the target class cannot have annotations added.
414 * The two method names must obey the same rules as defined by the annotations
415 * {@link ToString} and {@link FromString}.
416 * The converter will be used for subclasses unless overidden.
417 * <p>
418 * No new converters may be registered for the global singleton.
419 * <p>
420 * For example, {@code convert.registerMethods(Distance.class, "toString", "parse");}
421 *
422 * @param <T> the type of the converter
423 * @param cls the class to register a converter for, not null
424 * @param toStringMethodName the name of the method converting to a string, not null
425 * @param fromStringMethodName the name of the method converting from a string, not null
426 * @throws IllegalArgumentException if the class or method name are null or invalid
427 * @throws IllegalStateException if trying to alter the global singleton
428 */
429 public <T> void registerMethods(final Class<T> cls, String toStringMethodName, String fromStringMethodName) {
430 if (cls == null ) {
431 throw new IllegalArgumentException("Class must not be null");
432 }
433 if (toStringMethodName == null || fromStringMethodName == null) {
434 throw new IllegalArgumentException("Method names must not be null");
435 }
436 if (this == INSTANCE) {
437 throw new IllegalStateException("Global singleton cannot be extended");
438 }
439 Method toString = findToStringMethod(cls, toStringMethodName);
440 Method fromString = findFromStringMethod(cls, fromStringMethodName);
441 MethodsStringConverter<T> converter = new MethodsStringConverter<T>(cls, toString, fromString);
442 registered.putIfAbsent(cls, converter);
443 }
444
445 /**
446 * Registers a converter for a specific type by method and constructor.
447 * <p>
448 * This method allows the converter to be used when the target class cannot have annotations added.
449 * The two method name and constructor must obey the same rules as defined by the annotations
450 * {@link ToString} and {@link FromString}.
451 * The converter will be used for subclasses unless overidden.
452 * <p>
453 * No new converters may be registered for the global singleton.
454 * <p>
455 * For example, {@code convert.registerMethodConstructor(Distance.class, "toString");}
456 *
457 * @param <T> the type of the converter
458 * @param cls the class to register a converter for, not null
459 * @param toStringMethodName the name of the method converting to a string, not null
460 * @throws IllegalArgumentException if the class or method name are null or invalid
461 * @throws IllegalStateException if trying to alter the global singleton
462 */
463 public <T> void registerMethodConstructor(final Class<T> cls, String toStringMethodName) {
464 if (cls == null ) {
465 throw new IllegalArgumentException("Class must not be null");
466 }
467 if (toStringMethodName == null) {
468 throw new IllegalArgumentException("Method name must not be null");
469 }
470 if (this == INSTANCE) {
471 throw new IllegalStateException("Global singleton cannot be extended");
472 }
473 Method toString = findToStringMethod(cls, toStringMethodName);
474 Constructor<T> fromString = findFromStringConstructorByType(cls);
475 MethodConstructorStringConverter<T> converter = new MethodConstructorStringConverter<T>(cls, toString, fromString);
476 registered.putIfAbsent(cls, converter);
477 }
478
479 /**
480 * Finds the conversion method.
481 *
482 * @param cls the class to find a method for, not null
483 * @param methodName the name of the method to find, not null
484 * @return the method to call, null means use {@code toString}
485 */
486 private Method findToStringMethod(Class<?> cls, String methodName) {
487 Method m;
488 try {
489 m = cls.getMethod(methodName);
490 } catch (NoSuchMethodException ex) {
491 throw new IllegalArgumentException(ex);
492 }
493 if (Modifier.isStatic(m.getModifiers())) {
494 throw new IllegalArgumentException("Method must not be static: " + methodName);
495 }
496 return m;
497 }
498
499 /**
500 * Finds the conversion method.
501 *
502 * @param cls the class to find a method for, not null
503 * @param methodName the name of the method to find, not null
504 * @return the method to call, null means use {@code toString}
505 */
506 private Method findFromStringMethod(Class<?> cls, String methodName) {
507 Method m;
508 try {
509 m = cls.getMethod(methodName, String.class);
510 } catch (NoSuchMethodException ex) {
511 try {
512 m = cls.getMethod(methodName, CharSequence.class);
513 } catch (NoSuchMethodException ex2) {
514 throw new IllegalArgumentException("Method not found", ex2);
515 }
516 }
517 if (Modifier.isStatic(m.getModifiers()) == false) {
518 throw new IllegalArgumentException("Method must be static: " + methodName);
519 }
520 return m;
521 }
522
523 /**
524 * Finds the conversion method.
525 *
526 * @param <T> the type of the converter
527 * @param cls the class to find a method for, not null
528 * @return the method to call, null means use {@code toString}
529 */
530 private <T> Constructor<T> findFromStringConstructorByType(Class<T> cls) {
531 try {
532 return cls.getDeclaredConstructor(String.class);
533 } catch (NoSuchMethodException ex) {
534 try {
535 return cls.getDeclaredConstructor(CharSequence.class);
536 } catch (NoSuchMethodException ex2) {
537 throw new IllegalArgumentException("Constructor not found", ex2);
538 }
539 }
540 }
541
542 //-----------------------------------------------------------------------
543 /**
544 * Returns a simple string representation of the object.
545 *
546 * @return the string representation, never null
547 */
548 @Override
549 public String toString() {
550 return getClass().getSimpleName();
551 }
552
553 }