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}