mirror of
https://github.com/alibaba/canal.git
synced 2026-11-04 00:30:15 +08:00
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<parent>
|
<parent>
|
||||||
<artifactId>canal.client-adapter</artifactId>
|
<artifactId>canal.client-adapter</artifactId>
|
||||||
<groupId>com.alibaba.otter</groupId>
|
<groupId>com.alibaba.otter</groupId>
|
||||||
@@ -26,6 +27,16 @@
|
|||||||
<artifactId>druid</artifactId>
|
<artifactId>druid</artifactId>
|
||||||
<version>1.1.9</version>
|
<version>1.1.9</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-context</artifactId>
|
||||||
|
<version>5.0.5.RELEASE</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.yaml</groupId>
|
||||||
|
<artifactId>snakeyaml</artifactId>
|
||||||
|
<version>1.19</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.alibaba.otter.canal.client.adapter;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||||
@@ -21,8 +22,9 @@ public interface OuterAdapter {
|
|||||||
* 外部适配器初始化接口
|
* 外部适配器初始化接口
|
||||||
*
|
*
|
||||||
* @param configuration 外部适配器配置信息
|
* @param configuration 外部适配器配置信息
|
||||||
|
* @param envProperties 环境变量的配置属性
|
||||||
*/
|
*/
|
||||||
void init(OuterAdapterConfig configuration);
|
void init(OuterAdapterConfig configuration, Properties envProperties);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 往适配器中同步数据
|
* 往适配器中同步数据
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import org.springframework.util.PropertyPlaceholderHelper;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import com.alibaba.otter.canal.client.adapter.config.bind.PropertiesConfigurationFactory;
|
||||||
|
import com.alibaba.otter.canal.client.adapter.config.common.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将yaml内容绑定到指定对象, 遵循spring yml的绑定规范
|
||||||
|
*
|
||||||
|
* @author reweerma 2019-2-1 上午9:14:02
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
public class YmlConfigBinder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前内容绑定到指定对象
|
||||||
|
*
|
||||||
|
* @param content yml内容
|
||||||
|
* @param clazz 指定对象类型
|
||||||
|
* @return 对象
|
||||||
|
*/
|
||||||
|
public static <T> T bindYmlToObj(String content, Class<T> clazz) {
|
||||||
|
return bindYmlToObj(null, content, clazz, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前内容绑定到指定对象并指定内容编码格式
|
||||||
|
*
|
||||||
|
* @param content yml内容
|
||||||
|
* @param clazz 指定对象类型
|
||||||
|
* @param charset yml内容编码格式
|
||||||
|
* @return 对象
|
||||||
|
*/
|
||||||
|
public static <T> T bindYmlToObj(String content, Class<T> clazz, String charset) {
|
||||||
|
return bindYmlToObj(null, content, clazz, charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前内容指定前缀部分绑定到指定对象
|
||||||
|
*
|
||||||
|
* @param prefix 指定前缀
|
||||||
|
* @param content yml内容
|
||||||
|
* @param clazz 指定对象类型
|
||||||
|
* @return 对象
|
||||||
|
*/
|
||||||
|
public static <T> T bindYmlToObj(String prefix, String content, Class<T> clazz) {
|
||||||
|
return bindYmlToObj(prefix, content, clazz, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前内容指定前缀部分绑定到指定对象并指定内容编码格式
|
||||||
|
*
|
||||||
|
* @param prefix 指定前缀
|
||||||
|
* @param content yml内容
|
||||||
|
* @param clazz 指定对象类型
|
||||||
|
* @param charset yml内容编码格式
|
||||||
|
* @return 对象
|
||||||
|
*/
|
||||||
|
public static <T> T bindYmlToObj(String prefix, String content, Class<T> clazz, String charset) {
|
||||||
|
return bindYmlToObj(prefix, content, clazz, charset, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前内容指定前缀部分绑定到指定对象并用环境变量中的属性替换占位符, 例: 当前内容有属性 zkServers:
|
||||||
|
* ${zookeeper.servers} 在envProperties中有属性 zookeeper.servers:
|
||||||
|
* 192.168.0.1:2181,192.168.0.1:2181,192.168.0.1:2181 则当前内容 zkServers 会被替换为
|
||||||
|
* zkServers: 192.168.0.1:2181,192.168.0.1:2181,192.168.0.1:2181 注: 假设绑定的类中
|
||||||
|
* zkServers 属性是 List<String> 对象, 则会自动映射成List
|
||||||
|
*
|
||||||
|
* @param prefix 指定前缀
|
||||||
|
* @param content yml内容
|
||||||
|
* @param clazz 指定对象类型
|
||||||
|
* @param charset yml内容编码格式
|
||||||
|
* @return 对象
|
||||||
|
*/
|
||||||
|
public static <T> T bindYmlToObj(String prefix, String content, Class<T> clazz, String charset,
|
||||||
|
Properties baseProperties) {
|
||||||
|
try {
|
||||||
|
byte[] contentBytes;
|
||||||
|
if (charset == null) {
|
||||||
|
contentBytes = content.getBytes();
|
||||||
|
} else {
|
||||||
|
contentBytes = content.getBytes(charset);
|
||||||
|
}
|
||||||
|
YamlPropertySourceLoader propertySourceLoader = new YamlPropertySourceLoader();
|
||||||
|
Resource configResource = new ByteArrayResource(contentBytes);
|
||||||
|
PropertySource<?> propertySource = propertySourceLoader.load("manualBindConfig", configResource, null);
|
||||||
|
|
||||||
|
if (propertySource == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Properties properties = new Properties();
|
||||||
|
Map<String, Object> propertiesRes = new LinkedHashMap<>();
|
||||||
|
if (!StringUtils.isEmpty(prefix) && !prefix.endsWith(".")) {
|
||||||
|
prefix = prefix + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.putAll((Map<?, ?>) propertySource.getSource());
|
||||||
|
|
||||||
|
if (baseProperties != null) {
|
||||||
|
baseProperties.putAll(properties);
|
||||||
|
properties = baseProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object o : ((Map<?, ?>) propertySource.getSource()).entrySet()) {
|
||||||
|
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
|
||||||
|
String key = (String) entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
|
||||||
|
if (prefix != null) {
|
||||||
|
if (key != null && key.startsWith(prefix)) {
|
||||||
|
key = key.substring(prefix.length());
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null && value.toString().contains("${")) {
|
||||||
|
PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}");
|
||||||
|
value = propertyPlaceholderHelper.replacePlaceholders(value.toString(), properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
propertiesRes.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertiesRes.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertySource = new MapPropertySource(propertySource.getName(), propertiesRes);
|
||||||
|
|
||||||
|
T target = clazz.newInstance();
|
||||||
|
|
||||||
|
MutablePropertySources propertySources = new MutablePropertySources();
|
||||||
|
propertySources.addFirst(propertySource);
|
||||||
|
|
||||||
|
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(target);
|
||||||
|
factory.setPropertySources(propertySources);
|
||||||
|
factory.setIgnoreInvalidFields(true);
|
||||||
|
factory.setIgnoreUnknownFields(true);
|
||||||
|
|
||||||
|
factory.bindPropertiesToTarget();
|
||||||
|
|
||||||
|
return target;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link PropertyNamePatternsMatcher} that matches when a property name exactly
|
||||||
|
* matches one of the given names, or starts with one of the given names
|
||||||
|
* followed by a delimiter. This implementation is optimized for frequent calls.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
|
class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher {
|
||||||
|
|
||||||
|
private final char[] delimiters;
|
||||||
|
|
||||||
|
private final boolean ignoreCase;
|
||||||
|
|
||||||
|
private final String[] names;
|
||||||
|
|
||||||
|
protected DefaultPropertyNamePatternsMatcher(char[] delimiters, String... names){
|
||||||
|
this(delimiters, false, names);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase, String... names){
|
||||||
|
this(delimiters, ignoreCase, new HashSet<String>(Arrays.asList(names)));
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase, Set<String> names){
|
||||||
|
this.delimiters = delimiters;
|
||||||
|
this.ignoreCase = ignoreCase;
|
||||||
|
this.names = names.toArray(new String[names.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String propertyName) {
|
||||||
|
char[] propertyNameChars = propertyName.toCharArray();
|
||||||
|
boolean[] match = new boolean[this.names.length];
|
||||||
|
boolean noneMatched = true;
|
||||||
|
for (int i = 0; i < this.names.length; i++) {
|
||||||
|
if (this.names[i].length() <= propertyNameChars.length) {
|
||||||
|
match[i] = true;
|
||||||
|
noneMatched = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (noneMatched) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int charIndex = 0; charIndex < propertyNameChars.length; charIndex++) {
|
||||||
|
for (int nameIndex = 0; nameIndex < this.names.length; nameIndex++) {
|
||||||
|
if (match[nameIndex]) {
|
||||||
|
match[nameIndex] = false;
|
||||||
|
if (charIndex < this.names[nameIndex].length()) {
|
||||||
|
if (isCharMatch(this.names[nameIndex].charAt(charIndex), propertyNameChars[charIndex])) {
|
||||||
|
match[nameIndex] = true;
|
||||||
|
noneMatched = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
char charAfter = propertyNameChars[this.names[nameIndex].length()];
|
||||||
|
if (isDelimiter(charAfter)) {
|
||||||
|
match[nameIndex] = true;
|
||||||
|
noneMatched = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (noneMatched) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < match.length; i++) {
|
||||||
|
if (match[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCharMatch(char c1, char c2) {
|
||||||
|
if (this.ignoreCase) {
|
||||||
|
return Character.toLowerCase(c1) == Character.toLowerCase(c2);
|
||||||
|
}
|
||||||
|
return c1 == c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDelimiter(char c) {
|
||||||
|
for (char delimiter : this.delimiters) {
|
||||||
|
if (c == delimiter) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||||
|
|
||||||
|
import java.beans.PropertyEditorSupport;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link PropertyNamePatternsMatcher} that matches when a property name exactly
|
||||||
|
* matches one of the given names, or starts with one of the given names
|
||||||
|
* followed by a delimiter. This implementation is optimized for frequent calls.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
|
class InetAddressEditor extends PropertyEditorSupport {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAsText() {
|
||||||
|
return ((InetAddress) getValue()).getHostAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAsText(String text) throws IllegalArgumentException {
|
||||||
|
try {
|
||||||
|
setValue(InetAddress.getByName(text));
|
||||||
|
} catch (UnknownHostException ex) {
|
||||||
|
throw new IllegalArgumentException("Cannot locate host", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||||
|
|
||||||
|
import org.springframework.beans.PropertyValue;
|
||||||
|
|
||||||
|
import com.alibaba.otter.canal.client.adapter.config.common.PropertySource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link PropertyValue} that can provide information about its origin.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class OriginCapablePropertyValue extends PropertyValue {
|
||||||
|
|
||||||
|
private static final String ATTRIBUTE_PROPERTY_ORIGIN = "propertyOrigin";
|
||||||
|
|
||||||
|
private final PropertyOrigin origin;
|
||||||
|
|
||||||
|
OriginCapablePropertyValue(PropertyValue propertyValue){
|
||||||
|
this(propertyValue.getName(),
|
||||||
|
propertyValue.getValue(),
|
||||||
|
(PropertyOrigin) propertyValue.getAttribute(ATTRIBUTE_PROPERTY_ORIGIN));
|
||||||
|
}
|
||||||
|
|
||||||
|
OriginCapablePropertyValue(String name, Object value, String originName, PropertySource<?> originSource){
|
||||||
|
this(name, value, new PropertyOrigin(originSource, originName));
|
||||||
|
}
|
||||||
|
|
||||||
|
OriginCapablePropertyValue(String name, Object value, PropertyOrigin origin){
|
||||||
|
super(name, value);
|
||||||
|
this.origin = origin;
|
||||||
|
setAttribute(ATTRIBUTE_PROPERTY_ORIGIN, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyOrigin getOrigin() {
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String name = (this.origin != null ? this.origin.getName() : this.getName());
|
||||||
|
String source = (this.origin.getSource() != null ? this.origin.getSource().getName() : "unknown");
|
||||||
|
return "'" + name + "' from '" + source + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PropertyOrigin getOrigin(PropertyValue propertyValue) {
|
||||||
|
if (propertyValue instanceof OriginCapablePropertyValue) {
|
||||||
|
return ((OriginCapablePropertyValue) propertyValue).getOrigin();
|
||||||
|
}
|
||||||
|
return new OriginCapablePropertyValue(propertyValue).getOrigin();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.springframework.util.PatternMatchUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link PropertyNamePatternsMatcher} that delegates to
|
||||||
|
* {@link PatternMatchUtils#simpleMatch(String[], String)}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
|
class PatternPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher {
|
||||||
|
|
||||||
|
private final String[] patterns;
|
||||||
|
|
||||||
|
PatternPropertyNamePatternsMatcher(Collection<String> patterns){
|
||||||
|
this.patterns = (patterns != null ? patterns.toArray(new String[patterns.size()]) : new String[] {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String propertyName) {
|
||||||
|
return PatternMatchUtils.simpleMatch(this.patterns, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||||
|
|
||||||
|
import java.beans.PropertyDescriptor;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.PropertyValues;
|
||||||
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.beans.support.ResourceEditorRegistrar;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.MessageSourceAware;
|
||||||
|
import org.springframework.core.convert.ConversionService;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.validation.*;
|
||||||
|
|
||||||
|
import com.alibaba.otter.canal.client.adapter.config.common.PropertySources;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate some {@link Properties} (or optionally
|
||||||
|
* {@link org.springframework.core.env.PropertySources}) by binding them to an
|
||||||
|
* object of a specified type and then optionally running a {@link Validator}
|
||||||
|
* over it.
|
||||||
|
*
|
||||||
|
* @param <T> the target type
|
||||||
|
* @author Dave Syer
|
||||||
|
*/
|
||||||
|
public class PropertiesConfigurationFactory<T> implements FactoryBean<T>, 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.
|
||||||
|
* <p>
|
||||||
|
* 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).
|
||||||
|
* <p>
|
||||||
|
* 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<String> relaxedTargetNames = getRelaxedTargetNames();
|
||||||
|
Set<String> names = getNames(relaxedTargetNames);
|
||||||
|
PropertyValues propertyValues = getPropertySourcesPropertyValues(names, relaxedTargetNames);
|
||||||
|
dataBinder.bind(propertyValues);
|
||||||
|
if (this.validator != null) {
|
||||||
|
dataBinder.validate();
|
||||||
|
}
|
||||||
|
checkForBindingErrors(dataBinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterable<String> getRelaxedTargetNames() {
|
||||||
|
return (this.target != null
|
||||||
|
&& StringUtils.hasLength(this.targetName) ? new RelaxedNames(this.targetName) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getNames(Iterable<String> prefixes) {
|
||||||
|
Set<String> names = new LinkedHashSet<String>();
|
||||||
|
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<String> names, Iterable<String> relaxedTargetNames) {
|
||||||
|
PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names, relaxedTargetNames);
|
||||||
|
return new PropertySourcesPropertyValues(this.propertySources, names, includes, this.resolvePlaceholders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PropertyNamePatternsMatcher getPropertyNamePatternsMatcher(Set<String> names,
|
||||||
|
Iterable<String> 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<String> relaxedNames = new HashSet<String>();
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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> T getProperty(String key, Class<T> targetValueType) {
|
||||||
|
return getProperty(key, targetValueType, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getPropertyAsRawString(String key) {
|
||||||
|
return getProperty(key, String.class, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> T getProperty(String key, Class<T> 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 <T> Class<T> getPropertyAsClass(String key, Class<T> 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<T> targetClass = (Class<T>) 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.
|
||||||
|
* <p>
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<String> nonEnumerableFallbackNames;
|
||||||
|
|
||||||
|
private final PropertyNamePatternsMatcher includes;
|
||||||
|
|
||||||
|
private final Map<String, PropertyValue> propertyValues = new LinkedHashMap<String, PropertyValue>();
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<String, PropertySource<?>> collectionOwners = new ConcurrentHashMap<String, PropertySource<?>>();
|
||||||
|
|
||||||
|
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<String>) 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<String> includePatterns,
|
||||||
|
Collection<String> 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<String> 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<PropertyValue> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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> T convert(Object source, Class<T> 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<String, Enum> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends Enum> Converter<String, T> getConverter(Class<T> 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<T extends Enum> implements Converter<String, T> {
|
||||||
|
|
||||||
|
private final Class<T> enumType;
|
||||||
|
|
||||||
|
StringToEnum(Class<T> 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<T>) 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<Class<?>> EXCLUDED_EDITORS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Set<Class<?>> excluded = new HashSet<Class<?>>();
|
||||||
|
excluded.add(FileEditor.class);
|
||||||
|
EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Object BLANK = new Object();
|
||||||
|
|
||||||
|
private String namePrefix;
|
||||||
|
|
||||||
|
private boolean ignoreNestedProperties;
|
||||||
|
|
||||||
|
private MultiValueMap<String, String> nameAliases = new LinkedMultiValueMap<String, String>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<String, List<String>> aliases) {
|
||||||
|
this.nameAliases = new LinkedMultiValueMap<String, String>(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<PropertyValue> sortedValues = new ArrayList<PropertyValue>();
|
||||||
|
Set<String> modifiedNames = new HashSet<String>();
|
||||||
|
List<String> 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<String> getSortedPropertyNames(MutablePropertyValues propertyValues) {
|
||||||
|
List<String> names = new LinkedList<String>();
|
||||||
|
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<String> names) {
|
||||||
|
for (String name : new ArrayList<String>(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
|
||||||
|
* <ul>
|
||||||
|
* <li>Fuzzy matching can be employed for bean property names</li>
|
||||||
|
* <li>Period separators can be used instead of indexing ([...]) for map
|
||||||
|
* keys</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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<String, Object>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<Object,Object> but we know it's really
|
||||||
|
// Map<String,String>
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Class<?> valueType = descriptor.getMapValueTypeDescriptor().getObjectType();
|
||||||
|
return (valueType != null && CharSequence.class.isAssignableFrom(valueType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private boolean isBlanked(BeanWrapper wrapper, String propertyName, String key) {
|
||||||
|
Object value = (wrapper.isReadableProperty(propertyName) ? wrapper.getPropertyValue(propertyName) : null);
|
||||||
|
if (value instanceof Map) {
|
||||||
|
if (((Map) value).get(key) == BLANK) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extendCollectionIfNecessary(BeanWrapper wrapper, RelaxedDataBinder.BeanPath path, int index) {
|
||||||
|
String name = path.prefix(index);
|
||||||
|
TypeDescriptor elementDescriptor = wrapper.getPropertyTypeDescriptor(name).getElementTypeDescriptor();
|
||||||
|
if (!elementDescriptor.isMap() && !elementDescriptor.isCollection()
|
||||||
|
&& !elementDescriptor.getType().equals(Object.class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object extend = new LinkedHashMap<String, Object>();
|
||||||
|
if (!elementDescriptor.isMap() && path.isArrayIndex(index)) {
|
||||||
|
extend = new ArrayList<Object>();
|
||||||
|
}
|
||||||
|
wrapper.setPropertyValue(path.prefix(index + 1), extend);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extendMapIfNecessary(BeanWrapper wrapper, RelaxedDataBinder.BeanPath path, int index) {
|
||||||
|
String name = path.prefix(index);
|
||||||
|
TypeDescriptor parent = wrapper.getPropertyTypeDescriptor(name);
|
||||||
|
if (parent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TypeDescriptor descriptor = parent.getMapValueTypeDescriptor();
|
||||||
|
if (descriptor == null) {
|
||||||
|
descriptor = TypeDescriptor.valueOf(Object.class);
|
||||||
|
}
|
||||||
|
if (!descriptor.isMap() && !descriptor.isCollection() && !descriptor.getType().equals(Object.class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String extensionName = path.prefix(index + 1);
|
||||||
|
if (wrapper.isReadableProperty(extensionName)) {
|
||||||
|
Object currentValue = wrapper.getPropertyValue(extensionName);
|
||||||
|
if ((descriptor.isCollection() && currentValue instanceof Collection)
|
||||||
|
|| (!descriptor.isCollection() && currentValue instanceof Map)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object extend = new LinkedHashMap<String, Object>();
|
||||||
|
if (descriptor.isCollection()) {
|
||||||
|
extend = new ArrayList<Object>();
|
||||||
|
}
|
||||||
|
if (descriptor.getType().equals(Object.class) && path.isLastNode(index)) {
|
||||||
|
extend = BLANK;
|
||||||
|
}
|
||||||
|
wrapper.setPropertyValue(extensionName, extend);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getActualPropertyName(BeanWrapper target, String prefix, String name) {
|
||||||
|
String propertyName = resolvePropertyName(target, prefix, name);
|
||||||
|
if (propertyName == null) {
|
||||||
|
propertyName = resolveNestedPropertyName(target, prefix, name);
|
||||||
|
}
|
||||||
|
return (propertyName != null ? propertyName : name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveNestedPropertyName(BeanWrapper target, String prefix, String name) {
|
||||||
|
StringBuilder candidate = new StringBuilder();
|
||||||
|
for (String field : name.split("[_\\-\\.]")) {
|
||||||
|
candidate.append(candidate.length() > 0 ? "." : "");
|
||||||
|
candidate.append(field);
|
||||||
|
String nested = resolvePropertyName(target, prefix, candidate.toString());
|
||||||
|
if (nested != null) {
|
||||||
|
Class<?> type = target.getPropertyType(nested);
|
||||||
|
if ((type != null) && Map.class.isAssignableFrom(type)) {
|
||||||
|
// Special case for map property (gh-3836).
|
||||||
|
return nested + "[" + name.substring(candidate.length() + 1) + "]";
|
||||||
|
}
|
||||||
|
String propertyName = resolvePropertyName(target,
|
||||||
|
joinString(prefix, nested),
|
||||||
|
name.substring(candidate.length() + 1));
|
||||||
|
if (propertyName != null) {
|
||||||
|
return joinString(nested, propertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolvePropertyName(BeanWrapper target, String prefix, String name) {
|
||||||
|
Iterable<String> names = getNameAndAliases(name);
|
||||||
|
for (String nameOrAlias : names) {
|
||||||
|
for (String candidate : new RelaxedNames(nameOrAlias)) {
|
||||||
|
try {
|
||||||
|
if (target.getPropertyType(joinString(prefix, candidate)) != null) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
} catch (InvalidPropertyException ex) {
|
||||||
|
// swallow and continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joinString(String prefix, String name) {
|
||||||
|
return (StringUtils.hasLength(prefix) ? prefix + "." + name : name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterable<String> getNameAndAliases(String name) {
|
||||||
|
List<String> aliases = this.nameAliases.get(name);
|
||||||
|
if (aliases == null) {
|
||||||
|
return Collections.singleton(name);
|
||||||
|
}
|
||||||
|
List<String> nameAndAliases = new ArrayList<String>(aliases.size() + 1);
|
||||||
|
nameAndAliases.add(name);
|
||||||
|
nameAndAliases.addAll(aliases);
|
||||||
|
return nameAndAliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object wrapTarget(Object target) {
|
||||||
|
if (target instanceof Map) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> map = (Map<String, Object>) target;
|
||||||
|
target = new RelaxedDataBinder.MapHolder(map);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
|
||||||
|
if (propertyEditor == null || !EXCLUDED_EDITORS.contains(propertyEditor.getClass())) {
|
||||||
|
super.registerCustomEditor(requiredType, propertyEditor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerCustomEditor(Class<?> requiredType, String field, PropertyEditor propertyEditor) {
|
||||||
|
if (propertyEditor == null || !EXCLUDED_EDITORS.contains(propertyEditor.getClass())) {
|
||||||
|
super.registerCustomEditor(requiredType, field, propertyEditor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holder to allow Map targets to be bound.
|
||||||
|
*/
|
||||||
|
static class MapHolder {
|
||||||
|
|
||||||
|
private Map<String, Object> map;
|
||||||
|
|
||||||
|
MapHolder(Map<String, Object> map){
|
||||||
|
this.map = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMap(Map<String, Object> map) {
|
||||||
|
this.map = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getMap() {
|
||||||
|
return this.map;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A path though properties of a bean.
|
||||||
|
*/
|
||||||
|
private static class BeanPath {
|
||||||
|
|
||||||
|
private List<PathNode> nodes;
|
||||||
|
|
||||||
|
BeanPath(String path){
|
||||||
|
this.nodes = splitPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> prefixes() {
|
||||||
|
List<String> prefixes = new ArrayList<String>();
|
||||||
|
for (int index = 1; index < this.nodes.size(); index++) {
|
||||||
|
prefixes.add(prefix(index));
|
||||||
|
}
|
||||||
|
return prefixes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLastNode(int index) {
|
||||||
|
return index >= this.nodes.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PathNode> splitPath(String path) {
|
||||||
|
List<PathNode> nodes = new ArrayList<PathNode>();
|
||||||
|
String current = extractIndexedPaths(path, nodes);
|
||||||
|
for (String name : StringUtils.delimitedListToStringArray(current, ".")) {
|
||||||
|
if (StringUtils.hasText(name)) {
|
||||||
|
nodes.add(new RelaxedDataBinder.BeanPath.PropertyNode(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractIndexedPaths(String path, List<PathNode> nodes) {
|
||||||
|
int startRef = path.indexOf("[");
|
||||||
|
String current = path;
|
||||||
|
while (startRef >= 0) {
|
||||||
|
if (startRef > 0) {
|
||||||
|
nodes.addAll(splitPath(current.substring(0, startRef)));
|
||||||
|
}
|
||||||
|
int endRef = current.indexOf("]", startRef);
|
||||||
|
if (endRef > 0) {
|
||||||
|
String sub = current.substring(startRef + 1, endRef);
|
||||||
|
if (sub.matches("[0-9]+")) {
|
||||||
|
nodes.add(new RelaxedDataBinder.BeanPath.ArrayIndexNode(sub));
|
||||||
|
} else {
|
||||||
|
nodes.add(new RelaxedDataBinder.BeanPath.MapIndexNode(sub));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.substring(endRef + 1);
|
||||||
|
startRef = current.indexOf("[");
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void collapseKeys(int index) {
|
||||||
|
List<PathNode> revised = new ArrayList<PathNode>();
|
||||||
|
for (int i = 0; i < index; i++) {
|
||||||
|
revised.add(this.nodes.get(i));
|
||||||
|
}
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (int i = index; i < this.nodes.size(); i++) {
|
||||||
|
if (i > index) {
|
||||||
|
builder.append(".");
|
||||||
|
}
|
||||||
|
builder.append(this.nodes.get(i).name);
|
||||||
|
}
|
||||||
|
revised.add(new RelaxedDataBinder.BeanPath.PropertyNode(builder.toString()));
|
||||||
|
this.nodes = revised;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mapIndex(int index) {
|
||||||
|
RelaxedDataBinder.BeanPath.PathNode node = this.nodes.get(index);
|
||||||
|
if (node instanceof RelaxedDataBinder.BeanPath.PropertyNode) {
|
||||||
|
node = ((RelaxedDataBinder.BeanPath.PropertyNode) node).mapIndex();
|
||||||
|
}
|
||||||
|
this.nodes.set(index, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String prefix(int index) {
|
||||||
|
return range(0, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rename(int index, String name) {
|
||||||
|
this.nodes.get(index).name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String name(int index) {
|
||||||
|
if (index < this.nodes.size()) {
|
||||||
|
return this.nodes.get(index).name;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String range(int start, int end) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
RelaxedDataBinder.BeanPath.PathNode node = this.nodes.get(i);
|
||||||
|
builder.append(node);
|
||||||
|
}
|
||||||
|
if (builder.toString().startsWith(("."))) {
|
||||||
|
builder.replace(0, 1, "");
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isArrayIndex(int index) {
|
||||||
|
return this.nodes.get(index) instanceof RelaxedDataBinder.BeanPath.ArrayIndexNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProperty(int index) {
|
||||||
|
return this.nodes.get(index) instanceof RelaxedDataBinder.BeanPath.PropertyNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return prefix(this.nodes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PathNode {
|
||||||
|
|
||||||
|
protected String name;
|
||||||
|
|
||||||
|
PathNode(String name){
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ArrayIndexNode extends RelaxedDataBinder.BeanPath.PathNode {
|
||||||
|
|
||||||
|
ArrayIndexNode(String name){
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "[" + this.name + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MapIndexNode extends RelaxedDataBinder.BeanPath.PathNode {
|
||||||
|
|
||||||
|
MapIndexNode(String name){
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "[" + this.name + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PropertyNode extends RelaxedDataBinder.BeanPath.PathNode {
|
||||||
|
|
||||||
|
PropertyNode(String name){
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RelaxedDataBinder.BeanPath.MapIndexNode mapIndex() {
|
||||||
|
return new RelaxedDataBinder.BeanPath.MapIndexNode(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "." + this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended version of {@link BeanPropertyBindingResult} to support relaxed
|
||||||
|
* binding.
|
||||||
|
*/
|
||||||
|
private static class RelaxedBeanPropertyBindingResult extends BeanPropertyBindingResult {
|
||||||
|
|
||||||
|
private RelaxedConversionService conversionService;
|
||||||
|
|
||||||
|
RelaxedBeanPropertyBindingResult(Object target, String objectName, boolean autoGrowNestedPaths,
|
||||||
|
int autoGrowCollectionLimit, ConversionService conversionService){
|
||||||
|
super(target, objectName, autoGrowNestedPaths, autoGrowCollectionLimit);
|
||||||
|
this.conversionService = new RelaxedConversionService(conversionService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BeanWrapper createBeanWrapper() {
|
||||||
|
BeanWrapper beanWrapper = new RelaxedDataBinder.RelaxedBeanWrapper(getTarget());
|
||||||
|
beanWrapper.setConversionService(this.conversionService);
|
||||||
|
beanWrapper.registerCustomEditor(InetAddress.class, new InetAddressEditor());
|
||||||
|
return beanWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended version of {@link BeanWrapperImpl} to support relaxed binding.
|
||||||
|
*/
|
||||||
|
private static class RelaxedBeanWrapper extends BeanWrapperImpl {
|
||||||
|
|
||||||
|
private static final Set<String> BENIGN_PROPERTY_SOURCE_NAMES;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Set<String> names = new HashSet<String>();
|
||||||
|
names.add(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);
|
||||||
|
names.add(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME);
|
||||||
|
BENIGN_PROPERTY_SOURCE_NAMES = Collections.unmodifiableSet(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
RelaxedBeanWrapper(Object target){
|
||||||
|
super(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPropertyValue(PropertyValue pv) throws BeansException {
|
||||||
|
try {
|
||||||
|
super.setPropertyValue(pv);
|
||||||
|
} catch (NotWritablePropertyException ex) {
|
||||||
|
PropertyOrigin origin = OriginCapablePropertyValue.getOrigin(pv);
|
||||||
|
if (isBenign(origin)) {
|
||||||
|
logger.debug("Ignoring benign property binding failure", ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (origin == null) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
throw new RelaxedBindingNotWritablePropertyException(ex, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBenign(PropertyOrigin origin) {
|
||||||
|
String name = (origin != null ? origin.getSource().getName() : null);
|
||||||
|
return BENIGN_PROPERTY_SOURCE_NAMES.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RelaxedBindingNotWritablePropertyException extends NotWritablePropertyException {
|
||||||
|
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
private final PropertyOrigin propertyOrigin;
|
||||||
|
|
||||||
|
RelaxedBindingNotWritablePropertyException(NotWritablePropertyException ex, PropertyOrigin propertyOrigin){
|
||||||
|
super(ex.getBeanClass(), ex.getPropertyName());
|
||||||
|
this.propertyOrigin = propertyOrigin;
|
||||||
|
this.message = "Failed to bind '" + propertyOrigin.getName() + "' from '"
|
||||||
|
+ propertyOrigin.getSource().getName() + "' to '" + ex.getPropertyName() + "' property on '"
|
||||||
|
+ ex.getBeanClass().getName() + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return this.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyOrigin getPropertyOrigin() {
|
||||||
|
return this.propertyOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates relaxed name variations from a given source.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Dave Syer
|
||||||
|
* @see RelaxedDataBinder
|
||||||
|
*/
|
||||||
|
public final class RelaxedNames implements Iterable<String> {
|
||||||
|
|
||||||
|
private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([^A-Z-])([A-Z])");
|
||||||
|
|
||||||
|
private static final Pattern SEPARATED_TO_CAMEL_CASE_PATTERN = Pattern.compile("[_\\-.]");
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final Set<String> values = new LinkedHashSet<String>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link RelaxedNames} instance.
|
||||||
|
*
|
||||||
|
* @param name the source name. For the maximum number of variations specify the
|
||||||
|
* name using dashed notation (e.g. {@literal my-property-name}
|
||||||
|
*/
|
||||||
|
public RelaxedNames(String name){
|
||||||
|
this.name = (name != null ? name : "");
|
||||||
|
initialize(RelaxedNames.this.name, this.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<String> iterator() {
|
||||||
|
return this.values.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initialize(String name, Set<String> values) {
|
||||||
|
if (values.contains(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (RelaxedNames.Variation variation : RelaxedNames.Variation.values()) {
|
||||||
|
for (RelaxedNames.Manipulation manipulation : RelaxedNames.Manipulation.values()) {
|
||||||
|
String result = name;
|
||||||
|
result = manipulation.apply(result);
|
||||||
|
result = variation.apply(result);
|
||||||
|
values.add(result);
|
||||||
|
initialize(result, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name variations.
|
||||||
|
*/
|
||||||
|
enum Variation {
|
||||||
|
|
||||||
|
NONE {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
LOWERCASE {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
return (value.isEmpty() ? value : value.toLowerCase(Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
UPPERCASE {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
return (value.isEmpty() ? value : value.toUpperCase(Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
public abstract String apply(String value);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name manipulations.
|
||||||
|
*/
|
||||||
|
enum Manipulation {
|
||||||
|
|
||||||
|
NONE {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
HYPHEN_TO_UNDERSCORE {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
return (value.indexOf('-') != -1 ? value.replace('-', '_') : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
UNDERSCORE_TO_PERIOD {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
return (value.indexOf('_') != -1 ? value.replace('_', '.') : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
PERIOD_TO_UNDERSCORE {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
return (value.indexOf('.') != -1 ? value.replace('.', '_') : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
CAMELCASE_TO_UNDERSCORE {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
Matcher matcher = CAMEL_CASE_PATTERN.matcher(value);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
matcher = matcher.reset();
|
||||||
|
StringBuffer result = new StringBuffer();
|
||||||
|
while (matcher.find()) {
|
||||||
|
matcher.appendReplacement(result,
|
||||||
|
matcher.group(1) + '_' + StringUtils.uncapitalize(matcher.group(2)));
|
||||||
|
}
|
||||||
|
matcher.appendTail(result);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
CAMELCASE_TO_HYPHEN {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
Matcher matcher = CAMEL_CASE_PATTERN.matcher(value);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
matcher = matcher.reset();
|
||||||
|
StringBuffer result = new StringBuffer();
|
||||||
|
while (matcher.find()) {
|
||||||
|
matcher.appendReplacement(result,
|
||||||
|
matcher.group(1) + '-' + StringUtils.uncapitalize(matcher.group(2)));
|
||||||
|
}
|
||||||
|
matcher.appendTail(result);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
SEPARATED_TO_CAMELCASE {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
return separatedToCamelCase(value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
CASE_INSENSITIVE_SEPARATED_TO_CAMELCASE {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String value) {
|
||||||
|
return separatedToCamelCase(value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final char[] SUFFIXES = new char[] { '_', '-', '.' };
|
||||||
|
|
||||||
|
public abstract String apply(String value);
|
||||||
|
|
||||||
|
private static String separatedToCamelCase(String value, boolean caseInsensitive) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (String field : SEPARATED_TO_CAMEL_CASE_PATTERN.split(value)) {
|
||||||
|
field = (caseInsensitive ? field.toLowerCase(Locale.ENGLISH) : field);
|
||||||
|
builder.append(builder.length() != 0 ? StringUtils.capitalize(field) : field);
|
||||||
|
}
|
||||||
|
char lastChar = value.charAt(value.length() - 1);
|
||||||
|
for (char suffix : SUFFIXES) {
|
||||||
|
if (lastChar == suffix) {
|
||||||
|
builder.append(suffix);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@link RelaxedNames} for the given source camelCase source name.
|
||||||
|
*
|
||||||
|
* @param name the source name in camelCase
|
||||||
|
* @return the relaxed names
|
||||||
|
*/
|
||||||
|
public static RelaxedNames forCamelCase(String name) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (char c : name.toCharArray()) {
|
||||||
|
result.append(Character.isUpperCase(c) && result.length() > 0
|
||||||
|
&& result.charAt(result.length() - 1) != '-' ? "-" + Character.toLowerCase(c) : c);
|
||||||
|
}
|
||||||
|
return new RelaxedNames(result.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a String to a Char Array.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class StringToCharArrayConverter implements Converter<String, char[]> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] convert(String source) {
|
||||||
|
return source.toCharArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ResourceUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience base class for {@link Resource} implementations, pre-implementing
|
||||||
|
* typical behavior.
|
||||||
|
* <p>
|
||||||
|
* The "exists" method will check whether a File or InputStream can be opened;
|
||||||
|
* "isOpen" will always return false; "getURL" and "getFile" throw an exception;
|
||||||
|
* and "toString" will return the description.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 28.12.2003
|
||||||
|
*/
|
||||||
|
public abstract class AbstractResource implements Resource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation checks whether a File can be opened, falling back to
|
||||||
|
* whether an InputStream can be opened. This will cover both directories and
|
||||||
|
* content resources.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean exists() {
|
||||||
|
// Try file existence: can we find the file in the file system?
|
||||||
|
try {
|
||||||
|
return getFile().exists();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
// Fall back to stream existence: can we open the stream?
|
||||||
|
try {
|
||||||
|
InputStream is = getInputStream();
|
||||||
|
is.close();
|
||||||
|
return true;
|
||||||
|
} catch (Throwable isEx) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation always returns {@code true}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isReadable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation always returns {@code false}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isOpen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation throws a FileNotFoundException, assuming that the
|
||||||
|
* resource cannot be resolved to a URL.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public URL getURL() throws IOException {
|
||||||
|
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation builds a URI based on the URL returned by
|
||||||
|
* {@link #getURL()}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public URI getURI() throws IOException {
|
||||||
|
URL url = getURL();
|
||||||
|
try {
|
||||||
|
return ResourceUtils.toURI(url);
|
||||||
|
} catch (URISyntaxException ex) {
|
||||||
|
throw new RuntimeException("Invalid URI [" + url + "]", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation throws a FileNotFoundException, assuming that the
|
||||||
|
* resource cannot be resolved to an absolute file path.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public File getFile() throws IOException {
|
||||||
|
throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation reads the entire InputStream to calculate the content
|
||||||
|
* length. Subclasses will almost always be able to provide a more optimal
|
||||||
|
* version of this, e.g. checking a File length.
|
||||||
|
*
|
||||||
|
* @see #getInputStream()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long contentLength() throws IOException {
|
||||||
|
InputStream is = getInputStream();
|
||||||
|
Assert.state(is != null, "Resource InputStream must not be null");
|
||||||
|
try {
|
||||||
|
long size = 0;
|
||||||
|
byte[] buf = new byte[255];
|
||||||
|
int read;
|
||||||
|
while ((read = is.read(buf)) != -1) {
|
||||||
|
size += read;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
is.close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation checks the timestamp of the underlying File, if
|
||||||
|
* available.
|
||||||
|
*
|
||||||
|
* @see #getFileForLastModifiedCheck()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long lastModified() throws IOException {
|
||||||
|
long lastModified = getFileForLastModifiedCheck().lastModified();
|
||||||
|
if (lastModified == 0L) {
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
getDescription() + " cannot be resolved in the file system for resolving its last-modified timestamp");
|
||||||
|
}
|
||||||
|
return lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the File to use for timestamp checking.
|
||||||
|
* <p>
|
||||||
|
* The default implementation delegates to {@link #getFile()}.
|
||||||
|
*
|
||||||
|
* @return the File to use for timestamp checking (never {@code null})
|
||||||
|
* @throws FileNotFoundException if the resource cannot be resolved as an
|
||||||
|
* absolute file path, i.e. is not available in a file system
|
||||||
|
* @throws IOException in case of general resolution/reading failures
|
||||||
|
*/
|
||||||
|
protected File getFileForLastModifiedCheck() throws IOException {
|
||||||
|
return getFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation throws a FileNotFoundException, assuming that relative
|
||||||
|
* resources cannot be created for this resource.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public org.springframework.core.io.Resource createRelative(String relativePath) throws IOException {
|
||||||
|
throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation always returns {@code null}, assuming that this resource
|
||||||
|
* type does not have a filename.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getFilename() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation returns the description of this resource.
|
||||||
|
*
|
||||||
|
* @see #getDescription()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation compares description strings.
|
||||||
|
*
|
||||||
|
* @see #getDescription()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return (obj == this
|
||||||
|
|| (obj instanceof org.springframework.core.io.Resource
|
||||||
|
&& ((org.springframework.core.io.Resource) obj).getDescription().equals(getDescription())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation returns the description's hash code.
|
||||||
|
*
|
||||||
|
* @see #getDescription()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return getDescription().hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.springframework.core.io.InputStreamResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Resource} implementation for a given byte array.
|
||||||
|
* <p>
|
||||||
|
* Creates a {@link ByteArrayInputStream} for the given byte array.
|
||||||
|
* <p>
|
||||||
|
* Useful for loading content from any given byte array, without having to
|
||||||
|
* resort to a single-use {@link InputStreamResource}. Particularly useful for
|
||||||
|
* creating mail attachments from local content, where JavaMail needs to be able
|
||||||
|
* to read the stream multiple times.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @author Sam Brannen
|
||||||
|
* @see ByteArrayInputStream
|
||||||
|
* @see InputStreamResource
|
||||||
|
* @since 1.2.3
|
||||||
|
*/
|
||||||
|
public class ByteArrayResource extends AbstractResource {
|
||||||
|
|
||||||
|
private final byte[] byteArray;
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code ByteArrayResource}.
|
||||||
|
*
|
||||||
|
* @param byteArray the byte array to wrap
|
||||||
|
*/
|
||||||
|
public ByteArrayResource(byte[] byteArray){
|
||||||
|
this(byteArray, "resource loaded from byte array");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code ByteArrayResource} with a description.
|
||||||
|
*
|
||||||
|
* @param byteArray the byte array to wrap
|
||||||
|
* @param description where the byte array comes from
|
||||||
|
*/
|
||||||
|
public ByteArrayResource(byte[] byteArray, String description){
|
||||||
|
Assert.notNull(byteArray, "Byte array must not be null");
|
||||||
|
this.byteArray = byteArray;
|
||||||
|
this.description = (description != null ? description : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the underlying byte array.
|
||||||
|
*/
|
||||||
|
public final byte[] getByteArray() {
|
||||||
|
return this.byteArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation always returns {@code true}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean exists() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation returns the length of the underlying byte array.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long contentLength() {
|
||||||
|
return this.byteArray.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation returns a ByteArrayInputStream for the underlying byte
|
||||||
|
* array.
|
||||||
|
*
|
||||||
|
* @see ByteArrayInputStream
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return new ByteArrayInputStream(this.byteArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation returns a description that includes the passed-in
|
||||||
|
* {@code description}, if any.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Byte array resource [" + this.description + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation compares the underlying byte array.
|
||||||
|
*
|
||||||
|
* @see Arrays#equals(byte[], byte[])
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return (obj == this || (obj instanceof org.springframework.core.io.ByteArrayResource
|
||||||
|
&& Arrays.equals(((ByteArrayResource) obj).byteArray, this.byteArray)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation returns the hash code based on the underlying byte array.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return (byte[].class.hashCode() * 29 * this.byteArray.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composite {@link PropertySource} implementation that iterates over a set of
|
||||||
|
* {@link PropertySource} instances. Necessary in cases where multiple property
|
||||||
|
* sources share the same name, e.g. when multiple values are supplied to
|
||||||
|
* {@code @PropertySource}.
|
||||||
|
* <p>
|
||||||
|
* As of Spring 4.1.2, this class extends {@link EnumerablePropertySource}
|
||||||
|
* instead of plain {@link PropertySource}, exposing {@link #getPropertyNames()}
|
||||||
|
* based on the accumulated property names from all contained sources (as far as
|
||||||
|
* possible).
|
||||||
|
*
|
||||||
|
* @author Chris Beams
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.1
|
||||||
|
*/
|
||||||
|
public class CompositePropertySource extends EnumerablePropertySource<Object> {
|
||||||
|
|
||||||
|
private final Set<PropertySource<?>> propertySources = new LinkedHashSet<PropertySource<?>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code CompositePropertySource}.
|
||||||
|
*
|
||||||
|
* @param name the name of the property source
|
||||||
|
*/
|
||||||
|
public CompositePropertySource(String name){
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getProperty(String name) {
|
||||||
|
for (PropertySource<?> propertySource : this.propertySources) {
|
||||||
|
Object candidate = propertySource.getProperty(name);
|
||||||
|
if (candidate != null) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsProperty(String name) {
|
||||||
|
for (PropertySource<?> propertySource : this.propertySources) {
|
||||||
|
if (propertySource.containsProperty(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getPropertyNames() {
|
||||||
|
Set<String> names = new LinkedHashSet<String>();
|
||||||
|
for (PropertySource<?> propertySource : this.propertySources) {
|
||||||
|
if (!(propertySource instanceof EnumerablePropertySource)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Failed to enumerate property names due to non-enumerable property source: " + propertySource);
|
||||||
|
}
|
||||||
|
names.addAll(Arrays.asList(((EnumerablePropertySource<?>) propertySource).getPropertyNames()));
|
||||||
|
}
|
||||||
|
return StringUtils.toStringArray(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given {@link PropertySource} to the end of the chain.
|
||||||
|
*
|
||||||
|
* @param propertySource the PropertySource to add
|
||||||
|
*/
|
||||||
|
public void addPropertySource(PropertySource<?> propertySource) {
|
||||||
|
this.propertySources.add(propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given {@link PropertySource} to the start of the chain.
|
||||||
|
*
|
||||||
|
* @param propertySource the PropertySource to add
|
||||||
|
* @since 4.1
|
||||||
|
*/
|
||||||
|
public void addFirstPropertySource(PropertySource<?> propertySource) {
|
||||||
|
List<PropertySource<?>> existing = new ArrayList<PropertySource<?>>(this.propertySources);
|
||||||
|
this.propertySources.clear();
|
||||||
|
this.propertySources.add(propertySource);
|
||||||
|
this.propertySources.addAll(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all property sources that this composite source holds.
|
||||||
|
*
|
||||||
|
* @since 4.1.1
|
||||||
|
*/
|
||||||
|
public Collection<PropertySource<?>> getPropertySources() {
|
||||||
|
return this.propertySources;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String
|
||||||
|
.format("%s [name='%s', propertySources=%s]", getClass().getSimpleName(), this.name, this.propertySources);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link PropertySource} implementation capable of interrogating its
|
||||||
|
* underlying source object to enumerate all possible property name/value pairs.
|
||||||
|
* Exposes the {@link #getPropertyNames()} method to allow callers to introspect
|
||||||
|
* available properties without having to access the underlying source object.
|
||||||
|
* This also facilitates a more efficient implementation of
|
||||||
|
* {@link #containsProperty(String)}, in that it can call
|
||||||
|
* {@link #getPropertyNames()} and iterate through the returned array rather
|
||||||
|
* than attempting a call to {@link #getProperty(String)} which may be more
|
||||||
|
* expensive. Implementations may consider caching the result of
|
||||||
|
* {@link #getPropertyNames()} to fully exploit this performance opportunity.
|
||||||
|
* <p>
|
||||||
|
* Most framework-provided {@code PropertySource} implementations are
|
||||||
|
* enumerable; a counter-example would be {@code JndiPropertySource} where, due
|
||||||
|
* to the nature of JNDI it is not possible to determine all possible property
|
||||||
|
* names at any given time; rather it is only possible to try to access a
|
||||||
|
* property (via {@link #getProperty(String)}) in order to evaluate whether it
|
||||||
|
* is present or not.
|
||||||
|
*
|
||||||
|
* @author Chris Beams
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 3.1
|
||||||
|
*/
|
||||||
|
public abstract class EnumerablePropertySource<T> extends PropertySource<T> {
|
||||||
|
|
||||||
|
public EnumerablePropertySource(String name, T source){
|
||||||
|
super(name, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected EnumerablePropertySource(String name){
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether this {@code PropertySource} contains a property with the given
|
||||||
|
* name.
|
||||||
|
* <p>
|
||||||
|
* This implementation checks for the presence of the given name within the
|
||||||
|
* {@link #getPropertyNames()} array.
|
||||||
|
*
|
||||||
|
* @param name the name of the property to find
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean containsProperty(String name) {
|
||||||
|
return ObjectUtils.containsElement(getPropertyNames(), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the names of all properties contained by the {@linkplain #getSource()
|
||||||
|
* source} object (never {@code null}).
|
||||||
|
*/
|
||||||
|
public abstract String[] getPropertyNames();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.core.env.PropertiesPropertySource;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link PropertySource} that reads keys and values from a {@code Map} object.
|
||||||
|
*
|
||||||
|
* @author Chris Beams
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 3.1
|
||||||
|
* @see PropertiesPropertySource
|
||||||
|
*/
|
||||||
|
public class MapPropertySource extends EnumerablePropertySource<Map<String, Object>> {
|
||||||
|
|
||||||
|
public MapPropertySource(String name, Map<String, Object> source){
|
||||||
|
super(name, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getProperty(String name) {
|
||||||
|
return this.source.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsProperty(String name) {
|
||||||
|
return this.source.containsKey(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getPropertyNames() {
|
||||||
|
return StringUtils.toStringArray(this.source.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.springframework.core.env.PropertyResolver;
|
||||||
|
import org.springframework.core.env.PropertySourcesPropertyResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of the {@link PropertySources} interface. Allows
|
||||||
|
* manipulation of contained property sources and provides a constructor for
|
||||||
|
* copying an existing {@code PropertySources} instance.
|
||||||
|
* <p>
|
||||||
|
* Where <em>precedence</em> is mentioned in methods such as {@link #addFirst}
|
||||||
|
* and {@link #addLast}, this is with regard to the order in which property
|
||||||
|
* sources will be searched when resolving a given property with a
|
||||||
|
* {@link PropertyResolver}.
|
||||||
|
*
|
||||||
|
* @author Chris Beams
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @see PropertySourcesPropertyResolver
|
||||||
|
* @since 3.1
|
||||||
|
*/
|
||||||
|
public class MutablePropertySources implements PropertySources {
|
||||||
|
|
||||||
|
private final Log logger;
|
||||||
|
|
||||||
|
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<PropertySource<?>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link MutablePropertySources}
|
||||||
|
* object.
|
||||||
|
*/
|
||||||
|
public MutablePropertySources(){
|
||||||
|
this.logger = LogFactory.getLog(getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code MutablePropertySources} from the given propertySources
|
||||||
|
* object, preserving the original order of contained {@code PropertySource}
|
||||||
|
* objects.
|
||||||
|
*/
|
||||||
|
public MutablePropertySources(PropertySources propertySources){
|
||||||
|
this();
|
||||||
|
for (PropertySource<?> propertySource : propertySources) {
|
||||||
|
addLast(propertySource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link MutablePropertySources}
|
||||||
|
* object and inherit the given logger, usually from an enclosing
|
||||||
|
* {@link Environment}.
|
||||||
|
*/
|
||||||
|
MutablePropertySources(Log logger){
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(String name) {
|
||||||
|
return this.propertySourceList.contains(PropertySource.named(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PropertySource<?> get(String name) {
|
||||||
|
int index = this.propertySourceList.indexOf(PropertySource.named(name));
|
||||||
|
return (index != -1 ? this.propertySourceList.get(index) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<PropertySource<?>> iterator() {
|
||||||
|
return this.propertySourceList.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given property source object with highest precedence.
|
||||||
|
*/
|
||||||
|
public void addFirst(PropertySource<?> propertySource) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Adding PropertySource '" + propertySource.getName() + "' with highest search precedence");
|
||||||
|
}
|
||||||
|
removeIfPresent(propertySource);
|
||||||
|
this.propertySourceList.add(0, propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given property source object with lowest precedence.
|
||||||
|
*/
|
||||||
|
public void addLast(PropertySource<?> propertySource) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Adding PropertySource '" + propertySource.getName() + "' with lowest search precedence");
|
||||||
|
}
|
||||||
|
removeIfPresent(propertySource);
|
||||||
|
this.propertySourceList.add(propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given property source object with precedence immediately higher than
|
||||||
|
* the named relative property source.
|
||||||
|
*/
|
||||||
|
public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Adding PropertySource '" + propertySource.getName()
|
||||||
|
+ "' with search precedence immediately higher than '" + relativePropertySourceName + "'");
|
||||||
|
}
|
||||||
|
assertLegalRelativeAddition(relativePropertySourceName, propertySource);
|
||||||
|
removeIfPresent(propertySource);
|
||||||
|
int index = assertPresentAndGetIndex(relativePropertySourceName);
|
||||||
|
addAtIndex(index, propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given property source object with precedence immediately lower than
|
||||||
|
* the named relative property source.
|
||||||
|
*/
|
||||||
|
public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Adding PropertySource '" + propertySource.getName()
|
||||||
|
+ "' with search precedence immediately lower than '" + relativePropertySourceName + "'");
|
||||||
|
}
|
||||||
|
assertLegalRelativeAddition(relativePropertySourceName, propertySource);
|
||||||
|
removeIfPresent(propertySource);
|
||||||
|
int index = assertPresentAndGetIndex(relativePropertySourceName);
|
||||||
|
addAtIndex(index + 1, propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the precedence of the given property source, {@code -1} if not found.
|
||||||
|
*/
|
||||||
|
public int precedenceOf(PropertySource<?> propertySource) {
|
||||||
|
return this.propertySourceList.indexOf(propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove and return the property source with the given name, {@code null} if
|
||||||
|
* not found.
|
||||||
|
*
|
||||||
|
* @param name the name of the property source to find and remove
|
||||||
|
*/
|
||||||
|
public PropertySource<?> remove(String name) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Removing PropertySource '" + name + "'");
|
||||||
|
}
|
||||||
|
int index = this.propertySourceList.indexOf(PropertySource.named(name));
|
||||||
|
return (index != -1 ? this.propertySourceList.remove(index) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the property source with the given name with the given property
|
||||||
|
* source object.
|
||||||
|
*
|
||||||
|
* @param name the name of the property source to find and replace
|
||||||
|
* @param propertySource the replacement property source
|
||||||
|
* @throws IllegalArgumentException if no property source with the given name is
|
||||||
|
* present
|
||||||
|
* @see #contains
|
||||||
|
*/
|
||||||
|
public void replace(String name, PropertySource<?> propertySource) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Replacing PropertySource '" + name + "' with '" + propertySource.getName() + "'");
|
||||||
|
}
|
||||||
|
int index = assertPresentAndGetIndex(name);
|
||||||
|
this.propertySourceList.set(index, propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of {@link PropertySource} objects contained.
|
||||||
|
*/
|
||||||
|
public int size() {
|
||||||
|
return this.propertySourceList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.propertySourceList.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that the given property source is not being added relative to itself.
|
||||||
|
*/
|
||||||
|
protected void assertLegalRelativeAddition(String relativePropertySourceName, PropertySource<?> propertySource) {
|
||||||
|
String newPropertySourceName = propertySource.getName();
|
||||||
|
if (relativePropertySourceName.equals(newPropertySourceName)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"PropertySource named '" + newPropertySourceName + "' cannot be added relative to itself");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the given property source if it is present.
|
||||||
|
*/
|
||||||
|
protected void removeIfPresent(PropertySource<?> propertySource) {
|
||||||
|
this.propertySourceList.remove(propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given property source at a particular index in the list.
|
||||||
|
*/
|
||||||
|
private void addAtIndex(int index, PropertySource<?> propertySource) {
|
||||||
|
removeIfPresent(propertySource);
|
||||||
|
this.propertySourceList.add(index, propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the named property source is present and return its index.
|
||||||
|
*
|
||||||
|
* @param name {@linkplain PropertySource#getName() name of the property source}
|
||||||
|
* to find
|
||||||
|
* @throws IllegalArgumentException if the named property source is not present
|
||||||
|
*/
|
||||||
|
private int assertPresentAndGetIndex(String name) {
|
||||||
|
int index = this.propertySourceList.indexOf(PropertySource.named(name));
|
||||||
|
if (index == -1) {
|
||||||
|
throw new IllegalArgumentException("PropertySource named '" + name + "' does not exist");
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link PropertySource} implementation that extracts properties from a
|
||||||
|
* {@link Properties} object.
|
||||||
|
* <p>
|
||||||
|
* Note that because a {@code Properties} object is technically an
|
||||||
|
* {@code <Object, Object>} {@link java.util.Hashtable Hashtable}, one may
|
||||||
|
* contain non-{@code String} keys or values. This implementation, however is
|
||||||
|
* restricted to accessing only {@code String}-based keys and values, in the
|
||||||
|
* same fashion as {@link Properties#getProperty} and
|
||||||
|
* {@link Properties#setProperty}.
|
||||||
|
*
|
||||||
|
* @author Chris Beams
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 3.1
|
||||||
|
*/
|
||||||
|
public class PropertiesPropertySource extends MapPropertySource {
|
||||||
|
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
public PropertiesPropertySource(String name, Properties source){
|
||||||
|
super(name, (Map) source);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected PropertiesPropertySource(String name, Map<String, Object> source){
|
||||||
|
super(name, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class representing a source of name/value property pairs. The
|
||||||
|
* underlying {@linkplain #getSource() source object} may be of any type
|
||||||
|
* {@code T} that encapsulates properties. Examples include
|
||||||
|
* {@link java.util.Properties} objects, {@link java.util.Map} objects,
|
||||||
|
* {@code ServletContext} and {@code ServletConfig} objects (for access to init
|
||||||
|
* parameters). Explore the {@code PropertySource} type hierarchy to see
|
||||||
|
* provided implementations.
|
||||||
|
* <p>
|
||||||
|
* {@code PropertySource} objects are not typically used in isolation, but
|
||||||
|
* rather through a {@link PropertySources} object, which aggregates property
|
||||||
|
* sources and in conjunction with a {@link PropertyResolver} implementation
|
||||||
|
* that can perform precedence-based searches across the set of
|
||||||
|
* {@code PropertySources}.
|
||||||
|
* <p>
|
||||||
|
* {@code PropertySource} identity is determined not based on the content of
|
||||||
|
* encapsulated properties, but rather based on the {@link #getName() name} of
|
||||||
|
* the {@code PropertySource} alone. This is useful for manipulating
|
||||||
|
* {@code PropertySource} objects when in collection contexts. See operations in
|
||||||
|
* {@link MutablePropertySources} as well as the {@link #named(String)} and
|
||||||
|
* {@link #toString()} methods for details.
|
||||||
|
* <p>
|
||||||
|
* Note that when working
|
||||||
|
* with @{@link org.springframework.context.annotation.Configuration
|
||||||
|
* Configuration} classes that the @{@link PropertySource PropertySource}
|
||||||
|
* annotation provides a convenient and declarative way of adding property
|
||||||
|
* sources to the enclosing {@code Environment}.
|
||||||
|
*
|
||||||
|
* @author Chris Beams
|
||||||
|
* @since 3.1
|
||||||
|
* @see PropertySources
|
||||||
|
* @see MutablePropertySources
|
||||||
|
* @see PropertySource
|
||||||
|
*/
|
||||||
|
public abstract class PropertySource<T> {
|
||||||
|
|
||||||
|
protected final Log logger = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
|
protected final String name;
|
||||||
|
|
||||||
|
protected final T source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code PropertySource} with the given name and source object.
|
||||||
|
*/
|
||||||
|
public PropertySource(String name, T source){
|
||||||
|
Assert.hasText(name, "Property source name must contain at least one character");
|
||||||
|
Assert.notNull(source, "Property source must not be null");
|
||||||
|
this.name = name;
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code PropertySource} with the given name and with a new
|
||||||
|
* {@code Object} instance as the underlying source.
|
||||||
|
* <p>
|
||||||
|
* Often useful in testing scenarios when creating anonymous implementations
|
||||||
|
* that never query an actual source but rather return hard-coded values.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public PropertySource(String name){
|
||||||
|
this(name, (T) new Object());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the name of this {@code PropertySource}
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the underlying source object for this {@code PropertySource}.
|
||||||
|
*/
|
||||||
|
public T getSource() {
|
||||||
|
return this.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether this {@code PropertySource} contains the given name.
|
||||||
|
* <p>
|
||||||
|
* This implementation simply checks for a {@code null} return value from
|
||||||
|
* {@link #getProperty(String)}. Subclasses may wish to implement a more
|
||||||
|
* efficient algorithm if possible.
|
||||||
|
*
|
||||||
|
* @param name the property name to find
|
||||||
|
*/
|
||||||
|
public boolean containsProperty(String name) {
|
||||||
|
return (getProperty(name) != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the value associated with the given name, or {@code null} if not
|
||||||
|
* found.
|
||||||
|
*
|
||||||
|
* @param name the property to find
|
||||||
|
*/
|
||||||
|
public abstract Object getProperty(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This {@code PropertySource} object is equal to the given object if:
|
||||||
|
* <ul>
|
||||||
|
* <li>they are the same instance
|
||||||
|
* <li>the {@code name} properties for both objects are equal
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* No properties other than {@code name} are evaluated.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return (this == obj || (obj instanceof PropertySource
|
||||||
|
&& ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) obj).name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a hash code derived from the {@code name} property of this
|
||||||
|
* {@code PropertySource} object.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return ObjectUtils.nullSafeHashCode(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce concise output (type and name) if the current log level does not
|
||||||
|
* include debug. If debug is enabled, produce verbose output including the hash
|
||||||
|
* code of the PropertySource instance and every name/value property pair.
|
||||||
|
* <p>
|
||||||
|
* This variable verbosity is useful as a property source such as system
|
||||||
|
* properties or environment variables may contain an arbitrary number of
|
||||||
|
* property pairs, potentially leading to difficult to read exception and log
|
||||||
|
* messages.
|
||||||
|
*
|
||||||
|
* @see Log#isDebugEnabled()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
return getClass().getSimpleName() + "@" + System.identityHashCode(this) + " {name='" + this.name
|
||||||
|
+ "', properties=" + this.source + "}";
|
||||||
|
} else {
|
||||||
|
return getClass().getSimpleName() + " {name='" + this.name + "'}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@code PropertySource} implementation intended for collection
|
||||||
|
* comparison purposes only.
|
||||||
|
* <p>
|
||||||
|
* Primarily for internal use, but given a collection of {@code PropertySource}
|
||||||
|
* objects, may be used as follows:
|
||||||
|
*
|
||||||
|
* <pre class="code">
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* @code
|
||||||
|
* List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
|
||||||
|
* sources.add(new MapPropertySource("sourceA", mapA));
|
||||||
|
* sources.add(new MapPropertySource("sourceB", mapB));
|
||||||
|
* assert sources.contains(PropertySource.named("sourceA"));
|
||||||
|
* assert sources.contains(PropertySource.named("sourceB"));
|
||||||
|
* assert !sources.contains(PropertySource.named("sourceC"));
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* The returned {@code PropertySource} will throw
|
||||||
|
* {@code UnsupportedOperationException} if any methods other than
|
||||||
|
* {@code equals(Object)}, {@code hashCode()}, and {@code toString()} are
|
||||||
|
* called.
|
||||||
|
*
|
||||||
|
* @param name the name of the comparison {@code PropertySource} to be created
|
||||||
|
* and returned.
|
||||||
|
*/
|
||||||
|
public static PropertySource<?> named(String name) {
|
||||||
|
return new ComparisonPropertySource(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code PropertySource} to be used as a placeholder in cases where an actual
|
||||||
|
* property source cannot be eagerly initialized at application context creation
|
||||||
|
* time. For example, a {@code ServletContext}-based property source must wait
|
||||||
|
* until the {@code ServletContext} object is available to its enclosing
|
||||||
|
* {@code ApplicationContext}. In such cases, a stub should be used to hold the
|
||||||
|
* intended default position/order of the property source, then be replaced
|
||||||
|
* during context refresh.
|
||||||
|
*
|
||||||
|
* @see org.springframework.web.context.support.StandardServletEnvironment
|
||||||
|
* @see org.springframework.web.context.support.ServletContextPropertySource
|
||||||
|
*/
|
||||||
|
public static class StubPropertySource extends PropertySource<Object> {
|
||||||
|
|
||||||
|
public StubPropertySource(String name){
|
||||||
|
super(name, new Object());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always returns {@code null}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getProperty(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PropertySource#named(String)
|
||||||
|
*/
|
||||||
|
static class ComparisonPropertySource extends StubPropertySource {
|
||||||
|
|
||||||
|
private static final String USAGE_ERROR = "ComparisonPropertySource instances are for use with collection comparison only";
|
||||||
|
|
||||||
|
public ComparisonPropertySource(String name){
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getSource() {
|
||||||
|
throw new UnsupportedOperationException(USAGE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsProperty(String name) {
|
||||||
|
throw new UnsupportedOperationException(USAGE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProperty(String name) {
|
||||||
|
throw new UnsupportedOperationException(USAGE_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy interface located via {@link SpringFactoriesLoader} and used to load
|
||||||
|
* a {@link PropertySource}.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public interface PropertySourceLoader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file extensions that the loader supports (excluding the '.').
|
||||||
|
*
|
||||||
|
* @return the file extensions
|
||||||
|
*/
|
||||||
|
String[] getFileExtensions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the resource into a property source.
|
||||||
|
*
|
||||||
|
* @param name the name of the property source
|
||||||
|
* @param resource the resource to load
|
||||||
|
* @param profile the name of the profile to load or {@code null}. The profile
|
||||||
|
* can be used to load multi-document files (such as YAML). Simple property
|
||||||
|
* formats should {@code null} when asked to load a profile.
|
||||||
|
* @return a property source or {@code null}
|
||||||
|
* @throws IOException if the source cannot be loaded
|
||||||
|
*/
|
||||||
|
PropertySource<?> load(String name, Resource resource, String profile) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holder containing one or more {@link PropertySource} objects.
|
||||||
|
*
|
||||||
|
* @author Chris Beams
|
||||||
|
* @since 3.1
|
||||||
|
*/
|
||||||
|
public interface PropertySources extends Iterable<PropertySource<?>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether a property source with the given name is contained.
|
||||||
|
*
|
||||||
|
* @param name the {@linkplain PropertySource#getName() name of the property source} to find
|
||||||
|
*/
|
||||||
|
boolean contains(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the property source with the given name, {@code null} if not found.
|
||||||
|
*
|
||||||
|
* @param name the {@linkplain PropertySource#getName() name of the property source} to find
|
||||||
|
*/
|
||||||
|
PropertySource<?> get(String name);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import org.springframework.core.io.*;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a resource descriptor that abstracts from the actual type of
|
||||||
|
* underlying resource, such as a file or class path resource.
|
||||||
|
* <p>
|
||||||
|
* An InputStream can be opened for every resource if it exists in physical
|
||||||
|
* form, but a URL or File handle can just be returned for certain resources.
|
||||||
|
* The actual behavior is implementation-specific.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 28.12.2003
|
||||||
|
* @see #getInputStream()
|
||||||
|
* @see #getURL()
|
||||||
|
* @see #getURI()
|
||||||
|
* @see #getFile()
|
||||||
|
* @see WritableResource
|
||||||
|
* @see ContextResource
|
||||||
|
* @see UrlResource
|
||||||
|
* @see ClassPathResource
|
||||||
|
* @see FileSystemResource
|
||||||
|
* @see PathResource
|
||||||
|
* @see ByteArrayResource
|
||||||
|
* @see InputStreamResource
|
||||||
|
*/
|
||||||
|
public interface Resource extends InputStreamSource {
|
||||||
|
|
||||||
|
boolean exists();
|
||||||
|
|
||||||
|
boolean isReadable();
|
||||||
|
|
||||||
|
boolean isOpen();
|
||||||
|
|
||||||
|
URL getURL() throws IOException;
|
||||||
|
|
||||||
|
URI getURI() throws IOException;
|
||||||
|
|
||||||
|
File getFile() throws IOException;
|
||||||
|
|
||||||
|
long contentLength() throws IOException;
|
||||||
|
|
||||||
|
long lastModified() throws IOException;
|
||||||
|
|
||||||
|
org.springframework.core.io.Resource createRelative(String var1) throws IOException;
|
||||||
|
|
||||||
|
String getFilename();
|
||||||
|
|
||||||
|
String getDescription();
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link YamlProcessor.DocumentMatcher} backed by
|
||||||
|
* {@link Environment#getActiveProfiles()}. A YAML document may define a
|
||||||
|
* "spring.profiles" element as a comma-separated list of Spring profile names,
|
||||||
|
* optionally negated using the {@code !} character. If both negated and
|
||||||
|
* non-negated profiles are specified for a single document, at least one
|
||||||
|
* non-negated profile must match and no negated profiles may match.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Matt Benson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class SpringProfileDocumentMatcher implements YamlProcessor.DocumentMatcher {
|
||||||
|
|
||||||
|
private String[] activeProfiles = new String[0];
|
||||||
|
|
||||||
|
public SpringProfileDocumentMatcher(){
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpringProfileDocumentMatcher(String... profiles){
|
||||||
|
addActiveProfiles(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addActiveProfiles(String... profiles) {
|
||||||
|
LinkedHashSet<String> set = new LinkedHashSet<String>(Arrays.asList(this.activeProfiles));
|
||||||
|
Collections.addAll(set, profiles);
|
||||||
|
this.activeProfiles = set.toArray(new String[set.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public YamlProcessor.MatchStatus matches(Properties properties) {
|
||||||
|
List<String> profiles = extractSpringProfiles(properties);
|
||||||
|
ProfilesMatcher profilesMatcher = getProfilesMatcher();
|
||||||
|
Set<String> negative = extractProfiles(profiles, ProfileType.NEGATIVE);
|
||||||
|
Set<String> positive = extractProfiles(profiles, ProfileType.POSITIVE);
|
||||||
|
if (!CollectionUtils.isEmpty(negative)) {
|
||||||
|
if (profilesMatcher.matches(negative) == YamlProcessor.MatchStatus.FOUND) {
|
||||||
|
return YamlProcessor.MatchStatus.NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (CollectionUtils.isEmpty(positive)) {
|
||||||
|
return YamlProcessor.MatchStatus.FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profilesMatcher.matches(positive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> extractSpringProfiles(Properties properties) {
|
||||||
|
SpringProperties springProperties = new SpringProperties();
|
||||||
|
MutablePropertySources propertySources = new MutablePropertySources();
|
||||||
|
propertySources.addFirst(new PropertiesPropertySource("profiles", properties));
|
||||||
|
// PropertyValues propertyValues = new PropertySourcesPropertyValues(
|
||||||
|
// propertySources);
|
||||||
|
// new RelaxedDataBinder(springProperties, "spring").bind(propertyValues);
|
||||||
|
// TODO
|
||||||
|
List<String> profiles = springProperties.getProfiles();
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProfilesMatcher getProfilesMatcher() {
|
||||||
|
return (this.activeProfiles.length != 0 ? new ActiveProfilesMatcher(
|
||||||
|
new HashSet<String>(Arrays.asList(this.activeProfiles))) : new EmptyProfilesMatcher());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> extractProfiles(List<String> profiles, ProfileType type) {
|
||||||
|
if (CollectionUtils.isEmpty(profiles)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Set<String> extractedProfiles = new HashSet<String>();
|
||||||
|
for (String candidate : profiles) {
|
||||||
|
ProfileType candidateType = ProfileType.POSITIVE;
|
||||||
|
if (candidate.startsWith("!")) {
|
||||||
|
candidateType = ProfileType.NEGATIVE;
|
||||||
|
}
|
||||||
|
if (candidateType == type) {
|
||||||
|
extractedProfiles.add(type != ProfileType.POSITIVE ? candidate.substring(1) : candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extractedProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile match types.
|
||||||
|
*/
|
||||||
|
enum ProfileType {
|
||||||
|
|
||||||
|
POSITIVE, NEGATIVE
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for profile matchers.
|
||||||
|
*/
|
||||||
|
private abstract static class ProfilesMatcher {
|
||||||
|
|
||||||
|
public final YamlProcessor.MatchStatus matches(Set<String> profiles) {
|
||||||
|
if (CollectionUtils.isEmpty(profiles)) {
|
||||||
|
return YamlProcessor.MatchStatus.ABSTAIN;
|
||||||
|
}
|
||||||
|
return doMatches(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract YamlProcessor.MatchStatus doMatches(Set<String> profiles);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ProfilesMatcher} that matches when a value in {@code spring.profiles}
|
||||||
|
* is also in {@code spring.profiles.active}.
|
||||||
|
*/
|
||||||
|
private static class ActiveProfilesMatcher extends ProfilesMatcher {
|
||||||
|
|
||||||
|
private final Set<String> activeProfiles;
|
||||||
|
|
||||||
|
ActiveProfilesMatcher(Set<String> activeProfiles){
|
||||||
|
this.activeProfiles = activeProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected YamlProcessor.MatchStatus doMatches(Set<String> profiles) {
|
||||||
|
if (profiles.isEmpty()) {
|
||||||
|
return YamlProcessor.MatchStatus.NOT_FOUND;
|
||||||
|
}
|
||||||
|
for (String activeProfile : this.activeProfiles) {
|
||||||
|
if (profiles.contains(activeProfile)) {
|
||||||
|
return YamlProcessor.MatchStatus.FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return YamlProcessor.MatchStatus.NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ProfilesMatcher} that matches when {@code
|
||||||
|
* spring.profiles} is empty or contains a value with no text.
|
||||||
|
*
|
||||||
|
* @see StringUtils#hasText(String)
|
||||||
|
*/
|
||||||
|
private static class EmptyProfilesMatcher extends ProfilesMatcher {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public YamlProcessor.MatchStatus doMatches(Set<String> springProfiles) {
|
||||||
|
if (springProfiles.isEmpty()) {
|
||||||
|
return YamlProcessor.MatchStatus.FOUND;
|
||||||
|
}
|
||||||
|
for (String profile : springProfiles) {
|
||||||
|
if (!StringUtils.hasText(profile)) {
|
||||||
|
return YamlProcessor.MatchStatus.FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return YamlProcessor.MatchStatus.NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for binding {@code spring.profiles} property.
|
||||||
|
*/
|
||||||
|
static class SpringProperties {
|
||||||
|
|
||||||
|
private List<String> profiles = new ArrayList<String>();
|
||||||
|
|
||||||
|
public List<String> getProfiles() {
|
||||||
|
return this.profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProfiles(List<String> profiles) {
|
||||||
|
this.profiles = profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
import org.yaml.snakeyaml.constructor.Constructor;
|
||||||
|
import org.yaml.snakeyaml.nodes.MappingNode;
|
||||||
|
import org.yaml.snakeyaml.parser.ParserException;
|
||||||
|
import org.yaml.snakeyaml.reader.UnicodeReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for YAML factories.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 4.1
|
||||||
|
*/
|
||||||
|
public abstract class YamlProcessor {
|
||||||
|
|
||||||
|
private final Log logger = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
|
private ResolutionMethod resolutionMethod = ResolutionMethod.OVERRIDE;
|
||||||
|
|
||||||
|
private Resource[] resources = new Resource[0];
|
||||||
|
|
||||||
|
private List<DocumentMatcher> documentMatchers = Collections.emptyList();
|
||||||
|
|
||||||
|
private boolean matchDefault = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of document matchers allowing callers to selectively use only some of
|
||||||
|
* the documents in a YAML resource. In YAML documents are separated by
|
||||||
|
* <code>---<code> lines, and each document is converted to properties before
|
||||||
|
* the match is made. E.g.
|
||||||
|
*
|
||||||
|
* <pre class="code">
|
||||||
|
* environment: dev
|
||||||
|
* url: http://dev.bar.com
|
||||||
|
* name: Developer Setup
|
||||||
|
* ---
|
||||||
|
* environment: prod
|
||||||
|
* url:http://foo.bar.com
|
||||||
|
* name: My Cool App
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* when mapped with
|
||||||
|
*
|
||||||
|
* <pre class="code">
|
||||||
|
* setDocumentMatchers(properties -> ("prod"
|
||||||
|
* .equals(properties.getProperty("environment")) ? MatchStatus.FOUND : MatchStatus.NOT_FOUND));
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* would end up as
|
||||||
|
*
|
||||||
|
* <pre class="code">
|
||||||
|
* environment=prod
|
||||||
|
* url=http://foo.bar.com
|
||||||
|
* name=My Cool App
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public void setDocumentMatchers(DocumentMatcher... matchers) {
|
||||||
|
this.documentMatchers = Arrays.asList(matchers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag indicating that a document for which all the
|
||||||
|
* {@link #setDocumentMatchers(DocumentMatcher...) document matchers} abstain
|
||||||
|
* will nevertheless match. Default is {@code true}.
|
||||||
|
*/
|
||||||
|
public void setMatchDefault(boolean matchDefault) {
|
||||||
|
this.matchDefault = matchDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to use for resolving resources. Each resource will be converted to a
|
||||||
|
* Map, so this property is used to decide which map entries to keep in the
|
||||||
|
* final output from this factory. Default is {@link ResolutionMethod#OVERRIDE}.
|
||||||
|
*/
|
||||||
|
public void setResolutionMethod(ResolutionMethod resolutionMethod) {
|
||||||
|
Assert.notNull(resolutionMethod, "ResolutionMethod must not be null");
|
||||||
|
this.resolutionMethod = resolutionMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set locations of YAML {@link Resource resources} to be loaded.
|
||||||
|
*
|
||||||
|
* @see ResolutionMethod
|
||||||
|
*/
|
||||||
|
public void setResources(Resource... resources) {
|
||||||
|
this.resources = resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide an opportunity for subclasses to process the Yaml parsed from the
|
||||||
|
* supplied resources. Each resource is parsed in turn and the documents inside
|
||||||
|
* checked against the {@link #setDocumentMatchers(DocumentMatcher...)
|
||||||
|
* matchers}. If a document matches it is passed into the callback, along with
|
||||||
|
* its representation as Properties. Depending on the
|
||||||
|
* {@link #setResolutionMethod(ResolutionMethod)} not all of the documents will
|
||||||
|
* be parsed.
|
||||||
|
*
|
||||||
|
* @param callback a callback to delegate to once matching documents are found
|
||||||
|
* @see #createYaml()
|
||||||
|
*/
|
||||||
|
protected void process(MatchCallback callback) {
|
||||||
|
Yaml yaml = createYaml();
|
||||||
|
for (Resource resource : this.resources) {
|
||||||
|
boolean found = process(callback, yaml, resource);
|
||||||
|
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the {@link Yaml} instance to use.
|
||||||
|
*/
|
||||||
|
protected Yaml createYaml() {
|
||||||
|
return new Yaml(new StrictMapAppenderConstructor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
|
||||||
|
int count = 0;
|
||||||
|
try {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Loading from YAML: " + resource);
|
||||||
|
}
|
||||||
|
Reader reader = new UnicodeReader(resource.getInputStream());
|
||||||
|
try {
|
||||||
|
for (Object object : yaml.loadAll(reader)) {
|
||||||
|
if (object != null && process(asMap(object), callback)) {
|
||||||
|
count++;
|
||||||
|
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug(
|
||||||
|
"Loaded " + count + " document" + (count > 1 ? "s" : "") + " from YAML resource: " + resource);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.close();
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
handleProcessError(resource, ex);
|
||||||
|
}
|
||||||
|
return (count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleProcessError(Resource resource, IOException ex) {
|
||||||
|
if (this.resolutionMethod != ResolutionMethod.FIRST_FOUND
|
||||||
|
&& this.resolutionMethod != ResolutionMethod.OVERRIDE_AND_IGNORE) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
if (logger.isWarnEnabled()) {
|
||||||
|
logger.warn("Could not load map from " + resource + ": " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> asMap(Object object) {
|
||||||
|
// YAML can have numbers as keys
|
||||||
|
Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||||
|
if (!(object instanceof Map)) {
|
||||||
|
// A document can be a text literal
|
||||||
|
result.put("document", object);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Object, Object> map = (Map<Object, Object>) object;
|
||||||
|
for (Map.Entry<Object, Object> entry : map.entrySet()) {
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof Map) {
|
||||||
|
value = asMap(value);
|
||||||
|
}
|
||||||
|
Object key = entry.getKey();
|
||||||
|
if (key instanceof CharSequence) {
|
||||||
|
result.put(key.toString(), value);
|
||||||
|
} else {
|
||||||
|
// It has to be a map key in this case
|
||||||
|
result.put("[" + key.toString() + "]", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean process(Map<String, Object> map, MatchCallback callback) {
|
||||||
|
Properties properties = new Properties() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProperty(String key) {
|
||||||
|
Object value = get(key);
|
||||||
|
return (value != null ? value.toString() : null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
properties.putAll(getFlattenedMap(map));
|
||||||
|
|
||||||
|
if (this.documentMatchers.isEmpty()) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Merging document (no matchers set): " + map);
|
||||||
|
}
|
||||||
|
callback.process(properties, map);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
MatchStatus result = MatchStatus.ABSTAIN;
|
||||||
|
for (DocumentMatcher matcher : this.documentMatchers) {
|
||||||
|
MatchStatus match = matcher.matches(properties);
|
||||||
|
result = MatchStatus.getMostSpecific(match, result);
|
||||||
|
if (match == MatchStatus.FOUND) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Matched document with document matcher: " + properties);
|
||||||
|
}
|
||||||
|
callback.process(properties, map);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == MatchStatus.ABSTAIN && this.matchDefault) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Matched document with default matcher: " + map);
|
||||||
|
}
|
||||||
|
callback.process(properties, map);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Unmatched document: " + map);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a flattened version of the given map, recursively following any nested
|
||||||
|
* Map or Collection values. Entries from the resulting map retain the same
|
||||||
|
* order as the source. When called with the Map from a {@link MatchCallback}
|
||||||
|
* the result will contain the same values as the {@link MatchCallback}
|
||||||
|
* Properties.
|
||||||
|
*
|
||||||
|
* @param source the source map
|
||||||
|
* @return a flattened map
|
||||||
|
* @since 4.1.3
|
||||||
|
*/
|
||||||
|
protected final Map<String, Object> getFlattenedMap(Map<String, Object> source) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||||
|
buildFlattenedMap(result, source, null);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsText(CharSequence str) {
|
||||||
|
int strLen = str.length();
|
||||||
|
for (int i = 0; i < strLen; i++) {
|
||||||
|
if (!Character.isWhitespace(str.charAt(i))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildFlattenedMap(Map<String, Object> result, Map<String, Object> source, String path) {
|
||||||
|
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
if (path != null && !path.isEmpty() && containsText(path)) {
|
||||||
|
if (key.startsWith("[")) {
|
||||||
|
key = path + key;
|
||||||
|
} else {
|
||||||
|
key = path + '.' + key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof String) {
|
||||||
|
result.put(key, value);
|
||||||
|
} else if (value instanceof Map) {
|
||||||
|
// Need a compound key
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> map = (Map<String, Object>) value;
|
||||||
|
buildFlattenedMap(result, map, key);
|
||||||
|
} else if (value instanceof Collection) {
|
||||||
|
// Need a compound key
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Collection<Object> collection = (Collection<Object>) value;
|
||||||
|
int count = 0;
|
||||||
|
for (Object object : collection) {
|
||||||
|
buildFlattenedMap(result, Collections.singletonMap("[" + (count++) + "]", object), key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.put(key, (value != null ? value : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface used to process the YAML parsing results.
|
||||||
|
*/
|
||||||
|
public interface MatchCallback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the given representation of the parsing results.
|
||||||
|
*
|
||||||
|
* @param properties the properties to process (as a flattened representation
|
||||||
|
* with indexed keys in case of a collection or map)
|
||||||
|
* @param map the result map (preserving the original value structure in the
|
||||||
|
* YAML document)
|
||||||
|
*/
|
||||||
|
void process(Properties properties, Map<String, Object> map);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy interface used to test if properties match.
|
||||||
|
*/
|
||||||
|
public interface DocumentMatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if the given properties match.
|
||||||
|
*
|
||||||
|
* @param properties the properties to test
|
||||||
|
* @return the status of the match
|
||||||
|
*/
|
||||||
|
MatchStatus matches(Properties properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status returned from {@link DocumentMatcher#matches(Properties)}
|
||||||
|
*/
|
||||||
|
public enum MatchStatus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A match was found.
|
||||||
|
*/
|
||||||
|
FOUND,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No match was found.
|
||||||
|
*/
|
||||||
|
NOT_FOUND,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The matcher should not be considered.
|
||||||
|
*/
|
||||||
|
ABSTAIN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two {@link MatchStatus} items, returning the most specific status.
|
||||||
|
*/
|
||||||
|
public static MatchStatus getMostSpecific(MatchStatus a, MatchStatus b) {
|
||||||
|
return (a.ordinal() < b.ordinal() ? a : b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to use for resolving resources.
|
||||||
|
*/
|
||||||
|
public enum ResolutionMethod {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace values from earlier in the list.
|
||||||
|
*/
|
||||||
|
OVERRIDE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace values from earlier in the list, ignoring any failures.
|
||||||
|
*/
|
||||||
|
OVERRIDE_AND_IGNORE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take the first resource in the list that exists and use just that.
|
||||||
|
*/
|
||||||
|
FIRST_FOUND
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A specialized {@link Constructor} that checks for duplicate keys.
|
||||||
|
*/
|
||||||
|
protected static class StrictMapAppenderConstructor extends Constructor {
|
||||||
|
|
||||||
|
// Declared as public for use in subclasses
|
||||||
|
public StrictMapAppenderConstructor(){
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<Object, Object> constructMapping(MappingNode node) {
|
||||||
|
try {
|
||||||
|
return super.constructMapping(node);
|
||||||
|
} catch (IllegalStateException ex) {
|
||||||
|
throw new ParserException("while parsing MappingNode",
|
||||||
|
node.getStartMark(),
|
||||||
|
ex.getMessage(),
|
||||||
|
node.getEndMark());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<Object, Object> createDefaultMap() {
|
||||||
|
final Map<Object, Object> delegate = super.createDefaultMap();
|
||||||
|
return new AbstractMap<Object, Object>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object put(Object key, Object value) {
|
||||||
|
if (delegate.containsKey(key)) {
|
||||||
|
throw new IllegalStateException("Duplicate key: " + key);
|
||||||
|
}
|
||||||
|
return delegate.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Entry<Object, Object>> entrySet() {
|
||||||
|
return delegate.entrySet();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.yaml.snakeyaml.DumperOptions;
|
||||||
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
import org.yaml.snakeyaml.nodes.Tag;
|
||||||
|
import org.yaml.snakeyaml.representer.Representer;
|
||||||
|
import org.yaml.snakeyaml.resolver.Resolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy to load '.yml' (or '.yaml') files into a {@link PropertySource}.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class YamlPropertySourceLoader implements PropertySourceLoader {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getFileExtensions() {
|
||||||
|
return new String[] { "yml", "yaml" };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PropertySource<?> load(String name, Resource resource, String profile) throws IOException {
|
||||||
|
if (ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
|
||||||
|
Processor processor = new Processor(resource, profile);
|
||||||
|
Map<String, Object> source = processor.process();
|
||||||
|
if (!source.isEmpty()) {
|
||||||
|
return new MapPropertySource(name, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link YamlProcessor} to create a {@link Map} containing the property values.
|
||||||
|
* Similar to {@link YamlPropertiesFactoryBean} but retains the order of
|
||||||
|
* entries.
|
||||||
|
*/
|
||||||
|
private static class Processor extends YamlProcessor {
|
||||||
|
|
||||||
|
Processor(Resource resource, String profile){
|
||||||
|
if (profile == null) {
|
||||||
|
setMatchDefault(true);
|
||||||
|
setDocumentMatchers(new SpringProfileDocumentMatcher());
|
||||||
|
} else {
|
||||||
|
setMatchDefault(false);
|
||||||
|
setDocumentMatchers(new SpringProfileDocumentMatcher(profile));
|
||||||
|
}
|
||||||
|
setResources(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Yaml createYaml() {
|
||||||
|
return new Yaml(new StrictMapAppenderConstructor(), new Representer(), new DumperOptions(), new Resolver() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addImplicitResolver(Tag tag, Pattern regexp, String first) {
|
||||||
|
if (tag == Tag.TIMESTAMP) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.addImplicitResolver(tag, regexp, first);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> process() {
|
||||||
|
final Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||||
|
process(new MatchCallback() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void process(Properties properties, Map<String, Object> map) {
|
||||||
|
result.putAll(getFlattenedMap(map));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ public class Dml implements Serializable {
|
|||||||
private static final long serialVersionUID = 2611556444074013268L;
|
private static final long serialVersionUID = 2611556444074013268L;
|
||||||
|
|
||||||
private String destination; // 对应canal的实例或者MQ的topic
|
private String destination; // 对应canal的实例或者MQ的topic
|
||||||
|
private String groupId; // 对应mq的group id
|
||||||
private String database; // 数据库或schema
|
private String database; // 数据库或schema
|
||||||
private String table; // 表名
|
private String table; // 表名
|
||||||
private List<String> pkNames;
|
private List<String> pkNames;
|
||||||
@@ -36,6 +37,14 @@ public class Dml implements Serializable {
|
|||||||
this.destination = destination;
|
this.destination = destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getGroupId() {
|
||||||
|
return groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroupId(String groupId) {
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getDatabase() {
|
public String getDatabase() {
|
||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import com.alibaba.otter.canal.protocol.Message;
|
|||||||
*/
|
*/
|
||||||
public class MessageUtil {
|
public class MessageUtil {
|
||||||
|
|
||||||
public static List<Dml> parse4Dml(String destination, Message message) {
|
public static List<Dml> parse4Dml(String destination, String groupId, Message message) {
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -38,10 +38,12 @@ public class MessageUtil {
|
|||||||
|
|
||||||
final Dml dml = new Dml();
|
final Dml dml = new Dml();
|
||||||
dml.setDestination(destination);
|
dml.setDestination(destination);
|
||||||
|
dml.setGroupId(groupId);
|
||||||
dml.setDatabase(entry.getHeader().getSchemaName());
|
dml.setDatabase(entry.getHeader().getSchemaName());
|
||||||
dml.setTable(entry.getHeader().getTableName());
|
dml.setTable(entry.getHeader().getTableName());
|
||||||
dml.setType(eventType.toString());
|
dml.setType(eventType.toString());
|
||||||
dml.setEs(entry.getHeader().getExecuteTime());
|
dml.setEs(entry.getHeader().getExecuteTime());
|
||||||
|
dml.setIsDdl(rowChange.getIsDdl());
|
||||||
dml.setTs(System.currentTimeMillis());
|
dml.setTs(System.currentTimeMillis());
|
||||||
dml.setSql(rowChange.getSql());
|
dml.setSql(rowChange.getSql());
|
||||||
dmls.add(dml);
|
dmls.add(dml);
|
||||||
@@ -118,10 +120,10 @@ public class MessageUtil {
|
|||||||
return dmls;
|
return dmls;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Dml> flatMessage2Dml(String destination, List<FlatMessage> flatMessages) {
|
public static List<Dml> flatMessage2Dml(String destination, String groupId, List<FlatMessage> flatMessages) {
|
||||||
List<Dml> dmls = new ArrayList<Dml>(flatMessages.size());
|
List<Dml> dmls = new ArrayList<Dml>(flatMessages.size());
|
||||||
for (FlatMessage flatMessage : flatMessages) {
|
for (FlatMessage flatMessage : flatMessages) {
|
||||||
Dml dml = flatMessage2Dml(destination, flatMessage);
|
Dml dml = flatMessage2Dml(destination, groupId, flatMessage);
|
||||||
if (dml != null) {
|
if (dml != null) {
|
||||||
dmls.add(dml);
|
dmls.add(dml);
|
||||||
}
|
}
|
||||||
@@ -130,12 +132,13 @@ public class MessageUtil {
|
|||||||
return dmls;
|
return dmls;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Dml flatMessage2Dml(String destination, FlatMessage flatMessage) {
|
public static Dml flatMessage2Dml(String destination, String groupId, FlatMessage flatMessage) {
|
||||||
if (flatMessage == null) {
|
if (flatMessage == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Dml dml = new Dml();
|
Dml dml = new Dml();
|
||||||
dml.setDestination(destination);
|
dml.setDestination(destination);
|
||||||
|
dml.setGroupId(groupId);
|
||||||
dml.setDatabase(flatMessage.getDatabase());
|
dml.setDatabase(flatMessage.getDatabase());
|
||||||
dml.setTable(flatMessage.getTable());
|
dml.setTable(flatMessage.getTable());
|
||||||
dml.setPkNames(flatMessage.getPkNames());
|
dml.setPkNames(flatMessage.getPkNames());
|
||||||
|
|||||||
@@ -18,12 +18,6 @@
|
|||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.yaml</groupId>
|
|
||||||
<artifactId>snakeyaml</artifactId>
|
|
||||||
<version>1.19</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba.fastsql</groupId>
|
<groupId>com.alibaba.fastsql</groupId>
|
||||||
<artifactId>fastsql</artifactId>
|
<artifactId>fastsql</artifactId>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package com.alibaba.otter.canal.client.adapter.es;
|
package com.alibaba.otter.canal.client.adapter.es;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.elasticsearch.action.search.SearchResponse;
|
import org.elasticsearch.action.search.SearchResponse;
|
||||||
import org.elasticsearch.client.transport.TransportClient;
|
import org.elasticsearch.client.transport.TransportClient;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
@@ -28,11 +29,7 @@ import com.alibaba.otter.canal.client.adapter.es.monitor.ESConfigMonitor;
|
|||||||
import com.alibaba.otter.canal.client.adapter.es.service.ESEtlService;
|
import com.alibaba.otter.canal.client.adapter.es.service.ESEtlService;
|
||||||
import com.alibaba.otter.canal.client.adapter.es.service.ESSyncService;
|
import com.alibaba.otter.canal.client.adapter.es.service.ESSyncService;
|
||||||
import com.alibaba.otter.canal.client.adapter.es.support.ESTemplate;
|
import com.alibaba.otter.canal.client.adapter.es.support.ESTemplate;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
import com.alibaba.otter.canal.client.adapter.support.*;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
|
||||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
|
||||||
import com.alibaba.otter.canal.client.adapter.support.OuterAdapterConfig;
|
|
||||||
import com.alibaba.otter.canal.client.adapter.support.SPI;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ES外部适配器
|
* ES外部适配器
|
||||||
@@ -52,6 +49,8 @@ public class ESAdapter implements OuterAdapter {
|
|||||||
|
|
||||||
private ESConfigMonitor esConfigMonitor;
|
private ESConfigMonitor esConfigMonitor;
|
||||||
|
|
||||||
|
private Properties envProperties;
|
||||||
|
|
||||||
public TransportClient getTransportClient() {
|
public TransportClient getTransportClient() {
|
||||||
return transportClient;
|
return transportClient;
|
||||||
}
|
}
|
||||||
@@ -69,9 +68,10 @@ public class ESAdapter implements OuterAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(OuterAdapterConfig configuration) {
|
public void init(OuterAdapterConfig configuration, Properties envProperties) {
|
||||||
try {
|
try {
|
||||||
Map<String, ESSyncConfig> esSyncConfigTmp = ESSyncConfigLoader.load();
|
this.envProperties = envProperties;
|
||||||
|
Map<String, ESSyncConfig> esSyncConfigTmp = ESSyncConfigLoader.load(envProperties);
|
||||||
// 过滤不匹配的key的配置
|
// 过滤不匹配的key的配置
|
||||||
esSyncConfigTmp.forEach((key, config) -> {
|
esSyncConfigTmp.forEach((key, config) -> {
|
||||||
if ((config.getOuterAdapterKey() == null && configuration.getKey() == null)
|
if ((config.getOuterAdapterKey() == null && configuration.getKey() == null)
|
||||||
@@ -99,8 +99,21 @@ public class ESAdapter implements OuterAdapter {
|
|||||||
String schema = matcher.group(2);
|
String schema = matcher.group(2);
|
||||||
|
|
||||||
schemaItem.getAliasTableItems().values().forEach(tableItem -> {
|
schemaItem.getAliasTableItems().values().forEach(tableItem -> {
|
||||||
Map<String, ESSyncConfig> esSyncConfigMap = dbTableEsSyncConfig
|
Map<String, ESSyncConfig> esSyncConfigMap;
|
||||||
.computeIfAbsent(schema + "-" + tableItem.getTableName(), k -> new HashMap<>());
|
if (envProperties != null
|
||||||
|
&& !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||||
|
esSyncConfigMap = dbTableEsSyncConfig
|
||||||
|
.computeIfAbsent(StringUtils.trimToEmpty(config.getDestination()) + "-"
|
||||||
|
+ StringUtils.trimToEmpty(config.getGroupId()) + "_" + schema + "-"
|
||||||
|
+ tableItem.getTableName(),
|
||||||
|
k -> new ConcurrentHashMap<>());
|
||||||
|
} else {
|
||||||
|
esSyncConfigMap = dbTableEsSyncConfig
|
||||||
|
.computeIfAbsent(StringUtils.trimToEmpty(config.getDestination()) + "_" + schema + "-"
|
||||||
|
+ tableItem.getTableName(),
|
||||||
|
k -> new ConcurrentHashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
esSyncConfigMap.put(configName, config);
|
esSyncConfigMap.put(configName, config);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -138,8 +151,17 @@ public class ESAdapter implements OuterAdapter {
|
|||||||
public void sync(Dml dml) {
|
public void sync(Dml dml) {
|
||||||
String database = dml.getDatabase();
|
String database = dml.getDatabase();
|
||||||
String table = dml.getTable();
|
String table = dml.getTable();
|
||||||
Map<String, ESSyncConfig> configMap = dbTableEsSyncConfig.get(database + "-" + table);
|
Map<String, ESSyncConfig> configMap;
|
||||||
if (configMap != null) {
|
if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||||
|
configMap = dbTableEsSyncConfig
|
||||||
|
.get(StringUtils.trimToEmpty(dml.getDestination()) + "-" + StringUtils.trimToEmpty(dml.getGroupId())
|
||||||
|
+ "_" + database + "-" + table);
|
||||||
|
} else {
|
||||||
|
configMap = dbTableEsSyncConfig
|
||||||
|
.get(StringUtils.trimToEmpty(dml.getDestination()) + "_" + database + "-" + table);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configMap != null && !configMap.values().isEmpty()) {
|
||||||
esSyncService.sync(configMap.values(), dml);
|
esSyncService.sync(configMap.values(), dml);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,6 +230,9 @@ public class ESAdapter implements OuterAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
|
if (esConfigMonitor != null) {
|
||||||
|
esConfigMonitor.destroy();
|
||||||
|
}
|
||||||
if (transportClient != null) {
|
if (transportClient != null) {
|
||||||
transportClient.close();
|
transportClient.close();
|
||||||
}
|
}
|
||||||
@@ -215,10 +240,6 @@ public class ESAdapter implements OuterAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDestination(String task) {
|
public String getDestination(String task) {
|
||||||
if (esConfigMonitor != null) {
|
|
||||||
esConfigMonitor.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
ESSyncConfig config = esSyncConfig.get(task);
|
ESSyncConfig config = esSyncConfig.get(task);
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
return config.getDestination();
|
return config.getDestination();
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public class ESSyncConfig {
|
|||||||
|
|
||||||
private String outerAdapterKey; // adapter key
|
private String outerAdapterKey; // adapter key
|
||||||
|
|
||||||
|
private String groupId; // group id
|
||||||
|
|
||||||
private String destination; // canal destination
|
private String destination; // canal destination
|
||||||
|
|
||||||
private ESMapping esMapping;
|
private ESMapping esMapping;
|
||||||
@@ -52,6 +54,14 @@ public class ESSyncConfig {
|
|||||||
this.outerAdapterKey = outerAdapterKey;
|
this.outerAdapterKey = outerAdapterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getGroupId() {
|
||||||
|
return groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroupId(String groupId) {
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getDestination() {
|
public String getDestination() {
|
||||||
return destination;
|
return destination;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.alibaba.otter.canal.client.adapter.es.config;
|
|||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
|
||||||
|
|
||||||
|
import com.alibaba.otter.canal.client.adapter.config.YmlConfigBinder;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,17 +20,17 @@ public class ESSyncConfigLoader {
|
|||||||
|
|
||||||
private static Logger logger = LoggerFactory.getLogger(ESSyncConfigLoader.class);
|
private static Logger logger = LoggerFactory.getLogger(ESSyncConfigLoader.class);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
public static synchronized Map<String, ESSyncConfig> load(Properties envProperties) {
|
||||||
public static synchronized Map<String, ESSyncConfig> load() {
|
|
||||||
logger.info("## Start loading es mapping config ... ");
|
logger.info("## Start loading es mapping config ... ");
|
||||||
|
|
||||||
Map<String, ESSyncConfig> esSyncConfig = new LinkedHashMap<>();
|
Map<String, ESSyncConfig> esSyncConfig = new LinkedHashMap<>();
|
||||||
|
|
||||||
Map<String, String> configContentMap = MappingConfigsLoader.loadConfigs("es");
|
Map<String, String> configContentMap = MappingConfigsLoader.loadConfigs("es");
|
||||||
configContentMap.forEach((fileName, content) -> {
|
configContentMap.forEach((fileName, content) -> {
|
||||||
Map configMap = new Yaml().loadAs(content, Map.class); // yml自带的对象反射不是很稳定
|
ESSyncConfig config = YmlConfigBinder.bindYmlToObj(null, content, ESSyncConfig.class, null, envProperties);
|
||||||
JSONObject configJson = new JSONObject(configMap);
|
if (config == null) {
|
||||||
ESSyncConfig config = configJson.toJavaObject(ESSyncConfig.class);
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
config.validate();
|
config.validate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class SchemaItem {
|
|||||||
columnFields = new LinkedHashMap<>();
|
columnFields = new LinkedHashMap<>();
|
||||||
getSelectFields()
|
getSelectFields()
|
||||||
.forEach((fieldName, fieldItem) -> fieldItem.getColumnItems().forEach(columnItem -> {
|
.forEach((fieldName, fieldItem) -> fieldItem.getColumnItems().forEach(columnItem -> {
|
||||||
TableItem tableItem = getAliasTableItems().get(columnItem.getOwner());
|
// TableItem tableItem = getAliasTableItems().get(columnItem.getOwner());
|
||||||
// if (!tableItem.isSubQuery()) {
|
// if (!tableItem.isSubQuery()) {
|
||||||
List<FieldItem> fieldItems = columnFields.computeIfAbsent(
|
List<FieldItem> fieldItems = columnFields.computeIfAbsent(
|
||||||
columnItem.getOwner() + "." + columnItem.getColumnName(),
|
columnItem.getOwner() + "." + columnItem.getColumnName(),
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ public class ESTemplate {
|
|||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
esFieldData.forEach((key, value) -> {
|
esFieldData.forEach((key, value) -> {
|
||||||
if (value instanceof Map) {
|
if (value instanceof Map) {
|
||||||
HashMap mapValue = (HashMap) value;
|
Map<?, ?> mapValue = (Map<?, ?>) value;
|
||||||
if (mapValue.containsKey("lon") && mapValue.containsKey("lat") && mapValue.size() == 2) {
|
if (mapValue.containsKey("lon") && mapValue.containsKey("lat") && mapValue.size() == 2) {
|
||||||
sb.append("ctx._source")
|
sb.append("ctx._source")
|
||||||
.append("['")
|
.append("['")
|
||||||
@@ -461,7 +461,7 @@ public class ESTemplate {
|
|||||||
resultIdVal = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
|
resultIdVal = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dmlOld.get(columnName) != null && !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
if (dmlOld.containsKey(columnName) && !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||||
esFieldData.put(fieldItem.getFieldName(),
|
esFieldData.put(fieldItem.getFieldName(),
|
||||||
getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName));
|
getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
dataSourceKey: defaultDS
|
dataSourceKey: defaultDS
|
||||||
destination: example
|
destination: example
|
||||||
|
groupId: g1
|
||||||
esMapping:
|
esMapping:
|
||||||
_index: mytest_user
|
_index: mytest_user
|
||||||
_type: _doc
|
_type: _doc
|
||||||
_id: _id
|
_id: _id
|
||||||
# pk: id
|
# pk: id
|
||||||
sql: "select a.id as _id, a.name as _name, a.role_id as _role_id, b.role_name as _role_name,
|
sql: "select a.id as _id, a.name as _name, a.role_id as _role_id, b.role_name as _role_name,
|
||||||
a.c_time as _c_time, c.labels as _labels from user a
|
a.c_time as _c_time from user a
|
||||||
left join role b on b.id=a.role_id
|
left join role b on b.id=a.role_id"
|
||||||
left join (select user_id, group_concat(label order by id desc separator ';') as labels from label
|
|
||||||
group by user_id) c on c.user_id=a.id"
|
|
||||||
# objFields:
|
# objFields:
|
||||||
# _labels: array:;
|
# _labels: array:;
|
||||||
etlCondition: "where a.c_time>='{0}'"
|
etlCondition: "where a.c_time>='{0}'"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class ConfigLoadTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLoad() {
|
public void testLoad() {
|
||||||
Map<String, ESSyncConfig> configMap = ESSyncConfigLoader.load();
|
Map<String, ESSyncConfig> configMap = ESSyncConfigLoader.load(null);
|
||||||
ESSyncConfig config = configMap.get("mytest_user.yml");
|
ESSyncConfig config = configMap.get("mytest_user.yml");
|
||||||
Assert.assertNotNull(config);
|
Assert.assertNotNull(config);
|
||||||
Assert.assertEquals("defaultDS", config.getDataSourceKey());
|
Assert.assertEquals("defaultDS", config.getDataSourceKey());
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class Common {
|
|||||||
outerAdapterConfig.setProperties(properties);
|
outerAdapterConfig.setProperties(properties);
|
||||||
|
|
||||||
ESAdapter esAdapter = new ESAdapter();
|
ESAdapter esAdapter = new ESAdapter();
|
||||||
esAdapter.init(outerAdapterConfig);
|
esAdapter.init(outerAdapterConfig, null);
|
||||||
return esAdapter;
|
return esAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,6 @@
|
|||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.yaml</groupId>
|
|
||||||
<artifactId>snakeyaml</artifactId>
|
|
||||||
<version>1.19</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.hbase</groupId>
|
<groupId>org.apache.hbase</groupId>
|
||||||
<artifactId>hbase-client</artifactId>
|
<artifactId>hbase-client</artifactId>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package com.alibaba.otter.canal.client.adapter.hbase;
|
package com.alibaba.otter.canal.client.adapter.hbase;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
@@ -49,6 +46,8 @@ public class HbaseAdapter implements OuterAdapter {
|
|||||||
|
|
||||||
private HbaseConfigMonitor configMonitor;
|
private HbaseConfigMonitor configMonitor;
|
||||||
|
|
||||||
|
private Properties envProperties;
|
||||||
|
|
||||||
public Map<String, MappingConfig> getHbaseMapping() {
|
public Map<String, MappingConfig> getHbaseMapping() {
|
||||||
return hbaseMapping;
|
return hbaseMapping;
|
||||||
}
|
}
|
||||||
@@ -58,9 +57,10 @@ public class HbaseAdapter implements OuterAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(OuterAdapterConfig configuration) {
|
public void init(OuterAdapterConfig configuration, Properties envProperties) {
|
||||||
try {
|
try {
|
||||||
Map<String, MappingConfig> hbaseMappingTmp = MappingConfigLoader.load();
|
this.envProperties = envProperties;
|
||||||
|
Map<String, MappingConfig> hbaseMappingTmp = MappingConfigLoader.load(envProperties);
|
||||||
// 过滤不匹配的key的配置
|
// 过滤不匹配的key的配置
|
||||||
hbaseMappingTmp.forEach((key, mappingConfig) -> {
|
hbaseMappingTmp.forEach((key, mappingConfig) -> {
|
||||||
if ((mappingConfig.getOuterAdapterKey() == null && configuration.getKey() == null)
|
if ((mappingConfig.getOuterAdapterKey() == null && configuration.getKey() == null)
|
||||||
@@ -72,10 +72,19 @@ public class HbaseAdapter implements OuterAdapter {
|
|||||||
for (Map.Entry<String, MappingConfig> entry : hbaseMapping.entrySet()) {
|
for (Map.Entry<String, MappingConfig> entry : hbaseMapping.entrySet()) {
|
||||||
String configName = entry.getKey();
|
String configName = entry.getKey();
|
||||||
MappingConfig mappingConfig = entry.getValue();
|
MappingConfig mappingConfig = entry.getValue();
|
||||||
String k = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "."
|
String k;
|
||||||
+ mappingConfig.getHbaseMapping().getDatabase() + "."
|
if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||||
+ mappingConfig.getHbaseMapping().getTable();
|
k = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "-"
|
||||||
Map<String, MappingConfig> configMap = mappingConfigCache.computeIfAbsent(k, k1 -> new HashMap<>());
|
+ StringUtils.trimToEmpty(mappingConfig.getGroupId()) + "_"
|
||||||
|
+ mappingConfig.getHbaseMapping().getDatabase() + "-"
|
||||||
|
+ mappingConfig.getHbaseMapping().getTable();
|
||||||
|
} else {
|
||||||
|
k = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "_"
|
||||||
|
+ mappingConfig.getHbaseMapping().getDatabase() + "-"
|
||||||
|
+ mappingConfig.getHbaseMapping().getTable();
|
||||||
|
}
|
||||||
|
Map<String, MappingConfig> configMap = mappingConfigCache.computeIfAbsent(k,
|
||||||
|
k1 -> new ConcurrentHashMap<>());
|
||||||
configMap.put(configName, mappingConfig);
|
configMap.put(configName, mappingConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +116,29 @@ public class HbaseAdapter implements OuterAdapter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String destination = StringUtils.trimToEmpty(dml.getDestination());
|
String destination = StringUtils.trimToEmpty(dml.getDestination());
|
||||||
|
String groupId = StringUtils.trimToEmpty(dml.getGroupId());
|
||||||
String database = dml.getDatabase();
|
String database = dml.getDatabase();
|
||||||
String table = dml.getTable();
|
String table = dml.getTable();
|
||||||
Map<String, MappingConfig> configMap = mappingConfigCache.get(destination + "." + database + "." + table);
|
Map<String, MappingConfig> configMap;
|
||||||
|
if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||||
|
configMap = mappingConfigCache.get(destination + "-" + groupId + "_" + database + "-" + table);
|
||||||
|
} else {
|
||||||
|
configMap = mappingConfigCache.get(destination + "_" + database + "-" + table);
|
||||||
|
}
|
||||||
if (configMap != null) {
|
if (configMap != null) {
|
||||||
configMap.values().forEach(config -> hbaseSyncService.sync(config, dml));
|
List<MappingConfig> configs = new ArrayList<>();
|
||||||
|
configMap.values().forEach(config -> {
|
||||||
|
if (StringUtils.isNotEmpty(config.getGroupId())) {
|
||||||
|
if (config.getGroupId().equals(dml.getGroupId())) {
|
||||||
|
configs.add(config);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
configs.add(config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!configs.isEmpty()) {
|
||||||
|
configs.forEach(config -> hbaseSyncService.sync(config, dml));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ public class MappingConfig {
|
|||||||
|
|
||||||
private String outerAdapterKey; // adapter key
|
private String outerAdapterKey; // adapter key
|
||||||
|
|
||||||
|
private String groupId; // groupId
|
||||||
|
|
||||||
private String destination; // canal实例或MQ的topic
|
private String destination; // canal实例或MQ的topic
|
||||||
|
|
||||||
private HbaseMapping hbaseMapping; // hbase映射配置
|
private HbaseMapping hbaseMapping; // hbase映射配置
|
||||||
@@ -26,6 +28,14 @@ public class MappingConfig {
|
|||||||
this.dataSourceKey = dataSourceKey;
|
this.dataSourceKey = dataSourceKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getGroupId() {
|
||||||
|
return groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroupId(String groupId) {
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getOuterAdapterKey() {
|
public String getOuterAdapterKey() {
|
||||||
return outerAdapterKey;
|
return outerAdapterKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.alibaba.otter.canal.client.adapter.hbase.config;
|
|||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
|
||||||
|
|
||||||
|
import com.alibaba.otter.canal.client.adapter.config.YmlConfigBinder;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,17 +25,18 @@ public class MappingConfigLoader {
|
|||||||
*
|
*
|
||||||
* @return 配置名/配置文件名--对象
|
* @return 配置名/配置文件名--对象
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
public static Map<String, MappingConfig> load(Properties envProperties) {
|
||||||
public static Map<String, MappingConfig> load() {
|
|
||||||
logger.info("## Start loading hbase mapping config ... ");
|
logger.info("## Start loading hbase mapping config ... ");
|
||||||
|
|
||||||
Map<String, MappingConfig> result = new LinkedHashMap<>();
|
Map<String, MappingConfig> result = new LinkedHashMap<>();
|
||||||
|
|
||||||
Map<String, String> configContentMap = MappingConfigsLoader.loadConfigs("hbase");
|
Map<String, String> configContentMap = MappingConfigsLoader.loadConfigs("hbase");
|
||||||
configContentMap.forEach((fileName, content) -> {
|
configContentMap.forEach((fileName, content) -> {
|
||||||
Map configMap = new Yaml().loadAs(content, Map.class); // yml自带的对象反射不是很稳定
|
MappingConfig config = YmlConfigBinder
|
||||||
JSONObject configJson = new JSONObject(configMap);
|
.bindYmlToObj(null, content, MappingConfig.class, null, envProperties);
|
||||||
MappingConfig config = configJson.toJavaObject(MappingConfig.class);
|
if (config == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
config.validate();
|
config.validate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.alibaba.otter.canal.client.adapter.hbase.service;
|
package com.alibaba.otter.canal.client.adapter.hbase.service;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.ResultSetMetaData;
|
||||||
|
import java.sql.Timestamp;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -10,19 +11,23 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
|
||||||
import org.apache.hadoop.hbase.util.Bytes;
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfig;
|
import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfig;
|
||||||
import com.alibaba.otter.canal.client.adapter.hbase.support.*;
|
import com.alibaba.otter.canal.client.adapter.hbase.support.HRow;
|
||||||
|
import com.alibaba.otter.canal.client.adapter.hbase.support.HbaseTemplate;
|
||||||
|
import com.alibaba.otter.canal.client.adapter.hbase.support.PhType;
|
||||||
|
import com.alibaba.otter.canal.client.adapter.hbase.support.PhTypeUtil;
|
||||||
|
import com.alibaba.otter.canal.client.adapter.hbase.support.Type;
|
||||||
|
import com.alibaba.otter.canal.client.adapter.hbase.support.TypeUtil;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.JdbcTypeUtil;
|
import com.alibaba.otter.canal.client.adapter.support.JdbcTypeUtil;
|
||||||
|
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
dataSourceKey: defaultDS
|
dataSourceKey: defaultDS
|
||||||
destination: example
|
destination: example
|
||||||
|
groupId: g1
|
||||||
hbaseMapping:
|
hbaseMapping:
|
||||||
mode: STRING #NATIVE #PHOENIX
|
mode: STRING #NATIVE #PHOENIX
|
||||||
database: mytest # 数据库名
|
database: mytest # 数据库名
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public abstract class AbstractCanalAdapterWorker {
|
|||||||
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
|
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
protected String canalDestination; // canal实例
|
protected String canalDestination; // canal实例
|
||||||
|
protected String groupId = null; // groupId
|
||||||
protected List<List<OuterAdapter>> canalOuterAdapters; // 外部适配器
|
protected List<List<OuterAdapter>> canalOuterAdapters; // 外部适配器
|
||||||
protected CanalClientConfig canalClientConfig; // 配置
|
protected CanalClientConfig canalClientConfig; // 配置
|
||||||
protected ExecutorService groupInnerExecutorService; // 组内工作线程池
|
protected ExecutorService groupInnerExecutorService; // 组内工作线程池
|
||||||
@@ -56,7 +57,7 @@ public abstract class AbstractCanalAdapterWorker {
|
|||||||
// 组内适配器穿行运行,尽量不要配置组内适配器
|
// 组内适配器穿行运行,尽量不要配置组内适配器
|
||||||
adapters.forEach(adapter -> {
|
adapters.forEach(adapter -> {
|
||||||
long begin = System.currentTimeMillis();
|
long begin = System.currentTimeMillis();
|
||||||
List<Dml> dmls = MessageUtil.parse4Dml(canalDestination, message);
|
List<Dml> dmls = MessageUtil.parse4Dml(canalDestination, groupId, message);
|
||||||
if (dmls != null) {
|
if (dmls != null) {
|
||||||
batchSync(dmls, adapter);
|
batchSync(dmls, adapter);
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ public abstract class AbstractCanalAdapterWorker {
|
|||||||
// 组内适配器穿行运行,尽量不要配置组内适配器
|
// 组内适配器穿行运行,尽量不要配置组内适配器
|
||||||
outerAdapters.forEach(adapter -> {
|
outerAdapters.forEach(adapter -> {
|
||||||
long begin = System.currentTimeMillis();
|
long begin = System.currentTimeMillis();
|
||||||
List<Dml> dmls = MessageUtil.flatMessage2Dml(canalDestination, flatMessages);
|
List<Dml> dmls = MessageUtil.flatMessage2Dml(canalDestination, groupId, flatMessages);
|
||||||
batchSync(dmls, adapter);
|
batchSync(dmls, adapter);
|
||||||
|
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ public class CanalAdapterKafkaWorker extends AbstractCanalAdapterWorker {
|
|||||||
super(canalOuterAdapters);
|
super(canalOuterAdapters);
|
||||||
this.canalClientConfig = canalClientConfig;
|
this.canalClientConfig = canalClientConfig;
|
||||||
this.topic = topic;
|
this.topic = topic;
|
||||||
this.canalDestination = topic;
|
super.canalDestination = topic;
|
||||||
|
super.groupId = groupId;
|
||||||
this.flatMessage = flatMessage;
|
this.flatMessage = flatMessage;
|
||||||
this.connector = new KafkaCanalConnector(bootstrapServers,
|
this.connector = new KafkaCanalConnector(bootstrapServers,
|
||||||
topic,
|
topic,
|
||||||
@@ -45,10 +46,12 @@ public class CanalAdapterKafkaWorker extends AbstractCanalAdapterWorker {
|
|||||||
try {
|
try {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ExecutorService workerExecutor = Executors.newSingleThreadExecutor();
|
ExecutorService workerExecutor = Executors.newSingleThreadExecutor();
|
||||||
int retry = canalClientConfig.getRetries() == null || canalClientConfig.getRetries() == 0 ? 1 : canalClientConfig.getRetries();
|
int retry = canalClientConfig.getRetries() == null
|
||||||
|
|| canalClientConfig.getRetries() == 0 ? 1 : canalClientConfig.getRetries();
|
||||||
long timeout = canalClientConfig.getTimeout() == null ? 30000 : canalClientConfig.getTimeout(); // 默认超时30秒
|
long timeout = canalClientConfig.getTimeout() == null ? 30000 : canalClientConfig.getTimeout(); // 默认超时30秒
|
||||||
|
|
||||||
while (running) {
|
while (running) {
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ package com.alibaba.otter.canal.adapter.launcher.loader;
|
|||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
@@ -13,7 +10,12 @@ import java.util.concurrent.Future;
|
|||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.env.EnumerablePropertySource;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
import org.springframework.core.env.StandardEnvironment;
|
||||||
|
|
||||||
|
import com.alibaba.otter.canal.adapter.launcher.config.SpringContext;
|
||||||
import com.alibaba.otter.canal.client.adapter.OuterAdapter;
|
import com.alibaba.otter.canal.client.adapter.OuterAdapter;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.CanalClientConfig;
|
import com.alibaba.otter.canal.client.adapter.support.CanalClientConfig;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.ExtensionLoader;
|
import com.alibaba.otter.canal.client.adapter.support.ExtensionLoader;
|
||||||
@@ -62,7 +64,7 @@ public class CanalAdapterLoader {
|
|||||||
for (CanalClientConfig.Group connectorGroup : canalAdapter.getGroups()) {
|
for (CanalClientConfig.Group connectorGroup : canalAdapter.getGroups()) {
|
||||||
List<OuterAdapter> canalOutConnectors = new ArrayList<>();
|
List<OuterAdapter> canalOutConnectors = new ArrayList<>();
|
||||||
for (OuterAdapterConfig c : connectorGroup.getOuterAdapters()) {
|
for (OuterAdapterConfig c : connectorGroup.getOuterAdapters()) {
|
||||||
loadConnector(c, canalOutConnectors);
|
loadAdapter(c, canalOutConnectors);
|
||||||
}
|
}
|
||||||
canalOuterAdapterGroups.add(canalOutConnectors);
|
canalOuterAdapterGroups.add(canalOutConnectors);
|
||||||
}
|
}
|
||||||
@@ -91,7 +93,7 @@ public class CanalAdapterLoader {
|
|||||||
List<List<OuterAdapter>> canalOuterAdapterGroups = new ArrayList<>();
|
List<List<OuterAdapter>> canalOuterAdapterGroups = new ArrayList<>();
|
||||||
List<OuterAdapter> canalOuterAdapters = new ArrayList<>();
|
List<OuterAdapter> canalOuterAdapters = new ArrayList<>();
|
||||||
for (OuterAdapterConfig config : group.getOuterAdapters()) {
|
for (OuterAdapterConfig config : group.getOuterAdapters()) {
|
||||||
loadConnector(config, canalOuterAdapters);
|
loadAdapter(config, canalOuterAdapters);
|
||||||
}
|
}
|
||||||
canalOuterAdapterGroups.add(canalOuterAdapters);
|
canalOuterAdapterGroups.add(canalOuterAdapters);
|
||||||
|
|
||||||
@@ -103,8 +105,8 @@ public class CanalAdapterLoader {
|
|||||||
canalClientConfig.getFlatMessage());
|
canalClientConfig.getFlatMessage());
|
||||||
canalMQWorker.put(canalAdapter.getInstance() + "-kafka-" + group.getGroupId(), canalKafkaWorker);
|
canalMQWorker.put(canalAdapter.getInstance() + "-kafka-" + group.getGroupId(), canalKafkaWorker);
|
||||||
canalKafkaWorker.start();
|
canalKafkaWorker.start();
|
||||||
logger.info("Start adapter for canal-client mq topic: {} succeed", canalAdapter.getInstance() + "-"
|
logger.info("Start adapter for canal-client mq topic: {} succeed",
|
||||||
+ group.getGroupId());
|
canalAdapter.getInstance() + "-" + group.getGroupId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ("rocketMQ".equalsIgnoreCase(canalClientConfig.getMode())) {
|
} else if ("rocketMQ".equalsIgnoreCase(canalClientConfig.getMode())) {
|
||||||
@@ -114,7 +116,7 @@ public class CanalAdapterLoader {
|
|||||||
List<List<OuterAdapter>> canalOuterAdapterGroups = new ArrayList<>();
|
List<List<OuterAdapter>> canalOuterAdapterGroups = new ArrayList<>();
|
||||||
List<OuterAdapter> canalOuterAdapters = new ArrayList<>();
|
List<OuterAdapter> canalOuterAdapters = new ArrayList<>();
|
||||||
for (OuterAdapterConfig config : group.getOuterAdapters()) {
|
for (OuterAdapterConfig config : group.getOuterAdapters()) {
|
||||||
loadConnector(config, canalOuterAdapters);
|
loadAdapter(config, canalOuterAdapters);
|
||||||
}
|
}
|
||||||
canalOuterAdapterGroups.add(canalOuterAdapters);
|
canalOuterAdapterGroups.add(canalOuterAdapters);
|
||||||
CanalAdapterRocketMQWorker rocketMQWorker = new CanalAdapterRocketMQWorker(canalClientConfig,
|
CanalAdapterRocketMQWorker rocketMQWorker = new CanalAdapterRocketMQWorker(canalClientConfig,
|
||||||
@@ -128,14 +130,14 @@ public class CanalAdapterLoader {
|
|||||||
canalMQWorker.put(canalAdapter.getInstance() + "-rocketmq-" + group.getGroupId(), rocketMQWorker);
|
canalMQWorker.put(canalAdapter.getInstance() + "-rocketmq-" + group.getGroupId(), rocketMQWorker);
|
||||||
rocketMQWorker.start();
|
rocketMQWorker.start();
|
||||||
|
|
||||||
logger.info("Start adapter for canal-client mq topic: {} succeed", canalAdapter.getInstance() + "-"
|
logger.info("Start adapter for canal-client mq topic: {} succeed",
|
||||||
+ group.getGroupId());
|
canalAdapter.getInstance() + "-" + group.getGroupId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadConnector(OuterAdapterConfig config, List<OuterAdapter> canalOutConnectors) {
|
private void loadAdapter(OuterAdapterConfig config, List<OuterAdapter> canalOutConnectors) {
|
||||||
try {
|
try {
|
||||||
OuterAdapter adapter;
|
OuterAdapter adapter;
|
||||||
adapter = loader.getExtension(config.getName(), StringUtils.trimToEmpty(config.getKey()));
|
adapter = loader.getExtension(config.getName(), StringUtils.trimToEmpty(config.getKey()));
|
||||||
@@ -143,7 +145,23 @@ public class CanalAdapterLoader {
|
|||||||
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
||||||
// 替换ClassLoader
|
// 替换ClassLoader
|
||||||
Thread.currentThread().setContextClassLoader(adapter.getClass().getClassLoader());
|
Thread.currentThread().setContextClassLoader(adapter.getClass().getClassLoader());
|
||||||
adapter.init(config);
|
Environment env = (Environment) SpringContext.getBean(Environment.class);
|
||||||
|
Properties evnProperties = null;
|
||||||
|
if (env instanceof StandardEnvironment) {
|
||||||
|
evnProperties = new Properties();
|
||||||
|
for (PropertySource<?> propertySource : ((StandardEnvironment) env).getPropertySources()) {
|
||||||
|
if (propertySource instanceof EnumerablePropertySource) {
|
||||||
|
String[] names = ((EnumerablePropertySource<?>) propertySource).getPropertyNames();
|
||||||
|
for (String name : names) {
|
||||||
|
Object val = propertySource.getProperty(name);
|
||||||
|
if (val != null) {
|
||||||
|
evnProperties.put(name, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.init(config, evnProperties);
|
||||||
Thread.currentThread().setContextClassLoader(cl);
|
Thread.currentThread().setContextClassLoader(cl);
|
||||||
canalOutConnectors.add(adapter);
|
canalOutConnectors.add(adapter);
|
||||||
logger.info("Load canal adapter: {} succeed", config.getName());
|
logger.info("Load canal adapter: {} succeed", config.getName());
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ public class CanalAdapterRocketMQWorker extends AbstractCanalAdapterWorker {
|
|||||||
this.canalClientConfig = canalClientConfig;
|
this.canalClientConfig = canalClientConfig;
|
||||||
this.topic = topic;
|
this.topic = topic;
|
||||||
this.flatMessage = flatMessage;
|
this.flatMessage = flatMessage;
|
||||||
this.canalDestination = topic;
|
super.canalDestination = topic;
|
||||||
|
super.groupId = groupId;
|
||||||
this.connector = new RocketMQCanalConnector(nameServers,
|
this.connector = new RocketMQCanalConnector(nameServers,
|
||||||
topic,
|
topic,
|
||||||
groupId,
|
groupId,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.alibaba.otter.canal.adapter.launcher.monitor.remote;
|
package com.alibaba.otter.canal.adapter.launcher.monitor.remote;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
@@ -19,6 +18,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.alibaba.druid.pool.DruidDataSource;
|
import com.alibaba.druid.pool.DruidDataSource;
|
||||||
|
import com.alibaba.otter.canal.common.utils.CommonUtils;
|
||||||
import com.alibaba.otter.canal.common.utils.NamedThreadFactory;
|
import com.alibaba.otter.canal.common.utils.NamedThreadFactory;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.collect.MapMaker;
|
import com.google.common.collect.MapMaker;
|
||||||
@@ -115,7 +115,7 @@ public class DbRemoteConfigLoader implements RemoteConfigLoader {
|
|||||||
* @param content 文件内容
|
* @param content 文件内容
|
||||||
*/
|
*/
|
||||||
private void overrideLocalCanalConfig(String content) {
|
private void overrideLocalCanalConfig(String content) {
|
||||||
try (FileWriter writer = new FileWriter(getConfPath() + "application.yml")) {
|
try (FileWriter writer = new FileWriter(CommonUtils.getConfPath() + "application.yml")) {
|
||||||
writer.write(content);
|
writer.write(content);
|
||||||
writer.flush();
|
writer.flush();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -139,9 +139,7 @@ public class DbRemoteConfigLoader implements RemoteConfigLoader {
|
|||||||
/**
|
/**
|
||||||
* 加载有变动的adapter配置
|
* 加载有变动的adapter配置
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void loadModifiedAdapterConfigs() {
|
private void loadModifiedAdapterConfigs() {
|
||||||
Map<String, ConfigItem>[] res = new Map[2];
|
|
||||||
Map<String, ConfigItem> remoteConfigStatus = new HashMap<>();
|
Map<String, ConfigItem> remoteConfigStatus = new HashMap<>();
|
||||||
String sql = "select id, category, name, modified_time from canal_adapter_config";
|
String sql = "select id, category, name, modified_time from canal_adapter_config";
|
||||||
try (Connection conn = dataSource.getConnection();
|
try (Connection conn = dataSource.getConnection();
|
||||||
@@ -209,41 +207,6 @@ public class DbRemoteConfigLoader implements RemoteConfigLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean deleteDir(File dirFile) {
|
|
||||||
if (!dirFile.exists()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dirFile.isFile()) {
|
|
||||||
return dirFile.delete();
|
|
||||||
} else {
|
|
||||||
File[] files = dirFile.listFiles();
|
|
||||||
if (files == null || files.length == 0) {
|
|
||||||
return dirFile.delete();
|
|
||||||
}
|
|
||||||
for (File file : files) {
|
|
||||||
deleteDir(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dirFile.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取conf文件夹所在路径
|
|
||||||
*
|
|
||||||
* @return 路径地址
|
|
||||||
*/
|
|
||||||
private String getConfPath() {
|
|
||||||
String classpath = this.getClass().getResource("/").getPath();
|
|
||||||
String confPath = classpath + "../conf/";
|
|
||||||
if (new File(confPath).exists()) {
|
|
||||||
return confPath;
|
|
||||||
} else {
|
|
||||||
return classpath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动监听数据库变化
|
* 启动监听数据库变化
|
||||||
*/
|
*/
|
||||||
@@ -280,44 +243,4 @@ public class DbRemoteConfigLoader implements RemoteConfigLoader {
|
|||||||
logger.error(e.getMessage(), e);
|
logger.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RemoteAdapterMonitorImpl implements RemoteAdapterMonitor {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAdd(ConfigItem configItem) {
|
|
||||||
this.onModify(configItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onModify(ConfigItem configItem) {
|
|
||||||
String confPath = getConfPath();
|
|
||||||
String category = configItem.getCategory();
|
|
||||||
File categoryDir = new File(confPath + category);
|
|
||||||
if (!categoryDir.isDirectory()) {
|
|
||||||
boolean mkDirs = categoryDir.mkdirs();
|
|
||||||
if (!mkDirs) {
|
|
||||||
logger.info("## Create adapter category dir error: {}", category);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String name = configItem.getName();
|
|
||||||
try (FileWriter writer = new FileWriter(
|
|
||||||
confPath + category + "/" + configItem.getName())) {
|
|
||||||
writer.write(configItem.getContent());
|
|
||||||
writer.flush();
|
|
||||||
logger.info("## Loaded remote adapter config: {}/{}", category, name);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error(e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDelete(String name) {
|
|
||||||
File file = new File(getConfPath() + name);
|
|
||||||
if (file.exists()) {
|
|
||||||
deleteDir(file);
|
|
||||||
logger.info("## Deleted and reloaded remote adapter config: {}", name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.alibaba.otter.canal.adapter.launcher.monitor.remote;
|
||||||
|
|
||||||
|
import com.alibaba.otter.canal.common.utils.CommonUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程配置监听器实现
|
||||||
|
*
|
||||||
|
* @author rewerma 2019-01-25 下午05:20:16
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
public class RemoteAdapterMonitorImpl implements RemoteAdapterMonitor {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RemoteAdapterMonitorImpl.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdd(ConfigItem configItem) {
|
||||||
|
this.onModify(configItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onModify(ConfigItem configItem) {
|
||||||
|
String confPath = CommonUtils.getConfPath();
|
||||||
|
String category = configItem.getCategory();
|
||||||
|
File categoryDir = new File(confPath + category);
|
||||||
|
if (!categoryDir.isDirectory()) {
|
||||||
|
boolean mkDirs = categoryDir.mkdirs();
|
||||||
|
if (!mkDirs) {
|
||||||
|
logger.info("## Create adapter category dir error: {}", category);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String name = configItem.getName();
|
||||||
|
try (FileWriter writer = new FileWriter(confPath + category + "/" + configItem.getName())) {
|
||||||
|
writer.write(configItem.getContent());
|
||||||
|
writer.flush();
|
||||||
|
logger.info("## Loaded remote adapter config: {}/{}", category, name);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDelete(String name) {
|
||||||
|
File file = new File(CommonUtils.getConfPath() + name);
|
||||||
|
if (file.exists()) {
|
||||||
|
CommonUtils.deleteDir(file);
|
||||||
|
logger.info("## Deleted and reloaded remote adapter config: {}", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.alibaba.otter.canal.client.adapter.logger;
|
package com.alibaba.otter.canal.client.adapter.logger;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -25,7 +26,7 @@ public class LoggerAdapterExample implements OuterAdapter {
|
|||||||
private Logger logger = LoggerFactory.getLogger(this.getClass());
|
private Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(OuterAdapterConfig configuration) {
|
public void init(OuterAdapterConfig configuration, Properties envProperties) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,6 @@
|
|||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.yaml</groupId>
|
|
||||||
<artifactId>snakeyaml</artifactId>
|
|
||||||
<version>1.19</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>mysql</groupId>
|
<groupId>mysql</groupId>
|
||||||
<artifactId>mysql-connector-java</artifactId>
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import java.sql.SQLException;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
@@ -27,12 +25,7 @@ import com.alibaba.otter.canal.client.adapter.rdb.service.RdbEtlService;
|
|||||||
import com.alibaba.otter.canal.client.adapter.rdb.service.RdbMirrorDbSyncService;
|
import com.alibaba.otter.canal.client.adapter.rdb.service.RdbMirrorDbSyncService;
|
||||||
import com.alibaba.otter.canal.client.adapter.rdb.service.RdbSyncService;
|
import com.alibaba.otter.canal.client.adapter.rdb.service.RdbSyncService;
|
||||||
import com.alibaba.otter.canal.client.adapter.rdb.support.SyncUtil;
|
import com.alibaba.otter.canal.client.adapter.rdb.support.SyncUtil;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
import com.alibaba.otter.canal.client.adapter.support.*;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
|
||||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
|
||||||
import com.alibaba.otter.canal.client.adapter.support.OuterAdapterConfig;
|
|
||||||
import com.alibaba.otter.canal.client.adapter.support.SPI;
|
|
||||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RDB适配器实现类
|
* RDB适配器实现类
|
||||||
@@ -56,6 +49,8 @@ public class RdbAdapter implements OuterAdapter {
|
|||||||
|
|
||||||
private RdbConfigMonitor rdbConfigMonitor;
|
private RdbConfigMonitor rdbConfigMonitor;
|
||||||
|
|
||||||
|
private Properties envProperties;
|
||||||
|
|
||||||
public Map<String, MappingConfig> getRdbMapping() {
|
public Map<String, MappingConfig> getRdbMapping() {
|
||||||
return rdbMapping;
|
return rdbMapping;
|
||||||
}
|
}
|
||||||
@@ -74,8 +69,9 @@ public class RdbAdapter implements OuterAdapter {
|
|||||||
* @param configuration 外部适配器配置信息
|
* @param configuration 外部适配器配置信息
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void init(OuterAdapterConfig configuration) {
|
public void init(OuterAdapterConfig configuration, Properties envProperties) {
|
||||||
Map<String, MappingConfig> rdbMappingTmp = ConfigLoader.load();
|
this.envProperties = envProperties;
|
||||||
|
Map<String, MappingConfig> rdbMappingTmp = ConfigLoader.load(envProperties);
|
||||||
// 过滤不匹配的key的配置
|
// 过滤不匹配的key的配置
|
||||||
rdbMappingTmp.forEach((key, mappingConfig) -> {
|
rdbMappingTmp.forEach((key, mappingConfig) -> {
|
||||||
if ((mappingConfig.getOuterAdapterKey() == null && configuration.getKey() == null)
|
if ((mappingConfig.getOuterAdapterKey() == null && configuration.getKey() == null)
|
||||||
@@ -93,9 +89,15 @@ public class RdbAdapter implements OuterAdapter {
|
|||||||
String configName = entry.getKey();
|
String configName = entry.getKey();
|
||||||
MappingConfig mappingConfig = entry.getValue();
|
MappingConfig mappingConfig = entry.getValue();
|
||||||
if (!mappingConfig.getDbMapping().getMirrorDb()) {
|
if (!mappingConfig.getDbMapping().getMirrorDb()) {
|
||||||
String key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "."
|
String key;
|
||||||
+ mappingConfig.getDbMapping().getDatabase() + "."
|
if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||||
+ mappingConfig.getDbMapping().getTable();
|
key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "-"
|
||||||
|
+ StringUtils.trimToEmpty(mappingConfig.getGroupId()) + "_"
|
||||||
|
+ mappingConfig.getDbMapping().getDatabase() + "-" + mappingConfig.getDbMapping().getTable();
|
||||||
|
} else {
|
||||||
|
key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "_"
|
||||||
|
+ mappingConfig.getDbMapping().getDatabase() + "-" + mappingConfig.getDbMapping().getTable();
|
||||||
|
}
|
||||||
Map<String, MappingConfig> configMap = mappingConfigCache.computeIfAbsent(key,
|
Map<String, MappingConfig> configMap = mappingConfigCache.computeIfAbsent(key,
|
||||||
k1 -> new ConcurrentHashMap<>());
|
k1 -> new ConcurrentHashMap<>());
|
||||||
configMap.put(configName, mappingConfig);
|
configMap.put(configName, mappingConfig);
|
||||||
@@ -158,7 +160,7 @@ public class RdbAdapter implements OuterAdapter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
rdbSyncService.sync(mappingConfigCache, dmls);
|
rdbSyncService.sync(mappingConfigCache, dmls, envProperties);
|
||||||
rdbMirrorDbSyncService.sync(dmls);
|
rdbMirrorDbSyncService.sync(dmls);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.alibaba.otter.canal.client.adapter.rdb.config;
|
|||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
|
||||||
|
|
||||||
|
import com.alibaba.otter.canal.client.adapter.config.YmlConfigBinder;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,17 +25,18 @@ public class ConfigLoader {
|
|||||||
*
|
*
|
||||||
* @return 配置名/配置文件名--对象
|
* @return 配置名/配置文件名--对象
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
public static Map<String, MappingConfig> load(Properties envProperties) {
|
||||||
public static Map<String, MappingConfig> load() {
|
|
||||||
logger.info("## Start loading rdb mapping config ... ");
|
logger.info("## Start loading rdb mapping config ... ");
|
||||||
|
|
||||||
Map<String, MappingConfig> result = new LinkedHashMap<>();
|
Map<String, MappingConfig> result = new LinkedHashMap<>();
|
||||||
|
|
||||||
Map<String, String> configContentMap = MappingConfigsLoader.loadConfigs("rdb");
|
Map<String, String> configContentMap = MappingConfigsLoader.loadConfigs("rdb");
|
||||||
configContentMap.forEach((fileName, content) -> {
|
configContentMap.forEach((fileName, content) -> {
|
||||||
Map configMap = new Yaml().loadAs(content, Map.class); // yml自带的对象反射不是很稳定
|
MappingConfig config = YmlConfigBinder
|
||||||
JSONObject configJson = new JSONObject(configMap);
|
.bindYmlToObj(null, content, MappingConfig.class, null, envProperties);
|
||||||
MappingConfig config = configJson.toJavaObject(MappingConfig.class);
|
if (config == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
config.validate();
|
config.validate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public class MappingConfig {
|
|||||||
|
|
||||||
private String destination; // canal实例或MQ的topic
|
private String destination; // canal实例或MQ的topic
|
||||||
|
|
||||||
|
private String groupId; // groupId
|
||||||
|
|
||||||
private String outerAdapterKey; // 对应适配器的key
|
private String outerAdapterKey; // 对应适配器的key
|
||||||
|
|
||||||
private Boolean concurrent; // 是否并行同步
|
private Boolean concurrent; // 是否并行同步
|
||||||
@@ -31,6 +33,14 @@ public class MappingConfig {
|
|||||||
this.dataSourceKey = dataSourceKey;
|
this.dataSourceKey = dataSourceKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getGroupId() {
|
||||||
|
return groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroupId(String groupId) {
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getOuterAdapterKey() {
|
public String getOuterAdapterKey() {
|
||||||
return outerAdapterKey;
|
return outerAdapterKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public class MirrorDbConfig {
|
|||||||
|
|
||||||
private String fileName;
|
private String fileName;
|
||||||
private MappingConfig mappingConfig;
|
private MappingConfig mappingConfig;
|
||||||
Map<String, MappingConfig> tableConfig = new ConcurrentHashMap<>();
|
private Map<String, MappingConfig> tableConfig = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public static MirrorDbConfig create(String fileName, MappingConfig mappingConfig) {
|
public static MirrorDbConfig create(String fileName, MappingConfig mappingConfig) {
|
||||||
return new MirrorDbConfig(fileName, mappingConfig);
|
return new MirrorDbConfig(fileName, mappingConfig);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.io.File;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.alibaba.otter.canal.client.adapter.rdb.config.MirrorDbConfig;
|
|
||||||
import org.apache.commons.io.filefilter.FileFilterUtils;
|
import org.apache.commons.io.filefilter.FileFilterUtils;
|
||||||
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
|
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
|
||||||
import org.apache.commons.io.monitor.FileAlterationMonitor;
|
import org.apache.commons.io.monitor.FileAlterationMonitor;
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
import com.alibaba.otter.canal.client.adapter.rdb.support.SyncUtil;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.alibaba.otter.canal.client.adapter.rdb.config.MappingConfig;
|
import com.alibaba.otter.canal.client.adapter.rdb.config.MappingConfig;
|
||||||
import com.alibaba.otter.canal.client.adapter.rdb.config.MappingConfig.DbMapping;
|
import com.alibaba.otter.canal.client.adapter.rdb.config.MappingConfig.DbMapping;
|
||||||
|
import com.alibaba.otter.canal.client.adapter.rdb.support.SyncUtil;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
@@ -36,7 +36,8 @@ public class RdbEtlService {
|
|||||||
/**
|
/**
|
||||||
* 导入数据
|
* 导入数据
|
||||||
*/
|
*/
|
||||||
public static EtlResult importData(DataSource srcDS, DataSource targetDS, MappingConfig config, List<String> params) {
|
public static EtlResult importData(DataSource srcDS, DataSource targetDS, MappingConfig config,
|
||||||
|
List<String> params) {
|
||||||
EtlResult etlResult = new EtlResult();
|
EtlResult etlResult = new EtlResult();
|
||||||
AtomicLong successCount = new AtomicLong();
|
AtomicLong successCount = new AtomicLong();
|
||||||
List<String> errMsg = new ArrayList<>();
|
List<String> errMsg = new ArrayList<>();
|
||||||
@@ -53,8 +54,8 @@ public class RdbEtlService {
|
|||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
// 拼接sql
|
// 拼接sql
|
||||||
StringBuilder sql = new StringBuilder("SELECT * FROM " + dbMapping.getDatabase() + "."
|
StringBuilder sql = new StringBuilder(
|
||||||
+ dbMapping.getTable());
|
"SELECT * FROM " + dbMapping.getDatabase() + "." + dbMapping.getTable());
|
||||||
|
|
||||||
// 拼接条件
|
// 拼接条件
|
||||||
appendCondition(params, dbMapping, srcDS, sql);
|
appendCondition(params, dbMapping, srcDS, sql);
|
||||||
@@ -91,12 +92,8 @@ public class RdbEtlService {
|
|||||||
} else {
|
} else {
|
||||||
sqlFinal = sql + " LIMIT " + offset + "," + cnt;
|
sqlFinal = sql + " LIMIT " + offset + "," + cnt;
|
||||||
}
|
}
|
||||||
Future<Boolean> future = executor.submit(() -> executeSqlImport(srcDS,
|
Future<Boolean> future = executor
|
||||||
targetDS,
|
.submit(() -> executeSqlImport(srcDS, targetDS, sqlFinal, dbMapping, successCount, errMsg));
|
||||||
sqlFinal,
|
|
||||||
dbMapping,
|
|
||||||
successCount,
|
|
||||||
errMsg));
|
|
||||||
futures.add(future);
|
futures.add(future);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,11 +106,11 @@ public class RdbEtlService {
|
|||||||
executeSqlImport(srcDS, targetDS, sql.toString(), dbMapping, successCount, errMsg);
|
executeSqlImport(srcDS, targetDS, sql.toString(), dbMapping, successCount, errMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(dbMapping.getTable() + " etl completed in: " + (System.currentTimeMillis() - start) / 1000
|
logger.info(
|
||||||
+ "s!");
|
dbMapping.getTable() + " etl completed in: " + (System.currentTimeMillis() - start) / 1000 + "s!");
|
||||||
|
|
||||||
etlResult.setResultMessage("导入目标表 " + SyncUtil.getDbTableName(dbMapping) + " 数据:" + successCount.get()
|
etlResult
|
||||||
+ " 条");
|
.setResultMessage("导入目标表 " + SyncUtil.getDbTableName(dbMapping) + " 数据:" + successCount.get() + " 条");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error(e.getMessage(), e);
|
logger.error(e.getMessage(), e);
|
||||||
errMsg.add(hbaseTable + " etl failed! ==>" + e.getMessage());
|
errMsg.add(hbaseTable + " etl failed! ==>" + e.getMessage());
|
||||||
@@ -127,8 +124,8 @@ public class RdbEtlService {
|
|||||||
return etlResult;
|
return etlResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void appendCondition(List<String> params, DbMapping dbMapping, DataSource ds, StringBuilder sql)
|
private static void appendCondition(List<String> params, DbMapping dbMapping, DataSource ds,
|
||||||
throws SQLException {
|
StringBuilder sql) throws SQLException {
|
||||||
if (params != null && params.size() == 1 && dbMapping.getEtlCondition() == null) {
|
if (params != null && params.size() == 1 && dbMapping.getEtlCondition() == null) {
|
||||||
AtomicBoolean stExists = new AtomicBoolean(false);
|
AtomicBoolean stExists = new AtomicBoolean(false);
|
||||||
// 验证是否有SYS_TIME字段
|
// 验证是否有SYS_TIME字段
|
||||||
@@ -145,9 +142,9 @@ public class RdbEtlService {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} );
|
});
|
||||||
if (stExists.get()) {
|
if (stExists.get()) {
|
||||||
sql.append(" WHERE SYS_TIME >= '").append(params.get(0)).append("' ");
|
sql.append(" WHERE SYS_TIME >= '").append(params.get(0)).append("' ");
|
||||||
}
|
}
|
||||||
@@ -168,13 +165,12 @@ public class RdbEtlService {
|
|||||||
private static boolean executeSqlImport(DataSource srcDS, DataSource targetDS, String sql, DbMapping dbMapping,
|
private static boolean executeSqlImport(DataSource srcDS, DataSource targetDS, String sql, DbMapping dbMapping,
|
||||||
AtomicLong successCount, List<String> errMsg) {
|
AtomicLong successCount, List<String> errMsg) {
|
||||||
try {
|
try {
|
||||||
Util.sqlRS(srcDS, sql, rs -> {
|
Map<String, String> columnsMap = new LinkedHashMap<>();
|
||||||
int idx = 1;
|
Map<String, Integer> columnType = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
Util.sqlRS(targetDS, "SELECT * FROM " + SyncUtil.getDbTableName(dbMapping) + " LIMIT 1 ", rs -> {
|
||||||
try {
|
try {
|
||||||
boolean completed = false;
|
|
||||||
|
|
||||||
Map<String, Integer> columnType = new LinkedHashMap<>();
|
|
||||||
ResultSetMetaData rsd = rs.getMetaData();
|
ResultSetMetaData rsd = rs.getMetaData();
|
||||||
int columnCount = rsd.getColumnCount();
|
int columnCount = rsd.getColumnCount();
|
||||||
List<String> columns = new ArrayList<>();
|
List<String> columns = new ArrayList<>();
|
||||||
@@ -183,91 +179,105 @@ public class RdbEtlService {
|
|||||||
columns.add(rsd.getColumnName(i));
|
columns.add(rsd.getColumnName(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> columnsMap = SyncUtil.getColumnsMap(dbMapping, columns);
|
columnsMap.putAll(SyncUtil.getColumnsMap(dbMapping, columns));
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Util.sqlRS(srcDS, sql, rs -> {
|
||||||
|
int idx = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean completed = false;
|
||||||
|
|
||||||
// if (dbMapping.isMapAll()) {
|
// if (dbMapping.isMapAll()) {
|
||||||
// columnsMap = dbMapping.getAllColumns();
|
// columnsMap = dbMapping.getAllColumns();
|
||||||
// } else {
|
// } else {
|
||||||
// columnsMap = dbMapping.getTargetColumns();
|
// columnsMap = dbMapping.getTargetColumns();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
StringBuilder insertSql = new StringBuilder();
|
StringBuilder insertSql = new StringBuilder();
|
||||||
insertSql.append("INSERT INTO ").append(SyncUtil.getDbTableName(dbMapping)).append(" (");
|
insertSql.append("INSERT INTO ").append(SyncUtil.getDbTableName(dbMapping)).append(" (");
|
||||||
columnsMap.forEach((targetColumnName, srcColumnName) -> insertSql.append(targetColumnName).append(","));
|
columnsMap
|
||||||
|
.forEach((targetColumnName, srcColumnName) -> insertSql.append(targetColumnName).append(","));
|
||||||
|
|
||||||
int len = insertSql.length();
|
int len = insertSql.length();
|
||||||
insertSql.delete(len - 1, len).append(") VALUES (");
|
insertSql.delete(len - 1, len).append(") VALUES (");
|
||||||
int mapLen = columnsMap.size();
|
int mapLen = columnsMap.size();
|
||||||
for (int i = 0; i < mapLen; i++) {
|
for (int i = 0; i < mapLen; i++) {
|
||||||
insertSql.append("?,");
|
insertSql.append("?,");
|
||||||
}
|
}
|
||||||
len = insertSql.length();
|
len = insertSql.length();
|
||||||
insertSql.delete(len - 1, len).append(")");
|
insertSql.delete(len - 1, len).append(")");
|
||||||
try (Connection connTarget = targetDS.getConnection();
|
try (Connection connTarget = targetDS.getConnection();
|
||||||
PreparedStatement pstmt = connTarget.prepareStatement(insertSql.toString())) {
|
PreparedStatement pstmt = connTarget.prepareStatement(insertSql.toString())) {
|
||||||
connTarget.setAutoCommit(false);
|
connTarget.setAutoCommit(false);
|
||||||
|
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
pstmt.clearParameters();
|
pstmt.clearParameters();
|
||||||
|
|
||||||
// 删除数据
|
// 删除数据
|
||||||
Map<String, Object> values = new LinkedHashMap<>();
|
Map<String, Object> values = new LinkedHashMap<>();
|
||||||
StringBuilder deleteSql = new StringBuilder("DELETE FROM " + SyncUtil.getDbTableName(dbMapping)
|
StringBuilder deleteSql = new StringBuilder(
|
||||||
+ " WHERE ");
|
"DELETE FROM " + SyncUtil.getDbTableName(dbMapping) + " WHERE ");
|
||||||
appendCondition(dbMapping, deleteSql, values, rs);
|
appendCondition(dbMapping, deleteSql, values, rs);
|
||||||
try (PreparedStatement pstmt2 = connTarget.prepareStatement(deleteSql.toString())) {
|
try (PreparedStatement pstmt2 = connTarget.prepareStatement(deleteSql.toString())) {
|
||||||
int k = 1;
|
int k = 1;
|
||||||
for (Object val : values.values()) {
|
for (Object val : values.values()) {
|
||||||
pstmt2.setObject(k++, val);
|
pstmt2.setObject(k++, val);
|
||||||
}
|
}
|
||||||
pstmt2.execute();
|
pstmt2.execute();
|
||||||
}
|
|
||||||
|
|
||||||
int i = 1;
|
|
||||||
for (Map.Entry<String, String> entry : columnsMap.entrySet()) {
|
|
||||||
String targetClolumnName = entry.getKey();
|
|
||||||
String srcColumnName = entry.getValue();
|
|
||||||
if (srcColumnName == null) {
|
|
||||||
srcColumnName = targetClolumnName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer type = columnType.get(targetClolumnName.toLowerCase());
|
int i = 1;
|
||||||
|
for (Map.Entry<String, String> entry : columnsMap.entrySet()) {
|
||||||
|
String targetClolumnName = entry.getKey();
|
||||||
|
String srcColumnName = entry.getValue();
|
||||||
|
if (srcColumnName == null) {
|
||||||
|
srcColumnName = targetClolumnName;
|
||||||
|
}
|
||||||
|
|
||||||
Object value = rs.getObject(srcColumnName);
|
Integer type = columnType.get(targetClolumnName.toLowerCase());
|
||||||
if (value != null) {
|
|
||||||
SyncUtil.setPStmt(type, pstmt, value, i);
|
Object value = rs.getObject(srcColumnName);
|
||||||
} else {
|
if (value != null) {
|
||||||
pstmt.setNull(i, type);
|
SyncUtil.setPStmt(type, pstmt, value, i);
|
||||||
|
} else {
|
||||||
|
pstmt.setNull(i, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
i++;
|
pstmt.execute();
|
||||||
}
|
if (logger.isTraceEnabled()) {
|
||||||
|
logger.trace("Insert into target table, sql: {}", insertSql);
|
||||||
|
}
|
||||||
|
|
||||||
pstmt.execute();
|
if (idx % dbMapping.getCommitBatch() == 0) {
|
||||||
if (logger.isTraceEnabled()) {
|
connTarget.commit();
|
||||||
logger.trace("Insert into target table, sql: {}", insertSql);
|
completed = true;
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
successCount.incrementAndGet();
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("successful import count:" + successCount.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (!completed) {
|
||||||
if (idx % dbMapping.getCommitBatch() == 0) {
|
|
||||||
connTarget.commit();
|
connTarget.commit();
|
||||||
completed = true;
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
successCount.incrementAndGet();
|
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug("successful import count:" + successCount.get());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!completed) {
|
|
||||||
connTarget.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error(dbMapping.getTable() + " etl failed! ==>" + e.getMessage(), e);
|
logger.error(dbMapping.getTable() + " etl failed! ==>" + e.getMessage(), e);
|
||||||
errMsg.add(dbMapping.getTable() + " etl failed! ==>" + e.getMessage());
|
errMsg.add(dbMapping.getTable() + " etl failed! ==>" + e.getMessage());
|
||||||
}
|
}
|
||||||
return idx;
|
return idx;
|
||||||
} );
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error(e.getMessage(), e);
|
logger.error(e.getMessage(), e);
|
||||||
@@ -278,8 +288,8 @@ public class RdbEtlService {
|
|||||||
/**
|
/**
|
||||||
* 拼接目标表主键where条件
|
* 拼接目标表主键where条件
|
||||||
*/
|
*/
|
||||||
private static void appendCondition(DbMapping dbMapping, StringBuilder sql, Map<String, Object> values, ResultSet rs)
|
private static void appendCondition(DbMapping dbMapping, StringBuilder sql, Map<String, Object> values,
|
||||||
throws SQLException {
|
ResultSet rs) throws SQLException {
|
||||||
// 拼接主键
|
// 拼接主键
|
||||||
for (Map.Entry<String, String> entry : dbMapping.getTargetPk().entrySet()) {
|
for (Map.Entry<String, String> entry : dbMapping.getTargetPk().entrySet()) {
|
||||||
String targetColumnName = entry.getKey();
|
String targetColumnName = entry.getKey();
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ public class RdbMirrorDbSyncService {
|
|||||||
if (mirrorDbConfig == null) {
|
if (mirrorDbConfig == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (mirrorDbConfig.getMappingConfig() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (dml.getGroupId() != null && StringUtils.isNotEmpty(mirrorDbConfig.getMappingConfig().getGroupId())) {
|
||||||
|
if (!mirrorDbConfig.getMappingConfig().getGroupId().equals(dml.getGroupId())) {
|
||||||
|
continue; // 如果groupId不匹配则过滤
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (dml.getIsDdl() != null && dml.getIsDdl() && StringUtils.isNotEmpty(dml.getSql())) {
|
if (dml.getIsDdl() != null && dml.getIsDdl() && StringUtils.isNotEmpty(dml.getSql())) {
|
||||||
// DDL
|
// DDL
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
@@ -71,37 +80,35 @@ public class RdbMirrorDbSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!dmlList.isEmpty()) {
|
if (!dmlList.isEmpty()) {
|
||||||
rdbSyncService.sync(dmlList,
|
rdbSyncService.sync(dmlList, dml -> {
|
||||||
dml -> {
|
MirrorDbConfig mirrorDbConfig = mirrorDbConfigCache.get(dml.getDestination() + "." + dml.getDatabase());
|
||||||
MirrorDbConfig mirrorDbConfig = mirrorDbConfigCache.get(dml.getDestination() + "."
|
if (mirrorDbConfig == null) {
|
||||||
+ dml.getDatabase());
|
return false;
|
||||||
if (mirrorDbConfig == null) {
|
}
|
||||||
return false;
|
String table = dml.getTable();
|
||||||
}
|
MappingConfig config = mirrorDbConfig.getTableConfig().get(table);
|
||||||
String table = dml.getTable();
|
|
||||||
MappingConfig config = mirrorDbConfig.getTableConfig().get(table);
|
|
||||||
|
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.getConcurrent()) {
|
if (config.getConcurrent()) {
|
||||||
List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml);
|
List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml);
|
||||||
singleDmls.forEach(singleDml -> {
|
singleDmls.forEach(singleDml -> {
|
||||||
int hash = rdbSyncService.pkHash(config.getDbMapping(), singleDml.getData());
|
int hash = rdbSyncService.pkHash(config.getDbMapping(), singleDml.getData());
|
||||||
RdbSyncService.SyncItem syncItem = new RdbSyncService.SyncItem(config, singleDml);
|
RdbSyncService.SyncItem syncItem = new RdbSyncService.SyncItem(config, singleDml);
|
||||||
rdbSyncService.getDmlsPartition()[hash].add(syncItem);
|
rdbSyncService.getDmlsPartition()[hash].add(syncItem);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
int hash = 0;
|
int hash = 0;
|
||||||
List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml);
|
List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml);
|
||||||
singleDmls.forEach(singleDml -> {
|
singleDmls.forEach(singleDml -> {
|
||||||
RdbSyncService.SyncItem syncItem = new RdbSyncService.SyncItem(config, singleDml);
|
RdbSyncService.SyncItem syncItem = new RdbSyncService.SyncItem(config, singleDml);
|
||||||
rdbSyncService.getDmlsPartition()[hash].add(syncItem);
|
rdbSyncService.getDmlsPartition()[hash].add(syncItem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +126,7 @@ public class RdbMirrorDbSyncService {
|
|||||||
mappingConfig = new MappingConfig();
|
mappingConfig = new MappingConfig();
|
||||||
mappingConfig.setDataSourceKey(baseConfigMap.getDataSourceKey());
|
mappingConfig.setDataSourceKey(baseConfigMap.getDataSourceKey());
|
||||||
mappingConfig.setDestination(baseConfigMap.getDestination());
|
mappingConfig.setDestination(baseConfigMap.getDestination());
|
||||||
|
mappingConfig.setGroupId(baseConfigMap.getGroupId());
|
||||||
mappingConfig.setOuterAdapterKey(baseConfigMap.getOuterAdapterKey());
|
mappingConfig.setOuterAdapterKey(baseConfigMap.getOuterAdapterKey());
|
||||||
mappingConfig.setConcurrent(baseConfigMap.getConcurrent());
|
mappingConfig.setConcurrent(baseConfigMap.getConcurrent());
|
||||||
MappingConfig.DbMapping dbMapping = new MappingConfig.DbMapping();
|
MappingConfig.DbMapping dbMapping = new MappingConfig.DbMapping();
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.*;
|
import java.util.Properties;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
@@ -54,7 +59,6 @@ public class RdbSyncService {
|
|||||||
return columnsTypeCache;
|
return columnsTypeCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public RdbSyncService(DataSource dataSource, Integer threads, boolean skipDupException){
|
public RdbSyncService(DataSource dataSource, Integer threads, boolean skipDupException){
|
||||||
this(dataSource, threads, new ConcurrentHashMap<>(), skipDupException);
|
this(dataSource, threads, new ConcurrentHashMap<>(), skipDupException);
|
||||||
}
|
}
|
||||||
@@ -98,7 +102,7 @@ public class RdbSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (toExecute) {
|
if (toExecute) {
|
||||||
List<Future> futures = new ArrayList<>();
|
List<Future<Boolean>> futures = new ArrayList<>();
|
||||||
for (int i = 0; i < threads; i++) {
|
for (int i = 0; i < threads; i++) {
|
||||||
int j = i;
|
int j = i;
|
||||||
futures.add(executorThreads[i].submit(() -> {
|
futures.add(executorThreads[i].submit(() -> {
|
||||||
@@ -138,7 +142,7 @@ public class RdbSyncService {
|
|||||||
* @param mappingConfig 配置集合
|
* @param mappingConfig 配置集合
|
||||||
* @param dmls 批量 DML
|
* @param dmls 批量 DML
|
||||||
*/
|
*/
|
||||||
public void sync(Map<String, Map<String, MappingConfig>> mappingConfig, List<Dml> dmls) {
|
public void sync(Map<String, Map<String, MappingConfig>> mappingConfig, List<Dml> dmls, Properties envProperties) {
|
||||||
sync(dmls, dml -> {
|
sync(dmls, dml -> {
|
||||||
if (dml.getIsDdl() != null && dml.getIsDdl() && StringUtils.isNotEmpty(dml.getSql())) {
|
if (dml.getIsDdl() != null && dml.getIsDdl() && StringUtils.isNotEmpty(dml.getSql())) {
|
||||||
// DDL
|
// DDL
|
||||||
@@ -147,15 +151,24 @@ public class RdbSyncService {
|
|||||||
} else {
|
} else {
|
||||||
// DML
|
// DML
|
||||||
String destination = StringUtils.trimToEmpty(dml.getDestination());
|
String destination = StringUtils.trimToEmpty(dml.getDestination());
|
||||||
|
String groupId = StringUtils.trimToEmpty(dml.getGroupId());
|
||||||
String database = dml.getDatabase();
|
String database = dml.getDatabase();
|
||||||
String table = dml.getTable();
|
String table = dml.getTable();
|
||||||
Map<String, MappingConfig> configMap = mappingConfig.get(destination + "." + database + "." + table);
|
Map<String, MappingConfig> configMap;
|
||||||
|
if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||||
|
configMap = mappingConfig.get(destination + "-" + groupId + "_" + database + "-" + table);
|
||||||
|
} else {
|
||||||
|
configMap = mappingConfig.get(destination + "_" + database + "-" + table);
|
||||||
|
}
|
||||||
|
|
||||||
if (configMap == null) {
|
if (configMap == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean executed = false;
|
if (configMap.values().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (MappingConfig config : configMap.values()) {
|
for (MappingConfig config : configMap.values()) {
|
||||||
if (config.getConcurrent()) {
|
if (config.getConcurrent()) {
|
||||||
List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml);
|
List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml);
|
||||||
@@ -172,9 +185,8 @@ public class RdbSyncService {
|
|||||||
dmlsPartition[hash].add(syncItem);
|
dmlsPartition[hash].add(syncItem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
executed = true;
|
|
||||||
}
|
}
|
||||||
return executed;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
dataSourceKey: defaultDS
|
dataSourceKey: defaultDS
|
||||||
destination: example
|
destination: example
|
||||||
outerAdapterKey: oracle1
|
groupId: g1
|
||||||
|
outerAdapterKey: mysql1
|
||||||
concurrent: true
|
concurrent: true
|
||||||
dbMapping:
|
dbMapping:
|
||||||
database: mytest
|
database: mytest
|
||||||
table: user
|
table: user
|
||||||
targetTable: mytest.tb_user
|
targetTable: mytest2.user
|
||||||
targetPk:
|
targetPk:
|
||||||
id: id
|
id: id
|
||||||
# mapAll: true
|
# mapAll: true
|
||||||
@@ -17,9 +18,10 @@ dbMapping:
|
|||||||
test1:
|
test1:
|
||||||
|
|
||||||
|
|
||||||
# Mirror schema synchronize config
|
## Mirror schema synchronize config
|
||||||
#dataSourceKey: defaultDS
|
#dataSourceKey: defaultDS
|
||||||
#destination: example
|
#destination: example
|
||||||
|
#groupId: g1
|
||||||
#outerAdapterKey: mysql1
|
#outerAdapterKey: mysql1
|
||||||
#concurrent: true
|
#concurrent: true
|
||||||
#dbMapping:
|
#dbMapping:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class ConfigLoadTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLoad() {
|
public void testLoad() {
|
||||||
Map<String, MappingConfig> configMap = ConfigLoader.load();
|
Map<String, MappingConfig> configMap = ConfigLoader.load(null);
|
||||||
|
|
||||||
Assert.assertFalse(configMap.isEmpty());
|
Assert.assertFalse(configMap.isEmpty());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class Common {
|
|||||||
outerAdapterConfig.setProperties(properties);
|
outerAdapterConfig.setProperties(properties);
|
||||||
|
|
||||||
RdbAdapter adapter = new RdbAdapter();
|
RdbAdapter adapter = new RdbAdapter();
|
||||||
adapter.init(outerAdapterConfig);
|
adapter.init(outerAdapterConfig, null);
|
||||||
return adapter;
|
return adapter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.util.*;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import com.google.common.collect.MapMaker;
|
|
||||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||||
import org.apache.kafka.clients.consumer.KafkaConsumer;
|
import org.apache.kafka.clients.consumer.KafkaConsumer;
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ import org.springframework.beans.BeanUtils;
|
|||||||
* @Date 2018/12/17
|
* @Date 2018/12/17
|
||||||
*/
|
*/
|
||||||
public class KafkaFlatMessage extends FlatMessage {
|
public class KafkaFlatMessage extends FlatMessage {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 5748024400508080710L;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kafka 消息 offset
|
* Kafka 消息 offset
|
||||||
*/
|
*/
|
||||||
private long offset;
|
private long offset;
|
||||||
|
|
||||||
public KafkaFlatMessage(FlatMessage message, long offset) {
|
public KafkaFlatMessage(FlatMessage message, long offset){
|
||||||
super(message.getId());
|
super(message.getId());
|
||||||
BeanUtils.copyProperties(message, this);
|
BeanUtils.copyProperties(message, this);
|
||||||
this.offset = offset;
|
this.offset = offset;
|
||||||
|
|||||||
@@ -11,18 +11,20 @@ import org.springframework.beans.BeanUtils;
|
|||||||
* @Date 2018/12/17
|
* @Date 2018/12/17
|
||||||
*/
|
*/
|
||||||
public class KafkaMessage extends Message {
|
public class KafkaMessage extends Message {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -293120358490119447L;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kafka 消息 offset
|
* Kafka 消息 offset
|
||||||
*/
|
*/
|
||||||
private long offset;
|
private long offset;
|
||||||
|
|
||||||
public KafkaMessage(Message message, long offset) {
|
public KafkaMessage(Message message, long offset){
|
||||||
super(message.getId());
|
super(message.getId());
|
||||||
BeanUtils.copyProperties(message, this);
|
BeanUtils.copyProperties(message, this);
|
||||||
this.offset = offset;
|
this.offset = offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public long getOffset() {
|
public long getOffset() {
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.alibaba.otter.canal.common.utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用工具类
|
||||||
|
*
|
||||||
|
* @author rewerma 2019-01-25 下午05:20:16
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
public class CommonUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取conf文件夹所在路径
|
||||||
|
*
|
||||||
|
* @return 路径地址
|
||||||
|
*/
|
||||||
|
public static String getConfPath() {
|
||||||
|
String classpath = CommonUtils.class.getResource("/").getPath();
|
||||||
|
String confPath = classpath + "../conf/";
|
||||||
|
if (new File(confPath).exists()) {
|
||||||
|
return confPath;
|
||||||
|
} else {
|
||||||
|
return classpath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件夹
|
||||||
|
*
|
||||||
|
* @param dirFile 文件夹对象
|
||||||
|
* @return 是否删除成功
|
||||||
|
*/
|
||||||
|
public static boolean deleteDir(File dirFile) {
|
||||||
|
if (!dirFile.exists()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirFile.isFile()) {
|
||||||
|
return dirFile.delete();
|
||||||
|
} else {
|
||||||
|
File[] files = dirFile.listFiles();
|
||||||
|
if (files == null || files.length == 0) {
|
||||||
|
return dirFile.delete();
|
||||||
|
}
|
||||||
|
for (File file : files) {
|
||||||
|
deleteDir(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ public class CanalConstants {
|
|||||||
public static final String CANAL_MQ_TRANSACTION = ROOT + "." + "mq.transaction";
|
public static final String CANAL_MQ_TRANSACTION = ROOT + "." + "mq.transaction";
|
||||||
public static final String CANAL_ALIYUN_ACCESSKEY = ROOT + "." + "aliyun.accessKey";
|
public static final String CANAL_ALIYUN_ACCESSKEY = ROOT + "." + "aliyun.accessKey";
|
||||||
public static final String CANAL_ALIYUN_SECRETKEY = ROOT + "." + "aliyun.secretKey";
|
public static final String CANAL_ALIYUN_SECRETKEY = ROOT + "." + "aliyun.secretKey";
|
||||||
|
public static final String CANAL_MQ_PROPERTIES = ROOT + "." + "mq.properties";
|
||||||
|
|
||||||
public static String getInstanceModeKey(String destination) {
|
public static String getInstanceModeKey(String destination) {
|
||||||
return MessageFormat.format(INSTANCE_MODE_TEMPLATE, destination);
|
return MessageFormat.format(INSTANCE_MODE_TEMPLATE, destination);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.alibaba.otter.canal.deployer;
|
package com.alibaba.otter.canal.deployer;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
@@ -162,7 +163,16 @@ public class CanalStater {
|
|||||||
if (!StringUtils.isEmpty(transaction)) {
|
if (!StringUtils.isEmpty(transaction)) {
|
||||||
mqProperties.setTransaction(Boolean.valueOf(transaction));
|
mqProperties.setTransaction(Boolean.valueOf(transaction));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (Object key : properties.keySet()) {
|
||||||
|
key = StringUtils.trim(key.toString());
|
||||||
|
if (((String) key).startsWith(CanalConstants.CANAL_MQ_PROPERTIES)) {
|
||||||
|
String value = CanalController.getProperty(properties, (String) key);
|
||||||
|
String subKey = ((String) key).substring(CanalConstants.CANAL_MQ_PROPERTIES.length() + 1);
|
||||||
|
mqProperties.getProperties().put(subKey, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return mqProperties;
|
return mqProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.alibaba.otter.canal.deployer.monitor.remote;
|
package com.alibaba.otter.canal.deployer.monitor.remote;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -18,6 +17,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.alibaba.druid.pool.DruidDataSource;
|
import com.alibaba.druid.pool.DruidDataSource;
|
||||||
|
import com.alibaba.otter.canal.common.utils.CommonUtils;
|
||||||
import com.alibaba.otter.canal.common.utils.NamedThreadFactory;
|
import com.alibaba.otter.canal.common.utils.NamedThreadFactory;
|
||||||
import com.alibaba.otter.canal.deployer.CanalConstants;
|
import com.alibaba.otter.canal.deployer.CanalConstants;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
@@ -101,7 +101,7 @@ public class DbRemoteConfigLoader implements RemoteConfigLoader {
|
|||||||
* @param content 远程配置内容文本
|
* @param content 远程配置内容文本
|
||||||
*/
|
*/
|
||||||
private void overrideLocalCanalConfig(String content) {
|
private void overrideLocalCanalConfig(String content) {
|
||||||
try (FileWriter writer = new FileWriter(getConfPath() + "canal.properties")) {
|
try (FileWriter writer = new FileWriter(CommonUtils.getConfPath() + "canal.properties")) {
|
||||||
writer.write(content);
|
writer.write(content);
|
||||||
writer.flush();
|
writer.flush();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -213,26 +213,6 @@ public class DbRemoteConfigLoader implements RemoteConfigLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean deleteDir(File dirFile) {
|
|
||||||
if (!dirFile.exists()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dirFile.isFile()) {
|
|
||||||
return dirFile.delete();
|
|
||||||
} else {
|
|
||||||
File[] files = dirFile.listFiles();
|
|
||||||
if (files == null || files.length == 0) {
|
|
||||||
return dirFile.delete();
|
|
||||||
}
|
|
||||||
for (File file : files) {
|
|
||||||
deleteDir(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dirFile.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听 canal 主配置和 instance 配置变化
|
* 监听 canal 主配置和 instance 配置变化
|
||||||
*
|
*
|
||||||
@@ -281,53 +261,4 @@ public class DbRemoteConfigLoader implements RemoteConfigLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取conf文件夹所在路径
|
|
||||||
*
|
|
||||||
* @return 路径地址
|
|
||||||
*/
|
|
||||||
private String getConfPath() {
|
|
||||||
String classpath = this.getClass().getResource("/").getPath();
|
|
||||||
String confPath = classpath + ".." + File.separator + "conf" + File.separator;
|
|
||||||
if (new File(confPath).exists()) {
|
|
||||||
return confPath;
|
|
||||||
} else {
|
|
||||||
return classpath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 远程xxx/instance.properties配置监听器实现
|
|
||||||
*/
|
|
||||||
private class RemoteInstanceMonitorImpl implements RemoteInstanceMonitor {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAdd(ConfigItem configItem) {
|
|
||||||
this.onModify(configItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onModify(ConfigItem configItem) {
|
|
||||||
File instanceDir = new File(getConfPath() + configItem.getName());
|
|
||||||
if (!instanceDir.exists()) {
|
|
||||||
instanceDir.mkdirs();
|
|
||||||
}
|
|
||||||
try (FileWriter writer = new FileWriter(getConfPath() + configItem.getName() + "/instance.properties")) {
|
|
||||||
writer.write(configItem.getContent());
|
|
||||||
writer.flush();
|
|
||||||
logger.info("## Loaded remote instance config: {}/instance.properties ", configItem.getName());
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error(e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDelete(String instanceName) {
|
|
||||||
File file = new File(getConfPath() + instanceName + "/");
|
|
||||||
if (file.exists()) {
|
|
||||||
deleteDir(file);
|
|
||||||
logger.info("## Deleted and loaded remote instance config: {} ", instanceName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.alibaba.otter.canal.deployer.monitor.remote;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.alibaba.otter.canal.common.utils.CommonUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程xxx/instance.properties配置监听器实现
|
||||||
|
*
|
||||||
|
* @author rewerma 2019-01-25 下午05:20:16
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
public class RemoteInstanceMonitorImpl implements RemoteInstanceMonitor {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RemoteInstanceMonitorImpl.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdd(ConfigItem configItem) {
|
||||||
|
this.onModify(configItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onModify(ConfigItem configItem) {
|
||||||
|
String confDir = CommonUtils.getConfPath() + configItem.getName();
|
||||||
|
File instanceDir = new File(confDir);
|
||||||
|
if (!instanceDir.exists()) {
|
||||||
|
boolean mkDirs = instanceDir.mkdirs();
|
||||||
|
if (!mkDirs) {
|
||||||
|
logger.info("## Error to create instance config dir: {}", configItem.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (FileWriter writer = new FileWriter(confDir + "/instance.properties")) {
|
||||||
|
writer.write(configItem.getContent());
|
||||||
|
writer.flush();
|
||||||
|
logger.info("## Loaded remote instance config: {}/instance.properties ", configItem.getName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDelete(String instanceName) {
|
||||||
|
File file = new File(CommonUtils.getConfPath() + instanceName + "/");
|
||||||
|
if (file.exists()) {
|
||||||
|
CommonUtils.deleteDir(file);
|
||||||
|
logger.info("## Deleted and loaded remote instance config: {} ", instanceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -117,3 +117,4 @@ canal.mq.compressionType = none
|
|||||||
canal.mq.acks = all
|
canal.mq.acks = all
|
||||||
# use transaction for kafka flatMessage batch produce
|
# use transaction for kafka flatMessage batch produce
|
||||||
canal.mq.transaction = false
|
canal.mq.transaction = false
|
||||||
|
#canal.mq.properties. =
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.alibaba.otter.canal.common;
|
package com.alibaba.otter.canal.common;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* kafka 配置项
|
* kafka 配置项
|
||||||
*
|
*
|
||||||
@@ -8,22 +10,23 @@ package com.alibaba.otter.canal.common;
|
|||||||
*/
|
*/
|
||||||
public class MQProperties {
|
public class MQProperties {
|
||||||
|
|
||||||
private String servers = "127.0.0.1:6667";
|
private String servers = "127.0.0.1:6667";
|
||||||
private int retries = 0;
|
private int retries = 0;
|
||||||
private int batchSize = 16384;
|
private int batchSize = 16384;
|
||||||
private int lingerMs = 1;
|
private int lingerMs = 1;
|
||||||
private int maxRequestSize = 1048576;
|
private int maxRequestSize = 1048576;
|
||||||
private long bufferMemory = 33554432L;
|
private long bufferMemory = 33554432L;
|
||||||
private boolean filterTransactionEntry = true;
|
private boolean filterTransactionEntry = true;
|
||||||
private String producerGroup = "Canal-Producer";
|
private String producerGroup = "Canal-Producer";
|
||||||
private int canalBatchSize = 50;
|
private int canalBatchSize = 50;
|
||||||
private Long canalGetTimeout = 100L;
|
private Long canalGetTimeout = 100L;
|
||||||
private boolean flatMessage = true;
|
private boolean flatMessage = true;
|
||||||
private String compressionType = "none";
|
private String compressionType = "none";
|
||||||
private String acks = "all";
|
private String acks = "all";
|
||||||
private String aliyunAccessKey = "";
|
private String aliyunAccessKey = "";
|
||||||
private String aliyunSecretKey = "";
|
private String aliyunSecretKey = "";
|
||||||
private boolean transaction = false; // 是否开启事务
|
private boolean transaction = false; // 是否开启事务
|
||||||
|
private Properties properties = new Properties();
|
||||||
|
|
||||||
public static class CanalDestination {
|
public static class CanalDestination {
|
||||||
|
|
||||||
@@ -210,4 +213,12 @@ public class MQProperties {
|
|||||||
public void setTransaction(boolean transaction) {
|
public void setTransaction(boolean transaction) {
|
||||||
this.transaction = transaction;
|
this.transaction = transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Properties getProperties() {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProperties(Properties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,12 @@ public class CanalKafkaProducer implements CanalMQProducer {
|
|||||||
properties.put("max.request.size", kafkaProperties.getMaxRequestSize());
|
properties.put("max.request.size", kafkaProperties.getMaxRequestSize());
|
||||||
properties.put("buffer.memory", kafkaProperties.getBufferMemory());
|
properties.put("buffer.memory", kafkaProperties.getBufferMemory());
|
||||||
properties.put("key.serializer", StringSerializer.class.getName());
|
properties.put("key.serializer", StringSerializer.class.getName());
|
||||||
if(kafkaProperties.getTransaction()){
|
|
||||||
|
if (!kafkaProperties.getProperties().isEmpty()) {
|
||||||
|
properties.putAll(kafkaProperties.getProperties());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kafkaProperties.getTransaction()) {
|
||||||
properties.put("transactional.id", "canal-transactional-id");
|
properties.put("transactional.id", "canal-transactional-id");
|
||||||
} else {
|
} else {
|
||||||
properties.put("retries", kafkaProperties.getRetries());
|
properties.put("retries", kafkaProperties.getRetries());
|
||||||
|
|||||||
Reference in New Issue
Block a user