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