net.sourceforge.pmd.docs.RuleDocGenerator.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.pmd.docs.RuleDocGenerator.java

Source

/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.docs;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;

import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.RuleSet;
import net.sourceforge.pmd.RuleSetFactory;
import net.sourceforge.pmd.RuleSetNotFoundException;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.rule.RuleReference;
import net.sourceforge.pmd.lang.rule.XPathRule;
import net.sourceforge.pmd.properties.MultiValuePropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyDescriptor;

public class RuleDocGenerator {
    private static final Logger LOG = Logger.getLogger(RuleDocGenerator.class.getName());

    private static final String GENERATED_WARNING = "<!-- DO NOT EDIT THIS FILE. This file is generated from file ${source}. -->";
    private static final String GENERATED_WARNING_NO_SOURCE = "<!-- DO NOT EDIT THIS FILE. This file is generated. -->";

    private static final String LANGUAGE_INDEX_FILENAME_PATTERN = "docs/pages/pmd/rules/${language.tersename}.md";
    private static final String LANGUAGE_INDEX_PERMALINK_PATTERN = "pmd_rules_${language.tersename}.html";
    private static final String RULESET_INDEX_FILENAME_PATTERN = "docs/pages/pmd/rules/${language.tersename}/${ruleset.name}.md";
    private static final String RULESET_INDEX_PERMALINK_PATTERN = "pmd_rules_${language.tersename}_${ruleset.name}.html";

    private static final String DEPRECATION_LABEL_SMALL = "<span style=\"border-radius: 0.25em; color: #fff; padding: 0.2em 0.6em 0.3em; display: inline; background-color: #d9534f; font-size: 75%;\">Deprecated</span> ";
    private static final String DEPRECATION_LABEL = "<span style=\"border-radius: 0.25em; color: #fff; padding: 0.2em 0.6em 0.3em; display: inline; background-color: #d9534f;\">Deprecated</span> ";
    private static final String DEPRECATED_RULE_PROPERTY_MARKER = "deprecated!";

    private static final String GITHUB_SOURCE_LINK = "https://github.com/pmd/pmd/blob/master/";

    /** Maintains mapping from pmd terse language name to rouge highlighter language. */
    private static final Map<String, String> LANGUAGE_HIGHLIGHT_MAPPER = new HashMap<>();

    static {
        LANGUAGE_HIGHLIGHT_MAPPER.put("ecmascript", "javascript");
        LANGUAGE_HIGHLIGHT_MAPPER.put("pom", "xml");
        LANGUAGE_HIGHLIGHT_MAPPER.put("apex", "java");
        LANGUAGE_HIGHLIGHT_MAPPER.put("plsql", "sql");
    }

    private final Path root;
    private final FileWriter writer;

    public RuleDocGenerator(FileWriter writer, Path root) {
        this.writer = Objects.requireNonNull(writer, "A file writer must be provided");
        this.root = Objects.requireNonNull(root, "Root directory must be provided");

        Path docsDir = root.resolve("docs");
        if (!Files.exists(docsDir) || !Files.isDirectory(docsDir)) {
            throw new IllegalArgumentException("Couldn't find \"docs\" subdirectory");
        }
    }

    public void generate(Iterator<RuleSet> registeredRulesets, List<String> additionalRulesets) {
        Map<Language, List<RuleSet>> sortedRulesets;
        Map<Language, List<RuleSet>> sortedAdditionalRulesets;
        try {
            sortedRulesets = sortRulesets(registeredRulesets);
            sortedAdditionalRulesets = sortRulesets(resolveAdditionalRulesets(additionalRulesets));
            generateLanguageIndex(sortedRulesets, sortedAdditionalRulesets);
            generateRuleSetIndex(sortedRulesets);

            generateSidebar(sortedRulesets);

        } catch (RuleSetNotFoundException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void generateSidebar(Map<Language, List<RuleSet>> sortedRulesets) throws IOException {
        SidebarGenerator generator = new SidebarGenerator(writer, root);
        generator.generateSidebar(sortedRulesets);
    }

    private Iterator<RuleSet> resolveAdditionalRulesets(List<String> additionalRulesets)
            throws RuleSetNotFoundException {
        if (additionalRulesets == null) {
            return Collections.emptyIterator();
        }

        List<RuleSet> rulesets = new ArrayList<>();
        RuleSetFactory ruleSetFactory = new RuleSetFactory();
        for (String filename : additionalRulesets) {
            try {
                // do not take rulesets from pmd-test or pmd-core
                if (!filename.contains("pmd-test") && !filename.contains("pmd-core")) {
                    rulesets.add(ruleSetFactory.createRuleSet(filename));
                } else {
                    LOG.fine("Ignoring ruleset " + filename);
                }
            } catch (IllegalArgumentException e) {
                // ignore rulesets, we can't read
                LOG.log(Level.WARNING, "ruleset file " + filename + " ignored (" + e.getMessage() + ")", e);
            }
        }
        return rulesets.iterator();
    }

    private Path getAbsoluteOutputPath(String filename) {
        return root.resolve(FilenameUtils.normalize(filename));
    }

    private Map<Language, List<RuleSet>> sortRulesets(Iterator<RuleSet> rulesets) throws RuleSetNotFoundException {
        SortedMap<Language, List<RuleSet>> rulesetsByLanguage = new TreeMap<>();

        while (rulesets.hasNext()) {
            RuleSet ruleset = rulesets.next();
            Language language = getRuleSetLanguage(ruleset);

            if (!rulesetsByLanguage.containsKey(language)) {
                rulesetsByLanguage.put(language, new ArrayList<RuleSet>());
            }
            rulesetsByLanguage.get(language).add(ruleset);
        }

        for (List<RuleSet> rulesetsOfOneLanguage : rulesetsByLanguage.values()) {
            Collections.sort(rulesetsOfOneLanguage, new Comparator<RuleSet>() {
                @Override
                public int compare(RuleSet o1, RuleSet o2) {
                    return o1.getName().compareToIgnoreCase(o2.getName());
                }
            });
        }
        return rulesetsByLanguage;
    }

    /**
     * Rulesets could potentially contain rules from various languages.
     * But for built-in rulesets, all rules within one ruleset belong to
     * one language. So we take the language of the first rule.
     * @param ruleset
     * @return the terse name of the ruleset's language
     */
    private static Language getRuleSetLanguage(RuleSet ruleset) {
        Collection<Rule> rules = ruleset.getRules();
        if (rules.isEmpty()) {
            throw new RuntimeException("Ruleset " + ruleset.getFileName() + " is empty!");
        }
        return rules.iterator().next().getLanguage();
    }

    /**
     * Writes for each language an index file, which lists the rulesets, the rules
     * and links to the ruleset pages.
     * @param rulesets all registered/built-in rulesets
     * @param sortedAdditionalRulesets additional rulesets
     * @throws IOException
     */
    private void generateLanguageIndex(Map<Language, List<RuleSet>> rulesets,
            Map<Language, List<RuleSet>> sortedAdditionalRulesets) throws IOException {
        for (Map.Entry<Language, List<RuleSet>> entry : rulesets.entrySet()) {
            String languageTersename = entry.getKey().getTerseName();
            String filename = LANGUAGE_INDEX_FILENAME_PATTERN.replace("${language.tersename}", languageTersename);
            Path path = getAbsoluteOutputPath(filename);

            List<String> lines = new LinkedList<>();
            lines.add("---");
            lines.add("title: " + entry.getKey().getName() + " Rules");
            lines.add("tags: [rule_references, " + languageTersename + "]");
            lines.add("summary: Index of all built-in rules available for " + entry.getKey().getName());
            lines.add("language_name: " + entry.getKey().getName());
            lines.add("permalink: "
                    + LANGUAGE_INDEX_PERMALINK_PATTERN.replace("${language.tersename}", languageTersename));
            lines.add("folder: pmd/rules");
            lines.add("---");
            lines.add(GENERATED_WARNING_NO_SOURCE);

            for (RuleSet ruleset : entry.getValue()) {
                lines.add("## " + ruleset.getName());
                lines.add("");
                lines.add(
                        "{% include callout.html content=\"" + getRuleSetDescriptionSingleLine(ruleset) + "\" %}");
                lines.add("");

                for (Rule rule : getSortedRules(ruleset)) {
                    String link = RULESET_INDEX_PERMALINK_PATTERN
                            .replace("${language.tersename}", languageTersename)
                            .replace("${ruleset.name}", RuleSetUtils.getRuleSetFilename(ruleset));
                    if (rule instanceof RuleReference) {
                        RuleReference ref = (RuleReference) rule;
                        if (ruleset.getFileName().equals(ref.getRuleSetReference().getRuleSetFileName())) {
                            // rule renamed within same ruleset
                            lines.add("*   [" + rule.getName() + "](" + link + "#"
                                    + rule.getName().toLowerCase(Locale.ROOT) + "): " + DEPRECATION_LABEL_SMALL
                                    + "The rule has been renamed. Use instead " + "[" + ref.getRule().getName()
                                    + "](" + link + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ").");
                        } else {
                            // rule moved to another ruleset...
                            String otherLink = RULESET_INDEX_PERMALINK_PATTERN
                                    .replace("${language.tersename}", languageTersename)
                                    .replace("${ruleset.name}", RuleSetUtils
                                            .getRuleSetFilename(ref.getRuleSetReference().getRuleSetFileName()));
                            lines.add("*   [" + rule.getName() + "](" + link + "#"
                                    + rule.getName().toLowerCase(Locale.ROOT) + "): " + DEPRECATION_LABEL_SMALL
                                    + "The rule has been moved to another ruleset. Use instead " + "["
                                    + ref.getRule().getName() + "](" + otherLink + "#"
                                    + ref.getRule().getName().toLowerCase(Locale.ROOT) + ").");
                        }
                    } else {
                        link += "#" + rule.getName().toLowerCase(Locale.ROOT);
                        lines.add("*   [" + rule.getName() + "](" + link + "): "
                                + (rule.isDeprecated() ? DEPRECATION_LABEL_SMALL : "")
                                + getShortRuleDescription(rule));
                    }
                }
                lines.add("");
            }

            List<RuleSet> additionalRulesetsForLanguage = sortedAdditionalRulesets.get(entry.getKey());
            if (additionalRulesetsForLanguage != null) {
                lines.add("## Additional rulesets");
                lines.add("");

                for (RuleSet ruleset : additionalRulesetsForLanguage) {
                    boolean deprecated = RuleSetUtils.isRuleSetDeprecated(ruleset);

                    String rulesetName = ruleset.getName() + " (`" + RuleSetUtils.getRuleSetClasspath(ruleset)
                            + "`)";

                    if (!deprecated) {
                        lines.add("*   " + rulesetName + ":");
                        lines.add("");
                        lines.add("    " + getRuleSetDescriptionSingleLine(ruleset));
                        lines.add("");
                    } else {
                        lines.add("*   " + rulesetName + ":");
                        lines.add("");
                        lines.add(
                                "    " + DEPRECATION_LABEL_SMALL + " This ruleset is for backwards compatibility.");
                        lines.add("");
                    }

                    lines.add("    It contains the following rules:");
                    lines.add("");
                    StringBuilder rules = new StringBuilder();
                    for (Rule rule : getSortedRules(ruleset)) {
                        if (rules.length() == 0) {
                            rules.append("    ");
                        } else {
                            rules.append(", ");
                        }

                        Rule resolvedRule = RuleSetUtils.resolveRuleReferences(rule);
                        if (resolvedRule instanceof RuleReference) {
                            // Note: deprecated rulesets contain by definition only rule references
                            RuleReference ref = (RuleReference) resolvedRule;
                            String otherLink = RULESET_INDEX_PERMALINK_PATTERN
                                    .replace("${language.tersename}", languageTersename)
                                    .replace("${ruleset.name}", RuleSetUtils
                                            .getRuleSetFilename(ref.getRuleSetReference().getRuleSetFileName()));

                            rules.append("[").append(ref.getName()).append("](");
                            rules.append(otherLink).append("#")
                                    .append(ref.getRule().getName().toLowerCase(Locale.ROOT)).append(")");
                        } else {
                            rules.append(rule.getName());
                        }
                    }
                    lines.add(rules.toString());
                    lines.add("");
                }
                lines.add("");
            }

            System.out.println("Generated " + path);
            writer.write(path, lines);
        }
    }

    /**
     * Shortens and escapes (for markdown) some special characters. Otherwise the shortened text
     * could contain some unfinished sequences.
     * @param rule
     * @return
     */
    private static String getShortRuleDescription(Rule rule) {
        return StringEscapeUtils
                .escapeHtml4(
                        StringUtils.abbreviate(
                                StringUtils.stripToEmpty(rule.getDescription().replaceAll("\n|\r", "")
                                        .replaceAll("\\|", "\\\\|").replaceAll("`", "'").replaceAll("\\*", "")),
                                100));
    }

    private static String getRuleSetDescriptionSingleLine(RuleSet ruleset) {
        String description = ruleset.getDescription();
        description = StringEscapeUtils.escapeHtml4(description);
        description = description.replaceAll("\\n|\\r", " ");
        description = StringUtils.stripToEmpty(description);
        return description;
    }

    private static List<String> toLines(String s) {
        return Arrays.asList(s.split("\r\n|\n"));
    }

    /**
     * Generates for each ruleset a page. The page contains the details for each rule.
     *
     * @param rulesets all rulesets
     * @throws IOException
     */
    private void generateRuleSetIndex(Map<Language, List<RuleSet>> rulesets) throws IOException {
        for (Map.Entry<Language, List<RuleSet>> entry : rulesets.entrySet()) {
            Language language = entry.getKey();
            String languageTersename = language.getTerseName();
            String languageName = language.getName();
            for (RuleSet ruleset : entry.getValue()) {
                String rulesetFilename = RuleSetUtils.getRuleSetFilename(ruleset);
                String filename = RULESET_INDEX_FILENAME_PATTERN.replace("${language.tersename}", languageTersename)
                        .replace("${ruleset.name}", rulesetFilename);

                Path path = getAbsoluteOutputPath(filename);

                String permalink = RULESET_INDEX_PERMALINK_PATTERN
                        .replace("${language.tersename}", languageTersename)
                        .replace("${ruleset.name}", rulesetFilename);
                String ruleSetSourceFilepath = "../" + getRuleSetSourceFilepath(ruleset);

                List<String> lines = new LinkedList<>();
                lines.add("---");
                lines.add("title: " + ruleset.getName());
                lines.add("summary: " + getRuleSetDescriptionSingleLine(ruleset));
                lines.add("permalink: " + permalink);
                lines.add("folder: pmd/rules/" + languageTersename);
                lines.add("sidebaractiveurl: /"
                        + LANGUAGE_INDEX_PERMALINK_PATTERN.replace("${language.tersename}", languageTersename));
                lines.add("editmepath: " + ruleSetSourceFilepath);
                lines.add("keywords: " + getRuleSetKeywords(ruleset));
                lines.add("language: " + languageName);
                lines.add("---");
                lines.add(GENERATED_WARNING.replace("${source}", ruleSetSourceFilepath));

                for (Rule rule : getSortedRules(ruleset)) {
                    lines.add("## " + rule.getName());
                    lines.add("");

                    if (rule instanceof RuleReference) {
                        RuleReference ref = (RuleReference) rule;
                        if (ruleset.getFileName().equals(ref.getRuleSetReference().getRuleSetFileName())) {
                            // rule renamed within same ruleset
                            lines.add(DEPRECATION_LABEL);
                            lines.add("");
                            lines.add("This rule has been renamed. Use instead: [" + ref.getRule().getName() + "]("
                                    + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ")");
                            lines.add("");
                        } else {
                            // rule moved to another ruleset
                            String otherLink = RULESET_INDEX_PERMALINK_PATTERN
                                    .replace("${language.tersename}", languageTersename)
                                    .replace("${ruleset.name}", RuleSetUtils
                                            .getRuleSetFilename(ref.getRuleSetReference().getRuleSetFileName()));
                            lines.add(DEPRECATION_LABEL);
                            lines.add("");
                            lines.add("The rule has been moved to another ruleset. Use instead: ["
                                    + ref.getRule().getName() + "](" + otherLink + "#"
                                    + ref.getRule().getName().toLowerCase(Locale.ROOT) + ")");
                            lines.add("");
                        }
                    }

                    if (rule.isDeprecated()) {
                        lines.add(DEPRECATION_LABEL);
                        lines.add("");
                    }
                    if (rule.getSince() != null) {
                        lines.add("**Since:** PMD " + rule.getSince());
                        lines.add("");
                    }
                    lines.add(
                            "**Priority:** " + rule.getPriority() + " (" + rule.getPriority().getPriority() + ")");
                    lines.add("");

                    if (rule.getMinimumLanguageVersion() != null) {
                        lines.add("**Minimum Language Version:** " + rule.getLanguage().getName() + " "
                                + rule.getMinimumLanguageVersion().getVersion());
                        lines.add("");
                    }

                    lines.addAll(EscapeUtils.escapeLines(toLines(stripIndentation(rule.getDescription()))));
                    lines.add("");

                    if (rule instanceof XPathRule || rule instanceof RuleReference
                            && ((RuleReference) rule).getRule() instanceof XPathRule) {
                        lines.add("**This rule is defined by the following XPath expression:**");
                        lines.add("``` xpath");
                        lines.addAll(
                                toLines(StringUtils.stripToEmpty(rule.getProperty(XPathRule.XPATH_DESCRIPTOR))));
                        lines.add("```");
                    } else {
                        lines.add("**This rule is defined by the following Java class:** " + "["
                                + rule.getRuleClass() + "](" + GITHUB_SOURCE_LINK
                                + getRuleClassSourceFilepath(rule.getRuleClass()) + ")");
                    }
                    lines.add("");

                    if (!rule.getExamples().isEmpty()) {
                        lines.add("**Example(s):**");
                        lines.add("");
                        for (String example : rule.getExamples()) {
                            lines.add("``` " + mapLanguageForHighlighting(languageTersename));
                            lines.addAll(toLines(StringUtils.stripToEmpty(example)));
                            lines.add("```");
                            lines.add("");
                        }
                    }

                    List<PropertyDescriptor<?>> properties = new ArrayList<>(rule.getPropertyDescriptors());
                    // filter out standard properties
                    properties.remove(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR);
                    properties.remove(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR);
                    properties.remove(XPathRule.XPATH_DESCRIPTOR);
                    properties.remove(XPathRule.VERSION_DESCRIPTOR);

                    if (!properties.isEmpty()) {
                        lines.add("**This rule has the following properties:**");
                        lines.add("");
                        lines.add("|Name|Default Value|Description|Multivalued|");
                        lines.add("|----|-------------|-----------|-----------|");
                        for (PropertyDescriptor<?> propertyDescriptor : properties) {
                            String description = propertyDescriptor.description();
                            final boolean isDeprecated = isDeprecated(propertyDescriptor);
                            if (isDeprecated) {
                                description = description.substring(DEPRECATED_RULE_PROPERTY_MARKER.length());
                            }

                            String defaultValue = determineDefaultValueAsString(propertyDescriptor, rule, true);

                            String multiValued = "no";
                            if (propertyDescriptor.isMultiValue()) {
                                MultiValuePropertyDescriptor<?> multiValuePropertyDescriptor = (MultiValuePropertyDescriptor<?>) propertyDescriptor;
                                multiValued = "yes. Delimiter is '"
                                        + multiValuePropertyDescriptor.multiValueDelimiter() + "'.";
                            }

                            lines.add("|"
                                    + EscapeUtils.escapeMarkdown(
                                            StringEscapeUtils.escapeHtml4(propertyDescriptor.name()))
                                    + "|" + EscapeUtils.escapeMarkdown(StringEscapeUtils.escapeHtml4(defaultValue))
                                    + "|"
                                    + EscapeUtils.escapeMarkdown((isDeprecated ? DEPRECATION_LABEL_SMALL : "")
                                            + StringEscapeUtils.escapeHtml4(description))
                                    + "|" + EscapeUtils.escapeMarkdown(StringEscapeUtils.escapeHtml4(multiValued))
                                    + "|");
                        }
                        lines.add("");
                    }

                    if (properties.isEmpty()) {
                        lines.add("**Use this rule by referencing it:**");
                    } else {
                        lines.add("**Use this rule with the default properties by just referencing it:**");
                    }
                    lines.add("``` xml");
                    lines.add("<rule ref=\"category/" + languageTersename + "/" + rulesetFilename + ".xml/"
                            + rule.getName() + "\" />");
                    lines.add("```");
                    lines.add("");

                    if (properties.stream().anyMatch(it -> !isDeprecated(it))) {
                        lines.add("**Use this rule and customize it:**");
                        lines.add("``` xml");
                        lines.add("<rule ref=\"category/" + languageTersename + "/" + rulesetFilename + ".xml/"
                                + rule.getName() + "\">");
                        lines.add("    <properties>");
                        for (PropertyDescriptor<?> propertyDescriptor : properties) {
                            if (!isDeprecated(propertyDescriptor)) {
                                String defaultValue = determineDefaultValueAsString(propertyDescriptor, rule,
                                        false);
                                lines.add("        <property name=\"" + propertyDescriptor.name() + "\" value=\""
                                        + defaultValue + "\" />");
                            }
                        }
                        lines.add("    </properties>");
                        lines.add("</rule>");
                        lines.add("```");
                        lines.add("");
                    }
                }

                writer.write(path, lines);
                System.out.println("Generated " + path);
            }
        }
    }

    private static boolean isDeprecated(PropertyDescriptor<?> propertyDescriptor) {
        return propertyDescriptor.description() != null && propertyDescriptor.description().toLowerCase(Locale.ROOT)
                .startsWith(DEPRECATED_RULE_PROPERTY_MARKER);
    }

    private String determineDefaultValueAsString(PropertyDescriptor<?> propertyDescriptor, Rule rule, boolean pad) {
        String defaultValue = "";
        Object realDefaultValue = rule.getProperty(propertyDescriptor);
        @SuppressWarnings("unchecked") // just force it, we know it's the right type
        PropertyDescriptor<Object> captured = (PropertyDescriptor<Object>) propertyDescriptor;

        if (realDefaultValue != null) {
            defaultValue = captured.asDelimitedString(realDefaultValue);

            if (pad && propertyDescriptor.isMultiValue()) {
                @SuppressWarnings("unchecked") // multi valued properties are using a List
                MultiValuePropertyDescriptor<List<?>> multiPropertyDescriptor = (MultiValuePropertyDescriptor<List<?>>) propertyDescriptor;

                // surround the delimiter with spaces, so that the browser can wrap
                // the value nicely
                defaultValue = defaultValue.replaceAll(
                        Pattern.quote(String.valueOf(multiPropertyDescriptor.multiValueDelimiter())),
                        " " + multiPropertyDescriptor.multiValueDelimiter() + " ");
            }
        }
        return defaultValue;
    }

    private static String stripIndentation(String description) {
        if (description == null || description.isEmpty()) {
            return "";
        }

        String stripped = StringUtils.stripStart(description, "\n\r");
        stripped = StringUtils.stripEnd(stripped, "\n\r ");

        int indentation = 0;
        int strLen = stripped.length();
        while (Character.isWhitespace(stripped.charAt(indentation)) && indentation < strLen) {
            indentation++;
        }

        String[] lines = stripped.split("\\n");
        String prefix = StringUtils.repeat(' ', indentation);
        StringBuilder result = new StringBuilder(stripped.length());

        if (StringUtils.isNotEmpty(prefix)) {
            for (int i = 0; i < lines.length; i++) {
                String line = lines[i];
                if (i > 0) {
                    result.append(StringUtils.LF);
                }
                result.append(StringUtils.removeStart(line, prefix));
            }
        } else {
            result.append(stripped);
        }
        return result.toString();
    }

    /**
     * Simply maps PMD languages to rouge languages
     *
     * @param languageTersename
     * @return
     * @see <a href="https://github.com/jneen/rouge/wiki/List-of-supported-languages-and-lexers">List of supported languages</a>
     */
    private static String mapLanguageForHighlighting(String languageTersename) {
        if (LANGUAGE_HIGHLIGHT_MAPPER.containsKey(languageTersename)) {
            return LANGUAGE_HIGHLIGHT_MAPPER.get(languageTersename);
        }
        return languageTersename;
    }

    private String getRuleSetKeywords(RuleSet ruleset) {
        List<String> ruleNames = new LinkedList<>();
        for (Rule rule : ruleset.getRules()) {
            ruleNames.add(rule.getName());
        }
        return ruleset.getName() + ", " + StringUtils.join(ruleNames, ", ");
    }

    private List<Rule> getSortedRules(RuleSet ruleset) {
        List<Rule> sortedRules = new ArrayList<>(ruleset.getRules());
        Collections.sort(sortedRules, new Comparator<Rule>() {
            @Override
            public int compare(Rule o1, Rule o2) {
                return o1.getName().compareToIgnoreCase(o2.getName());
            }
        });
        return sortedRules;
    }

    /**
     * Searches for the source file of the given ruleset. This provides the information
     * for the "editme" link.
     *
     * @param ruleset the ruleset to search for.
     * @return
     * @throws IOException
     */
    private String getRuleSetSourceFilepath(RuleSet ruleset) throws IOException {
        final String rulesetFilename = FilenameUtils.normalize(StringUtils.chomp(ruleset.getFileName()));
        final List<Path> foundPathResult = new LinkedList<>();

        Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                String path = file.toString();
                if (path.contains("src") && path.endsWith(rulesetFilename)) {
                    foundPathResult.add(file);
                    return FileVisitResult.TERMINATE;
                }
                return super.visitFile(file, attrs);
            }
        });

        if (!foundPathResult.isEmpty()) {
            Path foundPath = foundPathResult.get(0);
            foundPath = root.relativize(foundPath);
            // Note: the path is normalized to unix path separators, so that the editme link
            // uses forward slashes
            return FilenameUtils.normalize(foundPath.toString(), true);
        }

        return StringUtils.chomp(ruleset.getFileName());
    }

    private String getRuleClassSourceFilepath(String ruleClass) throws IOException {
        final String relativeSourceFilename = ruleClass.replaceAll("\\.", Matcher.quoteReplacement(File.separator))
                + ".java";
        final List<Path> foundPathResult = new LinkedList<>();

        Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                String path = file.toString();
                if (path.contains("src") && path.endsWith(relativeSourceFilename)) {
                    foundPathResult.add(file);
                    return FileVisitResult.TERMINATE;
                }
                return super.visitFile(file, attrs);
            }
        });

        if (!foundPathResult.isEmpty()) {
            Path foundPath = foundPathResult.get(0);
            foundPath = root.relativize(foundPath);
            return FilenameUtils.normalize(foundPath.toString(), true);
        }

        return FilenameUtils.normalize(relativeSourceFilename, true);
    }
}