Fork me on GitHub

Basic usage

The approach used by OWNER APIs, is to define a Java interface associated to a properties file.

Suppose your properties file is defined as ServerConfig.properties:

port=80
hostname=foobar.com
maxThreads=100

To access this properties file you need to define a convenient Java interface ServerConfig.java in the same package:

import io.github.qubitpi.owner.Config;

public interface ServerConfig extends Config {
    int port();
    String hostname();
    @DefaultValue("42")
    int maxThreads();

    @DefaultValue("77")
    Optional<Integer> someInteger();
}

Notice that the above interface extends from Config, that is a marker interface recognized by OWNER as valid to work with.

We’ll call this interface the Properties Mapping Interface or just Mapping Interface since its goal is to map Properties into a an easy to use piece of code.

How does the mapping work?

Since the properties file does have the same name as the Java class, and they are located in the same package, the OWNER API will be able to automatically associate them.
For instance, if your mapping interface is called com.foo.bar.ServerConfig, OWNER will try to associate it to com.foo.bar.ServerConfig.properties, loading from the classpath.

The properties names defined in the properties file will be associated to the methods in the Java class having the same name.
For instance, the property port defined in the properties file will be associated to the method int port() in the Java class, the property hostname will be associated to the method String hostname() and the appropriate type conversion will apply automatically, so the method port() will return an int while the method hostname() will return a Java string, since the interface is defined in this way.

The mapping mechanism is fully customizable, as well the automatic type conversion we just introduced is flexible enough to cover most of the Java types as well as object types defined by the user.
You can see how in the next chapters.

Using the Config object

At this point, you can create the ServerConfig object and use it in your code:

ServerConfig cfg = ConfigFactory.create(ServerConfig.class);
System.out.println("Server " + cfg.hostname() + ":" + cfg.port() +
                   " will run " + cfg.maxThreads());

Using @DefaultValue and @Key annotations

Did you notice that in the above example it is specified @DefaultValue("42") annotation?

public interface ServerConfig extends Config {
    int port();
    String hostname();
    @DefaultValue("42")    // here!!!
    int maxThreads();
}

It is used in case the maxThread key is missing from the properties file.

This annotation gets automatically converted to int, since maxThreads() returns an int.

Using the annotations, you can also customize the property keys:

# Example of property file 'ServerConfig.properties'
server.http.port=80
server.host.name=foobar.com
server.max.threads=100

This time, as commonly happens in Java applications, the properties names are separated by dots. Instead of just “port” we have “server.http.port”, so we need to map this property name to the associated method using the @Key annotation.

/*
 * Example of ServerConfig.java interface mapping the previous 
 * properties file.
 */
public interface ServerConfig extends Config {
    @Key("server.http.port")
    int port();

    @Key("server.host.name")
    String hostname();

    @Key("server.max.threads");
    @DefaultValue("42")
    int maxThreads();
}

The @DefaultValue and @Key annotations are the basics to start using the OWNER API.

You can leave the properties file away during development!

During the development you may decide to just use the `@DefaultValue` to provide a default configuration, without really adding the properties file. You can add the properties file later or leave this task to the end user.

Undefined properties

Suppose you have defined a method in your mapping interface that cannot be resolved to any property loaded from a properties file, and this method doesn’t define a @DefaultValue what happens? Simple: it will return null, or a NullPointerException;

Suppose our ServerConfig class was looking like this:

public static interface ServerConfig extends Config {
    String hostname();
    int port();
    Boolean debugEnabled();
}

If we don’t have any ServerConfig.properties associated to it, when we call the method String hostname() it will return null, as well as when we call the method Boolean debugEnabled() since the return types String and Boolean are java objects. But if we call the method int port() then a NullPointerException will be raised.

Don't like the NullPointerException?
We support Optional as the returned config value. For example, instead of int port();, write it as Optional<Integer> port();. To make a default value in this case, simply use the same annotation such as @DefaultValue("8080"). The feature is based on the 3 rationals below:
  1. Method should "return empty arrays or collections, instead of nulls" (Effective Java, Joshua Bloch, 2nd Edition, Item 43)
  2. For better error handling, our methods "don't return null" (Clean Code, Robert C. Martin, 2009, Ch. 7, Don't Return Null)
  3. Oracle designed Optional, which was exactly intended to replace null with a new standard.
This feature allows us to promote null-check at compile-time. Compile-time check is beneficial by reducing runtime-errors.

"Why not using the original's @ConverterClass or a wrapper?"

Because we tried and concluded they are suboptimal in our case:
  1. This would require every single of our applications to load an extra converter class
  2. ConverterClass, i.e. Optional<?>, due to type erasure, loses type information of <?>, which original project does need in order to do proper conversion
At this moment, the fork allows the following config value types to be wrapped inside Optional:
  1. Boolean
  2. Integer
  3. String
  4. All Collection classes that the original project supports
We will keep working to catch up with all types supported by the original project.

Conclusions

Now you know the minimum to get productive with the OWNER API. But this is just the beginning. OWNER is a rich API that allows you to add additional behaviors and have more interesting features, so that you should be able to use this library virtually in any other context where you where using the java.util.Properties class.