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}