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