diff --git a/api/src/main/java/org/eclipse/microprofile/config/Config.java b/api/src/main/java/org/eclipse/microprofile/config/Config.java index 823846f1..3e6b2e22 100644 --- a/api/src/main/java/org/eclipse/microprofile/config/Config.java +++ b/api/src/main/java/org/eclipse/microprofile/config/Config.java @@ -34,7 +34,9 @@ import java.lang.reflect.Array; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.Converter; @@ -129,7 +131,9 @@ public interface Config { * if the property cannot be converted to the specified type * @throws java.util.NoSuchElementException * if the property is not defined or is defined as an empty string or the converter returns {@code null} + * @deprecated use {@link #get(String)} and {@link #as(Class)} instead */ + @Deprecated T getValue(String propertyName, Class propertyType); /** @@ -148,7 +152,9 @@ public interface Config { * @param propertyName * The configuration property name * @return the resolved property value as a {@link ConfigValue} + * @deprecated use {@link #get(String)} and methods on the config node */ + @Deprecated ConfigValue getConfigValue(String propertyName); /** @@ -171,7 +177,9 @@ public interface Config { * @throws java.util.NoSuchElementException * if the property isn't present in the configuration or is defined as an empty string or the converter * returns {@code null} + * @deprecated use {@link #get(String)} and #asList(Class) */ + @Deprecated default List getValues(String propertyName, Class propertyType) { @SuppressWarnings("unchecked") Class arrayType = (Class) Array.newInstance(propertyType, 0).getClass(); @@ -198,7 +206,9 @@ default List getValues(String propertyName, Class propertyType) { * * @throws IllegalArgumentException * if the property cannot be converted to the specified type + * @deprecated use {@link #get(String)} and {@link #as(Class)} instead */ + @Deprecated Optional getOptionalValue(String propertyName, Class propertyType); /** @@ -219,7 +229,9 @@ default List getValues(String propertyName, Class propertyType) { * * @throws java.lang.IllegalArgumentException * if the property cannot be converted to the specified type + * @deprecated use {@link #get(String)} and #asList(Class) */ + @Deprecated default Optional> getOptionalValues(String propertyName, Class propertyType) { @SuppressWarnings("unchecked") Class arrayType = (Class) Array.newInstance(propertyType, 0).getClass(); @@ -257,7 +269,7 @@ default Optional> getOptionalValues(String propertyName, Class pr * The returned sources will be sorted by descending ordinal value and name, which can be iterated in a thread-safe * manner. The {@link java.lang.Iterable Iterable} contains a fixed number of {@linkplain ConfigSource configuration * sources}, determined at application start time, and the config sources themselves may be static or dynamic. - * + * * @return the configuration sources */ Iterable getConfigSources(); @@ -291,4 +303,181 @@ default Optional> getOptionalValues(String propertyName, Class pr * If the current provider does not support unwrapping to the given type */ T unwrap(Class type); + + /* + * Tree handling methods + */ + + /** + * Fully qualified key of this config node (such as {@code server.port}). + * Returns an empty String for root config. + * + * @return key of this config + */ + String key(); + + /** + * Name of this node - the last element of a fully qualified key. + *

+ * For example for key {@code server.port} this method would return {@code port}. + * + * @return name of this node + */ + String name(); + + /** + * Single sub-node for the specified name. + * For example if requested for key {@code server}, this method would return a config + * representing the {@code server} node, which would have for example a child {@code port}. + * + * @param name name of the nested node to retrieve + * @return sub node, never null + */ + Config get(String name); + + /** + * A detached node removes prefixes of each sub-node of the current node. + *

+ * Let's assume this node is {@code server} and contains {@code host} and {@code port}. + * The method {@link #key()} for {@code host} would return {@code server.host}. + * If we call a method {@link #key()} on a detached instance, it would return just {@code host}. + * + * @return a detached config instance + */ + Config detach(); + + /** + * Type of this node. + * + * @return type + */ + Type type(); + + /** + * Returns {@code true} if the node exists, whether an object, a list, or a + * value node. + * + * @return {@code true} if the node exists + */ + default boolean exists() { + return type() != Type.MISSING; + } + + /** + * Returns {@code true} if this configuration node has a direct value. + *

+ * Example (using properties files) for each node type: + *

+ * {@link Type#OBJECT} - the node {@code server.tls} is an object node with direct value: + *

+     * # this is not recommended, yet it is possible:
+     * server.tls=true
+     * server.tls.version=1.2
+     * server.tls.keystore=abc.p12
+     * 
+ *

+ * {@link Type#LIST} - the node {@code server.ports} is a list node with direct value: + * TODO this may actually not be supported by the spec, as it can only be achieved through properties + *

+     * # this is not recommended, yet it is possible:
+     * server.ports=8080
+     * server.ports.0=8081
+     * server.ports.1=8082
+     * 
+ *

+ * {@link Type#LEAF} - the nodes {@code server.port} and {@code server.host} are values + *

+     * server.port=8080
+     * server.host=localhost
+     * 
+ * + * @return {@code true} if the node has direct value, {@code false} otherwise. + */ + boolean hasValue(); + + /** + * Typed value created using a converter function. + * The converter is called only if this config node exists. + * + * @param converter to create an instance from config node + * @param type of the object + * @return converted value of this node, or an empty optional if this node does not exist + * @throws java.lang.IllegalArgumentException if this config node cannot be converted to the desired type + */ + Optional as(Function converter); + + /** + * Typed value created using a configured converter. + * + * @param type class to convert to + * @param type of the object + * @return converted value of this node, or empty optional if this node does not exist + * @throws java.lang.IllegalArgumentException if this config node cannot be converted to the desired type + */ + Optional as(Class type); + + /** + * Map to a list of typed values. + * This method is only available if the current node is a {@link org.eclipse.microprofile.config.Config.Type#LIST} + * or if it has a direct value. In case a direct value of type String exists, it expects comma separated elements. + * + * @param type class to convert each element to + * @param type of the object + * @return list of typed values + * @throws java.lang.IllegalArgumentException if this config node cannot be converted to the desired type + */ + Optional> asList(Class type); + + /** + * Contains the (known) config values as a map of key->value pairs. + * + * @return map of sub keys of this config node, or empty if this node does not exist + */ + Optional> asMap(); + + /** + * A list of child nodes. + * In case this node is {@link org.eclipse.microprofile.config.Config.Type#LIST} returns the list in the correct order. + * In case this node is {@link org.eclipse.microprofile.config.Config.Type#OBJECT} returns all direct child nodes + * (in an unknown order) + * + * @return list of child nodes, or empty if this node does not exist + */ + Optional> asNodeList(); + + /* + * Shortcut helper methods + */ + default Optional asString() { + return as(String.class); + } + + default Optional asInt() { + return as(Integer.class); + } + + /** + * Config node type. + */ + enum Type { + /** + * Object node with named members and a possible direct value. + */ + OBJECT, + /** + * List node with a list of indexed parameters. + * Note that a list node can also be accessed as an object node - child elements + * have indexed keys starting from {@code 0}. + * List nodes may also have a direct value. + */ + LIST, + /** + * Value node is a leaf node - it does not have any child nodes, only direct value. + */ + LEAF, + /** + * Node is missing, it will return only empty values. + */ + MISSING + } } diff --git a/api/src/main/java/org/eclipse/microprofile/config/spi/ConfigNode.java b/api/src/main/java/org/eclipse/microprofile/config/spi/ConfigNode.java new file mode 100644 index 00000000..4064c375 --- /dev/null +++ b/api/src/main/java/org/eclipse/microprofile/config/spi/ConfigNode.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2017, 2021 Oracle and/or its affiliates. + * + * 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 org.eclipse.microprofile.config.spi; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Marker interface identifying a config node implementation. + */ +public interface ConfigNode { + /** + * Key of this config node. + * + * @return key of this node + */ + String key(); + + /** + * Get the type of this node. + * + * @return NodeType this node represents + */ + NodeType nodeType(); + + /** + * Get the direct value of this config node. Any node type can have a direct value. + * + * @return a value if present, {@code empty} otherwise + */ + Optional value(); + + /** + * Each node may have a direct value, and in addition may be an object node or a list node. + * This method returns true for any node with direct value. + * + * @return true if this node contains a value + */ + default boolean hasValue() { + return value().isPresent(); + } + + /** + * Config source that provided this value. + * + * @return config source + */ + ConfigSource configSource(); + + /** + * The actual priority of the config source that provided this value. + * + * @return config source priority + * @see #configSource() + */ + Integer sourcePriority(); + + /** + * Base types of config nodes. + */ + enum NodeType { + /** + * An object (complex structure), optionally may have a value. + */ + OBJECT, + /** + * A list of values, optionally may have a value. + */ + LIST, + /** + * Only has value. + */ + VALUE + } + + /** + * Single string-based configuration value. + */ + interface ValueNode extends ConfigNode { + @Override + default NodeType nodeType() { + return NodeType.VALUE; + } + + /** + * Get the value of this value node. + * @return string with the node value + */ + String get(); + } + + /** + * ConfigNode-based list of configuration values. + *

+ * List may contains instances of + * {@link ValueNode}, {@link ListNode} as well as {@link ObjectNode}. + */ + interface ListNode extends ConfigNode, List { + @Override + default NodeType nodeType() { + return NodeType.LIST; + } + } + + /** + * Configuration node representing a hierarchical structure. + *

+ * In the map exposed by this interface, the map keys are {@code String}s + * containing the fully-qualified dotted names of the config keys and the + * map values are the corresponding {@link ValueNode} or {@link ListNode} + * instances. The map never contains {@link ObjectNode} values because the + * {@link ObjectNode} is implemented as a flat map. + */ + interface ObjectNode extends ConfigNode, Map { + @Override + default NodeType nodeType() { + return NodeType.OBJECT; + } + } +} diff --git a/api/src/main/java/org/eclipse/microprofile/config/spi/ConfigSource.java b/api/src/main/java/org/eclipse/microprofile/config/spi/ConfigSource.java index 8d8bab8c..ee4747b4 100644 --- a/api/src/main/java/org/eclipse/microprofile/config/spi/ConfigSource.java +++ b/api/src/main/java/org/eclipse/microprofile/config/spi/ConfigSource.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.Set; /** @@ -182,12 +183,14 @@ default int getOrdinal() { /** * Return the value for the specified property in this configuration source. - * + * This method is used when * @param propertyName * the property name * @return the property value, or {@code null} if the property is not present */ - String getValue(String propertyName); + default String getValue(String propertyName) { + return null; + } /** * The name of the configuration source. The name might be used for logging or for analysis of configured values, @@ -198,4 +201,103 @@ default int getOrdinal() { * @return the name of the configuration source */ String getName(); + + /** + * Type of this config source. For backward compatibility returns {@link Type#PROPERTY} by default. + * + * @return source type + */ + default Type getType() { + return Type.PROPERTY; + } + + /** + * Whether the content of this config source can change externally. + * For backward compatibility returns {@code true} by default. + *

+ * This information is required when the type is other than {@link Type#PROPERTY}. + *

+ * Examples: + *

    + *
  • Environment variables are not mutable
  • + *
  • System properties may be considered mutable
  • + *
  • Classpath resource from a jar file is not mutable
  • + *
  • File based config source may be considered mutable
  • + *
+ * + * @return whether this is a mutable config source + */ + default boolean isMutable() { + return true; + } + + /** + * If this source {@link #getType()} is {@link Type#TREE}, this method is used to load the + * tree of the data. + * + * @return node content or empty optional if the underlying config source does not exist + */ + default Optional load() { + return Optional.empty(); + } + + /** + * If this source {@link #getType()} is {@link Type#LAZY}, this method is used to load each + * node. + * + * @return config node or empty optional if the node does not exist, or this is not a lazy + * config source + */ + default Optional load(String key) { + return Optional.empty(); + } + + /** + * If this config source is {@link #isMutable()} and its shape changes, it should notify + * the configuration implementation of such a change. + * Once the change listener is registered by configuration implementation, this source can + * start checking for changes. If any change occurs, the source should invoke the runnable. + * + * @param changeRunnable runnable to call when this config source changes + */ + default void changeListener(Runnable changeRunnable) { + } + + /** + * Type of the config source. + */ + enum Type { + /** + * This is for backward compatibility - request each property separately. + * Config uses {@link ConfigSource#getValue(String)} to get data of this source. + */ + PROPERTY, + /** + * This config source cannot provide the full structure, each node must be requested + * separately. + * Difference to {@link #PROPERTY} is that each node may actually be an object/list node. + *

+ * Examples: + *

    + *
  • Environment variables config source - each node is computed from key
  • + *
  • Distributed configurations, where we cannot list all nodes
  • + * Database config source based on tables with a lot of records + *
+ * + * Config uses {@link ConfigSource#load(String)} to get data of this source. + */ + LAZY, + /** + * This config source returns a tree structure and can read the whole tree at once. + *

+ * Examples: + *

    + *
  • System properties variables config source
  • + *
  • File/Classpath config sources
  • + *
+ * + * Config uses {@link ConfigSource#load()} to get data of this source. + */ + TREE + } } diff --git a/api/src/main/java/org/eclipse/microprofile/config/spi/Converter.java b/api/src/main/java/org/eclipse/microprofile/config/spi/Converter.java index 938ad628..361ef7c6 100644 --- a/api/src/main/java/org/eclipse/microprofile/config/spi/Converter.java +++ b/api/src/main/java/org/eclipse/microprofile/config/spi/Converter.java @@ -28,6 +28,8 @@ import java.io.Serializable; +import org.eclipse.microprofile.config.Config; + /** * A mechanism for converting configured values from {@link String} to any Java type. * @@ -150,4 +152,18 @@ public interface Converter extends Serializable { * if the given value was {@code null} */ T convert(String value) throws IllegalArgumentException, NullPointerException; + + /** + * Convert the given config node (may be an object, list or value) to the specified type. + * + * @param config config node to convert + * @return the converted value + * @throws IllegalArgumentException + * if the value cannot be converted to the specified type + * @throws NullPointerException + * if the given value was {@code null} + */ + default T convert(Config config) throws IllegalArgumentException, NullPointerException { + throw new IllegalArgumentException("Default implementation of converter"); + } }