Skip to content

Preprocessors

Since version 1.0.9, OWNER allows you to pre-process a property value before it is used by the library. This is useful, for example, to trim values, normalize the case, or strip inline comments from properties files.

A preprocessor is a class implementing the Preprocessor interface:

public interface Preprocessor {
String process(String input);
}

Preprocessors are attached to the configuration through the @PreprocessorClasses annotation, which can be applied on a single method or on the whole interface:

@PreprocessorClasses({ SkipInlineComments.class, Trim.class })
public interface ConfigWithPreprocessors extends Config {
@DefaultValue(" 300 ")
Integer pollingPeriod(); // returns 300: trimmed, then converted
@PreprocessorClasses(ToLowerCase.class)
@DefaultValue(" HelloWorld ")
String helloWorld(); // returns "helloworld"
@DefaultValue("This is the value ! this is an inline comment")
String skipsInlineComments(); // returns "This is the value"
}

…with the preprocessors implemented as:

public class Trim implements Preprocessor {
public String process(String input) {
return input.trim();
}
}
public class ToLowerCase implements Preprocessor {
public String process(String input) {
return input.toLowerCase();
}
}

Since 2.0.0 the class named in the annotation may have any visibility, and so may its constructor: it can be package-private next to the interface that uses it, or a private static class nested inside it.

public class Example {
public interface MyConfig extends Config {
@Key("prop.a")
@DefaultValue("a")
@PreprocessorClasses(ToUpperCase.class)
String propA();
}
private static class ToUpperCase implements Preprocessor {
@Override
public String process(String input) {
return input.toUpperCase();
}
}
}

A preprocessor is an implementation detail of the configuration that names it, and until 2.0.0 it had to be public — so a library using OWNER had to widen its own published API to satisfy ours. It was not being asked to be visible to OWNER, which would be fair: it was being asked to be visible to everyone, and even a package-private class sitting beside the interface was refused.

The same now holds for every class named in an annotation: a Converter, a Tokenizer and a decryptor. What has not changed is that the class needs a constructor taking no arguments — that is a reason no annotation can work around, and it is still refused with the class named in the message. Asked for in #186.

When resolving a property, OWNER applies method-level preprocessors first, then the interface-level ones, in the order they are listed in each annotation. In the helloWorld() example above, the applied chain is ToLowerCase, SkipInlineComments, Trim.

Preprocessing happens on the raw property value, before variables expansion, decryption, parametrized formatting and type conversion: whatever your preprocessors return is what the rest of the pipeline will see.

process(String) is never handed a null. If a property cannot be resolved to any value, a preprocessor is asked through a different method instead — processAbsent(String key), which is given the key and answers null by default, leaving the property absent exactly as before:

public class FromTheVault implements Preprocessor {
public String process(String input) {
return input;
}
@Override
public String processAbsent(String key) {
return vault.lookup(key); // null if the vault has not got it either
}
}

The key comes with the call because without it there would be nothing useful to do: a preprocessor that only learned of the absence could answer with a constant, and a constant is what @DefaultValue already is. With it, a preprocessor can look the value up somewhere OWNER knows nothing about, or name the property in what it throws.

What it returns carries on through the rest of the chain — the preprocessors after it see it through process(String) — and then through expansion, decryption, formatting and conversion, exactly like a value read from a file. What supplied it is not asked to process it again.

Asked for in #188.

To require a value, use @Mandatory instead

processAbsent is for computing a value, not for insisting on one. A property that must be set is @Mandatory: it throws MissingMandatoryPropertyException naming the key, needs no code, and reports every missing property at once when the configuration is created.

The two compose. @Mandatory is checked at creation, and a preprocessor that supplies a value satisfies it rather than failing startup for a property that would have worked — which costs one call to processAbsent then, and another when the property is first read.

Preprocessor classes need a public no-arg constructor

OWNER instantiates the classes listed in @PreprocessorClasses via reflection, so they must be public and provide a public default constructor (nested classes must be static).

Project maintained by Matteo Baccan, and the awesome contributors.

Developed with IntelliJ IDEA

Hosted on GitHub