001// Copyright 2023 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014package org.apache.tapestry5.internal.services; 015 016import java.util.ArrayList; 017import java.util.Collection; 018import java.util.Collections; 019import java.util.Comparator; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025 026import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; 027import org.apache.tapestry5.services.ComponentClassResolver; 028 029public class ComponentDependencyGraphvizGeneratorImpl implements ComponentDependencyGraphvizGenerator { 030 031 final private ComponentClassResolver componentClassResolver; 032 033 final private ComponentDependencyRegistry componentDependencyRegistry; 034 035 public ComponentDependencyGraphvizGeneratorImpl(ComponentDependencyRegistry componentDependencyRegistry, 036 ComponentClassResolver componentClassResolver) 037 { 038 super(); 039 this.componentDependencyRegistry = componentDependencyRegistry; 040 this.componentClassResolver = componentClassResolver; 041 } 042 043 @Override 044 public String generate(String... classNames) 045 { 046 047 final StringBuilder dotFile = new StringBuilder("digraph {\n\n"); 048 049 dotFile.append("\trankdir=LR;\n"); 050 dotFile.append("\tfontname=\"Helvetica,Arial,sans-serif\";\n"); 051 dotFile.append("\tsplines=ortho;\n\n"); 052 dotFile.append("\tnode [fontname=\"Helvetica,Arial,sans-serif\",fontsize=\"10pt\"];\n"); 053 dotFile.append("\tnode [shape=rect];\n\n"); 054 055 final Set<String> allClasses = new HashSet<>(); 056// final Set<String> dependenciesAlreadyOutput = new HashSet<>(); 057 058 final Map<String, Node> nodeMap = new HashMap<>(); 059 for (String className : classNames) 060 { 061 createNode(className, nodeMap); 062 for (DependencyType dependencyType : DependencyType.values()) 063 { 064 addDependencies(className, allClasses, dependencyType, nodeMap); 065 } 066 067 068 } 069 070 final List<Node> nodes = new ArrayList<>(nodeMap.values()); 071 Collections.sort(nodes, Comparator.comparing(n -> n.id)); 072 073 for (Node node : nodes) 074 { 075 dotFile.append(getNodeDefinition(node)); 076 } 077 078 dotFile.append("\n"); 079 080 for (Node node : nodes) 081 { 082 for (Dependency dependency : node.dependencies) 083 { 084 dotFile.append(getNodeDependencyDefinition(node, dependency.className, dependency.type)); 085 } 086 } 087 088 089 dotFile.append("\n"); 090 dotFile.append("}"); 091 092 return dotFile.toString(); 093 } 094 095 private String getNodeDefinition(Node node) 096 { 097 return String.format("\t%s [label=\"%s\", tooltip=\"%s\"];\n", node.id, node.label, node.className); 098 } 099 100 private String getNodeDependencyDefinition(Node node, String dependency, DependencyType dependencyType) 101 { 102 String extraDefinition; 103 switch (dependencyType) 104 { 105 case INJECT_PAGE: extraDefinition = " [style=dashed]"; break; 106 case SUPERCLASS: extraDefinition = " [arrowhead=empty]"; break; 107 default: extraDefinition = ""; 108 } 109 return String.format("\t%s -> %s%s\n", node.id, escapeNodeId(getNodeLabel(dependency)), extraDefinition); 110 } 111 112 private String getNodeLabel(String className) 113 { 114 final String logicalName = componentClassResolver.getLogicalName(className); 115 return getNodeLabel(className, logicalName, false); 116 } 117 118 private static String getNodeLabel(String className, final String logicalName, boolean beautify) { 119 return logicalName != null ? beautifyLogicalName(logicalName) : (beautify ? beautifyClassName(className) : className); 120 } 121 122 private static String beautifyLogicalName(String logicalName) { 123 return logicalName.startsWith("core/") ? logicalName.replace("core/", "") : logicalName; 124 } 125 126 private static String beautifyClassName(String className) 127 { 128 String name = className.substring(className.lastIndexOf('.') + 1); 129 if (className.contains(".base.")) 130 { 131 name += " (base class)"; 132 } 133 else if (className.contains(".mixins.")) 134 { 135 name += " (mixin)"; 136 } 137 return name; 138 } 139 140 private static String escapeNodeId(String label) { 141 return label.replace('.', '_').replace('/', '_'); 142 } 143 144 private void addDependencies(String className, Set<String> allClasses, DependencyType type, Map<String, Node> nodeMap) 145 { 146 if (!allClasses.contains(className)) 147 { 148 createNode(className, nodeMap); 149 for (String dependency : componentDependencyRegistry.getDependencies(className, type)) 150 { 151 addDependencies(dependency, allClasses, type, nodeMap); 152 } 153 allClasses.add(className); 154 } 155 } 156 157 private void createNode(String className, Map<String, Node> nodeMap) 158 { 159 if (!nodeMap.containsKey(className)) 160 { 161 Collection<Dependency> deps = new HashSet<>(); 162 for (DependencyType type : DependencyType.values()) 163 { 164 final Set<String> dependencies = componentDependencyRegistry.getDependencies(className, type); 165 for (String dependency : dependencies) 166 { 167 deps.add(new Dependency(dependency, type)); 168 } 169 } 170 nodeMap.put(className, new Node(getNodeLabel(className), className, deps)); 171 } 172 } 173 174 private static final class Dependency 175 { 176 final private String className; 177 final private DependencyType type; 178 public Dependency(String className, DependencyType type) 179 { 180 super(); 181 this.className = className; 182 this.type = type; 183 } 184 @Override 185 public String toString() 186 { 187 return "Dependency [className=" + className + ", type=" + type + "]"; 188 } 189 } 190 191 private static final class Node { 192 193 final private String id; 194 final private String className; 195 final private String label; 196 final private Set<Dependency> dependencies = new HashSet<>(); 197 198 public Node(String logicalName, String className, Collection<Dependency> dependencies) 199 { 200 super(); 201 this.label = getNodeLabel(className, logicalName, true); 202 this.id = escapeNodeId(getNodeLabel(className, logicalName, false)); 203 this.className = className; 204 this.dependencies.addAll(dependencies); 205 } 206 207 } 208}