Skip to content

Commit 4ca6a4f

Browse files
Merge pull request #224 from refactorfirst/set-url-on-chart-bubble
Set url on chart bubble
2 parents fb0c67b + 377b6af commit 4ca6a4f

6 files changed

Lines changed: 1385 additions & 831 deletions

File tree

‎.refactorfirst/refactor-first.json‎

Lines changed: 964 additions & 805 deletions
Large diffs are not rendered by default.

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ back to returning the Java-only DTO with a `log.warn`
115115
is for build failures, not for "Kotlin is absent".
116116

117117
## Testing Notes
118+
- **TDD is required when implementing code.** Write tests before production code using the red-green-refactor cycle. Use the Given-When-Then format for test structure.
118119
- JUnit 5 with parameterized tests
119120
- Test fixtures in `test-resources/src/test/resources`
120121
- For graph algorithm changes, check `JavaGraphBuilderTest` and `CircularReferenceCheckerTests`

‎report/src/main/java/org/hjug/refactorfirst/report/JsonGenerator.java‎

Lines changed: 232 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public void execute(
7777
log.info("RefactorFirst JSON successfully generated at {}", targetFile.toAbsolutePath());
7878

7979
// Copy Mustache template and viewer to .refactorfirst directory
80-
copyViewerResources(dotRefactorFirstDir);
80+
// copyViewerResources(dotRefactorFirstDir);
8181
}
8282

8383
/** Copies the Mustache template and browser viewer into the report directory. */
@@ -254,13 +254,25 @@ public RefactorFirstReportDTO generateReportData(
254254
String startVertex = vertexes[0].trim();
255255
String endVertex = vertexes[1].trim();
256256

257+
String sourcePath =
258+
codebaseGraphDTO.getClassToSourceFilePathMapping().get(startVertex);
259+
String targetPath =
260+
codebaseGraphDTO.getClassToSourceFilePathMapping().get(endVertex);
261+
String sourceUrl = (sourcePath != null && !sourcePath.isBlank()) ? repoUrl + sourcePath : "";
262+
String targetUrl = (targetPath != null && !targetPath.isBlank()) ? repoUrl + targetPath : "";
263+
264+
// Build rendered label with links to class files
265+
String renderedLabel =
266+
buildClassLinkLabel(startVertex, endVertex, sourceUrl, targetUrl, classesToRemove, (int)
267+
classGraph.getEdgeWeight(edgeInfo.getEdge()));
268+
257269
classRelList.add(ClassRelationshipDTO.builder()
258270
.sourceClass(startVertex)
259271
.targetClass(endVertex)
260272
.sourceMarked(classesToRemove.contains(startVertex))
261273
.targetMarked(classesToRemove.contains(endVertex))
262274
.weight((int) classGraph.getEdgeWeight(edgeInfo.getEdge()))
263-
.renderedLabel(renderPlainClassEdge(edgeInfo.getEdge()))
275+
.renderedLabel(renderedLabel)
264276
.priority(edgeInfo.getPriority())
265277
.cycleCount(edgeInfo.getCycleCount())
266278
.effortRank(edgeInfo.getEffortRank())
@@ -300,16 +312,81 @@ public RefactorFirstReportDTO generateReportData(
300312
String startVertex = vertexes[0].trim();
301313
String endVertex = vertexes[1].trim();
302314

315+
// Extract package URLs from class relationships in the package edge
316+
String sourceUrl = "";
317+
String targetUrl = "";
318+
Set<DefaultWeightedEdge> classRelationshipsInPackageRelationship = codebaseGraphDTO
319+
.getClassRelationshipsInPackageRelationship()
320+
.get(edgeInfo.getEdge());
321+
if (classRelationshipsInPackageRelationship != null) {
322+
for (DefaultWeightedEdge classEdge : classRelationshipsInPackageRelationship) {
323+
String[] classVertexes = extractVertexes(classEdge);
324+
String classStart = classVertexes[0].trim();
325+
String classEnd = classVertexes[1].trim();
326+
327+
// Get source file path for each class vertex
328+
String classStartPath = codebaseGraphDTO
329+
.getClassToSourceFilePathMapping()
330+
.get(classStart);
331+
String classEndPath = codebaseGraphDTO
332+
.getClassToSourceFilePathMapping()
333+
.get(classEnd);
334+
335+
// Derive package directory from source file path
336+
if (classStartPath != null && !classStartPath.isBlank()) {
337+
int lastSlash = classStartPath.lastIndexOf('/');
338+
if (lastSlash != -1) {
339+
String packageDir = classStartPath.substring(0, lastSlash + 1);
340+
// Check if this class belongs to the source package
341+
if (isClassInPackage(classStart, startVertex, codebaseGraphDTO)) {
342+
sourceUrl = repoUrl + packageDir;
343+
}
344+
// Check if this class belongs to the target package
345+
if (isClassInPackage(classStart, endVertex, codebaseGraphDTO)) {
346+
targetUrl = repoUrl + packageDir;
347+
}
348+
}
349+
}
350+
351+
if (classEndPath != null && !classEndPath.isBlank()) {
352+
int lastSlash = classEndPath.lastIndexOf('/');
353+
if (lastSlash != -1) {
354+
String packageDir = classEndPath.substring(0, lastSlash + 1);
355+
// Check if this class belongs to the source package
356+
if (isClassInPackage(classEnd, startVertex, codebaseGraphDTO)) {
357+
sourceUrl = repoUrl + packageDir;
358+
}
359+
// Check if this class belongs to the target package
360+
if (isClassInPackage(classEnd, endVertex, codebaseGraphDTO)) {
361+
targetUrl = repoUrl + packageDir;
362+
}
363+
}
364+
}
365+
}
366+
}
367+
368+
// Fallback to package name conversion if no class edges found
369+
if (sourceUrl.isBlank()) {
370+
sourceUrl = repoUrl + startVertex.replace('.', '/');
371+
}
372+
if (targetUrl.isBlank()) {
373+
targetUrl = repoUrl + endVertex.replace('.', '/');
374+
}
375+
303376
List<String> breakClassRels =
304377
cells.length > 4 && !cells[4].isBlank() ? List.of(cells[4].split("<br>")) : List.of();
305378

379+
// Build rendered label with links to package directories
380+
String renderedLabel =
381+
buildPackageLinkLabel(cells[0], startVertex, endVertex, sourceUrl, targetUrl, packagesToRemove);
382+
306383
packageRelList.add(PackageRelationshipDTO.builder()
307384
.sourcePackage(startVertex)
308385
.targetPackage(endVertex)
309386
.sourceMarked(packagesToRemove.contains(startVertex))
310387
.targetMarked(packagesToRemove.contains(endVertex))
311388
.weight((int) packageGraph.getEdgeWeight(edgeInfo.getEdge()))
312-
.renderedLabel(cells[0])
389+
.renderedLabel(renderedLabel)
313390
.priority(edgeInfo.getPriority())
314391
.cycleCount(edgeInfo.getCycleCount())
315392
.effortRank(edgeInfo.getEffortRank())
@@ -358,7 +435,7 @@ public RefactorFirstReportDTO generateReportData(
358435
for (String vertex : largestCycle.getVertexSet()) {
359436
String className;
360437
if (classesToRemove.contains(vertex)) {
361-
className = hyperlinkClass(vertex, repoUrl, codebaseGraphDTO) + "<strong>*</strong>";
438+
className = hyperlinkClass(vertex, repoUrl, codebaseGraphDTO) + "*";
362439
} else {
363440
className = hyperlinkClass(vertex, repoUrl, codebaseGraphDTO);
364441
}
@@ -368,7 +445,7 @@ public RefactorFirstReportDTO generateReportData(
368445
if (edge.toString().startsWith("(" + vertex + " :")) {
369446
if (classRelationshipsToRemove.contains(edge)) {
370447
edges.append("<strong>");
371-
edges.append(renderClassEdge(edge) + "<strong>*</strong>");
448+
edges.append(renderClassEdge(edge) + "*");
372449
edges.append("</strong>");
373450
} else {
374451
edges.append(renderClassEdge(edge));
@@ -426,17 +503,6 @@ public RefactorFirstReportDTO generateReportData(
426503
}
427504
}
428505

429-
/** Renders a relationship label as plain text for Mustache's escaped interpolation. */
430-
private String renderPlainClassEdge(DefaultWeightedEdge edge) {
431-
String[] vertexes = extractVertexes(edge);
432-
String startVertex = vertexes[0].trim();
433-
String endVertex = vertexes[1].trim();
434-
String startMarker = classesToRemove.contains(startVertex) ? "*" : "";
435-
String endMarker = classesToRemove.contains(endVertex) ? "*" : "";
436-
return getClassName(startVertex) + startMarker + " → " + getClassName(endVertex) + endMarker + " : "
437-
+ (int) classGraph.getEdgeWeight(edge);
438-
}
439-
440506
/** Converts ranked instances of one disharmony type into chart and table data. */
441507
private DisharmonySectionDTO buildDisharmonySection(
442508
DisharmonySpec spec, boolean showDetails, List<RankedDisharmony> ranked, String repoUrl) {
@@ -449,13 +515,15 @@ private DisharmonySectionDTO buildDisharmonySection(
449515
String label = rd.getFileName() != null
450516
? rd.getFileName()
451517
: rd.getRawPriority().toString();
518+
String url = repoUrl + rd.getPath();
452519
bubbles.add(createBubble(
453520
rd.getFileName(),
454521
label,
455522
rd.getEffortRank(),
456523
rd.getChangePronenessRank(),
457524
rd.getPriority(),
458-
maxPriority));
525+
maxPriority,
526+
url));
459527
}
460528

461529
DisharmonyChartDTO chartDTO = DisharmonyChartDTO.builder()
@@ -615,7 +683,13 @@ private DisharmonySectionDTO buildDisharmonySection(
615683

616684
/** Creates a chart bubble whose size and color reflect the finding priority. */
617685
public ChartJsBubbleDTO createBubble(
618-
String id, String label, int effortRank, int changePronenessRank, int priority, int maxPriority) {
686+
String id,
687+
String label,
688+
int effortRank,
689+
int changePronenessRank,
690+
int priority,
691+
int maxPriority,
692+
String url) {
619693

620694
int minRadius = 6;
621695
int maxRadius = 24;
@@ -655,6 +729,7 @@ public ChartJsBubbleDTO createBubble(
655729
.changePronenessRank(changePronenessRank)
656730
.color(color)
657731
.borderColor(borderColor)
732+
.url(url)
658733
.build();
659734
}
660735

@@ -667,4 +742,143 @@ private static String graphIdentifier(String value) {
667742
}
668743
return "graph_" + sanitized + "_" + Integer.toUnsignedString(original.hashCode(), 36);
669744
}
745+
746+
/** Checks if a class belongs to a given package by checking the fully qualified class name. */
747+
private boolean isClassInPackage(String className, String packageName, CodebaseGraphDTO codebaseGraphDTO) {
748+
if (className == null || packageName == null) {
749+
return false;
750+
}
751+
// Check if the class name starts with the package name followed by a dot
752+
int lastDot = className.lastIndexOf('.');
753+
return lastDot > 0 && className.substring(0, lastDot).equals(packageName);
754+
}
755+
756+
/**
757+
* Builds a rendered label with HTML links to package directories.
758+
* Transforms "org.hjug.graphbuilder.metrics &#8594; org.hjug.graphbuilder<strong>*</strong> : 1"
759+
* into "<a href="...">org.hjug.graphbuilder.metrics</a> &#8594; <a href="...">org.hjug.graphbuilder</a>* : 1"
760+
*/
761+
private String buildPackageLinkLabel(
762+
String originalLabel,
763+
String startVertex,
764+
String endVertex,
765+
String sourceUrl,
766+
String targetUrl,
767+
Set<String> packagesToRemove) {
768+
if (originalLabel == null || originalLabel.isBlank()) {
769+
return originalLabel;
770+
}
771+
772+
// The original label format is: "startPackage &#8594; endPackage : weight"
773+
// We need to wrap the package names with links
774+
String arrow = " &#8594; ";
775+
String weightSeparator = " : ";
776+
777+
// Split the label into parts
778+
String[] parts = originalLabel.split(weightSeparator);
779+
if (parts.length < 2) {
780+
return originalLabel;
781+
}
782+
783+
String edgePart = parts[0];
784+
String weight = parts[1];
785+
786+
// Split the edge part by arrow
787+
String[] packages = edgePart.split(arrow, 2);
788+
if (packages.length < 2) {
789+
return originalLabel;
790+
}
791+
792+
String startPackage = packages[0].trim();
793+
String endPackage = packages[1].trim();
794+
795+
// Check if start package has the removal marker (escaped or unescaped)
796+
boolean startMarked =
797+
startPackage.contains("<strong>*</strong>") || startPackage.contains("&lt;strong&gt;*&lt;/strong&gt;");
798+
if (startMarked) {
799+
startPackage = startPackage.replace("<strong>*</strong>", "").replace("&lt;strong&gt;*&lt;/strong&gt;", "");
800+
}
801+
802+
// Check if end package has the removal marker (escaped or unescaped)
803+
boolean endMarked =
804+
endPackage.contains("<strong>*</strong>") || endPackage.contains("&lt;strong&gt;*&lt;/strong&gt;");
805+
if (endMarked) {
806+
endPackage = endPackage.replace("<strong>*</strong>", "").replace("&lt;strong&gt;*&lt;/strong&gt;", "");
807+
}
808+
809+
// Build the new label with links
810+
StringBuilder newLabel = new StringBuilder();
811+
newLabel.append("<a href=\"")
812+
.append(escapeHtmlAttribute(sourceUrl))
813+
.append("\" target=\"_blank\">")
814+
.append(escapeHtmlLabel(startPackage))
815+
.append("</a>");
816+
if (startMarked) {
817+
newLabel.append("*");
818+
}
819+
newLabel.append(arrow);
820+
newLabel.append("<a href=\"")
821+
.append(escapeHtmlAttribute(targetUrl))
822+
.append("\" target=\"_blank\">")
823+
.append(escapeHtmlLabel(endPackage))
824+
.append("</a>");
825+
if (endMarked) {
826+
newLabel.append("*");
827+
}
828+
newLabel.append(weightSeparator).append(weight);
829+
830+
return newLabel.toString();
831+
}
832+
833+
/**
834+
* Builds a rendered label with HTML links to class files.
835+
* Transforms "GraphMetricsCollector → DependencyCollector : 1"
836+
* into "<a href="...">GraphMetricsCollector</a> → <a href="...">DependencyCollector</a>* : 1"
837+
*/
838+
private String buildClassLinkLabel(
839+
String startVertex,
840+
String endVertex,
841+
String sourceUrl,
842+
String targetUrl,
843+
Set<String> classesToRemove,
844+
int weight) {
845+
// Extract simple class names for display
846+
String startClass = getClassName(startVertex);
847+
String endClass = getClassName(endVertex);
848+
849+
// Check if classes are marked for removal
850+
boolean startMarked = classesToRemove.contains(startVertex);
851+
boolean endMarked = classesToRemove.contains(endVertex);
852+
853+
// Build the new label with links
854+
StringBuilder newLabel = new StringBuilder();
855+
if (sourceUrl != null && !sourceUrl.isBlank()) {
856+
newLabel.append("<a href=\"")
857+
.append(escapeHtmlAttribute(sourceUrl))
858+
.append("\" target=\"_blank\">")
859+
.append(escapeHtmlLabel(startClass))
860+
.append("</a>");
861+
} else {
862+
newLabel.append(escapeHtmlLabel(startClass));
863+
}
864+
if (startMarked) {
865+
newLabel.append("*");
866+
}
867+
newLabel.append(" &#8594; ");
868+
if (targetUrl != null && !targetUrl.isBlank()) {
869+
newLabel.append("<a href=\"")
870+
.append(escapeHtmlAttribute(targetUrl))
871+
.append("\" target=\"_blank\">")
872+
.append(escapeHtmlLabel(endClass))
873+
.append("</a>");
874+
} else {
875+
newLabel.append(escapeHtmlLabel(endClass));
876+
}
877+
if (endMarked) {
878+
newLabel.append("*");
879+
}
880+
newLabel.append(" : ").append(weight);
881+
882+
return newLabel.toString();
883+
}
670884
}

‎report/src/main/java/org/hjug/refactorfirst/report/model/ChartJsBubbleDTO.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ public class ChartJsBubbleDTO {
2020
private int changePronenessRank;
2121
private String color;
2222
private String borderColor;
23+
private String url;
2324
}

‎report/src/main/java/org/hjug/refactorfirst/report/model/ClassRelationshipDTO.java‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
public class ClassRelationshipDTO {
1313
private String sourceClass;
1414
private String targetClass;
15-
private String sourceUrl;
16-
private String targetUrl;
1715
private boolean sourceMarked;
1816
private boolean targetMarked;
1917
private int weight;

0 commit comments

Comments
 (0)