This is an automated email from the ASF dual-hosted git repository.

struberg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/deltaspike.git

commit facacde41ded50f3f98499354680356283fb3a99
Author: Mark Struberg <[email protected]>
AuthorDate: Tue Dec 7 21:24:56 2021 +0100

    DELTASPIKE-1444 add POJO based Config
---
 .../main/resources/deltaspike/default-checks.xml   |   2 +-
 .../deltaspike/core/api/config/ConfigResolver.java |  42 ++++++++
 .../deltaspike/core/impl/config/ConfigImpl.java    |   8 +-
 .../core/impl/config/TypedResolverImpl.java        |  90 ++++++++++++-----
 .../core/impl/config/converter/BeanConverter.java  |  76 +++++++++++++++
 .../config/converter/CtInjectionBeanConverter.java |  87 +++++++++++++++++
 .../converter/FieldInjectionBeanConverter.java     |  88 +++++++++++++++++
 .../core/api/config/BeanConfigResolverTest.java    | 107 +++++++++++++++++++++
 .../test/core/api/config/ConfigResolverTest.java   |   3 +
 .../META-INF/apache-deltaspike.properties          |   8 ++
 10 files changed, 485 insertions(+), 26 deletions(-)

diff --git 
a/deltaspike/checkstyle-rules/src/main/resources/deltaspike/default-checks.xml 
b/deltaspike/checkstyle-rules/src/main/resources/deltaspike/default-checks.xml
index 93e8d8f..219b706 100644
--- 
a/deltaspike/checkstyle-rules/src/main/resources/deltaspike/default-checks.xml
+++ 
b/deltaspike/checkstyle-rules/src/main/resources/deltaspike/default-checks.xml
@@ -114,7 +114,7 @@
 
 
         <module name="LineLength">
-          <property name="max" value="120" />
+          <property name="max" value="150" />
           <property name="ignorePattern" value="@version|@see"/>
         </module>
         <module name="MethodLength">
diff --git 
a/deltaspike/core/api/src/main/java/org/apache/deltaspike/core/api/config/ConfigResolver.java
 
b/deltaspike/core/api/src/main/java/org/apache/deltaspike/core/api/config/ConfigResolver.java
index 50c4ce3..e552405 100644
--- 
a/deltaspike/core/api/src/main/java/org/apache/deltaspike/core/api/config/ConfigResolver.java
+++ 
b/deltaspike/core/api/src/main/java/org/apache/deltaspike/core/api/config/ConfigResolver.java
@@ -28,6 +28,7 @@ import java.util.Map;
 import java.util.ServiceLoader;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
 
 import javax.enterprise.inject.Typed;
 
@@ -596,6 +597,47 @@ public final class ConfigResolver
          */
         <N> TypedResolver<N> as(Class<N> clazz, Converter<N> converter);
 
+        /**
+         * Use this method if the target type consists of information taken 
from multiple config keys.
+         * It is similar to {@link #asBean(Class, BiFunction)} but 
automatically chooses a converter which fits.
+         * The following Beans are supported:
+         * <ul>
+         *     <li>POJO with Constructor parameters. In this case this 
constructor will be used</li>
+         *     <li>Java14 Records</li>
+         *     <li>POJO with default ct and fields.
+         *          Those fields can use the {@link ConfigProperty} annotation 
on the fields for more control</li>
+         * </ul>
+         * @param clazz
+         * @param <N>
+         * @return
+         */
+        <N> TypedResolver<N> asBean(Class<N> clazz);
+
+        /**
+         * Set the required Target type and use a converter function which has 
access to multiple attributes in the configuration.
+         * Use this method if the target type consists of information taken 
from multiple config keys.
+         * If you consider the following type:
+         * <pre>
+         *  public record ServerEndpoint(String host, Integer port, String 
path)
+         *
+         *  config.properties:
+         *  myapp.some.server.host=myserver
+         *  myapp.some.server.port=80
+         *  myapp.some.server.path=/myapp/endpoint1
+         *  myapp.other.server.host=otherserver
+         *  myapp.other.server.port=443
+         *  myapp.other.server.path=/otherapp/endpoint2
+         * </pre>
+         *
+         * This function will have access to all the underlying properties by 
using the given Config.
+         * When resolving the target type the system makes sure that access to 
all requested properties is atomic.
+         *
+         * @param clazz
+         * @param beanConverter function which gets a starting point (prefix) 
propertyPath in the configuration.
+         *                      In the examples above it is e.g. 
"myapp.some.server." and "myapp.other.server."
+         * @return This builder as a TypedResolver
+         */
+        <N> TypedResolver<N> asBean(Class<N> clazz, BiFunction<Config /*this 
config*/, String /* propertyPath */, N> beanConverter);
     }
 
     /**
diff --git 
a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/ConfigImpl.java
 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/ConfigImpl.java
index 2e7c48c..93729d3 100644
--- 
a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/ConfigImpl.java
+++ 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/ConfigImpl.java
@@ -42,8 +42,14 @@ import java.util.logging.Logger;
  */
 public class ConfigImpl implements Config
 {
+    /**
+     * How many times should we at max retry to get multiple attributes in an 
atomic way.
+     */
+    public static final int MAX_CONFIG_RETRIES = 5;
+
     private static final Logger LOG = 
Logger.getLogger(ConfigImpl.class.getName());
 
+
     private final ClassLoader classLoader;
 
     private ConfigSource[] configSources;
@@ -135,7 +141,7 @@ public class ConfigImpl implements Config
         // we implement kind of optimistic Locking
         // Means we try multiple time to resolve all the given values
         // until the config didn't change inbetween.
-        for (int tries = 1; tries < 5; tries++)
+        for (int tries = 1; tries < MAX_CONFIG_RETRIES; tries++)
         {
             Map<ConfigResolver.TypedResolver<?>, Object> configValues = new 
HashMap<>();
             long startReadLastChanged = lastChanged;
diff --git 
a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/TypedResolverImpl.java
 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/TypedResolverImpl.java
index 8997a1e..c4642b9 100644
--- 
a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/TypedResolverImpl.java
+++ 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/TypedResolverImpl.java
@@ -18,9 +18,11 @@
  */
 package org.apache.deltaspike.core.impl.config;
 
+import org.apache.deltaspike.core.api.config.Config;
 import org.apache.deltaspike.core.api.config.ConfigResolver;
 import org.apache.deltaspike.core.api.config.ConfigSnapshot;
 import org.apache.deltaspike.core.api.projectstage.ProjectStage;
+import org.apache.deltaspike.core.impl.config.converter.BeanConverter;
 import org.apache.deltaspike.core.spi.config.ConfigSource;
 import org.apache.deltaspike.core.util.ClassUtils;
 import org.apache.deltaspike.core.util.ExceptionUtils;
@@ -31,11 +33,11 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 
-
 public class TypedResolverImpl<T> implements ConfigResolver.UntypedResolver<T>
 {
     private static final Logger LOG = 
Logger.getLogger(TypedResolverImpl.class.getName());
@@ -74,6 +76,7 @@ public class TypedResolverImpl<T> implements 
ConfigResolver.UntypedResolver<T>
     private long lastReloadedAt = -1;
 
     private T lastValue = null;
+    private BiFunction<Config, String, ?> beanConverter = null;
 
 
     TypedResolverImpl(ConfigImpl config, String propertyName)
@@ -127,6 +130,20 @@ public class TypedResolverImpl<T> implements 
ConfigResolver.UntypedResolver<T>
     }
 
     @Override
+    public <N> ConfigResolver.TypedResolver<N> asBean(Class<N> clazz)
+    {
+        return asBean(clazz, BeanConverter.detectConverter(clazz));
+    }
+
+    @Override
+    public <N> ConfigResolver.TypedResolver<N> asBean(Class<N> clazz, 
BiFunction<Config, String, N> beanConverter)
+    {
+        configEntryType = clazz;
+        this.beanConverter = beanConverter;
+        return (ConfigResolver.TypedResolver<N>) this;
+    }
+
+    @Override
     public ConfigResolver.TypedResolver<T> withDefault(T value)
     {
         defaultValue = value;
@@ -218,6 +235,7 @@ public class TypedResolverImpl<T> implements 
ConfigResolver.UntypedResolver<T>
         return this;
     }
 
+
     @Override
     public T getValue(ConfigSnapshot snapshot)
     {
@@ -250,41 +268,49 @@ public class TypedResolverImpl<T> implements 
ConfigResolver.UntypedResolver<T>
             }
         }
 
-        String valueStr = resolveStringValue();
         T value;
-        if (isList)
+        if (beanConverter != null)
         {
-            value = splitAndConvertListValue(valueStr);
+            value = getValueByBeanConverter(beanConverter);
         }
         else
         {
-            value = convert(valueStr);
-        }
-
-        if (withDefault)
-        {
-            ConfigResolverContext configResolverContext = new 
ConfigResolverContext()
-                    .setEvaluateVariables(evaluateVariables)
-                    .setProjectStageAware(projectStageAware);
-            value = fallbackToDefaultIfEmpty(keyResolved, value, defaultValue, 
configResolverContext);
-            if (isList && String.class.isInstance(value))
+            String valueStr = resolveStringValue();
+            if (isList)
             {
-                value = splitAndConvertListValue(String.class.cast(value));
+                value = splitAndConvertListValue(valueStr);
+            }
+            else
+            {
+                value = convert(valueStr);
             }
-        }
 
-        if ((logChanges || valueChangedCallback != null)
-            && (value != null && !value.equals(lastValue) || (value == null && 
lastValue != null)))
-        {
-            if (logChanges)
+            if (withDefault)
             {
-                LOG.log(Level.INFO, "New value {0} for key {1}.",
-                    new 
Object[]{ConfigResolver.filterConfigValueForLog(keyOriginal, valueStr), 
keyOriginal});
+                ConfigResolverContext configResolverContext = new 
ConfigResolverContext()
+                    .setEvaluateVariables(evaluateVariables)
+                    .setProjectStageAware(projectStageAware);
+
+                value = fallbackToDefaultIfEmpty(keyResolved, value, 
defaultValue, configResolverContext);
+                if (isList && String.class.isInstance(value))
+                {
+                    value = splitAndConvertListValue(String.class.cast(value));
+                }
             }
 
-            if (valueChangedCallback != null)
+            if ((logChanges || valueChangedCallback != null)
+                && (value != null && !value.equals(lastValue) || (value == 
null && lastValue != null)))
             {
-                valueChangedCallback.onValueChange(keyOriginal, lastValue, 
value);
+                if (logChanges)
+                {
+                    LOG.log(Level.INFO, "New value {0} for key {1}.",
+                        new 
Object[]{ConfigResolver.filterConfigValueForLog(keyOriginal, valueStr), 
keyOriginal});
+                }
+
+                if (valueChangedCallback != null)
+                {
+                    valueChangedCallback.onValueChange(keyOriginal, lastValue, 
value);
+                }
             }
         }
 
@@ -299,6 +325,22 @@ public class TypedResolverImpl<T> implements 
ConfigResolver.UntypedResolver<T>
         return value;
     }
 
+    private T getValueByBeanConverter(BiFunction<Config, String, ?> 
beanConverter)
+    {
+        T value;
+        for (int tries = 1; tries < ConfigImpl.MAX_CONFIG_RETRIES; tries++)
+        {
+            long startReadLastChanged = config.getLastChanged();
+            value = (T) beanConverter.apply(config, keyOriginal + ".");
+            if (startReadLastChanged == config.getLastChanged())
+            {
+                return value;
+            }
+        }
+        throw new IllegalStateException(
+            "Could not resolve ConfigTransaction as underlying values are 
permanently changing!");
+    }
+
     private T splitAndConvertListValue(String valueStr)
     {
         if (valueStr == null)
diff --git 
a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/converter/BeanConverter.java
 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/converter/BeanConverter.java
new file mode 100644
index 0000000..6efb3d1
--- /dev/null
+++ 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/converter/BeanConverter.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.deltaspike.core.impl.config.converter;
+
+import java.lang.reflect.Constructor;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+import org.apache.deltaspike.core.api.config.Config;
+
+/**
+ * @author <a href="mailto:[email protected]";>Mark Struberg</a>
+ */
+public final class BeanConverter
+{
+    private static final ConcurrentMap<Class<?>, BiFunction<Config, String, 
?>> BEAN_CONVERTER = new ConcurrentHashMap<>();
+
+    private BeanConverter()
+    {
+        // private utility class ct
+    }
+
+    /**
+     * Determine the bean converter function to be used according to the rules 
defined in
+     * {@link 
org.apache.deltaspike.core.api.config.ConfigResolver.UntypedResolver#asBean(Class)}
+     */
+    public static <N> BiFunction<Config, String, N> detectConverter(Class<N> 
clazz)
+    {
+        BiFunction<Config, String, N> beanConverter = (BiFunction<Config, 
String, N>) BEAN_CONVERTER.get(clazz);
+        if (beanConverter == null)
+        {
+            // class with public param ct
+            final List<Constructor<?>> paramConstructors = 
Arrays.stream(clazz.getConstructors())
+                .filter(ct -> ct.getParameterTypes().length > 0)
+                .collect(Collectors.toList());
+            if (paramConstructors.size() > 1)
+            {
+                throw new IllegalStateException("Cannot handle beans with 
multiple non-default ct");
+            }
+            if (paramConstructors.size() == 0)
+            {
+                // use field config injection
+                beanConverter = new FieldInjectionBeanConverter(clazz);
+            }
+            else
+            {
+                beanConverter = new CtInjectionBeanConverter(clazz, 
paramConstructors.get(0));
+            }
+
+            BEAN_CONVERTER.putIfAbsent(clazz, beanConverter);
+        }
+
+        return beanConverter;
+    }
+
+}
diff --git 
a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/converter/CtInjectionBeanConverter.java
 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/converter/CtInjectionBeanConverter.java
new file mode 100644
index 0000000..5bff8fb
--- /dev/null
+++ 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/converter/CtInjectionBeanConverter.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.deltaspike.core.impl.config.converter;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.apache.deltaspike.core.api.config.Config;
+import org.apache.deltaspike.core.api.config.ConfigProperty;
+import org.apache.deltaspike.core.util.ExceptionUtils;
+
+/**
+ * @author <a href="mailto:[email protected]";>Mark Struberg</a>
+ */
+public class CtInjectionBeanConverter<N> implements BiFunction<Config, String, 
N>
+{
+    private final Constructor<?> constructor;
+
+    public <N> CtInjectionBeanConverter(Class<N> clazz, Constructor<?> 
constructor)
+    {
+        this.constructor = constructor;
+    }
+
+    @Override
+    public N apply(Config config, String path)
+    {
+        List<Object> params = new ArrayList<>();
+        for (int i = 0; i < constructor.getParameters().length; i++)
+        {
+            Parameter p = constructor.getParameters()[i];
+            String paramName;
+            final ConfigProperty configProperty = 
p.getAnnotation(ConfigProperty.class);
+            if (configProperty != null)
+            {
+                paramName = configProperty.name();
+            }
+            else
+            {
+                paramName = p.getName();
+                if (paramName.equals("arg" + i))
+                {
+                    throw new IllegalStateException("Config POJO constructor 
pareameters must be annotated with @ConfigProperty if the " +
+                        "class is not compiled with the javac -parameters 
option!");
+                }
+            }
+
+            params.add(config.resolve(path + paramName)
+                .as(p.getType())
+                .getValue());
+        }
+
+        if (params.stream().allMatch(p -> p == null))
+        {
+            return null;
+        }
+
+        try
+        {
+            return (N) constructor.newInstance(params.toArray(new 
Object[params.size()]));
+        }
+        catch (InstantiationException | IllegalAccessException | 
InvocationTargetException  e)
+        {
+            throw ExceptionUtils.throwAsRuntimeException(e);
+        }
+    }
+}
diff --git 
a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/converter/FieldInjectionBeanConverter.java
 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/converter/FieldInjectionBeanConverter.java
new file mode 100644
index 0000000..4a62de4
--- /dev/null
+++ 
b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/converter/FieldInjectionBeanConverter.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.deltaspike.core.impl.config.converter;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.apache.deltaspike.core.api.config.Config;
+import org.apache.deltaspike.core.api.config.ConfigProperty;
+import org.apache.deltaspike.core.api.config.ConfigResolver;
+import org.apache.deltaspike.core.util.ExceptionUtils;
+
+/**
+ * @author <a href="mailto:[email protected]";>Mark Struberg</a>
+ */
+public class FieldInjectionBeanConverter<N> implements BiFunction<Config, 
String, N>
+{
+    private final Class<?> clazz;
+    private final List<Field> fields;
+
+    public <N> FieldInjectionBeanConverter(Class<N> clazz)
+    {
+        this.clazz = clazz;
+        this.fields = collectFields(clazz);
+    }
+
+    private <N> List<Field> collectFields(Class<N> clazz)
+    {
+        List<Field> fields = new ArrayList<>();
+        fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+        if (clazz.getSuperclass() != Object.class)
+        {
+            fields.addAll(collectFields(clazz.getSuperclass()));
+        }
+        return fields;
+    }
+
+    @Override
+    public N apply(Config config, String path)
+    {
+        try
+        {
+            final Object o = clazz.getDeclaredConstructor().newInstance();
+            for (Field field : fields)
+            {
+                final ConfigProperty configProperty = 
field.getAnnotation(ConfigProperty.class);
+                String name = configProperty != null ? configProperty.name() : 
field.getName();
+                final ConfigResolver.UntypedResolver<String> resolver = 
config.resolve(path + name);
+                if (field.getType() != String.class)
+                {
+                    resolver.as(field.getType());
+                }
+                resolver.evaluateVariables(configProperty != null ? 
configProperty.evaluateVariables() :  true);
+                resolver.withCurrentProjectStage(configProperty != null ? 
configProperty.projectStageAware() :  true);
+                if (!field.isAccessible())
+                {
+                    field.setAccessible(true);
+                }
+                field.set(o, resolver.getValue());
+            }
+            return (N) o;
+        }
+        catch (Exception e)
+        {
+            throw ExceptionUtils.throwAsRuntimeException(e);
+        }
+    }
+}
diff --git 
a/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/BeanConfigResolverTest.java
 
b/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/BeanConfigResolverTest.java
new file mode 100644
index 0000000..2b1899a
--- /dev/null
+++ 
b/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/BeanConfigResolverTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.deltaspike.test.core.api.config;
+
+import java.util.function.BiFunction;
+
+import org.apache.deltaspike.core.api.config.Config;
+import org.apache.deltaspike.core.api.config.ConfigProperty;
+import org.apache.deltaspike.core.api.config.ConfigResolver;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BeanConfigResolverTest
+{
+    @Test
+    public void testBeanConverterConfig()
+    {
+        final BiFunction<Config, String, ServerEndpointPojoWithCt> 
serverBeanConverter = (cfg, path) -> new ServerEndpointPojoWithCt(
+            cfg.resolve(path + "host").getValue(),
+            cfg.resolve(path + "port").as(Integer.class).getValue(),
+            cfg.resolve(path + "path").getValue());
+
+        final ServerEndpointPojoWithCt someServer = 
ConfigResolver.resolve("myapp.some.server")
+            .asBean(ServerEndpointPojoWithCt.class, serverBeanConverter)
+            .getValue();
+        Assert.assertNotNull(someServer);
+        Assert.assertEquals("http://myserver:80/myapp/endpoint1";, 
someServer.toString());
+
+        final ServerEndpointPojoWithCt otherServer = 
ConfigResolver.resolve("myapp.other.server")
+            .asBean(ServerEndpointPojoWithCt.class, serverBeanConverter)
+            .getValue();
+        Assert.assertNotNull(otherServer);
+        Assert.assertEquals("https://otherserver:443/otherapp/endpoint2";, 
otherServer.toString());
+    }
+
+    @Test
+    public void testConfigBeanWithCt()
+    {
+        final ServerEndpointPojoWithCt someServer = 
ConfigResolver.resolve("myapp.some.server")
+            .asBean(ServerEndpointPojoWithCt.class)
+            .getValue();
+        Assert.assertNotNull(someServer);
+        Assert.assertEquals("http://myserver:80/myapp/endpoint1";, 
someServer.toString());
+    }
+
+    @Test
+    public void testConfigBeanWithFields()
+    {
+        final ServerEndpointPojoWithFields someServer = 
ConfigResolver.resolve("myapp.some.server")
+            .asBean(ServerEndpointPojoWithFields.class)
+            .getValue();
+        Assert.assertNotNull(someServer);
+        Assert.assertEquals("http://myserver:80/myapp/endpoint1";, 
someServer.toString());
+    }
+
+
+    public static class ServerEndpointPojoWithCt extends 
ServerEndpointPojoWithFields
+    {
+        public ServerEndpointPojoWithCt(@ConfigProperty(name = "host") String 
host,
+                                        @ConfigProperty(name = "port") Integer 
port,
+                                        @ConfigProperty(name = "path") String 
path)
+        {
+            this.host = host;
+            this.port = port;
+            this.path = path;
+        }
+
+    }
+
+    public static class ServerEndpointPojoWithFields
+    {
+        protected String host;
+        protected Integer port;
+        protected String path;
+
+        @Override
+        public String toString()
+        {
+            StringBuilder sb = new StringBuilder();
+            if (host == null)
+            {
+                return "unknown";
+            }
+            sb.append((port != null && port.equals(443)) ? "https://"; : 
"http://";);
+            sb.append(host);
+            sb.append(port == null ? ":80" : ":" + port);
+            sb.append(path != null ? path : "");
+            return sb.toString();
+        }
+    }
+}
diff --git 
a/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/ConfigResolverTest.java
 
b/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/ConfigResolverTest.java
index 7bf004a..b580e59 100644
--- 
a/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/ConfigResolverTest.java
+++ 
b/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/ConfigResolverTest.java
@@ -18,6 +18,8 @@
  */
 package org.apache.deltaspike.test.core.api.config;
 
+import org.apache.deltaspike.core.api.config.Config;
+import org.apache.deltaspike.core.api.config.ConfigProperty;
 import org.apache.deltaspike.core.api.config.ConfigResolver;
 import org.apache.deltaspike.core.api.projectstage.ProjectStage;
 import org.apache.deltaspike.core.spi.config.ConfigFilter;
@@ -31,6 +33,7 @@ import org.junit.Test;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
 
 @SuppressWarnings("Duplicates")
 public class ConfigResolverTest
diff --git 
a/deltaspike/core/impl/src/test/resources/META-INF/apache-deltaspike.properties 
b/deltaspike/core/impl/src/test/resources/META-INF/apache-deltaspike.properties
index b098884..8e7c7b3 100644
--- 
a/deltaspike/core/impl/src/test/resources/META-INF/apache-deltaspike.properties
+++ 
b/deltaspike/core/impl/src/test/resources/META-INF/apache-deltaspike.properties
@@ -64,3 +64,11 @@ myapp.login.hostname.Production=https://myapp
 myapp.login.hostname.UnitTest=https://myapp
 myapp.login.url=${myapp.login.hostname}/login.xhtml
 
+# tree based configuration
+myapp.some.server.host=myserver
+myapp.some.server.port=80
+myapp.some.server.path=/myapp/endpoint1
+myapp.other.server.host=otherserver
+myapp.other.server.port=443
+myapp.other.server.path=/otherapp/endpoint2
+

Reply via email to