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