diff --git a/api/src/main/java/com/databasir/api/DocumentController.java b/api/src/main/java/com/databasir/api/DocumentController.java index 6ac8cf3..05d42b5 100644 --- a/api/src/main/java/com/databasir/api/DocumentController.java +++ b/api/src/main/java/com/databasir/api/DocumentController.java @@ -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> getDocumentFileTypes() { + List 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 getSimpleByProjectId(@PathVariable Integer projectId, diff --git a/api/src/main/java/com/databasir/api/Routes.java b/api/src/main/java/com/databasir/api/Routes.java index ca5731b..060efc7 100644 --- a/api/src/main/java/com/databasir/api/Routes.java +++ b/api/src/main/java/com/databasir/api/Routes.java @@ -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"; } diff --git a/api/src/main/java/com/databasir/api/config/WebConfig.java b/api/src/main/java/com/databasir/api/config/WebConfig.java index 5e3539a..4732fb1 100644 --- a/api/src/main/java/com/databasir/api/config/WebConfig.java +++ b/api/src/main/java/com/databasir/api/config/WebConfig.java @@ -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) { diff --git a/api/src/main/resources/application-local.properties b/api/src/main/resources/application-local.properties index 5ef23d5..f7c2ecb 100644 --- a/api/src/main/resources/application-local.properties +++ b/api/src/main/resources/application-local.properties @@ -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 diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 17e82cb..981ea4a 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -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 \ No newline at end of file +springdoc.api-docs.enabled=false +spring.mvc.async.request-timeout=3600000 \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index a58611a..c24f5ed 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -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' diff --git a/core/src/main/java/com/databasir/core/domain/document/data/DocumentFileTypeResponse.java b/core/src/main/java/com/databasir/core/domain/document/data/DocumentFileTypeResponse.java new file mode 100644 index 0000000..e8199de --- /dev/null +++ b/core/src/main/java/com/databasir/core/domain/document/data/DocumentFileTypeResponse.java @@ -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; + +} diff --git a/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileType.java b/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileType.java index 94e345f..89257bb 100644 --- a/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileType.java +++ b/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileType.java @@ -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; + } diff --git a/core/src/main/java/com/databasir/core/domain/document/generator/ExcelDocumentFileGenerator.java b/core/src/main/java/com/databasir/core/domain/document/generator/ExcelDocumentFileGenerator.java deleted file mode 100644 index 168420a..0000000 --- a/core/src/main/java/com/databasir/core/domain/document/generator/ExcelDocumentFileGenerator.java +++ /dev/null @@ -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() { - } -} diff --git a/core/src/main/java/com/databasir/core/domain/document/generator/plantuml/BasePlantUmlFileGenerator.java b/core/src/main/java/com/databasir/core/domain/document/generator/plantuml/BasePlantUmlFileGenerator.java new file mode 100644 index 0000000..2a52c19 --- /dev/null +++ b/core/src/main/java/com/databasir/core/domain/document/generator/plantuml/BasePlantUmlFileGenerator.java @@ -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 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(" "); + 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(); + } + } +} diff --git a/core/src/main/java/com/databasir/core/domain/document/generator/plantuml/PlantUmlErSvgFileGenerator.java b/core/src/main/java/com/databasir/core/domain/document/generator/plantuml/PlantUmlErSvgFileGenerator.java new file mode 100644 index 0000000..fc88e59 --- /dev/null +++ b/core/src/main/java/com/databasir/core/domain/document/generator/plantuml/PlantUmlErSvgFileGenerator.java @@ -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); + } +} diff --git a/core/src/main/java/com/databasir/core/domain/document/generator/plantuml/PlantUmlPngFileGenerator.java b/core/src/main/java/com/databasir/core/domain/document/generator/plantuml/PlantUmlPngFileGenerator.java new file mode 100644 index 0000000..0990470 --- /dev/null +++ b/core/src/main/java/com/databasir/core/domain/document/generator/plantuml/PlantUmlPngFileGenerator.java @@ -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); + } +} diff --git a/databasir-frontend b/databasir-frontend index 0f8da6c..7bac0c7 160000 --- a/databasir-frontend +++ b/databasir-frontend @@ -1 +1 @@ -Subproject commit 0f8da6c5011505d3e45d996ca59a022e3abcc5a7 +Subproject commit 7bac0c7f123c89a1e70c82d43f2c7c7d061dd943