View Javadoc

1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.util;
5   
6   import java.util.ArrayList;
7   import java.util.Iterator;
8   import java.util.List;
9   
10  /**
11   * A number of String-specific utility methods for use by PMD or its IDE plugins.
12   *
13   * @author BrianRemedios
14   */
15  public final class StringUtil {
16  
17  	public static final String[] EMPTY_STRINGS = new String[0];
18      private static final boolean SUPPORTS_UTF8 = System.getProperty("net.sourceforge.pmd.supportUTF8", "no").equals("yes");
19  
20      private StringUtil() {}
21  
22      /**
23       * Return whether the non-null text arg starts with any of the prefix
24       * values.
25       *
26       * @param text
27       * @param prefixes
28       * @return boolean
29       */
30      public static boolean startsWithAny(String text, String... prefixes) {
31  
32      	for (String prefix : prefixes) {
33      		if (text.startsWith(prefix)) return true;
34      	}
35  
36      	return false;
37      }
38  
39      /**
40       * Returns whether the non-null text arg matches any of the test values.
41       *
42       * @param text
43       * @param tests
44       * @return boolean
45       */
46      public static boolean isAnyOf(String text, String... tests) {
47  
48      	for (String test : tests) {
49      		if (text.equals(test)) return true;
50      	}
51  
52      	return false;
53      }
54  
55      /**
56       * Checks for the existence of any of the listed prefixes on the
57       * non-null text and removes them.
58       *
59       * @param text
60       * @param prefixes
61       * @return String
62       */
63      public static String withoutPrefixes(String text, String... prefixes) {
64  
65      	for (String prefix : prefixes) {
66      		if (text.startsWith(prefix)) {
67      			return text.substring(prefix.length());
68      		}
69      	}
70  
71      	return text;
72      }
73  
74      /**
75       * Returns true if the value arg is either null, empty, or full of whitespace characters.
76       * More efficient that calling (string).trim().length() == 0
77       *
78       * @param value
79       * @return <code>true</code> if the value is empty, <code>false</code> otherwise.
80       */
81      public static boolean isEmpty(String value) {
82  
83      	if (value == null || "".equals(value)) {
84      		return true;
85      	}
86  
87      	for (int i=0; i<value.length(); i++) {
88      		if (!Character.isWhitespace(value.charAt(i))) {
89      		    return false;
90      		}
91      	}
92  
93      	return true;
94      }
95  
96      /**
97       *
98       * @param value String
99       * @return boolean
100      */
101     public static boolean isNotEmpty(String value) {
102     	return !isEmpty(value);
103     }
104 
105     /**
106      * Returns true if both strings are effectively null or whitespace,
107      * returns false otherwise if they have actual text that differs.
108      *
109      * @param a
110      * @param b
111      * @return boolean
112      */
113 	public static boolean areSemanticEquals(String a, String b) {
114 
115 		if (a==null) { return isEmpty(b); }
116 		if (b==null) { return isEmpty(a); }
117 
118 		return a.equals(b);
119 	}
120 
121     /**
122      *
123      * @param original String
124      * @param oldChar char
125      * @param newString String
126      * @return String
127      */
128     public static String replaceString(final String original, char oldChar, final String newString) {
129 		int index = original.indexOf(oldChar);
130 		if (index < 0) {
131 		    return original;
132 		} else {
133 		    final String replace = newString == null ? "" : newString;
134 		    final StringBuilder buf = new StringBuilder(Math.max(16, original.length() + replace.length()));
135 		    int last = 0;
136 		    while (index != -1) {
137 				buf.append(original.substring(last, index));
138 				buf.append(replace);
139 				last = index + 1;
140 				index = original.indexOf(oldChar, last);
141 			    }
142 		    buf.append(original.substring(last));
143 		    return buf.toString();
144 		}
145     }
146 
147     /**
148      *
149      * @param original String
150      * @param oldString String
151      * @param newString String
152      * @return String
153      */
154     public static String replaceString(final String original, final String oldString, final String newString) {
155 		int index = original.indexOf(oldString);
156 		if (index < 0) {
157 		    return original;
158 		} else {
159 		    final String replace = newString == null ? "" : newString;
160 		    final StringBuilder buf = new StringBuilder(Math.max(16, original.length() + replace.length()));
161 		    int last = 0;
162 		    while (index != -1) {
163 				buf.append(original.substring(last, index));
164 				buf.append(replace);
165 				last = index + oldString.length();
166 				index = original.indexOf(oldString, last);
167 			    }
168 		    buf.append(original.substring(last));
169 		    return buf.toString();
170 		}
171     }
172 
173     /**
174      * Appends to a StringBuilder the String src where non-ASCII and
175      * XML special chars are escaped.
176      *
177      * @param buf The destination XML stream
178      * @param src The String to append to the stream
179      */
180     public static void appendXmlEscaped(StringBuilder buf, String src) {
181         appendXmlEscaped(buf, src, SUPPORTS_UTF8);
182     }
183 
184     /**
185      * Replace some whitespace characters so they are visually apparent.
186      * 
187      * @param o
188      * @return String
189      */
190 	public static String escapeWhitespace(Object o) {
191 		
192 		if (o == null) {
193 			return null;
194 		}
195 		String s = String.valueOf(o);
196 		s = s.replace("\n", "\\n");
197 		s = s.replace("\r", "\\r");
198 		s = s.replace("\t", "\\t");
199 		return s;
200 	}
201     
202     /**
203      *
204      * @param string String
205      * @return String
206      */
207     public static String htmlEncode(String string) {
208         String encoded = replaceString(string, '&', "&amp;");
209         encoded = replaceString(encoded, '<', "&lt;");
210         return replaceString(encoded, '>', "&gt;");
211     }
212 
213     /**
214      *
215      * @param buf
216      * @param src
217      * @param supportUTF8 override the default setting, whether special characters should be replaced
218      * with entities (<code>false</code>) or should be included as is (<code>true</code>).
219      * @see #appendXmlEscaped(StringBuilder, String)
220      *
221      * TODO - unify the method above with the one below
222      *
223      * public to support unit testing - make this package private, once the unit test classes are in the same package.
224 	 */
225     public static void appendXmlEscaped(StringBuilder buf, String src, boolean supportUTF8) {
226         char c;
227         for (int i = 0; i < src.length(); i++) {
228             c = src.charAt(i);
229             if (c > '~') {// 126
230                 if (!supportUTF8) {
231                     buf.append("&#x").append(Integer.toHexString(c)).append(';');
232                 } else {
233                     buf.append(c);
234                 }
235             } else if (c == '&') {
236                 buf.append("&amp;");
237             } else if (c == '"') {
238                 buf.append("&quot;");
239             } else if (c == '<') {
240                 buf.append("&lt;");
241             } else if (c == '>') {
242                 buf.append("&gt;");
243             } else {
244                 buf.append(c);
245             }
246         }
247     }
248 
249 	/**
250 	 * Parses the input source using the delimiter specified. This method is much
251 	 * faster than using the StringTokenizer or String.split(char) approach and
252 	 * serves as a replacement for String.split() for JDK1.3 that doesn't have it.
253      *
254      * FIXME - we're on JDK 1.4 now, can we replace this with String.split?
255 	 *
256 	 * @param source String
257 	 * @param delimiter char
258 	 * @return String[]
259 	 */
260 	public static String[] substringsOf(String source, char delimiter) {
261 
262 		if (source == null || source.length() == 0) {
263             return EMPTY_STRINGS;
264         }
265 
266 		int delimiterCount = 0;
267 		int length = source.length();
268 		char[] chars = source.toCharArray();
269 
270 		for (int i=0; i<length; i++) {
271 			if (chars[i] == delimiter) {
272 			    delimiterCount++;
273 			}
274 			}
275 
276 		if (delimiterCount == 0) {
277 		    return new String[] { source };
278 		}
279 
280 		String[] results = new String[delimiterCount+1];
281 
282 		int i = 0;
283 		int offset = 0;
284 
285 		while (offset <= length) {
286 			int pos = source.indexOf(delimiter, offset);
287 			if (pos < 0) {
288 			    pos = length;
289 			}
290 			results[i++] = pos == offset ? "" : source.substring(offset, pos);
291 			offset = pos + 1;
292 			}
293 
294 		return results;
295 	}
296 
297 	/**
298 	 * Much more efficient than StringTokenizer.
299 	 *
300 	 * @param str String
301 	 * @param separator char
302 	 * @return String[]
303 	 */
304 	  public static String[] substringsOf(String str, String separator) {
305 
306 	        if (str == null || str.length() == 0) {
307 	            return EMPTY_STRINGS;
308 	        }
309 
310 	        int index = str.indexOf(separator);
311 	        if (index == -1) {
312 	            return new String[]{str};
313 	        }
314 
315 	        List<String> list = new ArrayList<String>();
316 	        int currPos = 0;
317 	        int len = separator.length();
318 	        while (index != -1) {
319 	            list.add(str.substring(currPos, index));
320 	            currPos = index + len;
321 	            index = str.indexOf(separator, currPos);
322 	        }
323 	        list.add(str.substring(currPos));
324 	        return list.toArray(new String[list.size()]);
325 	 }
326 
327 
328 	/**
329 	 * Copies the elements returned by the iterator onto the string buffer
330 	 * each delimited by the separator.
331 	 *
332 	 * @param sb StringBuffer
333 	 * @param iter Iterator
334 	 * @param separator String
335 	 */
336 	public static void asStringOn(StringBuffer sb, Iterator<?> iter, String separator) {
337 
338 	    if (!iter.hasNext()) { return;  }
339 
340 	    sb.append(iter.next());
341 
342 	    while (iter.hasNext()) {
343 	    	sb.append(separator);
344 	        sb.append(iter.next());
345 	    }
346 	}
347 
348 	/**
349 	 * Copies the array items onto the string builder each delimited by the separator.
350 	 * Does nothing if the array is null or empty.
351 	 *
352 	 * @param sb StringBuilder
353 	 * @param items Object[]
354 	 * @param separator String
355 	 */
356 	public static void asStringOn(StringBuilder sb, Object[] items, String separator) {
357 
358 	    if (items == null || items.length == 0) { return;  }
359 
360 	    sb.append(items[0]);
361 
362 	    for (int i=1; i<items.length; i++) {
363 	    	sb.append(separator);
364 	        sb.append(items[i]);
365 	    }
366 	}
367 
368 	/**
369 	 * Return the length of the shortest string in the array.
370 	 * If the collection is empty or any one of them is
371 	 * null then it returns 0.
372 	 *
373 	 * @param strings String[]
374 	 * @return int
375 	 */
376 	public static int lengthOfShortestIn(String[] strings) {
377 
378 	    if (CollectionUtil.isEmpty(strings)) { return 0; }
379 
380 		int minLength = Integer.MAX_VALUE;
381 
382 		for (int i=0; i<strings.length; i++) {
383 			if (strings[i] == null) {
384 			    return 0;
385 			}
386 			minLength = Math.min(minLength, strings[i].length());
387 		}
388 
389 		return minLength;
390 	}
391 
392 	/**
393 	 * Determine the maximum number of common leading whitespace characters
394 	 * the strings share in the same sequence. Useful for determining how
395 	 * many leading characters can be removed to shift all the text in the
396 	 * strings to the left without misaligning them.
397 	 *
398 	 * @param strings String[]
399 	 * @return int
400 	 */
401 	public static int maxCommonLeadingWhitespaceForAll(String[] strings) {
402 
403 		int shortest = lengthOfShortestIn(strings);
404 		if (shortest == 0) {
405 		    return 0;
406 		}
407 
408 		char[] matches = new char[shortest];
409 
410 		String str;
411 		for (int m=0; m<matches.length; m++) {
412 			matches[m] = strings[0].charAt(m);
413 			if (!Character.isWhitespace(matches[m])) {
414 			    return m;
415 			}
416 			for (int i=0; i<strings.length; i++) {
417 				str = strings[i];
418 				if (str.charAt(m) != matches[m]) {
419 				    return m;
420 				}
421 				}
422 		}
423 
424 		return shortest;
425 	}
426 
427 	/**
428 	 * Trims off the leading characters off the strings up to the trimDepth
429 	 * specified. Returns the same strings if trimDepth = 0
430 	 *
431 	 * @param strings
432 	 * @param trimDepth
433 	 * @return String[]
434 	 */
435 	public static String[] trimStartOn(String[] strings, int trimDepth) {
436 
437 		if (trimDepth == 0) {
438 		    return strings;
439 		}
440 
441 		String[] results = new String[strings.length];
442 		for (int i=0; i<strings.length; i++) {
443 			results[i] = strings[i].substring(trimDepth);
444 		}
445 		return results;
446     }
447 
448     /**
449      * Left pads a string.
450      * @param s The String to pad
451      * @param length The desired minimum length of the resulting padded String
452      * @return The resulting left padded String
453      */
454     public static String lpad(String s, int length) {
455          String res = s;
456          if (length - s.length() > 0) {
457              char [] arr = new char[length - s.length()];
458              java.util.Arrays.fill(arr, ' ');
459              res = new StringBuilder(length).append(arr).append(s).toString();
460          }
461          return res;
462     }
463 
464     /**
465      * Are the two String values the same.
466      * The Strings can be optionally trimmed before checking.
467      * The Strings can be optionally compared ignoring case.
468      * The Strings can be have embedded whitespace standardized before comparing.
469      * Two null values are treated as equal.
470      *
471      * @param s1 The first String.
472      * @param s2 The second String.
473      * @param trim Indicates if the Strings should be trimmed before comparison.
474      * @param ignoreCase Indicates if the case of the Strings should ignored during comparison.
475      * @param standardizeWhitespace Indicates if the embedded whitespace should be standardized before comparison.
476      * @return <code>true</code> if the Strings are the same, <code>false</code> otherwise.
477      */
478     @SuppressWarnings("PMD.CompareObjectsWithEquals")
479     public static boolean isSame(String s1, String s2, boolean trim, boolean ignoreCase, boolean standardizeWhitespace) {
480 		if (s1 == s2) {
481 			return true;
482 		} else if (s1 == null || s2 == null) {
483 			return false;
484 		} else {
485 			if (trim) {
486 				s1 = s1.trim();
487 				s2 = s2.trim();
488 			}
489 			if (standardizeWhitespace) {
490 				// Replace all whitespace with a standard single space character.
491 				s1 = s1.replaceAll("\\s+", " ");
492 				s2 = s2.replaceAll("\\s+", " ");
493 			}
494 			return ignoreCase ? s1.equalsIgnoreCase(s2) : s1.equals(s2);
495 		}
496     }
497 
498 	/**
499 	 * Formats all items onto a string with separators if more than one
500 	 * exists, return an empty string if the items are null or empty.
501 	 *
502 	 * @param items Object[]
503 	 * @param separator String
504 	 * @return String
505 	 */
506 	public static String asString(Object[] items, String separator) {
507 
508 		if (items == null || items.length == 0) { return ""; }
509 		if (items.length == 1) { return items[0].toString(); }
510 
511 		StringBuilder sb = new StringBuilder(items[0].toString());
512 		for (int i=1; i<items.length; i++) {
513 			sb.append(separator).append(items[i]);
514 		}
515 
516 		return sb.toString();
517 	}
518 }