001/* 002 * This is free and unencumbered software released into the public domain. 003 * 004 * Anyone is free to copy, modify, publish, use, compile, sell, or 005 * distribute this software, either in source code form or as a compiled 006 * binary, for any purpose, commercial or non-commercial, and by any 007 * means. 008 * 009 * In jurisdictions that recognize copyright laws, the author or authors 010 * of this software dedicate any and all copyright interest in the 011 * software to the public domain. We make this dedication for the benefit 012 * of the public at large and to the detriment of our heirs and 013 * successors. We intend this dedication to be an overt act of 014 * relinquishment in perpetuity of all present and future rights to this 015 * software under copyright law. 016 * 017 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 018 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 019 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 020 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 021 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 022 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 023 * OTHER DEALINGS IN THE SOFTWARE. 024 * 025 * For more information, please refer to <http://unlicense.org/>. 026 */ 027 028package hm.binkley.util; 029 030import com.google.common.net.HostAndPort; 031import com.google.common.reflect.TypeToken; 032import org.springframework.core.io.DefaultResourceLoader; 033import org.springframework.core.io.Resource; 034import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 035 036import javax.annotation.Nonnull; 037import java.lang.invoke.MethodHandle; 038import java.lang.invoke.MethodType; 039import java.net.InetAddress; 040import java.net.InetSocketAddress; 041import java.net.URI; 042import java.nio.file.Path; 043import java.nio.file.Paths; 044import java.text.SimpleDateFormat; 045import java.util.Date; 046import java.util.HashMap; 047import java.util.List; 048import java.util.Map; 049import java.util.ResourceBundle; 050import java.util.Set; 051import java.util.TimeZone; 052import java.util.function.Function; 053import java.util.regex.Pattern; 054 055import static com.google.common.primitives.Primitives.wrap; 056import static java.lang.invoke.MethodHandles.lookup; 057import static java.lang.invoke.MethodType.methodType; 058import static java.net.InetSocketAddress.createUnresolved; 059import static java.util.Arrays.asList; 060import static java.util.Collections.unmodifiableSet; 061 062/** 063 * {@code Converter} is the opposite of {@code toString()}. It turns strings into objects. Useful, 064 * for example, when obtaining Java value types from XML. 065 * <p> 066 * Supports registered types <em>explicitly</em> with default {@link #register(TypeToken, 067 * Conversion) registered} conversions: <ul><li>{@link Class}</li> <li>{@link InetAddress}</li> 068 * <li>{@link InetSocketAddress}</li> <li>{@link Path}</li> <li>{@link Pattern}</li> <li>{@link 069 * Resource}</li> <li>List of {@link Resource}s</li> <li>{@link ResourceBundle}</li> <li>{@link 070 * TimeZone}</li> <li>{@link URI}</li></ul> 071 * <p> 072 * Supports other types <em>implicitly</em> by looking for a single-argument string factory method 073 * or constructor in this order: <ol><li>Factory method {@code parse(String)}</li> <li>Factory 074 * method {@code valueOf(String)}</li> <li>Factory method {@code of(String)}</li> <li>Constructor 075 * {@code T(String)}</li> <li>Constructor {@code T(CharSequence)}</li></ol> 076 * 077 * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a> 078 * @todo Remove duplication with xprop 079 * @todo Discuss concurrency safety 080 * @todo Do factory methods need to consider CharSequence? 081 * @see #register(Class, Conversion) 082 * @see #register(TypeToken, Conversion) 083 */ 084public final class Converter { 085 private static final MethodType STRING_CTOR = methodType(void.class, String.class); 086 private static final MethodType CHAR_SEQUENCE_CTOR = methodType(void.class, CharSequence.class); 087 private final Map<TypeToken<?>, Conversion<?, ? extends Exception>> conversions 088 = new HashMap<>(); 089 090 { 091 // JDK classes without standardly named String factory methods or String constructors 092 register(Class.class, Class::forName); 093 register(InetAddress.class, InetAddress::getByName); 094 register(InetSocketAddress.class, value -> { 095 final HostAndPort parsed = HostAndPort.fromString(value).requireBracketsForIPv6(); 096 return createUnresolved(parsed.getHostText(), parsed.getPort()); 097 }); 098 register(Path.class, Paths::get); 099 register(Pattern.class, Pattern::compile); 100 register(Resource.class, 101 value -> new DefaultResourceLoader(getClass().getClassLoader()).getResource(value)); 102 register(new TypeToken<List<Resource>>() { 103 }, value -> asList(new PathMatchingResourcePatternResolver(getClass().getClassLoader()) 104 .getResources(value))); 105 register(ResourceBundle.class, ResourceBundle::getBundle); 106 register(TimeZone.class, TimeZone::getTimeZone); 107 register(URI.class, URI::create); 108 } 109 110 /** 111 * Registers an object conversion. Use this for plain types without consideration of generics. 112 * 113 * @param type the class token, never missing 114 * @param factory the converter, never missing 115 * @param <T> the conversion type 116 * 117 * @throws DuplicateConversion if the conversion is already registered 118 */ 119 public <T> void register(@Nonnull final Class<T> type, @Nonnull final Conversion<T, ?> factory) 120 throws DuplicateConversion { 121 register(TypeToken.of(type), factory); 122 } 123 124 /** 125 * Registers an object conversion. Use this for types with generics, for example, collections. 126 * 127 * @param type the Guava type token, never missing 128 * @param factory the converter, never missing 129 * @param <T> the conversion type 130 * 131 * @throws DuplicateConversion if the conversion is already registered 132 */ 133 public <T> void register(@Nonnull final TypeToken<T> type, 134 @Nonnull final Conversion<T, ?> factory) 135 throws DuplicateConversion { 136 if (null != conversions.putIfAbsent(type, factory)) 137 throw new DuplicateConversion(type); 138 } 139 140 /** 141 * Registers a date format pattern for legacy {@code java.util.Date}. By default {@code 142 * Converter} uses the deprecated {@link Date#Date(String)} constructor. 143 * <p> 144 * This method is a convenience and a caution for surprising legacy date parsing. Better is to 145 * use {@link java.time} classes. 146 * 147 * @param pattern the date format pattern, never missing 148 */ 149 public void registerDate(@Nonnull final String pattern) { 150 register(Date.class, value -> new SimpleDateFormat(pattern).parse(value)); 151 } 152 153 /** 154 * Converts the given <var>value</var> into an instance of <var>type</var>. Use this for plain 155 * types without consideraton of generics. 156 * 157 * @param type the target type, never missing 158 * @param value the string to convert, never missing 159 * @param <T> the conversion type 160 * 161 * @return the converted instance of <var>type</var>, never missing 162 * 163 * @throws Exception if conversion fails 164 */ 165 @Nonnull 166 public <T> T convert(@Nonnull final Class<T> type, @Nonnull final String value) 167 throws Exception { 168 return convert(TypeToken.of(wrap(type)), value); 169 } 170 171 /** 172 * Converts the given <var>value</var> into an instance of <var>type</var>. Use this for types 173 * with generis, for example, collections. 174 * 175 * @param type the target type, never missing 176 * @param value the string to convert, never missing 177 * @param <T> the conversion type 178 * 179 * @return the converted instance of <var>type</var>, never missing 180 * 181 * @throws Exception if conversion fails 182 */ 183 @SuppressWarnings("unchecked") 184 @Nonnull 185 public <T> T convert(@Nonnull final TypeToken<T> type, @Nonnull final String value) 186 throws Exception { 187 final Class<? super T> rawType = type.getRawType(); 188 return String.class == rawType ? (T) value : factoryFor(type).convert(value); 189 } 190 191 /** 192 * Get an unmodifiable view of registered conversions. Supported conversions are those implicit 193 * and those registered; registered takes precedence. 194 * 195 * @return the set of registered conversion types, never missing 196 * 197 * @todo What is the best way to expose conversions for inspection/management? 198 */ 199 @Nonnull 200 public Set<TypeToken<?>> registered() { 201 return unmodifiableSet(conversions.keySet()); 202 } 203 204 private <T, E extends Exception> Conversion<T, E> factoryFor(final TypeToken<T> type) { 205 // Java 8 generics inference is brilliant but not perfect, requires the ugly cast 206 return asList((Function<TypeToken<T>, Conversion<T, E>>) this::getRegistered, 207 Converter::parse, Converter::valueOf, Converter::of, Converter::ctor).stream(). 208 map(f -> f.apply(type)). 209 filter(conversion -> null != conversion). 210 findFirst(). 211 orElseThrow(() -> new UnsupportedConversion(type)); 212 } 213 214 @SuppressWarnings("unchecked") 215 private <T, E extends Exception> Conversion<T, E> getRegistered(final TypeToken<T> type) { 216 return (Conversion<T, E>) conversions.get(type); 217 } 218 219 private static <T, E extends Exception> Conversion<T, E> parse(final TypeToken<T> type) 220 throws NoSuchMethodError { 221 return method(type, "parse"); 222 } 223 224 private static <T, E extends Exception> Conversion<T, E> method(final TypeToken<T> type, 225 final String name) { 226 final Class<?> raw = type.getRawType(); 227 // TODO: Parameter type must match exactly 228 try { 229 return thunk(lookup().findStatic(raw, name, methodType(raw, String.class))); 230 } catch (final NoSuchMethodException | IllegalAccessException ignored) { 231 } 232 try { 233 return thunk(lookup().findStatic(raw, name, methodType(raw, CharSequence.class))); 234 } catch (final NoSuchMethodException | IllegalAccessException ignored) { 235 } 236 return null; 237 } 238 239 @SuppressWarnings("unchecked") 240 private static <T, E extends Exception> Conversion<T, E> thunk(final MethodHandle handle) { 241 return value -> { 242 try { 243 return (T) handle.invoke(value); 244 } catch(final Error | RuntimeException e) { 245 throw e; 246 } catch (final Throwable t) { 247 throw (E) t; 248 } 249 }; 250 } 251 252 private static <T, E extends Exception> Conversion<T, E> valueOf(final TypeToken<T> type) { 253 return method(type, "valueOf"); 254 } 255 256 private static <T, E extends Exception> Conversion<T, E> of(final TypeToken<T> type) { 257 return method(type, "of"); 258 } 259 260 private static <T, E extends Exception> Conversion<T, E> ctor(final TypeToken<T> type) { 261 final Class<?> raw = type.getRawType(); 262 // TODO: Parameter type must match exactly 263 try { 264 return thunk(lookup().findConstructor(raw, STRING_CTOR)); 265 } catch (final NoSuchMethodException | IllegalAccessException ignored) { 266 } 267 try { 268 return thunk(lookup().findConstructor(raw, CHAR_SEQUENCE_CTOR)); 269 } catch (final NoSuchMethodException | IllegalAccessException ignored) { 270 } 271 return null; 272 } 273 274 /** 275 * Converts a string property value into a typed object. 276 * 277 * @param <T> the converted type 278 * @param <E> the exception type on failed converstion, use {@code RuntimeException} if none 279 */ 280 @FunctionalInterface 281 public interface Conversion<T, E extends Exception> { 282 /** 283 * Converts the given property <var>value</var> into a typed object. 284 * 285 * @param value the property value, never missing 286 * 287 * @return the typed object 288 * 289 * @throws E if conversion fails 290 */ 291 T convert(@Nonnull final String value) 292 throws E; 293 } 294 295 /** @todo Documentation */ 296 public static class DuplicateConversion 297 extends IllegalArgumentException { 298 private DuplicateConversion(final TypeToken<?> type) { 299 super(type.toString()); 300 } 301 } 302 303 /** @todo Documentation */ 304 public static class UnsupportedConversion 305 extends UnsupportedOperationException { 306 private UnsupportedConversion(final TypeToken<?> type) { 307 super(type.toString()); 308 } 309 } 310}