001/*
002  Licensed to the Apache Software Foundation (ASF) under one or more
003  contributor license agreements.  See the NOTICE file distributed with
004  this work for additional information regarding copyright ownership.
005  The ASF licenses this file to You under the Apache License, Version 2.0
006  (the "License"); you may not use this file except in compliance with
007  the License.  You may obtain a copy of the License at
008
009      http://www.apache.org/licenses/LICENSE-2.0
010
011  Unless required by applicable law or agreed to in writing, software
012  distributed under the License is distributed on an "AS IS" BASIS,
013  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014  See the License for the specific language governing permissions and
015  limitations under the License.
016 */
017
018package org.apache.commons.cli;
019
020import java.io.File;
021import java.io.FileInputStream;
022import java.math.BigDecimal;
023import java.math.BigInteger;
024import java.net.URL;
025import java.nio.file.Path;
026import java.util.Date;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Objects;
030
031/**
032 * TypeHandler will handle the pluggable conversion and verification of Option types. It handles the mapping of classes to bot converters and verifiers. It
033 * provides the default conversion and verification methods when converters and verifiers are not explicitly set.
034 * <p>
035 * If Options are serialized and deserialized their converters and verifiers will revert to the defaults defined in this class. To correctly de-serialize
036 * Options with custom converters and/or verifiers, using the default serialization methods, this class should be properly configured with the custom converters
037 * and verifiers for the specific class.
038 * </p>
039 */
040public class TypeHandler {
041
042    /**
043     * The default TypeHandler.
044     */
045    private static final TypeHandler DEFAULT = new TypeHandler();
046
047    /** Value of hex conversion of strings */
048    private static final int HEX_RADIX = 16;
049
050    /**
051     * Returns the class whose name is {@code className}.
052     *
053     * @param className the class name
054     * @return The class if it is found
055     * @throws ParseException if the class could not be found
056     */
057    public static Class<?> createClass(final String className) throws ParseException {
058        return createValue(className, Class.class);
059    }
060
061    /**
062     * Returns the date represented by {@code string}.
063     * <p>
064     * This method is not yet implemented and always throws an {@link UnsupportedOperationException}.
065     * </p>
066     *
067     * @param string the date string
068     * @return The date if {@code string} is a valid date string, otherwise return null.
069     */
070    public static Date createDate(final String string) {
071        return createValueUnchecked(string, Date.class);
072    }
073
074    /**
075     * Creates a default converter map.
076     *
077     * @return a default converter map.
078     * @since 1.7.0
079     */
080    public static Map<Class<?>, Converter<?, ? extends Throwable>> createDefaultMap() {
081        return putDefaultMap(new HashMap<>());
082    }
083
084    /**
085     * Returns the File represented by {@code string}.
086     *
087     * @param string the File location
088     * @return The file represented by {@code string}.
089     */
090    public static File createFile(final String string) {
091        return createValueUnchecked(string, File.class);
092    }
093
094    /**
095     * Creates the File[] represented by {@code string}.
096     *
097     * <p>
098     * This method is not yet implemented and always throws an {@link UnsupportedOperationException}.
099     * </p>
100     *
101     * @param string the paths to the files
102     * @return The File[] represented by {@code string}.
103     * @throws UnsupportedOperationException always
104     * @deprecated with no replacement
105     */
106    @Deprecated // since 1.7.0
107    public static File[] createFiles(final String string) {
108        // to implement/port:
109        // return FileW.findFiles(string);
110        throw new UnsupportedOperationException("Not yet implemented");
111    }
112
113    /**
114     * Creates a number from a String. If a '.' is present, it creates a Double, otherwise a Long.
115     *
116     * @param string the value
117     * @return the number represented by {@code string}
118     * @throws ParseException if {@code string} is not a number
119     */
120    @Deprecated // since 1.7.0
121    public static Number createNumber(final String string) throws ParseException {
122        return createValue(string, Number.class);
123    }
124
125    /**
126     * Creates an Object from the class name and empty constructor.
127     *
128     * @param className the argument value
129     * @return the initialized object
130     * @throws ParseException if the class could not be found or the object could not be created
131     * @deprecated use {@link #createValue(String, Class)}
132     */
133    @Deprecated // since 1.7.0
134    public static Object createObject(final String className) throws ParseException {
135        return createValue(className, Object.class);
136    }
137
138    /**
139     * Creates the URL represented by {@code string}.
140     *
141     * @param string the URL string
142     * @return The URL in {@code string} is well-formed
143     * @throws ParseException if the URL in {@code string} is not well-formed
144     */
145    public static URL createURL(final String string) throws ParseException {
146        return createValue(string, URL.class);
147    }
148
149    /**
150     * Creates the @code Object} of type {@code clazz} with the value of {@code string}.
151     *
152     * @param string the command line value
153     * @param clazz  the class representing the type of argument
154     * @param <T>    type of argument
155     * @return The instance of {@code clazz} initialized with the value of {@code string}.
156     * @throws ParseException if the value creation for the given class threw an exception.
157     */
158    public static <T> T createValue(final String string, final Class<T> clazz) throws ParseException {
159        try {
160            return getDefault().getConverter(clazz).apply(string);
161        } catch (final Throwable e) {
162            throw ParseException.wrap(e);
163        }
164    }
165
166    /**
167     * Creates the {@code Object} of type {@code obj} with the value of {@code string}.
168     *
169     * @param string the command line value
170     * @param obj    the type of argument
171     * @return The instance of {@code obj} initialized with the value of {@code string}.
172     * @throws ParseException if the value creation for the given object type failed
173     * @deprecated use {@link #createValue(String, Class)}
174     */
175    @Deprecated // since 1.7.0
176    public static Object createValue(final String string, final Object obj) throws ParseException {
177        return createValue(string, (Class<?>) obj);
178    }
179
180    /**
181     * Delegates to {@link #createValue(String, Class)} throwing IllegalArgumentException instead of ParseException.
182     *
183     * @param string the command line value
184     * @param clazz  the class representing the type of argument
185     * @param <T>    type of argument
186     * @return The instance of {@code clazz} initialized with the value of {@code string}.
187     * @throws IllegalArgumentException if the value creation for the given class threw an exception.
188     */
189    private static <T> T createValueUnchecked(final String string, final Class<T> clazz) {
190        try {
191            return createValue(string, clazz);
192        } catch (final ParseException e) {
193            throw new IllegalArgumentException(e);
194        }
195    }
196
197    /**
198     * Gets the default TypeHandler.
199     *
200     * @return the default TypeHandler.
201     * @since 1.7.0
202     */
203    public static TypeHandler getDefault() {
204        return DEFAULT;
205    }
206
207    /**
208     * Returns the opened FileInputStream represented by {@code string}.
209     *
210     * @param string the file location
211     * @return The file input stream represented by {@code string}.
212     * @throws ParseException if the file is not exist or not readable
213     * @deprecated use {@link #createValue(String, Class)}
214     */
215    @Deprecated // since 1.7.0
216    public static FileInputStream openFile(final String string) throws ParseException {
217        return createValue(string, FileInputStream.class);
218    }
219
220    private static Map<Class<?>, Converter<?, ? extends Throwable>> putDefaultMap(final Map<Class<?>, Converter<?, ? extends Throwable>> map) {
221        map.put(Object.class, Converter.OBJECT);
222        map.put(Class.class, Converter.CLASS);
223        map.put(Date.class, Converter.DATE);
224        map.put(File.class, Converter.FILE);
225        map.put(Path.class, Converter.PATH);
226        map.put(Number.class, Converter.NUMBER);
227        map.put(URL.class, Converter.URL);
228        map.put(FileInputStream.class, FileInputStream::new);
229        map.put(Long.class, Long::parseLong);
230        map.put(Integer.class, Integer::parseInt);
231        map.put(Short.class, Short::parseShort);
232        map.put(Byte.class, Byte::parseByte);
233        map.put(Character.class, s -> s.startsWith("\\u") ? Character.toChars(Integer.parseInt(s.substring(2), HEX_RADIX))[0] : s.charAt(0));
234        map.put(Double.class, Double::parseDouble);
235        map.put(Float.class, Float::parseFloat);
236        map.put(BigInteger.class, BigInteger::new);
237        map.put(BigDecimal.class, BigDecimal::new);
238        return map;
239    }
240
241    /**
242     * Map of Class to Converter.
243     * <p>
244     * For each entry, that Class' type must match the Converter's first type.
245     * </p>
246     */
247    private final Map<Class<?>, Converter<?, ? extends Throwable>> converterMap;
248
249    /**
250     * Constructs a default initialized instance.
251     */
252    public TypeHandler() {
253        this(createDefaultMap());
254    }
255
256    /**
257     * Constructs a default initialized instance.
258     * <p>
259     * For each entry, that Class' type must match the Converter's first type.
260     * </p>
261     *
262     * @param converterMap The converter map, not null.
263     * @since 1.7.0
264     */
265    public TypeHandler(final Map<Class<?>, Converter<?, ? extends Throwable>> converterMap) {
266        this.converterMap = Objects.requireNonNull(converterMap, "converterMap");
267    }
268
269    /**
270     * Gets the registered converter for the the Class, or {@link Converter#DEFAULT} if absent.
271     *
272     * @param <T>   The Class parameter type.
273     * @param clazz The Class to get the Converter for.
274     * @return the registered converter if any, {@link Converter#DEFAULT} otherwise.
275     * @since 1.7.0
276     */
277    @SuppressWarnings("unchecked") // returned value will have type T because it is fixed by clazz
278    public <T> Converter<T, ?> getConverter(final Class<T> clazz) {
279        return (Converter<T, ?>) converterMap.getOrDefault(clazz, Converter.DEFAULT);
280    }
281
282}