You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2024/01/29 13:42:25 UTC

(commons-cli) branch master updated: Add and use a Converter interface and implementations without using BeanUtils (#216)

This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-cli.git


The following commit(s) were added to refs/heads/master by this push:
     new 9256a4d  Add and use a Converter interface and implementations without using BeanUtils (#216)
9256a4d is described below

commit 9256a4dde44adf3d7e96373ecd018edbe89eb122
Author: Claude Warren <cl...@xenei.com>
AuthorDate: Mon Jan 29 14:42:20 2024 +0100

    Add and use a Converter interface and implementations without using BeanUtils (#216)
    
    * Fixes for CLI-321
    
    Adds BeanUtils dependency to POM and implements TypeHandler using the BeanUtils classes.
    
    Handles all methods that were in the original TypeHandler as well as other default classes provided by BeanUtils.
    Test updated to show proper parsing of types that previously were not implemented.
    
    Includes new cli.converters package that may be better handled by BeanUtils as it moves forward to support FunctionalInterfaces.
    
    * Fixed issues with date parsing
    
    * Switched to ConvertUtilsBean2 for conversions.
    
    Added getParsedOptionValues methods with default values.
    Fixed som javadoc,
    
    * Implementation with tests
    
    * fixed checkstyle issues
    
    * fixed spotbugs error & some javadoc
    
    * Updated spotbugs to 4.8.2.0
    
    * added javadocs and  tests
    
    * fixed breaking changes and javadoc
    
    * added since annotation to Converter and Verifier
    
    * Added an Enum Validator implementation
    
    * fixed formatting issues
    
    * fixed checkstyle error
    
    * Converted Verifier to Predicate<String>
    
    Verifier interface became static class to hold common Predicate<String> instances for verification.
    Moved EnumVerifier into Verifier as a static method.
    
    All references to Verifier as a data type chagned to Predicate<String>.
    
    * Moved Converter, Verifier and their respective tests to base cli package.
    
    * Removed verifier management and rebased to master
    
    * moved param <T> tags
    
    * updated numberic tests
    
    * Updated documentation, added Supplier<String> as a default provider for getOptionValue
    
    * fixed checkstyle issues
    
    * Added Path to TypeHandler and updated documentation
    
    * removed verifier
    
    * removed verifier
    
    * updated javadoc and site documentation
---
 pom.xml                                            |   6 +
 .../java/org/apache/commons/cli/CommandLine.java   | 109 +++++++++-
 .../java/org/apache/commons/cli/Converter.java     |  78 +++++++
 src/main/java/org/apache/commons/cli/Option.java   |  41 +++-
 .../java/org/apache/commons/cli/OptionBuilder.java |   2 +
 .../org/apache/commons/cli/ParseException.java     |  31 +++
 .../apache/commons/cli/PatternOptionBuilder.java   |  38 +++-
 .../java/org/apache/commons/cli/TypeHandler.java   | 207 ++++++++++++-------
 src/site/xdoc/usage.xml                            |  75 +++++++
 .../org/apache/commons/cli/ConverterTests.java     | 118 +++++++++++
 .../java/org/apache/commons/cli/OptionTest.java    |  41 ++++
 .../java/org/apache/commons/cli/OptionsTest.java   |  27 ++-
 .../commons/cli/PatternOptionBuilderTest.java      |  19 +-
 .../org/apache/commons/cli/TypeHandlerTest.java    | 225 ++++++++++++++-------
 .../java/org/apache/commons/cli/bug/BugsTest.java  |   2 +-
 15 files changed, 837 insertions(+), 182 deletions(-)

diff --git a/pom.xml b/pom.xml
index 411f7fc..ce88df2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -192,6 +192,11 @@
       <artifactId>junit-vintage-engine</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <properties>
@@ -246,6 +251,7 @@
         <configuration>
           <excludeFilterFile>${basedir}/src/conf/spotbugs-exclude-filter.xml</excludeFilterFile>
         </configuration>
+        <version>4.8.2.0</version>
       </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
diff --git a/src/main/java/org/apache/commons/cli/CommandLine.java b/src/main/java/org/apache/commons/cli/CommandLine.java
index a91891f..6570b82 100644
--- a/src/main/java/org/apache/commons/cli/CommandLine.java
+++ b/src/main/java/org/apache/commons/cli/CommandLine.java
@@ -25,6 +25,7 @@ import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Properties;
+import java.util.function.Supplier;
 
 /**
  * Represents list of arguments parsed against a {@link Options} descriptor.
@@ -254,6 +255,18 @@ public class CommandLine implements Serializable {
      * @return Value of the argument if option is set, and has an argument, otherwise {@code defaultValue}.
      */
     public String getOptionValue(final char opt, final String defaultValue) {
+        return getOptionValue(String.valueOf(opt), () -> defaultValue);
+    }
+
+    /**
+     * Gets the argument, if any, of an option.
+     *
+     * @param opt character name of the option
+     * @param defaultValue is a supplier for the default value to be returned if the option is not specified.
+     * @return Value of the argument if option is set, and has an argument, otherwise {@code defaultValue}.
+     * @since 1.7.0
+     */
+    public String getOptionValue(final char opt, final Supplier<String> defaultValue) {
         return getOptionValue(String.valueOf(opt), defaultValue);
     }
 
@@ -281,10 +294,22 @@ public class CommandLine implements Serializable {
      * @since 1.5.0
      */
     public String getOptionValue(final Option option, final String defaultValue) {
-        final String answer = getOptionValue(option);
-        return answer != null ? answer : defaultValue;
+        return getOptionValue(option, () -> defaultValue);
     }
 
+    /**
+     * Gets the first argument, if any, of an option.
+     *
+     * @param option name of the option.
+     * @param defaultValue is a supplier for the default value to be returned if the option is not specified.
+     * @return Value of the argument if option is set, and has an argument, otherwise {@code defaultValue}.
+     * @since 1.7.0
+     */
+    public String getOptionValue(final Option option, final Supplier<String> defaultValue) {
+        final String answer = getOptionValue(option);
+        return answer != null ? answer : defaultValue.get();
+    }
+    
     /**
      * Gets the first argument, if any, of this option.
      *
@@ -303,9 +328,22 @@ public class CommandLine implements Serializable {
      * @return Value of the argument if option is set, and has an argument, otherwise {@code defaultValue}.
      */
     public String getOptionValue(final String opt, final String defaultValue) {
+        return getOptionValue(resolveOption(opt), () -> defaultValue);
+    }
+
+    /**
+     * Gets the first argument, if any, of an option.
+     *
+     * @param opt name of the option.
+     * @param defaultValue is a supplier for the default value to be returned if the option is not specified.
+     * @return Value of the argument if option is set, and has an argument, otherwise {@code defaultValue}.
+     * @since 1.7.0
+     */
+    public String getOptionValue(final String opt, final Supplier<String> defaultValue) {
         return getOptionValue(resolveOption(opt), defaultValue);
     }
 
+    
     /**
      * Gets the array of values, if any, of an option.
      *
@@ -349,12 +387,13 @@ public class CommandLine implements Serializable {
      * Gets a version of this {@code Option} converted to a particular type.
      *
      * @param opt the name of the option.
+     * @param <T> The return type for the method.
      * @return the value parsed into a particular object.
      * @throws ParseException if there are problems turning the option value into the desired type
      * @see PatternOptionBuilder
      * @since 1.5.0
      */
-    public Object getParsedOptionValue(final char opt) throws ParseException {
+    public <T> T getParsedOptionValue(final char opt) throws ParseException {
         return getParsedOptionValue(String.valueOf(opt));
     }
 
@@ -362,33 +401,83 @@ public class CommandLine implements Serializable {
      * Gets a version of this {@code Option} converted to a particular type.
      *
      * @param option the name of the option.
+     * @param <T> The return type for the method.
      * @return the value parsed into a particular object.
      * @throws ParseException if there are problems turning the option value into the desired type
      * @see PatternOptionBuilder
      * @since 1.5.0
      */
-    public Object getParsedOptionValue(final Option option) throws ParseException {
+    public <T> T getParsedOptionValue(final Option option) throws ParseException {
+        return  getParsedOptionValue(option, null);
+    }
+
+    /**
+     * Gets a version of this {@code Option} converted to a particular type.
+     *
+     * @param opt the name of the option.
+     * @param <T> The return type for the method.
+     * @return the value parsed into a particular object.
+     * @throws ParseException if there are problems turning the option value into the desired type
+     * @see PatternOptionBuilder
+     * @since 1.2
+     */
+    public <T> T getParsedOptionValue(final String opt) throws ParseException {
+        return getParsedOptionValue(resolveOption(opt));
+    }
+    
+    /**
+     * Gets a version of this {@code Option} converted to a particular type.
+     *
+     * @param opt the name of the option.
+     * @param defaultValue the default value to return if opt is not set.
+     * @param <T> The return type for the method.
+     * @return the value parsed into a particular object.
+     * @throws ParseException if there are problems turning the option value into the desired type
+     * @see PatternOptionBuilder
+     * @since 1.7.0
+     */
+    public <T> T getParsedOptionValue(final char opt, final T defaultValue) throws ParseException {
+        return getParsedOptionValue(String.valueOf(opt), defaultValue);
+    }
+
+    /**
+     * Gets a version of this {@code Option} converted to a particular type.
+     *
+     * @param option the name of the option.
+     * @param defaultValue the default value to return if opt is not set.
+     * @param <T> The return type for the method.
+     * @return the value parsed into a particular object.
+     * @throws ParseException if there are problems turning the option value into the desired type
+     * @see PatternOptionBuilder
+     * @since 1.7.0
+     */
+    @SuppressWarnings("unchecked")
+    public <T> T getParsedOptionValue(final Option option, final T defaultValue) throws ParseException {
         if (option == null) {
             return null;
         }
         final String res = getOptionValue(option);
-        if (res == null) {
-            return null;
+
+        try {
+            return res == null ? defaultValue : (T) option.getConverter().apply(res);
+        } catch (Exception e) {
+            throw ParseException.wrap(e);
         }
-        return TypeHandler.createValue(res, option.getType());
     }
 
     /**
      * Gets a version of this {@code Option} converted to a particular type.
      *
      * @param opt the name of the option.
+     * @param defaultValue the default value to return if opt is not set.
+     * @param <T> The return type for the method.
      * @return the value parsed into a particular object.
      * @throws ParseException if there are problems turning the option value into the desired type
      * @see PatternOptionBuilder
-     * @since 1.2
+     * @since 1.7.0
      */
-    public Object getParsedOptionValue(final String opt) throws ParseException {
-        return getParsedOptionValue(resolveOption(opt));
+    public <T> T getParsedOptionValue(final String opt, final T defaultValue) throws ParseException {
+        return getParsedOptionValue(resolveOption(opt), defaultValue);
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/cli/Converter.java b/src/main/java/org/apache/commons/cli/Converter.java
new file mode 100644
index 0000000..3ac04a1
--- /dev/null
+++ b/src/main/java/org/apache/commons/cli/Converter.java
@@ -0,0 +1,78 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+package org.apache.commons.cli;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Path;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * The definition of the functional interface to call when doing a conversion.
+ * Like {@code Function<String,T>} but can throw an Exception.
+ *
+ * @param <T> The return type for the function.
+ * @since 1.7.0
+ */
+@FunctionalInterface
+public interface Converter<T> {
+
+    /** The default converter. Does nothing. */
+    Converter<?> DEFAULT = s -> s;
+
+    /** Class name converter. Calls {@code Class.forName}. */
+    Converter<Class<?>> CLASS = s -> Class.forName(s);
+
+    /** File name converter. Calls @{code new File(s)} */
+    Converter<File> FILE = s -> new File(s);
+
+    /** Path converter. Calls @{code new Path(s)} */
+    Converter<Path> PATH = s -> new File(s).toPath();
+
+    /**
+     * Number converter. Converts to a Double if a decimal point ('.') is in the
+     * string or a Long otherwise.
+     */
+    Converter<Number> NUMBER = s -> {
+        if (s.indexOf('.') != -1) {
+            return Double.valueOf(s);
+        }
+        return Long.valueOf(s);
+    };
+
+    /**
+     * Converts a class name to an instance of the class. Uses the Class converter
+     * to find the class and then call the default constructor.
+     * @see #CLASS
+     */
+    Converter<Object> OBJECT = s -> CLASS.apply(s).getConstructor().newInstance();
+
+    /** Creates a URL. Calls {@code new URL(s)}. */
+    Converter<URL> URL = s -> new URL(s);
+
+    /** Converts to a date using the format string Form "EEE MMM dd HH:mm:ss zzz yyyy". */
+    Converter<Date> DATE = s -> new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy").parse(s);
+
+    /**
+     * Applies the conversion function to the String argument.
+     * @param  str       the String to convert
+     * @return           the Object from the conversion.
+     * @throws Exception on error.
+     */
+    T apply(String str) throws Exception;
+}
diff --git a/src/main/java/org/apache/commons/cli/Option.java b/src/main/java/org/apache/commons/cli/Option.java
index dd2623d..6ffc0c5 100644
--- a/src/main/java/org/apache/commons/cli/Option.java
+++ b/src/main/java/org/apache/commons/cli/Option.java
@@ -82,6 +82,9 @@ public class Option implements Cloneable, Serializable {
 
         /** The character that is the value separator */
         private char valueSeparator;
+        
+        /** The converter to convert to type **/
+        private Converter<?> converter;
 
         /**
          * Constructs a new {@code Builder} with the minimum required parameters for an {@code Option} instance.
@@ -270,6 +273,19 @@ public class Option implements Cloneable, Serializable {
             this.valueSeparator = valueSeparator;
             return this;
         }
+
+        /**
+         * Sets the converter for the option.
+         * <p>Note: see {@link TypeHandler} for serialization discussion.</p>
+         * @param converter the Converter to use.
+         * @return this builder, to allow method chaining.
+         * @since 1.7.0
+         */
+        public Builder converter(final Converter<?> converter) {
+            this.converter = converter;
+            return this;
+        }
+        
     }
 
     /** Specifies the number of argument values has not been specified */
@@ -335,6 +351,9 @@ public class Option implements Cloneable, Serializable {
 
     /** The character that is the value separator. */
     private char valuesep;
+    
+    /** The explicit converter for this option.  May be null */
+    private transient Converter<?> converter;
 
     /**
      * Private constructor used by the nested Builder class.
@@ -351,6 +370,7 @@ public class Option implements Cloneable, Serializable {
         this.required = builder.required;
         this.type = builder.type;
         this.valuesep = builder.valueSeparator;
+        this.converter = builder.converter;
     }
 
     /**
@@ -423,7 +443,6 @@ public class Option implements Cloneable, Serializable {
         if (!acceptsArg()) {
             throw new IllegalArgumentException("Cannot add value, list full.");
         }
-
         // store value
         values.add(value);
     }
@@ -695,6 +714,8 @@ public class Option implements Cloneable, Serializable {
     }
 
     /**
+     * Returns whether this Option can have an optional argument.
+     * 
      * @return whether this Option can have an optional argument
      */
     public boolean hasOptionalArg() {
@@ -865,6 +886,24 @@ public class Option implements Cloneable, Serializable {
     public void setValueSeparator(final char sep) {
         this.valuesep = sep;
     }
+    
+    /**
+     * Gets the value to type converter.
+     * @return the value to type converter
+     * @since 1.7.0
+     */
+    public Converter<?> getConverter() {
+        return converter == null ? TypeHandler.getConverter(type) : converter;
+    }
+
+    /**
+     * Sets the value to type converter.
+     * @param converter The converter to convert the string value to the type.
+     * @since 1.7.0
+     */
+    public void setConverter(final Converter<?> converter) {
+        this.converter = converter;
+    }
 
     /**
      * Dump state, suitable for debugging.
diff --git a/src/main/java/org/apache/commons/cli/OptionBuilder.java b/src/main/java/org/apache/commons/cli/OptionBuilder.java
index 1e88655..4a377f4 100644
--- a/src/main/java/org/apache/commons/cli/OptionBuilder.java
+++ b/src/main/java/org/apache/commons/cli/OptionBuilder.java
@@ -31,6 +31,7 @@ package org.apache.commons.cli;
 @Deprecated
 public final class OptionBuilder {
 
+
     /** Long option */
     private static String longOption;
 
@@ -108,6 +109,7 @@ public final class OptionBuilder {
             option.setOptionalArg(optionalArg);
             option.setArgs(argCount);
             option.setType(type);
+            option.setConverter(TypeHandler.getConverter(type));
             option.setValueSeparator(valueSeparator);
             option.setArgName(argName);
         } finally {
diff --git a/src/main/java/org/apache/commons/cli/ParseException.java b/src/main/java/org/apache/commons/cli/ParseException.java
index 37bab98..f023779 100644
--- a/src/main/java/org/apache/commons/cli/ParseException.java
+++ b/src/main/java/org/apache/commons/cli/ParseException.java
@@ -21,11 +21,34 @@ package org.apache.commons.cli;
  * Base for Exceptions thrown during parsing of a command-line.
  */
 public class ParseException extends Exception {
+
     /**
      * This exception {@code serialVersionUID}.
      */
     private static final long serialVersionUID = 9112808380089253192L;
 
+    /**
+     * Converts any exception except {@code UnsupportedOperationException} to a {@code ParseException}.
+     * if {@code e} is an instance of {@code ParseException} it is returned, otherwise a {@code ParseException} is 
+     * created that wraps it.
+     * <p>
+     * Note: {@code UnsupportedOperationException} are not wrapped.  This is to solve a legacy expected exception problem and will be 
+     * removed in the future.</p> 
+     * @param e the exception to convert.
+     * @return the ParseException.
+     * @throws UnsupportedOperationException due to legacy expectations.  Will be removed in the future.
+     * @since 1.7.0
+     */
+    public static ParseException wrap(final Exception e) throws UnsupportedOperationException {
+        if (e instanceof UnsupportedOperationException) {
+            throw (UnsupportedOperationException) e;
+        }
+
+        if (e instanceof ParseException) {
+            return (ParseException) e;
+        }
+        return new ParseException(e);
+    }
     /**
      * Constructs a new {@code ParseException} with the specified detail message.
      *
@@ -34,4 +57,12 @@ public class ParseException extends Exception {
     public ParseException(final String message) {
         super(message);
     }
+    
+    /**
+     * Constructs a new {@code ParseException} wrapping the specified exception. 
+     * @param e the Exception to wrap.
+     */
+    public ParseException(final Exception e) {
+        super(e);
+    }
 }
diff --git a/src/main/java/org/apache/commons/cli/PatternOptionBuilder.java b/src/main/java/org/apache/commons/cli/PatternOptionBuilder.java
index 4e6919f..de3168e 100644
--- a/src/main/java/org/apache/commons/cli/PatternOptionBuilder.java
+++ b/src/main/java/org/apache/commons/cli/PatternOptionBuilder.java
@@ -102,14 +102,44 @@ public class PatternOptionBuilder {
 
     /** URL class */
     public static final Class<URL> URL_VALUE = URL.class;
+    
+    /** The converter to use for Unimplemented data types */
+    static final Converter<?> NOT_IMPLEMENTED = s -> { 
+        throw new UnsupportedOperationException("Not yet implemented");
+    };
+    
+    static {
+        registerTypes();
+    }
+
+    /**
+     * Registers custom {@code Converter}s with the {@code TypeHandler}. 
+     * @since 1.7.0
+     */
+    public static void registerTypes() {
+        TypeHandler.register(PatternOptionBuilder.FILES_VALUE, NOT_IMPLEMENTED);
+    }
 
     /**
      * Retrieve the class that {@code ch} represents.
      *
      * @param ch the specified character
      * @return The class that {@code ch} represents
+     * @deprecated use {@link #getValueType(char)}
      */
+    @Deprecated // since="1.7.0"
     public static Object getValueClass(final char ch) {
+        return getValueType(ch);
+    }
+    
+    /**
+     * Retrieve the class that {@code ch} represents.
+     *
+     * @param ch the specified character
+     * @return The class that {@code ch} represents
+     * @since 1.7.0
+     */
+    public static Class<?> getValueType(final char ch) {
         switch (ch) {
         case '@':
             return PatternOptionBuilder.OBJECT_VALUE;
@@ -154,6 +184,7 @@ public class PatternOptionBuilder {
         char opt = ' ';
         boolean required = false;
         Class<?> type = null;
+        Converter<?> converter = Converter.DEFAULT;
 
         final Options options = new Options();
 
@@ -164,19 +195,22 @@ public class PatternOptionBuilder {
             // details about it
             if (!isValueCode(ch)) {
                 if (opt != ' ') {
-                    final Option option = Option.builder(String.valueOf(opt)).hasArg(type != null).required(required).type(type).build();
+                    final Option option = Option.builder(String.valueOf(opt)).hasArg(type != null).required(required).type(type)
+                            .converter(converter).build();
 
                     // we have a previous one to deal with
                     options.addOption(option);
                     required = false;
                     type = null;
+                    converter = Converter.DEFAULT;
                 }
 
                 opt = ch;
             } else if (ch == '!') {
                 required = true;
             } else {
-                type = (Class<?>) getValueClass(ch);
+                type = getValueType(ch);
+                converter = TypeHandler.getConverter(getValueType(ch));
             }
         }
 
diff --git a/src/main/java/org/apache/commons/cli/TypeHandler.java b/src/main/java/org/apache/commons/cli/TypeHandler.java
index 25648f4..602942a 100644
--- a/src/main/java/org/apache/commons/cli/TypeHandler.java
+++ b/src/main/java/org/apache/commons/cli/TypeHandler.java
@@ -19,29 +19,114 @@ package org.apache.commons.cli;
 
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.net.MalformedURLException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.net.URL;
+import java.nio.file.Path;
 import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
- * This is a temporary implementation. TypeHandler will handle the pluggableness of OptionTypes and it will direct all
- * of these types of conversion functionalities to ConvertUtils component in Commons already. BeanUtils I think.
+ * TypeHandler will handle the pluggable conversion and verification of 
+ * Option types.  It handles the mapping of classes to bot converters and verifiers.
+ * It provides the default conversion and verification methods when converters and verifiers
+ * are not explicitly set.
+ * <p>
+ * If Options are serialized and deserialized their converters and verifiers will revert to the 
+ * defaults defined in this class.  To correctly de-serialize Options with custom converters and/or
+ * verifiers, using the default serialization methods, this class should be properly configured with the custom
+ * converters and verifiers for the specific class.
+ * </p>
  */
 public class TypeHandler {
+    
+    /** Value of hex conversion of strings */
+    private static final int HEX_RADIX = 16;
+
+    /** Map of classes to converters. */
+    private static Map<Class<?>, Converter<?>> converterMap = new HashMap<>();
+
+    static {
+        resetConverters();
+    }
+    
+    /**
+     * Resets the registered Converters to the default state.
+     * @since 1.7.0
+     */
+    public static void resetConverters() {
+        converterMap.clear();
+        converterMap.put(Object.class, Converter.OBJECT);
+        converterMap.put(Class.class, Converter.CLASS);
+        converterMap.put(Date.class, Converter.DATE);
+        converterMap.put(File.class, Converter.FILE);
+        converterMap.put(Path.class, Converter.PATH);
+        converterMap.put(Number.class, Converter.NUMBER);
+        converterMap.put(URL.class, Converter.URL);
+        converterMap.put(FileInputStream.class, s -> new FileInputStream(s));
+        converterMap.put(Long.class, Long::parseLong);
+        converterMap.put(Integer.class, Integer::parseInt);
+        converterMap.put(Short.class, Short::parseShort);
+        converterMap.put(Byte.class, Byte::parseByte);
+        converterMap.put(Character.class, s -> {
+            if (s.startsWith("\\u")) {
+                return Character.toChars(Integer.parseInt(s.substring(2), HEX_RADIX))[0];
+            } else {
+                return s.charAt(0);
+            } });
+        converterMap.put(Double.class, Double::parseDouble);
+        converterMap.put(Float.class, Float::parseFloat);
+        converterMap.put(BigInteger.class, s -> new BigInteger(s));
+        converterMap.put(BigDecimal.class, s -> new BigDecimal(s));
+    }
+    
+    /**
+     * Unregisters all Converters.
+     * @since 1.7.0
+     */
+    public static void noConverters() {
+        converterMap.clear();
+    }
+
+    /**
+     * Registers a Converter for a Class. If @code converter} is null registration is cleared for {@code clazz}, and 
+     * no converter will be used in processing.
+     * 
+     * @param clazz the Class to register the Converter and Verifier to.
+     * @param converter The Converter to associate with Class.  May be null.
+     * @since 1.7.0
+     */
+    public static void register(final Class<?> clazz, final Converter<?> converter) {
+        if (converter == null) {
+            converterMap.remove(clazz);
+        } else {
+            converterMap.put(clazz, converter);
+        }
+    }
+
+    /**
+     * Gets the converter for the the Class. Never null.
+     * @param clazz The Class to get the Converter for.
+     * @return the registered converter if any, {@link Converter#DEFAULT} otherwise.
+     * @since 1.7.0
+     */
+    public static Converter<?> getConverter(final Class<?> clazz) {
+        Converter<?> converter = converterMap.get(clazz);
+        return converter == null ? Converter.DEFAULT : converter;
+    }
+
     /**
      * Returns the class whose name is {@code className}.
      *
      * @param className the class name
      * @return The class if it is found
      * @throws ParseException if the class could not be found
+     * @deprecated use {@link #createValue(String, Class)}
      */
+    @Deprecated // since 1.7.0
     public static Class<?> createClass(final String className) throws ParseException {
-        try {
-            return Class.forName(className);
-        } catch (final ClassNotFoundException e) {
-            throw new ParseException("Unable to find the class: " + className);
-        }
+        return createValue(className, Class.class);
     }
 
     /**
@@ -51,10 +136,15 @@ public class TypeHandler {
      *
      * @param str the date string
      * @return The date if {@code str} is a valid date string, otherwise return null.
-     * @throws UnsupportedOperationException always
+     * @deprecated use {@link #createValue(String, Class)}
      */
+    @Deprecated // since 1.7.0
     public static Date createDate(final String str) {
-        throw new UnsupportedOperationException("Not yet implemented");
+        try {
+            return createValue(str, Date.class);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
     }
 
     /**
@@ -62,20 +152,27 @@ public class TypeHandler {
      *
      * @param str the File location
      * @return The file represented by {@code str}.
+     * @deprecated use {@link #createValue(String, Class)}
      */
+    @Deprecated // since 1.7.0
     public static File createFile(final String str) {
-        return new File(str);
+        try {
+            return createValue(str, File.class);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
     }
 
     /**
      * Returns the File[] represented by {@code str}.
-     * <p>
-     * This method is not yet implemented and always throws an {@link UnsupportedOperationException}.
+     * <p> This method is not yet implemented and always throws an {@link UnsupportedOperationException}.
      *
      * @param str the paths to the files
      * @return The File[] represented by {@code str}.
-     * @throws UnsupportedOperationException always
+     * @throws     UnsupportedOperationException always
+     * @deprecated with no replacement
      */
+    @Deprecated // since 1.7.0
     public static File[] createFiles(final String str) {
         // to implement/port:
         // return FileW.findFiles(str);
@@ -89,15 +186,9 @@ public class TypeHandler {
      * @return the number represented by {@code str}
      * @throws ParseException if {@code str} is not a number
      */
+    @Deprecated // since 1.7.0
     public static Number createNumber(final String str) throws ParseException {
-        try {
-            if (str.indexOf('.') != -1) {
-                return Double.valueOf(str);
-            }
-            return Long.valueOf(str);
-        } catch (final NumberFormatException e) {
-            throw new ParseException(e.getMessage());
-        }
+        return createValue(str, Number.class);
     }
 
     /**
@@ -106,21 +197,11 @@ public class TypeHandler {
      * @param className the argument value
      * @return the initialized object
      * @throws ParseException if the class could not be found or the object could not be created
+     * @deprecated use {@link #createValue(String, Class)}
      */
+    @Deprecated // since 1.7.0
     public static Object createObject(final String className) throws ParseException {
-        final Class<?> cl;
-
-        try {
-            cl = Class.forName(className);
-        } catch (final ClassNotFoundException cnfe) {
-            throw new ParseException("Unable to find the class: " + className);
-        }
-
-        try {
-            return cl.getConstructor().newInstance();
-        } catch (final Exception e) {
-            throw new ParseException(e.getClass().getName() + "; Unable to create an instance of: " + className);
-        }
+        return createValue(className, Object.class);
     }
 
     /**
@@ -129,54 +210,30 @@ public class TypeHandler {
      * @param str the URL string
      * @return The URL in {@code str} is well-formed
      * @throws ParseException if the URL in {@code str} is not well-formed
+     * @deprecated use {@link #createValue(String, Class)}
      */
+    @Deprecated // since 1.7.0
     public static URL createURL(final String str) throws ParseException {
-        try {
-            return new URL(str);
-        } catch (final MalformedURLException e) {
-            throw new ParseException("Unable to parse the URL: " + str);
-        }
+        return createValue(str, URL.class);
     }
 
     /**
-     * Returns the {@code Object} of type {@code clazz} with the value of {@code str}.
+     * Returns the @code Object} of type {@code clazz} with the value of
+     * {@code str}.
      *
      * @param str the command line value
      * @param clazz the class representing the type of argument
      * @param <T> type of argument
      * @return The instance of {@code clazz} initialized with the value of {@code str}.
-     * @throws ParseException if the value creation for the given class failed
+     * @throws ParseException if the value creation for the given class threw an exception.
      */
     @SuppressWarnings("unchecked") // returned value will have type T because it is fixed by clazz
     public static <T> T createValue(final String str, final Class<T> clazz) throws ParseException {
-        if (PatternOptionBuilder.STRING_VALUE == clazz) {
-            return (T) str;
-        }
-        if (PatternOptionBuilder.OBJECT_VALUE == clazz) {
-            return (T) createObject(str);
-        }
-        if (PatternOptionBuilder.NUMBER_VALUE == clazz) {
-            return (T) createNumber(str);
-        }
-        if (PatternOptionBuilder.DATE_VALUE == clazz) {
-            return (T) createDate(str);
-        }
-        if (PatternOptionBuilder.CLASS_VALUE == clazz) {
-            return (T) createClass(str);
-        }
-        if (PatternOptionBuilder.FILE_VALUE == clazz) {
-            return (T) createFile(str);
-        }
-        if (PatternOptionBuilder.EXISTING_FILE_VALUE == clazz) {
-            return (T) openFile(str);
-        }
-        if (PatternOptionBuilder.FILES_VALUE == clazz) {
-            return (T) createFiles(str);
-        }
-        if (PatternOptionBuilder.URL_VALUE == clazz) {
-            return (T) createURL(str);
+        try {
+            return (T) getConverter(clazz).apply(str);
+        } catch (Exception e) {
+            throw ParseException.wrap(e);
         }
-        throw new ParseException("Unable to handle the class: " + clazz);
     }
 
     /**
@@ -186,7 +243,9 @@ public class TypeHandler {
      * @param obj the type of argument
      * @return The instance of {@code obj} initialized with the value of {@code str}.
      * @throws ParseException if the value creation for the given object type failed
+     * @deprecated use {@link #createValue(String, Class)}
      */
+    @Deprecated // since 1.7.0
     public static Object createValue(final String str, final Object obj) throws ParseException {
         return createValue(str, (Class<?>) obj);
     }
@@ -197,12 +256,10 @@ public class TypeHandler {
      * @param str the file location
      * @return The file input stream represented by {@code str}.
      * @throws ParseException if the file is not exist or not readable
+     * @deprecated use {@link #createValue(String, Class)}
      */
+    @Deprecated // since 1.7.0
     public static FileInputStream openFile(final String str) throws ParseException {
-        try {
-            return new FileInputStream(str);
-        } catch (final FileNotFoundException e) {
-            throw new ParseException("Unable to find file: " + str);
-        }
+        return createValue(str, FileInputStream.class);
     }
 }
diff --git a/src/site/xdoc/usage.xml b/src/site/xdoc/usage.xml
index ee116c7..858f190 100644
--- a/src/site/xdoc/usage.xml
+++ b/src/site/xdoc/usage.xml
@@ -384,5 +384,80 @@ catch (ParseException exp) {
     System.out.println("Unexpected exception:" + exp.getMessage());
 }</source>
     </section>
+    <section name="Converting (Parsing) Option Values">
+        <p>
+            By in most cases the values on the command line are retrieved as Strings via the 
+            <code>commandLine.getOptionValue(key)</code> command.  However, it is possible for
+            the CLI library to convert the string into a different object.  For example to specify 
+            that the "count" option should reutrn an Integer the following code could be used:
+        </p>
+        <source>
+        public static void main(String[] args) {
+            Option count = Option.builder("count")
+                          .hasArg()
+                          .desc("the number of things")
+                          .type(Integer.class)
+                          .build();
+            Options options = new Options().addOption(cound);
+            // create the parser
+            CommandLineParser parser = new DefaultParser();
+            try {
+                // parse the command line arguments
+                CommandLine line = parser.parse(options, args);
+            }
+            catch (ParseException exp) {
+                // oops, something went wrong
+                System.err.println("Parsing failed.  Reason: " + exp.getMessage());
+            }
+            
+            try {
+                Integer value = line.getParsedOptionValue(count);
+                System.out.format("The value is %s\m", value );
+            } catch (ParseException e) {
+                e.printStackTrace();
+            }
+        }</source>
+        <p>
+            The value types natively supported by commons-cli are:
+            <ul>
+                <li>Object.class - The string value must be the name of a class with a no argument constructor</li>
+                <li>Class.class - The string value must be the name of a class</li>
+                <li>Date.class - The string value must be a date parsable by <code>new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy")</code></li>
+                <li>File.class - The string value is the name of the file.</li>
+                <li>Path.class - The string value is the name of a Path.</li>
+                <li>Number.class - The string value is a number representation can can be converted into an Integer or a Double.</li>
+                <li>URL.class - The string value is the textual representation of a URL</li>
+                <li>FileInputStream.class - The string value is passed to <code>new FileInputStream(s)</code>.</li>
+                <li>Long.class - The string value is a valid argument to <code>Long.parseLong()</code>.</li>
+                <li>Integer.class - The string value is a valid argument to <code>Integer.parseInt()</code>.</li>
+                <li>Short.class - The string value is a valid argument to <code>Short.parseShort()</code>.</li>
+                <li>Byte.class - The string value is a valid argument to <code>Byte.parseByte()</code>.</li>
+                <li>Character.class - The string value is either a UTF-8 encoding for a character (e.g. "\\u0124") or the first character from the String."</li>
+                <li>Double.class - The string value is a valid argument to <code>Double.parseDouble()</code>.</li>
+                <li>Float.class - The string value is a valid argument to <code>Float.parseFloat()</code>.</li>
+                <li>BigInteger.class - The string value is a valid argument to <code>new BigInteger(s)</code>.</li>
+                <li>BigDecimal.class - The string value is a valid argument to <code>new BigDecimal(s)</code>.</li>
+            </ul>
+            Additional types may be added to the automatic parsing system by calling <code>TypeHandler.register(Class&lt;T&gt; clazz, Converter&lt;T&gt; converter)</code>.
+            The <code>Class&lt;T&gt;</code> can be any defined class.  The converter is a function that takes a <code>String</code> argument and returns an instance of 
+            the class.  Any expection thrown by the constructor will be caught and reported as a <code>ParseException</code>
+        </p>
+        <p>
+            Conversions can be specified without using the <code>TypeHandler</code> class by specifying the converter 
+            directly during the option build.  For example: 
+            <source>
+                Option fooOpt = Option.builder("foo")
+                          .hasArg()
+                          .desc("the foo arg")
+                          .converter((s) -> new Foo(s))
+                          .build();
+            </source>
+            The above will create an option that passes the string value to the Foo constructor when <code>commandLine.getParsedOptionValue(fooOpt)</code> is called.
+        </p>
+        <p>
+            Conversions that are added to the TypeHandler or that are specified directly will not deserialize if the option is serialized unless the type is registered with the TypeHandler 
+            before deserialization begins.
+        </p>
+    </section>
   </body>
 </document>
diff --git a/src/test/java/org/apache/commons/cli/ConverterTests.java b/src/test/java/org/apache/commons/cli/ConverterTests.java
new file mode 100644
index 0000000..a647f70
--- /dev/null
+++ b/src/test/java/org/apache/commons/cli/ConverterTests.java
@@ -0,0 +1,118 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+package org.apache.commons.cli;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for standard Converters.
+ */
+public class ConverterTests {
+
+    @ParameterizedTest
+    @MethodSource("numberTestParameters")
+    public void numberTests(final String str, final Number expected) throws Exception {
+        if (expected != null) {
+            assertEquals(expected, Converter.NUMBER.apply(str));
+        } else {
+            assertThrows(NumberFormatException.class, () -> Converter.NUMBER.apply(str));
+        }
+    }
+
+    private static Stream<Arguments> numberTestParameters() {
+        List<Arguments> lst = new ArrayList<>();
+
+        lst.add(Arguments.of("123", Long.valueOf("123")));
+        lst.add(Arguments.of("12.3", Double.valueOf("12.3")));
+        lst.add(Arguments.of("-123", Long.valueOf("-123")));
+        lst.add(Arguments.of("-12.3", Double.valueOf("-12.3")));
+        lst.add(Arguments.of(".3", Double.valueOf("0.3")));
+        lst.add(Arguments.of("-.3", Double.valueOf("-0.3")));
+        lst.add(Arguments.of("0x5F", null));
+        lst.add(Arguments.of("2,3", null));
+        lst.add(Arguments.of("1.2.3", null));
+
+        return lst.stream();
+    }
+
+    @Test
+    public void classTests() throws Exception {
+
+        assertNotNull(Converter.CLASS.apply(this.getClass().getName()), this.getClass().getName());
+        assertNotNull(Converter.CLASS.apply(this.getClass().getCanonicalName()), this.getClass().getCanonicalName());
+        assertThrows(ClassNotFoundException.class, () -> Converter.CLASS.apply(this.getClass().getSimpleName()),
+                this.getClass().getSimpleName());
+        assertNotNull(Converter.CLASS.apply(this.getClass().getTypeName()), this.getClass().getTypeName());
+
+        assertThrows(ClassNotFoundException.class, () -> Converter.CLASS.apply("foo.bar"));
+        assertNotNull(Converter.CLASS.apply(AClassWithoutADefaultConstructor.class.getName()));
+    }
+
+    @Test
+    public void objectTests() throws Exception {
+        assertNotNull(Converter.OBJECT.apply(this.getClass().getName()), this.getClass().getName());
+        assertNotNull(Converter.OBJECT.apply(this.getClass().getCanonicalName()), this.getClass().getCanonicalName());
+        assertThrows(ClassNotFoundException.class, () -> Converter.OBJECT.apply(this.getClass().getSimpleName()),
+                this.getClass().getSimpleName());
+        assertNotNull(Converter.OBJECT.apply(this.getClass().getTypeName()), this.getClass().getTypeName());
+
+        assertThrows(ClassNotFoundException.class, () -> Converter.OBJECT.apply("foo.bar"));
+        assertThrows(NoSuchMethodException.class, () -> Converter.OBJECT.apply(AClassWithoutADefaultConstructor.class.getName()));
+    }
+
+    @Test
+    public void dateTests() throws Exception {
+        assertThrows(java.text.ParseException.class, () -> Converter.DATE.apply("whatever"));
+
+        Date d = new Date(1023400137000L);
+        assertEquals(d, Converter.DATE.apply("Thu Jun 06 17:48:57 EDT 2002"));
+
+        assertThrows(java.text.ParseException.class, () -> Converter.DATE.apply("Jun 06 17:48:57 EDT 2002"));
+    }
+
+    @Test
+    public void urlTests() throws Exception {
+        assertEquals(new URL("http://apache.org"), Converter.URL.apply("http://apache.org"));
+        assertThrows(java.net.MalformedURLException.class, () -> Converter.URL.apply("foo.bar"));
+    }
+
+    @Test
+    public void fileTests() throws Exception {
+        URL url = this.getClass().getClassLoader().getResource("./org/apache/commons/cli/existing-readable.file");
+        String fileName = url.toString().substring("file:".length());
+        assertNotNull(Converter.FILE.apply(fileName));
+    }
+
+    // A class without a default constructor.
+    public class AClassWithoutADefaultConstructor {
+        public AClassWithoutADefaultConstructor(final int i) {
+        }
+    }
+}
diff --git a/src/test/java/org/apache/commons/cli/OptionTest.java b/src/test/java/org/apache/commons/cli/OptionTest.java
index 7cc84f8..a49dbaa 100644
--- a/src/test/java/org/apache/commons/cli/OptionTest.java
+++ b/src/test/java/org/apache/commons/cli/OptionTest.java
@@ -25,6 +25,12 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
 import org.junit.Test;
 
 public class OptionTest {
@@ -232,4 +238,39 @@ public class OptionTest {
         assertEquals(DefaultOption.class, clone.getClass());
     }
 
+
+    private Option roundTrip(final Option o) throws IOException, ClassNotFoundException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(baos);
+        oos.writeObject(o);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ObjectInputStream ois = new ObjectInputStream(bais);
+        return (Option) ois.readObject();
+    }
+
+    @Test
+    public void testSerialization() throws IOException, ClassNotFoundException {
+
+        Option o = Option.builder("o").type(TypeHandlerTest.Instantiable.class).build();
+        assertEquals(Converter.DEFAULT, o.getConverter());
+        Option o2 = roundTrip(o);
+        assertEquals(Converter.DEFAULT, o2.getConverter());
+
+        // verify unregistered class converters and verifiers get reset to default.
+        o.setConverter(Converter.DATE);
+        o2 = roundTrip(o);
+        assertEquals(Converter.DEFAULT, o2.getConverter());
+
+        // verify registered class converters and verifiers do not get reset to default.
+        try {
+            TypeHandler.register(TypeHandlerTest.Instantiable.class, Converter.URL);
+            // verify earlier values still set.
+            assertEquals(Converter.DATE, o.getConverter());
+            o2 = roundTrip(o);
+            // verify set to registered value
+            assertEquals(Converter.URL, o2.getConverter());
+        } finally {
+            TypeHandler.register(TypeHandlerTest.Instantiable.class, null);
+        }
+    }
 }
diff --git a/src/test/java/org/apache/commons/cli/OptionsTest.java b/src/test/java/org/apache/commons/cli/OptionsTest.java
index 8452cba..3d7004f 100644
--- a/src/test/java/org/apache/commons/cli/OptionsTest.java
+++ b/src/test/java/org/apache/commons/cli/OptionsTest.java
@@ -49,8 +49,10 @@ public class OptionsTest {
     @Test
     public void testGetMatchingOpts() {
         final Options options = new Options();
-        options.addOption(OptionBuilder.withLongOpt("version").create());
-        options.addOption(OptionBuilder.withLongOpt("verbose").create());
+        OptionBuilder.withLongOpt("version");
+        options.addOption(OptionBuilder.create());
+        OptionBuilder.withLongOpt("verbose");
+        options.addOption(OptionBuilder.create());
 
         assertTrue(options.getMatchingOptions("foo").isEmpty());
         assertEquals(1, options.getMatchingOptions("version").size());
@@ -78,12 +80,16 @@ public class OptionsTest {
 
     @Test
     public void testHelpOptions() {
-        final Option longOnly1 = OptionBuilder.withLongOpt("long-only1").create();
-        final Option longOnly2 = OptionBuilder.withLongOpt("long-only2").create();
+        OptionBuilder.withLongOpt("long-only1");
+        final Option longOnly1 = OptionBuilder.create();
+        OptionBuilder.withLongOpt("long-only2");
+        final Option longOnly2 = OptionBuilder.create();
         final Option shortOnly1 = OptionBuilder.create("1");
         final Option shortOnly2 = OptionBuilder.create("2");
-        final Option bothA = OptionBuilder.withLongOpt("bothA").create("a");
-        final Option bothB = OptionBuilder.withLongOpt("bothB").create("b");
+        OptionBuilder.withLongOpt("bothA");
+        final Option bothA = OptionBuilder.create("a");
+        OptionBuilder.withLongOpt("bothB");
+        final Option bothB = OptionBuilder.create("b");
 
         final Options options = new Options();
         options.addOption(longOnly1);
@@ -121,7 +127,8 @@ public class OptionsTest {
     @Test
     public void testMissingOptionException() throws ParseException {
         final Options options = new Options();
-        options.addOption(OptionBuilder.isRequired().create("f"));
+        OptionBuilder.isRequired();
+        options.addOption(OptionBuilder.create("f"));
         try {
             new PosixParser().parse(options, new String[0]);
             fail("Expected MissingOptionException to be thrown");
@@ -133,8 +140,10 @@ public class OptionsTest {
     @Test
     public void testMissingOptionsException() throws ParseException {
         final Options options = new Options();
-        options.addOption(OptionBuilder.isRequired().create("f"));
-        options.addOption(OptionBuilder.isRequired().create("x"));
+        OptionBuilder.isRequired();
+        options.addOption(OptionBuilder.create("f"));
+        OptionBuilder.isRequired();
+        options.addOption(OptionBuilder.create("x"));
         try {
             new PosixParser().parse(options, new String[0]);
             fail("Expected MissingOptionException to be thrown");
diff --git a/src/test/java/org/apache/commons/cli/PatternOptionBuilderTest.java b/src/test/java/org/apache/commons/cli/PatternOptionBuilderTest.java
index 27c442a..a0ceb52 100644
--- a/src/test/java/org/apache/commons/cli/PatternOptionBuilderTest.java
+++ b/src/test/java/org/apache/commons/cli/PatternOptionBuilderTest.java
@@ -31,6 +31,7 @@ import java.util.Calendar;
 import java.util.Date;
 import java.util.Vector;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 /**
@@ -38,6 +39,11 @@ import org.junit.Test;
  */
 @SuppressWarnings("deprecation") // tests some deprecated classes
 public class PatternOptionBuilderTest {
+    @BeforeClass
+    public static void setup() {
+        PatternOptionBuilder.registerTypes();
+    }
+
     @Test
     public void testClassPattern() throws Exception {
         final Options options = PatternOptionBuilder.parsePattern("c+d+");
@@ -79,8 +85,10 @@ public class PatternOptionBuilderTest {
     public void testNumberPattern() throws Exception {
         final Options options = PatternOptionBuilder.parsePattern("n%d%x%");
         final CommandLineParser parser = new PosixParser();
-        final CommandLine line = parser.parse(options, new String[] {"-n", "1", "-d", "2.1", "-x", "3,5"});
+        // 3,5 fails validation.
+        //assertThrows(ParseException.class, () -> parser.parse(options, new String[] {"-n", "1", "-d", "2.1", "-x", "3,5"}));
 
+        CommandLine line = parser.parse(options, new String[] {"-n", "1", "-d", "2.1", "-x", "3,5"});
         assertEquals("n object class", Long.class, line.getOptionObject("n").getClass());
         assertEquals("n value", Long.valueOf(1), line.getOptionObject("n"));
 
@@ -153,13 +161,8 @@ public class PatternOptionBuilderTest {
             // expected
         }
 
-        // DATES NOT SUPPORTED YET
-        try {
-            assertEquals("date flag z", new Date(1023400137276L), line.getOptionObject('z'));
-            fail("Date is not supported yet, should have failed");
-        } catch (final UnsupportedOperationException uoe) {
-            // expected
-        }
+        assertEquals("date flag z", new Date(1023400137000L), line.getOptionObject('z'));
+
     }
 
     @Test
diff --git a/src/test/java/org/apache/commons/cli/TypeHandlerTest.java b/src/test/java/org/apache/commons/cli/TypeHandlerTest.java
index c288622..26c051d 100644
--- a/src/test/java/org/apache/commons/cli/TypeHandlerTest.java
+++ b/src/test/java/org/apache/commons/cli/TypeHandlerTest.java
@@ -19,125 +19,198 @@ package org.apache.commons.cli;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Stream;
 
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 public class TypeHandlerTest {
 
+    /** Used for Class and Object creation tests. */
     public static class Instantiable {
+
+        @Override
+        public boolean equals(final Object arg0) {
+            return arg0 instanceof Instantiable;
+        }
+
+        @Override
+        public int hashCode() {
+            return 1;
+        }
+    }
+
+    /* proof of equality for later tests */
+    @Test
+    public void testnstantiableEquals() {
+        assertEquals(new Instantiable(), new Instantiable());
     }
 
+    /** Used for Class and Object negative creation tests */
     public static final class NotInstantiable {
         private NotInstantiable() {
         }
+
     }
 
     @Test
-    public void testCreateValueClass() throws Exception {
-        final Object clazz = TypeHandler.createValue(Instantiable.class.getName(), PatternOptionBuilder.CLASS_VALUE);
-        assertEquals(Instantiable.class, clazz);
+    public void testRegister() {
+        assertEquals(Converter.DEFAULT, TypeHandler.getConverter(NotInstantiable.class));
+        try {
+            TypeHandler.register(NotInstantiable.class, Converter.DATE);
+            assertEquals(Converter.DATE, TypeHandler.getConverter(NotInstantiable.class));
+        } finally {
+            TypeHandler.register(NotInstantiable.class, null);
+            assertEquals(Converter.DEFAULT, TypeHandler.getConverter(NotInstantiable.class));
+        }
     }
 
     @Test
-    public void testCreateValueClass_notFound() {
-        assertThrows(ParseException.class, () ->
-                TypeHandler.createValue("what ever", PatternOptionBuilder.CLASS_VALUE));
+    public void testResetConverters() {
+        assertEquals(Converter.DEFAULT, TypeHandler.getConverter(NotInstantiable.class));
+        try {
+            TypeHandler.register(NotInstantiable.class, Converter.DATE);
+            assertEquals(Converter.DATE, TypeHandler.getConverter(NotInstantiable.class));
+            TypeHandler.resetConverters();
+            assertEquals(Converter.DEFAULT, TypeHandler.getConverter(NotInstantiable.class));
+            assertEquals(Converter.DEFAULT, TypeHandler.getConverter(NotInstantiable.class));
+        } finally {
+            TypeHandler.register(NotInstantiable.class, null);
+        }
     }
-
+    
     @Test
-    public void testCreateValueDate() {
-        assertThrows(UnsupportedOperationException.class, () ->
-                TypeHandler.createValue("what ever", PatternOptionBuilder.DATE_VALUE));
+    public void testNoConverters() {
+        assertEquals(Converter.NUMBER, TypeHandler.getConverter(Number.class));
+        try {
+            TypeHandler.noConverters();
+            assertEquals(Converter.DEFAULT, TypeHandler.getConverter(Number.class));
+        } finally {
+            TypeHandler.resetConverters();
+            assertEquals(Converter.NUMBER, TypeHandler.getConverter(Number.class));
+        }
     }
 
     @Test
     public void testCreateValueExistingFile() throws Exception {
-        try (FileInputStream result = TypeHandler.createValue("src/test/resources/org/apache/commons/cli/existing-readable.file",
-            PatternOptionBuilder.EXISTING_FILE_VALUE)) {
+        try (FileInputStream result = TypeHandler.createValue(
+                "src/test/resources/org/apache/commons/cli/existing-readable.file",
+                PatternOptionBuilder.EXISTING_FILE_VALUE)) {
             assertNotNull(result);
         }
     }
 
-    @Test
-    public void testCreateValueExistingFile_nonExistingFile() {
-        assertThrows(ParseException.class, () ->
-                TypeHandler.createValue("non-existing.file", PatternOptionBuilder.EXISTING_FILE_VALUE));
+    @SuppressWarnings("unchecked")
+    @ParameterizedTest(name = "{0} as {1}")
+    @MethodSource("createValueTestParameters")
+    public void createValueTests(final String str, final Class<?> type, final Object expected) throws Exception {
+        if (expected instanceof Class<?> && Throwable.class.isAssignableFrom((Class<?>) expected)) {
+            assertThrows((Class<Throwable>) expected, () -> TypeHandler.createValue(str, type));
+        } else {
+            assertEquals(expected, TypeHandler.createValue(str, type));
+        }
     }
 
-    @Test
-    public void testCreateValueFile() throws Exception {
-        final File result = TypeHandler.createValue("some-file.txt", PatternOptionBuilder.FILE_VALUE);
-        assertEquals("some-file.txt", result.getName());
-    }
+    private static Stream<Arguments> createValueTestParameters() {
+        // forse the PatternOptionBuilder to load / modify the TypeHandler table.
+        Class<?> ignore = PatternOptionBuilder.FILES_VALUE;
+        // reset the type handler table.
+        TypeHandler.resetConverters();
+        List<Arguments> lst = new ArrayList<>();
 
-    @Test
-    public void testCreateValueFiles() {
-        assertThrows(UnsupportedOperationException.class, () ->
-                TypeHandler.createValue("some.files", PatternOptionBuilder.FILES_VALUE));
-    }
+        try {
+            lst.add(Arguments.of(Instantiable.class.getName(), PatternOptionBuilder.CLASS_VALUE, Instantiable.class));
+            lst.add(Arguments.of("what ever", PatternOptionBuilder.CLASS_VALUE, ParseException.class));
 
-    @Test
-    public void testCreateValueInteger_failure() {
-        assertThrows(ParseException.class, () ->
-                TypeHandler.createValue("just-a-string", Integer.class));
-    }
+            lst.add(Arguments.of("what ever", PatternOptionBuilder.DATE_VALUE, ParseException.class));
+            lst.add(Arguments.of("Thu Jun 06 17:48:57 EDT 2002", PatternOptionBuilder.DATE_VALUE,
+                    new Date(1023400137000L)));
+            lst.add(Arguments.of("Jun 06 17:48:57 EDT 2002", PatternOptionBuilder.DATE_VALUE, ParseException.class));
 
-    @Test
-    public void testCreateValueNumber_Double() throws Exception {
-        assertEquals(1.5d, TypeHandler.createValue("1.5", PatternOptionBuilder.NUMBER_VALUE));
-    }
+            lst.add(Arguments.of("non-existing.file", PatternOptionBuilder.EXISTING_FILE_VALUE, ParseException.class));
 
-    @Test
-    public void testCreateValueNumber_Long() throws Exception {
-        assertEquals(Long.valueOf(15), TypeHandler.createValue("15", PatternOptionBuilder.NUMBER_VALUE));
-    }
+            lst.add(Arguments.of("some-file.txt", PatternOptionBuilder.FILE_VALUE, new File("some-file.txt")));
+            
+            lst.add(Arguments.of("some-path.txt", Path.class, new File("some-path.txt").toPath()));
 
-    @Test
-    public void testCreateValueNumber_noNumber() {
-        assertThrows(ParseException.class, () ->
-                TypeHandler.createValue("not a number", PatternOptionBuilder.NUMBER_VALUE));
-    }
+            // the PatternOptionBUilder.FILES_VALUE is not registered so it should just return the string
+            lst.add(Arguments.of("some.files", PatternOptionBuilder.FILES_VALUE, "some.files"));
 
-    @Test
-    public void testCreateValueObject_InstantiableClass() throws Exception {
-        final Object result = TypeHandler.createValue(Instantiable.class.getName(), PatternOptionBuilder.OBJECT_VALUE);
-        assertTrue(result instanceof Instantiable);
-    }
+            lst.add(Arguments.of("just-a-string", Integer.class, ParseException.class));
+            lst.add(Arguments.of("5", Integer.class, 5));
+            lst.add(Arguments.of("5.5", Integer.class, ParseException.class));
+            lst.add(Arguments.of(Long.valueOf(Long.MAX_VALUE).toString(), Integer.class, ParseException.class));
 
-    @Test
-    public void testCreateValueObject_notInstantiableClass() {
-        assertThrows(ParseException.class, () ->
-                TypeHandler.createValue(NotInstantiable.class.getName(), PatternOptionBuilder.OBJECT_VALUE));
-    }
+            lst.add(Arguments.of("just-a-string", Long.class, ParseException.class));
+            lst.add(Arguments.of("5", Long.class, 5L));
+            lst.add(Arguments.of("5.5", Long.class, ParseException.class));
 
-    @Test
-    public void testCreateValueObject_unknownClass() {
-        assertThrows(ParseException.class, () ->
-                TypeHandler.createValue("unknown", PatternOptionBuilder.OBJECT_VALUE));
-    }
+            lst.add(Arguments.of("just-a-string", Short.class, ParseException.class));
+            lst.add(Arguments.of("5", Short.class, (short) 5));
+            lst.add(Arguments.of("5.5", Short.class, ParseException.class));
+            lst.add(Arguments.of(Integer.valueOf(Integer.MAX_VALUE).toString(), Short.class, ParseException.class));
 
-    @Test
-    public void testCreateValueString() throws Exception {
-        assertEquals("String", TypeHandler.createValue("String", PatternOptionBuilder.STRING_VALUE));
-    }
+            lst.add(Arguments.of("just-a-string", Byte.class, ParseException.class));
+            lst.add(Arguments.of("5", Byte.class, (byte) 5));
+            lst.add(Arguments.of("5.5", Byte.class, ParseException.class));
+            lst.add(Arguments.of(Short.valueOf(Short.MAX_VALUE).toString(), Byte.class, ParseException.class));
 
-    @Test
-    public void testCreateValueURL() throws Exception {
-        final String urlString = "https://commons.apache.org";
-        final URL result = TypeHandler.createValue(urlString, PatternOptionBuilder.URL_VALUE);
-        assertEquals(urlString, result.toString());
-    }
+            lst.add(Arguments.of("just-a-string", Character.class, 'j'));
+            lst.add(Arguments.of("5", Character.class, '5'));
+            lst.add(Arguments.of("5.5", Character.class, '5'));
+            lst.add(Arguments.of("\\u0124", Character.class, Character.toChars(0x0124)[0]));
 
-    @Test
-    public void testCreateValueURL_malformed() {
-        assertThrows(ParseException.class, () ->
-                TypeHandler.createValue("malformed-url", PatternOptionBuilder.URL_VALUE));
-    }
+            lst.add(Arguments.of("just-a-string", Double.class, ParseException.class));
+            lst.add(Arguments.of("5", Double.class, 5d));
+            lst.add(Arguments.of("5.5", Double.class, 5.5));
+
+            lst.add(Arguments.of("just-a-string", Float.class, ParseException.class));
+            lst.add(Arguments.of("5", Float.class, 5f));
+            lst.add(Arguments.of("5.5", Float.class, 5.5f));
+            lst.add(Arguments.of(Double.valueOf(Double.MAX_VALUE).toString(), Float.class, Float.POSITIVE_INFINITY));
+
+            lst.add(Arguments.of("just-a-string", BigInteger.class, ParseException.class));
+            lst.add(Arguments.of("5", BigInteger.class, new BigInteger("5")));
+            lst.add(Arguments.of("5.5", BigInteger.class, ParseException.class));
+
+            lst.add(Arguments.of("just-a-string", BigDecimal.class, ParseException.class));
+            lst.add(Arguments.of("5", BigDecimal.class, new BigDecimal("5")));
+            lst.add(Arguments.of("5.5", BigDecimal.class, new BigDecimal(5.5)));
+
+            lst.add(Arguments.of("1.5", PatternOptionBuilder.NUMBER_VALUE, Double.valueOf(1.5)));
+            lst.add(Arguments.of("15", PatternOptionBuilder.NUMBER_VALUE, Long.valueOf(15)));
+            lst.add(Arguments.of("not a number", PatternOptionBuilder.NUMBER_VALUE, ParseException.class));
+
+            lst.add(Arguments.of(Instantiable.class.getName(), PatternOptionBuilder.OBJECT_VALUE, new Instantiable()));
+            lst.add(Arguments.of(NotInstantiable.class.getName(), PatternOptionBuilder.OBJECT_VALUE,
+                    ParseException.class));
+            lst.add(Arguments.of("unknown", PatternOptionBuilder.OBJECT_VALUE, ParseException.class));
 
+            lst.add(Arguments.of("String", PatternOptionBuilder.STRING_VALUE, "String"));
+
+            final String urlString = "https://commons.apache.org";
+            lst.add(Arguments.of(urlString, PatternOptionBuilder.URL_VALUE, new URL(urlString)));
+            lst.add(Arguments.of("Malformed-url", PatternOptionBuilder.URL_VALUE, ParseException.class));
+
+            return lst.stream();
+
+        } catch (MalformedURLException e) {
+            throw new RuntimeException(e);
+        }
+
+    }
 }
diff --git a/src/test/java/org/apache/commons/cli/bug/BugsTest.java b/src/test/java/org/apache/commons/cli/bug/BugsTest.java
index eed3381..124da0a 100644
--- a/src/test/java/org/apache/commons/cli/bug/BugsTest.java
+++ b/src/test/java/org/apache/commons/cli/bug/BugsTest.java
@@ -137,7 +137,7 @@ public class BugsTest {
         cmd.getOptionValue("f", "default f");
         cmd.getOptionValue("m", "default m");
         //
-        assertNull(cmd.getOptionValue((String) null, null));
+        assertNull(cmd.getOptionValue((String) null, (String) null));
         assertEquals("default", cmd.getOptionValue((String) null, "default"));
     }