mirror of
				https://github.com/vran-dev/databasir.git
				synced 2025-10-31 04:29:20 +08:00 
			
		
		
		
	feat: use plantuml to export er diagram
This commit is contained in:
		| @@ -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, | ||||
|   | ||||
| @@ -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"; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -17,3 +17,4 @@ databasir.db.driver-directory=drivers | ||||
| databasir.jwt.secret=${DATABASIR_JWT_SECRET:${random.uuid}} | ||||
| # api doc | ||||
| springdoc.api-docs.enabled=false | ||||
| spring.mvc.async.request-timeout=3600000 | ||||
| @@ -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' | ||||
|   | ||||
| @@ -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 | ||||
| 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; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
 Submodule databasir-frontend updated: 0f8da6c501...7bac0c7f12
									
								
							
		Reference in New Issue
	
	Block a user