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}