001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2014-2015 ForgeRock AS.
015 */
016
017package org.forgerock.maven.plugins.xcite;
018
019import org.forgerock.maven.plugins.xcite.utils.FileUtils;
020import org.forgerock.maven.plugins.xcite.utils.StringUtils;
021
022import java.io.File;
023import java.io.IOException;
024import java.util.ArrayList;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028/**
029 * Resolve citation strings in target files into quotes from source files.
030 */
031public class Resolver {
032
033    private boolean                         escapeXml;
034    private String                          indent;
035    private boolean                         outdent;
036    private File                            outputDirectory;
037
038    /**
039     * Construct a resolver.
040     *
041     * @param outputDirectory   Where to write files with quotes.
042     *                          Specify the source directory to replace files.
043     * @param escapeXml         Escape XML when quoting.
044     * @param indent            Indent quotes by this number of single spaces.
045     * @param outdent           Outdent to the left margin.
046     *                          When you specify
047     *                          both {@code indent} and {@code outdent},
048     *                          quotes are first outdented, then indented.
049     */
050    Resolver(File outputDirectory, boolean escapeXml, int indent, boolean outdent) {
051        this.outputDirectory    = outputDirectory;
052        this.escapeXml          = escapeXml;
053        this.indent             = new String(new char[indent]).replace('\0', ' ');
054        this.outdent            = outdent;
055    }
056
057    /**
058     * Resolve citation strings in a set of target files.
059     *
060     * @param sourceDirectory   Where to find files with citations.
061     * @param files             Relative file paths.
062     * @throws IOException      Failed to read or write a file.
063     */
064    void resolve(File sourceDirectory, String[] files) throws IOException {
065        for (String relativePath: files) {
066            resolve(sourceDirectory, new File(relativePath));
067        }
068    }
069
070    /**
071     * Resolve citation strings in a target file.
072     *
073     * @param baseDir       Where to find the file.
074     * @param file          Relative path to file.
075     * @throws IOException  Failed to read or write the file.
076     */
077    void resolve(File baseDir, File file) throws IOException {
078        File absFile = new File(baseDir, file.getPath());
079
080        if (!absFile.isFile()) {
081            return;
082        }
083
084        StringBuilder stringBuilder = new StringBuilder();
085        String prefix = "";
086        for (String line: FileUtils.getStrings(absFile)) {
087            stringBuilder.append(prefix);
088            prefix = System.getProperty("line.separator");
089            stringBuilder.append(resolve(absFile, line));
090        }
091
092        File out = new File(outputDirectory, file.getPath());
093        org.codehaus.plexus.util.FileUtils.fileWrite(
094                out, stringBuilder.toString());
095    }
096
097    /**
098     * Resolve citation strings a line of text.
099     *
100     * @param file          Absolute path to current file.
101     * @param line          Line potentially containing citations.
102     * @return              Line with quotes resolved.
103     * @throws IOException  Failed to process the line.
104     */
105    String resolve(File file, String line) throws IOException {
106
107        // Split the line into parts, where some are citations, some not.
108        String[] parts = split(line);
109
110        // For part that are citations, replace them with quotes.
111        int i = 0;
112        for (String part: parts) {
113            if (part == null) {
114                parts[i] = "";
115                ++i;
116                continue;
117            }
118
119            // Try to construct a Citation with both delimiters,
120            // the alternative % and also the default :.
121            Citation citation = null;
122            if (part.contains("%")) {
123                citation = Citation.valueOf(part, "%");
124            }
125            if (citation == null) {
126                citation = Citation.valueOf(part);
127            }
128
129            if (citation == null) { // The part is not a citation.
130                parts[i] = part;
131            } else {
132                String quote = getQuote(file, citation);
133
134                // If the quote is the same the original citation string,
135                // return the part unchanged.
136                parts[i] = (quote.equals(citation.toString())) ? part : quote;
137            }
138
139            ++i;
140        }
141
142        // Put the line back together into a single string.
143        StringBuilder stringBuilder = new StringBuilder();
144        for (String part: parts) {
145            stringBuilder.append(part);
146        }
147        return stringBuilder.toString();
148    }
149
150    /**
151     * Split a line into strings, where citation strings are separate.
152     *
153     * @param line The line to split.
154     * @return     The line split into strings. Null for null input.
155     */
156    String[] split(String line) {
157        if (line == null) {
158            return null;
159        }
160
161        if (line.isEmpty()) {
162            return new String[1];
163        }
164
165        ArrayList<String> parts = new ArrayList<String>();
166
167        // The line is composed of parts,
168        // possibly with text preceding each citation,
169        // possibly with trailing text following the last citation.
170        // For example, "pre [/test] [/test] post" splits into
171        // ["pre ", "[/test]", " ", "[/test]", " post"].
172
173        // open-bracket 1*(exclude close-bracket) close-bracket
174        Pattern citationCandidate = Pattern.compile("(\\[[^\\]]+\\])");
175        Matcher matcher = citationCandidate.matcher(line);
176        int index = 0;
177        while (matcher.find()) {
178            String before = line.substring(index, matcher.start());
179            if (!before.isEmpty()) {
180                parts.add(before);
181            }
182            parts.add(matcher.group());
183            index = matcher.end();
184        }
185        if (index < line.length()) {
186            parts.add(line.substring(index));
187        }
188
189        String[] results = new String[parts.size()];
190        return parts.toArray(results);
191    }
192
193    /**
194     * Return the quote for a Citation in the specified file.
195     *
196     * @param file          The file where the citation is found.
197     * @param citation      The citation to resolve.
198     * @return              The quote from the resolved citation.
199     * @throws IOException  Failed to read the quote from the file.
200     */
201    String getQuote(File file, Citation citation) throws IOException {
202
203        // Citations can have relative paths for the files they cite.
204        // Get an absolute path instead in order to read the quote file.
205        File citedFile = new File(citation.getPath());
206        if (!citedFile.isAbsolute()) {
207            String currentDirectory = file.getParent();
208            citedFile = new File(currentDirectory, citedFile.getPath());
209        }
210
211        // Either this is not a citation, or it is a broken citation.
212        if (!citedFile.exists() || !citedFile.isFile()) {
213            return citation.toString();
214        }
215
216        // Extract the raw quote from the cited file.
217        ArrayList<String> quoteLines = StringUtils.extractQuote(
218                FileUtils.getStrings(citedFile), citation.getStart(), citation.getEnd());
219
220        if (escapeXml) {
221            quoteLines = StringUtils.escapeXml(quoteLines);
222        }
223
224        if (outdent) {
225            quoteLines = StringUtils.outdent(quoteLines);
226        }
227
228        if (!indent.isEmpty()) {
229            quoteLines = StringUtils.indent(quoteLines, indent);
230        }
231
232        // Quotes can contain citations.
233        // Resolve any citations in the quote before returning it as a string.
234        StringBuilder stringBuilder = new StringBuilder();
235        String prefix = "";
236        for (String quoteLine: quoteLines) {
237            stringBuilder.append(prefix);
238            prefix = System.getProperty("line.separator");
239            stringBuilder.append(resolve(citedFile, quoteLine)); // TODO: loop?
240        }
241        return stringBuilder.toString();
242    }
243}