diff --git a/api/src/main/java/javax/config/ComposedConfig.java b/api/src/main/java/javax/config/ComposedConfig.java new file mode 100644 index 0000000..e67bc98 --- /dev/null +++ b/api/src/main/java/javax/config/ComposedConfig.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2016-2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.config; + +import javax.config.inject.ConfigProperty; +import javax.enterprise.util.Nonbinding; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Specifies a configuration type as composed configuration. + * Composed configuration types comprise multiple, potentially hierarchical, configuration values. + * + *

Examples

+ * + *

Composed, coherent configuration

+ *

+ * The following example defines a coherent server socket configuration. + * + *

+ * @ComposedConfig
+ * public class SocketConfig {
+ *
+ *     @ConfigProperty(name = "name")
+ *     private String name;
+ *
+ *     @ConfigProperty(name = "protocol", defaultValue = "http")
+ *     private String protocolName;
+ *
+ *     @ConfigProperty(name = "port")
+ *     private int port;
+ *
+ *     // getters & setters
+ * }
+ * 
+ *

+ * The {@code SocketConfig} configuration can be retrieved like any other configured value, by programmatic lookup, or dependency injection: + * + *

+ * public class SomeBean {
+ *
+ *     @Inject
+ *     @ConfigProperty(name = "server.socket")
+ *     private SocketConfig socketConfig;
+ *
+ * }
+ * 
+ *

+ * The example will resolve the configuration values as follows, provided by the corresponding property keys: + * + *

+ * server.socket.name
+ * server.socket.protocol
+ * server.socket.port
+ * 
+ * + *

Implicit property resolution

+ *

+ * It's possible to omit the individual {@link ConfigProperty} annotations on the fields of the composed type. + * In this case the composed property keys are derived from the field names: + *

+ * + *

+ * public class SomeBean {
+ *
+ *     @Inject
+ *     @ConfigProperty(name = "server.socket")
+ *     private SocketConfig socketConfig;
+ *
+ *     @ComposedConfig
+ *     public static class SocketConfig {
+ *
+ *         private String name;
+ *         private String protocol;
+ *         private int port;
+ *
+ *         // getters & setters
+ *     }
+ * }
+ * 
+ * + *

+ * This example will result in the same configuration resolution as in the previous example, apart from the default value for {@code server.socket.protocol}. + *

+ * If the property keys differ from the field names, they can be overridden individually via the {@link ConfigProperty} annotation. + * The same is true for default values, as seen before. + *

+ * The {@link ConfigProperty} annotation can be annotated on fields as well as on methods. + * The latter is useful if interfaces instead of classes are defined as composed types. + * Per default, methods are not implicitly taken to resolve composed configuration properties. + * If both fields and methods are annotated within a single type, methods take precedence. + *

+ * See the following example for a composed interface configuration type. + *

+ * + *

+ * @ComposedConfig
+ * public interface SocketConfig {
+ *
+ *     @ConfigProperty(name = "name")
+ *     String name();
+ *
+ *     @ConfigProperty(name = "protocol", defaultValue = "http")
+ *     String protocolName();
+ *
+ *     @ConfigProperty(name = "port")
+ *     int getPort();
+ * }
+ * 
+ *

+ * This example will result in the same configuration as before. + * + *

Hierarchical type resolution

+ *

+ * The configuration properties of composed types are resolved hierarchically. + * That is, properties in composed types are implicitly considered as possible composed types themselves, as well. + * This allows developers to define complex configuration structures without repeating annotations. + *

+ * The types of composed properties are therefore resolved as possible composed configuration types, if no built-in, custom, or implicit converters are defined. + *

+ * The hierarchical resolution works both for implicitly resolved fields and explicitly annotated members. + * + *

+ * @ComposedConfig
+ * public class ServerConfig {
+ *
+ *     private String host;
+ *     private SocketConfig socket;
+ *
+ *     // getters & setters
+ *
+ *     public static class SocketConfig {
+ *         private String name;
+ *         private String protocol;
+ *         private int port;
+ *
+ *         // getters & setters
+ *     }
+ *
+ * }
+ * 
+ *

+ * If a {@code ServerConfig} configuration type is retrieved, the property keys are resolved as follows: + * + *

+ * public class SomeBean {
+ *
+ *     @Inject
+ *     @ConfigProperty(name = "server")
+ *     private ServerConfig serverConfig;
+ *
+ * }
+ * 
+ *

+ * This leads to: + * + *

+ * server.host
+ * server.socket.name
+ * server.socket.protocol
+ * server.socket.port
+ * 
+ *

+ * The property keys are resolved by the field names, or the names defined in {@link ConfigProperty}, respectively, and combined via dot ({@code .}). + *

+ * The example above is congruent with annotating {@code SocketConfig} with {@link ComposedConfig}, as well. + * + *

Collection resolution

+ *

+ * Composed configuration types also resolve collections and array types. + * + *

+ * @ComposedConfig
+ * public class MultiSocketServerConfig {
+ *
+ *     private String[] hosts;
+ *     private List sockets;
+ *
+ *     // getters & setters
+ *
+ *     public static class SocketConfig {
+ *         private String name;
+ *         private String protocol;
+ *         private int port;
+ *
+ *         // getters & setters
+ *     }
+ *
+ * }
+ * 
+ *

+ * If the {@code MultiSocketServerConfig} type is resolved by key {@code alternative-server}, it results in the following: + * + *

+ * server.hosts.0
+ * server.hosts.1
+ *
+ * server.sockets.0.name
+ * server.sockets.0.protocol
+ * server.sockets.1.name
+ * 
+ *

+ * Element types of collections and arrays are resolved by an implicit zero-based index, which is part of the resulting, combined property key. + *

+ * This collection resolution works for array types, and types that are assignable to {@link java.util.Collection}. + * For unordered collection types, e.g. {@link java.util.Set}, the order in which the configured elements will be retrieved is non-deterministic, despite the (zero-based) indexed key names. + *

+ * Similar to singular sub-types, the element types within the collection or array are resolved by potentially existent converters, and resolved recursively if no built-in, custom, or implicit converters are defined. + * + * @author Sebastian Daschner + */ +@Retention(RUNTIME) +@Target(TYPE) +public @interface ComposedConfig { + + /** + * Only valid for injection of dynamically readable values, e.g. {@code Provider}! + * + * @return {@code TimeUnit} for {@link #cacheFor()} + */ + @Nonbinding + TimeUnit cacheTimeUnit() default TimeUnit.SECONDS; + + /** + * Only valid for injection of dynamically readable values, e.g. {@code Provider}! + * + * @return how long should dynamic values be locally cached. Measured in {@link #cacheTimeUnit()}. + */ + @Nonbinding + long cacheFor() default 0L; +} diff --git a/spec/src/main/asciidoc/composed-configuration.adoc b/spec/src/main/asciidoc/composed-configuration.adoc new file mode 100644 index 0000000..321e5e7 --- /dev/null +++ b/spec/src/main/asciidoc/composed-configuration.adoc @@ -0,0 +1,375 @@ +// +// Copyright (c) 2016-2018 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// Contributors: +// Sebastian Daschner + +[[composed-configuration]] +== Composed Configuration + +Developers can specify composed configuration types in order to define multiple, coherent configuration values, and access them conveniently and consistently. +Composed configuration types, which are annotated with `@ComposedConfig`, comprise multiple, potentially hierarchical, configuration values. + +=== Usage + +See the following example that defines a coherent server socket configuration. + +[source,java] +---- +@ComposedConfig +public class SocketConfig { + + @ConfigProperty(name = "name") + private String name; + + @ConfigProperty(name = "protocol", defaultValue = "http") + private String protocolName; + + @ConfigProperty(name = "port") + private int port; + + // getters & setters +} +---- + +The `SocketConfig` configuration can be retrieved like any other configured value, by programmatic lookup, or dependency injection: + +[source,java] +---- +public class SomeBean { + + @Inject + @ConfigProperty(name = "server.socket") + private SocketConfig socketConfig; + +} +---- + +The example will resolve the configuration values as follows, provided by the corresponding property keys: + +[source,text] +---- +socketConfig.getName() -> server.socket.name +socketConfig.getProtocolName() -> server.socket.protocol (or "http" if undefined) +socketConfig.getPort() -> server.socket.port +---- + + +==== Implicit Property Resolution + +It's possible to omit the individual `@ConfigProperty` annotations on the fields of the composed type. +In this case the composed property keys are derived from the field names: + +[source,java] +---- +public class SomeBean { + + @Inject + @ConfigProperty(name = "server.socket") + private SocketConfig socketConfig; + + @ComposedConfig + public static class SocketConfig { + + private String name; + private String protocol; + private int port; + + // getters & setters + } +} +---- + +This example will result in the same configuration resolution as in the previous example, apart from the default value (`http`) for `server.socket.protocol`. + +If the property keys differ from the field names, they can be overridden individually via the `@ConfigProperty` annotation. +The same is true for default values, as seen before. + +The `@ConfigProperty` annotation can be annotated on fields as well as on methods. +The latter is useful if interfaces instead of classes are defined as composed types. +Per default, methods are not implicitly taken to resolve composed configuration properties. +If both fields and methods are annotated within a single type, methods take precedence. + +See the following example for a composed interface configuration type. + +[source,java] +---- +@ComposedConfig +public interface SocketConfig { + + @ConfigProperty(name = "name") + String name(); + + @ConfigProperty(name = "protocol", defaultValue = "http") + String protocolName(); + + @ConfigProperty(name = "port") + int getPort(); +} +---- + +Again, this example will result in the same configuration as before. + + +==== Hierarchical Type Resolution + +The configuration properties of composed types are resolved hierarchically. +That is, properties in composed types are implicitly considered as possible composed types themselves, as well. +This allows developers to define complex configuration structures without repeating annotations. + +The types of composed properties are therefore resolved as possible composed configuration types, if no built-in, custom, or implicit converters are defined. + +The hierarchical resolution works both for implicitly resolved fields and explicitly annotated members. + +[source,java] +---- +@ComposedConfig +public class ServerConfig { + + private String host; + private SocketConfig socket; + + // getters & setters + + public static class SocketConfig { + private String name; + private String protocol; + private int port; + + // getters & setters + } + +} +---- + +If a `ServerConfig` configuration type is retrieved, the property keys are resolved as follows: + +[source,java] +---- +public class SomeBean { + + @Inject + @ConfigProperty(name = "server") + private ServerConfig serverConfig; + +} +---- + +This leads to: + +[source,text] +---- +serverConfig.getHost() -> server.host +serverConfig.getSocket().getName() -> server.socket.name +serverConfig.getSocket().getProtocol() -> server.socket.protocol +serverConfig.getSocket().getPort() -> server.socket.port +---- + +The property keys are resolved by the field names, or the names defined in `@ConfigProperty`, respectively, and combined via dot (`.`). + +The example above is congruent with annotating `SocketConfig` with `@ComposedConfig`, as well. + + +==== Collection Resolution + +Composed configuration types also resolve collections and array types. + +[source,java] +---- +@ComposedConfig +public class MultiSocketServerConfig { + + private String[] hosts; + private List sockets; + + // getters & setters + + public static class SocketConfig { + private String name; + private String protocol; + private int port; + + // getters & setters + } + +} +---- + +If the `MultiSocketServerConfig` type is resolved by key `alternative-server`, it results in the following: + +[source,text] +---- +serverConfig.getHosts()[0] -> server.hosts.0 +serverConfig.getHosts()[1] -> server.hosts.1 +... +serverConfig.getSockets().get(0).getName() -> server.sockets.0.name +serverConfig.getSockets().get(0).getProtocol() -> server.sockets.0.protocol +serverConfig.getSockets().get(1).getName() -> server.sockets.1.name +... +---- + +Element types of collections and arrays are resolved by an implicit zero-based index, which is part of the resulting, combined property key. + +This collection resolution works for array types, and types that are assignable to `java.util.Collection`. +For unordered collection types, e.g. `java.util.Set`, the order in which the configured elements will be retrieved is non-deterministic, despite the (zero-based) indexed key names. + +Similar to singular sub-types, the element types within the collection or array are resolved by potentially existent converters, and resolved recursively if no built-in, custom, or implicit converters are defined. + +[[resolution]] +=== Resolution + +The following examines how the resolution for composed configuration values works. +This information is particularly interesting for implementors. + +1. The `Config` implementation detects whether the type of a retrieved configuration value is a composed configuration. +This is handled equally, whether the configuration is retrieved programmatically, or via dependency injection. +The configuration value type is considered a composed type if the type definition is annotated with `@ComposedConfig`. + +2. The retrieval of a composed value MUST be performed by a single config source at a time, in order of their defined priority. +Due to the potential hierarchical nature of composed configuration, the individual sources must define coherent configuration compositions. +Defining multiple parts of composed values in multiple config sources is not supported. +The config sources will override the whole composition of a composed configuration value individually. + +3. The individual, potentially hierarchical properties are resolved by the implementation by inspecting the composed configuration type definition. +The following order MUST be followed, while subsequent, colliding definitions might override the resolved field names. + - every non-static, non-final field that is not annotated with `@ConfigProperty`, with its declared field name as key suffix and field type as configured property type + - every non-static, non-final field that is annotated with `@ConfigProperty`, with the specified name as key suffix, optional default value and field type as configured property type + - every non-static, non-void method that is annotated with `@ConfigProperty`, with the specified name as key suffix, optional default value and method return type as configured property type + +4. The resolved properties are looked-up via the config source following the same mechanism as for any other configuration values, except the constraints mentioned in steps 5. and 6. +The configured keys used lookup the configuration values are concatenated as follows: + - the keys of all config properties in the hierarchy of the composed configuration value (see step 5.), individually joined by a dot (`.`) + - the key suffix derived from the defined property +For example, a composed configuration `serverConfig` with lookup key `server`, and property `socket` with identical implicit property key (`socket`), and property `name` with identical implicit property key (`name`) will be resolved as property key `server.socket.name`. + +5. Properties within the composed type are themselves resolved and inspected for composed types. +Unlike general configuration lookup, configured property types are considered as composed types, if no built-in, custom, or implicit converter is defined for them. +If no build-in, custom, or implicit converter could be resolved by the implementation, following the corresponding priorities, the configured type is considered a composed configuration and resolution is performed recursively, starting from step 3. +To ensure configuration consistency, implementations MUST resolve hierarchical sub-types using a single config source throughout the whole hierarchy for a single (root) composed configuration. +Unlike conventional configuration lookup, configured properties contained in composed types do not cause an error in case single configured properties are undefined (i.e. the computed property key doesn't lead to a configured value) in the config source, +In case a configured property is not defined, the value of the corresponding field is the default primitive value (e.g. `0` for `int`, `false` for `boolean`), `null` for reference types, `Optional.EMPTY` for `java.util.Optional` types, an empty array, or an empty collection of the corresponding collection type, respectively, depending on the property type. +In order to notify users of JSR 382 about configuration mismatches implementations SHOULD emit a warning if none of the resolved properties could be resolved within a (root) composed configuration value. + +6. Properties within the composed type are themselves resolved and inspected for collection types. +Following types are considered collection types: + - array types + - types assignable to `java.util.Collection` +The configured type comprised in the collection type are resolved recursively within the same config source. +Configured types are resolved as configuration values using built-in, custom, or implicit converters with their defined priority, or considered as composed types, if no built-in, custom, or implicit converter is defined for them. +The property keys of configured collection types are computed as follows: + - the property key of the collection type itself is computed following the rules described in step 4. + - every element of the collection is indexed with a zero-based integer index, which is concatenated separated by a dot (`.`). +For example, a collection type configuration `socketNames` of type `List` with property key `socket-names`, within a composed configuration type `server` with identical property key will be resolved as property keys `server.socket-names.0`, `server.socket-names.1`, etc. +Types within the collection that are themselves composed types compute their properties starting from the property key of the collection type and following the rules described in step 4. +Collection types that are not explicitly ordered compute the property keys following the same rules with non-deterministic ordering of the elements. +To ensure configuration consistency, implementations MUST resolve collection sub-types using a single config source throughout the whole hierarchy for a single (root) composed configuration. + + +=== Relationship to CDI Beans + +In order to define multiple, coherent configuration values conveniently, it's possible to inject conventional CDI scoped beans that themselves define configuration properties, without regarding composed configuration values. +An example looks as follows: + +[source,java] +---- +public class SomeBean { + + @Inject + private SocketConfig socketConfig; + +} +---- + +The `SomeBean` injects the dependent-scoped bean which comprised the coherent configuration values. + +[source,java] +---- +public class SocketConfig { + + @Inject + @ConfigProperty(name = "server.socket.name") + private String name; + + @Inject + @ConfigProperty(name = "server.socket.protocol", defaultValue = "http") + private String protocolName; + + @Inject + @ConfigProperty(name = "server.socket.port") + private int port; + + // accessor methods +} +---- + +This usage doesn't require composed configuration values. + +Developers might want to prefer to use composed configuration values in the following cases: + - complex hierarchies of configuration values must be realized + - collection types are used within the composed configuration hierarchy + - leaner syntax is preferred (possibility to omit `@Inject` and `@ConfigProperty` annotations + - duplication of configuration property key prefixes (e.g. `server.socket` should be avoided + - interface types should be used to define composed configuration types + - composed configuration types are defined by a third-party, or being reused, and thus can't or shouldn't be annotated with JSR 382 annotations + + +=== Hierarchical Configuration File Formats + +Config sources that are backed by file format with a hierarchical structure, such as XML, JSON, or YAML, SHOULD resolve the individual properties following the same rules as described in steps 4. and 6. of <> in order to end up with the same hierarchical semantics of the property keys. + +The following example illustrates this recommendation for a hypothetical config source that resolves YAML configuration files. + +Given the following YAML configuration structure: + +[source,yaml] +---- +server: + name: pet-1 + hosts: + - "foo.example.com" + - "bar.example.com" + sockets: + - name: http-default + protocol: http + port: 80 + - name: https-default + protocol: https + port: 443 +---- + +The YAML structure SHOULD result in configuration properties that are congruent with the following properties file definition: + +[source,text] +---- +server.name=pet-1 +server.hosts.0=foo.example.com +server.hosts.1=bar.example.com + +server.sockets.0.name=http-default +server.sockets.0.protocol=http +server.sockets.0.port=80 + +server.sockets.1.name=https-default +server.sockets.1.protocol=https +server.sockets.1.port=443 +---- + + +=== Decisions & Considerations (for EG members, to be removed) +- No prefix attribute added on `@ComposedConfig`, since it arguable doesn't provide much benefits. The injection point has to specify some property name anyway. Discussed on 2018-09-06. +- Starting with supporting to annotate `@ComposedConfig` on types only, i.e. no qualifier and no annotation on the injection point. Discussed on 2018-09-06. +- The programmatic `ConfigAccessor` API doesn't require a specific method (such as `composed()`), since the resolution currently requires the composed type to be annotated with `@ComposedConfig`. diff --git a/spec/src/main/asciidoc/javaconfig-spec.asciidoc b/spec/src/main/asciidoc/javaconfig-spec.asciidoc index 49cb0a2..0075d05 100644 --- a/spec/src/main/asciidoc/javaconfig-spec.asciidoc +++ b/spec/src/main/asciidoc/javaconfig-spec.asciidoc @@ -48,3 +48,4 @@ include::converters.asciidoc[] include::configaccessor.asciidoc[] +include::composed-configuration.asciidoc[]