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 ForgeRock AS.
015 */
016
017package org.forgerock.maven.plugins.xcite;
018
019import java.util.StringTokenizer;
020import java.util.regex.Pattern;
021
022/**
023 * A citation references part of a text file to quote.
024 *
025 * <p>
026 *
027 * The string representation includes the path to the file to cite,
028 * and optionally start and end markers to frame the text to quote.
029 *
030 * <p>
031 *
032 * For example, {@code [/path/to/script.sh:# start:# end]}
033 *
034 * <p>
035 *
036 * Notice that the example has a UNIX-style path, {@code /path/to/script.sh},
037 * a start marker, {@code # start}, and an end marker, {@code # end}.
038 * The file, {@code script.sh}, could have text to quote between the markers.
039 * For example, {@code script.sh} might have the following content:
040 *
041 * <pre>
042 * #!/bin/bash
043 *
044 * # start
045 * wall &lt;&lt;EOM
046 *     Hello world
047 * EOM
048 * # end
049 *
050 * exit
051 * </pre>
052 *
053 * In this case the quote would be the following:
054 *
055 * <pre>
056 * wall &lt;&lt;EOM
057 *     Hello world
058 * EOM
059 * </pre>
060 *
061 * Start and end markers depend on the language of the source text.
062 * Markers should make sense to people who mark up the source text,
063 * and so they should be composed of visible characters.
064 * Markers must not include the delimiter character
065 * that is used to separate the path and the markers.
066 *
067 * <p>
068 *
069 * Markers either both be on the same line as the text to quote,
070 * or should be on separate lines from the text to quote.
071 * In other words, you can prepare a text to quote a single line
072 * either by adding a start marker on the line before
073 * and an end marker on the line after,
074 * or by prepending a start marker to the line before the text to quote
075 * and appending an end marker after.
076 *
077 * <p>
078 *
079 * The following example shows markers on separate lines.
080 *
081 * <pre>
082 * #start
083 * This is the text to quote.
084 * #end
085 * </pre>
086 *
087 * <p>
088 *
089 * The following example shows markers on the same line as the text to quote.
090 *
091 * <pre>
092 * #start This is the text to quote. #end
093 * </pre>
094 *
095 * More formally, the citation string representation is as follows.
096 *
097 * <pre>
098 *
099 * 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 */
117public 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}