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