Resolver.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2014-2015 ForgeRock AS.
 */

package org.forgerock.maven.plugins.xcite;

import org.forgerock.maven.plugins.xcite.utils.FileUtils;
import org.forgerock.maven.plugins.xcite.utils.StringUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Resolve citation strings in target files into quotes from source files.
 */
public class Resolver {

    private boolean                         escapeXml;
    private String                          indent;
    private boolean                         outdent;
    private File                            outputDirectory;

    /**
     * Construct a resolver.
     *
     * @param outputDirectory   Where to write files with quotes.
     *                          Specify the source directory to replace files.
     * @param escapeXml         Escape XML when quoting.
     * @param indent            Indent quotes by this number of single spaces.
     * @param outdent           Outdent to the left margin.
     *                          When you specify
     *                          both {@code indent} and {@code outdent},
     *                          quotes are first outdented, then indented.
     */
    Resolver(File outputDirectory, boolean escapeXml, int indent, boolean outdent) {
        this.outputDirectory    = outputDirectory;
        this.escapeXml          = escapeXml;
        this.indent             = new String(new char[indent]).replace('\0', ' ');
        this.outdent            = outdent;
    }

    /**
     * Resolve citation strings in a set of target files.
     *
     * @param sourceDirectory   Where to find files with citations.
     * @param files             Relative file paths.
     * @throws IOException      Failed to read or write a file.
     */
    void resolve(File sourceDirectory, String[] files) throws IOException {
        for (String relativePath: files) {
            resolve(sourceDirectory, new File(relativePath));
        }
    }

    /**
     * Resolve citation strings in a target file.
     *
     * @param baseDir       Where to find the file.
     * @param file          Relative path to file.
     * @throws IOException  Failed to read or write the file.
     */
    void resolve(File baseDir, File file) throws IOException {
        File absFile = new File(baseDir, file.getPath());

        if (!absFile.isFile()) {
            return;
        }

        StringBuilder stringBuilder = new StringBuilder();
        String prefix = "";
        for (String line: FileUtils.getStrings(absFile)) {
            stringBuilder.append(prefix);
            prefix = System.getProperty("line.separator");
            stringBuilder.append(resolve(absFile, line));
        }

        File out = new File(outputDirectory, file.getPath());
        org.codehaus.plexus.util.FileUtils.fileWrite(
                out, stringBuilder.toString());
    }

    /**
     * Resolve citation strings a line of text.
     *
     * @param file          Absolute path to current file.
     * @param line          Line potentially containing citations.
     * @return              Line with quotes resolved.
     * @throws IOException  Failed to process the line.
     */
    String resolve(File file, String line) throws IOException {

        // Split the line into parts, where some are citations, some not.
        String[] parts = split(line);

        // For part that are citations, replace them with quotes.
        int i = 0;
        for (String part: parts) {
            if (part == null) {
                parts[i] = "";
                ++i;
                continue;
            }

            // Try to construct a Citation with both delimiters,
            // the alternative % and also the default :.
            Citation citation = null;
            if (part.contains("%")) {
                citation = Citation.valueOf(part, "%");
            }
            if (citation == null) {
                citation = Citation.valueOf(part);
            }

            if (citation == null) { // The part is not a citation.
                parts[i] = part;
            } else {
                String quote = getQuote(file, citation);

                // If the quote is the same the original citation string,
                // return the part unchanged.
                parts[i] = (quote.equals(citation.toString())) ? part : quote;
            }

            ++i;
        }

        // Put the line back together into a single string.
        StringBuilder stringBuilder = new StringBuilder();
        for (String part: parts) {
            stringBuilder.append(part);
        }
        return stringBuilder.toString();
    }

    /**
     * Split a line into strings, where citation strings are separate.
     *
     * @param line The line to split.
     * @return     The line split into strings. Null for null input.
     */
    String[] split(String line) {
        if (line == null) {
            return null;
        }

        if (line.isEmpty()) {
            return new String[1];
        }

        ArrayList<String> parts = new ArrayList<String>();

        // The line is composed of parts,
        // possibly with text preceding each citation,
        // possibly with trailing text following the last citation.
        // For example, "pre [/test] [/test] post" splits into
        // ["pre ", "[/test]", " ", "[/test]", " post"].

        // open-bracket 1*(exclude close-bracket) close-bracket
        Pattern citationCandidate = Pattern.compile("(\\[[^\\]]+\\])");
        Matcher matcher = citationCandidate.matcher(line);
        int index = 0;
        while (matcher.find()) {
            String before = line.substring(index, matcher.start());
            if (!before.isEmpty()) {
                parts.add(before);
            }
            parts.add(matcher.group());
            index = matcher.end();
        }
        if (index < line.length()) {
            parts.add(line.substring(index));
        }

        String[] results = new String[parts.size()];
        return parts.toArray(results);
    }

    /**
     * Return the quote for a Citation in the specified file.
     *
     * @param file          The file where the citation is found.
     * @param citation      The citation to resolve.
     * @return              The quote from the resolved citation.
     * @throws IOException  Failed to read the quote from the file.
     */
    String getQuote(File file, Citation citation) throws IOException {

        // Citations can have relative paths for the files they cite.
        // Get an absolute path instead in order to read the quote file.
        File citedFile = new File(citation.getPath());
        if (!citedFile.isAbsolute()) {
            String currentDirectory = file.getParent();
            citedFile = new File(currentDirectory, citedFile.getPath());
        }

        // Either this is not a citation, or it is a broken citation.
        if (!citedFile.exists() || !citedFile.isFile()) {
            return citation.toString();
        }

        // Extract the raw quote from the cited file.
        ArrayList<String> quoteLines = StringUtils.extractQuote(
                FileUtils.getStrings(citedFile), citation.getStart(), citation.getEnd());

        if (escapeXml) {
            quoteLines = StringUtils.escapeXml(quoteLines);
        }

        if (outdent) {
            quoteLines = StringUtils.outdent(quoteLines);
        }

        if (!indent.isEmpty()) {
            quoteLines = StringUtils.indent(quoteLines, indent);
        }

        // Quotes can contain citations.
        // Resolve any citations in the quote before returning it as a string.
        StringBuilder stringBuilder = new StringBuilder();
        String prefix = "";
        for (String quoteLine: quoteLines) {
            stringBuilder.append(prefix);
            prefix = System.getProperty("line.separator");
            stringBuilder.append(resolve(citedFile, quoteLine)); // TODO: loop?
        }
        return stringBuilder.toString();
    }
}