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}