Salta ai contenuti

Questa pagina non è ancora tradotta in italiano: quello che segue è il testo inglese.

What's new

Questi contenuti non sono ancora disponibili nella tua lingua.

Every version of OWNER and what changed in it, newest first. The one at the top, 2.0.0, has not been released yet.

The 1.0.x announcements were written by Luigi R. Viggiano, the original author of OWNER, at the time of each release, and are kept as they were written — including the first person, and the links that have since gone stale — because they are a record of what happened rather than current documentation. The 2.0.0 one is by Matteo Baccan, who maintains the project now.

Published artifacts are listed in the releases on GitHub.

Version 2.0.0 is the first release since the project maintenance moved from Luigi Viggiano to Matteo Baccan. It brings a set of new features, a security hardening pass, a fully modernized build infrastructure, and a long list of dependency updates accumulated since 1.0.12. Java 8 is now the minimum runtime, which allowed the removal of some compatibility leftovers dating back to Java 6/7: see the “Removals” section below for the (short) migration instructions.

This release was prepared as 1.0.13 and renumbered before publication, because four of its changes alter the result of a configuration that used to work, and a patch number would have been a quiet place to put them. None is expected to affect a real configuration — the whole test suite of the project passes unchanged, and each is described in full below — but the number should say so rather than the changelog alone:

  • Braces are matched, not counted from the left. Up to 1.0.12 a ${ was closed by the first } that followed it; now it is closed by the one that matches it, which is what makes nested variables possible. Only the ${ sequence opens a level, so a lone brace inside an expression is still ordinary text. Should an unforeseen combination of braces read differently, -Downer.nested.variable.expansion=false restores the previous behaviour for the whole JVM, by running the previous implementation unchanged.

  • A circular variable reference is an error. A property whose value leads back to itself used to exhaust the stack, or — for the shape a=${a:default} — to produce an empty string. It now throws an IllegalArgumentException naming the chain. No cycle ever produced a useful value, but a configuration that quietly resolved to the empty string will now fail loudly, which is the point.

  • Repeated sibling elements in an XML source are numbered. Two elements of the same name under the same parent used to write the same key, so the second overwrote the first and every value but the last was lost without a word. They now become parent.tag[0] and parent.tag[1], which is what a list is read from, and there is no longer a parent.tag. An element that occurs only once is untouched and keeps its plain key, so a document with no repetition reads exactly as it did. What changes is the reading of documents that were already losing data: if a configuration reads parent.tag from an XML that repeats that element, it was getting the last of several values, chosen by nothing better than document order.

  • A registered type converter belongs to the factory it was registered on. setTypeConverter and removeTypeConverter are instance methods of the Factory, and until now they wrote a map every factory in the JVM shared: registering a converter on one factory converted the values of configurations created by all the others, and removing it on one took it away from every one of them. A configuration created by ConfigFactory.newInstance() is isolated in its properties, its loaders, its value handlers, its prefix and its strictness — conversion was the one thing that leaked. The static ConfigFactory.setTypeConverter is the default factory, exactly as setProperty and registerLoader are. If you registered a converter statically and created your configurations from a factory of your own, register it on that factory instead: it is the only code this can affect, and it is a one-line move. What has not changed is when the registry is read — registering a converter still changes what an existing Config object answers, and removing it changes it back.

Everything else is additive. In particular, a Map return type used to throw on every access, so the new grouping behaviour described below cannot change the result of any configuration that worked.

That claim was measured rather than argued: the test suite released with 1.0.12 was compiled and run against this version, which is the one test that asks what an upgrade actually costs. 216 tests, 212 green. The four that are not divide into two:

  • two are a feature of this release, doing its job. testNonInstantiableTokenizer and testConverterCantBeAccessed asserted that a private class named in an annotation cannot be instantiated — an UnsupportedOperationException caused by an IllegalAccessException. It can now, on purpose (see A class named in an annotation no longer has to be public, below), so the two old tests fail by proving the change;
  • two are the test harness rather than the library. PropertiesInvocationHandlerTest puts a Mockito @Spy on java.util.Properties, whose internals a spy does not carry on a current JDK. No API of this library is involved.

Seventeen of the ninety-one test files do not compile against 2.0.0 at all, and every one of them for the same reason: they reach for an internal utility this release removed — org.aeonbits.owner.util.Base64 and the Util.save, Util.delete, Util.saveJar, Util.eq, Util.debug and Util.newArray helpers, all listed under Removals below — or they construct PropertiesManager and PropertiesInvocationHandler, which are package-private and never were API. Reimplementing four of those helpers on the JDK methods the removals point at was five lines, after which the suite compiled.

So: no unintentional change of behaviour was found in the public API. The differences are the removals this page documents and one addition that makes something work where it used to be refused.

Beyond the individual changes, this release has an explicit goal: to bring the test coverage as high as it can practically go, and to bring the number of warnings reported by the static analysers down to zero. A library that other projects depend on for their configuration has to be trustworthy first and featureful second, and after several years without a release the most valuable thing to do was to verify — line by line — that the existing behaviour is the intended one. The “Code quality and test coverage” section below explains what this means in practice, and what it turned up.

OWNER v2.0.0 contains following enhancements and bug fixes.

Java 8 is the minimum runtime required by this release, and the machinery that existed only to support older JVMs is gone. If you are affected, migration is a one-liner in each case:

  • The owner-java8 artifact is gone. Its only feature, the support for default methods in config interfaces, is now built into the core: replace the owner-java8 dependency with owner and everything keeps working.
  • The owner-java8-extras artifact is gone. The DurationConverter, ByteSizeConverter and the ByteSize/ByteSizeUnit/ByteSizeStandard classes it contained are now part of the core owner artifact, with unchanged package names: replace the owner-java8-extras dependency with owner, and no import changes. They were shipped apart only because the core had to run on Java 6 and could not so much as name java.time.Duration; with Java 8 as the minimum that reason is gone, and neither the converters nor the byte size classes bring a dependency of their own. owner-extras is left with what actually needs a third party library on the classpath — the ZooKeeper loader.
  • ZooKeeperLoader moved from org.aeonbits.owner.loaders to org.aeonbits.owner.extras.loaders: change the import, nothing else. It was the one class of owner-extras sitting under a package the core artifact also owns, and a package cannot live in two modules: as long as it did, the two jars could never both be put on the module path, whatever name they declare. The class itself is unchanged, it never used anything package-private, and the Loader interface it implements stays exactly where it is — a custom loader of your own is unaffected, since it lives in a package of yours and only imports org.aeonbits.owner.loaders.Loader.
  • The internal utility class org.aeonbits.owner.util.Base64 is gone. It was a runtime-selection shim between java.util.Base64 (Java 8+) and javax.xml.bind.DatatypeConverter (Java 6/7), never used by the library API itself. If you referenced it, use java.util.Base64 directly: Base64.encode(bytes) becomes Base64.getEncoder().encodeToString(bytes) and Base64.decode(string) becomes Base64.getDecoder().decode(string).
  • The internal utility method org.aeonbits.owner.util.Util.eq(a, b) is gone: it predated java.util.Objects and did exactly what the standard Objects.equals(a, b) does — use that instead.
  • The test-support methods Util.save(File, Properties), Util.saveJar(File, String, Properties) and Util.delete(File) are gone from the public API: they were never used by the library itself and now live in the test suite. If you relied on them, the standard Properties.store, java.util.jar.JarOutputStream and java.nio.file.Files APIs cover the same ground.
  • A converter can be registered as an object, and not only as a class: factory.setTypeConverter(Duration.class, injector.getInstance(MyConverter.class)). That is the only shape a dependency injection container can hand over — a converter needing a collaborator of its own, an ObjectMapper or a data source, cannot be built out of a no-argument constructor — and it is what #222 asked for in 2018. registerLoader and registerValueHandler have always taken objects; this is the third. A converter named by a class is still built again for every conversion, so it must have no state; the object you register is the one that is used, for as long as the factory lives, which makes it yours to make thread safe. Converter is now Serializable, like Loader and ValueHandler, because a registered object travels with the configurations that factory created. The whole picture — what a container can supply and what it cannot — is under With a dependency injection container.

  • A configuration can be shown as itself. One properties file read by several mapping interfaces — one file to hand out, one interface per module — gave every one of those configurations the whole file: list(), store(), propertyNames() and toString() showed the other modules’ keys, the environment and the imports besides, so printing a configuration to a log printed somebody else’s database. @DeclaredOnly on the interface, or owner.declared.only on the Factory for the interfaces you did not write, restricts every view to the properties that interface declares — inherited ones included, sections included. It is #150, asked in 2015. It restricts what is shown and never what is loaded, which is not a nicety: a ${...} is resolved against the other properties, so a configuration that loaded only its own keys could not expand a variable pointing outside them. getProperty(key) is left unrestricted for the same reason.

  • A key that holds a variable is listed as it is read. @Key("${myproject.prefix}.debug") is read as myproject.debug, but the key as written is where a @DefaultValue is registered — so it was a real entry in the properties and every listing showed both, saying the configuration had two properties where it had one. Every view now shows the key that is read, and where both exist the loaded value wins, exactly as the lookup does: #230. The same rule reached save(File), where naming the key as written meant writing a property nothing can read and leaving the real one to whoever else reads the file — losing the value outright into a file that did not have the key yet.

  • A {0} in a parametrized property no longer fails in silence. This library formats with java.util.Formatter and a java.text.MessageFormat pattern is not a broken format string but a correct one in another dialect, so String.format raised nothing, returned the value as written and dropped the arguments without a word — which is what #118 was opened about in 2015. It is reported once per key now, with what to do instead. MessageFormat itself is still not supported and deliberately: a configuration binder is not an i18n engine, and a default method formats it in two lines with any formatter you like.

  • A properties file can be generated from a mapping interface, from the command line — java -cp app.jar:owner.jar org.aeonbits.owner.TemplateTool --into src/main/resources com.acme.MyConfig — which is what #3 asked for in 2013, the oldest issue this project had. It writes the @DefaultValue of each method with its @Description above it, where the convention looks for the file; it reads no source, so what comes out is what the code says rather than what the machine it ran on holds; it needs no Accessible on the interface, which save(File) does; and run twice it keeps what you edited in between, being the same writer.

  • Mutable.loadFromXML(InputStream), which #62 asked for in 2013. It is not a delegate to java.util.Properties.loadFromXML: it reads the document the way an XML source is read, so an XML of your own — <server><http><port>8080</port></http></server> — loads as server.http.port=8080, where Properties refuses it. It closes the asymmetry with storeToXML, which has been on Accessible since 1.0.5 with nothing on this side to read back what it writes. The stream is closed when the method returns, as the JDK’s loadFromXML closes its own.

  • A rolled-back change is no longer silent. A transactional listener may refuse a property change or a whole batch, and until now the refusal reached nobody but the listener that made it: the exception was caught and discarded, setProperty returned the old value, and “the property did not change and I cannot see why” had no answer. The library says it now, at CONFIG — what was refused, and the listener’s own message if it gave one. At CONFIG and not as a warning, because a listener refusing is a listener working: #58 asked for a RollbackListener, and what it wanted was to be told.

  • The conventional file can be named, with owner:default — the constant Config.Sources.CONVENTIONAL. Written among the sources of @Sources it stands, in place, for everything the configuration would look for if it declared none, so a configuration can have its own sources and the file named after it without spelling out the package:

    @LoadPolicy(LoadType.MERGE)
    @Sources({"file:~/myapp.conf", "system:env", Sources.CONVENTIONAL})
    public interface MyConfig extends Config { }

    The name is that of the interface handed to the ConfigFactory, not of the one carrying the annotation, so a base interface can send twenty configurations each to its own file. Followed by an extension — owner:default.xml — it stands for that one conventional source instead of all of them, which is how a single format is asked for and how an order of your own is written: {"owner:default.yaml", "owner:default.properties"} reads the YAML when it is there and the properties when it is not. An extension no loader offers is refused rather than passed over, since it is not a source that happens to be missing but one that could never exist. Closes the first half of #267; the second half — reading CORE_THREAD_NUMBER for core.thread_number — is what the relaxed binding above does.

  • A property may be spelt the way the file spells it, which closes #116, open since 2015. String firstName() now finds firstName, first-name, first_name or FIRST_NAME — and the last of those is a name a shell can actually set, so server.maxThreads is looked for as SERVER_MAX_THREADS and not as SERVER.MAX_THREADS: the environment form replaces every character that is not a letter or a digit with an underscore, which is the rule MicroProfile Config mandates and Spring Boot documents. The other three keep the shape of the key:

    first-name = Luigi

    Four forms and no more. Spring Boot 1 matched loosely — separators dropped, case ignored — and Boot 2 deliberately narrowed it; this is the narrow side of that split, and firstname, FirstName and first.name are not spellings of this key. The forms are derived from the key, so @Key("first-name") is equally found under FIRST_NAME, and one form applies to the whole key at once — prefixes and nesting included, so my-db.user-name is a spelling of myDb.userName and a whole section comes along with it.

    A value that was written beats one that was only defaulted, whichever spelling holds it; among written values the key the method resolves to comes first. Two spellings of one property in one configuration mean that one of them is inert, so it is reported as a WARNING naming both — and refused outright under owner.strict.

    Nothing is added to the properties and nothing is renamed: store(), list() and propertyNames() show the keys exactly as they were loaded, getProperty("firstName") still answers about firstName, and a Traceable origin stays attached to the key that really exists. The prefix a Map or an indexed list scans is matched as written, since there the prefix decides which keys are the group. On by default, off per method or per interface with @DisableFeature(RELAXED_BINDING). See How the key may be written.

  • @Min(12) int port() is finally checked, and a constraint nobody checks now says so. Bean Validation has always worked against an OWNER configuration — provided the methods were named as JavaBean getters. Validator.validate(config) walks properties, so @Min(12) int getPort() was a property called port and was checked, while @Min(12) int port() — the spelling this documentation teaches — was neither a property nor a field and was passed over without a word, with Hibernate Validator and Apache BVal alike. That is #201, open since 2018, and the dangerous half of it was the silence rather than the missing check.

    Put owner-extras and a validation provider on the class path and every constrained property is checked when the configuration is created, next to the @Mandatory check and for the same reason:

    org.aeonbits.owner.validation.ConfigValidationException:
    ServerConfig: 2 properties do not satisfy the constraints declared on them:
    'port' (port()): must be greater than or equal to 12;
    'hostname' (hostname()): must not be null

    Every violation in one exception, each naming the key — the line to go and change — and never the value, which would put a rejected password in a log. Sections are walked into, so a violation inside one arrives as server.port; Optional<@Min(12) Integer> and List<@Min(12) Integer> are unwrapped by the provider as the specification says; and a getter-named method is reported once, not once as a property and again as a method.

    The silence is the part that is gone. Four method shapes cannot be checked at creation time — one taking arguments has no key until it is called, a default method is your own code, a nested-section accessor is a view that is never null, and a constraint written on an Optional applies to the container — and each of them is now named, with its reason, as a WARNING, or refused outright under -Downer.strict=true. So is a configuration carrying constraints that nothing on the class path can check. An interface whose annotations belong to another framework says so with @DisableFeature(VALIDATION), which turns off the report as well as the check.

    Both spellings of the specification are supported — javax.validation for the Java 8 world this library still compiles for, jakarta.validation for everything current — and both are optional dependencies of owner-extras: nothing is shipped, nothing is transitive, and a configuration with no constraint on it pays nothing at all. ConfigValidator is a service like a loader, so another idea of what a constraint means can be plugged in from outside.

  • A value can name what decrypts it, and a cipher is finally shipped. Until now this library shipped none: org.aeonbits.owner.crypto was an SPI and a no-op, and the only concrete implementation lived in the test suite, where the documentation reproduced its source for you to copy. That class is AES/ECB with the passphrase used as the raw key, so the same plaintext always gave the same ciphertext and a file disclosed which of its secrets were equal. If you copied it, the Crypto support page now says what to do about it.

    In its place, a marker in the value:

    db.password = ${$aes-gcm::AAM0UBtPtHU9kZcgvqX673gZTlmMpp4RxRWoHOoDUGjJ...}
    jdbc.url = jdbc:h2:mem:test?password=${db.password}
    ConfigFactory.registerValueHandler(new AesGcmHandler(passphrase));

    Nothing goes on the interface, and the passphrase never comes from the properties — which would be the secret protecting the file, kept in the file. Two ciphers come with it:

    • ${$aes-gcm::…} — AES-256/GCM with a random IV per value, PBKDF2-HMAC-SHA256 at 210,000 iterations, salt and iteration count travelling in the token. One passphrase, which both writes and reads.
    • ${$rsa-oaep::…} — a key pair, so that whoever adds a secret to a configuration cannot read the ones already there. RSA-OAEP wrapping a per-value AES-256/GCM key, since RSA cannot encrypt an arbitrary value.

    EncryptTool, in the same jar, turns values into markers:

    $ printf 's3cr3t

hunter2 ’ | OWNER_PASSPHRASE=‘…’ java -cp owner-2.0.0.jar org.aeonbits.owner.handlers.EncryptTool > markers.txt

Neither the passphrase nor the values may be command-line arguments, and the tool refuses them there: a
command line stays in the shell history and is visible in `ps`.
**Being expansion is what makes it worth having.** `fill()` gets the secret, a value that refers to it
gets the secret rather than the ciphertext, and `store()` writes the marker back because the properties
hold its text and not its answer. Those last two are exactly what
[#285](https://github.com/matteobaccan/owner/issues/285) and half of
[#287](https://github.com/matteobaccan/owner/issues/287) reported, and they are settled by construction
rather than by a second mechanism.
`@EncryptedValue` and `@DecryptorClass` are **not deprecated** and still work as they always did — they
are what every configuration written before 2.0.0 uses. The one thing refused is carrying both on one
method, since expansion runs first and the decryptor would then be handed the plain secret.
* **JNDI is readable as a source**, in the `owner-extras` artifact, which closes
[#143](https://github.com/matteobaccan/owner/issues/143) — what a container binds, taking part in a
`MERGE` like any other source:
```java
@LoadPolicy(LoadType.MERGE)
@Sources({
"jndi:comp/env/myconfig",
"file:~/myconfig.properties",
"classpath:myconfig.properties" })
public interface MyConfig extends Config { }

A relative name is resolved against java:comp/env/, a java: name is used as written, and subcontexts are flattened with a dot like every other tree-shaped format. It needs no dependency, since JNDI is in the JDK. A binding that is not a scalar — a DataSource, a UserTransaction — is skipped and named at CONFIG rather than refused, because refusing a whole context over one of them would make the loader useless in the container it exists for. For a single entry rather than a context there is ${$jndi::comp/env/db/password}.

Only java: names are accepted, and there is deliberately no setting to allow others. A JNDI name carries its own scheme and InitialContext follows it over the network, so jndi:ldap://somewhere/x would be a configuration file turning into a request to somebody else’s server — and a @Sources spec is expanded before it is read, so it need not even be a constant. To reach a provider elsewhere, construct new JndiLoader(environment) in Java, where the decision sits next to the credentials it needs. That is the same rule this release applies to an encryption passphrase.

  • ValueHandler, the mechanism underneath it, is not about cryptography. OWNER reads the envelope — the $, the name, the :: — and hands everything after it to the handler as text. So a handler of your own is a two-method class:

    api.token = ${$vault::secret/data/app:v2}
    tls.key = ${$file::/run/secrets/tls.key}

    Handlers are registered and never discovered on the class path: a file format found there reads files that are already yours, while a handler found there would answer for the values inside them. A marker naming a handler nobody registered is an error rather than the empty string, which for a password is the worst answer available. See ValueHandlerExample.

  • A class named in an annotation no longer has to be public. A Preprocessor, a Converter, a Tokenizer and a decryptor may now be package-private, or a private static class nested inside the interface that names them, and their constructor may be private too:

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

    Each of these is an implementation detail of the configuration that names it, and requiring them to be public meant that a library using OWNER had to widen its own published API to satisfy ours. The class was not being asked to be visible to OWNER, which would be fair: the instantiation lives in org.aeonbits.owner.util, so “the same package” was never true of anybody else’s code and even a package-private class beside the interface was refused. What has not changed is that the class needs a constructor taking no arguments, which is still refused with its name in the message. Asked for in #186 in 2016. The converter also stops being built with Class.newInstance(), deprecated since Java 9, which swallowed the constructor’s own exception.

  • The hot reload interval can come from outside the interface. @HotReload(interval = "${ttl}") takes the time between two checks as text, with a ${variable} expanded from the properties of the ConfigFactory, the system properties and the environment — the same three, in the same order, that expand a @Sources spec. Five seconds in development and five minutes in production stop being two interfaces, or two builds:

    @HotReload(interval = "${owner.reload.interval}", type = ASYNC)
    @Sources("file:/etc/myapp/myapp.properties")
    interface MyConfig extends Config, Reloadable { }

    The value carries its own unit — 500ms, 30s, 5m, PT1H30M — so unit is not consulted, and a bare number is refused: the duration syntax reads one as milliseconds while @HotReload(5) next door means five seconds, and two neighbouring attributes cannot mean different things by the same digits. A value that is not a duration, a variable nobody set, and an interval that is not positive are all refused when the configuration object is created rather than at the first check. value and unit are untouched and still decide when interval is not written, so no existing configuration changes. Asked for in #179 in 2016, where the shape it took — a new attribute rather than a change to the type of value — was already the one Luigi Viggiano argued for. See Hot reload.

  • TOML is read, from a source whose path ends in .toml, and MyConfig.toml joins the names tried when a configuration declares no @Sources. Unlike YAML, TOML has a written specification and a conformance suite anyone can run, which is why it is parsed here rather than delegated the way HOCON is — and why the target is the whole of v1.0.0 rather than a subset we choose. toml-test runs in every build: every one of its 499 documents that must be refused is refused, and 204 of the 210 that must be read are read exactly as it expects. The six are an empty key and a dot inside a quoted key — the two places where TOML and this library’s flattening convention disagree, which is a decision about the convention rather than a gap in the parser.

    TOML is the format this library’s flattening convention was already shaped like: an [[array of tables]] is servers[0].host, a dotted key is the flattening, and a [table] is a prefix, so nothing had to be adapted on either side. A key written twice is refused, as TOML requires and as JSON already did.

    Values are kept as written, with one rule: where TOML offers several spellings of one value they are canonicalised, because otherwise they would convert to nothing. 1_000, 0xDEADBEEF, 0o755 and 0b1101 become plain decimals; inf and nan become Infinity and NaN; and the space TOML allows in place of a date-time’s T becomes a T. Strings and ordinary decimals are untouched. The four date-time types need nothing registered. See File formats.

  • The java.time types are read out of the boxLocalDate, LocalTime, LocalDateTime, OffsetDateTime, Instant, ZoneId, Year and the rest — with nothing to register. The conversion chain now understands two more ways of building a type from text: a public static of(String) and a public static parse(CharSequence), alongside the String constructor and valueOf(String) it already knew. Those four are the implicit converters MicroProfile Config defines, so the naming is the ecosystem’s rather than ours.

    None of the java.time types worked before: they have no String constructor and no valueOf, so the chain ran out and refused them. Where MicroProfile tries the String constructor last we keep trying it first, as this library always has — changing that would silently move a type that has both from one to the other. When a factory exists and rejects the text, its own exception is kept as the cause, so a bad date says which character was unexpected instead of only cannot convert. See Type conversion.

  • HOCON is read, from a source whose path ends in .conf, and MyConfig.conf joins the names tried when a configuration declares no @Sources. The document becomes the same keys every other format flattens to, so nested interfaces, indexed lists and maps of sections read it unchanged — substitutions, object merging and include included, those being the reference implementation’s to perform.

    It is the one format this project does not parse itself, and the reason is that it already has a parser. HOCON’s specification is an implementation, and the value of the format is reading the application.conf files that already exist; a hand-written subset would refuse substitutions, merging and include, which is to say it would be JSON with comments. Worse, OWNER already reads ${...} with different semantics, so an approximation would not fail on the files it could not handle — it would read them and quietly mean something else.

    It costs nothing to anyone who does not use it. com.typesafe:config is an optional dependency of owner-extras: it is not transitive, this project does not ship it, and you add it yourself. Nothing in the loader refers to it, so the loader is discovered and created like any other on a classpath without it, and only reading a .conf fails — naming the source and the artifact to add. See File formats. Closes #240.

  • The ZooKeeper loader no longer has to be registered. ZooKeeperLoader is now found on the classpath like every other loader, so @Sources("zookeeper://…") works with ConfigFactory.create and there is no registerLoader call and no factory of your own to write. Code that still registers it keeps working. Apache Curator is still an optional dependency and is still yours to declare — what changed is only that declaring it is now the whole of the setup.

    Being discovered means the loader is created in every application carrying owner-extras, most of which will not have Curator, so nothing in it refers to Curator any more: everything that does moved to a separate class reached only when a zookeeper: source is read. A configuration reading any other source is unaffected, and the loader contributes no file name to the ones tried when a configuration declares no @Sources. Reading a zookeeper: source without Curator now names the source and the artifact to add, where it used to be a NoClassDefFoundError.

  • default methods in config interfaces work out of the box with the core owner artifact: no extra dependency is needed anymore (see Removals above).

  • New Accessible.store(Writer, String) overload, mirroring Properties.store(Writer, String); the old javadoc note about it being unavailable dated back to the JDK 1.5 era.

  • The single-method SPI interfaces (Converter, Preprocessor, ReloadListener) are now marked @FunctionalInterface: converters, preprocessors and reload listeners can officially be written as lambdas.

  • A method can return an Optional of any supported type, which comes back empty when the property is defined nowhere and has no default, instead of returning null: Optional<Integer> port() reads the same value Integer port() does, and says in the signature that the caller has to deal with its absence. The wrapper only describes the absence, so everything else applies unchanged — @Key, @Prefix, the preprocessors, the variable expansion, the decryption, the tokenization of Optional<List<String>> — and a value that is wrong rather than missing keeps failing, so a typo does not silently become an empty Optional. An empty value stays a value, as it does everywhere else. @Mandatory and Optional written on the same method contradict each other and are reported when the Config object is created, while a @Mandatory written on the interface leaves an Optional method alone, being the exception it declares. See the documentation.

  • New @Sensitive annotation: the value of the annotated property (or of every property of the annotated interface) is printed as ******** by Accessible.list() and by toString(). A password written in clear in a properties file is a value like any other to this library, and a cfg.list(System.out) added while debugging and then forgotten is how one ends up in a log. Only the output meant to be read by a human is masked: the method itself, getProperty, fill, store, storeToXML and the JMX attributes keep returning the real value, since those are how a configuration is read and written back and masking them would replace the password with the mask in the file at the next save. Masking is not encryption — see @EncryptedValue for that, whose values are already printed as ciphertext — it only keeps a value from being printed by accident. The keys to mask are resolved when the Config object is created, so a parametrized property, whose key depends on the arguments, is left alone. See the documentation.

  • New @Mandatory annotation: mark a property (or a whole interface) as required, and get a MissingMandatoryPropertyException listing all the unresolvable keys when the Config is created, as well as on access if a mandatory property disappears later (e.g. after a hot reload). See the documentation. Originally proposed by Alexander Poulikakos in #216.

  • New @Prefix annotation: declare the common prefix of a group of keys once, on the interface, instead of repeating it in the @Key of every method. @Prefix("server.") makes String hostname() resolve to server.hostname, and it is prepended to the @Key value as well. The prefix belongs to the interface that declares the method, so it never leaks onto the methods a sub-interface inherits, at any depth of the hierarchy; it is expanded like the rest of the key, so @Prefix("servers.${env}.") selects a section at runtime; and it can be switched off per method or per interface with @DisableFeature(PREFIX), a new value of DisableableFeature. Nothing changes for existing configurations: an interface without @Prefix resolves its keys exactly as before. See the documentation. Originally proposed by Gmugra in #273.

  • A prefix can also be configured on the factory, for the interfaces that do not declare a @Prefix of their own: owner.key.prefix prepends a literal to every key, and owner.key.prefix.from.package derives it from the package of the interface declaring the method, so com.example.ServerConfig.port() reads com.example.port. Being derived rather than typed, the second form follows the class when it is moved to another package — and it extends to the keys the convention OWNER already applies to the name of the default properties file. It is set through the factory properties, so no method is added to the Factory interface, and it belongs to the factory rather than to the JVM: two factories do not interfere, and a library can create its own and be unaffected by what the application does. @Prefix wins over it, @DisableFeature(PREFIX) switches off both, and the prefix is read when the Config object is created and kept for its whole life — so reconfiguring the factory cannot rename the keys of what already exists, a reload resolves the same keys, and the mapping survives serialization. See the documentation. Answers the request in #259.

  • New @DefaultValue(useOnEmpty = true) flag: a property that is present but empty is normally a value like any other — port= is not a missing property, and on a numeric type it fails the conversion — which is the distinction MicroProfile Config, Quarkus and Spring Boot all draw, and what keeps a typo like port=8O80, written with the letter O, from silently becoming the default. The flag covers the one case where the distinction gets in the way: a value left empty by a template, as in port=${PORT} with PORT unset. With it, an empty value — whitespace included, and after the variables are expanded — falls back on the default as if the property were missing, while a value that is wrong rather than empty keeps failing. It is opt-in and per method, so nothing changes for existing configurations. See the documentation and the table of what an empty value does on each type. Partially answers #191.

  • ByteSize implements Comparable, ordering sizes by the amount of data they represent whatever unit they are written in, so 1 MB sorts before 1 MiB. The ordering is consistent with equality, which is what makes a TreeSet of byte sizes agree with a HashSet on which of them are duplicates.

  • New ByteSize.in(ByteSizeStandard): the same size written in the unit of that family that suits it — the largest one in which the value does not fall below one — so 2048576 bytes read as 2.048576 MB in SI and as 1.95367431640625 MiB in IEC. Where convertTo has to be told the unit, this needs only the family to choose from, which is what one usually has when a configured size is to be logged or shown. The answer is canonical, depending on the size and never on the unit it happened to be written in, and exact, every factor being a power of 1000 or of 1024.

  • ByteSize is Serializable, which a Config object already was. The unit is preserved along with the value, so a size written as 1 MB comes back reading as 1 MB and not as 1000000 B, and the stream is validated on the way in: deserialization runs no constructor, so a stream that does not describe a byte size is refused with an InvalidObjectException instead of producing an object that fails later.

  • #320: EnumSet and Set<Enum> are now supported by type conversion (thanks to @dexman545).

  • java.time.Duration is converted out of the box, like a File or a URL, instead of asking for @ConverterClass(DurationConverter.class) on every method that returns one: a timeout is the commonest typed setting after a number and a string, and the JDK has a type for it. 10 s, 500 ms, 1 d and the ISO-8601 form PT15M are all read, in collections, arrays and Optional like any other type. The time unit is required on this path: timeout=30 is refused with a message saying what to write, because a bare number would be read as milliseconds and whoever writes 30 means seconds far more often than that. The converter named explicitly keeps its previous behaviour, bare number included, so no configuration written before this release changes meaning; a converter registered for Duration, or named with @ConverterClass, still takes precedence over the automatic conversion. See the documentation.

  • #187: java.nio.file.Path is converted, with a leading ~ expanded to the user home exactly as java.io.File already was — the two ways of naming a path no longer disagree. Arrays and collections of Path follow. The reporter asked in 2016 whether this belonged in a separate module for Java 7; the question no longer arises, since Java 8 is the minimum runtime.

  • Variables can now carry a default value: ${db.host:localhost} resolves to localhost when db.host is defined nowhere, instead of to the empty string. Everything after the first colon is the default, colons included, so URLs, Windows paths and host:port pairs survive intact. Existing configurations are unaffected: the text inside ${...} is looked up as a property key in its entirety first, and only if there is no such property is the colon read as a separator — so a key like jdbc:url keeps resolving as before. The one behaviour that changes is a variable that used to resolve to nothing: ${a:b}, with neither a:b nor a defined, yielded the empty string up to 1.0.12 and yields b from now on. See the documentation. Proposed by Ilya Koshaleu in #256.

  • Variables can now be nested: the expression inside ${...} is expanded first, and the result is then looked up as a key, so ${servers.${env}.url} reads the key named by the value of env. It works at any depth, in the @Key, in a property value and in the @Sources specification, and it combines with the default values above — ${servers.${env}.url:http://localhost}. This is what makes a key depend on a key that itself depends on another one, the case that produced silently wrong lookups before. See the documentation. Proposed by Tomek in #326.

    Compatibility. This is the one change in this release that touches an existing parsing rule: up to 1.0.12 a ${ was closed by the first } that followed it, now it is closed by the one that matches it. Only the ${ sequence opens a nesting level, so a lone brace inside an expression remains ordinary text and a key such as a{b keeps resolving; ${} and an unbalanced ${ are left alone as before. A configuration that uses plain variables is therefore unaffected. Should some unforeseen combination of braces read differently, the whole behaviour can be switched off for the JVM with -Downer.nested.variable.expansion=false, which runs the substitution of the previous releases unchanged.

  • A Map return type now reads the group of properties below the key of the method, closing a request open since 2013 (#41):

    something.foo=1
    something.bar=2
    Map<String, Integer> something(); // {foo=1, bar=2}

    Both sides of the entry go through the regular type conversion, so Map<Integer, String> and Map<Colour, String> work as well; the group is named like any other key, so @Key, @Prefix and variable expansion all apply — @Key("servers.${env}") picks the section at runtime. A name with further dots keeps them, so something.a.b becomes the entry a.b; no match gives an empty map rather than null; the declared map type is honoured — a class is instantiated as itself, and an interface is given an implementation that satisfies it: LinkedHashMap for Map, TreeMap for SortedMap and NavigableMap, ConcurrentHashMap for ConcurrentMap, ConcurrentSkipListMap for ConcurrentNavigableMap, and an EnumMap over the enum it declares, which is built from the key type rather than refused for want of a no-argument constructor. A type nothing can satisfy is named in the message instead of failing as a ClassCastException on the way out; and @DefaultValue is refused on such a method, since a default belongs to the individual properties. A @ConverterClass still takes precedence, which is how the other shape of the request — one property whose value holds the pairs, as asked in #286 — keeps working. Nothing can break: a Map return type used to throw on every access, so no working configuration relied on it. See the documentation.

  • A list can be written one element per key, closing a request open since 2013 (#48):

    servers[0]=alpha
    servers[1]=beta
    List<String> servers(); // [alpha, beta]

    for every array and collection type, Optional included. The point of it is that an element written this way is one element whatever it contains, so servers[0]=a,b is a single value with a comma in it, which a comma-separated list cannot express at all — and it is how a list out of a JSON or a YAML source will survive being flattened into properties. The separator does not apply to an indexed element, there being nothing to split. Square brackets rather than servers.0 because the dot already belongs to the Map grouping above, which would otherwise make one layout of keys mean two things. An indexed key wins over a single value and over a @DefaultValue, and the elements must be numbered from zero without gaps: a gap is refused rather than closed up, since a list quietly shorter than the file describes, with everything after the gap moved, is not something the caller can notice. Nothing can break, servers[0] having been a property nothing read. See the documentation.

  • YAML is read, by a parser of ours, in the same owner-formats artifact — and it is a subset, which is said here rather than discovered later:

    server:
    host: localhost
    servers:
    - host: alpha
    - host: beta
    ports: [80, 443]

    Read: block mappings and sequences nested by indentation, a mapping opened on the same line as its dash, plain and quoted scalars, the block scalars | and > with their chomping indicators, flow collections — so a JSON document is read too, being valid YAML — comments, and a leading ---.

    Refused by name, with the line they are on: anchors, aliases and merge keys; tags; complex keys; a value continued on the next line without | or >; a second document in the same file; and a tab used as indentation. None of them is guessed at or quietly ignored, because a parser that half-understood one would change the meaning of a file rather than decline to read it.

    Types are not guessed, and that is what makes the parser possible at all. A scalar is kept as written and the method that reads it decides what it means, so enabled: yes is the text yes and country: no is the string no — the “Norway problem” simply does not arise. Implicit type resolution is most of what a complete YAML implementation does, and none of it is needed when the interface is where the types are declared. Write true when a boolean is meant. Issues #14 and #65.

  • JSON is read, by a parser of ours, in a new artifact:

    <dependency>
    <groupId>org.aeonbits.owner</groupId>
    <artifactId>owner-formats</artifactId>
    </dependency>

    Adding it is all there is to do — the loader declares itself, so a .json source is read as soon as the artifact is on the class path. The document’s shape becomes the keys, server.host and servers[0].host, which is the flattening every loader here already uses: a JSON document is therefore read by the same nested interfaces, indexed lists and grouped maps as anything else, and nothing about the mapping is specific to JSON.

    RFC 8259 and no more: no comments, no trailing commas, no unquoted names, no single quotes, no leading zeros. Those are JSON5 and JavaScript, and a file we accepted and other tools refused would be the worst of both. Every complaint names the line and the column, and a value is kept exactly as written — 1e3 stays 1e3, and a long past 2^53 keeps its last digits.

    Three things the specification leaves open, decided and written down: a null writes no key at all, Properties being unable to hold one; an empty array writes an empty value, which is already read as an empty collection; and a repeated name is refused, because JSON has a real way to write a list and a repetition is therefore a mistake rather than the shorthand it is in INI and XML.

    Why a separate artifact: the core ships the formats the JDK can already parse — Properties for properties, SAX for XML, and .env and INI are line-by-line variations on the first. A parser we write is code that chews untrusted input, and a defect in one would be a security release for everybody, including the majority who never load that format. It brings no dependency of its own. Issue #240.

  • Nested configuration interfaces. A method returning another interface that extends Config reads the section of the configuration below its own key:

    server.host=localhost
    server.port=8080
    ServerConfig server(); // server.host, server.port

    The accessor names the section rather than the type it returns, so @Key renames it and two methods can return the same interface without colliding. The nested object loads nothing of its own: it is a view over the properties its parent resolved, sharing one set of @Sources, one reload, one set of listeners and one mutable state for the whole tree. The objects are built when the configuration is created, so a @Mandatory property one level down is checked then like any other and a cycle in the types is refused there rather than at the first call. A @Prefix on a nested interface composes with the path it hangs from — unlike the prefix configured on a factory, which @Prefix overrides — because the path says where the object was hung and the annotation says how it names its own keys.

    Sections can be counted or named. List<ServerConfig> reads servers[0].host, servers[1].host, by the rules of any indexed list, which is exactly the shape a tree-structured source flattens to: an XML document with a repeated element is now read by an interface holding a list. A type holding a List of itself is a tree and is allowed, where a type holding itself is refused — the keys say how deep it goes. Map<String, ServerConfig> reads servers.alpha.host and servers.beta.host, the name of each section becoming the key of the entry, and @Key("servers.%s") ServerConfig server(String name) asks for one by name: together they answer the long-standing question of objects whose names are only known at run time.

    An Optional section is present when anything at all was written below its path. A @DefaultValue declared inside the nested interface is one such thing, so it makes the section permanently present: the two say the opposite of each other and the default wins. For the same reason @Mandatory written on the accessor of a section is refused when the configuration is created, since the check could never fail; @Mandatory on the properties inside is the one that means something. Nothing can break: a method returning an interface extending Config had no meaning before. See the documentation.

  • The library says which key each method reads. A wrong prefix makes every property vanish at once with nothing to show for it — no error, every method answering null or its default, and a file full of values that look right. At FINE every method now reports the key it resolves to, nested sections walked with it, and a key that is not yet final says which kind it is: one whose prefix is disabled, one whose arguments are formatted in at each call, one still holding variables. At CONFIG, one line names the prefix configured on the factory, which is singled out because it is the only prefix written in no source file at all. See the documentation.

  • A source that hot reload cannot watch says so. Watching means asking something whether it has changed, and only a file and system:properties can answer: a resource inside a jar served over the network, an http: source, system:env cannot. Those were dropped from the watch list in silence, which is where “I changed the file and nothing happened” comes from. It is now a WARNING naming them, once, when the configuration is created — different from an absent source, which stays silent, because here somebody wrote @HotReload and for that source it will never fire. What is being watched, of which kind and how often, is written at CONFIG beside it.

  • A source that was named and did not arrive is no longer passed over in silence. Both load policies ended in catch (IOException) { ignore() }, with a comment admitting it covered two different things: a file legitimately absent, which is how a fallback chain works, and a file that is there and cannot be read. The second produced a configuration full of defaults and said nothing about it. Now an absent source is still silent — with FIRST every miss but the last is the feature working, and a configuration with no @Sources probes four names per interface — while a source that is there and refuses is a WARNING, and declared sources of which not one could be read are a WARNING of their own, that being what a mistyped path looks like. Each is said once, not at every reload.

    Which of the two a failure was is deliberately not read off the exception: FileInputStream throws FileNotFoundException for a file that is missing, for a directory named where a file was meant, and for one it may not open — the three cases the rule exists to separate. For a file the filesystem is asked; only a source that is not a file falls back on the exception.

    And a source can now say that it has to be there: @Sources("file:/etc/app.properties#required=true") refuses the configuration when that one is missing or unreadable, including a classpath: resource that resolves to nothing — the case that never reaches a loader and would have been the one place the promise was dropped. It is Spring’s optional: the other way up, which is what keeps every fallback written so far working unchanged. Unlike a dialect, required is read by the library rather than by a loader, so no loader has to declare it. Issue #170 asked for the visible half of this.

  • A configuration can say where each property came from. A new interface of the Accessible family, Traceable, answers the question the merged properties cannot:

    @LoadPolicy(LoadType.MERGE)
    @Sources({"system:env", "file:config/app.properties"})
    interface MyConfig extends Config, Traceable { ... }
    cfg.originOf("port"); // file:config/app.properties
    cfg.originOf("port").kind(); // SOURCE, IMPORT, DEFAULT_VALUE or RUNTIME

    Merging is exactly what destroys this: after it, a value read from a file is the same property as one that came from the environment or from a @DefaultValue, and nothing in the map says which. So the origin is recorded while each source is read, and under MERGE the one recorded is the source whose value survived — the first declared. Under FIRST the sources after the one that answered are never read, and nothing is attributed to them. The origins follow the properties afterwards: a reload works them out again, setProperty makes a property one that was written at run time, and removing a property removes its origin with it.

    It was asked for by somebody whose store() wrote the whole environment back into the configuration file, and whose workaround — removing the environment variables by name before saving — failed for a property that was in both. Filtering by origin is the answer, and the recipe is in the documentation. A source never carries its credentials into an origin: https://user:secret@config/app.properties appears as https://***@config/app.properties, the same masking the log lines and the exception messages already use. Issue #277.

  • .env files are read, which is how container tooling carries configuration into a process — docker run --env-file, env_file in Compose, envFrom in Kubernetes, the secrets of a CI pipeline. Any source whose path ends in .env is read this way, values go through the usual type conversion, and the parser is ours: the core still has no dependencies.

    There is no .env standard, and the tools that read one disagree on the point that bites hardest, which is quoting: given NAME="Matteo", docker run --env-file gives you the quotes and the dotenv family does not — and Docker Compose does not agree with docker run. So OWNER does not implement “the .env format”, it implements a dialect, and there are three presets — docker, dotenv, compose — plus seven rules that can each be set on their own, for the tools that match none of them. docker is the default, because it does nothing at all to a value and a value that arrives with its quotes still attached is noticed at once, where quotes silently removed are not. A file that looks quoted under a dialect that keeps quotes draws one WARNING. A .env is never looked for on its own: it is not named after the configuration interface, so it is always named explicitly, and configurations that do not use one pay no extra lookup. See the documentation.

  • INI files are read — sections in square brackets and key = value below them, which is the shape of ~/.aws/credentials, ~/.gitconfig, a systemd unit and a good deal of what is already on a machine. A section becomes the prefix of the keys under it, needing no convention of its own since the dot is already how OWNER nests, and .ini and .cfg are both recognised and both looked for beside the configuration class.

    A repeated key is a listservers.host[0], servers.host[1] — exactly as repeated XML elements are numbered, a key occurring once keeping its plain key. This is the point the tools disagree on most: Python’s configparser refuses the file, git and systemd and Commons Configuration read a list, and the AWS SDK for Java keeps the last. A list is the answer because it is the one this release already gives to a repeated XML element, and reading the same shape two ways would be the surprise; the other three are available as options.

    There is no INI standard, so as with .env the rules are a dialect: ini by default — the conservative common denominator every surveyed tool agrees with — plus git, which reads a subsection, so [remote "origin"] holding a url becomes remote.origin.url, the very key git config prints; and python, which folds keys, accepts :, refuses a duplicate, continues a value by indentation and lets every section inherit [DEFAULT]. Eleven rules can be set one at a time over any of them.

    One thing the python dialect refuses rather than half-honours: ConfigParser interpolates %(name)s by default and OWNER never will, expanding ${…} itself after loading and across every source. A value holding %(…)s read under that dialect is an error naming the key and pointing at ${…}, because handing back the literal would make the same file mean one thing to Python and another here, quietly. See the documentation.

  • A source can carry options, written in its fragment. @Sources("file:.env#dialect=dotenv") sets the dialect for that file alone, several options separated by &. The rule is the same for every loader and every scheme: the query belongs to the protocol and the fragment belongs to OWNER. A query is never touched, so https://config/app.env?token=abc#dialect=dotenv sends the token to the server and keeps the dialect; and the fragment is the only place the options can be written at all for a resource inside a jar, whose URI has no query to speak of. An option a loader does not recognise is refused, not ignored, and the message names the option, the source and what would have been accepted — a misspelt option that passes in silence is a configuration that is wrong and says nothing. This works on a classpath: source as well as on a file. See the documentation.

  • A loader can be found on the classpath instead of being registered by hand: declare it in META-INF/services/org.aeonbits.owner.loaders.Loader and it is picked up when a factory is created, which is what a jar shipping a format is for. Being found enables it — it answers for its formats at once, and its default file names join the ones looked for when an interface declares no @Sources, as Spring Boot, MicroProfile and Gestalt all do with theirs.

    Where it lands is deliberately not the same in both directions. A found loader comes before the built-in ones when a source is matched, or PropertiesLoader — which accepts every URL it can resolve — would take its files; and last among the default file names, so that adding a jar to a build cannot make a stray MyConfig.yaml start beating the MyConfig.properties an application already reads. Registering a loader by hand keeps the front in both, that being something the application said on purpose.

    The searching is done by the thread’s context class loader, falling back on the one that loaded OWNER. That is right in an ordinary application and in an application server, and it is not right on a pooled thread carrying somebody else’s context, nor under OSGi; in those, registerLoader still works and depends on nothing. Since a loader that is not found does not fail — its file falls through to the properties loader and is read as properties, quietly — OWNER now reports what it found at the CONFIG logging level, including when it found nothing. org.aeonbits.owner.level = CONFIG is the switch, and it is silent unless you turn it on. See the documentation.

  • A format may go by more than one name. Loader.defaultSpecsFor(String) returns every default file name a loader offers, for the formats spelled two ways — .yaml and .yml, .ini and .cfg. It is a default method, and so is defaultSpecFor, which now returns null by default: declining to be looked for is a choice a loader is allowed to make, and SystemLoader and DotEnvLoader both make it. Nothing that implements Loader today has to change, or even to be recompiled.

  • A circular variable reference is now reported instead of being followed. A property whose value leads back to the property itself cannot be resolved, and an IllegalArgumentException names the chain that closes the loop — Circular variable reference: ${a} -> ${b} -> ${a} — where up to 1.0.12 the same configuration exhausted the stack with a StackOverflowError. A default value does not rescue it: db.host=${db.host:localhost} is the shell idiom for “keep it if set, otherwise use this”, but it relies on the substitution happening once at assignment, while OWNER expands variables when a property is read, and inside values. That line therefore describes a loop rather than a fallback, and it is reported as one — what was meant is db.host=localhost. See the documentation.

  • New @CollectionConverterClass annotation: hands the raw property value to a single converter instead of splitting it first and converting one element at a time, as @ConverterClass does. It is the way to opt out of the built-in tokenization — for a property holding a single JSON document, say — or to return a collection type OWNER cannot instantiate itself, such as an immutable one or an implementation without a no-argument constructor. Using it on a method that does not return a Collection reports which method is at fault instead of failing later with a ClassCastException. See the documentation. Contributed by Adam Huječek in #248, closing #206.

  • Security hardening of the XMLLoader against XXE attacks: external DTDs and entities are neutralized, secure processing limits entity expansion; the standard Java properties XML format keeps working as before.

  • #325: temporary files are now created with owner-only permissions via Files.createTempFile (thanks to @JLLeitschuh); when storing a Config to an existing file, the file permissions are preserved.

  • The jars declare an Automatic-Module-Nameorg.aeonbits.owner and org.aeonbits.owner.extras — so a requires written against them keeps resolving across releases, instead of depending on a module name derived from the file name and therefore from the version. The two artifacts no longer share a package either (see the ZooKeeperLoader move under Removals), so they can both sit on the module path.

  • Bytecode is still compatible with Java 8 at runtime, while the project is built with modern JDKs (compiler-release=8); a JDK 11 or superior is required to build from sources.

  • Javadoc completed and improved across the codebase.

  • Dependencies updated across the board, including security-driven pins: JUnit 4.13.2, Mockito 5.x, SLF4J 2.x, commons-codec 1.22, Curator with ZooKeeper forced to 3.9.5 and Netty aligned via BOM to address published vulnerabilities.

A large part of the work that went into 2.0.0 is not visible in the API. The objective was to raise the test coverage as far as it reasonably goes and to leave no warning unexamined, so that future changes start from a codebase that says what it does.

  • Four javadoc comments were documenting nothing, and are back on what they describe. A comment documents whatever is declared under it, so a new documented member inserted immediately below an existing one leaves that one stranded: it stays in the file, reads as though it were published, and is not. @Separator had lost its documentation the day @Description arrived, Accessible.storeToXML the day save(File) did, and the note explaining why OAEP is given its parameters explicitly had drifted onto an unrelated method. Neither the compiler nor javadoc says anything about this — javadoc warns about a member with no comment, and in each case the member below had one of its own. A test reads the source and refuses two javadoc comments in a row, so the next one cannot go unnoticed.
  • Test coverage extended, with the crypto, loaders, util and ConfigCache packages at or near 100%. The tests were written to pin down actual behaviour, not to move a percentage: several of them document decisions that were previously only implicit in the code.
  • Every warning triaged, one by one. The project is analysed on each push by CodeQL with the security-and-quality query pack, by SonarCloud, and by the inspections built into the IDEs used for development. Every finding was either fixed, or dismissed with a written technical justification explaining why the construct is correct as it stands — an analyser being wrong is a legitimate outcome, an unread warning is not.
  • The exercise was not cosmetic. Chasing warnings that looked like style issues surfaced genuine defects that had gone unnoticed for years. The array and collection conversion bug listed under “Bugs fixes” was found exactly this way: a CodeQL note about an inner class that could be made static turned out to be hiding a test that passed for the wrong reason, and behind it a real failure affecting any user converting a list of custom objects.
  • Tests that passed for the wrong reason were corrected. A green test suite is only meaningful if each test fails when the behaviour it describes breaks. Where a test was found to be satisfied by an accident of its fixtures rather than by the behaviour under test, the fixture was fixed and the assertion re-verified.

The intention is to keep this state: warnings are not allowed to accumulate between releases, and a finding is closed only when it has been understood.

  • New documentation for the Preprocessors feature (available since 1.0.9, never documented).
  • New documentation for the JMX support (available since 1.0.10, never documented).
  • New chapter on the Key prefix feature, and the list of the disableable features is now spelled out, with the version each one appeared in.
  • New sections on nested variables and on how to switch them off in Variables expansion.
  • New section on Sources and interface inheritance, writing down what was until now only implicit in the code: @Sources accumulates across the interfaces a mapping interface extends — by design, since it describes a set and not a single setting — while @LoadPolicy and @HotReload take the first annotation found. The section also documents the limitation the three of them share, that only the direct super-interfaces are read, so an annotation two levels up is silently ignored. The behaviour is unchanged in 2.0.0 and is now covered by tests, so that changing it will be a deliberate step.
  • New section on Mandatory properties in Basic usage.
  • Type conversion no longer stops at “Map is not supported”, a sentence that read as “cannot be done” and had been sending people away since at least #41. The chapter now describes the grouping above, and keeps the @ConverterClass recipe for the case where a single property value holds the pairs — arrays of maps included.
  • New section on overriding a property in a sub-interface, answering #421: an override redirects a property instead of adding one, since there is one method and therefore one key. Both of the things usually wanted there — keeping the base key readable, and making the concrete setting fall back to the base one — are shown written down explicitly, the second one as a three-level chain built with a variable.
  • Crypto support is no longer labelled as experimental: the @EncryptedValue and @DecryptorClass annotations have shipped unchanged since 1.0.10 and are part of the stable API.
  • Documentation refreshed to the current state of the project: installation instructions, build requirements, FAQ (encrypted properties are supported since 1.0.10, not 1.0.12 as previously stated; #229), links and navigation updated to the maintained repository and to the current CI services.
  • Continuous integration migrated from Travis CI to GitHub Actions: every push and pull request is built on all the supported LTS JDKs (11, 17, 21 and 25) with Maven caching.
  • Code quality and coverage tracked on SonarCloud; security scanning via CodeQL and Dependabot; dependencies kept current by Renovate.
  • Maven wrapper added; Maven Enforcer requires Maven 3.6.3+; Travis, Coveralls and WhiteSource/Mend leftovers removed.
  • The BSD license header is now enforced on every Java source file by the license-maven-plugin: mvn license:format adds or fixes it, and mvn license:check — bound to the verify phase, so it also runs in CI — fails the build when a file is missing it. Nineteen files had drifted without one over the years, and the copyright line existed in three different variants; both are now uniform. The Maven wrapper sources are excluded, as they ship under Apache 2.0.
  • The conventional file that won was decided by an order nobody had chosen. When a configuration has more than one of the files named after it, the first that exists is the one read — and that order was the registration order of the loaders, which exists to answer a different question: which loader can read this URI, where PropertiesLoader must come last because it accepts every URL it can resolve. As a side effect MyConfig.ini and MyConfig.cfg silently outranked MyConfig.properties, so an application reading its .properties for years would have stopped the day somebody dropped a .cfg in the same directory — .cfg being the most generic of the four names and the likeliest to belong to another tool. The two orders are now opposites on purpose: .properties, .xml, .ini, .cfg, and a loader found on the classpath still last of all. A loader you register yourself still comes first in both.

  • And when there is more than one of them, the library now says so, at WARNING, naming both files, which one was read and how to end the ambiguity — a refusal under owner.strict, like every other warning with a caller to refuse. No ordering rule can do better than choose which of two silences you get: the file you did not expect being read, or the file you did expect being ignored.

  • @Sources, @LoadPolicy and @HotReload were ignored two levels up. The three were looked for on the mapping interface and on the interfaces it extends directly, each by its own copy of the same loop, so an annotation on the parent of a parent did nothing: its sources were not loaded, its policy did not apply, and a @HotReload written there never fired — silently, since none of the three is required. The fourth annotation read at class level, @Prefix, has always counted at any depth, so the library contradicted itself depending on which one you were reading. There is one lookup now, and it walks the whole hierarchy breadth first: the interface, then everything it extends directly in the order of the extends clause, then their parents, each interface visited once. If you were repeating an annotation on the interface handed to the ConfigFactory to work around this, the repetition can go — and for @Sources it should, since the same file would now be listed twice.

  • @DecryptorClass was ignored unless it was on the interface handed to the factory. Not even its direct super-interfaces were read, so this was the worst of the family, and it failed in silence: an @EncryptedValue property came back as the cipher text stored in the file, which is a string like any other and breaks, if it breaks at all, wherever it is finally used — a wrong password at the far end of a connection, not an error where the mistake is. The decryptor of a configuration is a property of the configuration, so it is now found wherever in the hierarchy it is written. The same applies to the @Description that becomes the header of a saved file.

  • @DisableFeature on a super-interface made one configuration answer the same question two ways. A feature is disabled for a method — the annotation on the method or on the interface declaring it — and for the configuration object, which is what getProperty and fill have to ask, being declared on Accessible and never on the interface you wrote. One lookup served both and read the interface handed to the factory alone, so with @DisableFeature(VARIABLE_EXPANSION) written one level up, cfg.home() returned ${user.home} unexpanded while cfg.getProperty("home") expanded it. The two questions are told apart now: the method one still stops at the declaring interface — deliberately, or a blanket disable on a base interface would cancel a @Prefix written explicitly below it — and the object one reads the whole hierarchy, every declaration of it rather than the nearest, since the annotation carries a set and two interfaces may each switch off one feature.

  • The MyConfig.properties convention was appended even to a configuration that declared its sources. The convention and the declared sources were the same call, so every interface without @Sources contributed the default list — and Config itself has none, so it happened to every configuration there is, twice over for the plainest one. A configuration that declared @Sources("file:my.properties") was therefore also reading a MyConfig.properties left on the classpath, and the CONFIG diagnostic said no @Sources about an interface that had one. The convention is now what it reads like: the fallback for a configuration that names no source at all. If you were relying on the appended file, name it — @Sources({"file:my.properties", "classpath:com/acme/MyConfig.properties"}) does the same thing where it can be seen.

  • A source with no scheme made the ZooKeeper loader throw. ZooKeeperLoader.accept compared the URI’s scheme against its own without allowing for a URI that has none, so it raised a NullPointerException rather than answering. Every registered loader is asked about every source, so this bit anyone following the ZooKeeper documentation who also had a source written without a scheme — @Sources("myconfig.properties") — or a blank file:, which the library turns into an empty URI on purpose, that being what a source path built from an unset environment variable comes to. What such a source does is unchanged: no loader accepts it, so the library still says it cannot resolve one.

  • An XML document that broke its own DTD was read past it. XMLLoader validates — it must, the Java XML properties format being defined by a DTD — and a validity error was refused for that format and swallowed for every other. So a document of your own carrying a <!DOCTYPE> and then contradicting it came back complete, the forbidden part included, with nothing said. It was never a truncated document: a validity error is recoverable, the parse runs to the end, and what the caller got was more than the grammar allows rather than less.

    The swallowing was not gratuitous, which is why it survived so long: a validating parser reports a validity error for every document that declares no grammar at all — no grammar found — and ignoring that one is what makes reading ordinary XML possible. The test now is whether the document declares a grammar, not whose grammar it is. One that declares none is read as it is, and so is one naming an external DTD, which the XXE hardening neutralizes: the grammar never arrives, and a document cannot be held to a rule that was refused a reading.

    Where this refuses a file that 1.0.12 read, #validate=false on the source reads it again — for a grammar of your own and for the Java properties one alike. See the documentation.

  • An XML source carrying a query string was not recognised as XML. XMLLoader decided from URL.getFile(), which by contract is the path plus the query, so @Sources("https://config/app.xml?v=2") failed its own test, fell through to PropertiesLoader — which accepts everything the others turn down — and was read as a properties file. There was no error and no warning: the configuration came back holding nothing but its defaults. The format is now decided from the path alone, so a query changes nothing about how a source is read, and a query on a file: or jar: source, where it can mean nothing and would send the handler looking for a file whose name ends in ?v=2, is refused with a message saying that options go in the fragment.

  • #195: imported Map entries whose key or value is not a String are now rejected with an IllegalArgumentException naming the offending key, instead of being accepted and then silently misbehaving. Originally reported and fixed by Stefán Freyr Stefánsson in #197, extended here to cover keys as well as values.

    Imports are merged into a java.util.Properties, whose contract only admits String keys and values, but which extends Hashtable<Object, Object> and therefore accepts anything through putAll. The entry then became invisible to getProperty, in two different ways:

    public interface MyConfig extends Config {
    @Key("some.key")
    @DefaultValue("1")
    Integer someValue();
    }
    Import Up to 1.0.12 Since 2.0.0
    imports.put("some.key", 42) someValue() returns null, shadowing @DefaultValue("1") IllegalArgumentException at create()
    imports.put(42, "42") the entry is dropped, someValue() returns the default 1 IllegalArgumentException at create()
    imports.put("some.key", new StringBuilder("42")) someValue() returns null IllegalArgumentException at create()
    imports.put("some.key", "42") someValue() returns 42 unchanged, returns 42

    Compatibility. Code that imported only String keys and values is unaffected. Code that imported anything else was already getting a wrong value, or none, so no working behaviour is lost — but a heterogeneous Map that happened to be read only through its String entries will now fail fast at create() time rather than half-working. Note that a CharSequence is not sufficient, as Properties compares against String specifically: call toString() on StringBuilder/StringBuffer values before importing them. See the documentation.

    The validation lives in the factory, so it applies uniformly to ConfigFactory.create(), to a Factory obtained from ConfigFactory.newInstance(), and to ConfigCache.getOrCreate(); previously the equivalent check on null keys and values only covered the first of the three.

  • Fixed the conversion of arrays and collections when a single element cannot be converted. The converter is chosen once from the first element, so the remaining ones could still fail: their internal “skip” marker ended up being stored into the resulting array, surfacing as an IllegalArgumentException: array element type mismatch instead of the documented UnsupportedOperationException. A property like @DefaultValue("1, 2, foo, 4") mapped to a custom type now reports Cannot convert 'foo' to MyType, consistently with what already happened for a non-array property. For the same reason, a @ConverterClass returning null for an element now yields a null element instead of failing the whole conversion.

  • Conversion errors now name the property they come from: Cannot convert 'abc' to int became Cannot convert 'abc' to int for property 'server.port'. The message used to say what could not be converted but not where to go and fix it, which in a file with fifty properties left the search to be done by hand. The key named is the one the property is read with, @Key and @Prefix included (#191). When the value comes from a group of properties read as a Map, the key named is the individual entry — group.second, not group.

  • ByteSizeUnit.parse no longer depends on the default locale of the JVM. It lowercased the text without saying in which language, and in Turkish a capital I lowercases to the dotless ı: 512 KIB was therefore rejected as an invalid unit on a Turkish JVM and accepted everywhere else. Every IEC unit written in capitals was affected, since all of them carry an i.

  • ByteSize honours the equals/hashCode contract. equals compares the number of bytes, so 1 MB and 1000000 B are equal, while the hash code was derived from the value and the unit as they were written: the two were equal with different hash codes, which made the type unusable as a key of a HashMap or as an element of a HashSet — a set could hold the same size twice, and a lookup could miss. The class is now final, and both parts are rejected at construction when null, instead of failing later at the first arithmetic with no indication of where the missing part was written.

  • Fixed a NullPointerException masking the real error in the hot reload example when the configuration URI is invalid.

  • Test suite stability fixes (thread handling in multi-threading tests, wait times).

Downloadable artifacts are published on GitHub and on Maven Central Repository.

Released 7 June 2020

I just released version 1.0.12, it contains all the bug fixes included in 1.0.11 plus a fix to a multi threading issue that appeared in 1.0.11.

–Luigi.

OWNER v1.0.12 contains following enhancements and bug fixes.

  • None
  • None
  • Fixed #268: Calling a value is not thread safe (return another value)
  • Fixed #266: PropertyEditor - Concurrency Issues

Downloadable artifacts are published on GitHub and on Maven Central Repository.

OWNER v1.0.11 contains following enhancements and bug fixes.

  • #234: Allowing to format Key value by method arguments as with DefaultValue.
  • #255: Solves the thread contention problem reported on Issue #254; Note this has partially been rolled back in 1.0.12 due to bugs #268 and #266.
  • 64a7c07: Updated dependencies to work with Java 11 LTS.
  • #247: Documentation for system:properties and system:env.
  • Fixed Sonar and Travis.
  • #274: Documentation for system:properties and system:env, Update importing-properties.md.
  • #246: Fixed doc typos & errors and improved readability.
  • #242: FAQ broken link.
  • #224: Adding some documentation for bug #184 (Maps with null values cause an unclear exception).
  • Fixed Javadocs.
  • Updated documentation.
  • 2479d47: decryption not working when used in combination with variable substitution
  • 0b2d209: removed [double check locking] anti pattern.
  • #227: Fixes properties issue in loading file URLs.
  • #239: Allow property values to contain a ‘%’ character without being a format string.
  • #203, #241: ConcurrentModificationException on creating Config.
  • #226, #227: Empty system variables for file paths in @Sources cause URISyntaxException failures. Fixes properties issue in loading file URLs.

Released 1 March 2018

After long time (more than 2 years now), and many people asking for a new release, here we are. And here my apologies for the delay.

As you may know, I had serious health problems that kept me away from coding. Now my health is getting better, but I feel much slower in coding and using awesome tools like IntelliJ IDEA; in the meantime, my open source license has expired, so I hope the guys from JetBrains will be so nice to renew it :).

Also, I always found the maven release process being cumbersome so that also kept me away from the effort. Now I took some time to simply it a little bit, and I kept some note for the future.

In this release, a huge amount of work has been conducted by contributors, and I mostly did housekeeping with refactoring, code review, enforcing quality standards, asking for documentation and tests, and integrating the great ideas coming from the users’ community.

I took back the project recently to upgrade it to have Java 9 support, and simplify release deployment, and only now that I am writing this release note, I realize how many things have been added and was waiting to be released.

Documentation is very important; I hadn’t had the chance to keep all in sync, so many things here need to be documented. If you think you can help, feel free to help: this website is a sub-project owner-site, and uses Markdown language, which is very handy and quick to learn; the structure is quite easy to follow. Jekyll is used as site generator, which is written in Ruby and can be tricky for a Java dev like me, but it works awesomely with github. So feel free to help there too. There is also an ant script which allows you to launch Jekyll and live-preview the end result of your edits.

I don’t feel very comfortable in making promises, but I’d really like to give back life to this project and, for the future, avoid such a long wait for a release.

Please notice that at the moment I am not professionally working, I closed my consultancy company years back, and in this moment I am writing from a nice Coworking Space “ImpactHub” here in Torino. So, let me quickly say that donations are very welcome. Or if you want, you can hire me for some custom development on OWNER, training, or to help implementing your projects. This would definitely help keeping OWNER alive.

Credits to ALL the contributors of OWNER, and to the end-users of this neat library. To you all it goes my gratitude for this release.

Thank you!

–Luigi.


OWNER v1.0.10 contains following enhancements and bug fixes.

  • Added Java 9 support, dropped Java 6 support. All code and tests are running and built with Java 9, so you can use OWNER with the latest Java version. It was not trivial. If you want to use some specific feature like default methods in interfaces introduced in Java8, you still need to add owner-java8 dependency. I know… I didn’t want to create a new sub-module for Java 9 and every newer versions, if it’s not necessary. Also, I updated all the dependencies (testing, and optional) and Maven plugins, in order to have it working with Java 9. A huge thank you to my friend @sbordet.
  • Added list() method to ConfigCache. ConfigCache is a great way to centralise configuration for various parts of an application. This commit adds a list() method to the ConfigCache class, which lists the keys for all configurations present in the cache. This allows the entire application configuration to be inspected (e.g. for debugging) without the need for storing cache keys elsewhere. Thanks @kevin-canadian, who also was so nice to update the documentation on the website.
  • Added @EncryptedValue and @DecryptorClass annotations to allow hiding passwords stored in configuration properties. See #49, thanks @rrialq for the implementation and the awesome documentation.
  • Added a Java 8 duration converter class: DurationConverter.class in owner-java8-extras.jar . Thanks @StFS.
  • Added system properties and enviroment variable as sources: example @Sources({"system:properties", "system:env"}). See #110. Thanks @gintau for the implementation and @kevin-canadian for the idea.
  • Added ByteSizeConverter and DurationConverter classes in owner-java8-extras jar, see #155. Thanks @StFS, also for providing the necessary documentation and unit tests.
  • Added the ability to register default converters for types and classes defined by users. See #184. Thanks @StFS.
  • Added inheritance support for @Sources, @LoadPolicy and @HotReload. Sources defined for all extended interfaces will be merged. LoadPolicy and HotReload can be inherited and override by the extended interface. Thanks @chengmingwang.
  • Replaced fixBackslashForRegex with better implementation. Thanks @kiefinger.
  • Have ConfigFactory throw an exception on imported Maps having either null keys or null values. See #185, #184. Thanks @StFS.
  • Accept file URI containing spaces. Updated the uri processing to allow loading files that contain spaces in their paths. See #134. Thanks @icirellik.
  • Maps with null values cause an unclear exception. See #184. Thanks @StFS.
  • Set tar long file mode to posix in maven assembly plugin to avoid build errors. Thanks @gdenning.

Downloadable artifacts are published on GitHub and on Maven Central Repository.

Released 22 July 2015

v1.0.9 contains following enhancements and bug fixes.

  • Added fill(java.util.Map) method to the Accessible interface.
  • Added pre-processing feature. See #120, thanks @a1730 for the feedback.
  • None.
  • Config.Sources with ~ doesn’t create a valid URI on Windows. See #123, thanks @outofrange for spotting this bug.

Downloadable artifacts are published on GitHub and on Maven Central Repository.

Released 1 April 2015

v1.0.8 contains following enhancements and bug fixes.

  • Fixed the javadocs included in the tarballs/zips released.
  • None.
  • No owner-parent pom in Maven Central Repository. See #121, thanks @rajatvig for quickly spotting the issue.

Downloadable artifacts are published on GitHub and on Maven Central Repository.

Released 30 March 2015

v1.0.7 contains following enhancements and bug fixes.

  • None.
  • Fixed packaging: the owner-extras.jar was missing required classes. See #114. Thanks @ksaritek for the patience.

Downloadable artifacts are published on GitHub and on Maven Central Repository.

Released 18 November 2014

v1.0.6 contains following enhancements and bug fixes.

  • Added basic support for ZooKeeper #81. Thanks @ksaritek.
  • Added Java 8 Support (default and static methods on interfaces). See #94.
  • Added OSGi support. See #101.
  • Fixed documentation errors. See #88, #89, #92. Thanks @hemus2121.
  • Minor changes in build.xml (ant publishing script to gh-pages)
  • Use of default value for for properties using the Key Expansion mechanism #84.

Downloadable artifacts are published on GitHub and on Maven Central Repository.

Released 28 May 2014

v1.0.5.1 contains following enhancements and bug fixes.

  • Java8 fixes, so now it is officially supported.
  • Added UTF-8 Support for properties files. (See #77 and #78, thanks @SvetaNesterenko )
  • Added ConfigCache (Singleton) feature. (See #64)
  • Improved support for Android. Somebody wants to verify/help with this? (See #75)
  • Implemented variable expansion for @Key annotation. (See #63)
  • Restructured maven project to allow sub-modules.
  • Code cleanup, removed warnings.
  • Fixed compatibility issue on exception raised by Java7 and Java6. (See #71)

Downloadable artifacts are published on GitHub and on Maven Central Repository.

Released 9 October 2013

v1.0.5 contains following enhancements and bug fixes.

  • Support for XML. OWNER is now able to load not only from properties files, but also from XML files. The XML can follow the Java XML Properties format, or can be freely defined by the user.
    (See more in the documentation: XML support and see #5).
  • Added method registerLoader() to ConfigFactory, so the user can define new loaders for more file formats.
    (See #55).
  • Support for classpath: URLs in HotReload. Also it works with the default files associated to the mapping interface, when @Sources is not specified.
  • Added method Set<String> propertyNames() in the Accessible interface.
    (See #46).
  • Added Event support for property changes and reload. Both the events can now be transactional: the listener can be notified by an event before and after a property change or a reload takes place. The listener can check what is changed and eventually rollback the reload or property change operation.
    (See more in the documentation: Event support and see #47).
  • Added non-static ConfigFactory, so one can create independent instances of OWNER Factory objects.
    (See #43).
  • Added implementation on hashCode() and equals().
  • Added serialization capability to OWNER Config objects, so now they can be transferred through the network or transformed to byte streams.
    (See #54).
  • Allow @ConverterClass annotation to override default converters (i.e. primitive types, etc).
  • The interfaces Reloadable, Mutable and Accessible now extend from Config, so you don’t need anymore to extend directly from Config. For instance, your interface can now extend just from Mutable to generate an object which is also a valid Config object that can be instantiated by the ConfigFactory:

    config-hierarchy
  • Website sources reorganized: moved from gh-pages branch to master, with publish ant scripts build.xml.
  • Added news section, with release announcements and blog posts.
  • Fixed bugs on tests that were making the build failing on Windows systems.
  • Fixed bug #51, variables expansion, and path expansion not working properly with string containing the backslash characters '\'.
    Thanks NiXXeD.
  • Fixed bug #42, regarding the incompatibility of the OWNER library with the Google App Engine security restrictions.

Downloadable artifacts are published on Maven Central Repository.

Released 19 September 2013

v1.0.4.1 is a bug fix release for v1.0.4 branch.

  • Fixed some multi-threaded tests that were failing sometimes randomly during continuous integration.
  • Fixed bug #50, regarding hot reload not working when file name needs to be expanded.

Released 11 July 2013

v1.0.4 contains some key enhancements and bug fixes.

  • New website for documentation.
  • Added Sonar to keep high attention on code quality.
  • Added Travis CI to the project to track changes and run tests on different JDK versions.
  • Website code snippets now have syntax highlighting. Thanks ming13.
  • Fixed bug #40 about tilde expansion.
  • Fixed bug #17 Substitution and format not working as expected when used together.

Released 26 June 2013

v1.0.3.1 contains some key enhancements and bug fixes:

  • Fixed bug #35

Released 3 February 2013

v1.0.3 contains some key enhancements and bug fixes:

  • Fixed incompatibility with JRE 6 (project was compiled using JDK 7 and in some places I was catching ReflectiveOperationException that has been introduced in JDK 7).
  • Minor code cleanup/optimization.

See what’s new and what’s new part 2 articles for more information on this release.

Released 27 January 2013

v1.0.2 contains some key enhancements and bug fixes:

  • Changed package name from owner to org.aeonbits.owner. Sorry to break backward compatibility, but this has been necessary in order to publish the artifact on Maven Central Repository.
  • Custom & special return types.
  • Properties variables expansion.
  • Added possibility to specify Properties to import with the method ConfigFactory.create().
  • Added list() methods to aide debugging. User can specify these methods in his properties mapping interfaces.
  • Improved the documentation (this big file that you are reading), and Javadocs.

See what’s new and what’s new part 2 articles (most of them applies to 1.0.3 and 1.0.2 as well) for more information on this release.

Released 27 December 2012

v1.0.1 contains some key enhancements and bug fixes:

  • Removed commons-lang transitive dependency. Minor bug fixes.

Released 24 December 2012

v1.0.0 contains following key features:

  • Mapping between Java interfaces and properties files.
  • @DefaultValue and @Key annotations.
  • @Sources annotation for loading properties from specified urls.

See article Introducing OWNER, a tiny framework for Java Properties files.

2 March 2018

Hi All!

Just a quick post to announce that a Telegram Chat is available for quick Q&A.

If you don’t already know Telegram, you should check it out right now!

It’s possibly the fastest way you can get in touch with devs and other users and have your questions answered lightning fast.

So, join OWNER API Users and Devs Telegram Chat and let’s keep in touch!

–Luigi

Project maintained by Matteo Baccan, and the awesome contributors.

Developed with IntelliJ IDEA

Hosted on GitHub