001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.util; 029 030import org.opencms.file.CmsResource; 031import org.opencms.i18n.CmsEncoder; 032import org.opencms.i18n.I_CmsMessageBundle; 033import org.opencms.json.JSONException; 034import org.opencms.json.JSONObject; 035import org.opencms.main.CmsIllegalArgumentException; 036import org.opencms.main.CmsLog; 037import org.opencms.main.OpenCms; 038 039import java.awt.Color; 040import java.net.InetAddress; 041import java.net.NetworkInterface; 042import java.nio.charset.Charset; 043import java.util.ArrayList; 044import java.util.Collection; 045import java.util.Comparator; 046import java.util.HashMap; 047import java.util.Iterator; 048import java.util.LinkedHashMap; 049import java.util.List; 050import java.util.Locale; 051import java.util.Map; 052import java.util.regex.Matcher; 053import java.util.regex.Pattern; 054import java.util.regex.PatternSyntaxException; 055 056import org.apache.commons.logging.Log; 057import org.apache.oro.text.perl.MalformedPerl5PatternException; 058import org.apache.oro.text.perl.Perl5Util; 059 060import com.cybozu.labs.langdetect.Detector; 061import com.cybozu.labs.langdetect.DetectorFactory; 062import com.cybozu.labs.langdetect.LangDetectException; 063import com.google.common.base.Optional; 064 065/** 066 * Provides String utility functions.<p> 067 * 068 * @since 6.0.0 069 */ 070public final class CmsStringUtil { 071 072 /** 073 * Compares two Strings according to the count of containing slashes.<p> 074 * 075 * If both Strings contain the same count of slashes the Strings are compared.<p> 076 */ 077 public static class CmsSlashComparator implements Comparator<String> { 078 079 /** 080 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) 081 */ 082 public int compare(String a, String b) { 083 084 int slashCountA = countChar(a, '/'); 085 int slashCountB = countChar(b, '/'); 086 087 if (slashCountA < slashCountB) { 088 return 1; 089 } else if (slashCountA == slashCountB) { 090 return a.compareTo(b); 091 } else { 092 return -1; 093 } 094 } 095 } 096 097 /** Regular expression that matches the HTML body end tag. */ 098 public static final String BODY_END_REGEX = "<\\s*/\\s*body[^>]*>"; 099 100 /** Regular expression that matches the HTML body start tag. */ 101 public static final String BODY_START_REGEX = "<\\s*body[^>]*>"; 102 103 /** Constant for <code>"false"</code>. */ 104 public static final String FALSE = Boolean.toString(false); 105 106 /** a convenient shorthand to the line separator constant. */ 107 public static final String LINE_SEPARATOR = System.getProperty("line.separator"); 108 109 /** Context macro. */ 110 public static final String MACRO_OPENCMS_CONTEXT = "${OpenCmsContext}"; 111 112 /** Pattern to determine a locale for suffixes like '_de' or '_en_US'. */ 113 public static final Pattern PATTERN_LOCALE_SUFFIX = Pattern.compile( 114 "(.*)_([a-z]{2}(?:_[A-Z]{2})?)(?:\\.[^\\.]*)?$"); 115 116 /** Pattern to determine the document number for suffixes like '_0001'. */ 117 public static final Pattern PATTERN_NUMBER_SUFFIX = Pattern.compile("(.*)_(\\d+)(\\.[^\\.^\\n]*)?$"); 118 119 /** Pattern matching one or more slashes. */ 120 public static final Pattern PATTERN_SLASHES = Pattern.compile("/+"); 121 122 /** The place holder end sign in the pattern. */ 123 public static final String PLACEHOLDER_END = "}"; 124 125 /** The place holder start sign in the pattern. */ 126 public static final String PLACEHOLDER_START = "{"; 127 128 /** Contains all chars that end a sentence in the {@link #trimToSize(String, int, int, String)} method. */ 129 public static final char[] SENTENCE_ENDING_CHARS = {'.', '!', '?'}; 130 131 /** a convenient shorthand for tabulations. */ 132 public static final String TABULATOR = " "; 133 134 /** Constant for <code>"true"</code>. */ 135 public static final String TRUE = Boolean.toString(true); 136 137 /** Regex pattern that matches an end body tag. */ 138 private static final Pattern BODY_END_PATTERN = Pattern.compile(BODY_END_REGEX, Pattern.CASE_INSENSITIVE); 139 140 /** Regex pattern that matches a start body tag. */ 141 private static final Pattern BODY_START_PATTERN = Pattern.compile(BODY_START_REGEX, Pattern.CASE_INSENSITIVE); 142 143 /** Day constant. */ 144 private static final long DAYS = 1000 * 60 * 60 * 24; 145 146 /** Hour constant. */ 147 private static final long HOURS = 1000 * 60 * 60; 148 149 /** The log object for this class. */ 150 private static final Log LOG = CmsLog.getLog(CmsStringUtil.class); 151 152 /** OpenCms context replace String, static for performance reasons. */ 153 private static String m_contextReplace; 154 155 /** OpenCms context search String, static for performance reasons. */ 156 private static String m_contextSearch; 157 158 /** Minute constant. */ 159 private static final long MINUTES = 1000 * 60; 160 161 /** Second constant. */ 162 private static final long SECONDS = 1000; 163 164 /** Regex that matches an encoding String in an xml head. */ 165 private static final Pattern XML_ENCODING_REGEX = Pattern.compile( 166 "encoding\\s*=\\s*[\"'].+[\"']", 167 Pattern.CASE_INSENSITIVE); 168 169 /** Regex that matches an xml head. */ 170 private static final Pattern XML_HEAD_REGEX = Pattern.compile("<\\s*\\?.*\\?\\s*>", Pattern.CASE_INSENSITIVE); 171 172 /** Units used for duration parsing. */ 173 private static final String[] DURATION_UNTIS = {"d", "h", "m", "s", "ms"}; 174 175 /** Multipliers used for duration parsing. */ 176 private static final long[] DURATION_MULTIPLIERS = {24L * 60 * 60 * 1000, 60L * 60 * 1000, 60L * 1000, 1000L, 1L}; 177 178 /** Number and unit pattern for duration parsing. */ 179 private static final Pattern DURATION_NUMBER_AND_UNIT_PATTERN = Pattern.compile("([0-9]+)([a-z]+)"); 180 181 /** 182 * Default constructor (empty), private because this class has only 183 * static methods.<p> 184 */ 185 private CmsStringUtil() { 186 187 // empty 188 } 189 190 /** 191 * Adds leading and trailing slashes to a path, 192 * if the path does not already start or end with a slash.<p> 193 * 194 * <b>Directly exposed for JSP EL<b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 195 * 196 * @param path the path to which add the slashes 197 * 198 * @return the path with added leading and trailing slashes 199 */ 200 public static String addLeadingAndTrailingSlash(String path) { 201 202 StringBuffer buffer1 = new StringBuffer(); 203 if (!path.startsWith("/")) { 204 buffer1.append("/"); 205 } 206 buffer1.append(path); 207 if (!path.endsWith("/")) { 208 buffer1.append("/"); 209 } 210 return buffer1.toString(); 211 } 212 213 /** 214 * Returns a string representation for the given array using the given separator.<p> 215 * 216 * @param arg the array to transform to a String 217 * @param separator the item separator 218 * 219 * @return the String of the given array 220 */ 221 public static String arrayAsString(final String[] arg, String separator) { 222 223 StringBuffer result = new StringBuffer(); 224 for (int i = 0; i < arg.length; i++) { 225 result.append(arg[i]); 226 if ((i + 1) < arg.length) { 227 result.append(separator); 228 } 229 } 230 return result.toString(); 231 } 232 233 /** 234 * Changes the given filenames suffix from the current suffix to the provided suffix. 235 * 236 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 237 * 238 * @param filename the filename to be changed 239 * @param suffix the new suffix of the file 240 * 241 * @return the filename with the replaced suffix 242 */ 243 public static String changeFileNameSuffixTo(String filename, String suffix) { 244 245 int dotPos = filename.lastIndexOf('.'); 246 if (dotPos != -1) { 247 return filename.substring(0, dotPos + 1) + suffix; 248 } else { 249 // the string has no suffix 250 return filename; 251 } 252 } 253 254 /** 255 * Checks if a given name is composed only of the characters <code>a...z,A...Z,0...9</code> 256 * and the provided <code>constraints</code>.<p> 257 * 258 * If the check fails, an Exception is generated. The provided bundle and key is 259 * used to generate the Exception. 4 parameters are passed to the Exception:<ol> 260 * <li>The <code>name</code> 261 * <li>The first illegal character found 262 * <li>The position where the illegal character was found 263 * <li>The <code>constraints</code></ol> 264 * 265 * @param name the name to check 266 * @param constraints the additional character constraints 267 * @param key the key to use for generating the Exception (if required) 268 * @param bundle the bundle to use for generating the Exception (if required) 269 * 270 * @throws CmsIllegalArgumentException if the check fails (generated from the given key and bundle) 271 */ 272 public static void checkName(String name, String constraints, String key, I_CmsMessageBundle bundle) 273 throws CmsIllegalArgumentException { 274 275 int l = name.length(); 276 for (int i = 0; i < l; i++) { 277 char c = name.charAt(i); 278 if (((c < 'a') || (c > 'z')) 279 && ((c < '0') || (c > '9')) 280 && ((c < 'A') || (c > 'Z')) 281 && (constraints.indexOf(c) < 0)) { 282 283 throw new CmsIllegalArgumentException( 284 bundle.container(key, new Object[] {name, new Character(c), new Integer(i), constraints})); 285 } 286 } 287 } 288 289 /** 290 * Returns a string representation for the given collection using the given separator.<p> 291 * 292 * @param collection the collection to print 293 * @param separator the item separator 294 * 295 * @return the string representation for the given collection 296 */ 297 public static String collectionAsString(Collection<?> collection, String separator) { 298 299 StringBuffer string = new StringBuffer(128); 300 Iterator<?> it = collection.iterator(); 301 while (it.hasNext()) { 302 string.append(it.next()); 303 if (it.hasNext()) { 304 string.append(separator); 305 } 306 } 307 return string.toString(); 308 } 309 310 /** 311 * Compares two paths, ignoring leading and trailing slashes.<p> 312 * 313 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 314 * 315 * @param path1 the first path 316 * @param path2 the second path 317 * 318 * @return true if the paths are equal (ignoring leading and trailing slashes) 319 */ 320 public static boolean comparePaths(String path1, String path2) { 321 322 return addLeadingAndTrailingSlash(path1).equals(addLeadingAndTrailingSlash(path2)); 323 } 324 325 /** 326 * Counts the occurrence of a given char in a given String.<p> 327 * 328 * @param s the string 329 * @param c the char to count 330 * 331 * @return returns the count of occurrences of a given char in a given String 332 */ 333 public static int countChar(String s, char c) { 334 335 int counter = 0; 336 for (int i = 0; i < s.length(); i++) { 337 if (s.charAt(i) == c) { 338 counter++; 339 } 340 } 341 return counter; 342 } 343 344 /** 345 * Returns a String array representation for the given enum.<p> 346 * 347 * @param <T> the type of the enum 348 * @param values the enum values 349 * 350 * @return the representing String array 351 */ 352 public static <T extends Enum<T>> String[] enumNameToStringArray(T[] values) { 353 354 int i = 0; 355 String[] result = new String[values.length]; 356 for (T value : values) { 357 result[i++] = value.name(); 358 } 359 return result; 360 } 361 362 /** 363 * Replaces line breaks to <code><br/></code> and HTML control characters 364 * like <code>< > & "</code> with their HTML entity representation.<p> 365 * 366 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 367 * 368 * @param source the String to escape 369 * 370 * @return the escaped String 371 */ 372 public static String escapeHtml(String source) { 373 374 if (source == null) { 375 return null; 376 } 377 source = CmsEncoder.escapeXml(source); 378 source = CmsStringUtil.substitute(source, "\r", ""); 379 source = CmsStringUtil.substitute(source, "\n", "<br/>\n"); 380 return source; 381 } 382 383 /** 384 * Escapes a String so it may be used in JavaScript String definitions.<p> 385 * 386 * This method escapes 387 * line breaks (<code>\r\n,\n</code>) quotation marks (<code>".'</code>) 388 * and slash as well as backspace characters (<code>\,/</code>).<p> 389 * 390 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 391 * 392 * @param source the String to escape 393 * 394 * @return the escaped String 395 */ 396 public static String escapeJavaScript(String source) { 397 398 source = CmsStringUtil.substitute(source, "\\", "\\\\"); 399 source = CmsStringUtil.substitute(source, "\"", "\\\""); 400 source = CmsStringUtil.substitute(source, "\'", "\\\'"); 401 source = CmsStringUtil.substitute(source, "\r\n", "\\n"); 402 source = CmsStringUtil.substitute(source, "\n", "\\n"); 403 404 // to avoid XSS (closing script tags) in embedded Javascript 405 source = CmsStringUtil.substitute(source, "/", "\\/"); 406 return source; 407 } 408 409 /** 410 * Escapes a String so it may be used as a Perl5 regular expression.<p> 411 * 412 * This method replaces the following characters in a String:<br> 413 * <code>{}[]()\$^.*+/</code><p> 414 * 415 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 416 * 417 * @param source the string to escape 418 * 419 * @return the escaped string 420 */ 421 public static String escapePattern(String source) { 422 423 if (source == null) { 424 return null; 425 } 426 StringBuffer result = new StringBuffer(source.length() * 2); 427 for (int i = 0; i < source.length(); ++i) { 428 char ch = source.charAt(i); 429 switch (ch) { 430 case '\\': 431 result.append("\\\\"); 432 break; 433 case '/': 434 result.append("\\/"); 435 break; 436 case '$': 437 result.append("\\$"); 438 break; 439 case '^': 440 result.append("\\^"); 441 break; 442 case '.': 443 result.append("\\."); 444 break; 445 case '*': 446 result.append("\\*"); 447 break; 448 case '+': 449 result.append("\\+"); 450 break; 451 case '|': 452 result.append("\\|"); 453 break; 454 case '?': 455 result.append("\\?"); 456 break; 457 case '{': 458 result.append("\\{"); 459 break; 460 case '}': 461 result.append("\\}"); 462 break; 463 case '[': 464 result.append("\\["); 465 break; 466 case ']': 467 result.append("\\]"); 468 break; 469 case '(': 470 result.append("\\("); 471 break; 472 case ')': 473 result.append("\\)"); 474 break; 475 default: 476 result.append(ch); 477 } 478 } 479 return new String(result); 480 } 481 482 /** 483 * This method takes a part of a html tag definition, an attribute to extend within the 484 * given text and a default value for this attribute; and returns a <code>{@link Map}</code> 485 * with 2 values: a <code>{@link String}</code> with key <code>"text"</code> with the new text 486 * without the given attribute, and another <code>{@link String}</code> with key <code>"value"</code> 487 * with the new extended value for the given attribute, this value is surrounded by the same type of 488 * quotation marks as in the given text.<p> 489 * 490 * @param text the text to search in 491 * @param attribute the attribute to remove and extend from the text 492 * @param defValue a default value for the attribute, should not have any quotation mark 493 * 494 * @return a map with the new text and the new value for the given attribute 495 */ 496 public static Map<String, String> extendAttribute(String text, String attribute, String defValue) { 497 498 Map<String, String> retValue = new HashMap<String, String>(); 499 retValue.put("text", text); 500 retValue.put("value", "'" + defValue + "'"); 501 if ((text != null) && (text.toLowerCase().indexOf(attribute.toLowerCase()) >= 0)) { 502 // this does not work for things like "att=method()" without quotations. 503 String quotation = "\'"; 504 int pos1 = text.toLowerCase().indexOf(attribute.toLowerCase()); 505 // looking for the opening quotation mark 506 int pos2 = text.indexOf(quotation, pos1); 507 int test = text.indexOf("\"", pos1); 508 if ((test > -1) && ((pos2 == -1) || (test < pos2))) { 509 quotation = "\""; 510 pos2 = test; 511 } 512 // assuming there is a closing quotation mark 513 int pos3 = text.indexOf(quotation, pos2 + 1); 514 // building the new attribute value 515 String newValue = quotation + defValue + text.substring(pos2 + 1, pos3 + 1); 516 // removing the onload statement from the parameters 517 String newText = text.substring(0, pos1); 518 if (pos3 < text.length()) { 519 newText += text.substring(pos3 + 1); 520 } 521 retValue.put("text", newText); 522 retValue.put("value", newValue); 523 } 524 return retValue; 525 } 526 527 /** 528 * Extracts the content of a <code><body></code> tag in a HTML page.<p> 529 * 530 * This method should be pretty robust and work even if the input HTML does not contains 531 * a valid body tag.<p> 532 * 533 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 534 * 535 * @param content the content to extract the body from 536 * 537 * @return the extracted body tag content 538 */ 539 public static String extractHtmlBody(String content) { 540 541 Matcher startMatcher = BODY_START_PATTERN.matcher(content); 542 Matcher endMatcher = BODY_END_PATTERN.matcher(content); 543 544 int start = 0; 545 int end = content.length(); 546 547 if (startMatcher.find()) { 548 start = startMatcher.end(); 549 } 550 551 if (endMatcher.find(start)) { 552 end = endMatcher.start(); 553 } 554 555 return content.substring(start, end); 556 } 557 558 /** 559 * Extracts the xml encoding setting from an xml file that is contained in a String by parsing 560 * the xml head.<p> 561 * 562 * This is useful if you have a byte array that contains a xml String, 563 * but you do not know the xml encoding setting. Since the encoding setting 564 * in the xml head is usually encoded with standard US-ASCII, you usually 565 * just create a String of the byte array without encoding setting, 566 * and use this method to find the 'true' encoding. Then create a String 567 * of the byte array again, this time using the found encoding.<p> 568 * 569 * This method will return <code>null</code> in case no xml head 570 * or encoding information is contained in the input.<p> 571 * 572 * @param content the xml content to extract the encoding from 573 * 574 * @return the extracted encoding, or null if no xml encoding setting was found in the input 575 */ 576 public static String extractXmlEncoding(String content) { 577 578 String result = null; 579 Matcher xmlHeadMatcher = XML_HEAD_REGEX.matcher(content); 580 if (xmlHeadMatcher.find()) { 581 String xmlHead = xmlHeadMatcher.group(); 582 Matcher encodingMatcher = XML_ENCODING_REGEX.matcher(xmlHead); 583 if (encodingMatcher.find()) { 584 String encoding = encodingMatcher.group(); 585 int pos1 = encoding.indexOf('=') + 2; 586 String charset = encoding.substring(pos1, encoding.length() - 1); 587 if (Charset.isSupported(charset)) { 588 result = charset; 589 } 590 } 591 } 592 return result; 593 } 594 595 /** 596 * Shortens a resource name or path so that it is not longer than the provided maximum length.<p> 597 * 598 * In order to reduce the length of the resource name, only 599 * complete folder names are removed and replaced with ... successively, 600 * starting with the second folder. 601 * The first folder is removed only in case the result still does not fit 602 * if all subfolders have been removed.<p> 603 * 604 * Example: <code>formatResourceName("/myfolder/subfolder/index.html", 21)</code> 605 * returns <code>/myfolder/.../index.html</code>.<p> 606 * 607 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 608 * 609 * @param name the resource name to format 610 * @param maxLength the maximum length of the resource name (without leading <code>/...</code>) 611 * 612 * @return the formatted resource name 613 */ 614 public static String formatResourceName(String name, int maxLength) { 615 616 if (name == null) { 617 return null; 618 } 619 620 if (name.length() <= maxLength) { 621 return name; 622 } 623 624 int total = name.length(); 625 String[] names = CmsStringUtil.splitAsArray(name, "/"); 626 if (name.endsWith("/")) { 627 names[names.length - 1] = names[names.length - 1] + "/"; 628 } 629 for (int i = 1; (total > maxLength) && (i < (names.length - 1)); i++) { 630 if (i > 1) { 631 names[i - 1] = ""; 632 } 633 names[i] = "..."; 634 total = 0; 635 for (int j = 0; j < names.length; j++) { 636 int l = names[j].length(); 637 total += l + ((l > 0) ? 1 : 0); 638 } 639 } 640 if (total > maxLength) { 641 names[0] = (names.length > 2) ? "" : (names.length > 1) ? "..." : names[0]; 642 } 643 644 StringBuffer result = new StringBuffer(); 645 for (int i = 0; i < names.length; i++) { 646 if (names[i].length() > 0) { 647 result.append("/"); 648 result.append(names[i]); 649 } 650 } 651 652 return result.toString(); 653 } 654 655 /** 656 * Formats a runtime in the format hh:mm:ss, to be used e.g. in reports.<p> 657 * 658 * If the runtime is greater then 24 hours, the format dd:hh:mm:ss is used.<p> 659 * 660 * @param runtime the time to format 661 * 662 * @return the formatted runtime 663 */ 664 public static String formatRuntime(long runtime) { 665 666 long seconds = (runtime / SECONDS) % 60; 667 long minutes = (runtime / MINUTES) % 60; 668 long hours = (runtime / HOURS) % 24; 669 long days = runtime / DAYS; 670 StringBuffer strBuf = new StringBuffer(); 671 672 if (days > 0) { 673 if (days < 10) { 674 strBuf.append('0'); 675 } 676 strBuf.append(days); 677 strBuf.append(':'); 678 } 679 680 if (hours < 10) { 681 strBuf.append('0'); 682 } 683 strBuf.append(hours); 684 strBuf.append(':'); 685 686 if (minutes < 10) { 687 strBuf.append('0'); 688 } 689 strBuf.append(minutes); 690 strBuf.append(':'); 691 692 if (seconds < 10) { 693 strBuf.append('0'); 694 } 695 strBuf.append(seconds); 696 697 return strBuf.toString(); 698 } 699 700 /** 701 * Returns the color value (<code>{@link Color}</code>) for the given String value.<p> 702 * 703 * All parse errors are caught and the given default value is returned in this case.<p> 704 * 705 * @param value the value to parse as color 706 * @param defaultValue the default value in case of parsing errors 707 * @param key a key to be included in the debug output in case of parse errors 708 * 709 * @return the int value for the given parameter value String 710 */ 711 public static Color getColorValue(String value, Color defaultValue, String key) { 712 713 Color result; 714 try { 715 char pre = value.charAt(0); 716 if (pre != '#') { 717 value = "#" + value; 718 } 719 result = Color.decode(value); 720 } catch (Exception e) { 721 if (LOG.isDebugEnabled()) { 722 LOG.debug(Messages.get().getBundle().key(Messages.ERR_UNABLE_TO_PARSE_COLOR_2, value, key)); 723 } 724 result = defaultValue; 725 } 726 return result; 727 } 728 729 /** 730 * Returns the common parent path of two paths.<p> 731 * 732 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 733 * 734 * @param first the first path 735 * @param second the second path 736 * 737 * @return the common prefix path 738 */ 739 public static String getCommonPrefixPath(String first, String second) { 740 741 List<String> firstComponents = getPathComponents(first); 742 List<String> secondComponents = getPathComponents(second); 743 int minSize = Math.min(firstComponents.size(), secondComponents.size()); 744 StringBuffer resultBuffer = new StringBuffer(); 745 for (int i = 0; i < minSize; i++) { 746 if (firstComponents.get(i).equals(secondComponents.get(i))) { 747 resultBuffer.append("/"); 748 resultBuffer.append(firstComponents.get(i)); 749 } else { 750 break; 751 } 752 } 753 String result = resultBuffer.toString(); 754 if (result.length() == 0) { 755 result = "/"; 756 } 757 return result; 758 } 759 760 /** 761 * Returns the Ethernet-Address of the locale host.<p> 762 * 763 * A dummy ethernet address is returned, if the ip is 764 * representing the loopback address or in case of exceptions.<p> 765 * 766 * @return the Ethernet-Address 767 */ 768 public static String getEthernetAddress() { 769 770 try { 771 InetAddress ip = InetAddress.getLocalHost(); 772 if (!ip.isLoopbackAddress()) { 773 NetworkInterface network = NetworkInterface.getByInetAddress(ip); 774 byte[] mac = network.getHardwareAddress(); 775 StringBuilder sb = new StringBuilder(); 776 for (int i = 0; i < mac.length; i++) { 777 sb.append(String.format("%02X%s", new Byte(mac[i]), (i < (mac.length - 1)) ? ":" : "")); 778 } 779 return sb.toString(); 780 } 781 } catch (Throwable t) { 782 // if an exception occurred return a dummy address 783 } 784 // return a dummy ethernet address, if the ip is representing the loopback address or in case of exceptions 785 return CmsUUID.getDummyEthernetAddress(); 786 } 787 788 /** 789 * Returns the Integer (int) value for the given String value.<p> 790 * 791 * All parse errors are caught and the given default value is returned in this case.<p> 792 * 793 * @param value the value to parse as int 794 * @param defaultValue the default value in case of parsing errors 795 * @param key a key to be included in the debug output in case of parse errors 796 * 797 * @return the int value for the given parameter value String 798 */ 799 public static int getIntValue(String value, int defaultValue, String key) { 800 801 int result; 802 try { 803 result = Integer.valueOf(value).intValue(); 804 } catch (Exception e) { 805 if (LOG.isDebugEnabled()) { 806 LOG.debug(Messages.get().getBundle().key(Messages.ERR_UNABLE_TO_PARSE_INT_2, value, key)); 807 } 808 result = defaultValue; 809 } 810 return result; 811 } 812 813 /** 814 * Returns the closest Integer (int) value for the given String value.<p> 815 * 816 * All parse errors are caught and the given default value is returned in this case.<p> 817 * 818 * @param value the value to parse as int, can also represent a float value 819 * @param defaultValue the default value in case of parsing errors 820 * @param key a key to be included in the debug output in case of parse errors 821 * 822 * @return the closest int value for the given parameter value String 823 */ 824 public static int getIntValueRounded(String value, int defaultValue, String key) { 825 826 int result; 827 try { 828 result = Math.round(Float.parseFloat(value)); 829 } catch (Exception e) { 830 if (LOG.isDebugEnabled()) { 831 LOG.debug(Messages.get().getBundle().key(Messages.ERR_UNABLE_TO_PARSE_INT_2, value, key)); 832 } 833 result = defaultValue; 834 } 835 return result; 836 } 837 838 /** 839 * Returns a Locale calculated from the suffix of the given String, or <code>null</code> if no locale suffix is found.<p> 840 * 841 * The locale returned will include the optional country code if this was part of the suffix.<p> 842 * 843 * Calls {@link CmsResource#getName(String)} first, so the given name can also be a resource root path.<p> 844 * 845 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 846 * 847 * @param name the name to get the locale for 848 * 849 * @return the locale, or <code>null</code> 850 * 851 * @see #getLocaleSuffixForName(String) 852 */ 853 public static Locale getLocaleForName(String name) { 854 855 String suffix = getLocaleSuffixForName(CmsResource.getName(name)); 856 if (suffix != null) { 857 String laguageString = suffix.substring(0, 2); 858 return suffix.length() == 5 ? new Locale(laguageString, suffix.substring(3, 5)) : new Locale(laguageString); 859 } 860 return null; 861 } 862 863 /** 864 * Returns the locale for the given text based on the language detection library.<p> 865 * 866 * The result will be <code>null</code> if the detection fails or the detected locale is not configured 867 * in the 'opencms-system.xml' as available locale.<p> 868 * 869 * @param text the text to retrieve the locale for 870 * 871 * @return the detected locale for the given text 872 */ 873 public static Locale getLocaleForText(String text) { 874 875 // try to detect locale by language detector 876 if (isNotEmptyOrWhitespaceOnly(text)) { 877 try { 878 Detector detector = DetectorFactory.create(); 879 detector.append(text); 880 String lang = detector.detect(); 881 Locale loc = new Locale(lang); 882 if (OpenCms.getLocaleManager().getAvailableLocales().contains(loc)) { 883 return loc; 884 } 885 } catch (LangDetectException e) { 886 LOG.debug(e); 887 } 888 } 889 return null; 890 } 891 892 /** 893 * Returns the locale suffix from the given String, or <code>null</code> if no locae suffix is found.<p> 894 * 895 * Uses the the {@link #PATTERN_LOCALE_SUFFIX} to find a language_country occurrence in the 896 * given name and returns the first group of the match.<p> 897 * 898 * <b>Examples:</b> 899 * 900 * <ul> 901 * <li><code>rabbit_en_EN.html -> Locale[en_EN]</code> 902 * <li><code>rabbit_en_EN -> Locale[en_EN]</code> 903 * <li><code>rabbit_en.html -> Locale[en]</code> 904 * <li><code>rabbit_en -> Locale[en]</code> 905 * <li><code>rabbit_en. -> Locale[en]</code> 906 * <li><code>rabbit_enr -> null</code> 907 * <li><code>rabbit_en.tar.gz -> null</code> 908 * </ul> 909 * 910 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 911 * 912 * @param name the resource name to get the locale suffix for 913 * 914 * @return the locale suffix if found, <code>null</code> otherwise 915 */ 916 public static String getLocaleSuffixForName(String name) { 917 918 Matcher matcher = PATTERN_LOCALE_SUFFIX.matcher(name); 919 if (matcher.find()) { 920 return matcher.group(2); 921 } 922 return null; 923 } 924 925 /** 926 * Returns the Long (long) value for the given String value.<p> 927 * 928 * All parse errors are caught and the given default value is returned in this case.<p> 929 * 930 * @param value the value to parse as long 931 * @param defaultValue the default value in case of parsing errors 932 * @param key a key to be included in the debug output in case of parse errors 933 * 934 * @return the long value for the given parameter value String 935 */ 936 public static long getLongValue(String value, long defaultValue, String key) { 937 938 long result; 939 try { 940 result = Long.valueOf(value).longValue(); 941 } catch (Exception e) { 942 if (LOG.isDebugEnabled()) { 943 LOG.debug(Messages.get().getBundle().key(Messages.ERR_UNABLE_TO_PARSE_INT_2, value, key)); 944 } 945 result = defaultValue; 946 } 947 return result; 948 } 949 950 /** 951 * Splits a path into its non-empty path components.<p> 952 * 953 * If the path is the root path, an empty list will be returned.<p> 954 * 955 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 956 * 957 * @param path the path to split 958 * 959 * @return the list of non-empty path components 960 */ 961 public static List<String> getPathComponents(String path) { 962 963 List<String> result = CmsStringUtil.splitAsList(path, "/"); 964 Iterator<String> iter = result.iterator(); 965 while (iter.hasNext()) { 966 String token = iter.next(); 967 if (CmsStringUtil.isEmptyOrWhitespaceOnly(token)) { 968 iter.remove(); 969 } 970 } 971 return result; 972 } 973 974 /** 975 * Converts the given path to a path relative to a base folder, 976 * but only if it actually is a sub-path of the latter, 977 * otherwise <code>null</code> is returned.<p> 978 * 979 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 980 * 981 * @param base the base path 982 * @param path the path which should be converted to a relative path 983 * 984 * @return 'path' converted to a path relative to 'base', or null if 'path' is not a sub-folder of 'base' 985 */ 986 public static String getRelativeSubPath(String base, String path) { 987 988 String result = null; 989 base = CmsStringUtil.joinPaths(base, "/"); 990 path = CmsStringUtil.joinPaths(path, "/"); 991 if (path.startsWith(base)) { 992 result = path.substring(base.length()); 993 } 994 if (result != null) { 995 if (result.endsWith("/")) { 996 result = result.substring(0, result.length() - 1); 997 } 998 if (!result.startsWith("/")) { 999 result = "/" + result; 1000 } 1001 } 1002 return result; 1003 } 1004 1005 /** 1006 * Returns <code>true</code> if the provided String is either <code>null</code> 1007 * or the empty String <code>""</code>.<p> 1008 * 1009 * @param value the value to check 1010 * 1011 * @return true, if the provided value is null or the empty String, false otherwise 1012 */ 1013 public static boolean isEmpty(String value) { 1014 1015 return (value == null) || (value.length() == 0); 1016 } 1017 1018 /** 1019 * Returns <code>true</code> if the provided String is either <code>null</code> 1020 * or contains only white spaces.<p> 1021 * 1022 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 1023 * 1024 * @param value the value to check 1025 * 1026 * @return true, if the provided value is null or contains only white spaces, false otherwise 1027 */ 1028 public static boolean isEmptyOrWhitespaceOnly(String value) { 1029 1030 return isEmpty(value) || (value.trim().length() == 0); 1031 } 1032 1033 /** 1034 * Returns <code>true</code> if the provided Objects are either both <code>null</code> 1035 * or equal according to {@link Object#equals(Object)}.<p> 1036 * 1037 * @param value1 the first object to compare 1038 * @param value2 the second object to compare 1039 * 1040 * @return <code>true</code> if the provided Objects are either both <code>null</code> 1041 * or equal according to {@link Object#equals(Object)} 1042 */ 1043 public static boolean isEqual(Object value1, Object value2) { 1044 1045 if (value1 == null) { 1046 return (value2 == null); 1047 } 1048 return value1.equals(value2); 1049 } 1050 1051 /** 1052 * Returns <code>true</code> if the provided String is neither <code>null</code> 1053 * nor the empty String <code>""</code>.<p> 1054 * 1055 * @param value the value to check 1056 * 1057 * @return true, if the provided value is not null and not the empty String, false otherwise 1058 */ 1059 public static boolean isNotEmpty(String value) { 1060 1061 return (value != null) && (value.length() != 0); 1062 } 1063 1064 /** 1065 * Returns <code>true</code> if the provided String is neither <code>null</code> 1066 * nor contains only white spaces.<p> 1067 * 1068 * @param value the value to check 1069 * 1070 * @return <code>true</code>, if the provided value is <code>null</code> 1071 * or contains only white spaces, <code>false</code> otherwise 1072 */ 1073 public static boolean isNotEmptyOrWhitespaceOnly(String value) { 1074 1075 return (value != null) && (value.trim().length() > 0); 1076 } 1077 1078 /** 1079 * Checks if the first path is a prefix of the second path.<p> 1080 * 1081 * This method is different compared to {@link String#startsWith}, 1082 * because it considers <code>/foo/bar</code> to 1083 * be a prefix path of <code>/foo/bar/baz</code>, 1084 * but not of <code>/foo/bar42</code>. 1085 * 1086 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 1087 * 1088 * @param firstPath the first path 1089 * @param secondPath the second path 1090 * 1091 * @return true if the first path is a prefix path of the second path 1092 */ 1093 public static boolean isPrefixPath(String firstPath, String secondPath) { 1094 1095 firstPath = CmsStringUtil.joinPaths(firstPath, "/"); 1096 secondPath = CmsStringUtil.joinPaths(secondPath, "/"); 1097 return secondPath.startsWith(firstPath); 1098 } 1099 1100 /** 1101 * Checks if the given class name is a valid Java class name.<p> 1102 * 1103 * <b>Directly exposed for JSP EL</b>, not through {@link org.opencms.jsp.util.CmsJspElFunctions}.<p> 1104 * 1105 * @param className the name to check 1106 * 1107 * @return true if the given class name is a valid Java class name 1108 */ 1109 public static boolean isValidJavaClassName(String className) { 1110 1111 if (CmsStringUtil.isEmpty(className)) { 1112 return false; 1113 } 1114 int length = className.length(); 1115 boolean nodot = true; 1116 for (int i = 0; i < length; i++) { 1117 char ch = className.charAt(i); 1118 if (nodot) { 1119 if (ch == '.') { 1120 return false; 1121 } else if (Character.isJavaIdentifierStart(ch)) { 1122 nodot = false; 1123 } else { 1124 return false; 1125 } 1126 } else { 1127 if (ch == '.') { 1128 nodot = true; 1129 } else if (Character.isJavaIdentifierPart(ch)) { 1130 nodot = false; 1131 } else { 1132 return false; 1133 } 1134 } 1135 } 1136 return true; 1137 } 1138 1139 /** 1140 * Concatenates multiple paths and separates them with '/'.<p> 1141 * 1142 * Consecutive slashes will be reduced to a single slash in the resulting string. 1143 * For example, joinPaths("/foo/", "/bar", "baz") will return "/foo/bar/baz". 1144 * 1145 * @param paths the list of paths 1146 * 1147 * @return the joined path 1148 */ 1149 public static String joinPaths(List<String> paths) { 1150 1151 String result = listAsString(paths, "/"); 1152 // result may now contain multiple consecutive slashes, so reduce them to single slashes 1153 result = PATTERN_SLASHES.matcher(result).replaceAll("/"); 1154 return result; 1155 } 1156 1157 /** 1158 * Concatenates multiple paths and separates them with '/'.<p> 1159 * 1160 * Consecutive slashes will be reduced to a single slash in the resulting string. 1161 * For example joinPaths("/foo/", "/bar", "baz") will return "/foo/bar/baz".<p> 1162 * 1163 * If one of the argument paths already contains a double "//" this will also be reduced to '/'. 1164 * For example joinPaths("/foo//bar/", "/baz") will return "/foo/bar/baz". 1165 * 1166 * @param paths the array of paths 1167 * 1168 * @return the joined path 1169 */ 1170 public static String joinPaths(String... paths) { 1171 1172 StringBuffer result = new StringBuffer(paths.length * 32); 1173 boolean noSlash = true; 1174 for (int i = 0; i < paths.length; i++) { 1175 for (int j = 0; j < paths[i].length(); j++) { 1176 char c = paths[i].charAt(j); 1177 if (c != '/') { 1178 result.append(c); 1179 noSlash = true; 1180 } else if (noSlash) { 1181 result.append('/'); 1182 noSlash = false; 1183 } 1184 } 1185 if (noSlash && (i < (paths.length - 1))) { 1186 result.append('/'); 1187 noSlash = false; 1188 } 1189 } 1190 return result.toString(); 1191 } 1192 1193 /** 1194 * Returns the last index of any of the given chars in the given source.<p> 1195 * 1196 * If no char is found, -1 is returned.<p> 1197 * 1198 * @param source the source to check 1199 * @param chars the chars to find 1200 * 1201 * @return the last index of any of the given chars in the given source, or -1 1202 */ 1203 public static int lastIndexOf(String source, char[] chars) { 1204 1205 // now try to find an "sentence ending" char in the text in the "findPointArea" 1206 int result = -1; 1207 for (int i = 0; i < chars.length; i++) { 1208 int pos = source.lastIndexOf(chars[i]); 1209 if (pos > result) { 1210 // found new last char 1211 result = pos; 1212 } 1213 } 1214 return result; 1215 } 1216 1217 /** 1218 * Returns the last index a whitespace char the given source.<p> 1219 * 1220 * If no whitespace char is found, -1 is returned.<p> 1221 * 1222 * @param source the source to check 1223 * 1224 * @return the last index a whitespace char the given source, or -1 1225 */ 1226 public static int lastWhitespaceIn(String source) { 1227 1228 if (CmsStringUtil.isEmpty(source)) { 1229 return -1; 1230 } 1231 int pos = -1; 1232 for (int i = source.length() - 1; i >= 0; i--) { 1233 if (Character.isWhitespace(source.charAt(i))) { 1234 pos = i; 1235 break; 1236 } 1237 } 1238 return pos; 1239 } 1240 1241 /** 1242 * Returns a string representation for the given list using the given separator.<p> 1243 * 1244 * @param list the list to write 1245 * @param separator the item separator string 1246 * 1247 * @return the string representation for the given map 1248 */ 1249 public static String listAsString(List<?> list, String separator) { 1250 1251 StringBuffer string = new StringBuffer(128); 1252 Iterator<?> it = list.iterator(); 1253 while (it.hasNext()) { 1254 string.append(it.next()); 1255 if (it.hasNext()) { 1256 string.append(separator); 1257 } 1258 } 1259 return string.toString(); 1260 } 1261 1262 /** 1263 * Encodes a map with string keys and values as a JSON string with the same keys/values.<p> 1264 * 1265 * @param map the input map 1266 * @return the JSON data containing the map entries 1267 */ 1268 public static String mapAsJson(Map<String, String> map) { 1269 1270 JSONObject obj = new JSONObject(); 1271 for (Map.Entry<String, String> entry : map.entrySet()) { 1272 try { 1273 obj.put(entry.getKey(), entry.getValue()); 1274 } catch (JSONException e) { 1275 LOG.error(e.getLocalizedMessage(), e); 1276 } 1277 } 1278 return obj.toString(); 1279 } 1280 1281 /** 1282 * Returns a string representation for the given map using the given separators.<p> 1283 * 1284 * @param <K> type of map keys 1285 * @param <V> type of map values 1286 * @param map the map to write 1287 * @param sepItem the item separator string 1288 * @param sepKeyval the key-value pair separator string 1289 * 1290 * @return the string representation for the given map 1291 */ 1292 public static <K, V> String mapAsString(Map<K, V> map, String sepItem, String sepKeyval) { 1293 1294 StringBuffer string = new StringBuffer(128); 1295 Iterator<Map.Entry<K, V>> it = map.entrySet().iterator(); 1296 while (it.hasNext()) { 1297 Map.Entry<K, V> entry = it.next(); 1298 string.append(entry.getKey()); 1299 string.append(sepKeyval); 1300 string.append(entry.getValue()); 1301 if (it.hasNext()) { 1302 string.append(sepItem); 1303 } 1304 } 1305 return string.toString(); 1306 } 1307 1308 /** 1309 * Applies white space padding to the left of the given String.<p> 1310 * 1311 * @param input the input to pad left 1312 * @param size the size of the padding 1313 * 1314 * @return the input padded to the left 1315 */ 1316 public static String padLeft(String input, int size) { 1317 1318 return (new PrintfFormat("%" + size + "s")).sprintf(input); 1319 } 1320 1321 /** 1322 * Applies white space padding to the right of the given String.<p> 1323 * 1324 * @param input the input to pad right 1325 * @param size the size of the padding 1326 * 1327 * @return the input padded to the right 1328 */ 1329 public static String padRight(String input, int size) { 1330 1331 return (new PrintfFormat("%-" + size + "s")).sprintf(input); 1332 } 1333 1334 /** 1335 * Parses a duration and returns the corresponding number of milliseconds. 1336 * 1337 * Durations consist of a space-separated list of components of the form {number}{time unit}, 1338 * for example 1d 5m. The available units are d (days), h (hours), m (months), s (seconds), ms (milliseconds).<p> 1339 * 1340 * @param durationStr the duration string 1341 * @param defaultValue the default value to return in case the pattern does not match 1342 * @return the corresponding number of milliseconds 1343 */ 1344 public static final long parseDuration(String durationStr, long defaultValue) { 1345 1346 durationStr = durationStr.toLowerCase().trim(); 1347 Matcher matcher = DURATION_NUMBER_AND_UNIT_PATTERN.matcher(durationStr); 1348 long millis = 0; 1349 boolean matched = false; 1350 while (matcher.find()) { 1351 long number = Long.valueOf(matcher.group(1)).longValue(); 1352 String unit = matcher.group(2); 1353 long multiplier = 0; 1354 for (int j = 0; j < DURATION_UNTIS.length; j++) { 1355 if (unit.equals(DURATION_UNTIS[j])) { 1356 multiplier = DURATION_MULTIPLIERS[j]; 1357 break; 1358 } 1359 } 1360 if (multiplier == 0) { 1361 LOG.warn("parseDuration: Unknown unit " + unit); 1362 } else { 1363 matched = true; 1364 } 1365 millis += number * multiplier; 1366 } 1367 if (!matched) { 1368 millis = defaultValue; 1369 } 1370 return millis; 1371 } 1372 1373 /** 1374 * Replaces a constant prefix with another string constant in a given text.<p> 1375 * 1376 * If the input string does not start with the given prefix, Optional.absent() is returned.<p> 1377 * 1378 * @param text the text for which to replace the prefix 1379 * @param origPrefix the original prefix 1380 * @param newPrefix the replacement prefix 1381 * @param ignoreCase if true, upper-/lower case differences will be ignored 1382 * 1383 * @return an Optional containing either the string with the replaced prefix, or an absent value if the prefix could not be replaced 1384 */ 1385 public static Optional<String> replacePrefix(String text, String origPrefix, String newPrefix, boolean ignoreCase) { 1386 1387 String prefixTestString = ignoreCase ? text.toLowerCase() : text; 1388 origPrefix = ignoreCase ? origPrefix.toLowerCase() : origPrefix; 1389 if (prefixTestString.startsWith(origPrefix)) { 1390 return Optional.of(newPrefix + text.substring(origPrefix.length())); 1391 } else { 1392 return Optional.absent(); 1393 } 1394 } 1395 1396 /** 1397 * Splits a String into substrings along the provided char delimiter and returns 1398 * the result as an Array of Substrings.<p> 1399 * 1400 * @param source the String to split 1401 * @param delimiter the delimiter to split at 1402 * 1403 * @return the Array of splitted Substrings 1404 */ 1405 public static String[] splitAsArray(String source, char delimiter) { 1406 1407 List<String> result = splitAsList(source, delimiter); 1408 return result.toArray(new String[result.size()]); 1409 } 1410 1411 /** 1412 * Splits a String into substrings along the provided String delimiter and returns 1413 * the result as an Array of Substrings.<p> 1414 * 1415 * @param source the String to split 1416 * @param delimiter the delimiter to split at 1417 * 1418 * @return the Array of splitted Substrings 1419 */ 1420 public static String[] splitAsArray(String source, String delimiter) { 1421 1422 List<String> result = splitAsList(source, delimiter); 1423 return result.toArray(new String[result.size()]); 1424 } 1425 1426 /** 1427 * Splits a String into substrings along the provided char delimiter and returns 1428 * the result as a List of Substrings.<p> 1429 * 1430 * @param source the String to split 1431 * @param delimiter the delimiter to split at 1432 * 1433 * @return the List of splitted Substrings 1434 */ 1435 public static List<String> splitAsList(String source, char delimiter) { 1436 1437 return splitAsList(source, delimiter, false); 1438 } 1439 1440 /** 1441 * Splits a String into substrings along the provided char delimiter and returns 1442 * the result as a List of Substrings.<p> 1443 * 1444 * @param source the String to split 1445 * @param delimiter the delimiter to split at 1446 * @param trim flag to indicate if leading and trailing white spaces should be omitted 1447 * 1448 * @return the List of splitted Substrings 1449 */ 1450 public static List<String> splitAsList(String source, char delimiter, boolean trim) { 1451 1452 List<String> result = new ArrayList<String>(); 1453 int i = 0; 1454 int l = source.length(); 1455 int n = source.indexOf(delimiter); 1456 while (n != -1) { 1457 // zero - length items are not seen as tokens at start or end 1458 if ((i < n) || ((i > 0) && (i < l))) { 1459 result.add(trim ? source.substring(i, n).trim() : source.substring(i, n)); 1460 } 1461 i = n + 1; 1462 n = source.indexOf(delimiter, i); 1463 } 1464 // is there a non - empty String to cut from the tail? 1465 if (n < 0) { 1466 n = source.length(); 1467 } 1468 if (i < n) { 1469 result.add(trim ? source.substring(i).trim() : source.substring(i)); 1470 } 1471 return result; 1472 } 1473 1474 /** 1475 * Splits a String into substrings along the provided String delimiter and returns 1476 * the result as List of Substrings.<p> 1477 * 1478 * @param source the String to split 1479 * @param delimiter the delimiter to split at 1480 * 1481 * @return the Array of splitted Substrings 1482 */ 1483 public static List<String> splitAsList(String source, String delimiter) { 1484 1485 return splitAsList(source, delimiter, false); 1486 } 1487 1488 /** 1489 * Splits a String into substrings along the provided String delimiter and returns 1490 * the result as List of Substrings.<p> 1491 * 1492 * @param source the String to split 1493 * @param delimiter the delimiter to split at 1494 * @param trim flag to indicate if leading and trailing white spaces should be omitted 1495 * 1496 * @return the Array of splitted Substrings 1497 */ 1498 public static List<String> splitAsList(String source, String delimiter, boolean trim) { 1499 1500 int dl = delimiter.length(); 1501 if (dl == 1) { 1502 // optimize for short strings 1503 return splitAsList(source, delimiter.charAt(0), trim); 1504 } 1505 1506 List<String> result = new ArrayList<String>(); 1507 int i = 0; 1508 int l = source.length(); 1509 int n = source.indexOf(delimiter); 1510 while (n != -1) { 1511 // zero - length items are not seen as tokens at start or end: ",," is one empty token but not three 1512 if ((i < n) || ((i > 0) && (i < l))) { 1513 result.add(trim ? source.substring(i, n).trim() : source.substring(i, n)); 1514 } 1515 i = n + dl; 1516 n = source.indexOf(delimiter, i); 1517 } 1518 // is there a non - empty String to cut from the tail? 1519 if (n < 0) { 1520 n = source.length(); 1521 } 1522 if (i < n) { 1523 result.add(trim ? source.substring(i).trim() : source.substring(i)); 1524 } 1525 return result; 1526 } 1527 1528 /** 1529 * Splits a String into substrings along the provided <code>paramDelim</code> delimiter, 1530 * then each substring is treat as a key-value pair delimited by <code>keyValDelim</code>.<p> 1531 * 1532 * @param source the string to split 1533 * @param paramDelim the string to delimit each key-value pair 1534 * @param keyValDelim the string to delimit key and value 1535 * 1536 * @return a map of splitted key-value pairs 1537 */ 1538 public static Map<String, String> splitAsMap(String source, String paramDelim, String keyValDelim) { 1539 1540 int keyValLen = keyValDelim.length(); 1541 // use LinkedHashMap to preserve the order of items 1542 Map<String, String> params = new LinkedHashMap<String, String>(); 1543 Iterator<String> itParams = CmsStringUtil.splitAsList(source, paramDelim, true).iterator(); 1544 while (itParams.hasNext()) { 1545 String param = itParams.next(); 1546 int pos = param.indexOf(keyValDelim); 1547 String key = param; 1548 String value = ""; 1549 if (pos > 0) { 1550 key = param.substring(0, pos); 1551 if ((pos + keyValLen) < param.length()) { 1552 value = param.substring(pos + keyValLen); 1553 } 1554 } 1555 params.put(key, value); 1556 } 1557 return params; 1558 } 1559 1560 /** 1561 * Substitutes a pattern in a string using a {@link I_CmsRegexSubstitution}.<p> 1562 * 1563 * @param pattern the pattern to substitute 1564 * @param text the text in which the pattern should be substituted 1565 * @param sub the substitution handler 1566 * 1567 * @return the transformed string 1568 */ 1569 public static String substitute(Pattern pattern, String text, I_CmsRegexSubstitution sub) { 1570 1571 StringBuffer buffer = new StringBuffer(); 1572 Matcher matcher = pattern.matcher(text); 1573 while (matcher.find()) { 1574 matcher.appendReplacement(buffer, sub.substituteMatch(text, matcher)); 1575 } 1576 matcher.appendTail(buffer); 1577 return buffer.toString(); 1578 } 1579 1580 /** 1581 * Replaces a set of <code>searchString</code> and <code>replaceString</code> pairs, 1582 * given by the <code>substitutions</code> Map parameter.<p> 1583 * 1584 * @param source the string to scan 1585 * @param substitions the map of substitutions 1586 * 1587 * @return the substituted String 1588 * 1589 * @see #substitute(String, String, String) 1590 */ 1591 public static String substitute(String source, Map<String, String> substitions) { 1592 1593 String result = source; 1594 Iterator<Map.Entry<String, String>> it = substitions.entrySet().iterator(); 1595 while (it.hasNext()) { 1596 Map.Entry<String, String> entry = it.next(); 1597 result = substitute(result, entry.getKey(), entry.getValue().toString()); 1598 } 1599 return result; 1600 } 1601 1602 /** 1603 * Substitutes <code>searchString</code> in the given source String with <code>replaceString</code>.<p> 1604 * 1605 * This is a high-performance implementation which should be used as a replacement for 1606 * <code>{@link String#replaceAll(java.lang.String, java.lang.String)}</code> in case no 1607 * regular expression evaluation is required.<p> 1608 * 1609 * @param source the content which is scanned 1610 * @param searchString the String which is searched in content 1611 * @param replaceString the String which replaces <code>searchString</code> 1612 * 1613 * @return the substituted String 1614 */ 1615 public static String substitute(String source, String searchString, String replaceString) { 1616 1617 if (source == null) { 1618 return null; 1619 } 1620 1621 if (isEmpty(searchString)) { 1622 return source; 1623 } 1624 1625 if (replaceString == null) { 1626 replaceString = ""; 1627 } 1628 int len = source.length(); 1629 int sl = searchString.length(); 1630 int rl = replaceString.length(); 1631 int length; 1632 if (sl == rl) { 1633 length = len; 1634 } else { 1635 int c = 0; 1636 int s = 0; 1637 int e; 1638 while ((e = source.indexOf(searchString, s)) != -1) { 1639 c++; 1640 s = e + sl; 1641 } 1642 if (c == 0) { 1643 return source; 1644 } 1645 length = len - (c * (sl - rl)); 1646 } 1647 1648 int s = 0; 1649 int e = source.indexOf(searchString, s); 1650 if (e == -1) { 1651 return source; 1652 } 1653 StringBuffer sb = new StringBuffer(length); 1654 while (e != -1) { 1655 sb.append(source.substring(s, e)); 1656 sb.append(replaceString); 1657 s = e + sl; 1658 e = source.indexOf(searchString, s); 1659 } 1660 e = len; 1661 sb.append(source.substring(s, e)); 1662 return sb.toString(); 1663 } 1664 1665 /** 1666 * Substitutes the OpenCms context path (e.g. /opencms/opencms/) in a HTML page with a 1667 * special variable so that the content also runs if the context path of the server changes.<p> 1668 * 1669 * @param htmlContent the HTML to replace the context path in 1670 * @param context the context path of the server 1671 * 1672 * @return the HTML with the replaced context path 1673 */ 1674 public static String substituteContextPath(String htmlContent, String context) { 1675 1676 if (m_contextSearch == null) { 1677 m_contextSearch = "([^\\w/])" + context; 1678 m_contextReplace = "$1" + CmsStringUtil.escapePattern(CmsStringUtil.MACRO_OPENCMS_CONTEXT) + "/"; 1679 } 1680 return substitutePerl(htmlContent, m_contextSearch, m_contextReplace, "g"); 1681 } 1682 1683 /** 1684 * Substitutes searchString in content with replaceItem.<p> 1685 * 1686 * @param content the content which is scanned 1687 * @param searchString the String which is searched in content 1688 * @param replaceItem the new String which replaces searchString 1689 * @param occurences must be a "g" if all occurrences of searchString shall be replaced 1690 * 1691 * @return String the substituted String 1692 */ 1693 public static String substitutePerl(String content, String searchString, String replaceItem, String occurences) { 1694 1695 String translationRule = "s#" + searchString + "#" + replaceItem + "#" + occurences; 1696 Perl5Util perlUtil = new Perl5Util(); 1697 try { 1698 return perlUtil.substitute(translationRule, content); 1699 } catch (MalformedPerl5PatternException e) { 1700 if (LOG.isDebugEnabled()) { 1701 LOG.debug( 1702 Messages.get().getBundle().key(Messages.LOG_MALFORMED_TRANSLATION_RULE_1, translationRule), 1703 e); 1704 } 1705 } 1706 return content; 1707 } 1708 1709 /** 1710 * Returns the java String literal for the given String. <p> 1711 * 1712 * This is the form of the String that had to be written into source code 1713 * using the unicode escape sequence for special characters. <p> 1714 * 1715 * Example: "Ä" would be transformed to "\\u00C4".<p> 1716 * 1717 * @param s a string that may contain non-ascii characters 1718 * 1719 * @return the java unicode escaped string Literal of the given input string 1720 */ 1721 public static String toUnicodeLiteral(String s) { 1722 1723 StringBuffer result = new StringBuffer(); 1724 char[] carr = s.toCharArray(); 1725 1726 String unicode; 1727 for (int i = 0; i < carr.length; i++) { 1728 result.append("\\u"); 1729 // append leading zeros 1730 unicode = Integer.toHexString(carr[i]).toUpperCase(); 1731 for (int j = 4 - unicode.length(); j > 0; j--) { 1732 result.append("0"); 1733 } 1734 result.append(unicode); 1735 } 1736 return result.toString(); 1737 } 1738 1739 /** 1740 * This method transformes a string which matched a format with one or more place holders into another format. The 1741 * other format also includes the same number of place holders. Place holders start with 1742 * {@link org.opencms.util.CmsStringUtil#PLACEHOLDER_START} and end with {@link org.opencms.util.CmsStringUtil#PLACEHOLDER_END}.<p> 1743 * 1744 * @param oldFormat the original format 1745 * @param newFormat the new format 1746 * @param value the value which matched the original format and which shall be transformed into the new format 1747 * 1748 * @return the new value with the filled place holder with the information in the parameter value 1749 */ 1750 public static String transformValues(String oldFormat, String newFormat, String value) { 1751 1752 if (!oldFormat.contains(CmsStringUtil.PLACEHOLDER_START) 1753 || !oldFormat.contains(CmsStringUtil.PLACEHOLDER_END) 1754 || !newFormat.contains(CmsStringUtil.PLACEHOLDER_START) 1755 || !newFormat.contains(CmsStringUtil.PLACEHOLDER_END)) { 1756 // no place holders are set in correct format 1757 // that is why there is nothing to calculate and the value is the new format 1758 return newFormat; 1759 } 1760 //initialize the arrays with the values where the place holders starts 1761 ArrayList<Integer> oldValues = new ArrayList<Integer>(); 1762 ArrayList<Integer> newValues = new ArrayList<Integer>(); 1763 1764 // count the number of placeholders 1765 // for example these are three pairs: 1766 // old format: {.*}<b>{.*}</b>{.*} 1767 // new format: {}<strong>{}</strong>{} 1768 // get the number of place holders in the old format 1769 int oldNumber = 0; 1770 try { 1771 int counter = 0; 1772 Pattern pattern = Pattern.compile("\\{\\.\\*\\}"); 1773 Matcher matcher = pattern.matcher(oldFormat); 1774 // get the number of matches 1775 while (matcher.find()) { 1776 counter += 1; 1777 } 1778 oldValues = new ArrayList<Integer>(counter); 1779 matcher = pattern.matcher(oldFormat); 1780 while (matcher.find()) { 1781 int start = matcher.start() + 1; 1782 oldValues.add(oldNumber, new Integer(start)); 1783 oldNumber += 1; 1784 } 1785 } catch (PatternSyntaxException e) { 1786 // do nothing 1787 } 1788 // get the number of place holders in the new format 1789 int newNumber = 0; 1790 try { 1791 int counter = 0; 1792 Pattern pattern = Pattern.compile("\\{\\}"); 1793 Matcher matcher = pattern.matcher(newFormat); 1794 // get the number of matches 1795 while (matcher.find()) { 1796 counter += 1; 1797 } 1798 newValues = new ArrayList<Integer>(counter); 1799 matcher = pattern.matcher(newFormat); 1800 while (matcher.find()) { 1801 int start = matcher.start() + 1; 1802 newValues.add(newNumber, new Integer(start)); 1803 newNumber += 1; 1804 } 1805 } catch (PatternSyntaxException e) { 1806 // do nothing 1807 } 1808 // prove the numbers of place holders 1809 if (oldNumber != newNumber) { 1810 // not the same number of place holders in the old and in the new format 1811 return newFormat; 1812 } 1813 1814 // initialize the arrays with the values between the place holders 1815 ArrayList<String> oldBetween = new ArrayList<String>(oldNumber + 1); 1816 ArrayList<String> newBetween = new ArrayList<String>(newNumber + 1); 1817 1818 // get the values between the place holders for the old format 1819 // for this example with oldFormat: {.*}<b>{.*}</b>{.*} 1820 // this array is that: 1821 // --------- 1822 // | empty | 1823 // --------- 1824 // | <b> | 1825 // |-------- 1826 // | </b> | 1827 // |-------- 1828 // | empty | 1829 // |-------- 1830 int counter = 0; 1831 Iterator<Integer> iter = oldValues.iterator(); 1832 while (iter.hasNext()) { 1833 int start = iter.next().intValue(); 1834 if (counter == 0) { 1835 // the first entry 1836 if (start == 1) { 1837 // the first place holder starts at the beginning of the old format 1838 // for example: {.*}<b>... 1839 oldBetween.add(counter, ""); 1840 } else { 1841 // the first place holder starts NOT at the beginning of the old format 1842 // for example: <a>{.*}<b>... 1843 String part = oldFormat.substring(0, start - 1); 1844 oldBetween.add(counter, part); 1845 } 1846 } else { 1847 // the entries between the first and the last entry 1848 int lastStart = oldValues.get(counter - 1).intValue(); 1849 String part = oldFormat.substring(lastStart + 3, start - 1); 1850 oldBetween.add(counter, part); 1851 } 1852 counter += 1; 1853 } 1854 // the last element 1855 int lastElstart = oldValues.get(counter - 1).intValue(); 1856 if ((lastElstart + 2) == (oldFormat.length() - 1)) { 1857 // the last place holder ends at the end of the old format 1858 // for example: ...</b>{.*} 1859 oldBetween.add(counter, ""); 1860 } else { 1861 // the last place holder ends NOT at the end of the old format 1862 // for example: ...</b>{.*}</a> 1863 String part = oldFormat.substring(lastElstart + 3); 1864 oldBetween.add(counter, part); 1865 } 1866 1867 // get the values between the place holders for the new format 1868 // for this example with newFormat: {}<strong>{}</strong>{} 1869 // this array is that: 1870 // ------------| 1871 // | empty | 1872 // ------------| 1873 // | <strong> | 1874 // |-----------| 1875 // | </strong> | 1876 // |-----------| 1877 // | empty | 1878 // |-----------| 1879 counter = 0; 1880 iter = newValues.iterator(); 1881 while (iter.hasNext()) { 1882 int start = iter.next().intValue(); 1883 if (counter == 0) { 1884 // the first entry 1885 if (start == 1) { 1886 // the first place holder starts at the beginning of the new format 1887 // for example: {.*}<b>... 1888 newBetween.add(counter, ""); 1889 } else { 1890 // the first place holder starts NOT at the beginning of the new format 1891 // for example: <a>{.*}<b>... 1892 String part = newFormat.substring(0, start - 1); 1893 newBetween.add(counter, part); 1894 } 1895 } else { 1896 // the entries between the first and the last entry 1897 int lastStart = newValues.get(counter - 1).intValue(); 1898 String part = newFormat.substring(lastStart + 1, start - 1); 1899 newBetween.add(counter, part); 1900 } 1901 counter += 1; 1902 } 1903 // the last element 1904 lastElstart = newValues.get(counter - 1).intValue(); 1905 if ((lastElstart + 2) == (newFormat.length() - 1)) { 1906 // the last place holder ends at the end of the old format 1907 // for example: ...</b>{.*} 1908 newBetween.add(counter, ""); 1909 } else { 1910 // the last place holder ends NOT at the end of the old format 1911 // for example: ...</b>{.*}</a> 1912 String part = newFormat.substring(lastElstart + 1); 1913 newBetween.add(counter, part); 1914 } 1915 1916 // get the values in the place holders 1917 // for the example with: 1918 // oldFormat: {.*}<b>{.*}</b>{.*} 1919 // newFormat: {}<strong>{}</strong>{} 1920 // value: abc<b>def</b>ghi 1921 // it is used the array with the old values between the place holders to get the content in the place holders 1922 // this result array is that: 1923 // ------| 1924 // | abc | 1925 // ------| 1926 // | def | 1927 // |-----| 1928 // | ghi | 1929 // |-----| 1930 ArrayList<String> placeHolders = new ArrayList<String>(oldNumber); 1931 String tmpValue = value; 1932 // loop over all rows with the old values between the place holders and take the values between them in the 1933 // current property value 1934 for (int placeCounter = 0; placeCounter < (oldBetween.size() - 1); placeCounter++) { 1935 // get the two next values with the old values between the place holders 1936 String content = oldBetween.get(placeCounter); 1937 String nextContent = oldBetween.get(placeCounter + 1); 1938 // check the position of the first of the next values in the current property value 1939 int contPos = 0; 1940 int nextContPos = 0; 1941 if ((placeCounter == 0) && CmsStringUtil.isEmpty(content)) { 1942 // the first value in the values between the place holders is empty 1943 // for example: {.*}<p>... 1944 contPos = 0; 1945 } else { 1946 // the first value in the values between the place holders is NOT empty 1947 // for example: bla{.*}<p>... 1948 contPos = tmpValue.indexOf(content); 1949 } 1950 // check the position of the second of the next values in the current property value 1951 if (((placeCounter + 1) == (oldBetween.size() - 1)) && CmsStringUtil.isEmpty(nextContent)) { 1952 // the last value in the values between the place holders is empty 1953 // for example: ...<p>{.*} 1954 nextContPos = tmpValue.length(); 1955 } else { 1956 // the last value in the values between the place holders is NOT empty 1957 // for example: ...<p>{.*}bla 1958 nextContPos = tmpValue.indexOf(nextContent); 1959 } 1960 // every value must match the current value 1961 if ((contPos < 0) || (nextContPos < 0)) { 1962 return value; 1963 } 1964 // get the content of the current place holder 1965 String placeContent = tmpValue.substring(contPos + content.length(), nextContPos); 1966 placeHolders.add(placeCounter, placeContent); 1967 // cut off the currently visited part of the value 1968 tmpValue = tmpValue.substring(nextContPos); 1969 } 1970 1971 // build the new format 1972 // with following vectors from above: 1973 // old values between the place holders: 1974 // --------- 1975 // | empty | (old.1) 1976 // --------- 1977 // | <b> | (old.2) 1978 // |-------- 1979 // | </b> | (old.3) 1980 // |-------- 1981 // | empty | (old.4) 1982 // |-------- 1983 // 1984 // new values between the place holders: 1985 // ------------| 1986 // | empty | (new.1) 1987 // ------------| 1988 // | <strong> | (new.2) 1989 // |-----------| 1990 // | </strong> | (new.3) 1991 // |-----------| 1992 // | empty | (new.4) 1993 // |-----------| 1994 // 1995 // content of the place holders: 1996 // ------| 1997 // | abc | (place.1) 1998 // ------| 1999 // | def | (place.2) 2000 // |-----| 2001 // | ghi | (place.3) 2002 // |-----| 2003 // 2004 // the result is calculated in that way: 2005 // new.1 + place.1 + new.2 + place.2 + new.3 + place.3 + new.4 2006 String newValue = ""; 2007 // take the values between the place holders and add the content of the place holders 2008 for (int buildCounter = 0; buildCounter < newNumber; buildCounter++) { 2009 newValue = newValue + newBetween.get(buildCounter) + placeHolders.get(buildCounter); 2010 } 2011 newValue = newValue + newBetween.get(newNumber); 2012 // return the changed value 2013 return newValue; 2014 } 2015 2016 /** 2017 * Returns a substring of the source, which is at most length characters long.<p> 2018 * 2019 * This is the same as calling {@link #trimToSize(String, int, String)} with the 2020 * parameters <code>(source, length, " ...")</code>.<p> 2021 * 2022 * @param source the string to trim 2023 * @param length the maximum length of the string to be returned 2024 * 2025 * @return a substring of the source, which is at most length characters long 2026 */ 2027 public static String trimToSize(String source, int length) { 2028 2029 return trimToSize(source, length, length, " ..."); 2030 } 2031 2032 /** 2033 * Returns a substring of the source, which is at most length characters long, cut 2034 * in the last <code>area</code> chars in the source at a sentence ending char or whitespace.<p> 2035 * 2036 * If a char is cut, the given <code>suffix</code> is appended to the result.<p> 2037 * 2038 * @param source the string to trim 2039 * @param length the maximum length of the string to be returned 2040 * @param area the area at the end of the string in which to find a sentence ender or whitespace 2041 * @param suffix the suffix to append in case the String was trimmed 2042 * 2043 * @return a substring of the source, which is at most length characters long 2044 */ 2045 public static String trimToSize(String source, int length, int area, String suffix) { 2046 2047 if ((source == null) || (source.length() <= length)) { 2048 // no operation is required 2049 return source; 2050 } 2051 if (CmsStringUtil.isEmpty(suffix)) { 2052 // we need an empty suffix 2053 suffix = ""; 2054 } 2055 // must remove the length from the after sequence chars since these are always added in the end 2056 int modLength = length - suffix.length(); 2057 if (modLength <= 0) { 2058 // we are to short, return beginning of the suffix 2059 return suffix.substring(0, length); 2060 } 2061 int modArea = area + suffix.length(); 2062 if ((modArea > modLength) || (modArea < 0)) { 2063 // area must not be longer then max length 2064 modArea = modLength; 2065 } 2066 2067 // first reduce the String to the maximum allowed length 2068 String findPointSource = source.substring(modLength - modArea, modLength); 2069 2070 String result; 2071 // try to find an "sentence ending" char in the text 2072 int pos = lastIndexOf(findPointSource, SENTENCE_ENDING_CHARS); 2073 if (pos >= 0) { 2074 // found a sentence ender in the lookup area, keep the sentence ender 2075 result = source.substring(0, (modLength - modArea) + pos + 1) + suffix; 2076 } else { 2077 // no sentence ender was found, try to find a whitespace 2078 pos = lastWhitespaceIn(findPointSource); 2079 if (pos >= 0) { 2080 // found a whitespace, don't keep the whitespace 2081 result = source.substring(0, (modLength - modArea) + pos) + suffix; 2082 } else { 2083 // not even a whitespace was found, just cut away what's to long 2084 result = source.substring(0, modLength) + suffix; 2085 } 2086 } 2087 2088 return result; 2089 } 2090 2091 /** 2092 * Returns a substring of the source, which is at most length characters long.<p> 2093 * 2094 * If a char is cut, the given <code>suffix</code> is appended to the result.<p> 2095 * 2096 * This is almost the same as calling {@link #trimToSize(String, int, int, String)} with the 2097 * parameters <code>(source, length, length*, suffix)</code>. If <code>length</code> 2098 * if larger then 100, then <code>length* = length / 2</code>, 2099 * otherwise <code>length* = length</code>.<p> 2100 * 2101 * @param source the string to trim 2102 * @param length the maximum length of the string to be returned 2103 * @param suffix the suffix to append in case the String was trimmed 2104 * 2105 * @return a substring of the source, which is at most length characters long 2106 */ 2107 public static String trimToSize(String source, int length, String suffix) { 2108 2109 int area = (length > 100) ? length / 2 : length; 2110 return trimToSize(source, length, area, suffix); 2111 } 2112 2113 /** 2114 * Validates a value against a regular expression.<p> 2115 * 2116 * @param value the value to test 2117 * @param regex the regular expression 2118 * @param allowEmpty if an empty value is allowed 2119 * 2120 * @return <code>true</code> if the value satisfies the validation 2121 */ 2122 public static boolean validateRegex(String value, String regex, boolean allowEmpty) { 2123 2124 if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) { 2125 return allowEmpty; 2126 } 2127 Pattern pattern = Pattern.compile(regex); 2128 Matcher matcher = pattern.matcher(value); 2129 return matcher.matches(); 2130 } 2131}