, ApplicationContextAware, MessageSourceAware, InitializingBean {
+
+ private static final char[] EXACT_DELIMITERS = { '_', '.', '[' };
+
+ private static final char[] TARGET_NAME_DELIMITERS = { '_', '.' };
+
+ private static final Log logger = LogFactory.getLog(PropertiesConfigurationFactory.class);
+
+ private boolean ignoreUnknownFields = true;
+
+ private boolean ignoreInvalidFields;
+
+ private boolean exceptionIfInvalid = true;
+
+ private PropertySources propertySources;
+
+ private final T target;
+
+ private Validator validator;
+
+ private ApplicationContext applicationContext;
+
+ private MessageSource messageSource;
+
+ private boolean hasBeenBound = false;
+
+ private boolean ignoreNestedProperties = false;
+
+ private String targetName;
+
+ private ConversionService conversionService;
+
+ private boolean resolvePlaceholders = true;
+
+ /**
+ * Create a new {@link PropertiesConfigurationFactory} instance.
+ *
+ * @param target the target object to bind too
+ * @see #PropertiesConfigurationFactory(Class)
+ */
+ public PropertiesConfigurationFactory(T target){
+ Assert.notNull(target, "target must not be null");
+ this.target = target;
+ }
+
+ /**
+ * Create a new {@link PropertiesConfigurationFactory} instance.
+ *
+ * @param type the target type
+ * @see #PropertiesConfigurationFactory(Class)
+ */
+ @SuppressWarnings("unchecked")
+ public PropertiesConfigurationFactory(Class> type){
+ Assert.notNull(type, "type must not be null");
+ this.target = (T) BeanUtils.instantiate(type);
+ }
+
+ /**
+ * Flag to disable binding of nested properties (i.e. those with period
+ * separators in their paths). Can be useful to disable this if the name prefix
+ * is empty and you don't want to ignore unknown fields.
+ *
+ * @param ignoreNestedProperties the flag to set (default false)
+ */
+ public void setIgnoreNestedProperties(boolean ignoreNestedProperties) {
+ this.ignoreNestedProperties = ignoreNestedProperties;
+ }
+
+ /**
+ * Set whether to ignore unknown fields, that is, whether to ignore bind
+ * parameters that do not have corresponding fields in the target object.
+ *
+ * Default is "true". Turn this off to enforce that all bind parameters must
+ * have a matching field in the target object.
+ *
+ * @param ignoreUnknownFields if unknown fields should be ignored
+ */
+ public void setIgnoreUnknownFields(boolean ignoreUnknownFields) {
+ this.ignoreUnknownFields = ignoreUnknownFields;
+ }
+
+ /**
+ * Set whether to ignore invalid fields, that is, whether to ignore bind
+ * parameters that have corresponding fields in the target object which are not
+ * accessible (for example because of null values in the nested path).
+ *
+ * Default is "false". Turn this on to ignore bind parameters for nested objects
+ * in non-existing parts of the target object graph.
+ *
+ * @param ignoreInvalidFields if invalid fields should be ignored
+ */
+ public void setIgnoreInvalidFields(boolean ignoreInvalidFields) {
+ this.ignoreInvalidFields = ignoreInvalidFields;
+ }
+
+ /**
+ * Set the target name.
+ *
+ * @param targetName the target name
+ */
+ public void setTargetName(String targetName) {
+ this.targetName = targetName;
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
+ }
+
+ /**
+ * Set the message source.
+ *
+ * @param messageSource the message source
+ */
+ @Override
+ public void setMessageSource(MessageSource messageSource) {
+ this.messageSource = messageSource;
+ }
+
+ /**
+ * Set the property sources.
+ *
+ * @param propertySources the property sources
+ */
+ public void setPropertySources(PropertySources propertySources) {
+ this.propertySources = propertySources;
+ }
+
+ /**
+ * Set the conversion service.
+ *
+ * @param conversionService the conversion service
+ */
+ public void setConversionService(ConversionService conversionService) {
+ this.conversionService = conversionService;
+ }
+
+ /**
+ * Set the validator.
+ *
+ * @param validator the validator
+ */
+ public void setValidator(Validator validator) {
+ this.validator = validator;
+ }
+
+ /**
+ * Set a flag to indicate that an exception should be raised if a Validator is
+ * available and validation fails.
+ *
+ * @param exceptionIfInvalid the flag to set
+ * @deprecated as of 1.5, do not specify a {@link Validator} if validation
+ * should not occur
+ */
+ @Deprecated
+ public void setExceptionIfInvalid(boolean exceptionIfInvalid) {
+ this.exceptionIfInvalid = exceptionIfInvalid;
+ }
+
+ /**
+ * Flag to indicate that placeholders should be replaced during binding. Default
+ * is true.
+ *
+ * @param resolvePlaceholders flag value
+ */
+ public void setResolvePlaceholders(boolean resolvePlaceholders) {
+ this.resolvePlaceholders = resolvePlaceholders;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ bindPropertiesToTarget();
+ }
+
+ @Override
+ public Class> getObjectType() {
+ if (this.target == null) {
+ return Object.class;
+ }
+ return this.target.getClass();
+ }
+
+ @Override
+ public boolean isSingleton() {
+ return true;
+ }
+
+ @Override
+ public T getObject() throws Exception {
+ if (!this.hasBeenBound) {
+ bindPropertiesToTarget();
+ }
+ return this.target;
+ }
+
+ public void bindPropertiesToTarget() throws BindException {
+ Assert.state(this.propertySources != null, "PropertySources should not be null");
+ try {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Property Sources: " + this.propertySources);
+
+ }
+ this.hasBeenBound = true;
+ doBindPropertiesToTarget();
+ } catch (BindException ex) {
+ if (this.exceptionIfInvalid) {
+ throw ex;
+ }
+ logger.error("Failed to load Properties validation bean. " + "Your Properties may be invalid.", ex);
+ }
+ }
+
+ private void doBindPropertiesToTarget() throws BindException {
+ RelaxedDataBinder dataBinder = (this.targetName != null ? new RelaxedDataBinder(this.target,
+ this.targetName) : new RelaxedDataBinder(this.target));
+ if (this.validator != null && this.validator.supports(dataBinder.getTarget().getClass())) {
+ dataBinder.setValidator(this.validator);
+ }
+ if (this.conversionService != null) {
+ dataBinder.setConversionService(this.conversionService);
+ }
+ dataBinder.setAutoGrowCollectionLimit(Integer.MAX_VALUE);
+ dataBinder.setIgnoreNestedProperties(this.ignoreNestedProperties);
+ dataBinder.setIgnoreInvalidFields(this.ignoreInvalidFields);
+ dataBinder.setIgnoreUnknownFields(this.ignoreUnknownFields);
+ customizeBinder(dataBinder);
+ if (this.applicationContext != null) {
+ ResourceEditorRegistrar resourceEditorRegistrar = new ResourceEditorRegistrar(this.applicationContext,
+ this.applicationContext.getEnvironment());
+ resourceEditorRegistrar.registerCustomEditors(dataBinder);
+ }
+ Iterable relaxedTargetNames = getRelaxedTargetNames();
+ Set names = getNames(relaxedTargetNames);
+ PropertyValues propertyValues = getPropertySourcesPropertyValues(names, relaxedTargetNames);
+ dataBinder.bind(propertyValues);
+ if (this.validator != null) {
+ dataBinder.validate();
+ }
+ checkForBindingErrors(dataBinder);
+ }
+
+ private Iterable getRelaxedTargetNames() {
+ return (this.target != null
+ && StringUtils.hasLength(this.targetName) ? new RelaxedNames(this.targetName) : null);
+ }
+
+ private Set getNames(Iterable prefixes) {
+ Set names = new LinkedHashSet();
+ if (this.target != null) {
+ PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(this.target.getClass());
+ for (PropertyDescriptor descriptor : descriptors) {
+ String name = descriptor.getName();
+ if (!name.equals("class")) {
+ RelaxedNames relaxedNames = RelaxedNames.forCamelCase(name);
+ if (prefixes == null) {
+ for (String relaxedName : relaxedNames) {
+ names.add(relaxedName);
+ }
+ } else {
+ for (String prefix : prefixes) {
+ for (String relaxedName : relaxedNames) {
+ names.add(prefix + "." + relaxedName);
+ names.add(prefix + "_" + relaxedName);
+ }
+ }
+ }
+ }
+ }
+ }
+ return names;
+ }
+
+ private PropertyValues getPropertySourcesPropertyValues(Set names, Iterable relaxedTargetNames) {
+ PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names, relaxedTargetNames);
+ return new PropertySourcesPropertyValues(this.propertySources, names, includes, this.resolvePlaceholders);
+ }
+
+ private PropertyNamePatternsMatcher getPropertyNamePatternsMatcher(Set names,
+ Iterable relaxedTargetNames) {
+ if (this.ignoreUnknownFields && !isMapTarget()) {
+ // Since unknown fields are ignored we can filter them out early to save
+ // unnecessary calls to the PropertySource.
+ return new DefaultPropertyNamePatternsMatcher(EXACT_DELIMITERS, true, names);
+ }
+ if (relaxedTargetNames != null) {
+ // We can filter properties to those starting with the target name, but
+ // we can't do a complete filter since we need to trigger the
+ // unknown fields check
+ Set relaxedNames = new HashSet();
+ for (String relaxedTargetName : relaxedTargetNames) {
+ relaxedNames.add(relaxedTargetName);
+ }
+ return new DefaultPropertyNamePatternsMatcher(TARGET_NAME_DELIMITERS, true, relaxedNames);
+ }
+ // Not ideal, we basically can't filter anything
+ return PropertyNamePatternsMatcher.ALL;
+ }
+
+ private boolean isMapTarget() {
+ return this.target != null && Map.class.isAssignableFrom(this.target.getClass());
+ }
+
+ private void checkForBindingErrors(RelaxedDataBinder dataBinder) throws BindException {
+ BindingResult errors = dataBinder.getBindingResult();
+ if (errors.hasErrors()) {
+ logger.error("Properties configuration failed validation");
+ for (ObjectError error : errors.getAllErrors()) {
+ logger.error(this.messageSource != null ? this.messageSource.getMessage(error, Locale.getDefault())
+ + " (" + error + ")" : error);
+ }
+ if (this.exceptionIfInvalid) {
+ throw new BindException(errors);
+ }
+ }
+ }
+
+ /**
+ * Customize the data binder.
+ *
+ * @param dataBinder the data binder that will be used to bind and validate
+ */
+ protected void customizeBinder(DataBinder dataBinder) {
+ }
+}
diff --git a/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertyNamePatternsMatcher.java b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertyNamePatternsMatcher.java
new file mode 100644
index 00000000..0d7d4fe4
--- /dev/null
+++ b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertyNamePatternsMatcher.java
@@ -0,0 +1,38 @@
+package com.alibaba.otter.canal.client.adapter.config.bind;
+
+/**
+ * Strategy interface used to check if a property name matches specific
+ * criteria.
+ *
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+interface PropertyNamePatternsMatcher {
+
+ PropertyNamePatternsMatcher ALL = new PropertyNamePatternsMatcher() {
+
+ @Override
+ public boolean matches(String propertyName) {
+ return true;
+ }
+
+ };
+
+ PropertyNamePatternsMatcher NONE = new PropertyNamePatternsMatcher() {
+
+ @Override
+ public boolean matches(String propertyName) {
+ return false;
+ }
+
+ };
+
+ /**
+ * Return {@code true} of the property name matches.
+ *
+ * @param propertyName the property name
+ * @return {@code true} if the property name matches
+ */
+ boolean matches(String propertyName);
+
+}
diff --git a/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertyOrigin.java b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertyOrigin.java
new file mode 100644
index 00000000..7ad35f1a
--- /dev/null
+++ b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertyOrigin.java
@@ -0,0 +1,30 @@
+package com.alibaba.otter.canal.client.adapter.config.bind;
+
+import com.alibaba.otter.canal.client.adapter.config.common.PropertySource;
+
+/**
+ * The origin of a property, specifically its source and its name before any
+ * prefix was removed.
+ *
+ * @author Andy Wilkinson
+ * @since 1.3.0
+ */
+public class PropertyOrigin {
+
+ private final PropertySource> source;
+
+ private final String name;
+
+ PropertyOrigin(PropertySource> source, String name){
+ this.name = name;
+ this.source = source;
+ }
+
+ public PropertySource> getSource() {
+ return this.source;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+}
diff --git a/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertySourcesPropertyResolver.java b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertySourcesPropertyResolver.java
new file mode 100644
index 00000000..520e857f
--- /dev/null
+++ b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertySourcesPropertyResolver.java
@@ -0,0 +1,164 @@
+package com.alibaba.otter.canal.client.adapter.config.bind;
+
+import org.springframework.core.convert.ConversionException;
+import org.springframework.core.env.AbstractEnvironment;
+import org.springframework.core.env.AbstractPropertyResolver;
+import org.springframework.core.env.PropertyResolver;
+import org.springframework.util.ClassUtils;
+
+import com.alibaba.otter.canal.client.adapter.config.common.PropertySource;
+import com.alibaba.otter.canal.client.adapter.config.common.PropertySources;
+
+/**
+ * {@link PropertyResolver} implementation that resolves property values against
+ * an underlying set of {@link PropertySources}.
+ *
+ * @author Chris Beams
+ * @author Juergen Hoeller
+ * @see PropertySource
+ * @see PropertySources
+ * @see AbstractEnvironment
+ * @since 3.1
+ */
+public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {
+
+ private final PropertySources propertySources;
+
+ /**
+ * Create a new resolver against the given property sources.
+ *
+ * @param propertySources the set of {@link PropertySource} objects to use
+ */
+ public PropertySourcesPropertyResolver(PropertySources propertySources){
+ this.propertySources = propertySources;
+ }
+
+ @Override
+ public boolean containsProperty(String key) {
+ if (this.propertySources != null) {
+ for (PropertySource> propertySource : this.propertySources) {
+ if (propertySource.containsProperty(key)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String getProperty(String key) {
+ return getProperty(key, String.class, true);
+ }
+
+ @Override
+ public T getProperty(String key, Class targetValueType) {
+ return getProperty(key, targetValueType, true);
+ }
+
+ @Override
+ protected String getPropertyAsRawString(String key) {
+ return getProperty(key, String.class, false);
+ }
+
+ protected T getProperty(String key, Class targetValueType, boolean resolveNestedPlaceholders) {
+ if (this.propertySources != null) {
+ for (PropertySource> propertySource : this.propertySources) {
+ if (logger.isTraceEnabled()) {
+ logger
+ .trace("Searching for key '" + key + "' in PropertySource '" + propertySource.getName() + "'");
+ }
+ Object value = propertySource.getProperty(key);
+ if (value != null) {
+ if (resolveNestedPlaceholders && value instanceof String) {
+ value = resolveNestedPlaceholders((String) value);
+ }
+ logKeyFound(key, propertySource, value);
+ return convertValueIfNecessary(value, targetValueType);
+ }
+ }
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not find key '" + key + "' in any property source");
+ }
+ return null;
+ }
+
+ @Deprecated
+ public Class getPropertyAsClass(String key, Class targetValueType) {
+ if (this.propertySources != null) {
+ for (PropertySource> propertySource : this.propertySources) {
+ if (logger.isTraceEnabled()) {
+ logger.trace(String.format("Searching for key '%s' in [%s]", key, propertySource.getName()));
+ }
+ Object value = propertySource.getProperty(key);
+ if (value != null) {
+ logKeyFound(key, propertySource, value);
+ Class> clazz;
+ if (value instanceof String) {
+ try {
+ clazz = ClassUtils.forName((String) value, null);
+ } catch (Exception ex) {
+ throw new PropertySourcesPropertyResolver.ClassConversionException((String) value,
+ targetValueType,
+ ex);
+ }
+ } else if (value instanceof Class) {
+ clazz = (Class>) value;
+ } else {
+ clazz = value.getClass();
+ }
+ if (!targetValueType.isAssignableFrom(clazz)) {
+ throw new PropertySourcesPropertyResolver.ClassConversionException(clazz, targetValueType);
+ }
+ @SuppressWarnings("unchecked")
+ Class targetClass = (Class) clazz;
+ return targetClass;
+ }
+ }
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Could not find key '%s' in any property source", key));
+ }
+ return null;
+ }
+
+ /**
+ * Log the given key as found in the given {@link PropertySource}, resulting in
+ * the given value.
+ *
+ * The default implementation writes a debug log message with key and source. As
+ * of 4.3.3, this does not log the value anymore in order to avoid accidental
+ * logging of sensitive settings. Subclasses may override this method to change
+ * the log level and/or log message, including the property's value if desired.
+ *
+ * @param key the key found
+ * @param propertySource the {@code PropertySource} that the key has been found
+ * in
+ * @param value the corresponding value
+ * @since 4.3.1
+ */
+ protected void logKeyFound(String key, PropertySource> propertySource, Object value) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Found key '" + key + "' in PropertySource '" + propertySource.getName()
+ + "' with value of type " + value.getClass().getSimpleName());
+ }
+ }
+
+ @SuppressWarnings("serial")
+ @Deprecated
+ private static class ClassConversionException extends ConversionException {
+
+ public ClassConversionException(Class> actual, Class> expected){
+ super(String
+ .format("Actual type %s is not assignable to expected type %s", actual.getName(), expected.getName()));
+ }
+
+ public ClassConversionException(String actual, Class> expected, Exception ex){
+ super(
+ String
+ .format("Could not find/load class %s during attempt to convert to %s", actual, expected.getName()),
+ ex);
+ }
+ }
+
+}
diff --git a/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertySourcesPropertyValues.java b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertySourcesPropertyValues.java
new file mode 100644
index 00000000..d41cb27b
--- /dev/null
+++ b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/PropertySourcesPropertyValues.java
@@ -0,0 +1,233 @@
+package com.alibaba.otter.canal.client.adapter.config.bind;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+
+import org.springframework.beans.MutablePropertyValues;
+import org.springframework.beans.PropertyValue;
+import org.springframework.beans.PropertyValues;
+import org.springframework.util.Assert;
+import org.springframework.validation.DataBinder;
+
+import com.alibaba.otter.canal.client.adapter.config.common.CompositePropertySource;
+import com.alibaba.otter.canal.client.adapter.config.common.EnumerablePropertySource;
+import com.alibaba.otter.canal.client.adapter.config.common.PropertySource;
+import com.alibaba.otter.canal.client.adapter.config.common.PropertySources;
+
+/**
+ * A {@link PropertyValues} implementation backed by a {@link PropertySources},
+ * bridging the two abstractions and allowing (for instance) a regular
+ * {@link DataBinder} to be used with the latter.
+ *
+ * @author Dave Syer
+ * @author Phillip Webb
+ */
+public class PropertySourcesPropertyValues implements PropertyValues {
+
+ private static final Pattern COLLECTION_PROPERTY = Pattern
+ .compile("\\[(\\d+)\\](\\.\\S+)?");
+
+ private final PropertySources propertySources;
+
+ private final Collection nonEnumerableFallbackNames;
+
+ private final PropertyNamePatternsMatcher includes;
+
+ private final Map propertyValues = new LinkedHashMap();
+
+ private final ConcurrentHashMap> collectionOwners = new ConcurrentHashMap>();
+
+ private final boolean resolvePlaceholders;
+
+ /**
+ * Create a new PropertyValues from the given PropertySources.
+ *
+ * @param propertySources a PropertySources instance
+ */
+ public PropertySourcesPropertyValues(PropertySources propertySources){
+ this(propertySources, true);
+ }
+
+ /**
+ * Create a new PropertyValues from the given PropertySources that will
+ * optionally resolve placeholders.
+ *
+ * @param propertySources a PropertySources instance
+ * @param resolvePlaceholders {@code true} if placeholders should be resolved.
+ * @since 1.5.2
+ */
+ public PropertySourcesPropertyValues(PropertySources propertySources, boolean resolvePlaceholders){
+ this(propertySources, (Collection) null, PropertyNamePatternsMatcher.ALL, resolvePlaceholders);
+ }
+
+ /**
+ * Create a new PropertyValues from the given PropertySources.
+ *
+ * @param propertySources a PropertySources instance
+ * @param includePatterns property name patterns to include from system
+ * properties and environment variables
+ * @param nonEnumerableFallbackNames the property names to try in lieu of an
+ * {@link EnumerablePropertySource}.
+ */
+ public PropertySourcesPropertyValues(PropertySources propertySources, Collection includePatterns,
+ Collection nonEnumerableFallbackNames){
+ this(propertySources,
+ nonEnumerableFallbackNames,
+ new PatternPropertyNamePatternsMatcher(includePatterns),
+ true);
+ }
+
+ /**
+ * Create a new PropertyValues from the given PropertySources.
+ *
+ * @param propertySources a PropertySources instance
+ * @param nonEnumerableFallbackNames the property names to try in lieu of an
+ * {@link EnumerablePropertySource}.
+ * @param includes the property name patterns to include
+ * @param resolvePlaceholders flag to indicate the placeholders should be
+ * resolved
+ */
+ PropertySourcesPropertyValues(PropertySources propertySources, Collection nonEnumerableFallbackNames,
+ PropertyNamePatternsMatcher includes, boolean resolvePlaceholders){
+ Assert.notNull(propertySources, "PropertySources must not be null");
+ Assert.notNull(includes, "Includes must not be null");
+ this.propertySources = propertySources;
+ this.nonEnumerableFallbackNames = nonEnumerableFallbackNames;
+ this.includes = includes;
+ this.resolvePlaceholders = resolvePlaceholders;
+ PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
+ for (PropertySource> source : propertySources) {
+ processPropertySource(source, resolver);
+ }
+ }
+
+ private void processPropertySource(PropertySource> source, PropertySourcesPropertyResolver resolver) {
+ if (source instanceof CompositePropertySource) {
+ processCompositePropertySource((CompositePropertySource) source, resolver);
+ } else if (source instanceof EnumerablePropertySource) {
+ processEnumerablePropertySource((EnumerablePropertySource>) source, resolver, this.includes);
+ } else {
+ processNonEnumerablePropertySource(source, resolver);
+ }
+ }
+
+ private void processCompositePropertySource(CompositePropertySource source,
+ PropertySourcesPropertyResolver resolver) {
+ for (PropertySource> nested : source.getPropertySources()) {
+ processPropertySource(nested, resolver);
+ }
+ }
+
+ private void processEnumerablePropertySource(EnumerablePropertySource> source,
+ PropertySourcesPropertyResolver resolver,
+ PropertyNamePatternsMatcher includes) {
+ if (source.getPropertyNames().length > 0) {
+ for (String propertyName : source.getPropertyNames()) {
+ if (includes.matches(propertyName)) {
+ Object value = getEnumerableProperty(source, resolver, propertyName);
+ putIfAbsent(propertyName, value, source);
+ }
+ }
+ }
+ }
+
+ private Object getEnumerableProperty(EnumerablePropertySource> source, PropertySourcesPropertyResolver resolver,
+ String propertyName) {
+ try {
+ if (this.resolvePlaceholders) {
+ return resolver.getProperty(propertyName, Object.class);
+ }
+ } catch (RuntimeException ex) {
+ // Probably could not resolve placeholders, ignore it here
+ }
+ return source.getProperty(propertyName);
+ }
+
+ private void processNonEnumerablePropertySource(PropertySource> source,
+ PropertySourcesPropertyResolver resolver) {
+ // We can only do exact matches for non-enumerable property names, but
+ // that's better than nothing...
+ if (this.nonEnumerableFallbackNames == null) {
+ return;
+ }
+ for (String propertyName : this.nonEnumerableFallbackNames) {
+ if (!source.containsProperty(propertyName)) {
+ continue;
+ }
+ Object value = null;
+ try {
+ value = resolver.getProperty(propertyName, Object.class);
+ } catch (RuntimeException ex) {
+ // Probably could not convert to Object, weird, but ignorable
+ }
+ if (value == null) {
+ value = source.getProperty(propertyName.toUpperCase(Locale.ENGLISH));
+ }
+ putIfAbsent(propertyName, value, source);
+ }
+ }
+
+ @Override
+ public PropertyValue[] getPropertyValues() {
+ Collection values = this.propertyValues.values();
+ return values.toArray(new PropertyValue[values.size()]);
+ }
+
+ @Override
+ public PropertyValue getPropertyValue(String propertyName) {
+ PropertyValue propertyValue = this.propertyValues.get(propertyName);
+ if (propertyValue != null) {
+ return propertyValue;
+ }
+ for (PropertySource> source : this.propertySources) {
+ Object value = source.getProperty(propertyName);
+ propertyValue = putIfAbsent(propertyName, value, source);
+ if (propertyValue != null) {
+ return propertyValue;
+ }
+ }
+ return null;
+ }
+
+ private PropertyValue putIfAbsent(String propertyName, Object value, PropertySource> source) {
+ if (value != null && !this.propertyValues.containsKey(propertyName)) {
+ PropertySource> collectionOwner = this.collectionOwners
+ .putIfAbsent(COLLECTION_PROPERTY.matcher(propertyName).replaceAll("[]"), source);
+ if (collectionOwner == null || collectionOwner == source) {
+ PropertyValue propertyValue = new OriginCapablePropertyValue(propertyName, value, propertyName, source);
+ this.propertyValues.put(propertyName, propertyValue);
+ return propertyValue;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public PropertyValues changesSince(PropertyValues old) {
+ MutablePropertyValues changes = new MutablePropertyValues();
+ // for each property value in the new set
+ for (PropertyValue newValue : getPropertyValues()) {
+ // if there wasn't an old one, add it
+ PropertyValue oldValue = old.getPropertyValue(newValue.getName());
+ if (oldValue == null || !oldValue.equals(newValue)) {
+ changes.addPropertyValue(newValue);
+ }
+ }
+ return changes;
+ }
+
+ @Override
+ public boolean contains(String propertyName) {
+ return getPropertyValue(propertyName) != null;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.propertyValues.isEmpty();
+ }
+
+}
diff --git a/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/RelaxedConversionService.java b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/RelaxedConversionService.java
new file mode 100644
index 00000000..00a288b6
--- /dev/null
+++ b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/RelaxedConversionService.java
@@ -0,0 +1,127 @@
+package com.alibaba.otter.canal.client.adapter.config.bind;
+
+import java.util.EnumSet;
+import java.util.Locale;
+import java.util.Set;
+
+import org.springframework.core.convert.ConversionFailedException;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.convert.converter.ConverterFactory;
+import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.core.convert.support.GenericConversionService;
+import org.springframework.util.Assert;
+
+/**
+ * Internal {@link ConversionService} used by {@link RelaxedDataBinder} to
+ * support additional relaxed conversion.
+ *
+ * @author Phillip Webb
+ * @author Stephane Nicoll
+ * @since 1.1.0
+ */
+class RelaxedConversionService implements ConversionService {
+
+ private final ConversionService conversionService;
+
+ private final GenericConversionService additionalConverters;
+
+ /**
+ * Create a new {@link RelaxedConversionService} instance.
+ *
+ * @param conversionService and option root conversion service
+ */
+ RelaxedConversionService(ConversionService conversionService){
+ this.conversionService = conversionService;
+ this.additionalConverters = new GenericConversionService();
+ DefaultConversionService.addCollectionConverters(this.additionalConverters);
+ this.additionalConverters
+ .addConverterFactory(new RelaxedConversionService.StringToEnumIgnoringCaseConverterFactory());
+ this.additionalConverters.addConverter(new StringToCharArrayConverter());
+ }
+
+ @Override
+ public boolean canConvert(Class> sourceType, Class> targetType) {
+ return (this.conversionService != null && this.conversionService.canConvert(sourceType, targetType))
+ || this.additionalConverters.canConvert(sourceType, targetType);
+ }
+
+ @Override
+ public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
+ return (this.conversionService != null && this.conversionService.canConvert(sourceType, targetType))
+ || this.additionalConverters.canConvert(sourceType, targetType);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T convert(Object source, Class targetType) {
+ Assert.notNull(targetType, "The targetType to convert to cannot be null");
+ return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
+ }
+
+ @Override
+ public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
+ if (this.conversionService != null) {
+ try {
+ return this.conversionService.convert(source, sourceType, targetType);
+ } catch (ConversionFailedException ex) {
+ // Ignore and try the additional converters
+ }
+ }
+ return this.additionalConverters.convert(source, sourceType, targetType);
+ }
+
+ /**
+ * Clone of Spring's package private StringToEnumConverterFactory, but ignoring
+ * the case of the source.
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private static class StringToEnumIgnoringCaseConverterFactory implements ConverterFactory {
+
+ @Override
+ public Converter getConverter(Class targetType) {
+ Class> enumType = targetType;
+ while (enumType != null && !enumType.isEnum()) {
+ enumType = enumType.getSuperclass();
+ }
+ Assert.notNull(enumType, "The target type " + targetType.getName() + " does not refer to an enum");
+ return new RelaxedConversionService.StringToEnumIgnoringCaseConverterFactory.StringToEnum(enumType);
+ }
+
+ private class StringToEnum implements Converter {
+
+ private final Class enumType;
+
+ StringToEnum(Class enumType){
+ this.enumType = enumType;
+ }
+
+ @Override
+ public T convert(String source) {
+ if (source.isEmpty()) {
+ // It's an empty enum identifier: reset the enum value to null.
+ return null;
+ }
+ source = source.trim();
+ for (T candidate : (Set) EnumSet.allOf(this.enumType)) {
+ RelaxedNames names = new RelaxedNames(
+ candidate.name().replace('_', '-').toLowerCase(Locale.ENGLISH));
+ for (String name : names) {
+ if (name.equals(source)) {
+ return candidate;
+ }
+ }
+ if (candidate.name().equalsIgnoreCase(source)) {
+ return candidate;
+ }
+ }
+ throw new IllegalArgumentException(
+ "No enum constant " + this.enumType.getCanonicalName() + "." + source);
+ }
+
+ }
+
+ }
+
+}
diff --git a/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/RelaxedDataBinder.java b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/RelaxedDataBinder.java
new file mode 100644
index 00000000..dbcc7130
--- /dev/null
+++ b/client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/config/bind/RelaxedDataBinder.java
@@ -0,0 +1,729 @@
+package com.alibaba.otter.canal.client.adapter.config.bind;
+
+import java.beans.PropertyEditor;
+import java.net.InetAddress;
+import java.util.*;
+
+import org.springframework.beans.*;
+import org.springframework.beans.propertyeditors.FileEditor;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.AbstractPropertyBindingResult;
+import org.springframework.validation.BeanPropertyBindingResult;
+import org.springframework.validation.DataBinder;
+
+/**
+ * Binder implementation that allows caller to bind to maps and also allows
+ * property names to match a bit loosely (if underscores or dashes are removed
+ * and replaced with camel case for example).
+ *
+ * @author Dave Syer
+ * @author Phillip Webb
+ * @author Stephane Nicoll
+ * @author Andy Wilkinson
+ * @see RelaxedNames
+ */
+public class RelaxedDataBinder extends DataBinder {
+
+ private static final Set> EXCLUDED_EDITORS;
+
+ static {
+ Set> excluded = new HashSet>();
+ excluded.add(FileEditor.class);
+ EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded);
+ }
+
+ private static final Object BLANK = new Object();
+
+ private String namePrefix;
+
+ private boolean ignoreNestedProperties;
+
+ private MultiValueMap nameAliases = new LinkedMultiValueMap();
+
+ /**
+ * Create a new {@link RelaxedDataBinder} instance.
+ *
+ * @param target the target into which properties are bound
+ */
+ public RelaxedDataBinder(Object target){
+ super(wrapTarget(target));
+ }
+
+ /**
+ * Create a new {@link RelaxedDataBinder} instance.
+ *
+ * @param target the target into which properties are bound
+ * @param namePrefix An optional prefix to be used when reading properties
+ */
+ public RelaxedDataBinder(Object target, String namePrefix){
+ super(wrapTarget(target), (StringUtils.hasLength(namePrefix) ? namePrefix : DEFAULT_OBJECT_NAME));
+ this.namePrefix = cleanNamePrefix(namePrefix);
+ }
+
+ private String cleanNamePrefix(String namePrefix) {
+ if (!StringUtils.hasLength(namePrefix)) {
+ return null;
+ }
+ return (namePrefix.endsWith(".") ? namePrefix : namePrefix + ".");
+ }
+
+ /**
+ * Flag to disable binding of nested properties (i.e. those with period
+ * separators in their paths). Can be useful to disable this if the name prefix
+ * is empty and you don't want to ignore unknown fields.
+ *
+ * @param ignoreNestedProperties the flag to set (default false)
+ */
+ public void setIgnoreNestedProperties(boolean ignoreNestedProperties) {
+ this.ignoreNestedProperties = ignoreNestedProperties;
+ }
+
+ /**
+ * Set name aliases.
+ *
+ * @param aliases a map of property name to aliases
+ */
+ public void setNameAliases(Map> aliases) {
+ this.nameAliases = new LinkedMultiValueMap(aliases);
+ }
+
+ /**
+ * Add aliases to the {@link DataBinder}.
+ *
+ * @param name the property name to alias
+ * @param alias aliases for the property names
+ * @return this instance
+ */
+ public RelaxedDataBinder withAlias(String name, String... alias) {
+ for (String value : alias) {
+ this.nameAliases.add(name, value);
+ }
+ return this;
+ }
+
+ @Override
+ protected void doBind(MutablePropertyValues propertyValues) {
+ super.doBind(modifyProperties(propertyValues, getTarget()));
+ }
+
+ /**
+ * Modify the property values so that period separated property paths are valid
+ * for map keys. Also creates new maps for properties of map type that are null
+ * (assuming all maps are potentially nested). The standard bracket {@code[...]}
+ * dereferencing is also accepted.
+ *
+ * @param propertyValues the property values
+ * @param target the target object
+ * @return modified property values
+ */
+ private MutablePropertyValues modifyProperties(MutablePropertyValues propertyValues, Object target) {
+ propertyValues = getPropertyValuesForNamePrefix(propertyValues);
+ if (target instanceof RelaxedDataBinder.MapHolder) {
+ propertyValues = addMapPrefix(propertyValues);
+ }
+ BeanWrapper wrapper = new BeanWrapperImpl(target);
+ wrapper.setConversionService(new RelaxedConversionService(getConversionService()));
+ wrapper.setAutoGrowNestedPaths(true);
+ List sortedValues = new ArrayList();
+ Set modifiedNames = new HashSet();
+ List sortedNames = getSortedPropertyNames(propertyValues);
+ for (String name : sortedNames) {
+ PropertyValue propertyValue = propertyValues.getPropertyValue(name);
+ PropertyValue modifiedProperty = modifyProperty(wrapper, propertyValue);
+ if (modifiedNames.add(modifiedProperty.getName())) {
+ sortedValues.add(modifiedProperty);
+ }
+ }
+ return new MutablePropertyValues(sortedValues);
+ }
+
+ private List getSortedPropertyNames(MutablePropertyValues propertyValues) {
+ List names = new LinkedList();
+ for (PropertyValue propertyValue : propertyValues.getPropertyValueList()) {
+ names.add(propertyValue.getName());
+ }
+ sortPropertyNames(names);
+ return names;
+ }
+
+ /**
+ * Sort by name so that parent properties get processed first (e.g. 'foo.bar'
+ * before 'foo.bar.spam'). Don't use Collections.sort() because the order might
+ * be significant for other property names (it shouldn't be but who knows what
+ * people might be relying on, e.g. HSQL has a JDBCXADataSource where
+ * "databaseName" is a synonym for "url").
+ *
+ * @param names the names to sort
+ */
+ private void sortPropertyNames(List names) {
+ for (String name : new ArrayList(names)) {
+ int propertyIndex = names.indexOf(name);
+ RelaxedDataBinder.BeanPath path = new RelaxedDataBinder.BeanPath(name);
+ for (String prefix : path.prefixes()) {
+ int prefixIndex = names.indexOf(prefix);
+ if (prefixIndex >= propertyIndex) {
+ // The child property has a parent in the list in the wrong order
+ names.remove(name);
+ names.add(prefixIndex, name);
+ }
+ }
+ }
+ }
+
+ private MutablePropertyValues addMapPrefix(MutablePropertyValues propertyValues) {
+ MutablePropertyValues rtn = new MutablePropertyValues();
+ for (PropertyValue pv : propertyValues.getPropertyValues()) {
+ rtn.add("map." + pv.getName(), pv.getValue());
+ }
+ return rtn;
+ }
+
+ private MutablePropertyValues getPropertyValuesForNamePrefix(MutablePropertyValues propertyValues) {
+ if (!StringUtils.hasText(this.namePrefix) && !this.ignoreNestedProperties) {
+ return propertyValues;
+ }
+ MutablePropertyValues rtn = new MutablePropertyValues();
+ for (PropertyValue value : propertyValues.getPropertyValues()) {
+ String name = value.getName();
+ for (String prefix : new RelaxedNames(stripLastDot(this.namePrefix))) {
+ for (String separator : new String[] { ".", "_" }) {
+ String candidate = (StringUtils.hasLength(prefix) ? prefix + separator : prefix);
+ if (name.startsWith(candidate)) {
+ name = name.substring(candidate.length());
+ if (!(this.ignoreNestedProperties && name.contains("."))) {
+ PropertyOrigin propertyOrigin = OriginCapablePropertyValue.getOrigin(value);
+ rtn.addPropertyValue(
+ new OriginCapablePropertyValue(name, value.getValue(), propertyOrigin));
+ }
+ }
+ }
+ }
+ }
+ return rtn;
+ }
+
+ private String stripLastDot(String string) {
+ if (StringUtils.hasLength(string) && string.endsWith(".")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ return string;
+ }
+
+ private PropertyValue modifyProperty(BeanWrapper target, PropertyValue propertyValue) {
+ String name = propertyValue.getName();
+ String normalizedName = normalizePath(target, name);
+ if (!normalizedName.equals(name)) {
+ return new PropertyValue(normalizedName, propertyValue.getValue());
+ }
+ return propertyValue;
+ }
+
+ /**
+ * Normalize a bean property path to a format understood by a BeanWrapper. This
+ * is used so that
+ *
+ * - Fuzzy matching can be employed for bean property names
+ * - Period separators can be used instead of indexing ([...]) for map
+ * keys
+ *
+ *
+ * @param wrapper a bean wrapper for the object to bind
+ * @param path the bean path to bind
+ * @return a transformed path with correct bean wrapper syntax
+ */
+ protected String normalizePath(BeanWrapper wrapper, String path) {
+ return initializePath(wrapper, new RelaxedDataBinder.BeanPath(path), 0);
+ }
+
+ @Override
+ protected AbstractPropertyBindingResult createBeanPropertyBindingResult() {
+ return new RelaxedDataBinder.RelaxedBeanPropertyBindingResult(getTarget(),
+ getObjectName(),
+ isAutoGrowNestedPaths(),
+ getAutoGrowCollectionLimit(),
+ getConversionService());
+ }
+
+ private String initializePath(BeanWrapper wrapper, RelaxedDataBinder.BeanPath path, int index) {
+ String prefix = path.prefix(index);
+ String key = path.name(index);
+ if (path.isProperty(index)) {
+ key = getActualPropertyName(wrapper, prefix, key);
+ path.rename(index, key);
+ }
+ if (path.name(++index) == null) {
+ return path.toString();
+ }
+ String name = path.prefix(index);
+ TypeDescriptor descriptor = wrapper.getPropertyTypeDescriptor(name);
+ if (descriptor == null || descriptor.isMap()) {
+ if (isMapValueStringType(descriptor) || isBlanked(wrapper, name, path.name(index))) {
+ path.collapseKeys(index);
+ }
+ path.mapIndex(index);
+ extendMapIfNecessary(wrapper, path, index);
+ } else if (descriptor.isCollection()) {
+ extendCollectionIfNecessary(wrapper, path, index);
+ } else if (descriptor.getType().equals(Object.class)) {
+ if (isBlanked(wrapper, name, path.name(index))) {
+ path.collapseKeys(index);
+ }
+ path.mapIndex(index);
+ if (path.isLastNode(index)) {
+ wrapper.setPropertyValue(path.toString(), BLANK);
+ } else {
+ String next = path.prefix(index + 1);
+ if (wrapper.getPropertyValue(next) == null) {
+ wrapper.setPropertyValue(next, new LinkedHashMap());
+ }
+ }
+ }
+ return initializePath(wrapper, path, index);
+ }
+
+ private boolean isMapValueStringType(TypeDescriptor descriptor) {
+ if (descriptor == null || descriptor.getMapValueTypeDescriptor() == null) {
+ return false;
+ }
+ if (Properties.class.isAssignableFrom(descriptor.getObjectType())) {
+ // Properties is declared as Map