You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

StringUtils.java 19KB

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