Type conversion
OWER API supports properties conversion for primitive types and enums.
When you define the mapping interface you can use a wide set of return types,
and they will be automatically converted from String
to the primitive types
and enums:
// conversion happens from the value specified in the
// properties files (if available).
int maxThreads();
// conversion happens also from @DefaultValue
@DefaultValue("3.1415")
double pi();
// enum values are case sensitive!
// java.util.concurrent.TimeUnit is an enum
@DefaultValue("NANOSECONDS");
TimeUnit timeUnit();
It is possible to have configuration interfaces to declare business objects as return types, many are compatible and you can also define your own objects:
The easiest way is to define your business object with a public constructor
taking a single parameter of type java.lang.String
:
public class CustomType {
private final String text;
public CustomType(String text) {
this.text = text;
}
public String getText() {
return text;
}
}
public interface SpecialTypes extends Config {
@DefaultValue("foobar.txt")
File sampleFile();
@DefaultValue("https://matteobaccan.github.io/owner")
URL sampleURL();
@DefaultValue("example")
CustomType customType();
@DefaultValue("Hello %s!")
CustomType salutation(String name);
}
OWNER API will take the value “example” and pass it to the CustomType constructor then return it.
Arrays and Collections
OWNER have first class support for Java Arrays and Collections.
So now you can define properties like:
public class MyConfig extends Config {
@DefaultValue("apple, pear, orange")
public String[] fruit();
@Separator(";")
@DefaultValue("0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55")
public int[] fibonacci();
@DefaultValue("1, 2, 3, 4")
List<Integer> ints();
@DefaultValue(
"http://aeonbits.org, http://github.com, http://google.com")
MyOwnCollection<URL> myBookmarks();
// Concrete class are allowed (in this case java.util.Stack)
// when type is not specified <String> is assumed as default
@DefaultValue(
"The Lord of the Rings,The Little Prince,The Da Vinci Code")
Stack books();
}
You can use array of objects or primitive Java types, as well as Java
collections, as specified by interfaces Collection
,
List
, Set
, SortedSet
or concrete
implementations like Vector
, Stack
,
LinkedList
etc. or your own concrete implementation of the Java
Collections Framework interfaces, as long as your implementation class defines
a default no-arg constructor.
The Map
interface and sub-interfaces are not supported.
By default OWNER uses the comma ","
character to tokenize values for the
arrays and collections, but you can specify different characters (and regexp)
with the @Separator
annotation or, if your property format has a
more complex split logic, you can define your own tokenizer class via the
@TokenizerClass
annotation plus Tokenizer
interface.
Example:
public class MyConfig extends Config {
@Separator(";")
@DefaultValue("0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55")
public int[] fibonacci();
@TokenizerClass(CustomDashTokenizer.class)
@DefaultValue("foo-bar-baz")
public String[] withSeparatorClass();
}
public class CustomDashTokenizer implements Tokenizer {
// this logic can be as much complex as you need
@Override
public String[] tokens(String values) {
return values.split("-", -1);
}
}
The @Separator
and @TokenizerClass
annotations can be specified on method level and on class level. When specified
on method level, the annotation will affect only that method. When specified on
class level, the annotation will affect the complete class.
Annotations specified on method level override the setting specified on the class level:
@Separator(";")
public interface ArrayExample extends Config {
// takes the class level @Separator
@DefaultValue("1; 2; 3; 4")
public int[] semicolonSeparated();
// overrides the class-level @Separator(";")
@Separator(",")
@DefaultValue("1, 2, 3, 4")
public int[] commaSeparated();
// overrides the class level @Separator(";")
@TokenizerClass(CustomDashTokenizer.class)
@DefaultValue("1-2-3-4")
public int[] dashSeparated();
}
@Separator and @TokenizerClass don't go together!
Notice that it is invalid to specify together on the same level both@Separator
and @TokenizerClass
annotations:
you cannot specify two different ways to do the same thing!
So in following cases you’ll get a UnsupportedOperationException
:
// @Separator and @TokenizerClass cannot be used together
// on class level.
@TokenizerClass(CustomCommaTokenizer.class)
@Separator(",")
public interface Wrong extends Config {
// will throw UnsupportedOperationException!
@DefaultValue("1, 2, 3, 4")
public int[] commaSeparated();
}
public interface AlsoWrong extends Config {
// will throw UnsupportedOperationException!
// @Separator and @TokenizerClass cannot be
// used together on method level.
@Separator(";")
@TokenizerClass(CustomDashTokenizer.class)
@DefaultValue("0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55")
public int[] conflictingAnnotationsOnMethodLevel();
}
But even though the following example contains a conflict on class level (and should be considered a bug in the example), OWNER is able to resolve things correctly on method level:
// @Separator and @TokenizerClass cannot be used together
// on class level.
@Separator(";")
@TokenizerClass(CustomDashTokenizer.class)
public interface WrongButItWorks extends Config {
// but this overrides the class level annotations
// hence it will work!
@Separator(";")
@DefaultValue("1, 2, 3, 4")
public int[] commaSeparated();
}
It is not recommended to have above wrong annotations setup: it is considered a bug in the code, and even if this setup works at the moment, we may change this behavior in future.
The @ConverterClass annotation
OWNER provides the
@ConverterClass
annotation that allows the user to specify a customized conversion logic implementing the
Converter
interface.
interface MyConfig extends Config {
@DefaultValue("foobar.com:8080")
@ConverterClass(ServerConverter.class)
Server server();
@DefaultValue(
"google.com, yahoo.com:8080, matteobaccan.github.io/owner:4000")
@ConverterClass(ServerConverter.class)
Server[] servers();
}
class Server {
private final String name;
private final Integer port;
public Server(String name, Integer port) {
this.name = name;
this.port = port;
}
}
public class ServerConverter implements Converter<Server> {
public Server convert(Method targetMethod, String text) {
String[] split = text.split(":", -1);
String name = split[0];
Integer port = 80;
if (split.length >= 2)
port = Integer.valueOf(split[1]);
return new Server(name, port);
}
}
MyConfig cfg = ConfigFactory.create(MyConfig.class);
Server s = cfg.server(); // will return a single server
Server[] ss = cfg.servers(); // it works also with collections
In the above example, when calling the method servers()
that returns an array of Server objects, the ServerConverter
will be used several times to convert every single element. In any case the ServerConverter in the above example always
works with a single element.
To see the complete test cases supported by owner see ConverterClassTest on GitHub.
All the types supported by OWNER
But there is more. OWNER API supports automatic conversion for:
- Primitive types: boolean, byte, short, integer, long, float, double.
- Enums (notice that the conversion is case sensitive, so FOO != foo or Foo).
- java.lang.String, of course (no conversion is needed).
- java.net.URL, java.net.URI.
- java.io.File (the character
~
will be expanded touser.home
System Property). - java.lang.Class (this can be useful, for instance, if you want to load the jdbc driver, or similar cases).
- Any instantiable class declaring a public constructor with a single argument of type
java.lang.String
. - Any instantiable class declaring a public constructor with a single argument of type
java.lang.Object
. - Any class declaring a public static method
valueOf(java.lang.String)
that returns an instance of itself. - Any class for which you can register a
PropertyEditor
viaPropertyEditorManager.registerEditor()
. (See PropertyEditorTest as an example). - Any array having above types as elements.
- Any object that can be instantiated via
@ConverterClass
annotation explained before. - Any Java Collections of all above types: Set, List, SortedSet or concrete implementations like LinkedHashSet or user
defined collections having a default no-arg constructor.
Map
and sub-interfaces are not supported.
If OWNER API cannot find any way to map your business object, you’ll receive a UnsupportedOperationException
with some meaningful description to identify the problem as quickly as possible.
You can also register your custom PropertyEditor
to convert text properties into your business objects
using the static method PropertyEditorManager.registerEditor()
.
See also PropertyEditorSupport
, it may be useful if you want to implement a PropertyEditor
.
Converter classes shipped with OWNER
Since specifying duration and byte size values in configuration files is very common,
OWNER ships with converter classes for these as well as some classes for the types themselves.
The code relies on Java 8 features and therefore, these classes are a part of the owner-java8-extras
module. Also, you have to specify the @ConverterClass
annotation explicitly for these converters, they are not
applied automatically as is the case for the primitive (and more) types as described above.
Duration
For duration, the DurationConverter
class is provided which converts configuration strings to
java.time.Duration
.
Example:
public class DurationConfig extends Config {
@ConverterClass(DurationConverter.class)
@DefaultValue("10 ms")
Duration getTenMilliseconds();
@ConverterClass(DurationConverter.class)
@DefaultValue("10d")
Duration getTenDays();
// The DurationConverter class also supports
// ISO 8601 time format as described in the
// JavaDoc for java.time.Duration.
@ConverterClass(DurationConverter.class)
@DefaultValue("PT15M")
Duration iso8601FifteenMinutes();
}
The suffixes supported by DurationConverter are:
ns
,nano
,nanos
,nanosecond
,nanoseconds
us
,µs
,micro
,micros
,microsecond
,microseconds
ms
,milli
,millis
,millisecond
,milliseconds
s
,second
,seconds
m
,minute
,minutes
h
,hour
,hours
d
,day
,days
Byte Size
The Java API does not provide any classes to represent data sizes. Therefore,
OWNER provides this functionality with a set of classes in the
io.github.qubitpi.owner.util.bytesize
package: ByteSize
and ByteSizeUnit
.
The usage of these classes is best explained with an example:
[...]
ByteSize oneByte = new ByteSize(1, ByteSizeUnit.BYTES);
ByteSize oneMegaByte = new ByteSize(1, ByteSizeUnit.MEGABYTES);
// Units can be converted
ByteSize mbAsGb = oneMegaByte.convertTo(ByteSizeUnit.GIGABYTES);
// Both IEC and SI units are supported
ByteSize mbAsGiB = oneMegaByte.convertTo(ByteSizeUnit.GIBIBYTES);
// Get the number of bytes a ByteSize represents as a long
long oneMegaByteAsLong = oneMegaByte.getBytesAsLong();
For converting configuration strings into the ByteSize
type, the
ByteSizeConverter
class is provided.
Example:
public interface ByteSizeConfig extends Config {
@ConverterClass(ByteSizeConverter.class)
@DefaultValue("10 byte")
ByteSize singular10byteWithSpace();
@ConverterClass(ByteSizeConverter.class)
@DefaultValue("10byte")
ByteSize singular10byteWithoutSpace();
@ConverterClass(ByteSizeConverter.class)
@DefaultValue("10 bytes")
ByteSize plural10byte();
@ConverterClass(ByteSizeConverter.class)
@DefaultValue("10m")
ByteSize short10mebibytes();
@ConverterClass(ByteSizeConverter.class)
@DefaultValue("10mi")
ByteSize medium10mebibytes();
@ConverterClass(ByteSizeConverter.class)
@DefaultValue("10mib")
ByteSize long10mebibytes();
@ConverterClass(ByteSizeConverter.class)
@DefaultValue("10 megabytes")
ByteSize full10megabytes();
}
The suffixes supported by ByteSizeConverter are:
byte
,bytes
,b
kibibyte
,kibibytes
,k
,ki
,kib
kilobyte
,kilobytes
,kb
mebibyte
,mebibytes
,m
,mi
,mib
megabyte
,megabytes
,mb
gibibyte
,gibibytes
,g
,gi
,gib
gigabyte
,gigabytes
,gb
tebibyte
,tebibytes
,t
,ti
,tib
terabyte
,terabytes
,tb
pebibyte
,pebibytes
,p
,pi
,pib
petabyte
,petabytes
,pb
exbibyte
,exbibytes
,e
,ei
,eib
exabyte
,exabytes
,eb
zebibyte
,zebibytes
,z
,zi
,zib
zettabyte
,zettabytes
,zb
yobibyte
,yobibytes
,y
,yi
,yib
yottabyte
,yottabytes
,yb