ConversionModule

The ConversionModule offers conversion between compatible and otherwise incompatible data types. This has the benfit of convenience: from any place in the code you can call the ConversionModule to convert data from one type to another without having to worry about the implementation details, all you need to know is whether or not the conversion succeeded. Other benefits are that you can ensure type safety within your application at any point, that conversion can happen between otherwise incompatible types and that you can implement custom and/or override existing conversion methods.

The ConversionModule itself is modular. Conversion happens through converters which can be registered and unregistered on the fly. The ConversionModule registers a number of default converters for common data types, but custom implementations can and should be created to provide type conversion between types from different libraries and frameworks.

Converting an object

The convert(Object, Class) method in the ConversionModule is called to begin conversion. The given Object is the Object that needs to be converted, the Class is the class of the type you want the Object to be converted to. Both are non-null.

Step 1: DataConversionOverride

First the registered DataConversionOverrides are called to see if regular conversion should be overridden. If any of the DataConversionOverride return a non-null value, it is returned. An example of an override is used by the DataPathModule. Whenever a value represents a data path and that path represents a legal value of the requested type it is returned.

Step 2: DataConverter

If it returns a null value we start looking for a DataConverter. DataConverters are registered to one specific type. We look for a DataConverter registered for the given return type and if found, ask it to convert the Object.

Step 3: SuperTypeDataConverter

If no DataConverter is found, we look for a SuperTypeDataConverter. These are converters that are also registered to one type, but these respond to any type that subclasses it. This is useful to avoid writing the same code over and over for the same implementations, or when you cannot know beforehand which implementations will be available. An example of this is the CollectionConverter. It is common practice to use a zero-argument constructor for implementations of the Collection interface, thus the same converter can be used for every type of Collection. This allows us to convert all implementations of an interface without knowing all the implementations beforehand. A similar use case is the EnumConverter, which handles default conversion for all Enum types, converting String input to an upper case string with underscores as spaces and uses the static Enum.valueof(Class, String) call. Should a custom conversion for one of it’s subclasses still be required, a regular DataConverter can be registered in order to override a super type converter.

If at any point during conversion the Object could not be converted, a ConversionException is thrown with a message detailing what went wrong. This message is user-friendly and can be displayed to users.

Summary

In summary, conversion happens in this order
DataConversionOverride – accepts any object, if successful overrides conversion
DataConverter – accepts only objects of the type it is registered to
SuperTypeDataConverter – accepts any object whose type is a sub class of it’s registered type.

Storage

A Storage is a type of DataHolder that handles String-Object mappings and is tied to a ConversionModule to handle conversion. The Storage interface defines many useful methods to retrieve and convert values and ensure type safety. For example simple values can be retrieved through the get(String, Class) method. If the value found at the given key is already of the given type or can be converted by the ConversionModule to the correct type it will be returned, otherwise null is returned. If a value of a specific type is required to be present in the Storage at a given key, getAndAssert(String, Class) can be used instead. This will throw an exception if the value could not be found.

To get collections getCollection(String, Class, Class) can be used to specify collection type and element type respectively. For maps: getMap(String, Class, Class, Class) for map type, key type and value type.

A Storage can also contain other Storage objects. This is a common case when reading YML-JSON style configurations.