@@ -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 → org.hjug.graphbuilder<strong>*</strong> : 1"
759+ * into "<a href="...">org.hjug.graphbuilder.metrics</a> → <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 → endPackage : weight"
773+ // We need to wrap the package names with links
774+ String arrow = " → " ;
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 ("<strong>*</strong>" );
798+ if (startMarked ) {
799+ startPackage = startPackage .replace ("<strong>*</strong>" , "" ).replace ("<strong>*</strong>" , "" );
800+ }
801+
802+ // Check if end package has the removal marker (escaped or unescaped)
803+ boolean endMarked =
804+ endPackage .contains ("<strong>*</strong>" ) || endPackage .contains ("<strong>*</strong>" );
805+ if (endMarked ) {
806+ endPackage = endPackage .replace ("<strong>*</strong>" , "" ).replace ("<strong>*</strong>" , "" );
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 (" → " );
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}
0 commit comments