View Javadoc
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 }