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 addDependencies(className, allClasses, nodeMap); 063 } 064 065 final List<Node> nodes = new ArrayList<>(nodeMap.values()); 066 Collections.sort(nodes, Comparator.comparing(n -> n.id)); 067 068 for (Node node : nodes) 069 { 070 dotFile.append(getNodeDefinition(node)); 071 } 072 073 dotFile.append("\n"); 074 075 for (Node node : nodes) 076 { 077 for (Dependency dependency : node.dependencies) 078 { 079 dotFile.append(getNodeDependencyDefinition(node, dependency.className, dependency.type)); 080 } 081 } 082 083 084 dotFile.append("\n"); 085 dotFile.append("}"); 086 087 return dotFile.toString(); 088 } 089 090 private String getNodeDefinition(Node node) 091 { 092 return String.format("\t%s [label=\"%s\", tooltip=\"%s\"];\n", node.id, node.label, node.className); 093 } 094 095 private String getNodeDependencyDefinition(Node node, String dependency, DependencyType dependencyType) 096 { 097 String extraDefinition; 098 switch (dependencyType) 099 { 100 case INJECT_PAGE: extraDefinition = " [style=dashed]"; break; 101 case SUPERCLASS: extraDefinition = " [arrowhead=empty]"; break; 102 default: extraDefinition = ""; 103 } 104 return String.format("\t%s -> %s%s\n", node.id, escapeNodeId(getNodeLabel(dependency)), extraDefinition); 105 } 106 107 private String getNodeLabel(String className) 108 { 109 final String logicalName = componentClassResolver.getLogicalName(className); 110 return getNodeLabel(className, logicalName, false); 111 } 112 113 private static String getNodeLabel(String className, final String logicalName, boolean beautify) { 114 return logicalName != null ? beautifyLogicalName(logicalName) : (beautify ? beautifyClassName(className) : className); 115 } 116 117 private static String beautifyLogicalName(String logicalName) { 118 return logicalName.startsWith("core/") ? logicalName.replace("core/", "") : logicalName; 119 } 120 121 private static String beautifyClassName(String className) 122 { 123 String name = className.substring(className.lastIndexOf('.') + 1); 124 if (className.contains(".base.")) 125 { 126 name += " (base class)"; 127 } 128 else if (className.contains(".mixins.")) 129 { 130 name += " (mixin)"; 131 } 132 return name; 133 } 134 135 private static String escapeNodeId(String label) { 136 return label.replace('.', '_').replace('/', '_'); 137 } 138 139 private void addDependencies(String className, Set<String> allClasses, Map<String, Node> nodeMap) 140 { 141 if (!allClasses.contains(className)) 142 { 143 createNode(className, nodeMap); 144 allClasses.add(className); 145 for (DependencyType type : DependencyType.values()) 146 { 147 for (String dependency : componentDependencyRegistry.getDependencies(className, type)) 148 { 149 addDependencies(dependency, allClasses, nodeMap); 150 } 151 } 152 } 153 } 154 155 private void createNode(String className, Map<String, Node> nodeMap) 156 { 157 if (!nodeMap.containsKey(className)) 158 { 159 Collection<Dependency> deps = new HashSet<>(); 160 for (DependencyType type : DependencyType.values()) 161 { 162 final Set<String> dependencies = componentDependencyRegistry.getDependencies(className, type); 163 for (String dependency : dependencies) 164 { 165 deps.add(new Dependency(dependency, type)); 166 } 167 } 168 nodeMap.put(className, new Node(getNodeLabel(className), className, deps)); 169 } 170 } 171 172 private static final class Dependency 173 { 174 final private String className; 175 final private DependencyType type; 176 public Dependency(String className, DependencyType type) 177 { 178 super(); 179 this.className = className; 180 this.type = type; 181 } 182 @Override 183 public String toString() 184 { 185 return "Dependency [className=" + className + ", type=" + type + "]"; 186 } 187 } 188 189 private static final class Node 190 { 191 192 final private String id; 193 final private String className; 194 final private String label; 195 final private Set<Dependency> dependencies = new HashSet<>(); 196 197 public Node(String logicalName, String className, Collection<Dependency> dependencies) 198 { 199 super(); 200 this.label = getNodeLabel(className, logicalName, true); 201 this.id = escapeNodeId(getNodeLabel(className, logicalName, false)); 202 this.className = className; 203 this.dependencies.addAll(dependencies); 204 } 205 206 @Override 207 public String toString() 208 { 209 return "Node [id=" + id + ", className=" + className + ", dependencies=" + dependencies + ", label=" + label + "]"; 210 } 211 212 } 213 214}