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) {