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.utils;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.regex.Matcher;
22  import java.util.regex.Pattern;
23  
24  /**
25   * Utility methods for handling strings.
26   */
27  public final class StringUtils {
28  
29      /**
30       * Return lines joined with line separators as a String.
31       *
32       * @param lines Lines to join with line separators.
33       * @return Lines joined with line separators as a String.
34       */
35      public static String asString(final ArrayList<String> lines) {
36          StringBuilder stringBuilder = new StringBuilder();
37  
38          String prefix = "";
39          for (String line: lines) {
40              stringBuilder.append(prefix);
41              prefix = System.getProperty("line.separator");
42              stringBuilder.append(line);
43          }
44  
45          return stringBuilder.toString();
46      }
47  
48      /**
49       * Escape an array of quote strings, for safe inclusion in an XML document.
50       *
51       * <p>
52       *
53       * This method deals only with {@code &amp;, &lt;, &gt;, &quot;, &apos;}.
54       *
55       * @param strings   The array of strings to escape.
56       * @return          The array of escaped strings.
57       */
58      public static ArrayList<String> escapeXml(final ArrayList<String> strings) {
59          ArrayList<String> result = new ArrayList<String>();
60          if (strings == null || strings.isEmpty()) {
61              return result;
62          }
63  
64          for (String string: strings) {
65              result.add(escapeXml(string));
66          }
67  
68          return result;
69      }
70  
71      /**
72       * Escape a quote string to be safely included in an XML document.
73       *
74       * <p>
75       *
76       * This method deals only with {@code &amp;, &lt;, &gt;, &quot;, &apos;}.
77       *
78       * @param string    The string to escape.
79       * @return          The escaped string.
80       */
81      public static String escapeXml(final String string) {
82          return string
83                  .replace("&", "&amp;")
84                  .replace("<", "&lt;")
85                  .replace(">", "&gt;")
86                  .replace("\"", "&quot;")
87                  .replace("'", "&apos;");
88      }
89  
90      /**
91       * Extract a single quote from an array of strings.
92       *
93       * <p>
94       *
95       * The quote is surrounded by a string marking the start of the quote,
96       * and a string marking the end of the quote.
97       *
98       * <p>
99       *
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 }