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 }