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