    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}