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}