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.Collection; 017import java.util.HashSet; 018import java.util.Set; 019 020import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; 021import org.apache.tapestry5.services.ComponentClassResolver; 022 023public class ComponentDependencyGraphvizGeneratorImpl implements ComponentDependencyGraphvizGenerator { 024 025 final private ComponentClassResolver componentClassResolver; 026 027 final private ComponentDependencyRegistry componentDependencyRegistry; 028 029 030 public ComponentDependencyGraphvizGeneratorImpl(ComponentDependencyRegistry componentDependencyRegistry, 031 ComponentClassResolver componentClassResolver) 032 { 033 super(); 034 this.componentDependencyRegistry = componentDependencyRegistry; 035 this.componentClassResolver = componentClassResolver; 036 } 037 038 @Override 039 public String generate(String... classNames) 040 { 041 042 final StringBuilder dotFile = new StringBuilder("digraph {\n\n"); 043 044 dotFile.append("\trankdir=LR;\n"); 045 dotFile.append("\tfontname=\"Helvetica,Arial,sans-serif\";\n"); 046 dotFile.append("\tsplines=ortho;\n\n"); 047 dotFile.append("\tnode [fontname=\"Helvetica,Arial,sans-serif\",fontsize=\"10pt\"];\n"); 048 dotFile.append("\tnode [shape=rect];\n\n"); 049 050 final Set<String> allClasses = new HashSet<>(); 051 052 for (String className : classNames) 053 { 054 final Node node = createNode(componentClassResolver.getLogicalName(className), className); 055 dotFile.append(getNodeDefinition(node)); 056 for (DependencyType dependencyType : DependencyType.values()) 057 { 058 addDependencies(className, allClasses, dependencyType); 059 } 060 061 final StringBuilder dependencySection = new StringBuilder(); 062 063 for (Dependency dependency : node.dependencies) 064 { 065 dependencySection.append(getNodeDependencyDefinition(node, dependency.className, dependency.type)); 066 } 067 068 dotFile.append("\n"); 069 dotFile.append(dependencySection); 070 dotFile.append("\n"); 071 072 } 073 074 075 dotFile.append("}"); 076 077 return dotFile.toString(); 078 } 079 080 private String getNodeDefinition(Node node) 081 { 082 return String.format("\t%s [label=\"%s\", tooltip=\"%s\"];\n", node.id, node.label, node.className); 083 } 084 085 private String getNodeDependencyDefinition(Node node, String dependency, DependencyType dependencyType) 086 { 087 String extraDefinition; 088 switch (dependencyType) 089 { 090 case INJECT_PAGE: extraDefinition = " [style=dashed]"; break; 091 case SUPERCLASS: extraDefinition = " [arrowhead=empty]"; break; 092 default: extraDefinition = ""; 093 } 094 return String.format("\t%s -> %s%s\n", node.id, escapeNodeId(getNodeLabel(dependency)), extraDefinition); 095 } 096 097 private String getNodeLabel(String className) 098 { 099 final String logicalName = componentClassResolver.getLogicalName(className); 100 return getNodeLabel(className, logicalName, false); 101 } 102 103 private static String getNodeLabel(String className, final String logicalName, boolean beautify) { 104 return logicalName != null ? beautifyLogicalName(logicalName) : (beautify ? beautifyClassName(className) : className); 105 } 106 107 private static String beautifyLogicalName(String logicalName) { 108 return logicalName.startsWith("core/") ? logicalName.replace("core/", "") : logicalName; 109 } 110 111 private static String beautifyClassName(String className) 112 { 113 String name = className.substring(className.lastIndexOf('.') + 1); 114 if (className.contains(".base.")) 115 { 116 name += " (base class)"; 117 } 118 else if (className.contains(".mixins.")) 119 { 120 name += " (mixin)"; 121 } 122 return name; 123 } 124 125 private static String escapeNodeId(String label) { 126 return label.replace('.', '_').replace('/', '_'); 127 } 128 129 private void addDependencies(String className, Set<String> allClasses, DependencyType type) 130 { 131 if (!allClasses.contains(className)) 132 { 133 allClasses.add(className); 134 for (String dependency : componentDependencyRegistry.getDependencies(className, type)) 135 { 136 addDependencies(dependency, allClasses, type); 137 } 138 } 139 } 140 141 private Node createNode(String logicalName, String className) 142 { 143 Collection<Dependency> deps = new HashSet<>(); 144 for (DependencyType type : DependencyType.values()) 145 { 146 final Set<String> dependencies = componentDependencyRegistry.getDependencies(className, type); 147 for (String dependency : dependencies) 148 { 149 deps.add(new Dependency(dependency, type)); 150 } 151 } 152 return new Node(logicalName, className, deps); 153 } 154 155 private static final class Dependency 156 { 157 final private String className; 158 final private DependencyType type; 159 public Dependency(String className, DependencyType type) 160 { 161 super(); 162 this.className = className; 163 this.type = type; 164 } 165 } 166 167 private static final class Node { 168 169 final private String id; 170 final private String className; 171 final private String label; 172 final private Set<Dependency> dependencies = new HashSet<>(); 173 174 public Node(String logicalName, String className, Collection<Dependency> dependencies) 175 { 176 super(); 177 this.label = getNodeLabel(className, logicalName, true); 178 this.id = escapeNodeId(getNodeLabel(className, logicalName, false)); 179 this.className = className; 180 this.dependencies.addAll(dependencies); 181 } 182 183 } 184}