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.utils;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.regex.Matcher;
022import java.util.regex.Pattern;
023
024/**
025 * Utility methods for handling strings.
026 */
027public final class StringUtils {
028
029    /**
030     * Return lines joined with line separators as a String.
031     *
032     * @param lines Lines to join with line separators.
033     * @return Lines joined with line separators as a String.
034     */
035    public static String asString(final ArrayList<String> lines) {
036        StringBuilder stringBuilder = new StringBuilder();
037
038        String prefix = "";
039        for (String line: lines) {
040            stringBuilder.append(prefix);
041            prefix = System.getProperty("line.separator");
042            stringBuilder.append(line);
043        }
044
045        return stringBuilder.toString();
046    }
047
048    /**
049     * Escape an array of quote strings, for safe inclusion in an XML document.
050     *
051     * <p>
052     *
053     * This method deals only with {@code &amp;, &lt;, &gt;, &quot;, &apos;}.
054     *
055     * @param strings   The array of strings to escape.
056     * @return          The array of escaped strings.
057     */
058    public static ArrayList<String> escapeXml(final ArrayList<String> strings) {
059        ArrayList<String> result = new ArrayList<String>();
060        if (strings == null || strings.isEmpty()) {
061            return result;
062        }
063
064        for (String string: strings) {
065            result.add(escapeXml(string));
066        }
067
068        return result;
069    }
070
071    /**
072     * Escape a quote string to be safely included in an XML document.
073     *
074     * <p>
075     *
076     * This method deals only with {@code &amp;, &lt;, &gt;, &quot;, &apos;}.
077     *
078     * @param string    The string to escape.
079     * @return          The escaped string.
080     */
081    public static String escapeXml(final String string) {
082        return string
083                .replace("&", "&amp;")
084                .replace("<", "&lt;")
085                .replace(">", "&gt;")
086                .replace("\"", "&quot;")
087                .replace("'", "&apos;");
088    }
089
090    /**
091     * Extract a single quote from an array of strings.
092     *
093     * <p>
094     *
095     * The quote is surrounded by a string marking the start of the quote,
096     * and a string marking the end of the quote.
097     *
098     * <p>
099     *
100     * If the start marker is null or empty,
101     * then this method assumes the entire input text is the quote.
102     *
103     * <p>
104     *
105     * If the start marker exists but the end marker is null or empty,
106     * then this method assumes everything after the start marker is the quote.
107     *
108     * <p>
109     *
110     * This method does not allow a null or empty end marker.
111     * To quote from the start marker to the end of the text,
112     * use an end marker that does not show up in the text.
113     *
114     * <p>
115     *
116     * Unless the start marker and end marker are found on the same line,
117     * this method assumes the start marker and end marker lines
118     * are separate from the quote.
119     * In other words, for this case the method ignores
120     * substrings following the start marker on the same line
121     * and substrings preceding the end marker on the same line,
122     * unless both markers are on the same line.
123     *
124     * @param text                      Strings possibly containing a quote.
125     * @param start                     String marking start of quote.
126     * @param end                       String marking end of quote.
127     * @return                          Array of strings containing the quote.
128     * @throws IllegalArgumentException End marker was null or empty.
129     */
130    public static ArrayList<String> extractQuote(final ArrayList<String> text,
131                                                 final String start,
132                                                 final String end) {
133
134        if (text == null || text.isEmpty()) {
135            return new ArrayList<String>();
136        }
137
138        // No start marker: assume the whole text is the quote.
139        if (start == null || start.isEmpty()) {
140            return text;
141        }
142
143        if (end == null || end.isEmpty()) {
144            throw new IllegalArgumentException(
145                    "End marker cannot be null or empty");
146        }
147
148
149        ArrayList<String> quote = new ArrayList<String>();
150
151        final String literalStart = "^.*" + Pattern.quote(start);
152        final String literalEnd = Pattern.quote(end) + ".*$";
153        final String inline = literalStart + "(.+)" + literalEnd;
154        final Pattern inlinePattern = Pattern.compile(inline);
155
156        boolean inQuote = false;
157
158        for (String line: text) {
159
160            // Start and end markers on same line: single line quote.
161            if (!inQuote && line.matches(inline)) {
162                Matcher matcher = inlinePattern.matcher(line);
163                if (matcher.find()) {
164                    quote.add(matcher.group(1).trim());
165                }
166                return quote;
167            }
168
169            // Only start marker in the line: next line is in the quote.
170            if (!inQuote && line.contains(start)) {
171                inQuote = true;
172                continue;
173            }
174
175            // End marker in the line: done with the quote.
176            if (inQuote && line.contains(end)) {
177                break;
178            }
179
180            // Inside the quote: add the line to the quote.
181            if (inQuote) {
182                quote.add(line);
183            }
184        }
185
186        return stripEmpties(quote);
187    }
188
189    /**
190     * Extract a single quote from an array of strings.
191     *
192     * <p>
193     *
194     * The quote is surrounded by a single string
195     * that marks both the start of the quote and the end of the quote.
196     *
197     * <p>
198     *
199     * If the marker is null or empty, then the entire input text is the quote.
200     *
201     * <p>
202     *
203     * If the start marker exists but end marker is null or empty,
204     * then this method assumes everything after the start marker is the quote.
205     *
206     * <p>
207     *
208     * Unless the markers are found on the same line,
209     * this method assumes the markers lines are separate from the quote.
210     * In other words, for this case the method ignores
211     * substrings following the initial marker on the same line
212     * and substrings preceding the final marker on the same line,
213     * unless both markers are on the same line.
214     *
215     * @param text      The array of strings supposed to contain a quote.
216     * @param marker    The string marking the start and end of the quote.
217     * @return          The array of strings containing the quote.
218     */
219    public static ArrayList<String> extractQuote(final ArrayList<String> text,
220                                                 final String marker) {
221        return extractQuote(text, marker, marker);
222    }
223
224    /**
225     * Strip leading and trailing "empty" lines,
226     * where empty either means an empty string or string with only whitespace.
227     *
228     * @param text  The array of strings from which to strip empty lines.
229     * @return      Array of strings with leading and trailing empties removed.
230     */
231    private static ArrayList<String> stripEmpties(ArrayList<String> text) {
232        ArrayList<String> result = new ArrayList<String>();
233        if (text == null || text.isEmpty()) {
234            return result;
235        }
236
237        // Strip trailing empties
238        Collections.reverse(text);
239        result = stripLeadingEmpties(text);
240
241        // Strip leading empties
242        Collections.reverse(result);
243        result = stripLeadingEmpties(result);
244
245        return result;
246    }
247
248    /**
249     * Strip leading "empty" lines,
250     * where empty either means an empty string or string with only whitespace.
251     *
252     * @param text  The array of strings from which to strip empty lines.
253     * @return      The array of strings with leading empties removed.
254     */
255    private static ArrayList<String> stripLeadingEmpties(ArrayList<String> text) {
256        ArrayList<String> result = new ArrayList<String>();
257        if (text == null || text.isEmpty()) {
258            return result;
259        }
260
261        boolean inText = false;
262        for (String line: text) {
263            if (!inText && !line.trim().isEmpty()) {
264                inText = true;
265            }
266
267            if (inText) {
268                result.add(line);
269            }
270        }
271
272        return result;
273    }
274
275    /**
276     * Indent an array of strings.
277     *
278     * @param text      Array of strings to indent.
279     * @param indent    The indentation string, usually a series of spaces.
280     * @return          The indented array of strings.
281     */
282    public static ArrayList<String> indent(final ArrayList<String> text,
283                                           final String indent) {
284        ArrayList<String> result = new ArrayList<String>();
285        if (text == null || text.isEmpty()) {
286            return result;
287        }
288
289        if (indent == null || indent.isEmpty()) {
290            return text;
291        }
292
293        for (String line: text) {
294            result.add(indent + line);
295        }
296
297        return result;
298    }
299
300    /**
301     * Outdent an array of strings,
302     * removing an equal number of leftmost spaces from each string
303     * until at least one string starts with a non-space character.
304     *
305     * <p>
306     *
307     * Tab width (or height) depends and is subject to debate,
308     * so throw an exception if the initial whitespace includes a tab.
309     *
310     * @param indented                  Array of strings with leftmost spaces.
311     * @return                          Strings with leftmost spaces removed.
312     * @throws IllegalArgumentException Initial whitespace included a tab.
313     */
314    public static ArrayList<String> outdent(final ArrayList<String> indented) {
315        int indent = getIndent(indented);
316        return outdent(indented, indent);
317    }
318
319    /**
320     * Return the minimum indentation of an array of strings.
321     *
322     * <p>
323     *
324     * Tab width (or height) depends and is subject to debate,
325     * so throw an exception if the initial whitespace includes a tab.
326     *
327     * <p>
328     *
329     * Ignore lines that are nothing but spaces and a newline or CRLF.
330     *
331     * @param indented                  Array of strings with leftmost spaces.
332     * @return                          Min. number of consecutive indent spaces.
333     * @throws IllegalArgumentException Initial whitespace included a tab.
334     */
335    private static int getIndent(final ArrayList<String> indented) {
336
337        if (indented == null || indented.isEmpty()) {
338            return 0;
339        }
340
341        // Start with a huge theoretical indentation, then whittle it down.
342        int indent = Integer.MAX_VALUE;
343
344        int currentIndent;
345        for (String line: indented) {
346
347            Pattern initialWhitespace = Pattern.compile("^([ \\t]+)");
348            Matcher matcher = initialWhitespace.matcher(line);
349            String initialSpaces = "";
350            if (matcher.find()) {
351                initialSpaces = matcher.group();
352
353                if (initialSpaces.contains("\\t")) {
354                    throw new IllegalArgumentException(
355                            "Line has a tab in leading space: " + line);
356                }
357            }
358
359            // If the current indentation is the smallest so far, record it.
360            currentIndent = initialSpaces.length();
361            if (currentIndent < indent) {
362                indent = currentIndent;
363            }
364
365            // No sense in checking every line when there's nothing to do.
366            if (indent == 0) {
367                return indent;
368            }
369        }
370
371        return indent;
372    }
373
374    /**
375     * Outdent an array of strings,
376     * removing an equal number of leftmost spaces from each string
377     * until at least one string starts with a non-space character.
378     *
379     * @param indented  The array of strings with possible leftmost whitespace.
380     * @param indent    Min. number of leading white spaces on non-empty lines.
381     * @return          Array of strings with leftmost spaces removed.
382     */
383    private static ArrayList<String> outdent(final ArrayList<String> indented,
384                                             final int indent) {
385
386        if (indented == null || indented.isEmpty()) {
387            return new ArrayList<String>();
388        }
389
390        if (indent == 0) {
391            return indented;
392        }
393
394        String spaces = new String(new char[indent]).replace('\0', ' ');
395        ArrayList<String> outdented = new ArrayList<String>();
396        for (String line: indented) {
397            outdented.add(line.replaceFirst(spaces, ""));
398        }
399        return outdented;
400    }
401
402    /**
403     * Return an empty string if the string is only whitespace.
404     * Otherwise return the original string.
405     *
406     * @param string    The input string.
407     * @return          An empty string if the input string is only whitespace,
408     *                  otherwise the original string.
409     */
410    public static String removeEmptySpace(final String string) {
411        return (string.matches("^\\s+$")) ? "" : string;
412    }
413
414    /**
415     * Constructor not used.
416     */
417    private StringUtils() {
418        // Not used
419    }
420}