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.
Why 2.0.0
Section titled “Why 2.0.0”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=falserestores 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 anIllegalArgumentExceptionnaming 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]andparent.tag[1], which is what a list is read from, and there is no longer aparent.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 readsparent.tagfrom 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.
setTypeConverterandremoveTypeConverterare instance methods of theFactory, 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 byConfigFactory.newInstance()is isolated in its properties, its loaders, its value handlers, its prefix and its strictness — conversion was the one thing that leaked. The staticConfigFactory.setTypeConverteris the default factory, exactly assetPropertyandregisterLoaderare. 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.
The 1.0.12 test suite, run against 2.0.0
Section titled “The 1.0.12 test suite, run against 2.0.0”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.
testNonInstantiableTokenizerandtestConverterCantBeAccessedasserted that a private class named in an annotation cannot be instantiated — anUnsupportedOperationExceptioncaused by anIllegalAccessException. 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.
PropertiesInvocationHandlerTestputs a Mockito@Spyonjava.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.
RELEASE NOTES
Section titled “RELEASE NOTES”OWNER v2.0.0 contains following enhancements and bug fixes.
Removals
Section titled “Removals”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-java8artifact is gone. Its only feature, the support fordefaultmethods in config interfaces, is now built into the core: replace theowner-java8dependency withownerand everything keeps working. - The
owner-java8-extrasartifact is gone. TheDurationConverter,ByteSizeConverterand theByteSize/ByteSizeUnit/ByteSizeStandardclasses it contained are now part of the coreownerartifact, with unchanged package names: replace theowner-java8-extrasdependency withowner, and noimportchanges. They were shipped apart only because the core had to run on Java 6 and could not so much as namejava.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-extrasis left with what actually needs a third party library on the classpath — the ZooKeeper loader. ZooKeeperLoadermoved fromorg.aeonbits.owner.loaderstoorg.aeonbits.owner.extras.loaders: change the import, nothing else. It was the one class ofowner-extrassitting 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 theLoaderinterface 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 importsorg.aeonbits.owner.loaders.Loader.- The internal utility class
org.aeonbits.owner.util.Base64is gone. It was a runtime-selection shim betweenjava.util.Base64(Java 8+) andjavax.xml.bind.DatatypeConverter(Java 6/7), never used by the library API itself. If you referenced it, usejava.util.Base64directly:Base64.encode(bytes)becomesBase64.getEncoder().encodeToString(bytes)andBase64.decode(string)becomesBase64.getDecoder().decode(string). - The internal utility method
org.aeonbits.owner.util.Util.eq(a, b)is gone: it predatedjava.util.Objectsand did exactly what the standardObjects.equals(a, b)does — use that instead. - The test-support methods
Util.save(File, Properties),Util.saveJar(File, String, Properties)andUtil.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 standardProperties.store,java.util.jar.JarOutputStreamandjava.nio.file.FilesAPIs cover the same ground.
Enhancements
Section titled “Enhancements”-
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, anObjectMapperor a data source, cannot be built out of a no-argument constructor — and it is what #222 asked for in 2018.registerLoaderandregisterValueHandlerhave 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.Converteris nowSerializable, likeLoaderandValueHandler, 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()andtoString()showed the other modules’ keys, the environment and the imports besides, so printing a configuration to a log printed somebody else’s database.@DeclaredOnlyon the interface, orowner.declared.onlyon 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 asmyproject.debug, but the key as written is where a@DefaultValueis 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 reachedsave(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 withjava.util.Formatterand ajava.text.MessageFormatpattern is not a broken format string but a correct one in another dialect, soString.formatraised 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.MessageFormatitself is still not supported and deliberately: a configuration binder is not an i18n engine, and adefaultmethod 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@DefaultValueof each method with its@Descriptionabove 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 noAccessibleon the interface, whichsave(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 tojava.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 asserver.http.port=8080, wherePropertiesrefuses it. It closes the asymmetry withstoreToXML, which has been onAccessiblesince 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’sloadFromXMLcloses 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,
setPropertyreturned the old value, and “the property did not change and I cannot see why” had no answer. The library says it now, atCONFIG— what was refused, and the listener’s own message if it gave one. AtCONFIGand not as a warning, because a listener refusing is a listener working: #58 asked for aRollbackListener, and what it wanted was to be told. -
The conventional file can be named, with
owner:default— the constantConfig.Sources.CONVENTIONAL. Written among the sources of@Sourcesit 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 — readingCORE_THREAD_NUMBERforcore.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 findsfirstName,first-name,first_nameorFIRST_NAME— and the last of those is a name a shell can actually set, soserver.maxThreadsis looked for asSERVER_MAX_THREADSand not asSERVER.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 = LuigiFour 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,FirstNameandfirst.nameare not spellings of this key. The forms are derived from the key, so@Key("first-name")is equally found underFIRST_NAME, and one form applies to the whole key at once — prefixes and nesting included, somy-db.user-nameis a spelling ofmyDb.userNameand 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
WARNINGnaming both — and refused outright underowner.strict.Nothing is added to the properties and nothing is renamed:
store(),list()andpropertyNames()show the keys exactly as they were loaded,getProperty("firstName")still answers aboutfirstName, and aTraceableorigin stays attached to the key that really exists. The prefix aMapor 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 calledportand 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-extrasand a validation provider on the class path and every constrained property is checked when the configuration is created, next to the@Mandatorycheck 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 nullEvery 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>andList<@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
defaultmethod is your own code, a nested-section accessor is a view that is never null, and a constraint written on anOptionalapplies to the container — and each of them is now named, with its reason, as aWARNING, 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.validationfor the Java 8 world this library still compiles for,jakarta.validationfor everything current — and both are optional dependencies ofowner-extras: nothing is shipped, nothing is transitive, and a configuration with no constraint on it pays nothing at all.ConfigValidatoris 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.cryptowas 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: acommand 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 itgets the secret rather than the ciphertext, and `store()` writes the marker back because the propertieshold 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 constructionrather than by a second mechanism.
`@EncryptedValue` and `@DecryptorClass` are **not deprecated** and still work as they always did — theyare what every configuration written before 2.0.0 uses. The one thing refused is carrying both on onemethod, 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, aConverter, aTokenizerand a decryptor may now be package-private, or aprivate staticclass 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 {@Overridepublic 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 withClass.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 theConfigFactory, the system properties and the environment — the same three, in the same order, that expand a@Sourcesspec. 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— sounitis 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.valueandunitare untouched and still decide whenintervalis 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 ofvalue— was already the one Luigi Viggiano argued for. See Hot reload. -
TOML is read, from a source whose path ends in
.toml, andMyConfig.tomljoins 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-testruns 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]]isservers[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,0o755and0b1101become plain decimals;infandnanbecomeInfinityandNaN; and the space TOML allows in place of a date-time’sTbecomes aT. Strings and ordinary decimals are untouched. The four date-time types need nothing registered. See File formats. -
The
java.timetypes are read out of the box —LocalDate,LocalTime,LocalDateTime,OffsetDateTime,Instant,ZoneId,Yearand the rest — with nothing to register. The conversion chain now understands two more ways of building a type from text: a public staticof(String)and a public staticparse(CharSequence), alongside theStringconstructor andvalueOf(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.timetypes worked before: they have noStringconstructor and novalueOf, so the chain ran out and refused them. Where MicroProfile tries theStringconstructor 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, andMyConfig.confjoins 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 andincludeincluded, 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.conffiles that already exist; a hand-written subset would refuse substitutions, merging andinclude, 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:configis an optional dependency ofowner-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.conffails — naming the source and the artifact to add. See File formats. Closes #240. -
The ZooKeeper loader no longer has to be registered.
ZooKeeperLoaderis now found on the classpath like every other loader, so@Sources("zookeeper://…")works withConfigFactory.createand there is noregisterLoadercall 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 azookeeper: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 azookeeper:source without Curator now names the source and the artifact to add, where it used to be aNoClassDefFoundError.
-
defaultmethods in config interfaces work out of the box with the coreownerartifact: no extra dependency is needed anymore (see Removals above). -
New
Accessible.store(Writer, String)overload, mirroringProperties.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
Optionalof any supported type, which comes back empty when the property is defined nowhere and has no default, instead of returningnull:Optional<Integer> port()reads the same valueInteger 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 ofOptional<List<String>>— and a value that is wrong rather than missing keeps failing, so a typo does not silently become an emptyOptional. An empty value stays a value, as it does everywhere else.@MandatoryandOptionalwritten on the same method contradict each other and are reported when the Config object is created, while a@Mandatorywritten on the interface leaves anOptionalmethod alone, being the exception it declares. See the documentation. -
New
@Sensitiveannotation: the value of the annotated property (or of every property of the annotated interface) is printed as********byAccessible.list()and bytoString(). A password written in clear in a properties file is a value like any other to this library, and acfg.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,storeToXMLand 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@EncryptedValuefor 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
@Mandatoryannotation: mark a property (or a whole interface) as required, and get aMissingMandatoryPropertyExceptionlisting 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
@Prefixannotation: declare the common prefix of a group of keys once, on the interface, instead of repeating it in the@Keyof every method.@Prefix("server.")makesString hostname()resolve toserver.hostname, and it is prepended to the@Keyvalue 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 ofDisableableFeature. Nothing changes for existing configurations: an interface without@Prefixresolves 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
@Prefixof their own:owner.key.prefixprepends a literal to every key, andowner.key.prefix.from.packagederives it from the package of the interface declaring the method, socom.example.ServerConfig.port()readscom.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 theFactoryinterface, 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.@Prefixwins 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 likeport=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 inport=${PORT}withPORTunset. 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. -
ByteSizeimplementsComparable, ordering sizes by the amount of data they represent whatever unit they are written in, so1 MBsorts before1 MiB. The ordering is consistent with equality, which is what makes aTreeSetof byte sizes agree with aHashSeton 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 as2.048576 MBin SI and as1.95367431640625 MiBin IEC. WhereconvertTohas 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. -
ByteSizeisSerializable, which aConfigobject already was. The unit is preserved along with the value, so a size written as1 MBcomes back reading as1 MBand not as1000000 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 anInvalidObjectExceptioninstead of producing an object that fails later. -
#320:
EnumSetandSet<Enum>are now supported by type conversion (thanks to @dexman545). -
java.time.Durationis converted out of the box, like aFileor aURL, 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 dand the ISO-8601 formPT15Mare all read, in collections, arrays andOptionallike any other type. The time unit is required on this path:timeout=30is 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 forDuration, or named with@ConverterClass, still takes precedence over the automatic conversion. See the documentation. -
#187:
java.nio.file.Pathis converted, with a leading~expanded to the user home exactly asjava.io.Filealready was — the two ways of naming a path no longer disagree. Arrays and collections ofPathfollow. 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 tolocalhostwhendb.hostis defined nowhere, instead of to the empty string. Everything after the first colon is the default, colons included, so URLs, Windows paths andhost:portpairs 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 likejdbc:urlkeeps resolving as before. The one behaviour that changes is a variable that used to resolve to nothing:${a:b}, with neithera:bnoradefined, yielded the empty string up to 1.0.12 and yieldsbfrom 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 ofenv. It works at any depth, in the@Key, in a property value and in the@Sourcesspecification, 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 asa{bkeeps 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
Mapreturn type now reads the group of properties below the key of the method, closing a request open since 2013 (#41):something.foo=1something.bar=2Map<String, Integer> something(); // {foo=1, bar=2}Both sides of the entry go through the regular type conversion, so
Map<Integer, String>andMap<Colour, String>work as well; the group is named like any other key, so@Key,@Prefixand variable expansion all apply —@Key("servers.${env}")picks the section at runtime. A name with further dots keeps them, sosomething.a.bbecomes the entrya.b; no match gives an empty map rather thannull; the declared map type is honoured — a class is instantiated as itself, and an interface is given an implementation that satisfies it:LinkedHashMapforMap,TreeMapforSortedMapandNavigableMap,ConcurrentHashMapforConcurrentMap,ConcurrentSkipListMapforConcurrentNavigableMap, and anEnumMapover 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 aClassCastExceptionon the way out; and@DefaultValueis refused on such a method, since a default belongs to the individual properties. A@ConverterClassstill 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: aMapreturn 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]=alphaservers[1]=betaList<String> servers(); // [alpha, beta]for every array and collection type,
Optionalincluded. The point of it is that an element written this way is one element whatever it contains, soservers[0]=a,bis 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 thanservers.0because the dot already belongs to theMapgrouping 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-formatsartifact — and it is a subset, which is said here rather than discovered later:server:host: localhostservers:- host: alpha- host: betaports: [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: yesis the textyesandcountry: nois the stringno— 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. Writetruewhen 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
.jsonsource is read as soon as the artifact is on the class path. The document’s shape becomes the keys,server.hostandservers[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 —
1e3stays1e3, and a long past 2^53 keeps its last digits.Three things the specification leaves open, decided and written down: a
nullwrites no key at all,Propertiesbeing 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 —
Propertiesfor properties, SAX for XML, and.envand 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
Configreads the section of the configuration below its own key:server.host=localhostserver.port=8080ServerConfig server(); // server.host, server.portThe accessor names the section rather than the type it returns, so
@Keyrenames 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@Mandatoryproperty 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@Prefixon a nested interface composes with the path it hangs from — unlike the prefix configured on a factory, which@Prefixoverrides — 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>readsservers[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 aListof itself is a tree and is allowed, where a type holding itself is refused — the keys say how deep it goes.Map<String, ServerConfig>readsservers.alpha.hostandservers.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
Optionalsection is present when anything at all was written below its path. A@DefaultValuedeclared 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@Mandatorywritten on the accessor of a section is refused when the configuration is created, since the check could never fail;@Mandatoryon the properties inside is the one that means something. Nothing can break: a method returning an interface extendingConfighad 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
nullor its default, and a file full of values that look right. AtFINEevery 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. AtCONFIG, 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:propertiescan answer: a resource inside a jar served over the network, anhttp:source,system:envcannot. Those were dropped from the watch list in silence, which is where “I changed the file and nothing happened” comes from. It is now aWARNINGnaming them, once, when the configuration is created — different from an absent source, which stays silent, because here somebody wrote@HotReloadand for that source it will never fire. What is being watched, of which kind and how often, is written atCONFIGbeside 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 — withFIRSTevery miss but the last is the feature working, and a configuration with no@Sourcesprobes four names per interface — while a source that is there and refuses is aWARNING, and declared sources of which not one could be read are aWARNINGof 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:
FileInputStreamthrowsFileNotFoundExceptionfor 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 aclasspath: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’soptional:the other way up, which is what keeps every fallback written so far working unchanged. Unlike a dialect,requiredis 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
Accessiblefamily,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.propertiescfg.originOf("port").kind(); // SOURCE, IMPORT, DEFAULT_VALUE or RUNTIMEMerging 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 underMERGEthe one recorded is the source whose value survived — the first declared. UnderFIRSTthe 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,setPropertymakes 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.propertiesappears ashttps://***@config/app.properties, the same masking the log lines and the exception messages already use. Issue #277. -
.envfiles are read, which is how container tooling carries configuration into a process —docker run --env-file,env_filein Compose,envFromin Kubernetes, the secrets of a CI pipeline. Any source whose path ends in.envis read this way, values go through the usual type conversion, and the parser is ours: the core still has no dependencies.There is no
.envstandard, and the tools that read one disagree on the point that bites hardest, which is quoting: givenNAME="Matteo",docker run --env-filegives you the quotes and thedotenvfamily does not — and Docker Compose does not agree withdocker 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.dockeris 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 oneWARNING. A.envis 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 = valuebelow 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.iniand.cfgare both recognised and both looked for beside the configuration class.A repeated key is a list —
servers.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’sconfigparserrefuses 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
.envthe rules are a dialect:iniby default — the conservative common denominator every surveyed tool agrees with — plusgit, which reads a subsection, so[remote "origin"]holding aurlbecomesremote.origin.url, the very keygit configprints; andpython, 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
pythondialect refuses rather than half-honours:ConfigParserinterpolates%(name)sby default and OWNER never will, expanding${…}itself after loading and across every source. A value holding%(…)sread 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, sohttps://config/app.env?token=abc#dialect=dotenvsends 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 aclasspath: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.Loaderand 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 strayMyConfig.yamlstart beating theMyConfig.propertiesan 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,
registerLoaderstill 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 theCONFIGlogging level, including when it found nothing.org.aeonbits.owner.level = CONFIGis 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 —.yamland.yml,.iniand.cfg. It is adefaultmethod, and so isdefaultSpecFor, which now returnsnullby default: declining to be looked for is a choice a loader is allowed to make, andSystemLoaderandDotEnvLoaderboth make it. Nothing that implementsLoadertoday 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
IllegalArgumentExceptionnames 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 aStackOverflowError. 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 isdb.host=localhost. See the documentation. -
New
@CollectionConverterClassannotation: hands the raw property value to a single converter instead of splitting it first and converting one element at a time, as@ConverterClassdoes. 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 aCollectionreports which method is at fault instead of failing later with aClassCastException. See the documentation. Contributed by Adam Huječek in #248, closing #206. -
Security hardening of the
XMLLoaderagainst 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-Name—org.aeonbits.ownerandorg.aeonbits.owner.extras— so arequireswritten 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 theZooKeeperLoadermove 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.
Code quality and test coverage
Section titled “Code quality and test coverage”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.
@Separatorhad lost its documentation the day@Descriptionarrived,Accessible.storeToXMLthe daysave(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-qualityquery 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
staticturned 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.
Site Enhancements
Section titled “Site Enhancements”- 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:
@Sourcesaccumulates across the interfaces a mapping interface extends — by design, since it describes a set and not a single setting — while@LoadPolicyand@HotReloadtake 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 “
Mapis 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@ConverterClassrecipe 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
@EncryptedValueand@DecryptorClassannotations 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.
Infrastructure
Section titled “Infrastructure”- 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:formatadds or fixes it, andmvn license:check— bound to theverifyphase, 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.
Bugs fixes
Section titled “Bugs fixes”-
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
PropertiesLoadermust come last because it accepts every URL it can resolve. As a side effectMyConfig.iniandMyConfig.cfgsilently outrankedMyConfig.properties, so an application reading its.propertiesfor years would have stopped the day somebody dropped a.cfgin the same directory —.cfgbeing 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 underowner.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,@LoadPolicyand@HotReloadwere 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@HotReloadwritten 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 theextendsclause, then their parents, each interface visited once. If you were repeating an annotation on the interface handed to theConfigFactoryto work around this, the repetition can go — and for@Sourcesit should, since the same file would now be listed twice. -
@DecryptorClasswas 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@EncryptedValueproperty 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@Descriptionthat becomes the header of a saved file. -
@DisableFeatureon 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 whatgetPropertyandfillhave to ask, being declared onAccessibleand 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 whilecfg.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@Prefixwritten 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.propertiesconvention was appended even to a configuration that declared its sources. The convention and the declared sources were the same call, so every interface without@Sourcescontributed the default list — andConfigitself 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 aMyConfig.propertiesleft on the classpath, and theCONFIGdiagnostic 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.acceptcompared the URI’s scheme against its own without allowing for a URI that has none, so it raised aNullPointerExceptionrather 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 blankfile:, 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.
XMLLoadervalidates — 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=falseon 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.
XMLLoaderdecided fromURL.getFile(), which by contract is the path plus the query, so@Sources("https://config/app.xml?v=2")failed its own test, fell through toPropertiesLoader— 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 afile:orjar: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
Mapentries whose key or value is not aStringare now rejected with anIllegalArgumentExceptionnaming 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 admitsStringkeys and values, but which extendsHashtable<Object, Object>and therefore accepts anything throughputAll. The entry then became invisible togetProperty, 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()returnsnull, shadowing@DefaultValue("1")IllegalArgumentExceptionatcreate()imports.put(42, "42")the entry is dropped, someValue()returns the default1IllegalArgumentExceptionatcreate()imports.put("some.key", new StringBuilder("42"))someValue()returnsnullIllegalArgumentExceptionatcreate()imports.put("some.key", "42")someValue()returns42unchanged, returns 42Compatibility. Code that imported only
Stringkeys 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 heterogeneousMapthat happened to be read only through itsStringentries will now fail fast atcreate()time rather than half-working. Note that aCharSequenceis not sufficient, asPropertiescompares againstStringspecifically: calltoString()onStringBuilder/StringBuffervalues before importing them. See the documentation.The validation lives in the factory, so it applies uniformly to
ConfigFactory.create(), to aFactoryobtained fromConfigFactory.newInstance(), and toConfigCache.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 mismatchinstead of the documentedUnsupportedOperationException. A property like@DefaultValue("1, 2, foo, 4")mapped to a custom type now reportsCannot convert 'foo' to MyType, consistently with what already happened for a non-array property. For the same reason, a@ConverterClassreturningnullfor an element now yields anullelement instead of failing the whole conversion. -
Conversion errors now name the property they come from:
Cannot convert 'abc' to intbecameCannot 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,@Keyand@Prefixincluded (#191). When the value comes from a group of properties read as aMap, the key named is the individual entry —group.second, notgroup. -
ByteSizeUnit.parseno longer depends on the default locale of the JVM. It lowercased the text without saying in which language, and in Turkish a capitalIlowercases to the dotlessı:512 KIBwas 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 ani. -
ByteSizehonours theequals/hashCodecontract.equalscompares the number of bytes, so1 MBand1000000 Bare 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 aHashMapor as an element of aHashSet— a set could hold the same size twice, and a lookup could miss. The class is nowfinal, 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
NullPointerExceptionmasking 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.
1.0.12
Section titled “1.0.12”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.
RELEASE NOTES
Section titled “RELEASE NOTES”OWNER v1.0.12 contains following enhancements and bug fixes.
Enhancements
Section titled “Enhancements”- None
Site Enhancements
Section titled “Site Enhancements”- None
Bugs fixes
Section titled “Bugs fixes”- 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.
Enhancements
Section titled “Enhancements”- #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.
Site Enhancements
Section titled “Site Enhancements”- #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.
Bugs fixes
Section titled “Bugs fixes”- 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.
1.0.10
Section titled “1.0.10”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.
RELEASE NOTES
Section titled “RELEASE NOTES”OWNER v1.0.10 contains following enhancements and bug fixes.
Enhancements
Section titled “Enhancements”- 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-java8dependency. 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 toConfigCache. 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
@EncryptedValueand@DecryptorClassannotations 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.classinowner-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
ByteSizeConverterandDurationConverterclasses inowner-java8-extrasjar, 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,@LoadPolicyand@HotReload. Sources defined for all extended interfaces will be merged. LoadPolicy and HotReload can be inherited and override by the extended interface. Thanks @chengmingwang.
Bugs fixes
Section titled “Bugs fixes”- Replaced
fixBackslashForRegexwith better implementation. Thanks @kiefinger. - Have
ConfigFactorythrow 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.
Site Enhancements
Section titled “Site Enhancements”- Added Crypto support documentation page.
- Added ByteSize Converter converter and Duration Converter documentation section. Thanks @StFS.
- Chinese documentation has been contributed by @cyfonly and is available here. Sorry, I cannot check that everything is correct or update that! :-) See #172.
- Added security/stability badges by Meterian. Thanks @fdiotalevi, @bbossola
Downloadable artifacts are published on GitHub and on Maven Central Repository.
Released 22 July 2015
v1.0.9 contains following enhancements and bug fixes.
Enhancements
Section titled “Enhancements”- Added
fill(java.util.Map)method to theAccessibleinterface. - Added pre-processing feature. See #120, thanks @a1730 for the feedback.
Site Enhancements
Section titled “Site Enhancements”- None.
Bugs fixes
Section titled “Bugs fixes”- 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.
Enhancements
Section titled “Enhancements”- Fixed the javadocs included in the tarballs/zips released.
Site Enhancements
Section titled “Site Enhancements”- None.
Bugs fixes
Section titled “Bugs fixes”- No
owner-parentpom 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.
Enhancements
Section titled “Enhancements”- Added JMX Support. See #107 and #19. Thanks @robinmeiss. I still need to write the documentation on how to use it (sorry).
- Added examples module, containing some example Maven Java projects to show some of the API features. This gets packaged in the released archive artifacts (zip and tarballs).
Site Enhancements
Section titled “Site Enhancements”- None.
Bugs fixes
Section titled “Bugs fixes”- Fixed packaging: the
owner-extras.jarwas 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.
Enhancements
Section titled “Enhancements”- 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.
Site Enhancements
Section titled “Site Enhancements”- Fixed documentation errors. See #88, #89, #92. Thanks @hemus2121.
- Minor changes in build.xml (ant publishing script to gh-pages)
Bugs fixes
Section titled “Bugs fixes”- Use of default value for for properties using the Key Expansion mechanism #84.
Downloadable artifacts are published on GitHub and on Maven Central Repository.
1.0.5.1
Section titled “1.0.5.1”Released 28 May 2014
v1.0.5.1 contains following enhancements and bug fixes.
Enhancements
Section titled “Enhancements”- 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
@Keyannotation. (See #63) - Restructured maven project to allow sub-modules.
Site Enhancements
Section titled “Site Enhancements”- Documentation website minor style/layout, updates and improvements.
- Added SlideShare presentation in home page.
Bugs fixes
Section titled “Bugs fixes”- 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.
Enhancements
Section titled “Enhancements”- 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()toConfigFactory, 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@Sourcesis not specified. - Added method
Set<String> propertyNames()in theAccessibleinterface.
(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 OWNERFactoryobjects.
(See #43). - Added implementation on
hashCode()andequals(). - Added serialization capability to OWNER
Configobjects, so now they can be transferred through the network or transformed to byte streams.
(See #54). - Allow
@ConverterClassannotation to override default converters (i.e. primitive types, etc). - The interfaces
Reloadable,MutableandAccessiblenow extend fromConfig, 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 validConfigobject that can be instantiated by theConfigFactory:

Site Enhancements
Section titled “Site Enhancements”- Website sources reorganized: moved from
gh-pagesbranch tomaster, with publish ant scriptsbuild.xml. - Added news section, with release announcements and blog posts.
Bugs fixes
Section titled “Bugs fixes”- 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.
1.0.4.1
Section titled “1.0.4.1”Released 19 September 2013
v1.0.4.1 is a bug fix release for v1.0.4 branch.
Bugs fixes
Section titled “Bugs fixes”- 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.
Enhancements
Section titled “Enhancements”- New
@ConverterClassannotation. See The @ConverterClass annotation, #38. - Hot reload for file based sources. See Automatic “hot reload”, #15.
- toString() method can be invoked on the Config object to get some useful text for debugging. See The toString() method, #33.
- Added
Mutableinterface for the methods giving write access to the underlying properties structure: setProperty, removeProperty, clear. See The Mutable interface, #31. - Added
Accessibleinterface for thelist()methods used to aid debugging, and other methods giving read access to the underlying properties structure. See The Accessible interface. - Added the
reload()method that can be exposed implementing the interfaceReloadable. See Programmatic reload. - Fist class Java Arrays and Collections support in type conversion. Thanks ffbit. See Arrays and Collections, #21, #22 and #24.
- Implemented
@DisableFeatureannotation to provide the possibility to disable variable expansion and parametrized formatting. See Disabling Features, #20.
Site Enhancements
Section titled “Site Enhancements”- 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.
Bugs fixes
Section titled “Bugs fixes”- Fixed bug #40 about tilde expansion.
- Fixed bug #17 Substitution and format not working as expected when used together.
1.0.3.1
Section titled “1.0.3.1”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
ownertoorg.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.
@DefaultValueand@Keyannotations.@Sourcesannotation for loading properties from specified urls.
See article Introducing OWNER, a tiny framework for Java Properties files.
Other announcements
Section titled “Other announcements”Telegram Chat for Users and Devs
Section titled “Telegram Chat for Users and Devs”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