View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2014 ForgeRock AS.
15   */
16  
17  package org.forgerock.maven.plugins.xcite;
18  
19  import java.util.StringTokenizer;
20  import java.util.regex.Pattern;
21  
22  /**
23   * A citation references part of a text file to quote.
24   *
25   * <p>
26   *
27   * The string representation includes the path to the file to cite,
28   * and optionally start and end markers to frame the text to quote.
29   *
30   * <p>
31   *
32   * For example, {@code [/path/to/script.sh:# start:# end]}
33   *
34   * <p>
35   *
36   * Notice that the example has a UNIX-style path, {@code /path/to/script.sh},
37   * a start marker, {@code # start}, and an end marker, {@code # end}.
38   * The file, {@code script.sh}, could have text to quote between the markers.
39   * For example, {@code script.sh} might have the following content:
40   *
41   * <pre>
42   * #!/bin/bash
43   *
44   * # start
45   * wall &lt;&lt;EOM
46   *     Hello world
47   * EOM
48   * # end
49   *
50   * exit
51   * </pre>
52   *
53   * In this case the quote would be the following:
54   *
55   * <pre>
56   * wall &lt;&lt;EOM
57   *     Hello world
58   * EOM
59   * </pre>
60   *
61   * Start and end markers depend on the language of the source text.
62   * Markers should make sense to people who mark up the source text,
63   * and so they should be composed of visible characters.
64   * Markers must not include the delimiter character
65   * that is used to separate the path and the markers.
66   *
67   * <p>
68   *
69   * Markers either both be on the same line as the text to quote,
70   * or should be on separate lines from the text to quote.
71   * In other words, you can prepare a text to quote a single line
72   * either by adding a start marker on the line before
73   * and an end marker on the line after,
74   * or by prepending a start marker to the line before the text to quote
75   * and appending an end marker after.
76   *
77   * <p>
78   *
79   * The following example shows markers on separate lines.
80   *
81   * <pre>
82   * #start
83   * This is the text to quote.
84   * #end
85   * </pre>
86   *
87   * <p>
88   *
89   * The following example shows markers on the same line as the text to quote.
90   *
91   * <pre>
92   * #start This is the text to quote. #end
93   * </pre>
94   *
95   * More formally, the citation string representation is as follows.
96   *
97   * <pre>
98   *
99   * citation     = "[" path delimiter start-marker delimiter end-marker "]"
100  * citation     / "[" path delimiter start-marker "]"
101  * citation     / "[" path "]"
102  *
103  * path         = File.getPath()                ; Depends on the OS,
104  *                                              ; does not include delimiter
105  *
106  * delimiter    = ":" / "%"                     ; Default: ":"
107  *
108  * start-marker = 1*(VCHAR excluding delimiter) ; Depends on source language
109  *
110  * end-marker   = 1*(VCHAR excluding delimiter) ; Depends on source language
111  *
112  * </pre>
113  *
114  * Most systems allow file names that can cause problems with this scheme.
115  * Working around the problem is an exercise for the reader.
116  */
117 public class Citation {
118 
119     /**
120      * Initial wrapper to open the citation string representation.
121      */
122     private static final String OPEN  = "[";
123 
124     /**
125      * Final wrapper to close the citation string representation.
126      */
127     private static final String CLOSE = "]";
128 
129     private String path;        // Pathname for the file to quote
130     private char   delimiter;   // Delimiter for path, markers
131     private String start;       // Start marker
132     private String end;         // End marker
133 
134     /**
135      * Construct a citation from a path alone.
136      *
137      * <p>
138      *
139      * The path cannot be null.
140      *
141      * @param path      The pathname string for the file to quote. Not null.
142      * @throws IllegalArgumentException Path is broken somehow.
143      */
144     public Citation(final String path) {
145         build(path, ':', null, null);
146     }
147 
148     /**
149      * Construct a citation from a path, delimiter, and start marker,
150      * when the start marker and the end marker are the same.
151      *
152      * <p>
153      *
154      * The path cannot be null.
155      *
156      * <p>
157      *
158      * The start marker cannot contain the delimiter.
159      *
160      * @param path      The pathname string for the file to quote. Not null.
161      * @param delimiter The delimiter for path, markers.
162      * @param start     The start marker. If null or "", only path is useful.
163      * @throws IllegalArgumentException Path or marker is broken somehow.
164      */
165     public Citation(final String path, final char delimiter, final String start) {
166         build(path, delimiter, start, null);
167     }
168 
169     /**
170      * Construct a citation from a path, delimiter, start marker, and end marker.
171      *
172      * <p>
173      *
174      * The path cannot be null.
175      *
176      * <p>
177      *
178      * The markers cannot contain the delimiter.
179      *
180      * @param path      The pathname string for the file to quote. Not null.
181      * @param delimiter The delimiter for path, markers.
182      * @param start     The start marker. If null or "", only path is useful.
183      * @param end       The end marker. If null or "", {@code start} is both.
184      * @throws IllegalArgumentException Path or marker is broken somehow.
185      */
186     public Citation(final String path, final char delimiter,
187                     final String start, final String end) {
188         build(path, delimiter, start, end);
189     }
190 
191     /**
192      * Builder for constructing an instance.
193      *
194      * @param path      The pathname string for the file to quote. Not null.
195      * @param delimiter The delimiter for path, markers.
196      * @param start     The start marker. If null or "", only path is useful.
197      * @param end       The end marker. If null or "", {@code start} is both.
198      * @throws IllegalArgumentException Path or marker is broken somehow.
199      */
200     private void build(final String path, final char delimiter,
201                        final String start, final String end) {
202         setPath(path);
203         setDelimiter(delimiter);
204         setStart(start);
205         setEnd(end);
206     }
207 
208     /**
209      * Returns the value of the path.
210      *
211      * @return The value of the path.
212      */
213     public String getPath() {
214         return this.path;
215     }
216 
217     /**
218      * Sets the pathname string for the file to quote.
219      *
220      * @param path The pathname string for the file to quote.
221      * @throws IllegalArgumentException Path cannot be null.
222      */
223     public void setPath(final String path) {
224         if (path == null) {
225             throw new IllegalArgumentException("Path cannot be null.");
226         } else {
227             this.path = path;
228         }
229     }
230 
231     /**
232      * Returns the value of the delimiter.
233      *
234      * @return The value of the delimiter.
235      */
236     public char getDelimiter() {
237         return this.delimiter;
238     }
239 
240     /**
241      * Sets the value of the delimiter.
242      *
243      * @param delimiter The delimiter for path, markers.
244      */
245     public void setDelimiter(char delimiter) {
246         if (delimiter == ':' || delimiter == '%') {
247             this.delimiter = delimiter;
248         } else {
249             this.delimiter = ':';
250         }
251     }
252 
253     /**
254      * Returns the value of the start marker, which can be empty.
255      *
256      * @return The value of the start marker, which can be empty.
257      */
258     public String getStart() {
259         return this.start;
260     }
261 
262     /**
263      * Sets the value of the start marker, which must not contain the delimiter.
264      *
265      * @param start The value of the start marker.
266      * @throws IllegalArgumentException Start marker contains the delimiter.
267      */
268     public void setStart(String start) {
269         if (isNullOrEmpty(start)) {
270             this.start = "";
271         } else if (!start.contains(Character.toString(getDelimiter()))) {
272             this.start = start;
273         } else {
274             throw new IllegalArgumentException("Start marker: " + start
275                     + " contains delimiter: " + getDelimiter());
276         }
277     }
278 
279     /**
280      * Returns the value of the start marker, which can be empty.
281      *
282      * @return The value of the start marker, which can be empty.
283      */
284     public String getEnd() {
285         return this.end;
286     }
287 
288     /**
289      * Sets the value of the end marker, which must not contain the delimiter.
290      *
291      * @param end The value of the end marker.
292      * @throws IllegalArgumentException End marker contains the delimiter.
293      */
294     public void setEnd(String end) {
295         if (isNullOrEmpty(end)) {
296             this.end = getStart();
297         } else if (!end.contains(Character.toString(getDelimiter()))) {
298             this.end = end;
299         } else {
300             throw new IllegalArgumentException("End marker: " + end
301                     + " contains delimiter: " + getDelimiter());
302         }
303     }
304 
305     /**
306      * Returns true if the string is null or the string is empty.
307      *
308      * @param string The string.
309      * @return Whether the string is null or empty.
310      */
311     private static boolean isNullOrEmpty(final String string) {
312         return (string == null || string.isEmpty());
313     }
314 
315     /**
316      * Returns the string representation of this citation.
317      *
318      * @return The string representation of this citation.
319      */
320     public String toString() {
321         String start =
322                 (isNullOrEmpty(getStart()) ? "" : getDelimiter() + getStart());
323 
324         String end;
325         if (isNullOrEmpty(getStart()) || isNullOrEmpty(getEnd())) {
326             end = "";
327         } else {
328             end = getDelimiter() + getEnd();
329         }
330 
331         return OPEN + getPath() + start + end + CLOSE;
332     }
333 
334     /**
335      * Returns a Citation from the string representation.
336      *
337      * @param citation  The string representation.
338      * @param delimiter One of ":" or "%".
339      * @return A Citation corresponding to the string representation,
340      *         or null if the string representation does not parse.
341      */
342     public static Citation valueOf(final String citation, final String delimiter) {
343 
344         // No null delimiters.
345         if (delimiter == null) {
346             return null;
347         }
348 
349         // No illegal delimiters.
350         if (!(delimiter.equals(":") || delimiter.equals("%"))) {
351             return null;
352         }
353 
354         // OPEN marks the start of the citation string.
355         if (!citation.startsWith(OPEN)) {
356             return null;
357         }
358 
359         // CLOSE marks the end of the citation string.
360         if (!citation.endsWith(CLOSE)) {
361             return null;
362         }
363 
364         // Remove the OPEN & CLOSE wrappers.
365         String unwrapped = citation
366                 .replaceFirst(Pattern.quote(OPEN), "")
367                 .replaceFirst(Pattern.quote(CLOSE), "");
368 
369         // Starting with a delimiter means path is null.
370         if (unwrapped.startsWith(delimiter)) {
371             return null;
372         }
373 
374         // Delimiters should delimit actual values, not empty strings.
375         if (unwrapped.contains(delimiter + delimiter)) {
376             return null;
377         }
378 
379         // Tokenize using the delimiter, as values do not contain the delimiter.
380         StringTokenizer st = new StringTokenizer(unwrapped, delimiter);
381         String path  = null;
382         String start = null;
383         String end   = null;
384 
385         if (st.hasMoreTokens()) {    // path
386             path = st.nextToken();
387         }
388         if (st.hasMoreTokens()) {    // start
389             start = st.nextToken();
390         }
391         if (st.hasMoreTokens()) {    // end
392             end = st.nextToken();
393         }
394         if (st.hasMoreTokens()) {
395             return null;
396         }
397 
398         // Path must not be null.
399         if (isNullOrEmpty(path)) {
400             return null;
401         }
402 
403         return new Citation(path, ':', start, end);
404     }
405 
406     /**
407      * Returns a Citation from the string representation,
408      * assuming the default delimiter, {@code :}.
409      *
410      * @param citation The string representation.
411      * @return A Citation corresponding to the string representation,
412      *         or null if the string representation does not parse.
413      */
414     public static Citation valueOf(final String citation) {
415         return valueOf(citation, ":");
416     }
417 }