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}