    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}