All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hazelcast.shaded.io.github.classgraph.GraphvizDotfileGenerator Maven / Gradle / Ivy

The newest version!
/*
 * This file is part of ClassGraph.
 *
 * Author: Luke Hutchison
 *
 * Hosted at: https://github.com/classgraph/classgraph
 *
 * --
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Luke Hutchison
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without
 * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
 * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
 * OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.hazelcast.shaded.io.github.classgraph;

import java.util.BitSet;
import java.util.HashSet;
import java.util.Set;

import com.hazelcast.shaded.nonapi.io.github.classgraph.scanspec.ScanSpec;
import com.hazelcast.shaded.nonapi.io.github.classgraph.utils.CollectionUtils;

/** Builds a class graph visualization in Graphviz .dot file format. */
final class GraphvizDotfileGenerator {
    /** The color for standard classes. */
    private static final String STANDARD_CLASS_COLOR = "fff2b6";

    /** The color for interfaces. */
    private static final String INTERFACE_COLOR = "b6e7ff";

    /** The color for annotations. */
    private static final String ANNOTATION_COLOR = "f3c9ff";

    /** The wrap width for method parameters. */
    private static final int PARAM_WRAP_WIDTH = 40;

    /** Which characters are Unicode whitespace. */
    private static final BitSet IS_UNICODE_WHITESPACE = new BitSet(1 << 16);

    /**
     * Constructor.
     */
    private GraphvizDotfileGenerator() {
        // Cannot be constructed
    }

    static {
        // Valid unicode whitespace chars, see:
        // http://stackoverflow.com/questions/4731055/whitespace-matching-regex-java
        // Also see (for \n and \r -- a real example of Java stupidity):
        // https://stackoverflow.com/a/3866219/3950982
        final String wsChars = "\u0020" // SPACE
                + "\u0009" // CHARACTER TABULATION
                + "\n" // LINE FEED (LF)
                + "\u000B" // LINE TABULATION
                + "\u000C" // FORM FEED (FF)
                + "\r" // CARRIAGE RETURN (CR)
                + "\u0085" // NEXT LINE (NEL) 
                + "\u00A0" // NO-BREAK SPACE
                + "\u1680" // OGHAM SPACE MARK
                + "\u180E" // MONGOLIAN VOWEL SEPARATOR
                + "\u2000" // EN QUAD 
                + "\u2001" // EM QUAD 
                + "\u2002" // EN SPACE
                + "\u2003" // EM SPACE
                + "\u2004" // THREE-PER-EM SPACE
                + "\u2005" // FOUR-PER-EM SPACE
                + "\u2006" // SIX-PER-EM SPACE
                + "\u2007" // FIGURE SPACE
                + "\u2008" // PUNCTUATION SPACE
                + "\u2009" // THIN SPACE
                + "\u200A" // HAIR SPACE
                + "\u2028" // LINE SEPARATOR
                + "\u2029" // PARAGRAPH SEPARATOR
                + "\u202F" // NARROW NO-BREAK SPACE
                + "\u205F" // MEDIUM MATHEMATICAL SPACE
                + "\u3000"; // IDEOGRAPHIC SPACE
        for (int i = 0; i < wsChars.length(); i++) {
            IS_UNICODE_WHITESPACE.set(wsChars.charAt(i));
        }
    }

    /**
     * Checks if a character is Unicode whitespace.
     *
     * @param c
     *            the character
     * @return true if the character is Unicode whitespace
     */
    private static boolean isUnicodeWhitespace(final char c) {
        return IS_UNICODE_WHITESPACE.get(c);
    }

    /**
     * Encode HTML-unsafe characters as HTML entities.
     *
     * @param unsafeStr
     *            The string to escape to make HTML-safe.
     * @param turnNewlineIntoBreak
     *            If true, turn '\n' into a break element in the output.
     * @param buf
     *            the buf
     */
    private static void htmlEncode(final CharSequence unsafeStr, final boolean turnNewlineIntoBreak,
            final StringBuilder buf) {
        for (int i = 0, n = unsafeStr.length(); i < n; i++) {
            final char c = unsafeStr.charAt(i);
            switch (c) {
            case '&':
                buf.append("&");
                break;
            case '<':
                buf.append("<");
                break;
            case '>':
                buf.append(">");
                break;
            case '"':
                buf.append(""");
                break;
            case '\'':
                buf.append("'"); // See http://goo.gl/FzoP6m
                break;
            case '\\':
                buf.append("&lsol;");
                break;
            case '/':
                buf.append("/"); // '/' can be a dangerous char if attr values are not quoted
                break;
            // Encode a few common characters that like to get screwed up in some charset/browser variants
            case '—':
                buf.append("—");
                break;
            case '–':
                buf.append("–");
                break;
            case '“':
                buf.append("“");
                break;
            case '”':
                buf.append("”");
                break;
            case '‘':
                buf.append("‘");
                break;
            case '’':
                buf.append("’");
                break;
            case '«':
                buf.append("«");
                break;
            case '»':
                buf.append("»");
                break;
            case '£':
                buf.append("£");
                break;
            case '©':
                buf.append("©");
                break;
            case '®':
                buf.append("®");
                break;
            case (char) 0x00A0:
                buf.append(" ");
                break;
            case '\n':
                if (turnNewlineIntoBreak) {
                    buf.append("
"); } else { buf.append(' '); // Newlines function as whitespace in HTML text } break; default: if (c <= 32 || isUnicodeWhitespace(c)) { buf.append(' '); } else { buf.append(c); } break; } } } /** * Encode HTML-unsafe characters as HTML entities. * * @param unsafeStr * The string to escape to make HTML-safe. * @param buf * the buf */ private static void htmlEncode(final CharSequence unsafeStr, final StringBuilder buf) { htmlEncode(unsafeStr, /* turnNewlineIntoBreak = */ false, buf); } /** * Produce HTML label for class node. * * @param ci * the class info * @param shape * the shape to use * @param boxBgColor * the box background color * @param showFields * whether to show fields * @param showMethods * whether to show methods * @param useSimpleNames * whether to use simple names for classes in type signatures * @param scanSpec * the scan spec * @param buf * the buf */ private static void labelClassNodeHTML(final ClassInfo ci, final String shape, final String boxBgColor, final boolean showFields, final boolean showMethods, final boolean useSimpleNames, final ScanSpec scanSpec, final StringBuilder buf) { buf.append("[shape=").append(shape).append(",style=filled,fillcolor=\"#").append(boxBgColor) .append("\",label="); buf.append('<'); buf.append(""); // Class modifiers buf.append(""); if (ci.getName().contains(".")) { buf.append(""); } // Class name buf.append(""); // Create a color that matches the box background color, but is darker final float darkness = 0.8f; final int r = (int) (Integer.parseInt(boxBgColor.substring(0, 2), 16) * darkness); final int g = (int) (Integer.parseInt(boxBgColor.substring(2, 4), 16) * darkness); final int b = (int) (Integer.parseInt(boxBgColor.substring(4, 6), 16) * darkness); final String darkerColor = String.format("#%s%s%s%s%s%s", Integer.toString(r >> 4, 16), Integer.toString(r & 0xf, 16), Integer.toString(g >> 4, 16), Integer.toString(g & 0xf, 16), Integer.toString(b >> 4, 16), Integer.toString(b & 0xf, 16)); // Class annotations final AnnotationInfoList annotationInfo = ci.annotationInfo; if (annotationInfo != null && !annotationInfo.isEmpty()) { buf.append(""); final AnnotationInfoList annotationInfoSorted = new AnnotationInfoList(annotationInfo); CollectionUtils.sortIfNotEmpty(annotationInfoSorted); for (final AnnotationInfo ai : annotationInfoSorted) { final String annotationName = ai.getName(); if (!annotationName.startsWith("java.lang.annotation.")) { buf.append(""); buf.append(""); } } } // Fields final FieldInfoList fieldInfo = ci.fieldInfo; if (showFields && fieldInfo != null && !fieldInfo.isEmpty()) { final FieldInfoList fieldInfoSorted = new FieldInfoList(fieldInfo); CollectionUtils.sortIfNotEmpty(fieldInfoSorted); for (int i = fieldInfoSorted.size() - 1; i >= 0; --i) { // Remove serialVersionUID field if (fieldInfoSorted.get(i).getName().equals("serialVersionUID")) { fieldInfoSorted.remove(i); } } if (!fieldInfoSorted.isEmpty()) { buf.append(""); buf.append(""); } } // Methods final MethodInfoList methodInfo = ci.methodInfo; if (showMethods && methodInfo != null) { final MethodInfoList methodInfoSorted = new MethodInfoList(methodInfo); CollectionUtils.sortIfNotEmpty(methodInfoSorted); for (int i = methodInfoSorted.size() - 1; i >= 0; --i) { // Don't list static initializer blocks or methods of Object final MethodInfo mi = methodInfoSorted.get(i); final String name = mi.getName(); final int numParam = mi.getParameterInfo().length; if (name.equals("") || name.equals("hashCode") && numParam == 0 || name.equals("toString") && numParam == 0 || name.equals("equals") && numParam == 1 && mi.getTypeDescriptor().toString().equals("boolean (java.lang.Object)")) { methodInfoSorted.remove(i); } } if (!methodInfoSorted.isEmpty()) { buf.append(""); } } buf.append("
").append(ci.getModifiersStr()).append(' ') .append(ci.isEnum() ? "enum" : ci.isAnnotation() ? "@interface" : ci.isInterface() ? "interface" : "class") .append("
"); htmlEncode(ci.getPackageName() + ".", buf); buf.append("
"); htmlEncode(ci.getSimpleName(), buf); buf.append("
ANNOTATIONS
"); htmlEncode(ai.toString(), buf); buf.append("
") .append(scanSpec.ignoreFieldVisibility ? "" : "PUBLIC ") .append("FIELDS
"); buf.append(""); for (final FieldInfo fi : fieldInfoSorted) { buf.append(""); buf.append(""); // Field name buf.append(""); } buf.append("
"); // Field Annotations final AnnotationInfoList fieldAnnotationInfo = fi.annotationInfo; if (fieldAnnotationInfo != null) { for (final AnnotationInfo ai : fieldAnnotationInfo) { if (buf.charAt(buf.length() - 1) != ' ') { buf.append(' '); } htmlEncode(ai.toString(), buf); } } // Field modifiers if (scanSpec.ignoreFieldVisibility) { if (buf.charAt(buf.length() - 1) != ' ') { buf.append(' '); } buf.append(fi.getModifiersStr()); } // Field type if (buf.charAt(buf.length() - 1) != ' ') { buf.append(' '); } final TypeSignature typeSig = fi.getTypeSignatureOrTypeDescriptor(); htmlEncode(useSimpleNames ? typeSig.toStringWithSimpleNames() : typeSig.toString(), buf); buf.append(""); final String fieldName = fi.getName(); htmlEncode(fieldName, buf); buf.append("
"); buf.append("
"); buf.append(""); buf.append(""); for (final MethodInfo mi : methodInfoSorted) { buf.append(""); // Method annotations // TODO: wrap this cell if the contents get too long buf.append(""); // Method name buf.append(""); // Method parameters buf.append("" + ""); } buf.append("
") .append(scanSpec.ignoreMethodVisibility ? "" : "PUBLIC ") .append("METHODS
"); final AnnotationInfoList methodAnnotationInfo = mi.annotationInfo; if (methodAnnotationInfo != null) { for (final AnnotationInfo ai : methodAnnotationInfo) { if (buf.charAt(buf.length() - 1) != ' ') { buf.append(' '); } htmlEncode(ai.toString(), buf); } } // Method modifiers if (scanSpec.ignoreMethodVisibility) { if (buf.charAt(buf.length() - 1) != ' ') { buf.append(' '); } buf.append(mi.getModifiersStr()); } // Method return type if (buf.charAt(buf.length() - 1) != ' ') { buf.append(' '); } if (!mi.getName().equals("")) { // Don't list return type for constructors final TypeSignature resultTypeSig = mi.getTypeSignatureOrTypeDescriptor().getResultType(); htmlEncode( useSimpleNames ? resultTypeSig.toStringWithSimpleNames() : resultTypeSig.toString(), buf); } else { buf.append("<constructor>"); } buf.append(""); buf.append(""); if (mi.getName().equals("")) { // Show class name for constructors htmlEncode(ci.getSimpleName(), buf); } else { htmlEncode(mi.getName(), buf); } buf.append(" "); buf.append(""); buf.append('('); final MethodParameterInfo[] paramInfo = mi.getParameterInfo(); if (paramInfo.length != 0) { for (int i = 0, wrapPos = 0; i < paramInfo.length; i++) { if (i > 0) { buf.append(", "); wrapPos += 2; } if (wrapPos > PARAM_WRAP_WIDTH) { buf.append("
"); wrapPos = 0; } // Param annotation final AnnotationInfo[] paramAnnotationInfo = paramInfo[i].annotationInfo; if (paramAnnotationInfo != null) { for (final AnnotationInfo ai : paramAnnotationInfo) { final String ais = ai.toString(); if (!ais.isEmpty()) { if (buf.charAt(buf.length() - 1) != ' ') { buf.append(' '); } htmlEncode(ais, buf); wrapPos += 1 + ais.length(); if (wrapPos > PARAM_WRAP_WIDTH) { buf.append("
"); wrapPos = 0; } } } } // Param type final TypeSignature paramTypeSig = paramInfo[i].getTypeSignatureOrTypeDescriptor(); final String paramTypeStr = useSimpleNames ? paramTypeSig.toStringWithSimpleNames() : paramTypeSig.toString(); htmlEncode(paramTypeStr, buf); wrapPos += paramTypeStr.length(); // Param name final String paramName = paramInfo[i].getName(); if (paramName != null) { buf.append(" "); htmlEncode(paramName, buf); wrapPos += 1 + paramName.length(); buf.append(""); } } } buf.append(')'); buf.append("
"); buf.append("
"); buf.append(">]"); } /** * Generates a .dot file which can be fed into GraphViz for layout and visualization of the class graph. The * sizeX and sizeY parameters are the image output size to use (in inches) when GraphViz is asked to render the * .dot file. * * @param classInfoList * the class info list * @param sizeX * the size X * @param sizeY * the size Y * @param showFields * whether to show fields * @param showFieldTypeDependencyEdges * whether to show field type dependency edges * @param showMethods * whether to show methods * @param showMethodTypeDependencyEdges * whether to show method type dependency edges * @param showAnnotations * whether to show annotations * @param useSimpleNames * whether to use simple names for classes * @param scanSpec * the scan spec * @return the string */ static String generateGraphVizDotFile(final ClassInfoList classInfoList, final float sizeX, final float sizeY, final boolean showFields, final boolean showFieldTypeDependencyEdges, final boolean showMethods, final boolean showMethodTypeDependencyEdges, final boolean showAnnotations, final boolean useSimpleNames, final ScanSpec scanSpec) { final StringBuilder buf = new StringBuilder(1024 * 1024); buf.append("digraph {\n"); buf.append("size=\"").append(sizeX).append(',').append(sizeY).append("\";\n"); buf.append("layout=dot;\n"); buf.append("rankdir=\"BT\";\n"); buf.append("overlap=false;\n"); buf.append("splines=true;\n"); buf.append("pack=true;\n"); buf.append("graph [fontname = \"Courier, Regular\"]\n"); buf.append("node [fontname = \"Courier, Regular\"]\n"); buf.append("edge [fontname = \"Courier, Regular\"]\n"); final ClassInfoList standardClassNodes = classInfoList.getStandardClasses(); final ClassInfoList interfaceNodes = classInfoList.getInterfaces(); final ClassInfoList annotationNodes = classInfoList.getAnnotations(); for (final ClassInfo node : standardClassNodes) { buf.append('"').append(node.getName()).append('"'); labelClassNodeHTML(node, "box", STANDARD_CLASS_COLOR, showFields, showMethods, useSimpleNames, scanSpec, buf); buf.append(";\n"); } for (final ClassInfo node : interfaceNodes) { buf.append('"').append(node.getName()).append('"'); labelClassNodeHTML(node, "diamond", INTERFACE_COLOR, showFields, showMethods, useSimpleNames, scanSpec, buf); buf.append(";\n"); } for (final ClassInfo node : annotationNodes) { buf.append('"').append(node.getName()).append('"'); labelClassNodeHTML(node, "oval", ANNOTATION_COLOR, showFields, showMethods, useSimpleNames, scanSpec, buf); buf.append(";\n"); } final Set allVisibleNodes = new HashSet<>(); allVisibleNodes.addAll(standardClassNodes.getNames()); allVisibleNodes.addAll(interfaceNodes.getNames()); allVisibleNodes.addAll(annotationNodes.getNames()); buf.append('\n'); for (final ClassInfo classNode : standardClassNodes) { for (final ClassInfo directSuperclassNode : classNode.getSuperclasses().directOnly()) { if (directSuperclassNode != null && allVisibleNodes.contains(directSuperclassNode.getName()) && !directSuperclassNode.getName().equals("java.lang.Object")) { // class --> superclass buf.append(" \"").append(classNode.getName()).append("\" -> \"") .append(directSuperclassNode.getName()).append("\" [arrowsize=2.5]\n"); } } for (final ClassInfo implementedInterfaceNode : classNode.getInterfaces().directOnly()) { if (allVisibleNodes.contains(implementedInterfaceNode.getName())) { // class --<> implemented interface buf.append(" \"").append(classNode.getName()).append("\" -> \"") .append(implementedInterfaceNode.getName()) .append("\" [arrowhead=diamond, arrowsize=2.5]\n"); } } if (showFieldTypeDependencyEdges && classNode.fieldInfo != null) { for (final FieldInfo fi : classNode.fieldInfo) { for (final ClassInfo referencedFieldType : fi.findReferencedClassInfo(/* log = */ null)) { if (allVisibleNodes.contains(referencedFieldType.getName())) { // class --[ ] field type (open box) buf.append(" \"").append(referencedFieldType.getName()).append("\" -> \"") .append(classNode.getName()) .append("\" [arrowtail=obox, arrowsize=2.5, dir=back]\n"); } } } } if (showMethodTypeDependencyEdges && classNode.methodInfo != null) { for (final MethodInfo mi : classNode.methodInfo) { for (final ClassInfo referencedMethodType : mi.findReferencedClassInfo(/* log = */ null)) { if (allVisibleNodes.contains(referencedMethodType.getName())) { // class --[#] field type (open box) buf.append(" \"").append(referencedMethodType.getName()).append("\" -> \"") .append(classNode.getName()) .append("\" [arrowtail=box, arrowsize=2.5, dir=back]\n"); } } } } } for (final ClassInfo interfaceNode : interfaceNodes) { for (final ClassInfo superinterfaceNode : interfaceNode.getInterfaces().directOnly()) { if (allVisibleNodes.contains(superinterfaceNode.getName())) { // interface --<> superinterface buf.append(" \"").append(interfaceNode.getName()).append("\" -> \"") .append(superinterfaceNode.getName()).append("\" [arrowhead=diamond, arrowsize=2.5]\n"); } } } if (showAnnotations) { for (final ClassInfo annotationNode : annotationNodes) { for (final ClassInfo annotatedClassNode : annotationNode.getClassesWithAnnotationDirectOnly()) { if (allVisibleNodes.contains(annotatedClassNode.getName())) { // annotated class --o annotation buf.append(" \"").append(annotatedClassNode.getName()).append("\" -> \"") .append(annotationNode.getName()).append("\" [arrowhead=dot, arrowsize=2.5]\n"); } } for (final ClassInfo classWithMethodAnnotationNode : annotationNode .getClassesWithMethodAnnotationDirectOnly()) { if (allVisibleNodes.contains(classWithMethodAnnotationNode.getName())) { // class with method annotation --o method annotation buf.append(" \"").append(classWithMethodAnnotationNode.getName()).append("\" -> \"") .append(annotationNode.getName()).append("\" [arrowhead=odot, arrowsize=2.5]\n"); } } for (final ClassInfo classWithMethodAnnotationNode : annotationNode .getClassesWithFieldAnnotationDirectOnly()) { if (allVisibleNodes.contains(classWithMethodAnnotationNode.getName())) { // class with field annotation --o method annotation buf.append(" \"").append(classWithMethodAnnotationNode.getName()).append("\" -> \"") .append(annotationNode.getName()).append("\" [arrowhead=odot, arrowsize=2.5]\n"); } } } } buf.append('}'); return buf.toString(); } /** * Generate a .dot file which can be fed into GraphViz for layout and visualization of the class graph. The * returned graph shows inter-class dependencies only. The sizeX and sizeY parameters are the image output size * to use (in inches) when GraphViz is asked to render the .dot file. You must have called * {@link ClassGraph#enableInterClassDependencies()} before scanning to use this method. * * @param classInfoList * The list of nodes whose dependencies should be plotted in the graph. * @param sizeX * The GraphViz layout width in inches. * @param sizeY * The GraphViz layout width in inches. * @param includeExternalClasses * If true, include any dependency nodes in the graph that are not themselves in classInfoList. * @return the GraphViz file contents. * @throws IllegalArgumentException * if this {@link ClassInfoList} is empty or {@link ClassGraph#enableInterClassDependencies()} was * not called before scanning (since there would be nothing to graph). */ static String generateGraphVizDotFileFromInterClassDependencies(final ClassInfoList classInfoList, final float sizeX, final float sizeY, final boolean includeExternalClasses) { final StringBuilder buf = new StringBuilder(1024 * 1024); buf.append("digraph {\n"); buf.append("size=\"").append(sizeX).append(',').append(sizeY).append("\";\n"); buf.append("layout=dot;\n"); buf.append("rankdir=\"BT\";\n"); buf.append("overlap=false;\n"); buf.append("splines=true;\n"); buf.append("pack=true;\n"); buf.append("graph [fontname = \"Courier, Regular\"]\n"); buf.append("node [fontname = \"Courier, Regular\"]\n"); buf.append("edge [fontname = \"Courier, Regular\"]\n"); final Set allVisibleNodes = new HashSet<>(classInfoList); if (includeExternalClasses) { for (final ClassInfo ci : classInfoList) { allVisibleNodes.addAll(ci.getClassDependencies()); } } for (final ClassInfo ci : allVisibleNodes) { buf.append('"').append(ci.getName()).append('"'); buf.append("[shape=").append(ci.isAnnotation() ? "oval" : ci.isInterface() ? "diamond" : "box") .append(",style=filled,fillcolor=\"#").append(ci.isAnnotation() ? ANNOTATION_COLOR : ci.isInterface() ? INTERFACE_COLOR : STANDARD_CLASS_COLOR) .append("\",label="); buf.append('<'); buf.append(""); // Class modifiers buf.append(""); if (ci.getName().contains(".")) { buf.append(""); } // Class name buf.append(""); buf.append("
").append(ci.getModifiersStr()).append(' ') .append(ci.isEnum() ? "enum" : ci.isAnnotation() ? "@interface" : ci.isInterface() ? "interface" : "class") .append("
"); htmlEncode(ci.getPackageName(), buf); buf.append("
"); htmlEncode(ci.getSimpleName(), buf); buf.append("
"); buf.append(">];\n"); } buf.append('\n'); for (final ClassInfo ci : classInfoList) { for (final ClassInfo dep : ci.getClassDependencies()) { if (includeExternalClasses || allVisibleNodes.contains(dep)) { // class --> dep buf.append(" \"").append(ci.getName()).append("\" -> \"").append(dep.getName()) .append("\" [arrowsize=2.5]\n"); } } } buf.append('}'); return buf.toString(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy