feat: use plantuml to export er diagram
This commit is contained in:
parent
06f5044e39
commit
6c5965f466
|
@ -22,9 +22,10 @@ import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.springframework.data.domain.Sort.Direction.DESC;
|
import static org.springframework.data.domain.Sort.Direction.DESC;
|
||||||
|
|
||||||
|
@ -75,9 +76,10 @@ public class DocumentController {
|
||||||
Long version,
|
Long version,
|
||||||
@RequestParam DocumentFileType fileType) {
|
@RequestParam DocumentFileType fileType) {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
String fileName = "project[" + projectId + "]." + fileType.getFileExtension();
|
String projectName = projectService.getOne(projectId).getName();
|
||||||
|
String fileName = projectName + "." + fileType.getFileExtension();
|
||||||
headers.setContentDisposition(ContentDisposition.attachment()
|
headers.setContentDisposition(ContentDisposition.attachment()
|
||||||
.filename("demo.md", StandardCharsets.UTF_8)
|
.filename(fileName)
|
||||||
.build());
|
.build());
|
||||||
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
|
@ -85,6 +87,14 @@ public class DocumentController {
|
||||||
.body(out -> documentService.export(projectId, version, fileType, out));
|
.body(out -> documentService.export(projectId, version, fileType, out));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(Routes.Document.EXPORT_TYPES)
|
||||||
|
public JsonData<List<DocumentFileTypeResponse>> getDocumentFileTypes() {
|
||||||
|
List<DocumentFileTypeResponse> types = Arrays.stream(DocumentFileType.values())
|
||||||
|
.map(type -> new DocumentFileTypeResponse(type.getName(), type.getFileExtension(), type))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return JsonData.ok(types);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping(Routes.Document.GET_SIMPLE_ONE)
|
@GetMapping(Routes.Document.GET_SIMPLE_ONE)
|
||||||
@Operation(summary = "获取文档(无详情信息)")
|
@Operation(summary = "获取文档(无详情信息)")
|
||||||
public JsonData<DatabaseDocumentSimpleResponse> getSimpleByProjectId(@PathVariable Integer projectId,
|
public JsonData<DatabaseDocumentSimpleResponse> getSimpleByProjectId(@PathVariable Integer projectId,
|
||||||
|
|
|
@ -92,6 +92,8 @@ public interface Routes {
|
||||||
|
|
||||||
String EXPORT = BASE + "/projects/{projectId}/document_files";
|
String EXPORT = BASE + "/projects/{projectId}/document_files";
|
||||||
|
|
||||||
|
String EXPORT_TYPES = BASE + "/document_file_types";
|
||||||
|
|
||||||
String LIST_TABLES = BASE + "/projects/{projectId}/tables";
|
String LIST_TABLES = BASE + "/projects/{projectId}/tables";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,15 +11,30 @@ import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.web.config.EnableSpringDataWebSupport;
|
import org.springframework.data.web.config.EnableSpringDataWebSupport;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableSpringDataWebSupport
|
@EnableSpringDataWebSupport
|
||||||
public class WebConfig extends WebMvcConfigurerAdapter {
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
|
||||||
|
ThreadPoolTaskExecutor mvcExecutor = new ThreadPoolTaskExecutor();
|
||||||
|
int maxCorePoolSize = 64;
|
||||||
|
int availableProcessorCount = Runtime.getRuntime().availableProcessors();
|
||||||
|
int corePoolSize = Math.min(maxCorePoolSize, availableProcessorCount);
|
||||||
|
mvcExecutor.setCorePoolSize(corePoolSize);
|
||||||
|
mvcExecutor.setMaxPoolSize(maxCorePoolSize);
|
||||||
|
mvcExecutor.setThreadNamePrefix("mvc-executor-");
|
||||||
|
mvcExecutor.initialize();
|
||||||
|
configurer.setTaskExecutor(mvcExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
server.port=8080
|
server.port=8080
|
||||||
logging.level.org.jooq=INFO
|
logging.level.org.jooq=INFO
|
||||||
|
logging.level.com.databasir.core.domain.document.generator=debug
|
||||||
spring.jooq.sql-dialect=mysql
|
spring.jooq.sql-dialect=mysql
|
||||||
springdoc.swagger-ui.path=/open-api.html
|
springdoc.swagger-ui.path=/open-api.html
|
||||||
# flyway
|
# flyway
|
||||||
|
|
|
@ -16,4 +16,5 @@ spring.flyway.locations=classpath:db/migration
|
||||||
databasir.db.driver-directory=drivers
|
databasir.db.driver-directory=drivers
|
||||||
databasir.jwt.secret=${DATABASIR_JWT_SECRET:${random.uuid}}
|
databasir.jwt.secret=${DATABASIR_JWT_SECRET:${random.uuid}}
|
||||||
# api doc
|
# api doc
|
||||||
springdoc.api-docs.enabled=false
|
springdoc.api-docs.enabled=false
|
||||||
|
spring.mvc.async.request-timeout=3600000
|
|
@ -9,6 +9,8 @@ dependencies {
|
||||||
implementation project(":dao")
|
implementation project(":dao")
|
||||||
implementation project(":meta")
|
implementation project(":meta")
|
||||||
|
|
||||||
|
implementation 'net.sourceforge.plantuml:plantuml:1.2022.5'
|
||||||
|
|
||||||
// spring boot
|
// spring boot
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
|
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.databasir.core.domain.document.data;
|
||||||
|
|
||||||
|
import com.databasir.core.domain.document.generator.DocumentFileType;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DocumentFileTypeResponse {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String fileExtension;
|
||||||
|
|
||||||
|
private DocumentFileType type;
|
||||||
|
|
||||||
|
}
|
|
@ -7,7 +7,15 @@ import lombok.Getter;
|
||||||
@Getter
|
@Getter
|
||||||
public enum DocumentFileType {
|
public enum DocumentFileType {
|
||||||
|
|
||||||
MARKDOWN("md"), EXCEL("xlsx");
|
MARKDOWN("md", "Markdown"),
|
||||||
|
|
||||||
|
PLANT_UML_ER_SVG("svg", "UML SVG"),
|
||||||
|
|
||||||
|
PLANT_UML_ER_PNG("png", "UML PNG"),
|
||||||
|
;
|
||||||
|
|
||||||
private String fileExtension;
|
private String fileExtension;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
package com.databasir.core.domain.document.generator;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class ExcelDocumentFileGenerator implements DocumentFileGenerator {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean support(DocumentFileType type) {
|
|
||||||
return type == DocumentFileType.EXCEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void generate(DocumentFileGenerateContext context, OutputStream outputStream) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildTableWithSheet() {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
package com.databasir.core.domain.document.generator.plantuml;
|
||||||
|
|
||||||
|
import com.databasir.common.SystemException;
|
||||||
|
import com.databasir.core.domain.document.data.DatabaseDocumentResponse;
|
||||||
|
import com.databasir.core.domain.document.data.TableDocumentResponse;
|
||||||
|
import com.databasir.core.domain.document.generator.DocumentFileGenerator;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.sourceforge.plantuml.FileFormatOption;
|
||||||
|
import net.sourceforge.plantuml.SourceStringReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public abstract class BasePlantUmlFileGenerator implements DocumentFileGenerator {
|
||||||
|
|
||||||
|
public static final String LINE = "\r\n";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate(DocumentFileGenerateContext context, OutputStream outputStream) {
|
||||||
|
String dsl = new ErDsl(context).toDsl();
|
||||||
|
try {
|
||||||
|
new SourceStringReader(dsl).outputImage(outputStream, fileFormatOption());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("export plantuml error", e);
|
||||||
|
throw new SystemException("System error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract FileFormatOption fileFormatOption();
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ErDsl {
|
||||||
|
|
||||||
|
private final DocumentFileGenerateContext context;
|
||||||
|
|
||||||
|
private Set<String> foreignKeyRelations = new HashSet<>(16);
|
||||||
|
|
||||||
|
public String toDsl() {
|
||||||
|
DatabaseDocumentResponse databaseDocument = context.getDatabaseDocument();
|
||||||
|
StringBuilder dslBuilder = new StringBuilder(1024);
|
||||||
|
dslBuilder.append("@startuml").append(LINE);
|
||||||
|
|
||||||
|
// configuration
|
||||||
|
dslBuilder.append("' hide the spot").append(LINE);
|
||||||
|
dslBuilder.append("hide circle").append(LINE);
|
||||||
|
|
||||||
|
// entities
|
||||||
|
String entities = databaseDocument.getTables()
|
||||||
|
.stream()
|
||||||
|
.map(table -> toErDsl(table))
|
||||||
|
.collect(Collectors.joining(LINE));
|
||||||
|
dslBuilder.append(entities);
|
||||||
|
|
||||||
|
// relation
|
||||||
|
dslBuilder.append(LINE);
|
||||||
|
String relations = foreignKeyRelations.stream()
|
||||||
|
.collect(Collectors.joining(LINE));
|
||||||
|
dslBuilder.append(relations);
|
||||||
|
dslBuilder.append(LINE);
|
||||||
|
|
||||||
|
dslBuilder.append("@enduml");
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("------------------------------");
|
||||||
|
log.debug(dslBuilder.toString());
|
||||||
|
log.debug("------------------------------");
|
||||||
|
}
|
||||||
|
return dslBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toErDsl(TableDocumentResponse table) {
|
||||||
|
StringBuilder dslBuilder = new StringBuilder(1024);
|
||||||
|
dslBuilder.append("entity ").append(table.getName())
|
||||||
|
.append(" {");
|
||||||
|
table.getColumns()
|
||||||
|
.stream()
|
||||||
|
.filter(TableDocumentResponse.ColumnDocumentResponse::getIsPrimaryKey)
|
||||||
|
.forEach(primaryCol -> {
|
||||||
|
dslBuilder.append(LINE);
|
||||||
|
dslBuilder.append("*")
|
||||||
|
.append(primaryCol.getName())
|
||||||
|
.append(" : ")
|
||||||
|
.append(primaryCol.getType())
|
||||||
|
.append("(")
|
||||||
|
.append(primaryCol.getSize())
|
||||||
|
.append(")")
|
||||||
|
.append(" <PK> ");
|
||||||
|
dslBuilder.append(LINE);
|
||||||
|
dslBuilder.append("--");
|
||||||
|
});
|
||||||
|
table.getColumns()
|
||||||
|
.stream()
|
||||||
|
.filter(col -> !col.getIsPrimaryKey())
|
||||||
|
.forEach(col -> {
|
||||||
|
dslBuilder.append(LINE);
|
||||||
|
if ("NO".equalsIgnoreCase(col.getNullable())) {
|
||||||
|
dslBuilder.append("*");
|
||||||
|
}
|
||||||
|
dslBuilder.append(col.getName())
|
||||||
|
.append(" : ")
|
||||||
|
.append(col.getType())
|
||||||
|
.append("(")
|
||||||
|
.append(col.getSize())
|
||||||
|
.append(")");
|
||||||
|
if (col.getComment() != null && !"".equals(col.getComment().trim())) {
|
||||||
|
dslBuilder.append(" /* ").append(col.getComment()).append(" */");
|
||||||
|
}
|
||||||
|
dslBuilder.append(LINE);
|
||||||
|
});
|
||||||
|
dslBuilder.append("}");
|
||||||
|
dslBuilder.append(LINE);
|
||||||
|
|
||||||
|
table.getForeignKeys().forEach(fk -> {
|
||||||
|
String fkTableName = fk.getFkTableName();
|
||||||
|
String fkColumnName = fk.getFkColumnName();
|
||||||
|
String pkTableName = fk.getPkTableName();
|
||||||
|
String pkColumnName = fk.getPkColumnName();
|
||||||
|
StringBuilder relationBuilder = new StringBuilder();
|
||||||
|
relationBuilder.append(fkTableName).append("::").append(fkColumnName)
|
||||||
|
.append(" --> ")
|
||||||
|
.append(pkTableName).append("::").append(pkColumnName)
|
||||||
|
.append(" : ")
|
||||||
|
.append(Objects.requireNonNullElse(fk.getFkName(), ""));
|
||||||
|
foreignKeyRelations.add(relationBuilder.toString());
|
||||||
|
});
|
||||||
|
return dslBuilder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.databasir.core.domain.document.generator.plantuml;
|
||||||
|
|
||||||
|
import com.databasir.core.domain.document.generator.DocumentFileType;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.sourceforge.plantuml.FileFormat;
|
||||||
|
import net.sourceforge.plantuml.FileFormatOption;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class PlantUmlErSvgFileGenerator extends BasePlantUmlFileGenerator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean support(DocumentFileType type) {
|
||||||
|
return type == DocumentFileType.PLANT_UML_ER_SVG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FileFormatOption fileFormatOption() {
|
||||||
|
return new FileFormatOption(FileFormat.SVG);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.databasir.core.domain.document.generator.plantuml;
|
||||||
|
|
||||||
|
import com.databasir.core.domain.document.generator.DocumentFileType;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.sourceforge.plantuml.FileFormat;
|
||||||
|
import net.sourceforge.plantuml.FileFormatOption;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class PlantUmlPngFileGenerator extends BasePlantUmlFileGenerator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean support(DocumentFileType type) {
|
||||||
|
return type == DocumentFileType.PLANT_UML_ER_PNG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FileFormatOption fileFormatOption() {
|
||||||
|
return new FileFormatOption(FileFormat.PNG);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
Subproject commit 0f8da6c5011505d3e45d996ca59a022e3abcc5a7
|
Subproject commit 7bac0c7f123c89a1e70c82d43f2c7c7d061dd943
|
Loading…
Reference in New Issue