Value Types

Value types enable binary switches and arguments to be converted to a value when the command line option or argument is read. They take the string read in from the command line and convert it to an underlying Java type.

All command line options start life as a string. Value types enable conversion of the string read from the command line to an underlying type: For example, if the type is a double, then the command line switch value will be converted into a double and will return a double when retrieved; likewise, a datafile type will be converted from a string to a file and the return value will be the contents of the file as a byte[] array.

Properties can be defined for value types in order to provide error checking and data manipulation in order to perform the task of converting the command line value to the underlying type. Value types throw a ValueTypeConstructionException if there is anything wrong with the properties (including if the properties themselves are incorrect).

Several value types are provided out of the box but API users can define their own value types, discussed below. Furthermore it is also possible to defined custom value value types, as well as replace existing value types with new implementations if needed.

Creation of new value types and replacing existing value types are discussed below the descriptions of provided value types.

Numerical Types

Comprise of the following types:

  • bigdecimal: Return type represented by java.math.BigDecimal.
  • biginteger: Return type represented by java.math.BigInteger.
  • byte: Return type represented by java.lang.Byte.
  • double: Return type represented by java.lang.Double.
  • float: Return type represented by java.lang.Float.
  • int: Return type represented by java.lang.Integer.
  • long: Return type represented by java.lang.Long.
  • short: Return type represented by java.lang.Short.

Properties for all numerical types:

  • minimum: Set the minimum value that will be accepted. Optional.
  • maximum: Set the maximum value that will be accepted. Optional.

Example: Define a float for option name float-val with a minimum value of 0 and a maximum value of 10:

option.float-val.type = float
option.float-val.properties = min = 0, max = 10

Non-Numerical Value Types

boolean

Input string as a boolean value.

Return type represented by java.lang.Bool.

Boolean values will be represented by true and false unless overridden using truthMappings.

Properties:

  • truthMappings: specify any number of semi-colon separated key-value pairs of the form x = y values of truth properties. The key is a name that maps to a boolean value (ignoring case) true or false. Any number of mappings may be supplied but must contain at least one true value and at least one false value. Duplicate keys are not permitted. All keys and values are converted to lower case.

Example: Define properties that treats on and y as true and off and n as false for option name running:

option.running.type = boolean
option.running.properties = truthMappings = on = true ; y = true ; off = false ; n = false

char

Input string as a single character.

Return type represented by java.lang.Character.

Properties:

  • includes: Regular expression or string of characters to match. Unless regex is set to true, will be treated as a string of characters. Mutually exclusive with excludes. Optional.
  • excludes: Regular expression or string of characters to exclude. Unless regex is set to true, will be treated as a string of characters. Mutually exclusive with includes. Optional.
  • regex: true to treat the includes or excludes string as a regular expression.

Example: Match characters A through C and X through Z as uppercase or lower case as a non-regular expression for option name char-val:

option.char-val.type = char
option.char-val.properties = includes = abcABCxyzXYZ

Example: As above but use a regular expression:

option.char-val.type = char
option.char-val.properties = includes = [a-cA-Cx-zX-Z], regex = true

datafile

Input string as an existing file.

Value type to read a byte array from the given file which must exist.

Return type represented by a byte[] array.

Properties:

  • encoding: specify the encoding when reading the file. Optional; defaults to UTF-8.

date

Input string as a date.

Value type representing a date/time.

Return type represented by java.util.Date.

Properties:

  • dateFormat: Required. Specify date format according to Java date formatting rules, for example yyyy/MM/dd HH:mm:ss.

dir

Input string as a directory.

Value type representing a directory; by default, no checks are made as to the existence of the directory, although users can specify the file type property that will perform necessary checks.

Return type represented by java.io.File.

Properties:

  • dirType: Optional. One of:
    • exists: The directory must exist.
    • !exists: The directory must not exist.
  • mkdirs: Optional; if the directory does not exist, create it. If set, cannot have any of the following search-based properties set on it.
  • recursive: Optional; one of true or false. If true, recursively traverse all directories within the specified directory, otherwise just scan the directory specified for this directory type. Callers should ensure they have implemented org.statefive.clic.valuetype.DirUpdateListener to receive updates as the directory/directories are scanned. If using recursion, directories will be scanned using a breadth-first strategy unless a different strategy is employed (see below). If this property is set implementations are also required to set the listener-id (again, see below);
  • listener-id: Optional, unless recursive is set in which case it is mandatory: The listener ID. When updates to a directory are called by one of the updates specified inDirUpdateListener, listeners can check to see if the ID from the update matches their registered ID.
  • suffixes: Optional. Space-separated list of file suffixes that any files must match . Only files that match the specified suffixes (ignoring case) will be included in any updates to DirUpdateListener directoryTraversed(java.io.File, java.io.File[], java.lang.String).

Directory Update Listeners

To receive updates from the API, callers must implement org.statefive.clic.valuetype.DirUpdateListener, and add themselves to the list of directory listeners via

Clc.getInstance().addDirUpdateListener(listener);

The following method must be implemented:

boolean directoryTraversed(File dir, File[] files, String listenerId)

The return value, boolean, determines if the API should continue processing; if the implementation returns false, the directory will not be recursively traversed.

For each directory traversed, the API will update the method with the current directory and the list of files (not directories) within the directory, along with the listener-id of the directory type (se above).

listener-id enables the same application to register several directory types that search different directories and enable callers to determine which directory type the update was for.

file

Input string as a file.

Return type represented by java.io.File.

Value type representing a file; by default, no checks are made, although users can specify the file type property that will perform necessary checks.

Properties:

  • fileType: Optional. One of:
    • exists: The file must exist.
    • !exists: The file must not exist.

list

Input string representing a list of of values. Defaults to a list of string values unless overridden by the appropriate property. Can contain any value value type except list.

Lists are defined as a comma-separated list of values, unless the default character is overridden. If elements of a list contain the separator character, escape them with a backslash.

Return type represented by java.util.List.

Properties:

  • separatorChar: Character used to separate elements. Optional, defaults to a comma;
  • listValueType: List value type to set. Optional, defaults to string if not defined;
  • listValueTypeProperties: Properties to pass to the underlying value type of list elements. Optional; and
  • listValueTypeDefaultValue: Default value to set list elements to if any of the items between separator characters are empty. Optional.

Example: Create a list type of integers that must be between 0 and 10 (inclusive), with a default value of 5:

listValueType = int, listValueTypeProperties = min = 0, max = 10, listValueTypeDefaultValue = 5

Example: Create a list of strings that must all be lowercase and where the default value if unset will be set to unknown:

listValueType = string, listValueTypeProperties = match = [a-z]+, listValueTypeDefaultValue = unknown

string

This is the default value of value types unless the type is specified as anything other than string.

Return type represented by java.lang.String.

Properties:

  • match: Regular expression that data must match, an error being produced if the string doesn't match. Optional.

Example: Match oranges, apples or pears:

match = oranges|apples|pears

Example: Match any string that starts with F and ends in s with any number of alphanumeric characteres in between:

match = F[a-zA-Z0-9]*s

Creating Custom Value Types

Creating custom value types is easy and requires:

  • Implementing a new ValueType;
  • Adding the value type to the ValueTypeFactory;
  • Add the value type to required option and argument configurations.

To create a custom value type, create an appropriately named class that implements ValueType<T>. The following methods must be implemented:

Method Description
T getValue(String data) throws ValueTypeCreationException Given the specified data (typically passed in via data from the command line), attempt to convert to the appropriate type.
void setDefault(String data) throws ValueTypeCreationException Set the default value.
void setProperties(String properties) throws ValueTypeCreationException Set any properties for the type. It is up to callers how properties are defined and parsed.
public String getPackageName() The package name of the underlying converted type. If a primitive type, implementations should return null.
public String getJavaClassName() Get the Java class name of the underlying converted type. If a primitive type, implementations should return null.
public String getJavaPrimitiveName() If a Java primitive, get the name of the Java primitive name used in declarations, for example int, boolean etc.; if the underlying type is not a primitive, implementations should return null.
public String getValueTypeName() Get the name of the value type as recognised by the API; this is the <type> name specified when declaring an option.<config-name>.type = <type> or args.<config-name>.type = <type> line in the CLC file.

It is implementation-specific how properties set via setProperties(String) are parsed. Typically, = is used to assign values to property keys, and comma or semi-colon used to separate properties. This is not required and implementations can define their own syntax for defining how properties are assigned and what method is used to separate properties.

Let's define a new value type to take a file and check that it's size isn't greater than a specified (optional) maximum file size. It will provide a property named maxSize that can be defined to check the file when read in from the command line and check that the file does not exceed the maximum size allowed.

First, the class declaration - the class will be named FileUploadType:

public FileUploadType implements ValueType<File>

Since the value type is java.io.File, this means that when option string values are passed to the value type, the type will be converted to a File.

We'll store the followng member variables in order to create the new value type:

// file to upload:
private File file;
// maximum size of the file, default 5MB:
private long maxSize - 1024 * 1024 * 5;
// property text:
private String maxSizeText;

Prior to T getValue(String) being called by the value type factory, properties are extracted. We need to cater for the property maxSize which will be a private long member of the class, which can also be defined in the same format, e.g. 200MB, etc.

First, the code to parse the property maxSize:

    @Override
    public void setProperties(String properties) throws ValueTypeCreationException {
        if (!properties.contains("=")) {
            throw new ValueTypeCreationException("Invalid properties: " + properties);
        }
        String[] data = properties.split("=");
        String propKey = data[0].trim();
        String propValue = data[0].trim();
        if (!"maxSize".equals(propKey)) {
            throw new ValueTypeCreationException("Invalid property: "
                    + propKey + "; expected 'maxSize'");
        }
        // property value must be a whole number followed by either 'MB' 
        // or 'GB'; use a captured groups to get both parts:
        String regex = "([0-9])+([MmGg][Bb])";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(propValue);
        if (m.matches()) {
            maxSizeText = propValue;
            long size = Long.parseLong(m.group(1));
            String sizeStr = m.group(2);
            if ("MB".equalsIgnoreCase(sizeStr)) {
                maxSize = size * 1024 * 1024;
            } else {
                maxSize = size * 1024 * 1024 * 1024;
            }
        } else {
            throw new ValueTypeCreationException("Invalid value."
                    + " Expected <size><MB|GB>, e.g. 250MB.");
        }
    }

Now the actual call to create the file and check the file size when passed in via the command line:

    @Override
    public File getValue(String data) throws ValueTypeCreationException {
        file = new File(data);
        if (!file.exists() || !file.isFile()) {
            throw new ValueTypeCreationException("File doesn't exist (or"
                    + " is not a file): " + file.getName());
        }
        if (maxSize > 0 && file.length() > maxSize) {
            throw new ValueTypeCreationException("Invalid file size; "
                    + file.getName() + " exceeds maximum size " + maxSizeText);
        }
        return file;
    }

All that's needed to do now is to complete the remaining method implementations.

First, the name of the type as recognised by the type value in the configuration will be file-upload:


    @Override
    public String getValueTypeName() {
        return "file-upload";
    }

Finally for the implementation, the package name and class name will be those of the java.io.File class and the primitive name will be null:

    @Override
    public String getPackageName() {
        return File.class.getPackageName();
    }

    @Override
    public String getJavaClassName() {
        return File.class.getSimpleName();
    }

    @Override
    public String getJavaPrimitiveName() {
        return null;
    }

All that's required now is to add the value type to the API using the following calls. The calls must be made at application start, prior to making any calls to the CLC API:

ValueTypeBuilder<FileUploadType> genericFileUploadType
        = new ValueTypeBuilder<>(FileUploadType.class);
ValueTypeFactory.getInstance().registerValueTypeBuilder(
        "file-upload", genericFileSizeType);

That's it. If an option configuration is added to a CLC file, for example:

option.file-upload.opts = u/upload-ile
option.file-upload.hasArg = true
option.file-upload.argName = file
option.file-upload.description = Files to upload.
option.file-upload.type = file-upload
option.file-upload.properties = maxSize = 150MB

When the CLC file with this entry is parsed, any file passed to the API using the -u/--upload-file option will be checked by the configuration. For example if the file is specified by the argument:

--upload-file /imports/2026-04-05-100113.avi

… And the file size is greater than 150MB, the following error will be produced:

Invalid file size; 2026-04-05-100113.avi exceeds maximum size 150MB

Likewise, the type can be assigned to an argument configuration in very much the same way. Consider the following argument configuration that accepts any number of files (minimum 1 since optional isn't defined as true), each that will be checked for their size:

args.file-upload.argName = files...
args.file-upload.description = Files to upload.
args.file-upload.type = file-upload
args.file-upload.properties = maxSize = 150MB

Note that the above configuration would have to be the last argument configuration in order for it to take advantage of being able to be defined for any number of arguments.

Overriding Existing Value Types

It is possible tp replace existing value types.

All that is required is to create a new implementation (outlined above) of the same name and then remove the existing value type from the value type factory, then add the new implementation.

For example, consider a new implementation of the int value type with class name NewImprovedInt; to remove it call:

ValueTypeFactory.getInstance().removeRegisteredValueType(IntegralType.INTEGRAL);

This is functionally the same as calling

ValueTypeFactory.getInstance().removeRegisteredValueType("int");

Next, add the new implementation:

ValueTypeBuilder<NewImprovedInt> newImprovedInt
        = new ValueTypeBuilder<>(NewImprovedInt.class);
ValueTypeFactory.getInstance().registerValueTypeBuilder("int", newImprovedInt);

All CLC options that are defined as ints will now be constructed using the NewImprovedInt class.