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