You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2020/03/16 09:35:07 UTC

[camel] branch master updated: CAMEL-14717: camel-core - Binding properties add support for optional keys

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


The following commit(s) were added to refs/heads/master by this push:
     new 611b30c  CAMEL-14717: camel-core - Binding properties add support for optional keys
611b30c is described below

commit 611b30c683926e9d69b32993c77815df802b95ac
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Mon Mar 16 10:25:26 2020 +0100

    CAMEL-14717: camel-core - Binding properties add support for optional keys
---
 .../PropertyBindingSupportConfigurerTest.java      | 242 +++++++++++++++++++++
 .../camel/support/PropertyBindingSupportTest.java  |  76 +++++++
 .../apache/camel/support/IntrospectionSupport.java |   2 +-
 .../camel/support/PropertyBindingSupport.java      |  45 +++-
 4 files changed, 359 insertions(+), 6 deletions(-)

diff --git a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportConfigurerTest.java b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportConfigurerTest.java
new file mode 100644
index 0000000..8edf23b
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportConfigurerTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.Locale;
+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.GeneratedPropertyConfigurer;
+import org.junit.Test;
+
+/**
+ * Unit test for PropertyBindingSupport
+ */
+public class PropertyBindingSupportConfigurerTest extends ContextTestSupport {
+
+    private final MyConfigurer myConfigurer = new MyConfigurer();
+
+    @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 {
+        Bar bar = new Bar();
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("age", "33");
+        prop.put("{{committer}}", "true");
+        prop.put("gold-customer", "true");
+        prop.put("work.id", "123");
+        prop.put("work.name", "{{companyName}}");
+
+        myConfigurer.reset();
+        PropertyBindingSupport.build().withConfigurer(myConfigurer).withIgnoreCase(true).bind(context, bar, prop);
+        assertEquals(3, myConfigurer.getCounter());
+
+        assertEquals(33, bar.getAge());
+        assertTrue(bar.isRider());
+        assertTrue(bar.isGoldCustomer());
+        assertEquals(123, bar.getWork().getId());
+        assertEquals("Acme", bar.getWork().getName());
+
+        assertTrue("Should bind all properties", prop.isEmpty());
+    }
+
+    @Test
+    public void testPropertiesOptionallKey() throws Exception {
+        Bar bar = new Bar();
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("?AGE", "33");
+        prop.put("{{committer}}", "true");
+        prop.put("gOLd-Customer", "true");
+        prop.put("?silver-Customer", "true");
+        prop.put("?work.ID", "123");
+        prop.put("?WORk.naME", "{{companyName}}");
+        prop.put("?work.addresss", "Some street");
+        prop.put("?work.addresss.zip", "1234");
+
+        myConfigurer.reset();
+        PropertyBindingSupport.build().withConfigurer(myConfigurer).withIgnoreCase(true).bind(context, bar, prop);
+        assertEquals(3, myConfigurer.getCounter());
+
+        assertEquals(33, bar.getAge());
+        assertTrue(bar.isRider());
+        assertTrue(bar.isGoldCustomer());
+        assertEquals(123, bar.getWork().getId());
+        assertEquals("Acme", bar.getWork().getName());
+
+        assertFalse("Should NOT bind all properties", prop.isEmpty());
+        assertEquals(3, prop.size());
+        assertTrue(prop.containsKey("?silver-Customer"));
+        assertTrue(prop.containsKey("?work.addresss"));
+        assertTrue(prop.containsKey("?work.addresss.zip"));
+    }
+
+    @Test
+    public void testPropertiesOptionallKeyMandatory() throws Exception {
+        Bar bar = new Bar();
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("?AGE", "33");
+        prop.put("{{committer}}", "true");
+        prop.put("gOLd-Customer", "true");
+        prop.put("?silver-Customer", "true");
+        prop.put("?work.ID", "123");
+        prop.put("?WORk.naME", "{{companyName}}");
+        prop.put("?work.addresss", "Some street");
+        prop.put("?work.addresss.zip", "1234");
+
+        myConfigurer.reset();
+        PropertyBindingSupport.build().withConfigurer(myConfigurer).withIgnoreCase(true).withMandatory(true).bind(context, bar, prop);
+        assertEquals(3, myConfigurer.getCounter());
+
+        assertEquals(33, bar.getAge());
+        assertTrue(bar.isRider());
+        assertTrue(bar.isGoldCustomer());
+        assertEquals(123, bar.getWork().getId());
+        assertEquals("Acme", bar.getWork().getName());
+
+        assertFalse("Should NOT bind all properties", prop.isEmpty());
+        assertEquals(3, prop.size());
+        assertTrue(prop.containsKey("?silver-Customer"));
+        assertTrue(prop.containsKey("?work.addresss"));
+        assertTrue(prop.containsKey("?work.addresss.zip"));
+
+        // should not fail as we marked the option as optional
+        prop.put("?unknown", "123");
+        PropertyBindingSupport.build().withConfigurer(myConfigurer).withIgnoreCase(true).withMandatory(true).bind(context, bar, prop);
+        prop.remove("?unknown");
+
+        // should fail as its mandatory
+        prop.put("unknown", "123");
+        try {
+            PropertyBindingSupport.build().withConfigurer(myConfigurer).withIgnoreCase(true).withMandatory(true).bind(context, bar, prop);
+            fail("Should fail");
+        } catch (PropertyBindingException e) {
+            assertEquals("unknown", e.getPropertyName());
+        }
+    }
+
+    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;
+        }
+    }
+
+    private class MyConfigurer implements GeneratedPropertyConfigurer {
+
+        private int counter;
+
+        @Override
+        public boolean configure(CamelContext camelContext, Object target, String name, Object value, boolean ignoreCase) {
+            name = name.toLowerCase(Locale.US);
+            name = name.replaceAll("-","");
+            if (target instanceof Bar) {
+                Bar bar = (Bar) target;
+                if ("age".equals(name)) {
+                    bar.withAge(Integer.parseInt(value.toString()));
+                    counter++;
+                    return true;
+                } else if ("rider".equals(name)) {
+                    bar.withRider(Boolean.parseBoolean(value.toString()));
+                    counter++;
+                    return true;
+                } else if ("work".equals(name)) {
+                    bar.work((Company) value);
+                    counter++;
+                    return true;
+                } else if ("goldcustomer".equals(name)) {
+                    bar.goldCustomer(Boolean.parseBoolean(value.toString()));
+                    counter++;
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        public int getCounter() {
+            return counter;
+        }
+
+        public void reset() {
+            counter = 0;
+        }
+    }
+
+}
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 7b86a3e..4d8fa36 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
@@ -383,6 +383,82 @@ public class PropertyBindingSupportTest extends ContextTestSupport {
         assertEquals(false, foo.getAnimal().isDangerous());
     }
 
+    @Test
+    public void testPropertiesOptionallKey() throws Exception {
+        Foo foo = new Foo();
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("name", "James");
+        prop.put("?bar.AGE", "33");
+        prop.put("BAR.{{committer}}", "true");
+        prop.put("bar.gOLd-Customer", "true");
+        prop.put("?bar.silver-Customer", "true");
+        prop.put("?bAr.work.ID", "123");
+        prop.put("?bar.WORk.naME", "{{companyName}}");
+        prop.put("?bar.work.addresss", "Some street");
+        prop.put("?bar.work.addresss.zip", "1234");
+
+        PropertyBindingSupport.build().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("Should NOT bind all properties", prop.isEmpty());
+        assertEquals(3, prop.size());
+        assertTrue(prop.containsKey("?bar.silver-Customer"));
+        assertTrue(prop.containsKey("?bar.work.addresss"));
+        assertTrue(prop.containsKey("?bar.work.addresss.zip"));
+    }
+
+    @Test
+    public void testPropertiesOptionallKeyMandatory() throws Exception {
+        Foo foo = new Foo();
+
+        Map<String, Object> prop = new HashMap<>();
+        prop.put("name", "James");
+        prop.put("bar.AGE", "33");
+        prop.put("BAR.{{committer}}", "true");
+        prop.put("bar.gOLd-Customer", "true");
+        prop.put("?bar.silver-Customer", "true");
+        prop.put("?bAr.work.ID", "123");
+        prop.put("?bar.WORk.naME", "{{companyName}}");
+        prop.put("?bar.work.addresss", "Some street");
+        prop.put("?bar.work.addresss.zip", "1234");
+
+        PropertyBindingSupport.build().withIgnoreCase(true).withMandatory(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("Should NOT bind all properties", prop.isEmpty());
+        assertEquals(3, prop.size());
+        assertTrue(prop.containsKey("?bar.silver-Customer"));
+        assertTrue(prop.containsKey("?bar.work.addresss"));
+        assertTrue(prop.containsKey("?bar.work.addresss.zip"));
+
+        // should not fail as we marked the option as optional
+        prop.put("?bar.unknown", "123");
+        PropertyBindingSupport.build().withIgnoreCase(true).withMandatory(true).bind(context, foo, prop);
+        prop.remove("?bar.unknown");
+
+        // should fail as its mandatory
+        prop.put("bar.unknown", "123");
+        try {
+            PropertyBindingSupport.build().withIgnoreCase(true).withMandatory(true).bind(context, foo, prop);
+            fail("Should fail");
+        } catch (PropertyBindingException e) {
+            assertEquals("bar.unknown", e.getPropertyName());
+        }
+    }
+
     public static class Foo {
         private String name;
         private Bar bar = new Bar();
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/IntrospectionSupport.java b/core/camel-support/src/main/java/org/apache/camel/support/IntrospectionSupport.java
index db8b473..178e193 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/IntrospectionSupport.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/IntrospectionSupport.java
@@ -783,7 +783,7 @@ public final class IntrospectionSupport {
     }
 
     static boolean isPropertyPlaceholder(CamelContext context, Object value) {
-        if (context != null) {
+        if (context != null && value != null) {
             String text = value.toString();
             return text.contains(PropertiesComponent.PREFIX_TOKEN) && text.contains(PropertiesComponent.SUFFIX_TOKEN);
         }
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 9ed54cc..5c60834 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
@@ -57,6 +57,13 @@ import static org.apache.camel.util.ObjectHelper.isNotEmpty;
  *                               #class:com.foo.MyClass('Hello World', 5, true)</li>.
  *     <li>ignore case - Whether to ignore case for property keys<li>
  * </ul>
+ *
+ * Keys can be marked as optional if the key name starts with a question mark, such as:
+ * <pre>
+ * foo=123
+ * ?bar=false
+ * </pre>
+ * Where foo is mandatory, and bar is optional.
  */
 public final class PropertyBindingSupport {
 
@@ -464,12 +471,18 @@ public final class PropertyBindingSupport {
         final String uOptionPrefix = ignoreCase && isNotEmpty(optionPrefix) ? optionPrefix.toUpperCase(Locale.US) : "";
         final int size = properties.size();
 
+        // use configuer first to set the options it can do
         if (configurer != null) {
             for (Iterator<Map.Entry<String, Object>> iter = properties.entrySet().iterator(); iter.hasNext();) {
                 Map.Entry<String, Object> entry = iter.next();
                 String key = entry.getKey();
                 Object value = entry.getValue();
 
+                final boolean optional = key.startsWith("?");
+                if (optional) {
+                    key = key.substring(1);
+                }
+
                 // property configurer does not support nested names so skip if the name has a dot
                 if (key.indexOf('.') == -1) {
                     try {
@@ -491,15 +504,28 @@ public final class PropertyBindingSupport {
             }
         }
 
-        // must set reference parameters first before the other bindings
+        // then we must set reference parameters before the other bindings
         setReferenceProperties(camelContext, target, properties);
 
-        // sort the keys by nesting level
+        // sort the keys by nesting level and set the remainder
         properties.keySet().stream()
             .sorted(Comparator.comparingInt(s -> StringHelper.countChar(s, '.')))
             .forEach(key -> {
+                Object text = properties.get(key);
                 final String propertyKey = key;
-                final Object value = properties.get(key);
+                final boolean optional = key.startsWith("?");
+                if (optional) {
+                    key = key.substring(1);
+                }
+                if (placeholder) {
+                    // resolve property placeholders
+                    key = camelContext.resolvePropertyPlaceholders(key);
+                    if (text instanceof String) {
+                        // resolve property placeholders
+                        text = camelContext.resolvePropertyPlaceholders(text.toString());
+                    }
+                }
+                final Object value = text;
 
                 if (isNotEmpty(optionPrefix)) {
                     boolean match = key.startsWith(optionPrefix) || ignoreCase && key.toUpperCase(Locale.US).startsWith(uOptionPrefix);
@@ -512,7 +538,11 @@ public final class PropertyBindingSupport {
                 boolean bound = false;
                 if (configurer != null) {
                     // attempt configurer first
-                    bound = configurer.configure(camelContext, target, key, value, ignoreCase);
+                    try {
+                        bound = configurer.configure(camelContext, target, key, value, ignoreCase);
+                    } catch (Exception e) {
+                        throw new PropertyBindingException(target, key, value, e);
+                    }
                 }
                 if (!bound) {
                     bound = bindProperty(camelContext, target, key, value, ignoreCase, nesting, deepNesting, fluentBuilder, allowPrivateSetter, reference, placeholder);
@@ -520,7 +550,7 @@ public final class PropertyBindingSupport {
                 if (bound && removeParameter) {
                     properties.remove(propertyKey);
                 }
-                if (mandatory && !bound) {
+                if (mandatory && !optional && !bound) {
                     throw new PropertyBindingException(target, propertyKey, value);
                 }
             }
@@ -730,6 +760,11 @@ public final class PropertyBindingSupport {
             Map.Entry<String, Object> entry = it.next();
             String name = entry.getKey();
 
+            final boolean optional = name.startsWith("?");
+            if (optional) {
+                name = name.substring(1);
+            }
+
             // we only support basic keys
             if (name.contains(".") || name.contains("[") || name.contains("]")) {
                 continue;