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