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

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

commit 1acf5bcb990825141e3f5618879cc90416aa43b2
Author: Claus Ibsen <claus.ib...@gmail.com>
AuthorDate: Tue Aug 25 12:05:51 2020 +0200

    CAMEL-15437: properties-binding: support binding from maps of maps
---
 .../support/PropertyBindingSupportFlattenTest.java | 405 +++++++++++++++++++++
 .../camel/support/PropertyBindingSupportTest.java  |   1 -
 .../camel/support/PropertyBindingSupport.java      | 105 +++++-
 3 files changed, 505 insertions(+), 6 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportFlattenTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportFlattenTest.java
new file mode 100644
index 0000000..6450e13
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportFlattenTest.java
@@ -0,0 +1,405 @@
+/*
+ * 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.camel.support;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.PropertyBindingException;
+import org.apache.camel.spi.PropertiesComponent;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit test for PropertyBindingSupport
+ */
+public class PropertyBindingSupportFlattenTest extends ContextTestSupport {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+
+        Company work = new Company();
+        work.setId(456);
+        work.setName("Acme");
+        context.getRegistry().bind("myWork", work);
+
+        Properties placeholders = new Properties();
+        placeholders.put("companyName", "Acme");
+        placeholders.put("committer", "rider");
+        context.getPropertiesComponent().setInitialProperties(placeholders);
+
+        return context;
+    }
+
+    @Test
+    public void testProperties() throws Exception {
+        Foo foo = new Foo();
+
+        // Map.of requires JDK9 onwards and we are still compatible with Java 8
+        Map<String, Object> work = new HashMap<>();
+        work.put("id", "123");
+        work.put("name", "{{companyName}}");
+
+        Map<String, Object> bar = new HashMap<>();
+        bar.put("age", "33");
+        bar.put("{{committer}}", "true");
+        bar.put("gold-customer", "true");
+        bar.put("work", work);
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("name", "James");
+        map.put("bar", bar);
+
+        PropertyBindingSupport.bindWithFlattenProperties(context, foo, map);
+
+        assertEquals("James", foo.getName());
+        assertEquals(33, foo.getBar().getAge());
+        assertTrue(foo.getBar().isRider());
+        assertTrue(foo.getBar().isGoldCustomer());
+        assertEquals(123, foo.getBar().getWork().getId());
+        assertEquals("Acme", foo.getBar().getWork().getName());
+
+        assertTrue(map.isEmpty(), "Should bind all properties");
+    }
+
+    @Test
+    public void testProperty() throws Exception {
+        PropertiesComponent pc = context.getPropertiesComponent();
+        Properties prop = new Properties();
+        prop.setProperty("customerName", "James");
+        prop.setProperty("customerAge", "33");
+        prop.setProperty("workKey", "customerWork");
+        prop.setProperty("customerWork", "Acme");
+        pc.setInitialProperties(prop);
+
+        Foo foo = new Foo();
+
+        // Map.of requires JDK9 onwards and we are still compatible with Java 8
+        Map<String, Object> work = new HashMap<>();
+        work.put("id", "456");
+        work.put("name", "#property:{{workKey}}");
+
+        Map<String, Object> bar = new HashMap<>();
+        bar.put("age", "#property:customerAge");
+        bar.put("rider", "true");
+        bar.put("gold-customer", "true");
+        bar.put("work", work);
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("name", "#property:customerName");
+        map.put("bar", bar);
+
+        PropertyBindingSupport.bindWithFlattenProperties(context, foo, map);
+
+        assertEquals("James", foo.getName());
+        assertEquals(33, foo.getBar().getAge());
+        assertTrue(foo.getBar().isRider());
+        assertTrue(foo.getBar().isGoldCustomer());
+        assertEquals(456, foo.getBar().getWork().getId());
+        assertEquals("Acme", foo.getBar().getWork().getName());
+
+        try {
+            PropertyBindingSupport.build().bind(context, foo, "name", 
"#property:unknown");
+            fail("Should have thrown exception");
+        } catch (PropertyBindingException e) {
+            assertEquals("name", e.getPropertyName());
+            assertEquals("#property:unknown", e.getValue());
+            assertEquals("Property with key unknown not found by properties 
component", e.getCause().getMessage());
+        }
+    }
+
+    @Test
+    public void testWithFluentBuilder() throws Exception {
+        Foo foo = new Foo();
+
+        // Map.of requires JDK9 onwards and we are still compatible with Java 8
+        Map<String, Object> work = new HashMap<>();
+        work.put("name", "{{companyName}}");
+
+        Map<String, Object> bar = new HashMap<>();
+        bar.put("age", "33");
+        bar.put("rider", "true");
+        bar.put("gold-customer", "true");
+        bar.put("work", work);
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("bar", bar);
+
+        
PropertyBindingSupport.build().withCamelContext(context).withTarget(foo).withProperty("name",
 "James")
+                .withProperty("bar.work.id", "123")
+                .withFlattenProperties(true)
+                // and add the rest
+                .withProperties(prop).bind();
+
+        assertEquals("James", foo.getName());
+        assertEquals(33, foo.getBar().getAge());
+        assertTrue(foo.getBar().isRider());
+        assertTrue(foo.getBar().isGoldCustomer());
+        assertEquals(123, foo.getBar().getWork().getId());
+        assertEquals("Acme", foo.getBar().getWork().getName());
+
+        assertTrue(prop.isEmpty(), "Should bind all properties");
+    }
+
+    @Test
+    public void testPropertiesNoReflection() throws Exception {
+        Foo foo = new Foo();
+
+        // Map.of requires JDK9 onwards and we are still compatible with Java 8
+        Map<String, Object> bar = new HashMap<>();
+        bar.put("AGE", "33");
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("bar", bar);
+        prop.put("name", "James");
+
+        
PropertyBindingSupport.build().withFlattenProperties(true).withReflection(false).bind(context,
 foo, prop);
+
+        assertNull(foo.getName());
+        assertEquals(0, foo.getBar().getAge());
+
+        // should not bind any properties as reflection is off
+        assertEquals(2, prop.size());
+    }
+
+    @Test
+    public void testPropertiesIgnoreCase() throws Exception {
+        Foo foo = new Foo();
+
+        // Map.of requires JDK9 onwards and we are still compatible with Java 8
+        Map<String, Object> work = new HashMap<>();
+        work.put("naME", "{{companyName}}");
+        work.put("ID", "123");
+
+        Map<String, Object> bar = new HashMap<>();
+        bar.put("AGE", "33");
+        bar.put("{{committer}}", "true");
+        bar.put("gOLd-Customer", "true");
+        bar.put("WoRk", work);
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("bar", bar);
+        prop.put("name", "James");
+
+        
PropertyBindingSupport.build().withFlattenProperties(true).withIgnoreCase(true).bind(context,
 foo, prop);
+
+        assertEquals("James", foo.getName());
+        assertEquals(33, foo.getBar().getAge());
+        assertTrue(foo.getBar().isRider());
+        assertTrue(foo.getBar().isGoldCustomer());
+        assertEquals(123, foo.getBar().getWork().getId());
+        assertEquals("Acme", foo.getBar().getWork().getName());
+
+        assertTrue(prop.isEmpty(), "Should bind all properties");
+    }
+
+    @Test
+    public void testPropertiesDash() throws Exception {
+        Foo foo = new Foo();
+
+        // Map.of requires JDK9 onwards and we are still compatible with Java 8
+        Map<String, Object> work = new HashMap<>();
+        work.put("name", "{{companyName}}");
+        work.put("id", "123");
+
+        Map<String, Object> bar = new HashMap<>();
+        bar.put("age", "33");
+        bar.put("{{committer}}", "true");
+        bar.put("gold-customer", "true");
+        bar.put("work", work);
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("bar", bar);
+        prop.put("name", "James");
+
+        
PropertyBindingSupport.build().withFlattenProperties(true).bind(context, foo, 
prop);
+
+        assertEquals("James", foo.getName());
+        assertEquals(33, foo.getBar().getAge());
+        assertTrue(foo.getBar().isRider());
+        assertTrue(foo.getBar().isGoldCustomer());
+        assertEquals(123, foo.getBar().getWork().getId());
+        assertEquals("Acme", foo.getBar().getWork().getName());
+
+        assertTrue(prop.isEmpty(), "Should bind all properties");
+    }
+
+    @Test
+    public void testBindPropertiesWithOptionPrefix() throws Exception {
+        Foo foo = new Foo();
+
+        // Map.of requires JDK9 onwards and we are still compatible with Java 8
+        Map<String, Object> work = new HashMap<>();
+        work.put("name", "{{companyName}}");
+        work.put("id", "123");
+
+        Map<String, Object> bar = new HashMap<>();
+        bar.put("age", "33");
+        bar.put("{{committer}}", "true");
+        bar.put("gold-customer", "true");
+        bar.put("work", work);
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("bar", bar);
+        prop.put("name", "James");
+
+        Map<String, Object> other = new HashMap<>();
+        other.put("something", "test");
+
+        Map<String, Object> root = new HashMap<>();
+        root.put("prefix", prop);
+        root.put("other", other);
+
+        Map<String, Object> my = new HashMap<>();
+        my.put("my", root);
+
+        
PropertyBindingSupport.build().withFlattenProperties(true).withOptionPrefix("my.prefix.").bind(context,
 foo, my);
+
+        assertEquals("James", foo.getName());
+        assertEquals(33, foo.getBar().getAge());
+        assertTrue(foo.getBar().isRider());
+        assertTrue(foo.getBar().isGoldCustomer());
+        assertEquals(123, foo.getBar().getWork().getId());
+        assertEquals("Acme", foo.getBar().getWork().getName());
+        assertEquals("test", other.get("something"));
+        assertEquals(1, other.size());
+    }
+
+    @Test
+    public void testPropertiesOptionalKey() throws Exception {
+        Foo foo = new Foo();
+
+        // Map.of requires JDK9 onwards and we are still compatible with Java 8
+        Map<String, Object> adr = new HashMap<>();
+        adr.put("?addresss", "Some street");
+        adr.put("?zip", "1234");
+
+        Map<String, Object> work = new HashMap<>();
+        work.put("?naME", "{{companyName}}");
+        work.put("?ID", "123");
+        work.put("address", adr);
+
+        Map<String, Object> bar = new HashMap<>();
+        bar.put("AGE", "33");
+        bar.put("{{committer}}", "true");
+        bar.put("gOLd-Customer", "true");
+        bar.put("?silver-Customer", "true");
+        bar.put("WoRk", work);
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("bar", bar);
+        prop.put("name", "James");
+
+        
PropertyBindingSupport.build().withFlattenProperties(true).withIgnoreCase(true).bind(context,
 foo, prop);
+
+        assertEquals("James", foo.getName());
+        assertEquals(33, foo.getBar().getAge());
+        assertTrue(foo.getBar().isRider());
+        assertTrue(foo.getBar().isGoldCustomer());
+        assertEquals(123, foo.getBar().getWork().getId());
+        assertEquals("Acme", foo.getBar().getWork().getName());
+
+        assertFalse(prop.isEmpty(), "Should NOT bind all properties");
+        assertTrue(bar.containsKey("?silver-Customer"));
+        assertTrue(adr.containsKey("?addresss"));
+        assertTrue(adr.containsKey("?zip"));
+    }
+
+    public static class Foo {
+        private String name;
+        private Bar bar = new Bar();
+        private Animal animal;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public Bar getBar() {
+            return bar;
+        }
+
+        public void setBar(Bar bar) {
+            this.bar = bar;
+        }
+
+        public Animal getAnimal() {
+            return animal;
+        }
+
+        public void setAnimal(Animal animal) {
+            this.animal = animal;
+        }
+    }
+
+    public static class Bar {
+        private int age;
+        private boolean rider;
+        private Company work; // has no default value but Camel can automatic
+                             // create one if there is a setter
+        private boolean goldCustomer;
+
+        public int getAge() {
+            return age;
+        }
+
+        public boolean isRider() {
+            return rider;
+        }
+
+        public Company getWork() {
+            return work;
+        }
+
+        public boolean isGoldCustomer() {
+            return goldCustomer;
+        }
+
+        // this has no setter but only builders
+        // and mix the builders with both styles (with as prefix and no prefix
+        // at all)
+
+        public Bar withAge(int age) {
+            this.age = age;
+            return this;
+        }
+
+        public Bar withRider(boolean rider) {
+            this.rider = rider;
+            return this;
+        }
+
+        public Bar work(Company work) {
+            this.work = work;
+            return this;
+        }
+
+        public Bar goldCustomer(boolean goldCustomer) {
+            this.goldCustomer = goldCustomer;
+            return this;
+        }
+    }
+
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportTest.java
index d9371b0..e1e34e5 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportTest.java
@@ -577,7 +577,6 @@ public class PropertyBindingSupportTest extends 
ContextTestSupport {
             
PropertyBindingSupport.build().withIgnoreCase(true).withMandatory(true).bind(context,
 foo, prop);
             fail("Should fail");
         } catch (PropertyBindingException e) {
-            //            assertEquals("bar.unknown", e.getPropertyName());
             assertEquals("unknown", e.getPropertyName());
         }
     }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java
index 246d339..2332f1e 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java
@@ -97,6 +97,7 @@ public final class PropertyBindingSupport {
         private Object target;
         private Map<String, Object> properties;
         private boolean removeParameters = true;
+        private boolean flattenProperties;
         private boolean mandatory;
         private boolean nesting = true;
         private boolean deepNesting = true;
@@ -160,6 +161,14 @@ public final class PropertyBindingSupport {
         }
 
         /**
+         * Whether properties should be flattened (when properties is a map of 
maps).
+         */
+        public Builder withFlattenProperties(boolean flattenProperties) {
+            this.flattenProperties = flattenProperties;
+            return this;
+        }
+
+        /**
          * Whether all parameters should be mandatory and successfully bound
          */
         public Builder withMandatory(boolean mandatory) {
@@ -265,7 +274,7 @@ public final class PropertyBindingSupport {
             }
 
             return doBindProperties(camelContext, target, removeParameters ? 
properties : new HashMap<>(properties),
-                    optionPrefix, ignoreCase, true, mandatory,
+                    optionPrefix, ignoreCase, removeParameters, 
flattenProperties, mandatory,
                     nesting, deepNesting, fluentBuilder, allowPrivateSetter, 
reference, placeholder, reflection, configurer);
         }
 
@@ -283,7 +292,7 @@ public final class PropertyBindingSupport {
             Map<String, Object> prop = properties != null ? properties : 
this.properties;
 
             return doBindProperties(context, obj, removeParameters ? prop : 
new HashMap<>(prop),
-                    optionPrefix, ignoreCase, true, mandatory,
+                    optionPrefix, ignoreCase, removeParameters, 
flattenProperties, mandatory,
                     nesting, deepNesting, fluentBuilder, allowPrivateSetter, 
reference, placeholder, reflection, configurer);
         }
 
@@ -300,7 +309,7 @@ public final class PropertyBindingSupport {
             Map<String, Object> properties = new HashMap<>(1);
             properties.put(key, value);
 
-            return doBindProperties(camelContext, target, properties, 
optionPrefix, ignoreCase, true, mandatory,
+            return doBindProperties(camelContext, target, properties, 
optionPrefix, ignoreCase, true, false, mandatory,
                     nesting, deepNesting, fluentBuilder, allowPrivateSetter, 
reference, placeholder, reflection, configurer);
         }
 
@@ -524,7 +533,7 @@ public final class PropertyBindingSupport {
      *
      * @param  camelContext the camel context
      * @param  target       the target object
-     * @param  properties   the properties where the bound properties will be 
removed from
+     * @param  properties   the properties (as flat key=value paris) where the 
bound properties will be removed
      * @return              true if one or more properties was bound
      * @see                 #build()
      */
@@ -538,6 +547,29 @@ public final class PropertyBindingSupport {
     }
 
     /**
+     * Binds the properties to the target object, and removes the property 
that was bound from properties.
+     * <p/>
+     * This method uses the default settings, and if you need to configure any 
setting then use the fluent builder
+     * {@link #build()} where each option can be customized, such as whether 
parameter should be removed, or whether
+     * options are mandatory etc.
+     *
+     * @param  camelContext the camel context
+     * @param  target       the target object
+     * @param  properties   the properties as (map of maps) where the 
properties will be flattened, and bound properties
+     *                      will be removed
+     * @return              true if one or more properties was bound
+     * @see                 #build()
+     */
+    public static boolean bindWithFlattenProperties(CamelContext camelContext, 
Object target, Map<String, Object> properties) {
+        // mandatory parameters
+        org.apache.camel.util.ObjectHelper.notNull(camelContext, 
"camelContext");
+        org.apache.camel.util.ObjectHelper.notNull(target, "target");
+        org.apache.camel.util.ObjectHelper.notNull(properties, "properties");
+
+        return 
PropertyBindingSupport.build().withFlattenProperties(true).bind(camelContext, 
target, properties);
+    }
+
+    /**
      * Used for making it easier to support using option prefix in property 
binding and to remove the bound properties
      * from the input map.
      */
@@ -579,6 +611,64 @@ public final class PropertyBindingSupport {
     }
 
     /**
+     * Used for flatten properties when they are a map of maps
+     */
+    private static class FlattenMap extends LinkedHashMap<String, Object> {
+
+        private final Map<String, Object> originalMap;
+
+        public FlattenMap(Map<String, Object> map) {
+            this.originalMap = map;
+            flatten("", originalMap);
+        }
+
+        @SuppressWarnings("unchecked")
+        private void flatten(String prefix, Map<?, Object> map) {
+            for (Map.Entry<?, Object> entry : map.entrySet()) {
+                String key = entry.getKey().toString();
+                boolean optional = key.startsWith("?");
+                if (optional) {
+                    key = key.substring(1);
+                }
+                Object value = entry.getValue();
+                String keyPrefix = (optional ? "?" : "") + (prefix.isEmpty() ? 
key : prefix + "." + key);
+                if (value instanceof Map) {
+                    flatten(keyPrefix, (Map<?, Object>) value);
+                } else {
+                    put(keyPrefix, value);
+                }
+            }
+        }
+
+        @Override
+        public Object remove(Object key) {
+            // we only need to care about the remove method,
+            // so we can remove the corresponding key from the original map
+
+            // walk key with dots to remove right node
+            String[] parts = key.toString().split("\\.");
+            Map map = originalMap;
+            for (int i = 0; i < parts.length; i++) {
+                String part = parts[i];
+                Object obj = map.get(part);
+                if (i == parts.length - 1) {
+                    map.remove(part);
+                } else if (obj instanceof Map) {
+                    map = (Map) obj;
+                }
+            }
+
+            // remove empty middle maps
+            Object answer = super.remove(key);
+            if (super.isEmpty()) {
+                originalMap.clear();
+            }
+            return answer;
+        }
+
+    }
+
+    /**
      * Used for sorting the property keys when doing property binding. We need 
to sort the keys in a specific order so
      * we process the binding in a way that allows us to walk down the OGNL 
object graph and build empty nodes on the
      * fly, and as well handle map/list and array types as well.
@@ -622,6 +712,7 @@ public final class PropertyBindingSupport {
      * @param  optionPrefix       the prefix used to filter properties
      * @param  ignoreCase         whether to ignore case for property keys
      * @param  removeParameter    whether to remove bound parameters
+     * @param  flattenProperties  whether properties should be flattened (when 
properties is a map of maps)
      * @param  mandatory          whether all parameters must be bound
      * @param  nesting            whether nesting is in use
      * @param  deepNesting        whether deep nesting is in use, where Camel 
will attempt to walk as deep as possible
@@ -638,7 +729,7 @@ public final class PropertyBindingSupport {
      */
     private static boolean doBindProperties(
             CamelContext camelContext, Object target, Map<String, Object> 
properties,
-            String optionPrefix, boolean ignoreCase, boolean removeParameter, 
boolean mandatory,
+            String optionPrefix, boolean ignoreCase, boolean removeParameter, 
boolean flattenProperties, boolean mandatory,
             boolean nesting, boolean deepNesting, boolean fluentBuilder, 
boolean allowPrivateSetter,
             boolean reference, boolean placeholder,
             boolean reflection, PropertyConfigurer configurer) {
@@ -647,6 +738,10 @@ public final class PropertyBindingSupport {
             return false;
         }
 
+        if (flattenProperties) {
+            properties = new FlattenMap(properties);
+        }
+
         boolean answer = false;
 
         if (optionPrefix != null) {

Reply via email to