Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree node support. #711

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 190 additions & 1 deletion api/src/main/java/org/eclipse/microprofile/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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> T getValue(String propertyName, Class<T> propertyType);

/**
Expand All @@ -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);

/**
Expand All @@ -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 <T> List<T> getValues(String propertyName, Class<T> propertyType) {
@SuppressWarnings("unchecked")
Class<T[]> arrayType = (Class<T[]>) Array.newInstance(propertyType, 0).getClass();
Expand All @@ -198,7 +206,9 @@ default <T> List<T> getValues(String propertyName, Class<T> propertyType) {
*
* @throws IllegalArgumentException
* if the property cannot be converted to the specified type
* @deprecated use {@link #get(String)} and {@link #as(Class)} instead
*/
@Deprecated
<T> Optional<T> getOptionalValue(String propertyName, Class<T> propertyType);

/**
Expand All @@ -219,7 +229,9 @@ default <T> List<T> getValues(String propertyName, Class<T> 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 <T> Optional<List<T>> getOptionalValues(String propertyName, Class<T> propertyType) {
@SuppressWarnings("unchecked")
Class<T[]> arrayType = (Class<T[]>) Array.newInstance(propertyType, 0).getClass();
Expand Down Expand Up @@ -257,7 +269,7 @@ default <T> Optional<List<T>> getOptionalValues(String propertyName, Class<T> 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<ConfigSource> getConfigSources();
Expand Down Expand Up @@ -291,4 +303,181 @@ default <T> Optional<List<T>> getOptionalValues(String propertyName, Class<T> pr
* If the current provider does not support unwrapping to the given type
*/
<T> T unwrap(Class<T> 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.
* <p>
* 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.
* <p>
* 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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about the use case here. When do you need to do this? Is this purly about shorter the key string?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to create a config that "cuts" the prefix from all of its nodes. Use case is when you need to configure a component that expects a map (such as JSON-B, CDI).
If you would not have such a method, you would need to create a map, and then rebuild the map by doing a substring on each key


/**
* 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.
* <p>
* Example (using properties files) for each node type:
* <p>
* {@link Type#OBJECT} - the node {@code server.tls} is an object node with direct value:
* <pre>
* # this is not recommended, yet it is possible:
* server.tls=true
* server.tls.version=1.2
* server.tls.keystore=abc.p12
* </pre>
* <p>
* {@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
* <pre>
* # this is not recommended, yet it is possible:
* server.ports=8080
* server.ports.0=8081
* server.ports.1=8082
* </pre>
* <p>
* {@link Type#LEAF} - the nodes {@code server.port} and {@code server.host} are values
* <pre>
* server.port=8080
* server.host=localhost
* </pre>
*
* @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 <T> 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
*/
<T> Optional<T> as(Function<Config, T> converter);

/**
* Typed value created using a configured converter.
*
* @param type class to convert to
* @param <T> 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
*/
<T> Optional<T> as(Class<T> 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 <T> type of the object
* @return list of typed values
* @throws java.lang.IllegalArgumentException if this config node cannot be converted to the desired type
*/
<T> Optional<List<T>> asList(Class<T> 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<Map<String, String>> 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<List<Config>> asNodeList();

/*
* Shortcut helper methods
*/
default Optional<String> asString() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only callable if the hasValue() returns true. I am wondering whether we layer this further and move some of the methods to LeafNode.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is practically not true - there can be a converter from config node to string that could work even for list nodes (e.g. converting a list to a comma separated string)

return as(String.class);
}

default Optional<Integer> asInt() {
return as(Integer.class);
}

/**
* Config node type.
*/
enum Type {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have NodeType under ConfigNode and this Type again for config node type. Can we list one out and refer to it in both Config and ConfigNode?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because these are different scopes and different meanings.
One is intended for SPI (ConfigNode.Type) that supports just three types.
The other is for API (Config.Type) that supports also MISSING.
User's do not see ConfigNode unless they implement ConfigSource; SPI does not use Config.
So separation of concerns.

/**
* Object node with named members and a possible direct value.
*/
OBJECT,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the Type structure, but not the enum value names. I think it is pretty much: Parent, List, Leaf, Empty

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parent is not a very good name, as it can have a value (it is basically a tree node that may contain child nodes and a value).
Leaf is OK. Empty vs. Missing - I think missing is more spot on (as this config node is missing from configuration). Empty may mean something different (e.g. the node exists, but is empty).
The values should still be all upper case (best practice for naming enum values)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed value to LEAF

/**
* 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
}
}
135 changes: 135 additions & 0 deletions api/src/main/java/org/eclipse/microprofile/config/spi/ConfigNode.java
Original file line number Diff line number Diff line change
@@ -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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't "marker interface" usually refer to interfaces without methods?

*/
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();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some time, in the far, far future, the spec will require a minimum of Java 17. Should we prepare for this day by declaring the type enum to be a temporary workaround until this interface can be sealed in a future version?


/**
* 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<String> 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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find this confusing. LIST and VALUE are all OBJECT. How about STRUCTURE or COMPLEX to differentiate them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is paritally taken from JSON, where JsonObject is the specific type (something that contains other nodes) and JsonStructure is the superset.
I am using List instead of Array, as I don't like arrays much in general. But otherwise this copies JSON-P naming

JsonObject -> OBJECT
JsonArray -> LIST (because collections are better than arrays ;) )
JsonValue -> VALUE

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't make to the correlation with Json before you mentioned. I think if we prefix with Config, it might be less confusing, but refering to ConfigNode.OBJECT as ConfigNode.ConfigOBJECT is mouthful. It is not ideal, but I have not got a better name as yet:(. Maybe someone else has.

/**
* 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.
* <p>
* List may contains instances of
* {@link ValueNode}, {@link ListNode} as well as {@link ObjectNode}.
*/
interface ListNode extends ConfigNode, List<ConfigNode> {
@Override
default NodeType nodeType() {
return NodeType.LIST;
}
}

/**
* Configuration node representing a hierarchical structure.
* <p>
* 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<String, ConfigNode> {
@Override
default NodeType nodeType() {
return NodeType.OBJECT;
}
}
}
Loading