001package org.andromda.utils; 002 003import java.io.BufferedReader; 004import java.io.IOException; 005import java.io.StringReader; 006import java.util.regex.Matcher; 007import java.util.regex.Pattern; 008import org.andromda.utils.inflector.EnglishInflector; 009import org.apache.commons.lang.StringUtils; 010import org.apache.commons.lang.WordUtils; 011import org.apache.log4j.Logger; 012 013/** 014 * A utility object for doing string manipulation operations that are commonly 015 * needed by the code generation templates. 016 * 017 * @author Matthias Bohlen 018 * @author Chris Shaw 019 * @author Chad Brandon 020 * @author Wouter Zoons 021 * @author Bob Fields 022 */ 023public class StringUtilsHelper 024 extends StringUtils 025{ 026 /** 027 * The logger instance. 028 */ 029 private static final Logger logger = Logger.getLogger(StringUtilsHelper.class); 030 031 /** 032 * <p> Replaces a given suffix of the source string with a new one. If the 033 * suffix isn't present, the string is returned unmodified. 034 * </p> 035 * 036 * @param src the <code>String</code> for which the suffix should be 037 * replaced 038 * @param suffixOld a <code>String</code> with the suffix that should be 039 * replaced 040 * @param suffixNew a <code>String</code> with the new suffix 041 * @return a <code>String</code> with the given suffix replaced or 042 * unmodified if the suffix isn't present 043 */ 044 public static String replaceSuffix( 045 final String src, 046 final String suffixOld, 047 final String suffixNew) 048 { 049 if (src.endsWith(suffixOld)) 050 { 051 return src.substring(0, src.length() - suffixOld.length()) + suffixNew; 052 } 053 return src; 054 } 055 056 /** 057 * <p> Returns the argument string as a camel cased name beginning with an 058 * uppercased letter. 059 * </p> 060 * <p> Non word characters be removed and the letter following such a 061 * character will be uppercased. 062 * </p> 063 * 064 * @param string any string 065 * @return the string converted to a camel cased name beginning with a lower 066 * cased letter. 067 */ 068 public static String upperCamelCaseName(final String string) 069 { 070 if (StringUtils.isEmpty(string)) 071 { 072 return string; 073 } 074 075 final String[] parts = splitAtNonWordCharacters(string); 076 final StringBuilder conversionBuffer = new StringBuilder(); 077 for (String part : parts) 078 { 079 if (part.length() < 2) 080 { 081 conversionBuffer.append(part.toUpperCase()); 082 } 083 else 084 { 085 conversionBuffer.append(part.substring(0, 1).toUpperCase()); 086 conversionBuffer.append(part.substring(1)); 087 } 088 } 089 return conversionBuffer.toString(); 090 } 091 092 /** 093 * Removes the last occurrence of the oldValue found within the string. 094 * 095 * @param string the String to remove the <code>value</code> from. 096 * @param value the value to remove. 097 * @return String the resulting string. 098 */ 099 public static String removeLastOccurrence( 100 String string, 101 final String value) 102 { 103 if (string != null && value != null) 104 { 105 StringBuilder buf = new StringBuilder(); 106 int index = string.lastIndexOf(value); 107 if (index != -1) 108 { 109 buf.append(string.substring(0, index)); 110 buf.append(string.substring( 111 index + value.length(), 112 string.length())); 113 string = buf.toString(); 114 } 115 } 116 return string; 117 } 118 119 /** 120 * <p> Returns the argument string as a camel cased name beginning with a 121 * lowercased letter. 122 * </p> 123 * <p> Non word characters be removed and the letter following such a 124 * character will be uppercased. 125 * </p> 126 * 127 * @param string any string 128 * @return the string converted to a camel cased name beginning with a lower 129 * cased letter. 130 */ 131 public static String lowerCamelCaseName(final String string) 132 { 133 return uncapitalize(upperCamelCaseName(string)); 134 } 135 136 /** 137 * Returns true if the input string starts with a lowercase letter. 138 * Used for validations of property/operation names against naming conventions. 139 * 140 * @param string any string 141 * @return true/false, null if null input 142 */ 143 public static Boolean startsWithLowercaseLetter(final String string) 144 { 145 if (string==null || string.length()<1) 146 { 147 return null; 148 } 149 final String start = string.substring(0, 1); 150 return isAllLowerCase(start) && isAlpha(start); 151 } 152 153 /** 154 * Returns true if the input string starts with an uppercase letter. 155 * Used for validations of Class names against naming conventions. 156 * 157 * @param string any string 158 * @return true/false, null if null input 159 */ 160 public static Boolean startsWithUppercaseLetter(final String string) 161 { 162 if (string==null) 163 { 164 return null; 165 } 166 final String start = string.substring(0, 1); 167 return isAllUpperCase(start) && isAlpha(start); 168 } 169 170 /** 171 * Converts the argument into a message key in a properties resource bundle, 172 * all lowercase characters, words are separated by dots. 173 * 174 * @param string any string 175 * @return the string converted to a value that would be well-suited for a 176 * message key 177 */ 178 public static String toResourceMessageKey(final String string) 179 { 180 return separate(StringUtils.trimToEmpty(string), ".").toLowerCase(); 181 } 182 183 /** 184 * Converts into a string suitable as a human readable phrase, First 185 * character is uppercase (the rest is left unchanged), words are separated 186 * by a space. 187 * 188 * @param string any string 189 * @return the string converted to a value that would be well-suited for a 190 * human readable phrase 191 */ 192 public static String toPhrase(final String string) 193 { 194 return capitalize(separate(string, " ")); 195 } 196 197 /** 198 * Converts the argument to lowercase, removes all non-word characters, and 199 * replaces each of those sequences by the separator. 200 * @param string 201 * @param separator 202 * @return separated string 203 */ 204 public static String separate( 205 final String string, 206 final String separator) 207 { 208 if (StringUtils.isBlank(string)) 209 { 210 return string; 211 } 212 213 final String[] parts = splitAtNonWordCharacters(string); 214 final StringBuilder buffer = new StringBuilder(); 215 216 for (int i = 0; i < parts.length - 1; i++) 217 { 218 if (StringUtils.isNotBlank(parts[i])) 219 { 220 buffer.append(parts[i]).append(separator); 221 } 222 } 223 return buffer.append(parts[parts.length - 1]).toString(); 224 } 225 226 /** 227 * Splits at each sequence of non-word characters. Sequences of capitals 228 * will be left untouched. 229 */ 230 private static String[] splitAtNonWordCharacters(final String string) 231 { 232 final Pattern capitalSequencePattern = Pattern.compile("[A-Z]+"); 233 final Matcher matcher = capitalSequencePattern.matcher(StringUtils.trimToEmpty(string)); 234 final StringBuffer buffer = new StringBuffer(); 235 while (matcher.find()) 236 { 237 matcher.appendReplacement(buffer, ' ' + matcher.group()); 238 } 239 matcher.appendTail(buffer); 240 241 // split on all non-word characters: make sure we send the good parts 242 return buffer.toString().split("[^A-Za-z0-9]+"); 243 } 244 245 /** 246 * Suffixes each line with the argument suffix. 247 * 248 * @param multiLines A String, optionally containing many lines 249 * @param suffix The suffix to append to the end of each line 250 * @return String The input String with the suffix appended at the end of 251 * each line 252 */ 253 public static String suffixLines( 254 final String multiLines, 255 final String suffix) 256 { 257 final String[] lines = StringUtils.trimToEmpty(multiLines).split(LINE_SEPARATOR); 258 final StringBuilder linesBuffer = new StringBuilder(); 259 for (String line : lines) 260 { 261 linesBuffer.append(line); 262 linesBuffer.append(suffix); 263 linesBuffer.append(LINE_SEPARATOR); 264 } 265 return linesBuffer.toString(); 266 } 267 268 /** 269 * Converts any multi-line String into a version that is suitable to be 270 * included as-is in properties resource bundle. 271 * 272 * @param multiLines A String, optionally containing many lines 273 * @return String The input String with a backslash appended at the end of 274 * each line, or <code>null</code> if the input String was blank. 275 */ 276 public static String toResourceMessage(String multiLines) 277 { 278 String resourceMessage = null; 279 if (StringUtils.isNotBlank(multiLines)) 280 { 281 final String suffix = "\\"; 282 multiLines = suffixLines(multiLines, ' ' + suffix).trim(); 283 while (multiLines.endsWith(suffix)) 284 { 285 multiLines = multiLines.substring( 286 0, 287 multiLines.lastIndexOf(suffix)).trim(); 288 } 289 resourceMessage = multiLines; 290 } 291 return resourceMessage; 292 } 293 294 /** 295 * Takes an English word as input and prefixes it with 'a ' or 'an ' 296 * depending on the first character of the argument String. <p> The 297 * characters 'a', 'e', 'i' and 'o' will yield the 'an' predicate while all 298 * the others will yield the 'a' predicate. 299 * </p> 300 * 301 * @param word the word needing the predicate 302 * @return the argument prefixed with the predicate 303 */ 304 public static String prefixWithAPredicate(final String word) 305 { 306 // todo: this method could be implemented with better logic, for example to support 'an r' and 'a rattlesnake' 307 308 final StringBuilder formattedBuffer = new StringBuilder(); 309 310 formattedBuffer.append("a "); 311 formattedBuffer.append(word); 312 313 char firstChar = word.charAt(0); 314 switch (firstChar) 315 { 316 case 'a': // fall-through 317 case 'e': // fall-through 318 case 'i': // fall-through 319 case 'o': 320 formattedBuffer.insert(1, 'n'); 321 break; 322 default: 323 } 324 325 return formattedBuffer.toString(); 326 } 327 328 /** 329 * Converts multi-line text into a single line, normalizing whitespace in the 330 * process. This means whitespace characters will not follow each other 331 * directly. The resulting String will be trimmed. If the 332 * input String is null the return value will be an empty string. 333 * 334 * @param string A String, may be null 335 * @return The argument in a single line 336 */ 337 public static String toSingleLine(String string) 338 { 339 // remove anything that is greater than 1 space. 340 return (string == null) ? "" : string.replaceAll("[$\\s]+", " ").trim(); 341 } 342 343 /** 344 * Linguistically pluralizes a singular noun. 345 * <ul> 346 * <li><code>noun</code> becomes <code>nouns</code></li> 347 * <li><code>key</code> becomes <code>keys</code></li> 348 * <li><code>word</code> becomes <code>words</code></li> 349 * <li><code>property</code> becomes <code>properties</code></li> 350 * <li><code>bus</code> becomes <code>busses</code></li> 351 * <li><code>boss</code> becomes <code>bosses</code></li> 352 * </ul> 353 * <p> Whitespace as well as <code>null</code> arguments will return an 354 * empty String. 355 * </p> 356 * 357 * @param singularNoun A singular noun to pluralize 358 * @return The plural of the argument singularNoun or the empty String if the argument is 359 * <code>null</code> or blank. 360 */ 361 public static String pluralize(final String singularNoun) 362 { 363 final String plural = EnglishInflector.pluralize(singularNoun); 364 return plural == null ? "" : plural.trim(); 365 } 366 367 /** 368 * Formats the argument string without any indentation, the text will be 369 * wrapped at the default column. 370 * @param plainText 371 * @return formatted string 372 * 373 * @see #format(String, String) 374 */ 375 public static String format(final String plainText) 376 { 377 return format(plainText, ""); 378 } 379 380 /** 381 * Formats the given argument with the specified indentation, wrapping the 382 * text at a 64 column margin. 383 * @param plainText 384 * @param indentation 385 * @return formatted string 386 * 387 * @see #format(String, String, int) 388 */ 389 public static String format( 390 final String plainText, 391 final String indentation) 392 { 393 return format(plainText, indentation, 100 - indentation.length()); 394 } 395 396 /** 397 * Formats the given argument with the specified indentation, wrapping the 398 * text at the desired column margin. The returned String will not be suited 399 * for display in HTML environments. 400 * @param plainText 401 * @param indentation 402 * @param wrapAtColumn 403 * @return formatted string 404 * 405 * @see #format(String, String, int, boolean) 406 */ 407 public static String format( 408 final String plainText, 409 final String indentation, 410 final int wrapAtColumn) 411 { 412 return format(plainText, indentation, wrapAtColumn, true); 413 } 414 415 /** 416 * <p> 417 * Formats the given argument with the specified indentation, wrapping the 418 * text at the desired column margin. 419 * </p> 420 * <p> 421 * When enabling <em>htmlStyle</em> the returned text will be suitable for 422 * display in HTML environments such as JavaDoc, all newlines will be 423 * replaced by paragraphs. 424 * </p> 425 * <p> 426 * This method trims the input text: all leading and trailing whitespace 427 * will be removed. 428 * </p> 429 * <p> 430 * If for some reason this method would fail it will return the 431 * <em>plainText</em> argument. 432 * </p> 433 * 434 * @param plainText the text to format, the empty string will be returned in 435 * case this argument is <code>null</code>; long words will be 436 * placed on a newline but will never be wrapped 437 * @param indentation the empty string will be used if this argument would 438 * be <code>null</code> 439 * @param wrapAtColumn does not take into account the length of the 440 * indentation, needs to be strictly positive 441 * @param htmlStyle whether or not to make sure the returned string is 442 * suited for display in HTML environments such as JavaDoc 443 * @return a String instance which represents the formatted input, never 444 * <code>null</code> 445 * @throws IllegalArgumentException when the <em>wrapAtColumn</em> 446 * argument is not strictly positive 447 */ 448 public static String format( 449 final String plainText, 450 String indentation, 451 final int wrapAtColumn, 452 final boolean htmlStyle) 453 { 454 // - we cannot wrap at a column index less than 1 455 if (wrapAtColumn < 1) 456 { 457 throw new IllegalArgumentException("Cannot wrap at column less than 1: " + wrapAtColumn); 458 } 459 460 // unspecified indentation will use the empty string 461 if (indentation == null) 462 { 463 indentation = ""; 464 } 465 466 // - null plaintext will yield the empty string 467 if (StringUtils.isBlank(plainText)) 468 { 469 return indentation; 470 } 471 472 final String lineSeparator = LINE_SEPARATOR; 473 474 String format; 475 476 try 477 { 478 // - this buffer will contain the formatted text 479 final StringBuilder formattedText = new StringBuilder(); 480 481 // - we'll be reading lines from this reader 482 final BufferedReader reader = new BufferedReader(new StringReader(plainText)); 483 484 String line = reader.readLine(); 485 486 // - test whether or not we reached the end of the stream 487 while (line != null) 488 { 489 if (StringUtils.isNotBlank(line)) 490 { 491 // Remove leading/trailing whitespace before adding indentation and html formatting. 492 //line = line.trim(); 493 // - in HTML mode we start each new line on a paragraph 494 if (htmlStyle) 495 { 496 formattedText.append(indentation); 497 formattedText.append("<p>"); 498 formattedText.append(lineSeparator); 499 } 500 501 // - WordUtils.wrap never indents the first line so we do it 502 // here 503 formattedText.append(indentation); 504 505 // - append the wrapped text, the indentation is prefixed 506 // with a newline 507 formattedText.append(WordUtils.wrap( 508 line.trim(), 509 wrapAtColumn, 510 lineSeparator + indentation, 511 false)); 512 513 // - in HTML mode we need to close the paragraph 514 if (htmlStyle) 515 { 516 formattedText.append(lineSeparator); 517 formattedText.append(indentation); 518 formattedText.append("</p>"); 519 } 520 } 521 522 // - read the next line 523 line = reader.readLine(); 524 525 // - only add a newline when the next line is not empty and some 526 // string have already been added 527 if (formattedText.length() > 0 && StringUtils.isNotBlank(line)) 528 { 529 formattedText.append(lineSeparator); 530 } 531 } 532 533 // - close the reader as there is nothing more to read 534 reader.close(); 535 536 // - set the return value 537 format = formattedText.toString(); 538 } 539 catch (final IOException ioException) 540 { 541 logger.error("Could not format text: " + plainText, ioException); 542 format = plainText; 543 } 544 545 return format; 546 } 547 548 /** 549 * The line separator. 550 */ 551 private static final String LINE_SEPARATOR = "\n"; 552 553 /** 554 * Gets the line separator. 555 * 556 * @return the line separator. 557 */ 558 public static String getLineSeparator() 559 { 560 // - for reasons of platform compatibility we do not use the 'line.separator' property 561 // since this will break the build on different platforms (for example 562 // when comparing cartridge output zips) 563 return LINE_SEPARATOR; 564 } 565}