From 53ef374de4a7fc0615e598c25c529eb793ee237b Mon Sep 17 00:00:00 2001
From: vran <vran_dev@foxmail.com>
Date: Mon, 27 Dec 2021 17:58:08 +0800
Subject: [PATCH] feature: complete render and model factory

---
 README.md                                     | 38 +++++++-
 core/build.gradle                             |  5 +-
 .../core/doc/config/DocConfiguration.java     |  5 --
 .../doc/factory/DatabaseDocConfiguration.java | 49 +++++++++++
 .../core/doc/factory/DatabaseDocFactory.java  |  3 +-
 .../databasir/core/doc/factory/Sortable.java  |  4 +-
 .../doc/factory/TableColumnDocFactory.java    |  4 +-
 .../doc/factory/TableDocCreateContext.java    | 25 ------
 .../core/doc/factory/TableDocFactory.java     |  3 +-
 .../doc/factory/TableIndexDocFactory.java     |  3 +-
 .../doc/factory/TableTriggerDocFactory.java   |  3 +-
 .../MysqlTableTriggerDocFactory.java          | 10 ++-
 .../factory/jdbc/JdbcDatabaseDocFactory.java  | 45 +++++++---
 .../jdbc/JdbcTableColumnDocFactory.java       | 60 +++++++++++--
 .../doc/factory/jdbc/JdbcTableDocFactory.java | 52 ++++++++++-
 .../jdbc/JdbcTableIndexDocFactory.java        | 58 ++++++++++++-
 .../jdbc/JdbcTableTriggerDocFactory.java      |  6 +-
 .../databasir/core/doc/model/ColumnDoc.java   | 24 ++++-
 .../databasir/core/doc/model/DatabaseDoc.java | 21 +++++
 .../databasir/core/doc/model/IndexDoc.java    | 17 ++++
 .../databasir/core/doc/model/TableDoc.java    | 25 ++++++
 .../databasir/core/doc/model/TriggerDoc.java  | 20 +++++
 .../core/doc/render/ColumnValueConverter.java | 19 ----
 .../render/DefaultColumnValueConverter.java   |  4 -
 .../com/databasir/core/doc/render/Render.java |  7 +-
 .../core/doc/render/RenderConfiguration.java  | 52 ++++++++++-
 .../databasir/core/doc/render/Renders.java    | 16 ----
 .../doc/render/markdown/MarkdownRender.java   | 87 ++++++++++++++++++-
 core/src/test/java/App.java                   | 39 +++++++++
 29 files changed, 585 insertions(+), 119 deletions(-)
 delete mode 100644 core/src/main/java/com/databasir/core/doc/config/DocConfiguration.java
 create mode 100644 core/src/main/java/com/databasir/core/doc/factory/DatabaseDocConfiguration.java
 delete mode 100644 core/src/main/java/com/databasir/core/doc/factory/TableDocCreateContext.java
 delete mode 100644 core/src/main/java/com/databasir/core/doc/render/ColumnValueConverter.java
 delete mode 100644 core/src/main/java/com/databasir/core/doc/render/DefaultColumnValueConverter.java
 delete mode 100644 core/src/main/java/com/databasir/core/doc/render/Renders.java
 create mode 100644 core/src/test/java/App.java

diff --git a/README.md b/README.md
index 67f115b..1e1d8c2 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,37 @@
-# Databasir
\ No newline at end of file
+# Databasir
+
+Database document generator
+
+# Features
+
+- render as markdown
+- render as html (TODO)
+- render as PDF (TODO)
+
+# Quick Start
+
+```java
+// First: get database connection
+Class.forName("com.mysql.cj.jdbc.Driver");
+Properties info=new Properties();
+info.put("user","root");
+info.put("password","123456");
+// this config is used by mysql
+info.put("useInformationSchema","true");
+String url="jdbc:mysql://localhost:3306/patient?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true";
+var connection=DriverManager.getConnection(url,info);
+
+// Second: generate doc model
+var config=DatabaseDocConfiguration.builder()
+    .databaseName("patient")
+    .connection(connection)
+    .build();
+DatabaseDoc doc = JdbcDatabaseDocFactory.of().create(config).orElseThrow();
+
+// Final: Render as markdown
+try(FileOutputStream out=new FileOutputStream("doc.md")){
+    MarkdownRender.of(new RenderConfiguration()).rendering(doc,out);
+}catch(IOException e){
+    throw new IllegalStateException(e);
+}
+```
\ No newline at end of file
diff --git a/core/build.gradle b/core/build.gradle
index eae2759..9a94a40 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -1,4 +1,5 @@
 dependencies {
-
-
+    implementation 'mysql:mysql-connector-java:8.0.27'
+    implementation 'commons-io:commons-io:2.11.0'
+    implementation 'org.commonmark:commonmark:0.18.1'
 }
diff --git a/core/src/main/java/com/databasir/core/doc/config/DocConfiguration.java b/core/src/main/java/com/databasir/core/doc/config/DocConfiguration.java
deleted file mode 100644
index 941333e..0000000
--- a/core/src/main/java/com/databasir/core/doc/config/DocConfiguration.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.databasir.core.doc.config;
-
-public class DocConfiguration {
-
-}
diff --git a/core/src/main/java/com/databasir/core/doc/factory/DatabaseDocConfiguration.java b/core/src/main/java/com/databasir/core/doc/factory/DatabaseDocConfiguration.java
new file mode 100644
index 0000000..1b8b263
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/doc/factory/DatabaseDocConfiguration.java
@@ -0,0 +1,49 @@
+package com.databasir.core.doc.factory;
+
+import com.databasir.core.doc.factory.jdbc.*;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.sql.Connection;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+@Builder
+@Getter
+public class DatabaseDocConfiguration {
+
+    private String databaseName;
+
+    private Connection connection;
+
+    @Builder.Default
+    private List<String> ignoreTableRegexes = Collections.emptyList();
+
+    @Builder.Default
+    private List<String> ignoreColumnRegexes = Collections.emptyList();
+
+    @Builder.Default
+    private DatabaseDocFactory databaseDocFactory = new JdbcDatabaseDocFactory();
+
+    @Builder.Default
+    private TableDocFactory tableDocFactory = new JdbcTableDocFactory();
+
+    @Builder.Default
+    private TableIndexDocFactory tableIndexDocFactory = new JdbcTableIndexDocFactory();
+
+    @Builder.Default
+    private TableTriggerDocFactory tableTriggerDocFactory = new JdbcTableTriggerDocFactory();
+
+    @Builder.Default
+    private TableColumnDocFactory tableColumnDocFactory = new JdbcTableColumnDocFactory();
+
+    public boolean ignoredTable(String tableName) {
+        return ignoreTableRegexes.stream().anyMatch(regex -> Pattern.matches(regex, tableName));
+    }
+
+    public boolean ignoredColumn(String column) {
+        return ignoreColumnRegexes.stream().anyMatch(regex -> Pattern.matches(regex, column));
+    }
+
+}
diff --git a/core/src/main/java/com/databasir/core/doc/factory/DatabaseDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/DatabaseDocFactory.java
index b36f21a..6e4cdb8 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/DatabaseDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/DatabaseDocFactory.java
@@ -2,11 +2,10 @@ package com.databasir.core.doc.factory;
 
 import com.databasir.core.doc.model.DatabaseDoc;
 
-import java.sql.Connection;
 import java.util.Optional;
 
 public interface DatabaseDocFactory extends Sortable<DatabaseDocFactory> {
 
-    Optional<DatabaseDoc> create(Connection connection, String databaseName);
+    Optional<DatabaseDoc> create(DatabaseDocConfiguration configuration);
 
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/Sortable.java b/core/src/main/java/com/databasir/core/doc/factory/Sortable.java
index 6c3c7a7..4d8e4db 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/Sortable.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/Sortable.java
@@ -5,12 +5,12 @@ public interface Sortable<T extends Sortable<?>> extends Comparable<T> {
     /**
      * @return priority, min -> max means low -> high
      */
-    default int priority() {
+    default int order() {
         return Integer.MIN_VALUE;
     }
 
     @Override
     default int compareTo(T o) {
-        return Integer.compare(this.priority(), o.priority());
+        return Integer.compare(this.order(), o.order());
     }
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/TableColumnDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/TableColumnDocFactory.java
index ec7d672..698c309 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/TableColumnDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/TableColumnDocFactory.java
@@ -2,9 +2,11 @@ package com.databasir.core.doc.factory;
 
 import com.databasir.core.doc.model.ColumnDoc;
 
+import java.sql.DatabaseMetaData;
 import java.util.List;
 
 public interface TableColumnDocFactory extends Sortable<TableColumnDocFactory> {
 
-    List<ColumnDoc> create(TableDocCreateContext context);
+    List<ColumnDoc> create(String tableName, DatabaseMetaData metaData, DatabaseDocConfiguration configuration);
+
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/TableDocCreateContext.java b/core/src/main/java/com/databasir/core/doc/factory/TableDocCreateContext.java
deleted file mode 100644
index c3f0a9a..0000000
--- a/core/src/main/java/com/databasir/core/doc/factory/TableDocCreateContext.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.databasir.core.doc.factory;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-@Getter
-public class TableDocCreateContext {
-
-    private String database;
-
-    private String tableName;
-
-    private Connection connection;
-
-    private DatabaseMetaData databaseMetaData;
-
-}
diff --git a/core/src/main/java/com/databasir/core/doc/factory/TableDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/TableDocFactory.java
index 1ec52e5..5640404 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/TableDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/TableDocFactory.java
@@ -2,10 +2,11 @@ package com.databasir.core.doc.factory;
 
 import com.databasir.core.doc.model.TableDoc;
 
+import java.sql.DatabaseMetaData;
 import java.util.List;
 
 public interface TableDocFactory extends Sortable<TableDocFactory> {
 
-    List<TableDoc> create(TableDocCreateContext context);
+    List<TableDoc> create(DatabaseMetaData metaData, DatabaseDocConfiguration configuration);
 
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/TableIndexDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/TableIndexDocFactory.java
index 5046801..42ba249 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/TableIndexDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/TableIndexDocFactory.java
@@ -2,10 +2,11 @@ package com.databasir.core.doc.factory;
 
 import com.databasir.core.doc.model.IndexDoc;
 
+import java.sql.DatabaseMetaData;
 import java.util.List;
 
 public interface TableIndexDocFactory extends Sortable<TableIndexDocFactory> {
 
-    List<IndexDoc> create(TableDocCreateContext context);
+    List<IndexDoc> create(String tableName, DatabaseMetaData metaData, DatabaseDocConfiguration configuration);
 
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/TableTriggerDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/TableTriggerDocFactory.java
index 84b8445..e4caeb9 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/TableTriggerDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/TableTriggerDocFactory.java
@@ -2,9 +2,10 @@ package com.databasir.core.doc.factory;
 
 import com.databasir.core.doc.model.TriggerDoc;
 
+import java.sql.DatabaseMetaData;
 import java.util.List;
 
 public interface TableTriggerDocFactory extends Sortable<TableTriggerDocFactory> {
 
-    List<TriggerDoc> create(TableDocCreateContext context);
+    List<TriggerDoc> create(String tableName, DatabaseMetaData metaData, DatabaseDocConfiguration configuration);
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/extension/MysqlTableTriggerDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/extension/MysqlTableTriggerDocFactory.java
index a273163..bef512e 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/extension/MysqlTableTriggerDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/extension/MysqlTableTriggerDocFactory.java
@@ -1,16 +1,20 @@
 package com.databasir.core.doc.factory.extension;
 
-import com.databasir.core.doc.factory.TableDocCreateContext;
+import com.databasir.core.doc.factory.DatabaseDocConfiguration;
 import com.databasir.core.doc.factory.TableTriggerDocFactory;
 import com.databasir.core.doc.model.TriggerDoc;
 
+import java.sql.DatabaseMetaData;
+import java.util.Collections;
 import java.util.List;
 
 public class MysqlTableTriggerDocFactory implements TableTriggerDocFactory {
 
     @Override
-    public List<TriggerDoc> create(TableDocCreateContext context) {
-        return null;
+    public List<TriggerDoc> create(String tableName,
+                                   DatabaseMetaData metaData,
+                                   DatabaseDocConfiguration configuration) {
+        return Collections.emptyList();
     }
 
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcDatabaseDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcDatabaseDocFactory.java
index 0758c73..d092c3a 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcDatabaseDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcDatabaseDocFactory.java
@@ -1,24 +1,45 @@
 package com.databasir.core.doc.factory.jdbc;
 
+import com.databasir.core.doc.factory.DatabaseDocConfiguration;
 import com.databasir.core.doc.factory.*;
 import com.databasir.core.doc.model.DatabaseDoc;
+import com.databasir.core.doc.model.TableDoc;
 
-import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 
 public class JdbcDatabaseDocFactory implements DatabaseDocFactory {
 
-    private TableDocFactory tableDocFactory;
-
-    private TableColumnDocFactory tableColumnDocFactory;
-
-    private TableTriggerDocFactory tableTriggerDocFactory;
-
-    private TableIndexDocFactory tableIndexDocFactory;
-
-    @Override
-    public Optional<DatabaseDoc> create(Connection connection, String database) {
-        return Optional.empty();
+    public static JdbcDatabaseDocFactory of() {
+        return new JdbcDatabaseDocFactory();
     }
 
+    @Override
+    public Optional<DatabaseDoc> create(DatabaseDocConfiguration configuration) {
+        try {
+            DatabaseMetaData metaData = configuration.getConnection().getMetaData();
+            ResultSet catalogs = metaData.getCatalogs();
+            while (catalogs.next()) {
+                String catalogName = catalogs.getString("TABLE_CAT");
+                if (Objects.equals(configuration.getDatabaseName(), catalogName)) {
+                    TableDocFactory tableDocFactory = configuration.getTableDocFactory();
+                    List<TableDoc> tableDocs = tableDocFactory.create(metaData, configuration);
+                    DatabaseDoc databaseDoc = DatabaseDoc.builder()
+                            .productName(metaData.getDatabaseProductName())
+                            .productVersion(metaData.getDatabaseProductVersion())
+                            .databaseName(catalogName)
+                            .tables(tableDocs)
+                            .build();
+                    return Optional.of(databaseDoc);
+                }
+            }
+            return Optional.empty();
+        } catch (SQLException e) {
+            throw new IllegalStateException(e);
+        }
+    }
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableColumnDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableColumnDocFactory.java
index 19635ef..ea1d70d 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableColumnDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableColumnDocFactory.java
@@ -1,24 +1,72 @@
 package com.databasir.core.doc.factory.jdbc;
 
+import com.databasir.core.doc.factory.DatabaseDocConfiguration;
 import com.databasir.core.doc.factory.TableColumnDocFactory;
-import com.databasir.core.doc.factory.TableDocCreateContext;
 import com.databasir.core.doc.model.ColumnDoc;
 import lombok.extern.slf4j.Slf4j;
 
+import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 @Slf4j
 public class JdbcTableColumnDocFactory implements TableColumnDocFactory {
+
     @Override
-    public List<ColumnDoc> create(TableDocCreateContext context) {
-
+    public List<ColumnDoc> create(String tableName,
+                                  DatabaseMetaData metaData,
+                                  DatabaseDocConfiguration configuration) {
         try {
-            ResultSet indexResults = context.getDatabaseMetaData().getIndexInfo(context.getDatabase(), null, context.getTableName(), false, false);
+            return doCreate(tableName, metaData, configuration);
         } catch (SQLException e) {
+            throw new IllegalStateException(e);
         }
-
-        return null;
     }
+
+    private List<ColumnDoc> doCreate(String tableName,
+                                     DatabaseMetaData metaData,
+                                     DatabaseDocConfiguration configuration) throws SQLException {
+        List<ColumnDoc> columnDocs = new ArrayList<>();
+        String database = configuration.getDatabaseName();
+        ResultSet columnsResult;
+        try {
+            columnsResult = metaData.getColumns(database, null, tableName, null);
+        } catch (SQLException e) {
+            log.warn("warn: ignore " + database + "." + tableName);
+            return columnDocs;
+        }
+        while (columnsResult.next()) {
+            String columnName = columnsResult.getString("COLUMN_NAME");
+            if (configuration.ignoredColumn(columnName)) {
+                if (log.isDebugEnabled()) {
+                    log.warn("ignore column: " + columnName);
+                }
+            } else {
+                String columnType = columnsResult.getString("TYPE_NAME");
+                Integer columnSize = columnsResult.getInt("COLUMN_SIZE");
+                Integer decimalDigits = columnsResult.getInt("DECIMAL_DIGITS");
+                String defaultValue = columnsResult.getString("COLUMN_DEF");
+                boolean isNullable = Objects.equals("YES", columnsResult.getString("IS_NULLABLE"));
+                boolean isAutoIncrement = Objects.equals("YES", columnsResult.getString("IS_AUTOINCREMENT"));
+                String columnComment = columnsResult.getString("REMARKS");
+                ColumnDoc columnDoc = ColumnDoc.builder()
+                        .name(columnName)
+                        .type(columnType)
+                        .size(columnSize)
+                        .decimalDigits(decimalDigits)
+                        .isNullable(isNullable)
+                        .isAutoIncrement(isAutoIncrement)
+                        .comment(columnComment)
+                        .defaultValue(defaultValue)
+                        .build();
+                columnDocs.add(columnDoc);
+            }
+
+        }
+        return columnDocs;
+    }
+
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableDocFactory.java
index 16adc5a..214ac53 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableDocFactory.java
@@ -1,15 +1,59 @@
 package com.databasir.core.doc.factory.jdbc;
 
-import com.databasir.core.doc.factory.TableDocCreateContext;
-import com.databasir.core.doc.factory.TableDocFactory;
+import com.databasir.core.doc.factory.DatabaseDocConfiguration;
+import com.databasir.core.doc.factory.*;
 import com.databasir.core.doc.model.TableDoc;
+import lombok.extern.slf4j.Slf4j;
 
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.List;
 
+@Slf4j
 public class JdbcTableDocFactory implements TableDocFactory {
 
     @Override
-    public List<TableDoc> create(TableDocCreateContext context) {
-        return null;
+    public List<TableDoc> create(DatabaseMetaData metaData,
+                                 DatabaseDocConfiguration configuration) {
+        try {
+            return doCreateTableDoc(metaData, configuration);
+        } catch (SQLException e) {
+            throw new IllegalStateException(e);
+        }
     }
+
+    private List<TableDoc> doCreateTableDoc(DatabaseMetaData metaData,
+                                            DatabaseDocConfiguration configuration) throws SQLException {
+        List<TableDoc> tableDocs = new ArrayList<>();
+        if (metaData == null) {
+            return tableDocs;
+        }
+
+        String databaseName = configuration.getDatabaseName();
+        ResultSet tablesResult = metaData.getTables(databaseName, null, null, null);
+        while (tablesResult.next()) {
+            String tableName = tablesResult.getString("TABLE_NAME");
+            if (configuration.ignoredTable(tableName)) {
+                if (log.isDebugEnabled()) {
+                    log.debug("ignore table: " + tableName);
+                }
+            } else {
+                String tableType = tablesResult.getString("TABLE_TYPE");
+                String tableComment = tablesResult.getString("REMARKS");
+                TableDoc tableDoc = TableDoc.builder()
+                        .tableName(tableName)
+                        .tableType(tableType)
+                        .tableComment(tableComment)
+                        .columns(configuration.getTableColumnDocFactory().create(tableName, metaData, configuration))
+                        .indexes(configuration.getTableIndexDocFactory().create(tableName, metaData, configuration))
+                        .triggers(configuration.getTableTriggerDocFactory().create(tableName, metaData, configuration))
+                        .build();
+                tableDocs.add(tableDoc);
+            }
+        }
+        return tableDocs;
+    }
+
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableIndexDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableIndexDocFactory.java
index 1aef6c5..d214e8a 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableIndexDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableIndexDocFactory.java
@@ -1,14 +1,64 @@
 package com.databasir.core.doc.factory.jdbc;
 
-import com.databasir.core.doc.factory.TableDocCreateContext;
+import com.databasir.core.doc.factory.DatabaseDocConfiguration;
 import com.databasir.core.doc.factory.TableIndexDocFactory;
 import com.databasir.core.doc.model.IndexDoc;
+import lombok.extern.slf4j.Slf4j;
 
-import java.util.List;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.*;
 
+@Slf4j
 public class JdbcTableIndexDocFactory implements TableIndexDocFactory {
+
     @Override
-    public List<IndexDoc> create(TableDocCreateContext context) {
-        return null;
+    public List<IndexDoc> create(String tableName, DatabaseMetaData metaData, DatabaseDocConfiguration configuration) {
+        try {
+            return doCreateIndexDocs(tableName, metaData, configuration);
+        } catch (SQLException e) {
+            throw new IllegalStateException(e);
+        }
     }
+
+    private List<IndexDoc> doCreateIndexDocs(String tableName,
+                                             DatabaseMetaData metaData,
+                                             DatabaseDocConfiguration configuration)
+            throws SQLException {
+        List<IndexDoc> indexDocs = new ArrayList<>();
+        String databaseName = configuration.getDatabaseName();
+        if (databaseName == null || tableName == null || metaData == null) {
+            return indexDocs;
+        }
+        ResultSet indexResults;
+        try {
+            indexResults = metaData.getIndexInfo(databaseName, null, tableName, false, false);
+        } catch (SQLException e) {
+            log.warn("warn: ignore " + databaseName + "." + tableName);
+            return indexDocs;
+        }
+
+        Map<String, IndexDoc> docsGroupByName = new HashMap<>();
+        while (indexResults.next()) {
+            Boolean nonUnique = indexResults.getBoolean("NON_UNIQUE");
+            String indexName = indexResults.getString("INDEX_NAME");
+            String columnName = indexResults.getString("COLUMN_NAME");
+            if (docsGroupByName.containsKey(indexName)) {
+                docsGroupByName.get(indexName).getColumnNames().add(columnName);
+            } else {
+                List<String> columns = new ArrayList<>();
+                columns.add(columnName);
+                IndexDoc columnDoc = IndexDoc.builder()
+                        .indexName(indexName)
+                        .columnNames(columns)
+                        .isPrimaryKey(Objects.equals("PRIMARY", indexName))
+                        .isUniqueKey(Objects.equals(nonUnique, false))
+                        .build();
+                docsGroupByName.put(indexName, columnDoc);
+            }
+        }
+        return new ArrayList<>(docsGroupByName.values());
+    }
+
 }
diff --git a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableTriggerDocFactory.java b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableTriggerDocFactory.java
index 74e1283..1c10674 100644
--- a/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableTriggerDocFactory.java
+++ b/core/src/main/java/com/databasir/core/doc/factory/jdbc/JdbcTableTriggerDocFactory.java
@@ -1,18 +1,18 @@
 package com.databasir.core.doc.factory.jdbc;
 
-import com.databasir.core.doc.factory.TableDocCreateContext;
+import com.databasir.core.doc.factory.DatabaseDocConfiguration;
 import com.databasir.core.doc.factory.TableTriggerDocFactory;
 import com.databasir.core.doc.model.TriggerDoc;
 
+import java.sql.DatabaseMetaData;
 import java.util.Collections;
 import java.util.List;
 
 public class JdbcTableTriggerDocFactory implements TableTriggerDocFactory {
 
     @Override
-    public List<TriggerDoc> create(TableDocCreateContext context) {
+    public List<TriggerDoc> create(String tableName, DatabaseMetaData metaData, DatabaseDocConfiguration configuration) {
         // note: jdbc not support get triggers
         return Collections.emptyList();
     }
-
 }
diff --git a/core/src/main/java/com/databasir/core/doc/model/ColumnDoc.java b/core/src/main/java/com/databasir/core/doc/model/ColumnDoc.java
index 4c18e41..96df982 100644
--- a/core/src/main/java/com/databasir/core/doc/model/ColumnDoc.java
+++ b/core/src/main/java/com/databasir/core/doc/model/ColumnDoc.java
@@ -1,4 +1,26 @@
 package com.databasir.core.doc.model;
 
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
 public class ColumnDoc {
-}
+
+    private String name;
+
+    private String comment;
+
+    private String type;
+
+    private String defaultValue;
+
+    private Integer size;
+
+    private Integer decimalDigits;
+
+    private Boolean isNullable;
+
+    private Boolean isAutoIncrement;
+
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/databasir/core/doc/model/DatabaseDoc.java b/core/src/main/java/com/databasir/core/doc/model/DatabaseDoc.java
index f33b8a4..00a029b 100644
--- a/core/src/main/java/com/databasir/core/doc/model/DatabaseDoc.java
+++ b/core/src/main/java/com/databasir/core/doc/model/DatabaseDoc.java
@@ -1,7 +1,28 @@
 package com.databasir.core.doc.model;
 
+import lombok.Builder;
 import lombok.Data;
 
+import java.util.Collections;
+import java.util.List;
+
 @Data
+@Builder
 public class DatabaseDoc {
+
+    private String productName;
+
+    private String productVersion;
+
+    private String driverName;
+
+    private String driverVersion;
+
+    private String databaseName;
+
+    private String remark;
+
+    @Builder.Default
+    private List<TableDoc> tables = Collections.emptyList();
+
 }
diff --git a/core/src/main/java/com/databasir/core/doc/model/IndexDoc.java b/core/src/main/java/com/databasir/core/doc/model/IndexDoc.java
index c727242..5097234 100644
--- a/core/src/main/java/com/databasir/core/doc/model/IndexDoc.java
+++ b/core/src/main/java/com/databasir/core/doc/model/IndexDoc.java
@@ -1,4 +1,21 @@
 package com.databasir.core.doc.model;
 
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@Builder
 public class IndexDoc {
+
+    private String indexName;
+
+    @Builder.Default
+    private List<String> columnNames = Collections.emptyList();
+
+    private Boolean isPrimaryKey;
+
+    private Boolean isUniqueKey;
 }
diff --git a/core/src/main/java/com/databasir/core/doc/model/TableDoc.java b/core/src/main/java/com/databasir/core/doc/model/TableDoc.java
index fdb271b..58c8617 100644
--- a/core/src/main/java/com/databasir/core/doc/model/TableDoc.java
+++ b/core/src/main/java/com/databasir/core/doc/model/TableDoc.java
@@ -1,4 +1,29 @@
 package com.databasir.core.doc.model;
 
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@Builder
 public class TableDoc {
+
+    private String tableName;
+
+    private String tableType;
+
+    private String tableComment;
+
+    @Builder.Default
+    private List<ColumnDoc> columns = Collections.emptyList();
+
+    @Builder.Default
+    private List<TriggerDoc> triggers = Collections.emptyList();
+
+    @Builder.Default
+    private List<IndexDoc> indexes = Collections.emptyList();
+
+    private String remark;
 }
diff --git a/core/src/main/java/com/databasir/core/doc/model/TriggerDoc.java b/core/src/main/java/com/databasir/core/doc/model/TriggerDoc.java
index c03f10e..82a987c 100644
--- a/core/src/main/java/com/databasir/core/doc/model/TriggerDoc.java
+++ b/core/src/main/java/com/databasir/core/doc/model/TriggerDoc.java
@@ -1,4 +1,24 @@
 package com.databasir.core.doc.model;
 
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * now: only support mysql, postgresql.
+ */
+@Data
+@Builder
 public class TriggerDoc {
+
+    private String name;
+
+    /**
+     * example 1: BEFORE UPDATE
+     * example 2: AFTER INSERT
+     */
+    private String timing;
+
+    private String statement;
+
+    private String createAt;
 }
diff --git a/core/src/main/java/com/databasir/core/doc/render/ColumnValueConverter.java b/core/src/main/java/com/databasir/core/doc/render/ColumnValueConverter.java
deleted file mode 100644
index 4dbafab..0000000
--- a/core/src/main/java/com/databasir/core/doc/render/ColumnValueConverter.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.databasir.core.doc.render;
-
-import java.util.Objects;
-
-public interface ColumnValueConverter {
-
-    default String convertDataType(String originType) {
-        return originType;
-    }
-
-    default String convertIsNotNull(Boolean isNotNull) {
-        return Objects.equals(isNotNull, true) ? "YES" : "";
-    }
-
-    default String convertIsAutoIncrement(Boolean isAutoIncrement) {
-        return Objects.equals(isAutoIncrement, true) ? "YES" : "";
-    }
-
-}
diff --git a/core/src/main/java/com/databasir/core/doc/render/DefaultColumnValueConverter.java b/core/src/main/java/com/databasir/core/doc/render/DefaultColumnValueConverter.java
deleted file mode 100644
index 1a3b34c..0000000
--- a/core/src/main/java/com/databasir/core/doc/render/DefaultColumnValueConverter.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.databasir.core.doc.render;
-
-public class DefaultColumnValueConverter implements ColumnValueConverter {
-}
diff --git a/core/src/main/java/com/databasir/core/doc/render/Render.java b/core/src/main/java/com/databasir/core/doc/render/Render.java
index 379e006..a1c7279 100644
--- a/core/src/main/java/com/databasir/core/doc/render/Render.java
+++ b/core/src/main/java/com/databasir/core/doc/render/Render.java
@@ -1,11 +1,16 @@
 package com.databasir.core.doc.render;
 
 import com.databasir.core.doc.model.DatabaseDoc;
+import com.databasir.core.doc.render.markdown.MarkdownRender;
 
+import java.io.IOException;
 import java.io.OutputStream;
 
 public interface Render {
 
-    void rendering(DatabaseDoc doc, OutputStream outputStream);
+    void rendering(DatabaseDoc doc, OutputStream outputStream) throws IOException;
 
+    static Render markdownRender(RenderConfiguration configuration) {
+        return MarkdownRender.of(configuration);
+    }
 }
diff --git a/core/src/main/java/com/databasir/core/doc/render/RenderConfiguration.java b/core/src/main/java/com/databasir/core/doc/render/RenderConfiguration.java
index 9b8c1ba..de0482f 100644
--- a/core/src/main/java/com/databasir/core/doc/render/RenderConfiguration.java
+++ b/core/src/main/java/com/databasir/core/doc/render/RenderConfiguration.java
@@ -1,5 +1,13 @@
 package com.databasir.core.doc.render;
 
+import com.databasir.core.doc.model.ColumnDoc;
+import com.databasir.core.doc.model.IndexDoc;
+import lombok.Data;
+
+import java.util.LinkedHashMap;
+import java.util.function.Function;
+
+@Data
 public class RenderConfiguration {
 
     private Boolean renderTables = true;
@@ -8,11 +16,47 @@ public class RenderConfiguration {
 
     private Boolean renderIndexes = true;
 
-    private Boolean renderViews = false;
-
     private Boolean renderTriggers = false;
 
-    private Boolean renderProducers = false;
+    private LinkedHashMap<String, Function<ColumnDoc, String>> columnTitleAndValueMapping = columnTitleAndValueMapping();
 
-    private ColumnValueConverter columnValueConverter = new DefaultColumnValueConverter();
+    private LinkedHashMap<String, Function<IndexDoc, String>> indexTitleAndValueMapping = indexTitleAndValueMapping();
+
+    protected LinkedHashMap<String, Function<ColumnDoc, String>> columnTitleAndValueMapping() {
+        LinkedHashMap<String, Function<ColumnDoc, String>> mapping = new LinkedHashMap<>();
+        mapping.put("Name", ColumnDoc::getName);
+        mapping.put("Type", column -> {
+            String type;
+            if (column.getDecimalDigits() == null || column.getDecimalDigits().equals(0)) {
+                type = column.getType()
+                        + "(" + column.getSize().toString() + ")";
+            } else {
+                type = column.getType()
+                        + "(" + column.getSize().toString() + ", " + column.getDecimalDigits().toString() + ")";
+            }
+            return type;
+        });
+        mapping.put("Not Null", column -> column.getIsNullable() ? "" : "YES");
+        mapping.put("Auto Increment", column -> column.getIsAutoIncrement() ? "YES" : "");
+        mapping.put("Default", column -> {
+            if (column.getDefaultValue() == null) {
+                return "";
+            }
+            if (column.getDefaultValue().trim().equals("")) {
+                return "'" + column.getDefaultValue() + "'";
+            }
+            return column.getDefaultValue();
+        });
+        mapping.put("Comment", ColumnDoc::getComment);
+        return mapping;
+    }
+
+    protected LinkedHashMap<String, Function<IndexDoc, String>> indexTitleAndValueMapping() {
+        LinkedHashMap<String, Function<IndexDoc, String>> mapping = new LinkedHashMap<>();
+        mapping.put("Name", IndexDoc::getIndexName);
+        mapping.put("IsPrimary", index -> index.getIsPrimaryKey() ? "YES" : "");
+        mapping.put("IsUnique", index -> index.getIsUniqueKey() ? "YES" : "");
+        mapping.put("Columns", index -> String.join(", ", index.getColumnNames()));
+        return mapping;
+    }
 }
diff --git a/core/src/main/java/com/databasir/core/doc/render/Renders.java b/core/src/main/java/com/databasir/core/doc/render/Renders.java
deleted file mode 100644
index a1ffbe2..0000000
--- a/core/src/main/java/com/databasir/core/doc/render/Renders.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.databasir.core.doc.render;
-
-import com.databasir.core.doc.model.DatabaseDoc;
-
-import java.io.OutputStream;
-
-public class Renders {
-
-    private Render markdownRender = null;
-
-    public void render(DatabaseDoc doc,
-                       OutputStream outputStream,
-                       RenderConfiguration config) {
-        markdownRender.rendering(doc, outputStream);
-    }
-}
diff --git a/core/src/main/java/com/databasir/core/doc/render/markdown/MarkdownRender.java b/core/src/main/java/com/databasir/core/doc/render/markdown/MarkdownRender.java
index 040db8e..cd12e1b 100644
--- a/core/src/main/java/com/databasir/core/doc/render/markdown/MarkdownRender.java
+++ b/core/src/main/java/com/databasir/core/doc/render/markdown/MarkdownRender.java
@@ -1,4 +1,89 @@
 package com.databasir.core.doc.render.markdown;
 
-public class MarkdownRender {
+import com.databasir.core.doc.model.DatabaseDoc;
+import com.databasir.core.doc.model.TableDoc;
+import com.databasir.core.doc.render.Render;
+import com.databasir.core.doc.render.RenderConfiguration;
+import lombok.Getter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class MarkdownRender implements Render {
+
+    @Getter
+    private final RenderConfiguration config;
+
+    protected MarkdownRender(RenderConfiguration config) {
+        this.config = config;
+    }
+
+    public static MarkdownRender of(RenderConfiguration config) {
+        return new MarkdownRender(config);
+    }
+
+    @Override
+    public void rendering(DatabaseDoc doc,
+                          OutputStream outputStream) throws IOException {
+        MarkdownBuilder contentBuilder = MarkdownBuilder.builder();
+        contentBuilder.primaryTitle(doc.getDatabaseName());
+        if (config.getRenderTables()) {
+            for (TableDoc table : doc.getTables()) {
+                buildTableName(contentBuilder, table);
+                if (config.getRenderColumns()) {
+                    buildColumns(contentBuilder, table);
+                }
+                if (config.getRenderIndexes()) {
+                    buildIndexes(contentBuilder, table);
+                }
+            }
+        }
+        outputStream.write(contentBuilder.build().getBytes(StandardCharsets.UTF_8));
+    }
+
+    private void buildTableName(MarkdownBuilder contentBuilder, TableDoc table) {
+        String tableName;
+        if (table.getTableComment() == null || table.getTableComment().trim().isEmpty()) {
+            tableName = table.getTableName();
+        } else {
+            tableName = table.getTableName() + "(" + table.getTableComment() + ")";
+        }
+        contentBuilder.secondTitle(tableName);
+    }
+
+    private void buildColumns(MarkdownBuilder contentBuilder, TableDoc table) {
+        contentBuilder.unorderedList(Collections.singletonList("columns"));
+        List<List<String>> allColumnRows = table.getColumns()
+                .stream()
+                .map(column -> config.getColumnTitleAndValueMapping()
+                        .values()
+                        .stream()
+                        .map(mapping -> mapping.apply(column))
+                        .collect(Collectors.toList()))
+                .collect(Collectors.toList());
+        contentBuilder.table(tableTitles(), allColumnRows);
+    }
+
+    private void buildIndexes(MarkdownBuilder contentBuilder, TableDoc table) {
+        contentBuilder.unorderedList(Collections.singletonList("indexes"));
+        List<List<String>> allIndexRows = table.getIndexes().stream()
+                .map(index -> config.getIndexTitleAndValueMapping()
+                        .values()
+                        .stream()
+                        .map(mapping -> mapping.apply(index))
+                        .collect(Collectors.toList()))
+                .collect(Collectors.toList());
+        contentBuilder.table(indexTitles(), allIndexRows);
+    }
+
+    private List<String> tableTitles() {
+        return new ArrayList<>(config.getColumnTitleAndValueMapping().keySet());
+    }
+
+    private List<String> indexTitles() {
+        return new ArrayList<>(config.getIndexTitleAndValueMapping().keySet());
+    }
 }
diff --git a/core/src/test/java/App.java b/core/src/test/java/App.java
new file mode 100644
index 0000000..3d13cc3
--- /dev/null
+++ b/core/src/test/java/App.java
@@ -0,0 +1,39 @@
+import com.databasir.core.doc.factory.DatabaseDocConfiguration;
+import com.databasir.core.doc.factory.jdbc.JdbcDatabaseDocFactory;
+import com.databasir.core.doc.model.DatabaseDoc;
+import com.databasir.core.doc.render.Render;
+import com.databasir.core.doc.render.RenderConfiguration;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Properties;
+
+public class App {
+    public static void main(String[] args) throws SQLException, ClassNotFoundException {
+        // get database connection
+        Class.forName("com.mysql.cj.jdbc.Driver");
+        Properties info = new Properties();
+        info.put("user", "root");
+        info.put("password", "123456");
+        // this config is used by mysql
+        info.put("useInformationSchema", "true");
+        String url = "jdbc:mysql://localhost:3306/patient?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true";
+        var connection = DriverManager.getConnection(url, info);
+
+        // generate doc model
+        var config = DatabaseDocConfiguration.builder()
+                .databaseName("patient")
+                .connection(connection)
+                .build();
+        DatabaseDoc doc = JdbcDatabaseDocFactory.of().create(config).orElseThrow();
+
+        // render as markdown
+        try (FileOutputStream out = new FileOutputStream("doc.md")) {
+            Render.markdownRender(new RenderConfiguration()).rendering(doc, out);
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+}