diff --git a/client-adapter/elasticsearch/pom.xml b/client-adapter/elasticsearch/pom.xml
new file mode 100644
index 00000000..b5a8c11b
--- /dev/null
+++ b/client-adapter/elasticsearch/pom.xml
@@ -0,0 +1,77 @@
+
+
+
+ canal.client-adapter
+ com.alibaba.otter
+ 1.1.1-SNAPSHOT
+
+ 4.0.0
+ com.alibaba.otter
+ client-adapter.elasticsearch
+ jar
+ canal client adapter ElasticSearch module for otter ${project.version}
+
+
+
+ com.alibaba.otter
+ client-adapter.common
+ ${project.version}
+ provided
+
+
+ org.yaml
+ snakeyaml
+ 1.19
+ provided
+
+
+ com.alibaba.fastsql
+ fastsql
+ 2.0.0_preview_644
+
+
+ org.elasticsearch
+ elasticsearch
+ 6.2.3
+
+
+ org.elasticsearch.client
+ transport
+ 6.2.3
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 2.4
+
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/ESAdapter.java b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/ESAdapter.java
new file mode 100644
index 00000000..8e2523e4
--- /dev/null
+++ b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/ESAdapter.java
@@ -0,0 +1,82 @@
+package com.alibaba.otter.canal.client.adapter.es;
+
+import java.util.List;
+import java.util.Map;
+
+import com.alibaba.otter.canal.client.adapter.es.service.ESSyncService;
+import org.elasticsearch.client.transport.TransportClient;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.transport.client.PreBuiltTransportClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.alibaba.otter.canal.client.adapter.OuterAdapter;
+import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
+import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfigLoader;
+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外部适配器
+ *
+ * @author rewerma 2018-10-20
+ * @version 1.0.0
+ */
+@SPI("es")
+public class ESAdapter implements OuterAdapter {
+
+ private static Logger logger = LoggerFactory.getLogger(ESAdapter.class);
+
+ private static volatile Map esSyncConfig = null; // 文件名对应配置
+
+ private TransportClient transportClient;
+
+ private ESSyncService esSyncService;
+
+ @Override
+ public void init(OuterAdapterConfig configuration) {
+ try {
+ if (esSyncConfig == null) {
+ synchronized (ESSyncConfig.class) {
+ if (esSyncConfig == null) {
+ esSyncConfig = ESSyncConfigLoader.load();
+ }
+ }
+ }
+ Map properties = configuration.getProperties();
+ Settings.Builder settingBuilder = Settings.builder();
+ properties.forEach(settingBuilder::put);
+ Settings settings = settingBuilder.build();
+ transportClient = new PreBuiltTransportClient(settings);
+ esSyncService = new ESSyncService(transportClient);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void sync(Dml dml) {
+ }
+
+ @Override
+ public EtlResult etl(String task, List params) {
+ return null;
+ }
+
+ @Override
+ public Map count(String task) {
+ return null;
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+
+ @Override
+ public String getDestination(String task) {
+ return null;
+ }
+}
diff --git a/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/ESSyncConfig.java b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/ESSyncConfig.java
new file mode 100644
index 00000000..be0b712b
--- /dev/null
+++ b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/ESSyncConfig.java
@@ -0,0 +1,137 @@
+package com.alibaba.otter.canal.client.adapter.es.config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ESSyncConfig {
+
+ private String dataSourceKey; // 数据源key
+
+ private ESMapping esMapping;
+
+ public void validate() {
+ if (esMapping._index == null) {
+ throw new NullPointerException("esMapping._index");
+ }
+ if (esMapping._type == null) {
+ throw new NullPointerException("esMapping._type");
+ }
+ if (esMapping._id == null && esMapping.pk == null) {
+ throw new NullPointerException("esMapping._id and esMapping.pk");
+ }
+ if (esMapping.sql == null) {
+ throw new NullPointerException("esMapping.sql");
+ }
+ }
+
+ public String getDataSourceKey() {
+ return dataSourceKey;
+ }
+
+ public void setDataSourceKey(String dataSourceKey) {
+ this.dataSourceKey = dataSourceKey;
+ }
+
+ public ESMapping getEsMapping() {
+ return esMapping;
+ }
+
+ public void setEsMapping(ESMapping esMapping) {
+ this.esMapping = esMapping;
+ }
+
+ public static class ESMapping {
+
+ private String _index;
+ private String _type;
+ private String _id;
+ private String pk;
+ private String parent;
+ private String sql;
+ private List skips = new ArrayList<>();
+ private boolean alwaysSql = false;
+ private int commitBatch = 1000;
+ private String etlCondition;
+
+ public String get_index() {
+ return _index;
+ }
+
+ public void set_index(String _index) {
+ this._index = _index;
+ }
+
+ public String get_type() {
+ return _type;
+ }
+
+ public void set_type(String _type) {
+ this._type = _type;
+ }
+
+ public String get_id() {
+ return _id;
+ }
+
+ public void set_id(String _id) {
+ this._id = _id;
+ }
+
+ public String getPk() {
+ return pk;
+ }
+
+ public void setPk(String pk) {
+ this.pk = pk;
+ }
+
+ public String getParent() {
+ return parent;
+ }
+
+ public void setParent(String parent) {
+ this.parent = parent;
+ }
+
+ public List getSkips() {
+ return skips;
+ }
+
+ public void setSkips(List skips) {
+ this.skips = skips;
+ }
+
+ public String getSql() {
+ return sql;
+ }
+
+ public void setSql(String sql) {
+ this.sql = sql;
+ }
+
+ public boolean isAlwaysSql() {
+ return alwaysSql;
+ }
+
+ public void setAlwaysSql(boolean alwaysSql) {
+ this.alwaysSql = alwaysSql;
+ }
+
+ public int getCommitBatch() {
+ return commitBatch;
+ }
+
+ public void setCommitBatch(int commitBatch) {
+ this.commitBatch = commitBatch;
+ }
+
+ public String getEtlCondition() {
+ return etlCondition;
+ }
+
+ public void setEtlCondition(String etlCondition) {
+ this.etlCondition = etlCondition;
+ }
+
+ }
+}
diff --git a/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/ESSyncConfigLoader.java b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/ESSyncConfigLoader.java
new file mode 100644
index 00000000..7e156409
--- /dev/null
+++ b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/ESSyncConfigLoader.java
@@ -0,0 +1,89 @@
+package com.alibaba.otter.canal.client.adapter.es.config;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.Yaml;
+
+import com.alibaba.otter.canal.client.adapter.support.AdapterConfigs;
+
+public class ESSyncConfigLoader {
+
+ private static Logger logger = LoggerFactory.getLogger(ESSyncConfigLoader.class);
+
+ private static final String BASE_PATH = "es";
+
+ public static Map load() {
+ logger.info("## Start loading mapping config ... ");
+
+ Map result = new LinkedHashMap<>();
+
+ Collection configs = AdapterConfigs.get("es");
+ for (String c : configs) {
+ if (c == null) {
+ continue;
+ }
+ c = c.trim();
+ if (c.equals("") || c.startsWith("#")) {
+ continue;
+ }
+
+ ESSyncConfig config;
+ String configContent = null;
+
+ if (c.endsWith(".yml")) {
+ configContent = readConfigContent(BASE_PATH + "/" + c);
+ }
+
+ config = new Yaml().loadAs(configContent, ESSyncConfig.class);
+
+ try {
+ config.validate();
+ } catch (Exception e) {
+ throw new RuntimeException("ERROR Config: " + c + " " + e.getMessage(), e);
+ }
+ result.put(c, config);
+ }
+
+ logger.info("## Mapping config loaded");
+ return result;
+ }
+
+ public static String readConfigContent(String config) {
+ InputStream in = null;
+ try {
+ // 先取本地文件,再取类路径
+ File configFile = new File("config/" + config);
+ if (configFile.exists()) {
+ in = new FileInputStream(configFile);
+ } else {
+ in = ESSyncConfigLoader.class.getClassLoader().getResourceAsStream(config);
+ }
+ if (in == null) {
+ throw new RuntimeException("Config file not found.");
+ }
+
+ byte[] bytes = new byte[in.available()];
+ in.read(bytes);
+ return new String(bytes, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException("Read yml config error ", e);
+ } finally {
+ try {
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+}
diff --git a/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/SchemaItem.java b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/SchemaItem.java
new file mode 100644
index 00000000..88e29eb6
--- /dev/null
+++ b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/SchemaItem.java
@@ -0,0 +1,364 @@
+package com.alibaba.otter.canal.client.adapter.es.config;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SchemaItem {
+
+ private Map aliasTableItems = new LinkedHashMap<>();
+ private Map selectFields = new LinkedHashMap<>();
+ private String sql;
+
+ private volatile Map> tableItemAliases;
+ private volatile Map> columnFields;
+
+ public void init() {
+ this.getTableItemAliases();
+ this.getColumnFields();
+ aliasTableItems.values().forEach(tableItem -> {
+ tableItem.getRelationTableFields();
+ tableItem.getRelationSelectFields();
+ });
+ }
+
+ public Map getAliasTableItems() {
+ return aliasTableItems;
+ }
+
+ public void setAliasTableItems(Map aliasTableItems) {
+ this.aliasTableItems = aliasTableItems;
+ }
+
+ public String getSql() {
+ return sql;
+ }
+
+ public void setSql(String sql) {
+ this.sql = sql;
+ }
+
+ public Map getSelectFields() {
+ return selectFields;
+ }
+
+ public void setSelectFields(Map selectFields) {
+ this.selectFields = selectFields;
+ }
+
+ public Map> getTableItemAliases() {
+ if (tableItemAliases == null) {
+ synchronized (SchemaItem.class) {
+ if (tableItemAliases == null) {
+ tableItemAliases = new LinkedHashMap<>();
+ aliasTableItems.forEach((alias, tableItem) -> {
+ List aliases = tableItemAliases
+ .computeIfAbsent(tableItem.getTableName().toLowerCase(), k -> new ArrayList<>());
+ aliases.add(tableItem);
+ });
+ }
+ }
+ }
+ return tableItemAliases;
+ }
+
+ public Map> getColumnFields() {
+ if (columnFields == null) {
+ synchronized (SchemaItem.class) {
+ if (columnFields == null) {
+ columnFields = new LinkedHashMap<>();
+ getSelectFields()
+ .forEach((fieldName, fieldItem) -> fieldItem.getColumnItems().forEach(columnItem -> {
+ TableItem tableItem = getAliasTableItems().get(columnItem.getOwner());
+ if (!tableItem.isSubQuery()) {
+ List fieldItems = columnFields.computeIfAbsent(
+ columnItem.getOwner() + "." + columnItem.getColumnName(),
+ k -> new ArrayList<>());
+ fieldItems.add(fieldItem);
+ } else {
+ tableItem.getSubQueryFields().forEach(subQueryField -> {
+ List fieldItems = columnFields.computeIfAbsent(
+ columnItem.getOwner() + "." + subQueryField.getColumn().getColumnName(),
+ k -> new ArrayList<>());
+ fieldItems.add(fieldItem);
+ });
+ }
+ }));
+ }
+ }
+ }
+ return columnFields;
+
+ }
+
+ public TableItem getMainTable() {
+ if (!aliasTableItems.isEmpty()) {
+ return aliasTableItems.values().iterator().next();
+ } else {
+ return null;
+ }
+ }
+
+ public static class TableItem {
+
+ private SchemaItem schemaItem;
+
+ private String schema;
+ private String tableName;
+ private String alias;
+ private String subQuerySql;
+ private List subQueryFields = new ArrayList<>();
+ private List relationFields = new ArrayList<>();
+
+ private boolean main;
+ private boolean subQuery;
+
+ private volatile List relationTableFields; // 当前表关联条件字段
+ private volatile List relationSelectFieldItem; // 关联条件字段在select中的对应字段
+
+ public TableItem(SchemaItem schemaItem){
+ this.schemaItem = schemaItem;
+ }
+
+ public SchemaItem getSchemaItem() {
+ return schemaItem;
+ }
+
+ public void setSchemaItem(SchemaItem schemaItem) {
+ this.schemaItem = schemaItem;
+ }
+
+ public String getSchema() {
+ return schema;
+ }
+
+ public void setSchema(String schema) {
+ this.schema = schema;
+ }
+
+ public String getTableName() {
+ return tableName;
+ }
+
+ public void setTableName(String tableName) {
+ this.tableName = tableName;
+ }
+
+ public String getAlias() {
+ return alias;
+ }
+
+ public void setAlias(String alias) {
+ this.alias = alias;
+ }
+
+ public String getSubQuerySql() {
+ return subQuerySql;
+ }
+
+ public void setSubQuerySql(String subQuerySql) {
+ this.subQuerySql = subQuerySql;
+ }
+
+ public boolean isMain() {
+ return main;
+ }
+
+ public void setMain(boolean main) {
+ this.main = main;
+ }
+
+ public boolean isSubQuery() {
+ return subQuery;
+ }
+
+ public void setSubQuery(boolean subQuery) {
+ this.subQuery = subQuery;
+ }
+
+ public List getSubQueryFields() {
+ return subQueryFields;
+ }
+
+ public void setSubQueryFields(List subQueryFields) {
+ this.subQueryFields = subQueryFields;
+ }
+
+ public List getRelationFields() {
+ return relationFields;
+ }
+
+ public void setRelationFields(List relationFields) {
+ this.relationFields = relationFields;
+ }
+
+ public List getRelationTableFields() {
+ if (relationTableFields == null) {
+ synchronized (SchemaItem.class) {
+ if (relationTableFields == null) {
+ relationTableFields = new ArrayList<>();
+ getRelationFields().forEach(relationFieldsPair -> {
+ FieldItem leftFieldItem = relationFieldsPair.getLeftFieldItem();
+ FieldItem rightFieldItem = relationFieldsPair.getRightFieldItem();
+ if (getAlias().equals(leftFieldItem.getOwner())) {
+ relationTableFields.add(leftFieldItem);
+ } else if (getAlias().equals(rightFieldItem.getOwner())) {
+ relationTableFields.add(rightFieldItem);
+ }
+ });
+ }
+ }
+ }
+ return relationTableFields;
+ }
+
+ public List getRelationSelectFields() {
+ if (relationSelectFieldItem == null) {
+ synchronized (SchemaItem.class) {
+ if (relationSelectFieldItem == null) {
+ relationSelectFieldItem = new ArrayList<>();
+ getRelationFields().forEach(relationFieldsPair -> {
+ FieldItem leftFieldItem = relationFieldsPair.getLeftFieldItem();
+ List selectFieldItem = getSchemaItem().getColumnFields()
+ .get(leftFieldItem.getOwner() + "." + leftFieldItem.getColumn().getColumnName());
+ if (selectFieldItem != null && !selectFieldItem.isEmpty()) {
+ relationSelectFieldItem.addAll(selectFieldItem);
+ } else {
+ FieldItem rightFieldItem = relationFieldsPair.getRightFieldItem();
+ selectFieldItem = getSchemaItem().getColumnFields()
+ .get(rightFieldItem.getOwner() + "." + rightFieldItem.getColumn().getColumnName());
+ if (selectFieldItem != null && !selectFieldItem.isEmpty()) {
+ relationSelectFieldItem.addAll(selectFieldItem);
+ } else {
+ throw new UnsupportedOperationException(
+ "Relation condition column must in select columns.");
+ }
+ }
+ });
+ }
+ }
+ }
+ return relationSelectFieldItem;
+ }
+ }
+
+ public static class RelationFieldsPair {
+
+ private FieldItem leftFieldItem;
+ private FieldItem rightFieldItem;
+
+ public RelationFieldsPair(FieldItem leftFieldItem, FieldItem rightFieldItem){
+ this.leftFieldItem = leftFieldItem;
+ this.rightFieldItem = rightFieldItem;
+ }
+
+ public FieldItem getLeftFieldItem() {
+ return leftFieldItem;
+ }
+
+ public void setLeftFieldItem(FieldItem leftFieldItem) {
+ this.leftFieldItem = leftFieldItem;
+ }
+
+ public FieldItem getRightFieldItem() {
+ return rightFieldItem;
+ }
+
+ public void setRightFieldItem(FieldItem rightFieldItem) {
+ this.rightFieldItem = rightFieldItem;
+ }
+ }
+
+ public static class FieldItem {
+
+ private String fieldName;
+ private List columnItems = new ArrayList<>();
+ private List owners = new ArrayList<>();
+
+ private boolean method;
+ private boolean binaryOp;
+
+ public String getFieldName() {
+ return fieldName;
+ }
+
+ public void setFieldName(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ public List getColumnItems() {
+ return columnItems;
+ }
+
+ public void setColumnItems(List columnItems) {
+ this.columnItems = columnItems;
+ }
+
+ public boolean isMethod() {
+ return method;
+ }
+
+ public void setMethod(boolean method) {
+ this.method = method;
+ }
+
+ public boolean isBinaryOp() {
+ return binaryOp;
+ }
+
+ public void setBinaryOp(boolean binaryOp) {
+ this.binaryOp = binaryOp;
+ }
+
+ public List getOwners() {
+ return owners;
+ }
+
+ public void setOwners(List owners) {
+ this.owners = owners;
+ }
+
+ public void addColumn(ColumnItem columnItem) {
+ columnItems.add(columnItem);
+ }
+
+ public ColumnItem getColumn() {
+ if (!columnItems.isEmpty()) {
+ return columnItems.get(0);
+ } else {
+ return null;
+ }
+ }
+
+ public String getOwner() {
+ if (!owners.isEmpty()) {
+ return owners.get(0);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public static class ColumnItem {
+
+ private String owner;
+ private String columnName;
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public String getColumnName() {
+ return columnName;
+ }
+
+ public void setColumnName(String columnName) {
+ this.columnName = columnName;
+ }
+ }
+}
diff --git a/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/SqlParser.java b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/SqlParser.java
new file mode 100644
index 00000000..78fb93ec
--- /dev/null
+++ b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/SqlParser.java
@@ -0,0 +1,205 @@
+package com.alibaba.otter.canal.client.adapter.es.config;
+
+import static com.alibaba.fastsql.sql.ast.expr.SQLBinaryOperator.BooleanAnd;
+import static com.alibaba.fastsql.sql.ast.expr.SQLBinaryOperator.Equality;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.alibaba.fastsql.sql.SQLUtils;
+import com.alibaba.fastsql.sql.ast.SQLExpr;
+import com.alibaba.fastsql.sql.ast.expr.SQLBinaryOpExpr;
+import com.alibaba.fastsql.sql.ast.expr.SQLIdentifierExpr;
+import com.alibaba.fastsql.sql.ast.expr.SQLMethodInvokeExpr;
+import com.alibaba.fastsql.sql.ast.expr.SQLPropertyExpr;
+import com.alibaba.fastsql.sql.ast.statement.*;
+import com.alibaba.fastsql.sql.dialect.mysql.ast.statement.MySqlSelectQueryBlock;
+import com.alibaba.fastsql.sql.dialect.mysql.parser.MySqlStatementParser;
+import com.alibaba.fastsql.sql.parser.ParserException;
+import com.alibaba.fastsql.sql.parser.SQLStatementParser;
+import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.ColumnItem;
+import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.FieldItem;
+import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.RelationFieldsPair;
+import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.TableItem;
+
+/**
+ * ES同步指定sql格式解析
+ *
+ * @author rewerma 2018-10-26 下午03:45:49
+ * @version 1.0.0
+ */
+public class SqlParser {
+
+ /**
+ * 解析sql
+ *
+ * @param sql
+ * @return
+ */
+ public static SchemaItem parse(String sql) {
+ try {
+ SQLStatementParser parser = new MySqlStatementParser(sql);
+ SQLSelectStatement statement = (SQLSelectStatement) parser.parseStatement();
+ MySqlSelectQueryBlock sqlSelectQueryBlock = (MySqlSelectQueryBlock) statement.getSelect().getQuery();
+
+ SchemaItem schemaItem = new SchemaItem();
+ schemaItem.setSql(SQLUtils.toMySqlString(sqlSelectQueryBlock));
+ SQLTableSource sqlTableSource = sqlSelectQueryBlock.getFrom();
+ List tableItems = new ArrayList<>();
+ SqlParser.visitSelectTable(schemaItem, sqlTableSource, tableItems, null);
+ tableItems.forEach(tableItem -> schemaItem.getAliasTableItems().put(tableItem.getAlias(), tableItem));
+
+ List fieldItems = collectSelectQueryFields(sqlSelectQueryBlock);
+ fieldItems.forEach(fieldItem -> schemaItem.getSelectFields().put(fieldItem.getFieldName(), fieldItem));
+
+ schemaItem.init();
+
+ if (schemaItem.getAliasTableItems().isEmpty() || schemaItem.getSelectFields().isEmpty()) {
+ throw new ParserException("Parse sql error");
+ }
+ return schemaItem;
+ } catch (Exception e) {
+ throw new ParserException();
+ }
+ }
+
+ /**
+ * 归集字段
+ *
+ * @param sqlSelectQueryBlock
+ * @return
+ */
+ private static List collectSelectQueryFields(MySqlSelectQueryBlock sqlSelectQueryBlock) {
+ return sqlSelectQueryBlock.getSelectList().stream().map(selectItem -> {
+ FieldItem fieldItem = new FieldItem();
+ fieldItem.setFieldName(selectItem.getAlias());
+ visitColumn(selectItem.getExpr(), fieldItem);
+ return fieldItem;
+ }).collect(Collectors.toList());
+ }
+
+ /**
+ * 解析字段
+ *
+ * @param expr
+ * @param fieldItem
+ */
+ private static void visitColumn(SQLExpr expr, FieldItem fieldItem) {
+ if (expr instanceof SQLIdentifierExpr) {
+ // 无owner
+ SQLIdentifierExpr identifierExpr = (SQLIdentifierExpr) expr;
+ if (fieldItem.getFieldName() == null) {
+ fieldItem.setFieldName(identifierExpr.getName());
+ }
+ ColumnItem columnItem = new ColumnItem();
+ columnItem.setColumnName(identifierExpr.getName());
+ fieldItem.getOwners().add(null);
+ fieldItem.addColumn(columnItem);
+ } else if (expr instanceof SQLPropertyExpr) {
+ // 有owner
+ SQLPropertyExpr sqlPropertyExpr = (SQLPropertyExpr) expr;
+ if (fieldItem.getFieldName() == null) {
+ fieldItem.setFieldName(sqlPropertyExpr.getName());
+ }
+ fieldItem.getOwners().add(sqlPropertyExpr.getOwnernName());
+ ColumnItem columnItem = new ColumnItem();
+ columnItem.setColumnName(sqlPropertyExpr.getName());
+ columnItem.setOwner(sqlPropertyExpr.getOwnernName());
+ fieldItem.addColumn(columnItem);
+ } else if (expr instanceof SQLMethodInvokeExpr) {
+ SQLMethodInvokeExpr methodInvokeExpr = (SQLMethodInvokeExpr) expr;
+ fieldItem.setMethod(true);
+ for (SQLExpr sqlExpr : methodInvokeExpr.getArguments()) {
+ visitColumn(sqlExpr, fieldItem);
+ }
+ } else if (expr instanceof SQLBinaryOpExpr) {
+ SQLBinaryOpExpr sqlBinaryOpExpr = (SQLBinaryOpExpr) expr;
+ fieldItem.setBinaryOp(true);
+ visitColumn(sqlBinaryOpExpr.getLeft(), fieldItem);
+ visitColumn(sqlBinaryOpExpr.getRight(), fieldItem);
+ }
+ }
+
+ /**
+ * 解析表
+ */
+ private static void visitSelectTable(SchemaItem schemaItem, SQLTableSource sqlTableSource,
+ List tableItems, TableItem tableItemTmp) {
+ if (sqlTableSource instanceof SQLExprTableSource) {
+ SQLExprTableSource sqlExprTableSource = (SQLExprTableSource) sqlTableSource;
+ TableItem tableItem;
+ if (tableItemTmp != null) {
+ tableItem = tableItemTmp;
+ } else {
+ tableItem = new TableItem(schemaItem);
+ }
+ tableItem.setSchema(sqlExprTableSource.getSchema());
+ tableItem.setTableName(sqlExprTableSource.getTableName());
+ if (tableItem.getAlias() == null) {
+ tableItem.setAlias(sqlExprTableSource.getAlias());
+ }
+ if (tableItems.isEmpty()) {
+ // 第一张表为主表
+ tableItem.setMain(true);
+ }
+ tableItems.add(tableItem);
+ } else if (sqlTableSource instanceof SQLJoinTableSource) {
+ SQLJoinTableSource sqlJoinTableSource = (SQLJoinTableSource) sqlTableSource;
+ SQLTableSource leftTableSource = sqlJoinTableSource.getLeft();
+ visitSelectTable(schemaItem, leftTableSource, tableItems, null);
+ SQLTableSource rightTableSource = sqlJoinTableSource.getRight();
+ TableItem rightTableItem = new TableItem(schemaItem);
+ // 解析on条件字段
+ visitOnCondition(sqlJoinTableSource.getCondition(), rightTableItem);
+ visitSelectTable(schemaItem, rightTableSource, tableItems, rightTableItem);
+
+ } else if (sqlTableSource instanceof SQLSubqueryTableSource) {
+ SQLSubqueryTableSource subQueryTableSource = (SQLSubqueryTableSource) sqlTableSource;
+ MySqlSelectQueryBlock sqlSelectQuery = (MySqlSelectQueryBlock) subQueryTableSource.getSelect().getQuery();
+ TableItem tableItem;
+ if (tableItemTmp != null) {
+ tableItem = tableItemTmp;
+ } else {
+ tableItem = new TableItem(schemaItem);
+ }
+ tableItem.setAlias(subQueryTableSource.getAlias());
+ tableItem.setSubQuerySql(SQLUtils.toMySqlString(sqlSelectQuery));
+ tableItem.setSubQuery(true);
+ tableItem.setSubQueryFields(collectSelectQueryFields(sqlSelectQuery));
+ visitSelectTable(schemaItem, sqlSelectQuery.getFrom(), tableItems, tableItem);
+ }
+ }
+
+ /**
+ * 解析on条件
+ *
+ * @param expr
+ * @param tableItem
+ */
+ private static void visitOnCondition(SQLExpr expr, TableItem tableItem) {
+ if (!(expr instanceof SQLBinaryOpExpr)) {
+ throw new UnsupportedOperationException();
+ }
+ SQLBinaryOpExpr sqlBinaryOpExpr = (SQLBinaryOpExpr) expr;
+ if (sqlBinaryOpExpr.getOperator() == BooleanAnd) {
+ visitOnCondition(sqlBinaryOpExpr.getLeft(), tableItem);
+ visitOnCondition(sqlBinaryOpExpr.getRight(), tableItem);
+ } else if (sqlBinaryOpExpr.getOperator() == Equality) {
+ FieldItem leftFieldItem = new FieldItem();
+ visitColumn(sqlBinaryOpExpr.getLeft(), leftFieldItem);
+ if (leftFieldItem.getColumnItems().size() != 1 || leftFieldItem.isMethod() || leftFieldItem.isBinaryOp()) {
+ throw new UnsupportedOperationException("Unsupported for complex of on-condition");
+ }
+ FieldItem rightFieldItem = new FieldItem();
+ visitColumn(sqlBinaryOpExpr.getRight(), rightFieldItem);
+ if (rightFieldItem.getColumnItems().size() != 1 || rightFieldItem.isMethod()
+ || rightFieldItem.isBinaryOp()) {
+ throw new UnsupportedOperationException("Unsupported for complex of on-condition");
+ }
+ tableItem.getRelationFields().add(new RelationFieldsPair(leftFieldItem, rightFieldItem));
+ } else {
+ throw new UnsupportedOperationException("Unsupported for complex of on-condition");
+ }
+ }
+}
diff --git a/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/service/ESSyncService.java b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/service/ESSyncService.java
new file mode 100644
index 00000000..c3eb1e3e
--- /dev/null
+++ b/client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/service/ESSyncService.java
@@ -0,0 +1,15 @@
+package com.alibaba.otter.canal.client.adapter.es.service;
+
+import org.elasticsearch.client.transport.TransportClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ESSyncService {
+ private Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private TransportClient transportClient;
+
+ public ESSyncService(TransportClient transportClient) {
+ this.transportClient = transportClient;
+ }
+}
diff --git a/client-adapter/elasticsearch/src/main/resources/META-INF/canal/com.alibaba.otter.canal.client.adapter.OuterAdapter b/client-adapter/elasticsearch/src/main/resources/META-INF/canal/com.alibaba.otter.canal.client.adapter.OuterAdapter
new file mode 100644
index 00000000..b30310ee
--- /dev/null
+++ b/client-adapter/elasticsearch/src/main/resources/META-INF/canal/com.alibaba.otter.canal.client.adapter.OuterAdapter
@@ -0,0 +1 @@
+es=com.alibaba.otter.canal.client.adapter.es.ESAdapter
\ No newline at end of file
diff --git a/client-adapter/elasticsearch/src/main/resources/es/myetst_user.yml b/client-adapter/elasticsearch/src/main/resources/es/myetst_user.yml
new file mode 100644
index 00000000..df6239b6
--- /dev/null
+++ b/client-adapter/elasticsearch/src/main/resources/es/myetst_user.yml
@@ -0,0 +1,12 @@
+dataSourceKey: defaultDS
+esMapping:
+ _index: mytest_user
+ _type: _doc
+ _id: id
+# pk: id
+ sql: "select a.id, concat(a.name,'_test') as name, a.role_id, b.name as role_name, c.labels from user.a
+ left join role b on a.role_id=b.id
+ left join (select user_id, group_concat(label,',') as labels from user_label
+ group by user_id) c on c.user_id=a.id"
+ commitBatch: 3000
+ etlCondition: "where a.c_time>='{0}' or b.c_time>='{0}' or c.c_time>='{0}'"
\ No newline at end of file
diff --git a/client-adapter/elasticsearch/src/test/java/com/alibaba/otter/canal/client/adapter/es/test/ConfigLoadTest.java b/client-adapter/elasticsearch/src/test/java/com/alibaba/otter/canal/client/adapter/es/test/ConfigLoadTest.java
new file mode 100644
index 00000000..b2586632
--- /dev/null
+++ b/client-adapter/elasticsearch/src/test/java/com/alibaba/otter/canal/client/adapter/es/test/ConfigLoadTest.java
@@ -0,0 +1,31 @@
+package com.alibaba.otter.canal.client.adapter.es.test;
+
+import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
+import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfigLoader;
+import com.alibaba.otter.canal.client.adapter.support.AdapterConfigs;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class ConfigLoadTest {
+
+ @Before
+ public void before() {
+ AdapterConfigs.put("es", "myetst_user.yml");
+ }
+
+ @Test
+ public void testLoad() {
+ Map configMap = ESSyncConfigLoader.load();
+ ESSyncConfig config = configMap.get("myetst_user.yml");
+ Assert.assertNotNull(config);
+ Assert.assertEquals("defaultDS", config.getDataSourceKey());
+ ESSyncConfig.ESMapping esMapping = config.getEsMapping();
+ Assert.assertEquals("mytest_user", esMapping.get_index());
+ Assert.assertEquals("_doc", esMapping.get_type());
+ Assert.assertEquals("id", esMapping.get_id());
+ Assert.assertNotNull(esMapping.getSql());
+ }
+}
diff --git a/client-adapter/elasticsearch/src/test/java/com/alibaba/otter/canal/client/adapter/es/test/SqlParseTest.java b/client-adapter/elasticsearch/src/test/java/com/alibaba/otter/canal/client/adapter/es/test/SqlParseTest.java
new file mode 100644
index 00000000..20025ad5
--- /dev/null
+++ b/client-adapter/elasticsearch/src/test/java/com/alibaba/otter/canal/client/adapter/es/test/SqlParseTest.java
@@ -0,0 +1,51 @@
+package com.alibaba.otter.canal.client.adapter.es.test;
+
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem;
+import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.FieldItem;
+import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.TableItem;
+import com.alibaba.otter.canal.client.adapter.es.config.SqlParser;
+
+public class SqlParseTest {
+
+ @Test
+ public void parseTest() {
+ // String sql = "select a.id,d.user_id2, concat(name,'_', a.nick) as name,
+ // b.name as roleNme, d.name as typeName,concat(d.label,'_') as label,"
+ // + " c.name as refName from user a left join role b on b.user_id=a.id "
+ // + "left join type d on d.user_id=a.id and d.user_id2=a.p_id "
+ // + "left join ( select ref_id,group_concat(name,',') as name from role group
+ // by ref_id ) c on c.ref_id=a.id "
+ // + "where a.id=1";
+ String sql = "select a.id, concat(a.name,'_test') as name, a.role_id, b.name as role_name, c.labels from user a "
+ + "left join role b on a.role_id=b.id "
+ + "left join (select user_id, group_concat(label,',') as labels from user_label "
+ + "group by user_id) c on c.user_id=a.id";
+ SchemaItem schemaItem = SqlParser.parse(sql);
+
+ // 通过表名找 TableItem
+ List tableItems = schemaItem.getTableItemAliases().get("user_label".toLowerCase());
+ tableItems.forEach(tableItem -> Assert.assertEquals("c", tableItem.getAlias()));
+
+ TableItem tableItem = tableItems.get(0);
+ Assert.assertFalse(tableItem.isMain());
+ Assert.assertTrue(tableItem.isSubQuery());
+ // 通过字段名找 FieldItem
+ List fieldItems = schemaItem.getColumnFields().get(tableItem.getAlias() + ".label".toLowerCase());
+ fieldItems.forEach(
+ fieldItem -> Assert.assertEquals("c.labels", fieldItem.getOwner() + "." + fieldItem.getFieldName()));
+
+ // 获取当前表关联条件字段
+ List relationTableFields = tableItem.getRelationTableFields();
+ relationTableFields.forEach(fieldItem -> Assert.assertEquals("user_id", fieldItem.getColumn().getColumnName()));
+
+ // 获取关联字段在select中的对应字段
+ List relationSelectFieldItem = tableItem.getRelationSelectFields();
+ relationSelectFieldItem.forEach(fieldItem -> Assert.assertEquals("c.labels",
+ fieldItem.getOwner() + "." + fieldItem.getColumn().getColumnName()));
+ }
+}
diff --git a/client-adapter/pom.xml b/client-adapter/pom.xml
index e2be0d35..b7f66111 100644
--- a/client-adapter/pom.xml
+++ b/client-adapter/pom.xml
@@ -21,6 +21,7 @@
common
logger
hbase
+ elasticsearch
launcher