1 package org.andromda.utils;
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.StringReader;
6 import java.util.regex.Matcher;
7 import java.util.regex.Pattern;
8 import org.andromda.utils.inflector.EnglishInflector;
9 import org.apache.commons.lang.StringUtils;
10 import org.apache.commons.lang.WordUtils;
11 import org.apache.log4j.Logger;
12
13 /**
14 * A utility object for doing string manipulation operations that are commonly
15 * needed by the code generation templates.
16 *
17 * @author Matthias Bohlen
18 * @author Chris Shaw
19 * @author Chad Brandon
20 * @author Wouter Zoons
21 * @author Bob Fields
22 */
23 public class StringUtilsHelper
24 extends StringUtils
25 {
26 /**
27 * The logger instance.
28 */
29 private static final Logger logger = Logger.getLogger(StringUtilsHelper.class);
30
31 /**
32 * <p> Replaces a given suffix of the source string with a new one. If the
33 * suffix isn't present, the string is returned unmodified.
34 * </p>
35 *
36 * @param src the <code>String</code> for which the suffix should be
37 * replaced
38 * @param suffixOld a <code>String</code> with the suffix that should be
39 * replaced
40 * @param suffixNew a <code>String</code> with the new suffix
41 * @return a <code>String</code> with the given suffix replaced or
42 * unmodified if the suffix isn't present
43 */
44 public static String replaceSuffix(
45 final String src,
46 final String suffixOld,
47 final String suffixNew)
48 {
49 if (src.endsWith(suffixOld))
50 {
51 return src.substring(0, src.length() - suffixOld.length()) + suffixNew;
52 }
53 return src;
54 }
55
56 /**
57 * <p> Returns the argument string as a camel cased name beginning with an
58 * uppercased letter.
59 * </p>
60 * <p> Non word characters be removed and the letter following such a
61 * character will be uppercased.
62 * </p>
63 *
64 * @param string any string
65 * @return the string converted to a camel cased name beginning with a lower
66 * cased letter.
67 */
68 public static String upperCamelCaseName(final String string)
69 {
70 if (StringUtils.isEmpty(string))
71 {
72 return string;
73 }
74
75 final String[] parts = splitAtNonWordCharacters(string);
76 final StringBuilder conversionBuffer = new StringBuilder();
77 for (String part : parts)
78 {
79 if (part.length() < 2)
80 {
81 conversionBuffer.append(part.toUpperCase());
82 }
83 else
84 {
85 conversionBuffer.append(part.substring(0, 1).toUpperCase());
86 conversionBuffer.append(part.substring(1));
87 }
88 }
89 return conversionBuffer.toString();
90 }
91
92 /**
93 * Removes the last occurrence of the oldValue found within the string.
94 *
95 * @param string the String to remove the <code>value</code> from.
96 * @param value the value to remove.
97 * @return String the resulting string.
98 */
99 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 }