James Moger
2012-09-10 fabe060d3a435f116128851f828e35c2af5fde67
commit | author | age
f13c4c 1 /*
JM 2  * Copyright 2011 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
87cc1e 16 package com.gitblit.utils;
JM 17
b34048 18 import java.io.ByteArrayOutputStream;
87cc1e 19 import java.io.UnsupportedEncodingException;
ae9e15 20 import java.nio.ByteBuffer;
JM 21 import java.nio.CharBuffer;
22 import java.nio.charset.CharacterCodingException;
23 import java.nio.charset.Charset;
24 import java.nio.charset.CharsetDecoder;
25 import java.nio.charset.IllegalCharsetNameException;
26 import java.nio.charset.UnsupportedCharsetException;
87cc1e 27 import java.security.MessageDigest;
JM 28 import java.security.NoSuchAlgorithmException;
f339f5 29 import java.util.ArrayList;
ae9e15 30 import java.util.Arrays;
0b9119 31 import java.util.Collection;
94750e 32 import java.util.Collections;
JM 33 import java.util.Comparator;
ae9e15 34 import java.util.LinkedHashSet;
87cc1e 35 import java.util.List;
ae9e15 36 import java.util.Set;
eb870f 37 import java.util.regex.Matcher;
JM 38 import java.util.regex.Pattern;
f339f5 39 import java.util.regex.PatternSyntaxException;
8c9a20 40
d9f687 41 /**
JM 42  * Utility class of string functions.
43  * 
44  * @author James Moger
45  * 
46  */
87cc1e 47 public class StringUtils {
8c9a20 48
JM 49     public static final String MD5_TYPE = "MD5:";
309c55 50
d5623a 51     public static final String COMBINED_MD5_TYPE = "CMD5:";
3e087a 52
d9f687 53     /**
JM 54      * Returns true if the string is null or empty.
55      * 
56      * @param value
57      * @return true if string is null or empty
58      */
87cc1e 59     public static boolean isEmpty(String value) {
JM 60         return value == null || value.trim().length() == 0;
61     }
62
d9f687 63     /**
JM 64      * Replaces carriage returns and line feeds with html line breaks.
65      * 
66      * @param string
67      * @return plain text with html line breaks
68      */
87cc1e 69     public static String breakLinesForHtml(String string) {
JM 70         return string.replace("\r\n", "<br/>").replace("\r", "<br/>").replace("\n", "<br/>");
71     }
72
d9f687 73     /**
JM 74      * Prepare text for html presentation. Replace sensitive characters with
75      * html entities.
76      * 
77      * @param inStr
78      * @param changeSpace
79      * @return plain text escaped for html
80      */
87cc1e 81     public static String escapeForHtml(String inStr, boolean changeSpace) {
8a53c0 82         StringBuilder retStr = new StringBuilder();
87cc1e 83         int i = 0;
JM 84         while (i < inStr.length()) {
85             if (inStr.charAt(i) == '&') {
86                 retStr.append("&amp;");
87             } else if (inStr.charAt(i) == '<') {
88                 retStr.append("&lt;");
89             } else if (inStr.charAt(i) == '>') {
90                 retStr.append("&gt;");
91             } else if (inStr.charAt(i) == '\"') {
92                 retStr.append("&quot;");
93             } else if (changeSpace && inStr.charAt(i) == ' ') {
94                 retStr.append("&nbsp;");
95             } else if (changeSpace && inStr.charAt(i) == '\t') {
96                 retStr.append(" &nbsp; &nbsp;");
8c9a20 97             } else {
JM 98                 retStr.append(inStr.charAt(i));
99             }
100             i++;
101         }
102         return retStr.toString();
103     }
104
d9f687 105     /**
JM 106      * Decode html entities back into plain text characters.
107      * 
108      * @param inStr
109      * @return returns plain text from html
110      */
85c2e6 111     public static String decodeFromHtml(String inStr) {
JM 112         return inStr.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">")
113                 .replace("&quot;", "\"").replace("&nbsp;", " ");
114     }
115
d9f687 116     /**
JM 117      * Encodes a url parameter by escaping troublesome characters.
118      * 
119      * @param inStr
120      * @return properly escaped url
121      */
8c9a20 122     public static String encodeURL(String inStr) {
8a53c0 123         StringBuilder retStr = new StringBuilder();
8c9a20 124         int i = 0;
JM 125         while (i < inStr.length()) {
126             if (inStr.charAt(i) == '/') {
127                 retStr.append("%2F");
128             } else if (inStr.charAt(i) == ' ') {
129                 retStr.append("%20");
2a7306 130             } else {
87cc1e 131                 retStr.append(inStr.charAt(i));
2a7306 132             }
87cc1e 133             i++;
JM 134         }
135         return retStr.toString();
136     }
137
d9f687 138     /**
JM 139      * Flatten the list of strings into a single string with a space separator.
140      * 
141      * @param values
142      * @return flattened list
143      */
0b9119 144     public static String flattenStrings(Collection<String> values) {
8a2e9c 145         return flattenStrings(values, " ");
JM 146     }
147
d9f687 148     /**
JM 149      * Flatten the list of strings into a single string with the specified
150      * separator.
151      * 
152      * @param values
153      * @param separator
154      * @return flattened list
155      */
0b9119 156     public static String flattenStrings(Collection<String> values, String separator) {
87cc1e 157         StringBuilder sb = new StringBuilder();
JM 158         for (String value : values) {
8a2e9c 159             sb.append(value).append(separator);
87cc1e 160         }
6e666f 161         if (sb.length() > 0) {
JM 162             // truncate trailing separator
163             sb.setLength(sb.length() - separator.length());
164         }
87cc1e 165         return sb.toString().trim();
JM 166     }
167
d9f687 168     /**
JM 169      * Returns a string trimmed to a maximum length with trailing ellipses. If
170      * the string length is shorter than the max, the original string is
171      * returned.
172      * 
173      * @param value
174      * @param max
175      * @return trimmed string
176      */
87cc1e 177     public static String trimString(String value, int max) {
JM 178         if (value.length() <= max) {
179             return value;
180         }
181         return value.substring(0, max - 3) + "...";
182     }
183
d9f687 184     /**
JM 185      * Left pad a string with the specified character, if the string length is
186      * less than the specified length.
187      * 
188      * @param input
189      * @param length
190      * @param pad
191      * @return left-padded string
192      */
87cc1e 193     public static String leftPad(String input, int length, char pad) {
JM 194         if (input.length() < length) {
195             StringBuilder sb = new StringBuilder();
196             for (int i = 0, len = length - input.length(); i < len; i++) {
197                 sb.append(pad);
198             }
199             sb.append(input);
200             return sb.toString();
201         }
202         return input;
203     }
204
d9f687 205     /**
JM 206      * Right pad a string with the specified character, if the string length is
207      * less then the specified length.
208      * 
209      * @param input
210      * @param length
211      * @param pad
212      * @return right-padded string
213      */
87cc1e 214     public static String rightPad(String input, int length, char pad) {
JM 215         if (input.length() < length) {
216             StringBuilder sb = new StringBuilder();
217             sb.append(input);
218             for (int i = 0, len = length - input.length(); i < len; i++) {
219                 sb.append(pad);
220             }
221             return sb.toString();
222         }
223         return input;
224     }
225
d9f687 226     /**
JM 227      * Calculates the SHA1 of the string.
228      * 
229      * @param text
230      * @return sha1 of the string
231      */
87cc1e 232     public static String getSHA1(String text) {
JM 233         try {
234             byte[] bytes = text.getBytes("iso-8859-1");
235             return getSHA1(bytes);
236         } catch (UnsupportedEncodingException u) {
237             throw new RuntimeException(u);
238         }
239     }
240
d9f687 241     /**
JM 242      * Calculates the SHA1 of the byte array.
243      * 
244      * @param bytes
245      * @return sha1 of the byte array
246      */
87cc1e 247     public static String getSHA1(byte[] bytes) {
JM 248         try {
249             MessageDigest md = MessageDigest.getInstance("SHA-1");
250             md.update(bytes, 0, bytes.length);
8c9a20 251             byte[] digest = md.digest();
JM 252             return toHex(digest);
87cc1e 253         } catch (NoSuchAlgorithmException t) {
JM 254             throw new RuntimeException(t);
f97bf0 255         }
8c9a20 256     }
JM 257
d9f687 258     /**
JM 259      * Calculates the MD5 of the string.
260      * 
261      * @param string
262      * @return md5 of the string
263      */
8c9a20 264     public static String getMD5(String string) {
JM 265         try {
266             MessageDigest md = MessageDigest.getInstance("MD5");
267             md.reset();
268             md.update(string.getBytes("iso-8859-1"));
269             byte[] digest = md.digest();
270             return toHex(digest);
5450d0 271         } catch (UnsupportedEncodingException u) {
JM 272             throw new RuntimeException(u);
273         } catch (NoSuchAlgorithmException t) {
274             throw new RuntimeException(t);
8c9a20 275         }
JM 276     }
277
d9f687 278     /**
JM 279      * Returns the hex representation of the byte array.
280      * 
281      * @param bytes
282      * @return byte array as hex string
283      */
8c9a20 284     private static String toHex(byte[] bytes) {
JM 285         StringBuilder sb = new StringBuilder(bytes.length * 2);
286         for (int i = 0; i < bytes.length; i++) {
287             if (((int) bytes[i] & 0xff) < 0x10) {
288                 sb.append('0');
289             }
290             sb.append(Long.toString((int) bytes[i] & 0xff, 16));
291         }
292         return sb.toString();
293     }
85c2e6 294
d9f687 295     /**
JM 296      * Returns the root path of the specified path. Returns a blank string if
297      * there is no root path.
298      * 
299      * @param path
300      * @return root path or blank
301      */
00afd7 302     public static String getRootPath(String path) {
JM 303         if (path.indexOf('/') > -1) {
ec97f7 304             return path.substring(0, path.lastIndexOf('/'));
00afd7 305         }
JM 306         return "";
307     }
008322 308
d9f687 309     /**
JM 310      * Returns the path remainder after subtracting the basePath from the
311      * fullPath.
312      * 
313      * @param basePath
314      * @param fullPath
315      * @return the relative path
316      */
008322 317     public static String getRelativePath(String basePath, String fullPath) {
a125cf 318         String relativePath = fullPath.substring(basePath.length()).replace('\\', '/');
JM 319         if (relativePath.charAt(0) == '/') {
320             relativePath = relativePath.substring(1);
321         }
322         return relativePath;
323     }
8c9a20 324
d9f687 325     /**
JM 326      * Splits the space-separated string into a list of strings.
327      * 
328      * @param value
329      * @return list of strings
330      */
f339f5 331     public static List<String> getStringsFromValue(String value) {
JM 332         return getStringsFromValue(value, " ");
333     }
8c9a20 334
d9f687 335     /**
JM 336      * Splits the string into a list of string by the specified separator.
337      * 
338      * @param value
339      * @param separator
340      * @return list of strings
341      */
f339f5 342     public static List<String> getStringsFromValue(String value, String separator) {
3d699c 343         List<String> strings = new ArrayList<String>();
U 344         try {
345             String[] chunks = value.split(separator + "(?=([^\"]*\"[^\"]*\")*[^\"]*$)");            
346             for (String chunk : chunks) {
347                 chunk = chunk.trim();
348                 if (chunk.length() > 0) {
349                     if (chunk.charAt(0) == '"' && chunk.charAt(chunk.length() - 1) == '"') {
350                         // strip double quotes
351                         chunk = chunk.substring(1, chunk.length() - 1).trim();
352                     }
353                     strings.add(chunk);
354                 }
355             }
356         } catch (PatternSyntaxException e) {
357             throw new RuntimeException(e);
358         }
359         return strings;
360     }
831469 361
JM 362     /**
363      * Validates that a name is composed of letters, digits, or limited other
364      * characters.
365      * 
366      * @param name
367      * @return the first invalid character found or null if string is acceptable
368      */
369     public static Character findInvalidCharacter(String name) {
370         char[] validChars = { '/', '.', '_', '-' };
371         for (char c : name.toCharArray()) {
372             if (!Character.isLetterOrDigit(c)) {
373                 boolean ok = false;
374                 for (char vc : validChars) {
375                     ok |= c == vc;
376                 }
377                 if (!ok) {
378                     return c;
379                 }
380             }
381         }
382         return null;
383     }
384
385     /**
386      * Simple fuzzy string comparison. This is a case-insensitive check. A
387      * single wildcard * value is supported.
388      * 
389      * @param value
390      * @param pattern
391      * @return true if the value matches the pattern
392      */
393     public static boolean fuzzyMatch(String value, String pattern) {
394         if (value.equalsIgnoreCase(pattern)) {
395             return true;
396         }
397         if (pattern.contains("*")) {
398             boolean prefixMatches = false;
399             boolean suffixMatches = false;
400
401             int wildcard = pattern.indexOf('*');
402             String prefix = pattern.substring(0, wildcard).toLowerCase();
403             prefixMatches = value.toLowerCase().startsWith(prefix);
404
405             if (pattern.length() > (wildcard + 1)) {
406                 String suffix = pattern.substring(wildcard + 1).toLowerCase();
407                 suffixMatches = value.toLowerCase().endsWith(suffix);
408                 return prefixMatches && suffixMatches;
409             }
410             return prefixMatches || suffixMatches;
411         }
412         return false;
413     }
94750e 414
JM 415     /**
416      * Compare two repository names for proper group sorting.
417      * 
418      * @param r1
419      * @param r2
420      * @return
421      */
422     public static int compareRepositoryNames(String r1, String r2) {
423         // sort root repositories first, alphabetically
424         // then sort grouped repositories, alphabetically
425         int s1 = r1.indexOf('/');
426         int s2 = r2.indexOf('/');
427         if (s1 == -1 && s2 == -1) {
428             // neither grouped
429             return r1.compareTo(r2);
430         } else if (s1 > -1 && s2 > -1) {
431             // both grouped
432             return r1.compareTo(r2);
433         } else if (s1 == -1) {
434             return -1;
435         } else if (s2 == -1) {
436             return 1;
437         }
438         return 0;
439     }
440
441     /**
442      * Sort grouped repository names.
443      * 
444      * @param list
445      */
446     public static void sortRepositorynames(List<String> list) {
447         Collections.sort(list, new Comparator<String>() {
448             @Override
449             public int compare(String o1, String o2) {
450                 return compareRepositoryNames(o1, o2);
451             }
452         });
453     }
309c55 454
JM 455     public static String getColor(String value) {
456         int cs = 0;
457         for (char c : getMD5(value.toLowerCase()).toCharArray()) {
458             cs += c;
459         }
460         int n = (cs % 360);        
461         float hue = ((float) n) / 360;
462         return hsvToRgb(hue, 0.90f, 0.65f);
463     }
464
465     public static String hsvToRgb(float hue, float saturation, float value) {
466         int h = (int) (hue * 6);
467         float f = hue * 6 - h;
468         float p = value * (1 - saturation);
469         float q = value * (1 - f * saturation);
470         float t = value * (1 - (1 - f) * saturation);
471
472         switch (h) {
473         case 0:
474             return rgbToString(value, t, p);
475         case 1:
476             return rgbToString(q, value, p);
477         case 2:
478             return rgbToString(p, value, t);
479         case 3:
480             return rgbToString(p, q, value);
481         case 4:
482             return rgbToString(t, p, value);
483         case 5:
484             return rgbToString(value, p, q);
485         default:
486             throw new RuntimeException(
487                     "Something went wrong when converting from HSV to RGB. Input was " + hue + ", "
488                             + saturation + ", " + value);
489         }
490     }
491
492     public static String rgbToString(float r, float g, float b) {
493         String rs = Integer.toHexString((int) (r * 256));
494         String gs = Integer.toHexString((int) (g * 256));
495         String bs = Integer.toHexString((int) (b * 256));
496         return "#" + rs + gs + bs;
497     }
6c6fbf 498     
12c31e 499     /**
JM 500      * Strips a trailing ".git" from the value.
501      * 
502      * @param value
503      * @return a stripped value or the original value if .git is not found
504      */
6c6fbf 505     public static String stripDotGit(String value) {
JM 506         if (value.toLowerCase().endsWith(".git")) {
507             return value.substring(0, value.length() - 4);
508         }
509         return value;
510     }
672296 511     
12c31e 512     /**
JM 513      * Count the number of lines in a string.
514      * 
515      * @param value
516      * @return the line count
517      */
672296 518     public static int countLines(String value) {
JM 519         if (isEmpty(value)) {
520             return 0;
521         }
522         return value.split("\n").length;
523     }
12c31e 524     
JM 525     /**
526      * Returns the file extension of a path.
527      * 
528      * @param path
529      * @return a blank string or a file extension
530      */
531     public static String getFileExtension(String path) {
532         int lastDot = path.lastIndexOf('.');
533         if (lastDot > -1) {
534             return path.substring(lastDot + 1);
535         }
536         return "";
537     }
f3b625 538     
JC 539     /**
540      * Replace all occurences of a substring within a string with
541      * another string.
542      * 
543      * From Spring StringUtils.
544      * 
545      * @param inString String to examine
546      * @param oldPattern String to replace
547      * @param newPattern String to insert
548      * @return a String with the replacements
549      */
550     public static String replace(String inString, String oldPattern, String newPattern) {
551         StringBuilder sb = new StringBuilder();
552         int pos = 0; // our position in the old string
553         int index = inString.indexOf(oldPattern);
554         // the index of an occurrence we've found, or -1
555         int patLen = oldPattern.length();
556         while (index >= 0) {
557             sb.append(inString.substring(pos, index));
558             sb.append(newPattern);
559             pos = index + patLen;
560             index = inString.indexOf(oldPattern, pos);
561         }
562         sb.append(inString.substring(pos));
563         // remember to append any characters to the right of a match
564         return sb.toString();
565     }
ae9e15 566     
JM 567     /**
568      * Decodes a string by trying several charsets until one does not throw a
569      * coding exception.  Last resort is to interpret as UTF-8 with illegal
570      * character substitution.
571      * 
572      * @param content
573      * @param charsets optional
574      * @return a string
575      */
576     public static String decodeString(byte [] content, String... charsets) {
577         Set<String> sets = new LinkedHashSet<String>();
578         if (!ArrayUtils.isEmpty(charsets)) {
579             sets.addAll(Arrays.asList(charsets));
580         }
086c04 581         String value = null;
ae9e15 582         sets.addAll(Arrays.asList("UTF-8", "ISO-8859-1", Charset.defaultCharset().name()));
JM 583         for (String charset : sets) {
584             try {
585                 Charset cs = Charset.forName(charset);
586                 CharsetDecoder decoder = cs.newDecoder();
587                 CharBuffer buffer = decoder.decode(ByteBuffer.wrap(content));
086c04 588                 value = buffer.toString();
JM 589                 break;
ae9e15 590             } catch (CharacterCodingException e) {
JM 591                 // ignore and advance to the next charset
592             } catch (IllegalCharsetNameException e) {
593                 // ignore illegal charset names
594             } catch (UnsupportedCharsetException e) {
595                 // ignore unsupported charsets
596             }
597         }
086c04 598         if (value.startsWith("\uFEFF")) {
JM 599             // strip UTF-8 BOM
600             return value.substring(1);
601         }
602         return value;
ae9e15 603     }
eb870f 604     
JM 605     /**
606      * Attempt to extract a repository name from a given url using regular
607      * expressions.  If no match is made, then return whatever trails after
608      * the final / character.
609      * 
610      * @param regexUrls
611      * @return a repository path
612      */
613     public static String extractRepositoryPath(String url, String... urlpatterns) {
614         for (String urlPattern : urlpatterns) {
615             Pattern p = Pattern.compile(urlPattern);
616             Matcher m = p.matcher(url);
617             while (m.find()) {
618                 String repositoryPath = m.group(1);
619                 return repositoryPath;
620             }
621         }
622         // last resort
623         if (url.lastIndexOf('/') > -1) {
624             return url.substring(url.lastIndexOf('/') + 1);
625         }
626         return url;
627     }
b34048 628     
JM 629     /**
630      * Converts a string with \nnn sequences into a UTF-8 encoded string.
631      * @param input
632      * @return
633      */
634     public static String convertOctal(String input) {
635         try {
636             ByteArrayOutputStream bytes = new ByteArrayOutputStream();
637             Pattern p = Pattern.compile("(\\\\\\d{3})");
638             Matcher m = p.matcher(input);
639             int i = 0;
640             while (m.find()) {
641                 bytes.write(input.substring(i, m.start()).getBytes("UTF-8"));
642                 // replace octal encoded value
643                 // strip leading \ character
644                 String oct = m.group().substring(1);
645                 bytes.write(Integer.parseInt(oct, 8));
646                 i = m.end();            
647             }
648             if (bytes.size() == 0) {
649                 // no octal matches
650                 return input;
651             } else {
652                 if (i < input.length()) {
653                     // add remainder of string
654                     bytes.write(input.substring(i).getBytes("UTF-8"));
655                 }
656             }
657             return bytes.toString("UTF-8");
658         } catch (Exception e) {
659             e.printStackTrace();
660         }
661         return input;
662     }
309c55 663 }