This is an automated email from the ASF dual-hosted git repository. lburgazzoli pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel.git
commit 493e93d44ba82d201a3a75e095b43a547b86c271 Author: lburgazzoli <lburgazz...@gmail.com> AuthorDate: Tue Sep 1 19:40:57 2020 +0200 CAMEL-14672: Invoke customizers as part of services initialization --- .../org/apache/camel/spi/ComponentCustomizer.java | 209 +++++- .../org/apache/camel/spi/DataFormatCustomizer.java | 213 +++++- .../org/apache/camel/spi/DataFormatResolver.java | 10 - .../org/apache/camel/spi/LanguageCustomizer.java | 213 +++++- .../org/apache/camel/spi/LifecycleStrategy.java | 18 + .../camel/impl/engine/AbstractCamelContext.java | 76 +- .../impl/engine/CustomizersLifecycleStrategy.java | 87 +++ .../impl/engine/DefaultDataFormatResolver.java | 14 - .../camel/impl/engine/DefaultLanguageResolver.java | 7 - .../org/apache/camel/support/CustomizersTest.java | 457 ++++++++++++ .../org/apache/camel/main/MainCustomizerTest.java | 99 +++ .../apache/camel/support/CustomizersSupport.java | 129 ++++ .../camel/support/PropertyBindingSupport.java | 787 ++++++++++----------- .../camel/support/PropertyConfigurerHelper.java | 52 +- .../org/apache/camel/util/CollectionHelper.java | 1 + 15 files changed, 1901 insertions(+), 471 deletions(-) diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/ComponentCustomizer.java b/core/camel-api/src/main/java/org/apache/camel/spi/ComponentCustomizer.java index 59dc6c7..2a2cf28 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/ComponentCustomizer.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/ComponentCustomizer.java @@ -16,14 +16,217 @@ */ package org.apache.camel.spi; +import java.util.function.BiPredicate; + import org.apache.camel.Component; +import org.apache.camel.Ordered; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.function.ThrowingBiConsumer; +import org.apache.camel.util.function.ThrowingConsumer; +/** + * To apply custom configurations to {@link Component} instances. + */ @FunctionalInterface -public interface ComponentCustomizer<T extends Component> { +public interface ComponentCustomizer extends Ordered { + /** + * Create a generic {@link Builder}. + * + * @return the {@link Builder} + */ + static Builder<Component> builder() { + return builder(Component.class); + } + + /** + * Create a typed {@link Builder} that can process a concrete component type instance. + * + * @param type the concrete type of the {@link Component} + * @return the {@link Builder} + */ + static <T extends Component> Builder<T> builder(Class<T> type) { + return new Builder<>(type); + } + + /** + * Create a {@link ComponentCustomizer} that can process a concrete component type instance. + * + * @param type the concrete type of the {@link Component} + * @param consumer the {@link Component} configuration logic + * @return the {@link ComponentCustomizer} + */ + static <T extends Component> ComponentCustomizer forType(Class<T> type, ThrowingConsumer<T, Exception> consumer) { + return builder(type).build(consumer); + } + /** * Customize the specified {@link Component}. * - * @param component the component to customize + * @param name the unique name of the component + * @param target the component to configure + */ + void configure(String name, Component target); + + /** + * Checks whether this customizer should be applied to the given {@link Component}. + * + * @param name the unique name of the component + * @param target the component to configure + * @return <tt>true</tt> if the customizer should be applied + */ + default boolean isEnabled(String name, Component target) { + return true; + } + + @Override + default int getOrder() { + return 0; + } + + // *************************************** + // + // Filter + // + // *************************************** + + /** + * Used as additional filer mechanism to control if customizers need to be applied or not. Each of this filter is + * applied before any of the discovered {@link ComponentCustomizer} is invoked. + * </p> + * This interface is useful to implement enable/disable logic on a group of customizers. + */ + @FunctionalInterface + interface Policy extends BiPredicate<String, Component> { + /** + * A simple deny-all policy. + * + * @return false + */ + static Policy none() { + return new Policy() { + @Override + public boolean test(String s, Component target) { + return false; + } + }; + } + + /** + * A simple allow-all policy. + * + * @return true + */ + static Policy any() { + return new Policy() { + @Override + public boolean test(String s, Component target) { + return true; + } + }; + } + } + + // *************************************** + // + // Builders + // + // *************************************** + + /** + * A fluent builder to create a {@link ComponentCustomizer} instance. + * + * @param <T> the concrete type of the {@link Component}. */ - void customize(T component); + class Builder<T extends Component> { + private final Class<T> type; + private BiPredicate<String, Component> condition; + private int order; + + public Builder(Class<T> type) { + this.type = type; + } + + public Builder<T> withOrder(int order) { + this.order = order; + + return this; + } + + public Builder<T> withCondition(BiPredicate<String, Component> condition) { + this.condition = condition; + + return this; + } + + public ComponentCustomizer build(ThrowingConsumer<T, Exception> consumer) { + return build(new ThrowingBiConsumer<String, T, Exception>() { + @Override + public void accept(String name, T target) throws Exception { + consumer.accept(target); + } + }); + } + + public ComponentCustomizer build(ThrowingBiConsumer<String, T, Exception> consumer) { + final int order = this.order; + final BiPredicate<String, Component> condition = condition(); + + return new ComponentCustomizer() { + @SuppressWarnings("unchecked") + @Override + public void configure(String name, Component target) { + ObjectHelper.notNull(name, "component name"); + ObjectHelper.notNull(target, "component instance"); + + try { + consumer.accept(name, (T) target); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isEnabled(String name, Component target) { + ObjectHelper.notNull(name, "component name"); + ObjectHelper.notNull(target, "component instance"); + + return condition.test(name, target); + } + + @Override + public int getOrder() { + return order; + } + }; + } + + private BiPredicate<String, Component> condition() { + if (type.equals(Component.class)) { + return this.condition != null + ? this.condition + : new BiPredicate<String, Component>() { + @Override + public boolean test(String s, Component language) { + return true; + } + }; + } + + if (condition == null) { + return new BiPredicate<String, Component>() { + @Override + public boolean test(String name, Component target) { + return type.isAssignableFrom(target.getClass()); + } + }; + } + + return new BiPredicate<String, Component>() { + @Override + public boolean test(String name, Component target) { + return type.isAssignableFrom(target.getClass()) && condition.test(name, target); + } + }; + } + } } diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/DataFormatCustomizer.java b/core/camel-api/src/main/java/org/apache/camel/spi/DataFormatCustomizer.java index 7e128f6..7507f75 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/DataFormatCustomizer.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/DataFormatCustomizer.java @@ -16,12 +16,217 @@ */ package org.apache.camel.spi; +import java.util.function.BiPredicate; + +import org.apache.camel.Component; +import org.apache.camel.Ordered; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.function.ThrowingBiConsumer; +import org.apache.camel.util.function.ThrowingConsumer; + +/** + * To apply custom configurations to {@link DataFormat} instances. + */ @FunctionalInterface -public interface DataFormatCustomizer<T extends DataFormat> { +public interface DataFormatCustomizer extends Ordered { + /** + * Create a generic {@link Builder}. + * + * @return the {@link Builder} + */ + static Builder<DataFormat> builder() { + return builder(DataFormat.class); + } + + /** + * Create a typed {@link Builder} that can process a concrete data format type instance. + * + * @param type the concrete type of the {@link Component} + * @return the {@link ComponentCustomizer.Builder} + */ + static <T extends DataFormat> Builder<T> builder(Class<T> type) { + return new Builder<>(type); + } + /** - * Customize the specified {@link DataFormat} + * Create a {@link DataFormatCustomizer} that can process a concrete data format type instance. * - * @param dataformat the dataformat to customize + * @param type the concrete type of the {@link DataFormat} + * @param consumer the {@link DataFormat} configuration logic + * @return the {@link DataFormatCustomizer} */ - void customize(T dataformat); + static <T extends DataFormat> DataFormatCustomizer forType(Class<T> type, ThrowingConsumer<T, Exception> consumer) { + return builder(type).build(consumer); + } + + /** + * Customize the specified {@link DataFormat}. + * + * @param name the unique name of the data format + * @param target the data format to configure + */ + void configure(String name, DataFormat target); + + /** + * Checks whether this customizer should be applied to the given {@link DataFormat}. + * + * @param name the unique name of the data format + * @param target the data format to configure + * @return <tt>true</tt> if the customizer should be applied + */ + default boolean isEnabled(String name, DataFormat target) { + return true; + } + + @Override + default int getOrder() { + return 0; + } + + // *************************************** + // + // Filter + // + // *************************************** + + /** + * Used as additional filer mechanism to control if customizers need to be applied or not. Each of this filter is + * applied before any of the discovered {@link DataFormatCustomizer} is invoked. + * </p> + * This interface is useful to implement enable/disable logic on a group of customizers. + */ + @FunctionalInterface + interface Policy extends BiPredicate<String, DataFormat> { + /** + * A simple deny-all policy. + * + * @return false + */ + static Policy none() { + return new Policy() { + @Override + public boolean test(String s, DataFormat target) { + return false; + } + }; + } + + /** + * A simple allow-all policy. + * + * @return true + */ + static Policy any() { + return new Policy() { + @Override + public boolean test(String s, DataFormat target) { + return true; + } + }; + } + } + + // *************************************** + // + // Builders + // + // *************************************** + + /** + * A fluent builder to create a {@link DataFormatCustomizer} instance. + * + * @param <T> the concrete type of the {@link DataFormat}. + */ + class Builder<T extends DataFormat> { + private final Class<T> type; + private BiPredicate<String, DataFormat> condition; + private int order; + + public Builder(Class<T> type) { + this.type = type; + } + + public Builder<T> withOrder(int order) { + this.order = order; + + return this; + } + + public Builder<T> withCondition(BiPredicate<String, DataFormat> condition) { + this.condition = condition; + + return this; + } + + public DataFormatCustomizer build(ThrowingConsumer<T, Exception> consumer) { + return build(new ThrowingBiConsumer<String, T, Exception>() { + @Override + public void accept(String name, T target) throws Exception { + consumer.accept(target); + } + }); + } + + public DataFormatCustomizer build(ThrowingBiConsumer<String, T, Exception> consumer) { + final int order = this.order; + final BiPredicate<String, DataFormat> condition = condition(); + + return new DataFormatCustomizer() { + @SuppressWarnings("unchecked") + @Override + public void configure(String name, DataFormat target) { + ObjectHelper.notNull(name, "data format name"); + ObjectHelper.notNull(target, "data format instance"); + + try { + consumer.accept(name, (T) target); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isEnabled(String name, DataFormat target) { + ObjectHelper.notNull(name, "data format name"); + ObjectHelper.notNull(target, "data format instance"); + + return condition.test(name, target); + } + + @Override + public int getOrder() { + return order; + } + }; + } + + private BiPredicate<String, DataFormat> condition() { + if (type.equals(DataFormat.class)) { + return this.condition != null + ? this.condition + : new BiPredicate<String, DataFormat>() { + @Override + public boolean test(String s, DataFormat language) { + return true; + } + }; + } + + if (condition == null) { + return new BiPredicate<String, DataFormat>() { + @Override + public boolean test(String name, DataFormat target) { + return type.isAssignableFrom(target.getClass()); + } + }; + } + + return new BiPredicate<String, DataFormat>() { + @Override + public boolean test(String name, DataFormat target) { + return type.isAssignableFrom(target.getClass()) && condition.test(name, target); + } + }; + } + } } diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/DataFormatResolver.java b/core/camel-api/src/main/java/org/apache/camel/spi/DataFormatResolver.java index 12b47ad..492f247 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/DataFormatResolver.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/DataFormatResolver.java @@ -22,16 +22,6 @@ import org.apache.camel.CamelContext; * Represents a resolver of data formats. */ public interface DataFormatResolver { - - /** - * Resolves the given data format given its name. - * - * @param name the name of the data format to lookup in {@link org.apache.camel.spi.Registry} or create - * @param context the camel context - * @return the data format or <tt>null</tt> if not possible to resolve - */ - DataFormat resolveDataFormat(String name, CamelContext context); - /** * Creates the given data format given its name. * diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/LanguageCustomizer.java b/core/camel-api/src/main/java/org/apache/camel/spi/LanguageCustomizer.java index 34b3218..76c2ea8 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/LanguageCustomizer.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/LanguageCustomizer.java @@ -16,12 +16,217 @@ */ package org.apache.camel.spi; +import java.util.function.BiPredicate; + +import org.apache.camel.Component; +import org.apache.camel.Ordered; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.function.ThrowingBiConsumer; +import org.apache.camel.util.function.ThrowingConsumer; + +/** + * To apply custom configurations to {@link Language} instances. + */ @FunctionalInterface -public interface LanguageCustomizer<T extends Language> { +public interface LanguageCustomizer extends Ordered { + /** + * Create a generic {@link Builder}. + * + * @return the {@link Builder} + */ + static Builder<Language> builder() { + return builder(Language.class); + } + + /** + * Create a typed {@link Builder} that can process a concrete language type instance. + * + * @param type the concrete type of the {@link Component} + * @return the {@link ComponentCustomizer.Builder} + */ + static <T extends Language> Builder<T> builder(Class<T> type) { + return new Builder<>(type); + } + /** - * Customize the specified {@link Language} + * Create a {@link DataFormatCustomizer} that can process a concrete language type instance. * - * @param language the language to customize + * @param type the concrete type of the {@link Language} + * @param consumer the {@link Language} configuration logic + * @return the {@link LanguageCustomizer} */ - void customize(T language); + static <T extends Language> LanguageCustomizer forType(Class<T> type, ThrowingConsumer<T, Exception> consumer) { + return builder(type).build(consumer); + } + + /** + * Customize the specified {@link Language}. + * + * @param name the unique name of the language + * @param target the language to configure + */ + void configure(String name, Language target); + + /** + * Checks whether this customizer should be applied to the given {@link Language}. + * + * @param name the unique name of the language + * @param target the language to configure + * @return <tt>true</tt> if the customizer should be applied + */ + default boolean isEnabled(String name, Language target) { + return true; + } + + @Override + default int getOrder() { + return 0; + } + + // *************************************** + // + // Filter + // + // *************************************** + + /** + * Used as additional filer mechanism to control if customizers need to be applied or not. Each of this filter is + * applied before any of the discovered {@link LanguageCustomizer} is invoked. + * </p> + * This interface is useful to implement enable/disable logic on a group of customizers. + */ + @FunctionalInterface + interface Policy extends BiPredicate<String, Language> { + /** + * A simple deny-all policy. + * + * @return false + */ + static Policy none() { + return new Policy() { + @Override + public boolean test(String s, Language target) { + return false; + } + }; + } + + /** + * A simple allow-all policy. + * + * @return true + */ + static Policy any() { + return new Policy() { + @Override + public boolean test(String s, Language target) { + return true; + } + }; + } + } + + // *************************************** + // + // Builders + // + // *************************************** + + /** + * A fluent builder to create a {@link LanguageCustomizer} instance. + * + * @param <T> the concrete type of the {@link Language}. + */ + class Builder<T extends Language> { + private final Class<T> type; + private BiPredicate<String, Language> condition; + private int order; + + public Builder(Class<T> type) { + this.type = type; + } + + public Builder<T> withOrder(int order) { + this.order = order; + + return this; + } + + public Builder<T> withCondition(BiPredicate<String, Language> condition) { + this.condition = condition; + + return this; + } + + public LanguageCustomizer build(ThrowingConsumer<T, Exception> consumer) { + return build(new ThrowingBiConsumer<String, T, Exception>() { + @Override + public void accept(String name, T target) throws Exception { + consumer.accept(target); + } + }); + } + + public LanguageCustomizer build(ThrowingBiConsumer<String, T, Exception> consumer) { + final int order = this.order; + final BiPredicate<String, Language> condition = this.condition != null ? this.condition : (name, target) -> true; + + return new LanguageCustomizer() { + @SuppressWarnings("unchecked") + @Override + public void configure(String name, Language target) { + ObjectHelper.notNull(name, "language name"); + ObjectHelper.notNull(target, "language instance"); + + try { + consumer.accept(name, (T) target); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isEnabled(String name, Language target) { + ObjectHelper.notNull(name, "language name"); + ObjectHelper.notNull(target, "language instance"); + + return type.isAssignableFrom(target.getClass()) && condition.test(name, target); + } + + @Override + public int getOrder() { + return order; + } + }; + } + + private BiPredicate<String, Language> condition() { + if (type.equals(Language.class)) { + return this.condition != null + ? this.condition + : new BiPredicate<String, Language>() { + @Override + public boolean test(String s, Language language) { + return true; + } + }; + } + + if (condition == null) { + return new BiPredicate<String, Language>() { + @Override + public boolean test(String name, Language target) { + return type.isAssignableFrom(target.getClass()); + } + }; + } + + return new BiPredicate<String, Language>() { + @Override + public boolean test(String name, Language target) { + return type.isAssignableFrom(target.getClass()) && condition.test(name, target); + } + }; + } + } } diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/LifecycleStrategy.java b/core/camel-api/src/main/java/org/apache/camel/spi/LifecycleStrategy.java index a31edec..f837cae 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/LifecycleStrategy.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/LifecycleStrategy.java @@ -144,6 +144,24 @@ public interface LifecycleStrategy { void onEndpointRemove(Endpoint endpoint); /** + * Notification on {@link DataFormat} being resolved from the {@link Registry} + * + * @param name the unique name of the {@link DataFormat} + * @param dataFormat the resolved {@link DataFormat} + */ + default void onDataFormatCreated(String name, DataFormat dataFormat) { + } + + /** + * Notification on a {@link Language} instance being resolved. + * + * @param name the unique name of the {@link Language} + * @param language the created {@link Language} + */ + default void onLanguageCreated(String name, Language language) { + } + + /** * Notification on adding a {@link Service}. * * @param context the camel context diff --git a/core/camel-base/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java b/core/camel-base/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java index f8a9252..5a6a30f 100644 --- a/core/camel-base/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java +++ b/core/camel-base/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java @@ -151,6 +151,7 @@ import org.apache.camel.support.LRUCacheFactory; import org.apache.camel.support.NormalizedUri; import org.apache.camel.support.OrderedComparator; import org.apache.camel.support.ProcessorEndpoint; +import org.apache.camel.support.ResolverHelper; import org.apache.camel.support.jsse.SSLContextParameters; import org.apache.camel.support.service.BaseService; import org.apache.camel.support.service.ServiceHelper; @@ -186,6 +187,7 @@ public abstract class AbstractCamelContext extends BaseService private final List<StartupListener> startupListeners = new CopyOnWriteArrayList<>(); private final DeferServiceStartupListener deferStartupListener = new DeferServiceStartupListener(); private final Map<String, Language> languages = new ConcurrentHashMap<>(); + private final Map<String, DataFormat> dataformats = new ConcurrentHashMap<>(); private final List<LifecycleStrategy> lifecycleStrategies = new CopyOnWriteArrayList<>(); private final ThreadLocal<Boolean> isStartingRoutes = new ThreadLocal<>(); private final ThreadLocal<Boolean> isSetupRoutes = new ThreadLocal<>(); @@ -329,6 +331,9 @@ public abstract class AbstractCamelContext extends BaseService // add a default LifecycleStrategy that discover strategies on the registry and invoke them this.lifecycleStrategies.add(new OnCamelContextLifecycleStrategy()); + // add a default LifecycleStrategy to customize services using customizers from registry + this.lifecycleStrategies.add(new CustomizersLifecycleStrategy(this)); + if (build) { try { build(); @@ -1730,22 +1735,38 @@ public abstract class AbstractCamelContext extends BaseService public Language resolveLanguage(String language) { Language answer; synchronized (languages) { - answer = languages.get(language); + // as first iteration, check if there is a language instance for the given name + // bound to the registry + answer = ResolverHelper.lookupLanguageInRegistryWithFallback(getCamelContextReference(), language); + if (answer != null) { + Language old = languages.put(language, answer); + // if the language has already been loaded, thus it is already registered + // in the local language cache, we can return it as it has already been + // initialized and configured + if (old == answer) { + return answer; + } + } else { + answer = languages.get(language); - // check if the language is singleton, if so return the shared - // instance - if (IsSingleton.test(answer)) { - return answer; + // check if the language is singleton, if so return the shared + // instance + if (IsSingleton.test(answer)) { + return answer; + } else { + answer = null; + } } - // language not known or not singleton, then use resolver - answer = getLanguageResolver().resolveLanguage(language, getCamelContextReference()); + if (answer == null) { + // language not known or not singleton, then use resolver + answer = getLanguageResolver().resolveLanguage(language, getCamelContextReference()); + } - // inject CamelContext if aware if (answer != null) { - if (answer instanceof CamelContextAware) { - ((CamelContextAware) answer).setCamelContext(getCamelContextReference()); - } + // inject CamelContext if aware + CamelContextAware.trySetCamelContext(answer, getCamelContextReference()); + if (answer instanceof Service) { try { startService((Service) answer); @@ -1754,6 +1775,10 @@ public abstract class AbstractCamelContext extends BaseService } } + for (LifecycleStrategy strategy : lifecycleStrategies) { + strategy.onLanguageCreated(language, answer); + } + languages.put(language, answer); } } @@ -3739,14 +3764,25 @@ public abstract class AbstractCamelContext extends BaseService @Override public DataFormat resolveDataFormat(String name) { - DataFormat answer = getDataFormatResolver().resolveDataFormat(name, getCamelContextReference()); + final DataFormat answer = dataformats.computeIfAbsent(name, new Function<String, DataFormat>() { + @Override + public DataFormat apply(String s) { + DataFormat df = ResolverHelper.lookupDataFormatInRegistryWithFallback(getCamelContextReference(), name); - // inject CamelContext if aware - if (answer instanceof CamelContextAware) { - ((CamelContextAware) answer).setCamelContext(getCamelContextReference()); - } + if (df != null) { + // inject CamelContext if aware + CamelContextAware.trySetCamelContext(df, getCamelContextReference()); - return answer; + for (LifecycleStrategy strategy : lifecycleStrategies) { + strategy.onDataFormatCreated(name, df); + } + } + + return df; + } + }); + + return answer != null ? answer : createDataFormat(name); } @Override @@ -3754,8 +3790,10 @@ public abstract class AbstractCamelContext extends BaseService DataFormat answer = getDataFormatResolver().createDataFormat(name, getCamelContextReference()); // inject CamelContext if aware - if (answer instanceof CamelContextAware) { - ((CamelContextAware) answer).setCamelContext(getCamelContextReference()); + CamelContextAware.trySetCamelContext(answer, getCamelContextReference()); + + for (LifecycleStrategy strategy : lifecycleStrategies) { + strategy.onDataFormatCreated(name, answer); } return answer; diff --git a/core/camel-base/src/main/java/org/apache/camel/impl/engine/CustomizersLifecycleStrategy.java b/core/camel-base/src/main/java/org/apache/camel/impl/engine/CustomizersLifecycleStrategy.java new file mode 100644 index 0000000..d2b5855 --- /dev/null +++ b/core/camel-base/src/main/java/org/apache/camel/impl/engine/CustomizersLifecycleStrategy.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.camel.impl.engine; + +import java.util.Comparator; +import java.util.Set; + +import org.apache.camel.CamelContext; +import org.apache.camel.Component; +import org.apache.camel.Ordered; +import org.apache.camel.spi.ComponentCustomizer; +import org.apache.camel.spi.DataFormat; +import org.apache.camel.spi.DataFormatCustomizer; +import org.apache.camel.spi.Language; +import org.apache.camel.spi.LanguageCustomizer; +import org.apache.camel.spi.Registry; +import org.apache.camel.support.LifecycleStrategySupport; + +class CustomizersLifecycleStrategy extends LifecycleStrategySupport { + private final CamelContext camelContext; + + public CustomizersLifecycleStrategy(CamelContext camelContext) { + this.camelContext = camelContext; + } + + @Override + public void onComponentAdd(String name, Component component) { + Registry registry = this.camelContext.getRegistry(); + if (registry == null) { + return; + } + + Set<ComponentCustomizer.Policy> filters = registry.findByType(ComponentCustomizer.Policy.class); + if (filters.isEmpty() || filters.stream().allMatch(filter -> filter.test(name, component))) { + registry.findByType(ComponentCustomizer.class).stream() + .sorted(Comparator.comparingInt(Ordered::getOrder)) + .filter(customizer -> customizer.isEnabled(name, component)) + .forEach(customizer -> customizer.configure(name, component)); + } + } + + @Override + public void onDataFormatCreated(String name, DataFormat dataFormat) { + Registry registry = this.camelContext.getRegistry(); + if (registry == null) { + return; + } + + Set<DataFormatCustomizer.Policy> filters = registry.findByType(DataFormatCustomizer.Policy.class); + if (filters.isEmpty() || filters.stream().allMatch(filter -> filter.test(name, dataFormat))) { + registry.findByType(DataFormatCustomizer.class).stream() + .sorted(Comparator.comparingInt(Ordered::getOrder)) + .filter(customizer -> customizer.isEnabled(name, dataFormat)) + .forEach(customizer -> customizer.configure(name, dataFormat)); + } + } + + @Override + public void onLanguageCreated(String name, Language language) { + Registry registry = this.camelContext.getRegistry(); + if (registry == null) { + return; + } + + Set<LanguageCustomizer.Policy> filters = registry.findByType(LanguageCustomizer.Policy.class); + if (filters.isEmpty() || filters.stream().allMatch(filter -> filter.test(name, language))) { + registry.findByType(LanguageCustomizer.class).stream() + .sorted(Comparator.comparingInt(Ordered::getOrder)) + .filter(customizer -> customizer.isEnabled(name, language)) + .forEach(customizer -> customizer.configure(name, language)); + } + } +} diff --git a/core/camel-base/src/main/java/org/apache/camel/impl/engine/DefaultDataFormatResolver.java b/core/camel-base/src/main/java/org/apache/camel/impl/engine/DefaultDataFormatResolver.java index 4d01e3e..525cba8 100644 --- a/core/camel-base/src/main/java/org/apache/camel/impl/engine/DefaultDataFormatResolver.java +++ b/core/camel-base/src/main/java/org/apache/camel/impl/engine/DefaultDataFormatResolver.java @@ -33,20 +33,6 @@ public class DefaultDataFormatResolver implements DataFormatResolver { private FactoryFinder dataformatFactory; @Override - public DataFormat resolveDataFormat(String name, CamelContext context) { - // lookup in registry first - DataFormat dataFormat = ResolverHelper.lookupDataFormatInRegistryWithFallback(context, name); - - if (dataFormat == null) { - // If not found in the registry, try to create a new instance using - // a DataFormatFactory or from resources - dataFormat = createDataFormat(name, context); - } - - return dataFormat; - } - - @Override public DataFormat createDataFormat(String name, CamelContext context) { DataFormat dataFormat = null; diff --git a/core/camel-base/src/main/java/org/apache/camel/impl/engine/DefaultLanguageResolver.java b/core/camel-base/src/main/java/org/apache/camel/impl/engine/DefaultLanguageResolver.java index 4043f9e..d596361 100644 --- a/core/camel-base/src/main/java/org/apache/camel/impl/engine/DefaultLanguageResolver.java +++ b/core/camel-base/src/main/java/org/apache/camel/impl/engine/DefaultLanguageResolver.java @@ -23,7 +23,6 @@ import org.apache.camel.NoSuchLanguageException; import org.apache.camel.spi.FactoryFinder; import org.apache.camel.spi.Language; import org.apache.camel.spi.LanguageResolver; -import org.apache.camel.support.ResolverHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,12 +41,6 @@ public class DefaultLanguageResolver implements LanguageResolver { @Override public Language resolveLanguage(String name, CamelContext context) { - // lookup in registry first - Language languageReg = ResolverHelper.lookupLanguageInRegistryWithFallback(context, name); - if (languageReg != null) { - return languageReg; - } - Class<?> type = null; try { type = findLanguage(name, context); diff --git a/core/camel-core/src/test/java/org/apache/camel/support/CustomizersTest.java b/core/camel-core/src/test/java/org/apache/camel/support/CustomizersTest.java new file mode 100644 index 0000000..151f67d --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/support/CustomizersTest.java @@ -0,0 +1,457 @@ +/* + * 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.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import org.apache.camel.Exchange; +import org.apache.camel.component.log.LogComponent; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.language.tokenizer.TokenizeLanguage; +import org.apache.camel.spi.ComponentCustomizer; +import org.apache.camel.spi.DataFormat; +import org.apache.camel.spi.DataFormatCustomizer; +import org.apache.camel.spi.DataFormatFactory; +import org.apache.camel.spi.Language; +import org.apache.camel.spi.LanguageCustomizer; +import org.apache.camel.support.processor.DefaultExchangeFormatter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.apache.camel.util.CollectionHelper.propertiesOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CustomizersTest { + + // ***************************** + // + // Helpers + // + // ***************************** + + public static Stream<Arguments> disableLanguageCustomizationProperties() { + return Stream.of( + Arguments.of(propertiesOf( + "camel.customizer.enabled", "true", + "camel.customizer.language.enabled", "true", + "camel.customizer.language.tokenize.enabled", "false")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "true", + "camel.customizer.language.enabled", "false")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "false"))); + } + + public static Stream<Arguments> enableLanguageCustomizationProperties() { + return Stream.of( + Arguments.of(propertiesOf( + "camel.customizer.enabled", "false", + "camel.customizer.language.enabled", "false", + "camel.customizer.language.tokenize.enabled", "true")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "false", + "camel.customizer.language.enabled", "true")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "true"))); + } + + public static Stream<Arguments> disableDataFormatCustomizationProperties() { + return Stream.of( + Arguments.of(propertiesOf( + "camel.customizer.enabled", "true", + "camel.customizer.dataformat.enabled", "true", + "camel.customizer.dataformat.my-df.enabled", "false")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "true", + "camel.customizer.dataformat.enabled", "false")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "false"))); + } + + public static Stream<Arguments> enableDataFormatCustomizationProperties() { + return Stream.of( + Arguments.of(propertiesOf( + "camel.customizer.enabled", "false", + "camel.customizer.dataformat.enabled", "false", + "camel.customizer.dataformat.my-df.enabled", "true")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "false", + "camel.customizer.dataformat.enabled", "true")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "true"))); + } + + public static Stream<Arguments> disableComponentCustomizationProperties() { + return Stream.of( + Arguments.of(propertiesOf( + "camel.customizer.enabled", "true", + "camel.customizer.component.enabled", "true", + "camel.customizer.component.log.enabled", "false")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "true", + "camel.customizer.component.enabled", "false")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "false"))); + } + + public static Stream<Arguments> enableComponentCustomizationProperties() { + return Stream.of( + Arguments.of(propertiesOf( + "camel.customizer.enabled", "false", + "camel.customizer.component.enabled", "false", + "camel.customizer.component.log.enabled", "true")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "false", + "camel.customizer.component.enabled", "true")), + Arguments.of(propertiesOf( + "camel.customizer.enabled", "true"))); + } + + // ***************************** + // + // Component + // + // ***************************** + + @Test + public void testComponentCustomization() { + DefaultCamelContext context = new DefaultCamelContext(); + context.getRegistry().bind( + "log-customizer", + ComponentCustomizer.forType( + LogComponent.class, + target -> target.setExchangeFormatter(new MyExchangeFormatter()))); + + assertTrue(context.getComponent("log", LogComponent.class).getExchangeFormatter() instanceof MyExchangeFormatter); + } + + @Test + public void testComponentCustomizationWithFilter() { + DefaultCamelContext context = new DefaultCamelContext(); + context.getRegistry().bind( + "customizer-filter", + ComponentCustomizer.Policy.none()); + context.getRegistry().bind( + "log-customizer", + ComponentCustomizer.forType( + LogComponent.class, + target -> target.setExchangeFormatter(new MyExchangeFormatter()))); + + assertFalse(context.getComponent("log", LogComponent.class).getExchangeFormatter() instanceof MyExchangeFormatter); + } + + @Test + public void testComponentCustomizationWithFluentBuilder() { + DefaultCamelContext context = new DefaultCamelContext(); + context.getRegistry().bind( + "log-customizer", + ComponentCustomizer.forType( + LogComponent.class, + target -> target.setExchangeFormatter(new MyExchangeFormatter()))); + + assertTrue(context.getComponent("log", LogComponent.class).getExchangeFormatter() instanceof MyExchangeFormatter); + } + + @ParameterizedTest + @MethodSource("disableComponentCustomizationProperties") + public void testComponentCustomizationDisabledByProperty(Properties properties) { + DefaultCamelContext context = new DefaultCamelContext(); + context.getPropertiesComponent().setInitialProperties(properties); + + context.getRegistry().bind( + "customizer-filter", + new CustomizersSupport.ComponentCustomizationEnabledPolicy()); + context.getRegistry().bind( + "log-customizer", + ComponentCustomizer.forType( + LogComponent.class, + target -> target.setExchangeFormatter(new MyExchangeFormatter()))); + + assertFalse(context.getComponent("log", LogComponent.class).getExchangeFormatter() instanceof MyExchangeFormatter); + } + + @ParameterizedTest + @MethodSource("enableComponentCustomizationProperties") + public void testComponentCustomizationEnabledByProperty(Properties properties) { + DefaultCamelContext context = new DefaultCamelContext(); + context.getPropertiesComponent().setInitialProperties(properties); + + context.getRegistry().bind( + "customizer-filter", + new CustomizersSupport.ComponentCustomizationEnabledPolicy()); + context.getRegistry().bind( + "log-customizer", + ComponentCustomizer.forType( + LogComponent.class, + target -> target.setExchangeFormatter(new MyExchangeFormatter()))); + + assertTrue(context.getComponent("log", LogComponent.class).getExchangeFormatter() instanceof MyExchangeFormatter); + } + + // ***************************** + // + // Data Format + // + // ***************************** + + @Test + public void testDataFormatCustomization() { + AtomicInteger counter = new AtomicInteger(); + + DefaultCamelContext context = new DefaultCamelContext(); + context.getRegistry().bind( + "my-df", + (DataFormatFactory) MyDataFormat::new); + context.getRegistry().bind( + "my-df-customizer", + DataFormatCustomizer.forType(MyDataFormat.class, target -> target.setId(counter.incrementAndGet()))); + + DataFormat df1 = context.resolveDataFormat("my-df"); + DataFormat df2 = context.resolveDataFormat("my-df"); + + assertNotEquals(df1, df2); + + assertTrue(df1 instanceof MyDataFormat); + assertEquals(1, ((MyDataFormat) df1).getId()); + assertTrue(df2 instanceof MyDataFormat); + assertEquals(2, ((MyDataFormat) df2).getId()); + } + + @Test + public void testDataFormatCustomizationWithFilter() { + AtomicInteger counter = new AtomicInteger(); + + DefaultCamelContext context = new DefaultCamelContext(); + context.getRegistry().bind( + "customizer-filter", + DataFormatCustomizer.Policy.none()); + context.getRegistry().bind( + "my-df", + (DataFormatFactory) MyDataFormat::new); + context.getRegistry().bind( + "my-df-customizer", + DataFormatCustomizer.forType(MyDataFormat.class, target -> target.setId(counter.incrementAndGet()))); + + DataFormat df1 = context.resolveDataFormat("my-df"); + DataFormat df2 = context.resolveDataFormat("my-df"); + + assertNotEquals(df1, df2); + + assertTrue(df1 instanceof MyDataFormat); + assertEquals(0, ((MyDataFormat) df1).getId()); + assertTrue(df2 instanceof MyDataFormat); + assertEquals(0, ((MyDataFormat) df2).getId()); + } + + @ParameterizedTest + @MethodSource("disableDataFormatCustomizationProperties") + public void testDataFormatCustomizationDisabledByProperty(Properties properties) { + AtomicInteger counter = new AtomicInteger(); + + DefaultCamelContext context = new DefaultCamelContext(); + context.getPropertiesComponent().setInitialProperties(properties); + + context.getRegistry().bind( + "customizer-filter", + new CustomizersSupport.DataFormatCustomizationEnabledPolicy()); + context.getRegistry().bind( + "my-df", + (DataFormatFactory) MyDataFormat::new); + context.getRegistry().bind( + "my-df-customizer", + DataFormatCustomizer.forType(MyDataFormat.class, target -> target.setId(counter.incrementAndGet()))); + + DataFormat df1 = context.resolveDataFormat("my-df"); + assertEquals(0, ((MyDataFormat) df1).getId()); + } + + @ParameterizedTest + @MethodSource("enableDataFormatCustomizationProperties") + public void testDataFormatCustomizationEnabledByProperty(Properties properties) { + DefaultCamelContext context = new DefaultCamelContext(); + context.getPropertiesComponent().setInitialProperties(properties); + + context.getRegistry().bind( + "customizer-filter", + new CustomizersSupport.DataFormatCustomizationEnabledPolicy()); + context.getRegistry().bind( + "my-df", + (DataFormatFactory) MyDataFormat::new); + context.getRegistry().bind( + "my-df-customizer", + DataFormatCustomizer.forType(MyDataFormat.class, target -> target.setId(1))); + + DataFormat df1 = context.resolveDataFormat("my-df"); + assertEquals(1, ((MyDataFormat) df1).getId()); + } + + // ***************************** + // + // Language + // + // ***************************** + + @Test + public void testLanguageCustomizationFromRegistry() { + AtomicInteger counter = new AtomicInteger(); + + DefaultCamelContext context = new DefaultCamelContext(); + context.getRegistry().bind( + "tokenize", + new TokenizeLanguage()); + context.getRegistry().bind( + "tokenize-customizer", + LanguageCustomizer.forType(TokenizeLanguage.class, target -> target.setGroup("" + counter.incrementAndGet()))); + + Language l1 = context.resolveLanguage("tokenize"); + assertTrue(l1 instanceof TokenizeLanguage); + assertEquals("1", ((TokenizeLanguage) l1).getGroup()); + + Language l2 = context.resolveLanguage("tokenize"); + assertTrue(l2 instanceof TokenizeLanguage); + assertEquals("1", ((TokenizeLanguage) l2).getGroup()); + + // as the language is resolved via the registry, then the instance is the same + // even if it is not a singleton + assertSame(l1, l2); + } + + @Test + public void testLanguageCustomizationFromResource() { + AtomicInteger counter = new AtomicInteger(); + + DefaultCamelContext context = new DefaultCamelContext(); + context.getRegistry().bind( + "tokenize-customizer", + LanguageCustomizer.forType(TokenizeLanguage.class, target -> target.setGroup("" + counter.incrementAndGet()))); + + Language l1 = context.resolveLanguage("tokenize"); + assertTrue(l1 instanceof TokenizeLanguage); + assertEquals("1", ((TokenizeLanguage) l1).getGroup()); + + Language l2 = context.resolveLanguage("tokenize"); + assertTrue(l2 instanceof TokenizeLanguage); + assertEquals("2", ((TokenizeLanguage) l2).getGroup()); + + assertNotSame(l1, l2); + } + + @Test + public void testLanguageCustomizationFromResourceWithFilter() { + AtomicInteger counter = new AtomicInteger(); + + DefaultCamelContext context = new DefaultCamelContext(); + context.getRegistry().bind( + "customizer-filter", + LanguageCustomizer.Policy.none()); + context.getRegistry().bind( + "tokenize-customizer", + LanguageCustomizer.forType(TokenizeLanguage.class, target -> target.setGroup("" + counter.incrementAndGet()))); + + Language l1 = context.resolveLanguage("tokenize"); + assertTrue(l1 instanceof TokenizeLanguage); + assertNull(((TokenizeLanguage) l1).getGroup()); + + Language l2 = context.resolveLanguage("tokenize"); + assertTrue(l2 instanceof TokenizeLanguage); + assertNull(((TokenizeLanguage) l2).getGroup()); + + assertNotSame(l1, l2); + } + + @ParameterizedTest + @MethodSource("disableLanguageCustomizationProperties") + public void testLanguageCustomizationDisabledByProperty(Properties initialProperties) { + DefaultCamelContext context = new DefaultCamelContext(); + context.getPropertiesComponent().setInitialProperties(initialProperties); + + context.getRegistry().bind( + "customizer-filter", + new CustomizersSupport.LanguageCustomizationEnabledPolicy()); + context.getRegistry().bind( + "tokenize-customizer", + LanguageCustomizer.forType(TokenizeLanguage.class, target -> target.setGroup("something"))); + + assertNotEquals("something", ((TokenizeLanguage) context.resolveLanguage("tokenize")).getGroup()); + } + + @ParameterizedTest + @MethodSource("enableLanguageCustomizationProperties") + public void testLanguageCustomizationEnabledByProperty(Properties initialProperties) { + DefaultCamelContext context = new DefaultCamelContext(); + context.getPropertiesComponent().setInitialProperties(initialProperties); + + context.getRegistry().bind( + "customizer-filter", + new CustomizersSupport.LanguageCustomizationEnabledPolicy()); + context.getRegistry().bind( + "tokenize-customizer", + LanguageCustomizer.forType(TokenizeLanguage.class, target -> target.setGroup("something"))); + + assertEquals("something", ((TokenizeLanguage) context.resolveLanguage("tokenize")).getGroup()); + } + + // ***************************** + // + // Model + // + // ***************************** + + public static class MyDataFormat implements DataFormat { + private int id; + + @Override + public void marshal(Exchange exchange, Object graph, OutputStream stream) throws Exception { + } + + @Override + public Object unmarshal(Exchange exchange, InputStream stream) throws Exception { + return null; + } + + @Override + public void start() { + } + + @Override + public void stop() { + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + } + + public static class MyExchangeFormatter extends DefaultExchangeFormatter { + } +} diff --git a/core/camel-main/src/test/java/org/apache/camel/main/MainCustomizerTest.java b/core/camel-main/src/test/java/org/apache/camel/main/MainCustomizerTest.java new file mode 100644 index 0000000..c4c8031 --- /dev/null +++ b/core/camel-main/src/test/java/org/apache/camel/main/MainCustomizerTest.java @@ -0,0 +1,99 @@ +/* + * 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.main; + +import org.apache.camel.BindToRegistry; +import org.apache.camel.component.log.LogComponent; +import org.apache.camel.spi.ComponentCustomizer; +import org.apache.camel.support.CustomizersSupport; +import org.apache.camel.support.processor.DefaultExchangeFormatter; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MainCustomizerTest { + @Test + public void testComponentCustomizer() { + Main main = new Main(); + + try { + main.configure().addConfigurationClass(MyConfiguration.class); + main.start(); + + LogComponent component = main.getCamelContext().getComponent("log", LogComponent.class); + assertTrue(component.getExchangeFormatter() instanceof MyFormatter); + } finally { + main.stop(); + } + } + + @Test + public void testComponentCustomizerDisabledWithPolicy() { + Main main = new Main(); + + try { + main.bind("my-filter", ComponentCustomizer.Policy.none()); + main.configure().addConfigurationClass(MyConfiguration.class); + main.start(); + + LogComponent component = main.getCamelContext().getComponent("log", LogComponent.class); + assertFalse(component.getExchangeFormatter() instanceof MyFormatter); + } finally { + main.stop(); + } + } + + @Test + public void testComponentCustomizerDisabledWithProperties() { + Main main = new Main(); + + try { + main.addInitialProperty("camel.customizer.component.log.enabled", "false"); + main.bind("my-filter", ComponentCustomizer.Policy.any()); + main.configure().addConfigurationClass(MyConfiguration.class); + main.start(); + + LogComponent component = main.getCamelContext().getComponent("log", LogComponent.class); + assertFalse(component.getExchangeFormatter() instanceof MyFormatter); + } finally { + main.stop(); + } + } + + // **************************** + // + // Helpers + // + // **************************** + + public static class MyConfiguration { + @BindToRegistry + public ComponentCustomizer logCustomizer() { + return ComponentCustomizer.builder(LogComponent.class) + .build(component -> component.setExchangeFormatter(new MyFormatter())); + } + + @BindToRegistry + public ComponentCustomizer.Policy componentCustomizationPolicy() { + return new CustomizersSupport.ComponentCustomizationEnabledPolicy(); + } + } + + public static class MyFormatter extends DefaultExchangeFormatter { + } +} diff --git a/core/camel-support/src/main/java/org/apache/camel/support/CustomizersSupport.java b/core/camel-support/src/main/java/org/apache/camel/support/CustomizersSupport.java new file mode 100644 index 0000000..6f28ad8 --- /dev/null +++ b/core/camel-support/src/main/java/org/apache/camel/support/CustomizersSupport.java @@ -0,0 +1,129 @@ +/* + * 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 org.apache.camel.CamelContext; +import org.apache.camel.CamelContextAware; +import org.apache.camel.Component; +import org.apache.camel.spi.ComponentCustomizer; +import org.apache.camel.spi.DataFormat; +import org.apache.camel.spi.DataFormatCustomizer; +import org.apache.camel.spi.Language; +import org.apache.camel.spi.LanguageCustomizer; + +public final class CustomizersSupport { + private CustomizersSupport() { + } + + /** + * Determine the value of the "enabled" flag for a hierarchy of properties. + * + * @param camelContext the {@link CamelContext} + * @param prefixes an ordered list of prefixed (less restrictive to more restrictive) + * @return the value of the key `enabled` for most restrictive prefix + */ + public static boolean isEnabled(CamelContext camelContext, String... prefixes) { + boolean answer = true; + + // Loop over all the prefixes to find out the value of the key `enabled` + // for the most restrictive prefix. + for (String prefix : prefixes) { + String property = prefix.endsWith(".") ? prefix + "enabled" : prefix + ".enabled"; + + // evaluate the value of the current prefix using the parent one as + // default value so if the `enabled` property is not set, the parent + // one is used. + answer = camelContext.getPropertiesComponent() + .resolveProperty(property) + .map(Boolean::valueOf) + .orElse(answer); + } + + return answer; + } + + /** + * Base class for policies + */ + private static class CamelContextAwarePolicy implements CamelContextAware { + private CamelContext camelContext; + + @Override + public CamelContext getCamelContext() { + return this.camelContext; + } + + @Override + public void setCamelContext(CamelContext camelContext) { + this.camelContext = camelContext; + } + } + + /** + * A {@link ComponentCustomizer.Policy} that uses a hierarchical lists of properties to determine if customization + * is enabled for the given {@link org.apache.camel.Component}. + */ + public static final class ComponentCustomizationEnabledPolicy + extends CamelContextAwarePolicy + implements ComponentCustomizer.Policy { + + @Override + public boolean test(String name, Component target) { + return isEnabled( + getCamelContext(), + "camel.customizer", + "camel.customizer.component", + "camel.customizer.component." + name); + } + } + + /** + * A {@link DataFormatCustomizer.Policy} that uses a hierarchical lists of properties to determine if customization + * is enabled for the given {@link org.apache.camel.spi.DataFormat}. + */ + public static final class DataFormatCustomizationEnabledPolicy + extends CamelContextAwarePolicy + implements DataFormatCustomizer.Policy { + + @Override + public boolean test(String name, DataFormat target) { + return isEnabled( + getCamelContext(), + "camel.customizer", + "camel.customizer.dataformat", + "camel.customizer.dataformat." + name); + } + } + + /** + * A {@link LanguageCustomizer.Policy} that uses a hierarchical lists of properties to determine if customization is + * enabled for the given {@link org.apache.camel.spi.Language}. + */ + public static final class LanguageCustomizationEnabledPolicy + extends CamelContextAwarePolicy + implements LanguageCustomizer.Policy { + + @Override + public boolean test(String name, Language target) { + return isEnabled( + getCamelContext(), + "camel.customizer", + "camel.customizer.language", + "camel.customizer.language." + name); + } + } +} 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 fc896bc..5fca48b 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 @@ -34,13 +34,11 @@ import java.util.Set; import java.util.TreeMap; import org.apache.camel.CamelContext; -import org.apache.camel.Component; import org.apache.camel.ExtendedCamelContext; import org.apache.camel.PropertyBindingException; import org.apache.camel.spi.BeanIntrospection; import org.apache.camel.spi.PropertyConfigurer; import org.apache.camel.spi.PropertyConfigurerGetter; -import org.apache.camel.support.service.ServiceHelper; import org.apache.camel.util.StringHelper; import org.apache.camel.util.StringQuoteHelper; @@ -88,233 +86,6 @@ import static org.apache.camel.util.StringHelper.startsWithIgnoreCase; */ public final class PropertyBindingSupport { - /** - * To use a fluent builder style to configure this property binding support. - */ - public static class Builder { - - private CamelContext camelContext; - 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; - private boolean reference = true; - private boolean placeholder = true; - private boolean fluentBuilder = true; - private boolean allowPrivateSetter = true; - private boolean ignoreCase; - private String optionPrefix; - private boolean reflection = true; - private PropertyConfigurer configurer; - - /** - * CamelContext to be used - */ - public Builder withCamelContext(CamelContext camelContext) { - this.camelContext = camelContext; - return this; - } - - /** - * Target object that should have parameters bound - */ - public Builder withTarget(Object target) { - this.target = target; - return this; - } - - /** - * The properties to use for binding - */ - public Builder withProperties(Map<String, Object> properties) { - if (this.properties == null) { - this.properties = properties; - } else { - // there may be existing options so add those if missing - // we need to mutate existing as we are may be removing bound properties - this.properties.forEach(properties::putIfAbsent); - this.properties = properties; - } - return this; - } - - /** - * Adds property to use for binding - */ - public Builder withProperty(String key, Object value) { - if (this.properties == null) { - this.properties = new LinkedHashMap<>(); - } - this.properties.put(key, value); - return this; - } - - /** - * Whether parameters should be removed when its bound - */ - public Builder withRemoveParameters(boolean removeParameters) { - this.removeParameters = removeParameters; - return this; - } - - /** - * 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) { - this.mandatory = mandatory; - return this; - } - - /** - * Whether nesting is in use - */ - public Builder withNesting(boolean nesting) { - this.nesting = nesting; - return this; - } - - /** - * Whether deep nesting is in use, where Camel will attempt to walk as deep as possible by creating new objects - * in the OGNL graph if a property has a setter and the object can be created from a default no-arg constructor. - */ - public Builder withDeepNesting(boolean deepNesting) { - this.deepNesting = deepNesting; - return this; - } - - /** - * Whether reference parameter (syntax starts with #) is in use - */ - public Builder withReference(boolean reference) { - this.reference = reference; - return this; - } - - /** - * Whether to use Camels property placeholder to resolve placeholders on keys and values - */ - public Builder withPlaceholder(boolean placeholder) { - this.placeholder = placeholder; - return this; - } - - /** - * Whether fluent builder is allowed as a valid getter/setter - */ - public Builder withFluentBuilder(boolean fluentBuilder) { - this.fluentBuilder = fluentBuilder; - return this; - } - - /** - * Whether properties should be filtered by prefix. * Note that the prefix is removed from the key before the - * property is bound. - */ - public Builder withAllowPrivateSetter(boolean allowPrivateSetter) { - this.allowPrivateSetter = allowPrivateSetter; - return this; - } - - /** - * Whether to ignore case in the property names (keys). - */ - public Builder withIgnoreCase(boolean ignoreCase) { - this.ignoreCase = ignoreCase; - return this; - } - - /** - * Whether properties should be filtered by prefix. Note that the prefix is removed from the key before the - * property is bound. - */ - public Builder withOptionPrefix(String optionPrefix) { - this.optionPrefix = optionPrefix; - return this; - } - - /** - * Whether to use the configurer to configure the properties. - */ - public Builder withConfigurer(PropertyConfigurer configurer) { - this.configurer = configurer; - return this; - } - - /** - * Whether to allow using reflection (when there is no configurer available). - */ - public Builder withReflection(boolean reflection) { - this.reflection = reflection; - return this; - } - - /** - * Binds the properties to the target object, and removes the property that was bound from properties. - * - * @return true if one or more properties was bound - */ - public boolean bind() { - // mandatory parameters - org.apache.camel.util.ObjectHelper.notNull(camelContext, "camelContext"); - org.apache.camel.util.ObjectHelper.notNull(target, "target"); - - if (properties == null || properties.isEmpty()) { - return false; - } - - return doBindProperties(camelContext, target, removeParameters ? properties : new HashMap<>(properties), - optionPrefix, ignoreCase, removeParameters, flattenProperties, mandatory, - nesting, deepNesting, fluentBuilder, allowPrivateSetter, reference, placeholder, reflection, configurer); - } - - /** - * Binds the properties to the target object, and removes the property that was bound from properties. - * - * @param camelContext the camel context - * @param target the target object - * @param properties the properties where the bound properties will be removed from - * @return true if one or more properties was bound - */ - public boolean bind(CamelContext camelContext, Object target, Map<String, Object> properties) { - CamelContext context = camelContext != null ? camelContext : this.camelContext; - Object obj = target != null ? target : this.target; - Map<String, Object> prop = properties != null ? properties : this.properties; - - return doBindProperties(context, obj, removeParameters ? prop : new HashMap<>(prop), - optionPrefix, ignoreCase, removeParameters, flattenProperties, mandatory, - nesting, deepNesting, fluentBuilder, allowPrivateSetter, reference, placeholder, reflection, configurer); - } - - /** - * Binds the property to the target object. - * - * @param camelContext the camel context - * @param target the target object - * @param key the property key - * @param value the property value - * @return true if the property was bound - */ - public boolean bind(CamelContext camelContext, Object target, String key, Object value) { - Map<String, Object> properties = new HashMap<>(1); - properties.put(key, value); - - return doBindProperties(camelContext, target, properties, optionPrefix, ignoreCase, true, false, mandatory, - nesting, deepNesting, fluentBuilder, allowPrivateSetter, reference, placeholder, reflection, configurer); - } - - } - private PropertyBindingSupport() { } @@ -322,21 +93,6 @@ public final class PropertyBindingSupport { return new Builder(); } - @FunctionalInterface - public interface OnAutowiring { - - /** - * Callback when a property was autowired on a bean - * - * @param target the targeted bean - * @param propertyName the name of the property - * @param propertyType the type of the property - * @param value the property value - */ - void onAutowire(Object target, String propertyName, Class propertyType, Object value); - - } - /** * This will discover all the properties on the target, and automatic bind the properties that are null by looking * up in the registry to see if there is a single instance of the same type as the property. This is used for @@ -391,38 +147,18 @@ public final class PropertyBindingSupport { Map<String, Object> properties = new LinkedHashMap<>(); // if there a configurer - PropertyConfigurer configurer = null; - PropertyConfigurerGetter getter; - if (target instanceof Component) { - // the component needs to be initialized to have the configurer ready - ServiceHelper.initService(target); - configurer = ((Component) target).getComponentPropertyConfigurer(); - } - if (configurer == null) { - String name = target.getClass().getName(); - if (target instanceof ExtendedCamelContext) { - // special for camel context itself as we have an extended configurer - name = ExtendedCamelContext.class.getName(); - } - - if (isNotEmpty(name)) { - // see if there is a configurer for it - configurer = camelContext.adapt(ExtendedCamelContext.class).getConfigurerResolver() - .resolvePropertyConfigurer(name, camelContext); - } - } + PropertyConfigurer configurer = PropertyConfigurerHelper.resolvePropertyConfigurer(camelContext, target); // use configurer to get all the current options and its values Map<String, Object> getterAllOption = null; if (configurer instanceof PropertyConfigurerGetter) { - getter = (PropertyConfigurerGetter) configurer; - final PropertyConfigurerGetter lambdaGetter = getter; + final PropertyConfigurerGetter getter = (PropertyConfigurerGetter) configurer; final Object lambdaTarget = target; getterAllOption = getter.getAllOptions(target); getterAllOption.forEach((key, type) -> { // we only need the complex types if (isComplexUserType((Class) type)) { - Object value = lambdaGetter.getOptionValue(lambdaTarget, key, true); + Object value = getter.getOptionValue(lambdaTarget, key, true); properties.put(key, value); } }); @@ -570,139 +306,6 @@ public final class PropertyBindingSupport { } /** - * Used for making it easier to support using option prefix in property binding and to remove the bound properties - * from the input map. - */ - private static class OptionPrefixMap extends LinkedHashMap<String, Object> { - - private final String optionPrefix; - private final Map<String, Object> originalMap; - - public OptionPrefixMap(Map<String, Object> map, String optionPrefix) { - this.originalMap = map; - this.optionPrefix = optionPrefix; - // copy from original map into our map without the option prefix - map.forEach((k, v) -> { - if (startsWithIgnoreCase(k, optionPrefix)) { - put(k.substring(optionPrefix.length()), v); - } else if (startsWithIgnoreCase(k, "?" + optionPrefix)) { - put(k.substring(optionPrefix.length() + 1), v); - } - }); - } - - @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 - Set<String> toBeRemoved = new HashSet<>(); - originalMap.forEach((k, v) -> { - if (startsWithIgnoreCase(k, optionPrefix)) { - toBeRemoved.add(k); - } else if (startsWithIgnoreCase(k, "?" + optionPrefix)) { - toBeRemoved.add(k); - } - }); - toBeRemoved.forEach(originalMap::remove); - - return super.remove(key); - } - - } - - /** - * 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. - */ - private static final class PropertyBindingKeyComparator implements Comparator<String> { - - private final Map<String, Object> map; - - private PropertyBindingKeyComparator(Map<String, Object> map) { - this.map = map; - } - - @Override - public int compare(String o1, String o2) { - // 1) sort by nested level (shortest OGNL graph first) - int n1 = StringHelper.countChar(o1, '.'); - int n2 = StringHelper.countChar(o2, '.'); - if (n1 != n2) { - return Integer.compare(n1, n2); - } - // 2) sort by reference (as it may refer to other beans in the OGNL graph) - Object v1 = map.get(o1); - Object v2 = map.get(o2); - boolean ref1 = v1 instanceof String && ((String) v1).startsWith("#"); - boolean ref2 = v2 instanceof String && ((String) v2).startsWith("#"); - if (ref1 != ref2) { - return Boolean.compare(ref1, ref2); - } - // 3) sort by name - return o1.compareTo(o2); - } - } - - /** * Binds the properties with the given prefix to the target object, and removes the property that was bound from * properties. Note that the prefix is removed from the key before the property is bound. * @@ -798,8 +401,7 @@ public final class PropertyBindingSupport { if (configurer == null) { // do we have a configurer by any chance - configurer = camelContext.adapt(ExtendedCamelContext.class).getConfigurerResolver() - .resolvePropertyConfigurer(newClass.getName(), camelContext); + configurer = PropertyConfigurerHelper.resolvePropertyConfigurer(camelContext, newClass); } // we should only walk and create OGNL path for the middle graph @@ -862,11 +464,9 @@ public final class PropertyBindingSupport { Class<?> collectionType = (Class<?>) ((PropertyConfigurerGetter) configurer) .getCollectionValueType(newTarget, undashKey(key), ignoreCase); if (collectionType != null) { - configurer = camelContext.adapt(ExtendedCamelContext.class).getConfigurerResolver() - .resolvePropertyConfigurer(collectionType.getName(), camelContext); + configurer = PropertyConfigurerHelper.resolvePropertyConfigurer(camelContext, collectionType); } else { - configurer = camelContext.adapt(ExtendedCamelContext.class).getConfigurerResolver() - .resolvePropertyConfigurer(prop.getClass().getName(), camelContext); + configurer = PropertyConfigurerHelper.resolvePropertyConfigurer(camelContext, prop.getClass()); } } // prepare for next iterator @@ -2052,4 +1652,379 @@ public final class PropertyBindingSupport { return key; } + @FunctionalInterface + public interface OnAutowiring { + + /** + * Callback when a property was autowired on a bean + * + * @param target the targeted bean + * @param propertyName the name of the property + * @param propertyType the type of the property + * @param value the property value + */ + void onAutowire(Object target, String propertyName, Class propertyType, Object value); + + } + + /** + * To use a fluent builder style to configure this property binding support. + */ + public static class Builder { + + private CamelContext camelContext; + 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; + private boolean reference = true; + private boolean placeholder = true; + private boolean fluentBuilder = true; + private boolean allowPrivateSetter = true; + private boolean ignoreCase; + private String optionPrefix; + private boolean reflection = true; + private PropertyConfigurer configurer; + + /** + * CamelContext to be used + */ + public Builder withCamelContext(CamelContext camelContext) { + this.camelContext = camelContext; + return this; + } + + /** + * Target object that should have parameters bound + */ + public Builder withTarget(Object target) { + this.target = target; + return this; + } + + /** + * The properties to use for binding + */ + public Builder withProperties(Map<String, Object> properties) { + if (this.properties == null) { + this.properties = properties; + } else { + // there may be existing options so add those if missing + // we need to mutate existing as we are may be removing bound properties + this.properties.forEach(properties::putIfAbsent); + this.properties = properties; + } + return this; + } + + /** + * Adds property to use for binding + */ + public Builder withProperty(String key, Object value) { + if (this.properties == null) { + this.properties = new LinkedHashMap<>(); + } + this.properties.put(key, value); + return this; + } + + /** + * Whether parameters should be removed when its bound + */ + public Builder withRemoveParameters(boolean removeParameters) { + this.removeParameters = removeParameters; + return this; + } + + /** + * 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) { + this.mandatory = mandatory; + return this; + } + + /** + * Whether nesting is in use + */ + public Builder withNesting(boolean nesting) { + this.nesting = nesting; + return this; + } + + /** + * Whether deep nesting is in use, where Camel will attempt to walk as deep as possible by creating new objects + * in the OGNL graph if a property has a setter and the object can be created from a default no-arg constructor. + */ + public Builder withDeepNesting(boolean deepNesting) { + this.deepNesting = deepNesting; + return this; + } + + /** + * Whether reference parameter (syntax starts with #) is in use + */ + public Builder withReference(boolean reference) { + this.reference = reference; + return this; + } + + /** + * Whether to use Camels property placeholder to resolve placeholders on keys and values + */ + public Builder withPlaceholder(boolean placeholder) { + this.placeholder = placeholder; + return this; + } + + /** + * Whether fluent builder is allowed as a valid getter/setter + */ + public Builder withFluentBuilder(boolean fluentBuilder) { + this.fluentBuilder = fluentBuilder; + return this; + } + + /** + * Whether properties should be filtered by prefix. * Note that the prefix is removed from the key before the + * property is bound. + */ + public Builder withAllowPrivateSetter(boolean allowPrivateSetter) { + this.allowPrivateSetter = allowPrivateSetter; + return this; + } + + /** + * Whether to ignore case in the property names (keys). + */ + public Builder withIgnoreCase(boolean ignoreCase) { + this.ignoreCase = ignoreCase; + return this; + } + + /** + * Whether properties should be filtered by prefix. Note that the prefix is removed from the key before the + * property is bound. + */ + public Builder withOptionPrefix(String optionPrefix) { + this.optionPrefix = optionPrefix; + return this; + } + + /** + * Whether to use the configurer to configure the properties. + */ + public Builder withConfigurer(PropertyConfigurer configurer) { + this.configurer = configurer; + return this; + } + + /** + * Whether to allow using reflection (when there is no configurer available). + */ + public Builder withReflection(boolean reflection) { + this.reflection = reflection; + return this; + } + + /** + * Binds the properties to the target object, and removes the property that was bound from properties. + * + * @return true if one or more properties was bound + */ + public boolean bind() { + // mandatory parameters + org.apache.camel.util.ObjectHelper.notNull(camelContext, "camelContext"); + org.apache.camel.util.ObjectHelper.notNull(target, "target"); + + if (properties == null || properties.isEmpty()) { + return false; + } + + return doBindProperties(camelContext, target, removeParameters ? properties : new HashMap<>(properties), + optionPrefix, ignoreCase, removeParameters, flattenProperties, mandatory, + nesting, deepNesting, fluentBuilder, allowPrivateSetter, reference, placeholder, reflection, configurer); + } + + /** + * Binds the properties to the target object, and removes the property that was bound from properties. + * + * @param camelContext the camel context + * @param target the target object + * @param properties the properties where the bound properties will be removed from + * @return true if one or more properties was bound + */ + public boolean bind(CamelContext camelContext, Object target, Map<String, Object> properties) { + CamelContext context = camelContext != null ? camelContext : this.camelContext; + Object obj = target != null ? target : this.target; + Map<String, Object> prop = properties != null ? properties : this.properties; + + return doBindProperties(context, obj, removeParameters ? prop : new HashMap<>(prop), + optionPrefix, ignoreCase, removeParameters, flattenProperties, mandatory, + nesting, deepNesting, fluentBuilder, allowPrivateSetter, reference, placeholder, reflection, configurer); + } + + /** + * Binds the property to the target object. + * + * @param camelContext the camel context + * @param target the target object + * @param key the property key + * @param value the property value + * @return true if the property was bound + */ + public boolean bind(CamelContext camelContext, Object target, String key, Object value) { + Map<String, Object> properties = new HashMap<>(1); + properties.put(key, value); + + return doBindProperties(camelContext, target, properties, optionPrefix, ignoreCase, true, false, mandatory, + nesting, deepNesting, fluentBuilder, allowPrivateSetter, reference, placeholder, reflection, configurer); + } + + } + + /** + * Used for making it easier to support using option prefix in property binding and to remove the bound properties + * from the input map. + */ + private static class OptionPrefixMap extends LinkedHashMap<String, Object> { + + private final String optionPrefix; + private final Map<String, Object> originalMap; + + public OptionPrefixMap(Map<String, Object> map, String optionPrefix) { + this.originalMap = map; + this.optionPrefix = optionPrefix; + // copy from original map into our map without the option prefix + map.forEach((k, v) -> { + if (startsWithIgnoreCase(k, optionPrefix)) { + put(k.substring(optionPrefix.length()), v); + } else if (startsWithIgnoreCase(k, "?" + optionPrefix)) { + put(k.substring(optionPrefix.length() + 1), v); + } + }); + } + + @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 + Set<String> toBeRemoved = new HashSet<>(); + originalMap.forEach((k, v) -> { + if (startsWithIgnoreCase(k, optionPrefix)) { + toBeRemoved.add(k); + } else if (startsWithIgnoreCase(k, "?" + optionPrefix)) { + toBeRemoved.add(k); + } + }); + toBeRemoved.forEach(originalMap::remove); + + return super.remove(key); + } + + } + + /** + * 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. + */ + private static final class PropertyBindingKeyComparator implements Comparator<String> { + + private final Map<String, Object> map; + + private PropertyBindingKeyComparator(Map<String, Object> map) { + this.map = map; + } + + @Override + public int compare(String o1, String o2) { + // 1) sort by nested level (shortest OGNL graph first) + int n1 = StringHelper.countChar(o1, '.'); + int n2 = StringHelper.countChar(o2, '.'); + if (n1 != n2) { + return Integer.compare(n1, n2); + } + // 2) sort by reference (as it may refer to other beans in the OGNL graph) + Object v1 = map.get(o1); + Object v2 = map.get(o2); + boolean ref1 = v1 instanceof String && ((String) v1).startsWith("#"); + boolean ref2 = v2 instanceof String && ((String) v2).startsWith("#"); + if (ref1 != ref2) { + return Boolean.compare(ref1, ref2); + } + // 3) sort by name + return o1.compareTo(o2); + } + } + } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/PropertyConfigurerHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/PropertyConfigurerHelper.java index 97b7ee2..b04397c 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/PropertyConfigurerHelper.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/PropertyConfigurerHelper.java @@ -17,8 +17,10 @@ package org.apache.camel.support; import org.apache.camel.CamelContext; +import org.apache.camel.Component; import org.apache.camel.ExtendedCamelContext; -import org.apache.camel.spi.GeneratedPropertyConfigurer; +import org.apache.camel.spi.PropertyConfigurer; +import org.apache.camel.support.service.ServiceHelper; import org.apache.camel.util.ObjectHelper; /** @@ -39,13 +41,55 @@ public final class PropertyConfigurerHelper { * @param target the target object for which we need a {@link org.apache.camel.spi.PropertyConfigurer} * @return the resolved configurer, or <tt>null</tt> if no configurer could be found */ - public static GeneratedPropertyConfigurer resolvePropertyConfigurer(CamelContext context, Object target) { + public static PropertyConfigurer resolvePropertyConfigurer(CamelContext context, Object target) { ObjectHelper.notNull(target, "target"); ObjectHelper.notNull(context, "context"); + PropertyConfigurer configurer = null; + + if (target instanceof Component) { + // the component needs to be initialized to have the configurer ready + ServiceHelper.initService(target); + configurer = ((Component) target).getComponentPropertyConfigurer(); + } + + if (configurer == null) { + String name = target.getClass().getName(); + if (target instanceof ExtendedCamelContext) { + // special for camel context itself as we have an extended configurer + name = ExtendedCamelContext.class.getName(); + } + + // see if there is a configurer for it + configurer = context.adapt(ExtendedCamelContext.class) + .getConfigurerResolver() + .resolvePropertyConfigurer(name, context); + } + + return configurer; + } + + /** + * Resolves the given configurer. + * + * @param context the camel context + * @param targetType the target object type for which we need a {@link org.apache.camel.spi.PropertyConfigurer} + * @return the resolved configurer, or <tt>null</tt> if no configurer could be found + */ + public static PropertyConfigurer resolvePropertyConfigurer(CamelContext context, Class<?> targetType) { + ObjectHelper.notNull(targetType, "targetType"); + ObjectHelper.notNull(context, "context"); + + String name = targetType.getName(); + if (ExtendedCamelContext.class.isAssignableFrom(targetType)) { + // special for camel context itself as we have an extended configurer + name = ExtendedCamelContext.class.getName(); + } + + // see if there is a configurer for it return context.adapt(ExtendedCamelContext.class) .getConfigurerResolver() - .resolvePropertyConfigurer(target.getClass().getName(), context); + .resolvePropertyConfigurer(name, context); } /** @@ -60,7 +104,7 @@ public final class PropertyConfigurerHelper { ObjectHelper.notNull(target, "target"); ObjectHelper.notNull(context, "context"); - GeneratedPropertyConfigurer configurer = resolvePropertyConfigurer(context, target); + PropertyConfigurer configurer = resolvePropertyConfigurer(context, target); if (type.isInstance(configurer)) { return type.cast(configurer); } diff --git a/core/camel-util/src/main/java/org/apache/camel/util/CollectionHelper.java b/core/camel-util/src/main/java/org/apache/camel/util/CollectionHelper.java index 39ccc3d..89dc186 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/CollectionHelper.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/CollectionHelper.java @@ -164,6 +164,7 @@ public final class CollectionHelper { /** * Build a map from varargs. */ + @SuppressWarnings("unchecked") public static <K, V> Map<K, V> mapOf(Supplier<Map<K, V>> creator, K key, V value, Object... keyVals) { Map<K, V> map = creator.get(); map.put(key, value);