feat: use plantuml to export er diagram

This commit is contained in:
vran 2022-05-25 23:40:59 +08:00
parent 06f5044e39
commit 6c5965f466
13 changed files with 244 additions and 30 deletions

View File

@ -22,9 +22,10 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import javax.validation.Valid;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.springframework.data.domain.Sort.Direction.DESC;
@ -75,9 +76,10 @@ public class DocumentController {
Long version,
@RequestParam DocumentFileType fileType) {
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()
.filename("demo.md", StandardCharsets.UTF_8)
.filename(fileName)
.build());
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
return ResponseEntity.ok()
@ -85,6 +87,14 @@ public class DocumentController {
.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)
@Operation(summary = "获取文档(无详情信息)")
public JsonData<DatabaseDocumentSimpleResponse> getSimpleByProjectId(@PathVariable Integer projectId,

View File

@ -92,6 +92,8 @@ public interface Routes {
String EXPORT = BASE + "/projects/{projectId}/document_files";
String EXPORT_TYPES = BASE + "/document_file_types";
String LIST_TABLES = BASE + "/projects/{projectId}/tables";
}

View File

@ -11,15 +11,30 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
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.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.time.format.DateTimeFormatter;
@Configuration
@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
public void addCorsMappings(CorsRegistry registry) {

View File

@ -1,5 +1,6 @@
server.port=8080
logging.level.org.jooq=INFO
logging.level.com.databasir.core.domain.document.generator=debug
spring.jooq.sql-dialect=mysql
springdoc.swagger-ui.path=/open-api.html
# flyway

View File

@ -16,4 +16,5 @@ spring.flyway.locations=classpath:db/migration
databasir.db.driver-directory=drivers
databasir.jwt.secret=${DATABASIR_JWT_SECRET:${random.uuid}}
# api doc
springdoc.api-docs.enabled=false
springdoc.api-docs.enabled=false
spring.mvc.async.request-timeout=3600000

View File

@ -9,6 +9,8 @@ dependencies {
implementation project(":dao")
implementation project(":meta")
implementation 'net.sourceforge.plantuml:plantuml:1.2022.5'
// spring boot
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-validation'

View File

@ -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;
}

View File

@ -7,7 +7,15 @@ import lombok.Getter;
@Getter
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 name;
}

View File

@ -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() {
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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