Skip to content

Commit 2e45c23

Browse files
authored
Merge pull request #1035 from dddjava/agent/application-list-html
一覧出力を追加
2 parents 39b474d + 44fdfe7 commit 2e45c23

9 files changed

Lines changed: 419 additions & 3 deletions

File tree

jig-core/src/main/java/org/dddjava/jig/HandleResultImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ public boolean isOutputDiagram() {
5454
Insight,
5555
Sequence,
5656
Glossary,
57-
PackageSummary -> false;
57+
PackageSummary,
58+
ListOutput -> false;
5859
};
5960
}
6061

jig-core/src/main/java/org/dddjava/jig/adapter/JigDocumentGenerator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public JigDocumentGenerator(JigDocumentContext jigDocumentContext, JigService ji
6868
compositeAdapter.register(new SummaryAdapter(jigService, new ThymeleafSummaryWriter(templateEngine, jigDocumentContext)));
6969
compositeAdapter.register(new InsightAdapter(jigService, templateEngine, jigDocumentContext));
7070
compositeAdapter.register(new RepositorySummaryAdapter(jigService, templateEngine, jigDocumentContext));
71+
compositeAdapter.register(new ListOutputAdapter(jigService, templateEngine, jigDocumentContext));
7172
}
7273

7374
public JigResult generate(JigRepository jigRepository) {
@@ -131,7 +132,7 @@ HandleResult generateDocument(JigDocument jigDocument, Path outputDirectory, Jig
131132
case DomainSummary, ApplicationSummary, UsecaseSummary, EntrypointSummary,
132133
PackageRelationDiagram, BusinessRuleRelationDiagram, CategoryDiagram, CategoryUsageDiagram,
133134
ServiceMethodCallHierarchyDiagram,
134-
BusinessRuleList, ApplicationList,
135+
BusinessRuleList, ApplicationList, ListOutput,
135136
RepositorySummary, Insight, Sequence -> compositeAdapter.invoke(jigDocument, jigRepository);
136137
};
137138

@@ -165,6 +166,7 @@ private void generateAssets() {
165166
copyAsset("package.js", assetsPath);
166167
copyAsset("glossary.js", assetsPath);
167168
copyAsset("insight.js", assetsPath);
169+
copyAsset("list-output.js", assetsPath);
168170
} catch (IOException e) {
169171
throw new UncheckedIOException(e);
170172
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package org.dddjava.jig.adapter.thymeleaf;
2+
3+
import org.dddjava.jig.adapter.HandleDocument;
4+
import org.dddjava.jig.adapter.JigDocumentWriter;
5+
import org.dddjava.jig.application.JigService;
6+
import org.dddjava.jig.domain.model.data.members.fields.JigFieldId;
7+
import org.dddjava.jig.domain.model.data.types.TypeId;
8+
import org.dddjava.jig.domain.model.documents.documentformat.JigDocument;
9+
import org.dddjava.jig.domain.model.documents.stationery.JigDocumentContext;
10+
import org.dddjava.jig.domain.model.information.JigRepository;
11+
import org.dddjava.jig.domain.model.information.inputs.Entrypoint;
12+
import org.dddjava.jig.domain.model.information.inputs.InputAdapters;
13+
import org.thymeleaf.TemplateEngine;
14+
import org.thymeleaf.context.Context;
15+
16+
import java.nio.file.Path;
17+
import java.util.List;
18+
import java.util.Locale;
19+
import java.util.Map;
20+
import java.util.stream.Collectors;
21+
22+
@HandleDocument
23+
public class ListOutputAdapter {
24+
25+
private final JigService jigService;
26+
private final TemplateEngine templateEngine;
27+
private final JigDocumentContext jigDocumentContext;
28+
29+
public ListOutputAdapter(JigService jigService, TemplateEngine templateEngine, JigDocumentContext jigDocumentContext) {
30+
this.jigService = jigService;
31+
this.templateEngine = templateEngine;
32+
this.jigDocumentContext = jigDocumentContext;
33+
}
34+
35+
@HandleDocument(JigDocument.ListOutput)
36+
public List<Path> invoke(JigRepository repository, JigDocument jigDocument) {
37+
InputAdapters inputAdapters = jigService.inputAdapters(repository);
38+
String controllerJson = inputAdapters.listEntrypoint().stream()
39+
.map(this::formatControllerJson)
40+
.collect(Collectors.joining(",", "[", "]"));
41+
42+
String listJson = """
43+
{"controllers": %s}
44+
""".formatted(controllerJson);
45+
46+
JigDocumentWriter jigDocumentWriter = new JigDocumentWriter(jigDocument, jigDocumentContext.outputDirectory());
47+
Map<String, Object> contextMap = Map.of(
48+
"title", jigDocumentWriter.jigDocument().label(),
49+
"listJson", listJson
50+
);
51+
52+
Context context = new Context(Locale.ROOT, contextMap);
53+
String template = jigDocumentWriter.jigDocument().fileName();
54+
55+
jigDocumentWriter.writeTextAs(".html",
56+
writer -> templateEngine.process(template, context, writer));
57+
return jigDocumentWriter.outputFilePaths();
58+
}
59+
60+
private String formatControllerJson(Entrypoint entrypoint) {
61+
String usingFieldTypesJson = entrypoint.jigMethod().usingFields().jigFieldIds().stream()
62+
.map(JigFieldId::declaringTypeId)
63+
.map(TypeId::asSimpleText)
64+
.sorted()
65+
.map(this::escape)
66+
.map(value -> "\"" + value + "\"")
67+
.collect(Collectors.joining(",", "[", "]"));
68+
return """
69+
{"packageName": "%s", "typeName": "%s", "methodSignature": "%s", "returnType": "%s", "typeLabel": "%s", "usingFieldTypes": %s, "cyclomaticComplexity": %d, "path": "%s"}
70+
""".formatted(
71+
escape(entrypoint.packageId().asText()),
72+
escape(entrypoint.typeId().asSimpleText()),
73+
escape(entrypoint.jigMethod().simpleMethodSignatureText()),
74+
escape(entrypoint.jigMethod().returnType().simpleName()),
75+
escape(entrypoint.jigType().label()),
76+
usingFieldTypesJson,
77+
entrypoint.jigMethod().instructions().cyclomaticComplexity(),
78+
escape(entrypoint.fullPathText()));
79+
}
80+
81+
private String escape(String string) {
82+
return string
83+
.replace("\\", "\\\\")
84+
.replace("\"", "\\\"")
85+
.replace("\r", "\\r")
86+
.replace("\n", "\\n");
87+
}
88+
}

jig-core/src/main/java/org/dddjava/jig/domain/model/documents/documentformat/JigDocument.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ public enum JigDocument {
7777
ApplicationList(
7878
JigDocumentLabel.of("機能一覧", "ApplicationList"),
7979
"application"),
80+
/**
81+
* 一覧出力
82+
*
83+
* 一覧をHTMLで出力する。
84+
*/
85+
ListOutput(
86+
JigDocumentLabel.of("一覧出力", "ListOutput"),
87+
"list-output"),
8088

8189
/**
8290
* サービスメソッド呼び出し図
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
function getListData() {
2+
const jsonText = document.getElementById("list-data")?.textContent || "{}";
3+
/** @type {{controllers?: Array<{
4+
* packageName: string,
5+
* typeName: string,
6+
* methodSignature: string,
7+
* returnType: string,
8+
* typeLabel: string,
9+
* usingFieldTypes: string[],
10+
* cyclomaticComplexity: number,
11+
* path: string
12+
* }>} | Array<{
13+
* packageName: string,
14+
* typeName: string,
15+
* methodSignature: string,
16+
* returnType: string,
17+
* typeLabel: string,
18+
* usingFieldTypes: string[],
19+
* cyclomaticComplexity: number,
20+
* path: string
21+
* }>} */
22+
const listData = JSON.parse(jsonText);
23+
if (Array.isArray(listData)) {
24+
return listData;
25+
}
26+
return listData.controllers ?? [];
27+
}
28+
29+
function escapeCsvValue(value) {
30+
const text = String(value ?? "")
31+
.replace(/\r\n/g, "\n")
32+
.replace(/\r/g, "\n");
33+
return `"${text.replace(/"/g, "\"\"")}"`;
34+
}
35+
36+
function formatFieldTypes(fieldTypes) {
37+
if (!fieldTypes) return "";
38+
if (Array.isArray(fieldTypes)) {
39+
return fieldTypes.join("\n");
40+
}
41+
return String(fieldTypes);
42+
}
43+
44+
function buildControllerCsv(items) {
45+
const header = [
46+
"パッケージ名",
47+
"クラス名",
48+
"メソッドシグネチャ",
49+
"メソッド戻り値の型",
50+
"クラス別名",
51+
"使用しているフィールドの型",
52+
"循環的複雑度",
53+
"パス",
54+
];
55+
const rows = items.map(item => [
56+
item.packageName ?? "",
57+
item.typeName ?? "",
58+
item.methodSignature ?? "",
59+
item.returnType ?? "",
60+
item.typeLabel ?? "",
61+
formatFieldTypes(item.usingFieldTypes),
62+
item.cyclomaticComplexity ?? "",
63+
item.path ?? "",
64+
]);
65+
const lines = [header, ...rows].map(row => row.map(escapeCsvValue).join(","));
66+
return lines.join("\r\n");
67+
}
68+
69+
function downloadCsv(text, filename) {
70+
const blob = new Blob([text], {type: "text/csv;charset=utf-8;"});
71+
const url = URL.createObjectURL(blob);
72+
const anchor = document.createElement("a");
73+
anchor.href = url;
74+
anchor.download = filename;
75+
document.body.appendChild(anchor);
76+
anchor.click();
77+
anchor.remove();
78+
URL.revokeObjectURL(url);
79+
}
80+
81+
function renderControllerTable(items) {
82+
const tableBody = document.querySelector("#controller-list tbody");
83+
if (!tableBody) return;
84+
tableBody.innerHTML = "";
85+
86+
const fragment = document.createDocumentFragment();
87+
items.forEach(item => {
88+
const row = document.createElement("tr");
89+
const values = [
90+
item.packageName,
91+
item.typeName,
92+
item.methodSignature,
93+
item.returnType,
94+
item.typeLabel,
95+
formatFieldTypes(item.usingFieldTypes),
96+
item.cyclomaticComplexity,
97+
item.path,
98+
];
99+
values.forEach((value, index) => {
100+
const cell = document.createElement("td");
101+
if (index === 6) {
102+
cell.className = "number";
103+
}
104+
cell.textContent = value ?? "";
105+
row.appendChild(cell);
106+
});
107+
fragment.appendChild(row);
108+
});
109+
110+
tableBody.appendChild(fragment);
111+
}
112+
113+
if (typeof document !== "undefined") {
114+
document.addEventListener("DOMContentLoaded", function () {
115+
if (!document.body.classList.contains("list-output")) return;
116+
const items = getListData();
117+
renderControllerTable(items);
118+
119+
const exportButton = document.getElementById("export-csv");
120+
if (exportButton) {
121+
exportButton.addEventListener("click", () => {
122+
const csvText = buildControllerCsv(items);
123+
downloadCsv(csvText, "list-output.csv");
124+
});
125+
}
126+
});
127+
}
128+
129+
// Nodeのテスト用エクスポート。ブラウザでは無視される。
130+
if (typeof module !== "undefined" && module.exports) {
131+
module.exports = {
132+
getListData,
133+
escapeCsvValue,
134+
formatFieldTypes,
135+
buildControllerCsv,
136+
renderControllerTable,
137+
};
138+
}

jig-core/src/main/resources/templates/assets/style.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,11 @@ label {
465465
display: none;
466466
}
467467

468+
/* 一覧出力のヘッダは折り返さない */
469+
.list-output table thead th {
470+
white-space: nowrap;
471+
}
472+
468473
/* テーブルの行をゼブラスタイルにする */
469474
table.zebra tbody tr:nth-child(odd) {
470475
background-color: #f9f9f9;

jig-core/src/main/resources/templates/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ <h2>概要: HTML</h2>
2727
<li th:if="${Insight}"><a href="#" th:href="${Insight}">インサイト</a> (incubate)</li>
2828
</ul>
2929
</section>
30+
<section>
31+
<h2>一覧: HTML</h2>
32+
<ul>
33+
<li th:if="${ListOutput}"><a href="#" th:href="${ListOutput}">一覧出力</a> (incubate)</li>
34+
</ul>
35+
</section>
3036
<section>
3137
<h2>一覧: Excel</h2>
3238
<ul>
@@ -51,4 +57,4 @@ <h3 th:text="${diagram.label()}">XXX</h3>
5157
</main>
5258

5359
</body>
54-
</html>
60+
</html>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!DOCTYPE html>
2+
<html lang="ja">
3+
<head th:replace="~{fragment-base::head(${title})}">
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<link href="./assets/style.css" rel="stylesheet">
7+
<link rel="icon" href="./assets/favicon.ico">
8+
<title>一覧出力</title>
9+
</head>
10+
<body class="list-output">
11+
<header class="top" th:replace="~{fragment-base::header(title=${title})}">たいとる</header>
12+
<main>
13+
<h1 th:text="${title}">一覧出力</h1>
14+
15+
<aside class="notice">
16+
<p>注意: このドキュメントはincubate(作成中)です。次のバージョンで大きく変更したり削除したりする可能性があります。</p>
17+
</aside>
18+
19+
<section>
20+
<h2>CONTROLLER</h2>
21+
<div>
22+
<button id="export-csv" type="button">CSV出力</button>
23+
</div>
24+
<table id="controller-list" class="zebra">
25+
<thead>
26+
<tr>
27+
<th>パッケージ名</th>
28+
<th>クラス名</th>
29+
<th>メソッドシグネチャ</th>
30+
<th>メソッド戻り値の型</th>
31+
<th>クラス別名</th>
32+
<th>使用しているフィールドの型</th>
33+
<th>循環的複雑度</th>
34+
<th>パス</th>
35+
</tr>
36+
</thead>
37+
<tbody></tbody>
38+
</table>
39+
</section>
40+
</main>
41+
42+
<script id="list-data" type="application/json" th:utext="${listJson}">
43+
{
44+
"controllers": [
45+
{
46+
"packageName": "com.example",
47+
"typeName": "ExampleController",
48+
"methodSignature": "getExample()",
49+
"returnType": "Example",
50+
"typeLabel": "例",
51+
"usingFieldTypes": ["ExampleRepository"],
52+
"cyclomaticComplexity": 1,
53+
"path": "GET /example"
54+
}
55+
]
56+
}
57+
</script>
58+
59+
<th:block th:replace="~{fragment-base::scripts}">
60+
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
61+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>
62+
<script src="./assets/jig.js"></script>
63+
</th:block>
64+
<script src="./assets/list-output.js"></script>
65+
</body>
66+
</html>

0 commit comments

Comments
 (0)