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 &, <, >, ", '}. 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 &, <, >, ", '}. 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("&", "&") 84 .replace("<", "<") 85 .replace(">", ">") 86 .replace("\"", """) 87 .replace("'", "'"); 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 }