Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion jig-core/src/main/java/org/dddjava/jig/HandleResultImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public boolean isOutputDiagram() {
Insight,
Sequence,
Glossary,
PackageSummary -> false;
PackageSummary,
ListOutput -> false;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public JigDocumentGenerator(JigDocumentContext jigDocumentContext, JigService ji
compositeAdapter.register(new SummaryAdapter(jigService, new ThymeleafSummaryWriter(templateEngine, jigDocumentContext)));
compositeAdapter.register(new InsightAdapter(jigService, templateEngine, jigDocumentContext));
compositeAdapter.register(new RepositorySummaryAdapter(jigService, templateEngine, jigDocumentContext));
compositeAdapter.register(new ListOutputAdapter(jigService, templateEngine, jigDocumentContext));
}

public JigResult generate(JigRepository jigRepository) {
Expand Down Expand Up @@ -131,7 +132,7 @@ HandleResult generateDocument(JigDocument jigDocument, Path outputDirectory, Jig
case DomainSummary, ApplicationSummary, UsecaseSummary, EntrypointSummary,
PackageRelationDiagram, BusinessRuleRelationDiagram, CategoryDiagram, CategoryUsageDiagram,
ServiceMethodCallHierarchyDiagram,
BusinessRuleList, ApplicationList,
BusinessRuleList, ApplicationList, ListOutput,
RepositorySummary, Insight, Sequence -> compositeAdapter.invoke(jigDocument, jigRepository);
};

Expand Down Expand Up @@ -165,6 +166,7 @@ private void generateAssets() {
copyAsset("package.js", assetsPath);
copyAsset("glossary.js", assetsPath);
copyAsset("insight.js", assetsPath);
copyAsset("list-output.js", assetsPath);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.dddjava.jig.adapter.thymeleaf;

import org.dddjava.jig.adapter.HandleDocument;
import org.dddjava.jig.adapter.JigDocumentWriter;
import org.dddjava.jig.application.JigService;
import org.dddjava.jig.domain.model.data.members.fields.JigFieldId;
import org.dddjava.jig.domain.model.data.types.TypeId;
import org.dddjava.jig.domain.model.documents.documentformat.JigDocument;
import org.dddjava.jig.domain.model.documents.stationery.JigDocumentContext;
import org.dddjava.jig.domain.model.information.JigRepository;
import org.dddjava.jig.domain.model.information.inputs.Entrypoint;
import org.dddjava.jig.domain.model.information.inputs.InputAdapters;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

@HandleDocument
public class ListOutputAdapter {

private final JigService jigService;
private final TemplateEngine templateEngine;
private final JigDocumentContext jigDocumentContext;

public ListOutputAdapter(JigService jigService, TemplateEngine templateEngine, JigDocumentContext jigDocumentContext) {
this.jigService = jigService;
this.templateEngine = templateEngine;
this.jigDocumentContext = jigDocumentContext;
}

@HandleDocument(JigDocument.ListOutput)
public List<Path> invoke(JigRepository repository, JigDocument jigDocument) {
InputAdapters inputAdapters = jigService.inputAdapters(repository);
String controllerJson = inputAdapters.listEntrypoint().stream()
.map(this::formatControllerJson)
.collect(Collectors.joining(",", "[", "]"));

String listJson = """
{"controllers": %s}
""".formatted(controllerJson);

JigDocumentWriter jigDocumentWriter = new JigDocumentWriter(jigDocument, jigDocumentContext.outputDirectory());
Map<String, Object> contextMap = Map.of(
"title", jigDocumentWriter.jigDocument().label(),
"listJson", listJson
);

Context context = new Context(Locale.ROOT, contextMap);
String template = jigDocumentWriter.jigDocument().fileName();

jigDocumentWriter.writeTextAs(".html",
writer -> templateEngine.process(template, context, writer));
return jigDocumentWriter.outputFilePaths();
}

private String formatControllerJson(Entrypoint entrypoint) {
String usingFieldTypesJson = entrypoint.jigMethod().usingFields().jigFieldIds().stream()
.map(JigFieldId::declaringTypeId)
.map(TypeId::asSimpleText)
.sorted()
.map(this::escape)
.map(value -> "\"" + value + "\"")
.collect(Collectors.joining(",", "[", "]"));
return """
{"packageName": "%s", "typeName": "%s", "methodSignature": "%s", "returnType": "%s", "typeLabel": "%s", "usingFieldTypes": %s, "cyclomaticComplexity": %d, "path": "%s"}
""".formatted(
escape(entrypoint.packageId().asText()),
escape(entrypoint.typeId().asSimpleText()),
escape(entrypoint.jigMethod().simpleMethodSignatureText()),
escape(entrypoint.jigMethod().returnType().simpleName()),
escape(entrypoint.jigType().label()),
usingFieldTypesJson,
entrypoint.jigMethod().instructions().cyclomaticComplexity(),
escape(entrypoint.fullPathText()));
}

private String escape(String string) {
return string
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\r", "\\r")
.replace("\n", "\\n");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ public enum JigDocument {
ApplicationList(
JigDocumentLabel.of("機能一覧", "ApplicationList"),
"application"),
/**
* 一覧出力
*
* 一覧をHTMLで出力する。
*/
ListOutput(
JigDocumentLabel.of("一覧出力", "ListOutput"),
"list-output"),

/**
* サービスメソッド呼び出し図
Expand Down
138 changes: 138 additions & 0 deletions jig-core/src/main/resources/templates/assets/list-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
function getListData() {
const jsonText = document.getElementById("list-data")?.textContent || "{}";
/** @type {{controllers?: Array<{
* packageName: string,
* typeName: string,
* methodSignature: string,
* returnType: string,
* typeLabel: string,
* usingFieldTypes: string[],
* cyclomaticComplexity: number,
* path: string
* }>} | Array<{
* packageName: string,
* typeName: string,
* methodSignature: string,
* returnType: string,
* typeLabel: string,
* usingFieldTypes: string[],
* cyclomaticComplexity: number,
* path: string
* }>} */
const listData = JSON.parse(jsonText);
if (Array.isArray(listData)) {
return listData;
}
return listData.controllers ?? [];
}

function escapeCsvValue(value) {
const text = String(value ?? "")
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
return `"${text.replace(/"/g, "\"\"")}"`;
}

function formatFieldTypes(fieldTypes) {
if (!fieldTypes) return "";
if (Array.isArray(fieldTypes)) {
return fieldTypes.join("\n");
}
return String(fieldTypes);
}

function buildControllerCsv(items) {
const header = [
"パッケージ名",
"クラス名",
"メソッドシグネチャ",
"メソッド戻り値の型",
"クラス別名",
"使用しているフィールドの型",
"循環的複雑度",
"パス",
];
const rows = items.map(item => [
item.packageName ?? "",
item.typeName ?? "",
item.methodSignature ?? "",
item.returnType ?? "",
item.typeLabel ?? "",
formatFieldTypes(item.usingFieldTypes),
item.cyclomaticComplexity ?? "",
item.path ?? "",
]);
const lines = [header, ...rows].map(row => row.map(escapeCsvValue).join(","));
return lines.join("\r\n");
}

function downloadCsv(text, filename) {
const blob = new Blob([text], {type: "text/csv;charset=utf-8;"});
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
}

function renderControllerTable(items) {
const tableBody = document.querySelector("#controller-list tbody");
if (!tableBody) return;
tableBody.innerHTML = "";

const fragment = document.createDocumentFragment();
items.forEach(item => {
const row = document.createElement("tr");
const values = [
item.packageName,
item.typeName,
item.methodSignature,
item.returnType,
item.typeLabel,
formatFieldTypes(item.usingFieldTypes),
item.cyclomaticComplexity,
item.path,
];
values.forEach((value, index) => {
const cell = document.createElement("td");
if (index === 6) {
cell.className = "number";
}
cell.textContent = value ?? "";
row.appendChild(cell);
});
fragment.appendChild(row);
});

tableBody.appendChild(fragment);
}

if (typeof document !== "undefined") {
document.addEventListener("DOMContentLoaded", function () {
if (!document.body.classList.contains("list-output")) return;
const items = getListData();
renderControllerTable(items);

const exportButton = document.getElementById("export-csv");
if (exportButton) {
exportButton.addEventListener("click", () => {
const csvText = buildControllerCsv(items);
downloadCsv(csvText, "list-output.csv");
});
}
});
}

// Nodeのテスト用エクスポート。ブラウザでは無視される。
if (typeof module !== "undefined" && module.exports) {
module.exports = {
getListData,
escapeCsvValue,
formatFieldTypes,
buildControllerCsv,
renderControllerTable,
};
}
5 changes: 5 additions & 0 deletions jig-core/src/main/resources/templates/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,11 @@ label {
display: none;
}

/* 一覧出力のヘッダは折り返さない */
.list-output table thead th {
white-space: nowrap;
}

/* テーブルの行をゼブラスタイルにする */
table.zebra tbody tr:nth-child(odd) {
background-color: #f9f9f9;
Expand Down
8 changes: 7 additions & 1 deletion jig-core/src/main/resources/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ <h2>概要: HTML</h2>
<li th:if="${Insight}"><a href="#" th:href="${Insight}">インサイト</a> (incubate)</li>
</ul>
</section>
<section>
<h2>一覧: HTML</h2>
<ul>
<li th:if="${ListOutput}"><a href="#" th:href="${ListOutput}">一覧出力</a> (incubate)</li>
</ul>
</section>
<section>
<h2>一覧: Excel</h2>
<ul>
Expand All @@ -51,4 +57,4 @@ <h3 th:text="${diagram.label()}">XXX</h3>
</main>

</body>
</html>
</html>
66 changes: 66 additions & 0 deletions jig-core/src/main/resources/templates/list-output.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="ja">
<head th:replace="~{fragment-base::head(${title})}">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="./assets/style.css" rel="stylesheet">
<link rel="icon" href="./assets/favicon.ico">
<title>一覧出力</title>
</head>
<body class="list-output">
<header class="top" th:replace="~{fragment-base::header(title=${title})}">たいとる</header>
<main>
<h1 th:text="${title}">一覧出力</h1>

<aside class="notice">
<p>注意: このドキュメントはincubate(作成中)です。次のバージョンで大きく変更したり削除したりする可能性があります。</p>
</aside>

<section>
<h2>CONTROLLER</h2>
<div>
<button id="export-csv" type="button">CSV出力</button>
</div>
<table id="controller-list" class="zebra">
<thead>
<tr>
<th>パッケージ名</th>
<th>クラス名</th>
<th>メソッドシグネチャ</th>
<th>メソッド戻り値の型</th>
<th>クラス別名</th>
<th>使用しているフィールドの型</th>
<th>循環的複雑度</th>
<th>パス</th>
</tr>
</thead>
<tbody></tbody>
</table>
</section>
</main>

<script id="list-data" type="application/json" th:utext="${listJson}">
{
"controllers": [
{
"packageName": "com.example",
"typeName": "ExampleController",
"methodSignature": "getExample()",
"returnType": "Example",
"typeLabel": "例",
"usingFieldTypes": ["ExampleRepository"],
"cyclomaticComplexity": 1,
"path": "GET /example"
}
]
}
</script>

<th:block th:replace="~{fragment-base::scripts}">
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>
<script src="./assets/jig.js"></script>
</th:block>
<script src="./assets/list-output.js"></script>
</body>
</html>
Loading