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