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, Character.valueOf(c), Integer.valueOf(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", Byte.valueOf(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 * Specialized version of splitAsMap used for splitting option lists for widgets. 1653 * 1654 * <p>This used the separator characters (':' for key/value, '|' for entries), but also allows escaping of 1655 * these characters with backslashes ('\'), to enable use of colons/pipes in keys and values. Backslashes themselves 1656 * can also be escaped. 1657 * 1658 * @param optionsStr the string representing the option list 1659 * @return the options map 1660 */ 1661 public static Map<String, String> splitOptions(String optionsStr) { 1662 1663 // state machine with 4 states - combination of whether we are currently either in the key or the value of an entry, and whether the last character was the escape character '\' or not 1664 // (could be done more simply with java.util.regex.Pattern in the JVM, but we want to keep the implementation the same as in the GWT code, where we don't have it.) 1665 final int S_KEY = 0; 1666 final int S_VALUE = 1; 1667 final int S_KEY_ESC = 2; 1668 final int S_VALUE_ESC = 3; 1669 1670 boolean nextEntry = false; 1671 StringBuilder keyBuffer = new StringBuilder(); 1672 StringBuilder valueBuffer = new StringBuilder(); 1673 Map<String, String> result = new LinkedHashMap<>(); 1674 int length = optionsStr.length(); 1675 int state = S_KEY; 1676 // one more iteration than the number of characters, to flush the buffers 1677 for (int i = 0; i < (length + 1); i++) { 1678 nextEntry = false; 1679 char ch = 0; 1680 if (i < length) { 1681 ch = optionsStr.charAt(i); 1682 } 1683 switch (state) { 1684 case S_KEY: 1685 if ((ch == '|') || (ch == 0)) { 1686 nextEntry = true; 1687 valueBuffer = null; // we have to keep track of whether a value was found to trim the key correctly, and we set the value buffer to null to do this 1688 state = S_KEY; 1689 } else if (ch == '\\') { 1690 state = S_KEY_ESC; 1691 } else if (ch == ':') { 1692 state = S_VALUE; 1693 } else { 1694 keyBuffer.append(ch); 1695 } 1696 break; 1697 case S_KEY_ESC: 1698 if (ch == 0) { 1699 nextEntry = true; 1700 valueBuffer = null; 1701 } else { 1702 keyBuffer.append(ch); 1703 state = S_KEY; 1704 } 1705 break; 1706 case S_VALUE: 1707 if ((ch == '|') || (ch == 0)) { 1708 nextEntry = true; 1709 state = S_KEY; 1710 } else if (ch == '\\') { 1711 state = S_VALUE_ESC; 1712 } else { 1713 if (valueBuffer != null) { 1714 valueBuffer.append(ch); 1715 } 1716 } 1717 break; 1718 case S_VALUE_ESC: 1719 if (ch == 0) { 1720 nextEntry = true; 1721 state = S_KEY; 1722 } else { 1723 if (valueBuffer != null) { 1724 valueBuffer.append(ch); 1725 } 1726 state = S_VALUE; 1727 } 1728 break; 1729 default: 1730 } 1731 if (nextEntry) { 1732 nextEntry = false; 1733 String key = keyBuffer.toString(); 1734 // trim leading whitespace in key and trailing whitespace in value, for compatibility with splitAsMap 1735 key = key.replaceFirst("^\\s+", ""); 1736 String value; 1737 if (valueBuffer != null) { 1738 value = valueBuffer.toString(); 1739 value = value.replaceFirst("\\s+$", ""); 1740 } else { 1741 // we just have a key, so we trim it on the right as well to get the same result as splitAsMap 1742 value =""; 1743 key = key.replaceFirst("\\s+$", ""); 1744 } 1745 if (key.length() > 0) { 1746 result.put(key, value); 1747 } 1748 keyBuffer = new StringBuilder(); 1749 valueBuffer = new StringBuilder(); 1750 } 1751 } 1752 return result; 1753 } 1754 1755 /** 1756 * Substitutes a pattern in a string using a {@link I_CmsRegexSubstitution}.<p> 1757 * 1758 * @param pattern the pattern to substitute 1759 * @param text the text in which the pattern should be substituted 1760 * @param sub the substitution handler 1761 * 1762 * @return the transformed string 1763 */ 1764 public static String substitute(Pattern pattern, String text, I_CmsRegexSubstitution sub) { 1765 1766 if (text == null) { 1767 return null; 1768 } 1769 StringBuffer buffer = new StringBuffer(); 1770 Matcher matcher = pattern.matcher(text); 1771 while (matcher.find()) { 1772 matcher.appendReplacement(buffer, sub.substituteMatch(text, matcher)); 1773 } 1774 matcher.appendTail(buffer); 1775 return buffer.toString(); 1776 } 1777 1778 /** 1779 * Replaces a set of <code>searchString</code> and <code>replaceString</code> pairs, 1780 * given by the <code>substitutions</code> Map parameter.<p> 1781 * 1782 * @param source the string to scan 1783 * @param substitions the map of substitutions 1784 * 1785 * @return the substituted String 1786 * 1787 * @see #substitute(String, String, String) 1788 */ 1789 public static String substitute(String source, Map<String, String> substitions) { 1790 1791 String result = source; 1792 Iterator<Map.Entry<String, String>> it = substitions.entrySet().iterator(); 1793 while (it.hasNext()) { 1794 Map.Entry<String, String> entry = it.next(); 1795 result = substitute(result, entry.getKey(), entry.getValue().toString()); 1796 } 1797 return result; 1798 } 1799 1800 /** 1801 * Substitutes <code>searchString</code> in the given source String with <code>replaceString</code>.<p> 1802 * 1803 * This is a high-performance implementation which should be used as a replacement for 1804 * <code>{@link String#replaceAll(java.lang.String, java.lang.String)}</code> in case no 1805 * regular expression evaluation is required.<p> 1806 * 1807 * @param source the content which is scanned 1808 * @param searchString the String which is searched in content 1809 * @param replaceString the String which replaces <code>searchString</code> 1810 * 1811 * @return the substituted String 1812 */ 1813 public static String substitute(String source, String searchString, String replaceString) { 1814 1815 if (source == null) { 1816 return null; 1817 } 1818 1819 if (isEmpty(searchString)) { 1820 return source; 1821 } 1822 1823 if (replaceString == null) { 1824 replaceString = ""; 1825 } 1826 int len = source.length(); 1827 int sl = searchString.length(); 1828 int rl = replaceString.length(); 1829 int length; 1830 if (sl == rl) { 1831 length = len; 1832 } else { 1833 int c = 0; 1834 int s = 0; 1835 int e; 1836 while ((e = source.indexOf(searchString, s)) != -1) { 1837 c++; 1838 s = e + sl; 1839 } 1840 if (c == 0) { 1841 return source; 1842 } 1843 length = len - (c * (sl - rl)); 1844 } 1845 1846 int s = 0; 1847 int e = source.indexOf(searchString, s); 1848 if (e == -1) { 1849 return source; 1850 } 1851 StringBuffer sb = new StringBuffer(length); 1852 while (e != -1) { 1853 sb.append(source.substring(s, e)); 1854 sb.append(replaceString); 1855 s = e + sl; 1856 e = source.indexOf(searchString, s); 1857 } 1858 e = len; 1859 sb.append(source.substring(s, e)); 1860 return sb.toString(); 1861 } 1862 1863 /** 1864 * Substitutes the OpenCms context path (e.g. /opencms/opencms/) in a HTML page with a 1865 * special variable so that the content also runs if the context path of the server changes.<p> 1866 * 1867 * @param htmlContent the HTML to replace the context path in 1868 * @param context the context path of the server 1869 * 1870 * @return the HTML with the replaced context path 1871 */ 1872 public static String substituteContextPath(String htmlContent, String context) { 1873 1874 if (m_contextSearch == null) { 1875 m_contextSearch = "([^\\w/])" + context; 1876 m_contextReplace = "$1" + CmsStringUtil.escapePattern(CmsStringUtil.MACRO_OPENCMS_CONTEXT) + "/"; 1877 } 1878 return substitutePerl(htmlContent, m_contextSearch, m_contextReplace, "g"); 1879 } 1880 1881 /** 1882 * Substitutes searchString in content with replaceItem.<p> 1883 * 1884 * @param content the content which is scanned 1885 * @param searchString the String which is searched in content 1886 * @param replaceItem the new String which replaces searchString 1887 * @param occurences must be a "g" if all occurrences of searchString shall be replaced 1888 * 1889 * @return String the substituted String 1890 */ 1891 public static String substitutePerl(String content, String searchString, String replaceItem, String occurences) { 1892 1893 String translationRule = "s#" + searchString + "#" + replaceItem + "#" + occurences; 1894 Perl5Util perlUtil = new Perl5Util(); 1895 try { 1896 return perlUtil.substitute(translationRule, content); 1897 } catch (MalformedPerl5PatternException e) { 1898 if (LOG.isDebugEnabled()) { 1899 LOG.debug( 1900 Messages.get().getBundle().key(Messages.LOG_MALFORMED_TRANSLATION_RULE_1, translationRule), 1901 e); 1902 } 1903 } 1904 return content; 1905 } 1906 1907 /** 1908 * Returns the java String literal for the given String. <p> 1909 * 1910 * This is the form of the String that had to be written into source code 1911 * using the unicode escape sequence for special characters. <p> 1912 * 1913 * Example: "Ä" would be transformed to "\\u00C4".<p> 1914 * 1915 * @param s a string that may contain non-ascii characters 1916 * 1917 * @return the java unicode escaped string Literal of the given input string 1918 */ 1919 public static String toUnicodeLiteral(String s) { 1920 1921 StringBuffer result = new StringBuffer(); 1922 char[] carr = s.toCharArray(); 1923 1924 String unicode; 1925 for (int i = 0; i < carr.length; i++) { 1926 result.append("\\u"); 1927 // append leading zeros 1928 unicode = Integer.toHexString(carr[i]).toUpperCase(); 1929 for (int j = 4 - unicode.length(); j > 0; j--) { 1930 result.append("0"); 1931 } 1932 result.append(unicode); 1933 } 1934 return result.toString(); 1935 } 1936 1937 /** 1938 * This method transformes a string which matched a format with one or more place holders into another format. The 1939 * other format also includes the same number of place holders. Place holders start with 1940 * {@link org.opencms.util.CmsStringUtil#PLACEHOLDER_START} and end with {@link org.opencms.util.CmsStringUtil#PLACEHOLDER_END}.<p> 1941 * 1942 * @param oldFormat the original format 1943 * @param newFormat the new format 1944 * @param value the value which matched the original format and which shall be transformed into the new format 1945 * 1946 * @return the new value with the filled place holder with the information in the parameter value 1947 */ 1948 public static String transformValues(String oldFormat, String newFormat, String value) { 1949 1950 if (!oldFormat.contains(CmsStringUtil.PLACEHOLDER_START) 1951 || !oldFormat.contains(CmsStringUtil.PLACEHOLDER_END) 1952 || !newFormat.contains(CmsStringUtil.PLACEHOLDER_START) 1953 || !newFormat.contains(CmsStringUtil.PLACEHOLDER_END)) { 1954 // no place holders are set in correct format 1955 // that is why there is nothing to calculate and the value is the new format 1956 return newFormat; 1957 } 1958 //initialize the arrays with the values where the place holders starts 1959 ArrayList<Integer> oldValues = new ArrayList<Integer>(); 1960 ArrayList<Integer> newValues = new ArrayList<Integer>(); 1961 1962 // count the number of placeholders 1963 // for example these are three pairs: 1964 // old format: {.*}<b>{.*}</b>{.*} 1965 // new format: {}<strong>{}</strong>{} 1966 // get the number of place holders in the old format 1967 int oldNumber = 0; 1968 try { 1969 int counter = 0; 1970 Pattern pattern = Pattern.compile("\\{\\.\\*\\}"); 1971 Matcher matcher = pattern.matcher(oldFormat); 1972 // get the number of matches 1973 while (matcher.find()) { 1974 counter += 1; 1975 } 1976 oldValues = new ArrayList<Integer>(counter); 1977 matcher = pattern.matcher(oldFormat); 1978 while (matcher.find()) { 1979 int start = matcher.start() + 1; 1980 oldValues.add(oldNumber, Integer.valueOf(start)); 1981 oldNumber += 1; 1982 } 1983 } catch (PatternSyntaxException e) { 1984 // do nothing 1985 } 1986 // get the number of place holders in the new format 1987 int newNumber = 0; 1988 try { 1989 int counter = 0; 1990 Pattern pattern = Pattern.compile("\\{\\}"); 1991 Matcher matcher = pattern.matcher(newFormat); 1992 // get the number of matches 1993 while (matcher.find()) { 1994 counter += 1; 1995 } 1996 newValues = new ArrayList<Integer>(counter); 1997 matcher = pattern.matcher(newFormat); 1998 while (matcher.find()) { 1999 int start = matcher.start() + 1; 2000 newValues.add(newNumber, Integer.valueOf(start)); 2001 newNumber += 1; 2002 } 2003 } catch (PatternSyntaxException e) { 2004 // do nothing 2005 } 2006 // prove the numbers of place holders 2007 if (oldNumber != newNumber) { 2008 // not the same number of place holders in the old and in the new format 2009 return newFormat; 2010 } 2011 2012 // initialize the arrays with the values between the place holders 2013 ArrayList<String> oldBetween = new ArrayList<String>(oldNumber + 1); 2014 ArrayList<String> newBetween = new ArrayList<String>(newNumber + 1); 2015 2016 // get the values between the place holders for the old format 2017 // for this example with oldFormat: {.*}<b>{.*}</b>{.*} 2018 // this array is that: 2019 // --------- 2020 // | empty | 2021 // --------- 2022 // | <b> | 2023 // |-------- 2024 // | </b> | 2025 // |-------- 2026 // | empty | 2027 // |-------- 2028 int counter = 0; 2029 Iterator<Integer> iter = oldValues.iterator(); 2030 while (iter.hasNext()) { 2031 int start = iter.next().intValue(); 2032 if (counter == 0) { 2033 // the first entry 2034 if (start == 1) { 2035 // the first place holder starts at the beginning of the old format 2036 // for example: {.*}<b>... 2037 oldBetween.add(counter, ""); 2038 } else { 2039 // the first place holder starts NOT at the beginning of the old format 2040 // for example: <a>{.*}<b>... 2041 String part = oldFormat.substring(0, start - 1); 2042 oldBetween.add(counter, part); 2043 } 2044 } else { 2045 // the entries between the first and the last entry 2046 int lastStart = oldValues.get(counter - 1).intValue(); 2047 String part = oldFormat.substring(lastStart + 3, start - 1); 2048 oldBetween.add(counter, part); 2049 } 2050 counter += 1; 2051 } 2052 // the last element 2053 int lastElstart = oldValues.get(counter - 1).intValue(); 2054 if ((lastElstart + 2) == (oldFormat.length() - 1)) { 2055 // the last place holder ends at the end of the old format 2056 // for example: ...</b>{.*} 2057 oldBetween.add(counter, ""); 2058 } else { 2059 // the last place holder ends NOT at the end of the old format 2060 // for example: ...</b>{.*}</a> 2061 String part = oldFormat.substring(lastElstart + 3); 2062 oldBetween.add(counter, part); 2063 } 2064 2065 // get the values between the place holders for the new format 2066 // for this example with newFormat: {}<strong>{}</strong>{} 2067 // this array is that: 2068 // ------------| 2069 // | empty | 2070 // ------------| 2071 // | <strong> | 2072 // |-----------| 2073 // | </strong> | 2074 // |-----------| 2075 // | empty | 2076 // |-----------| 2077 counter = 0; 2078 iter = newValues.iterator(); 2079 while (iter.hasNext()) { 2080 int start = iter.next().intValue(); 2081 if (counter == 0) { 2082 // the first entry 2083 if (start == 1) { 2084 // the first place holder starts at the beginning of the new format 2085 // for example: {.*}<b>... 2086 newBetween.add(counter, ""); 2087 } else { 2088 // the first place holder starts NOT at the beginning of the new format 2089 // for example: <a>{.*}<b>... 2090 String part = newFormat.substring(0, start - 1); 2091 newBetween.add(counter, part); 2092 } 2093 } else { 2094 // the entries between the first and the last entry 2095 int lastStart = newValues.get(counter - 1).intValue(); 2096 String part = newFormat.substring(lastStart + 1, start - 1); 2097 newBetween.add(counter, part); 2098 } 2099 counter += 1; 2100 } 2101 // the last element 2102 lastElstart = newValues.get(counter - 1).intValue(); 2103 if ((lastElstart + 2) == (newFormat.length() - 1)) { 2104 // the last place holder ends at the end of the old format 2105 // for example: ...</b>{.*} 2106 newBetween.add(counter, ""); 2107 } else { 2108 // the last place holder ends NOT at the end of the old format 2109 // for example: ...</b>{.*}</a> 2110 String part = newFormat.substring(lastElstart + 1); 2111 newBetween.add(counter, part); 2112 } 2113 2114 // get the values in the place holders 2115 // for the example with: 2116 // oldFormat: {.*}<b>{.*}</b>{.*} 2117 // newFormat: {}<strong>{}</strong>{} 2118 // value: abc<b>def</b>ghi 2119 // it is used the array with the old values between the place holders to get the content in the place holders 2120 // this result array is that: 2121 // ------| 2122 // | abc | 2123 // ------| 2124 // | def | 2125 // |-----| 2126 // | ghi | 2127 // |-----| 2128 ArrayList<String> placeHolders = new ArrayList<String>(oldNumber); 2129 String tmpValue = value; 2130 // loop over all rows with the old values between the place holders and take the values between them in the 2131 // current property value 2132 for (int placeCounter = 0; placeCounter < (oldBetween.size() - 1); placeCounter++) { 2133 // get the two next values with the old values between the place holders 2134 String content = oldBetween.get(placeCounter); 2135 String nextContent = oldBetween.get(placeCounter + 1); 2136 // check the position of the first of the next values in the current property value 2137 int contPos = 0; 2138 int nextContPos = 0; 2139 if ((placeCounter == 0) && CmsStringUtil.isEmpty(content)) { 2140 // the first value in the values between the place holders is empty 2141 // for example: {.*}<p>... 2142 contPos = 0; 2143 } else { 2144 // the first value in the values between the place holders is NOT empty 2145 // for example: bla{.*}<p>... 2146 contPos = tmpValue.indexOf(content); 2147 } 2148 // check the position of the second of the next values in the current property value 2149 if (((placeCounter + 1) == (oldBetween.size() - 1)) && CmsStringUtil.isEmpty(nextContent)) { 2150 // the last value in the values between the place holders is empty 2151 // for example: ...<p>{.*} 2152 nextContPos = tmpValue.length(); 2153 } else { 2154 // the last value in the values between the place holders is NOT empty 2155 // for example: ...<p>{.*}bla 2156 nextContPos = tmpValue.indexOf(nextContent); 2157 } 2158 // every value must match the current value 2159 if ((contPos < 0) || (nextContPos < 0)) { 2160 return value; 2161 } 2162 // get the content of the current place holder 2163 String placeContent = tmpValue.substring(contPos + content.length(), nextContPos); 2164 placeHolders.add(placeCounter, placeContent); 2165 // cut off the currently visited part of the value 2166 tmpValue = tmpValue.substring(nextContPos); 2167 } 2168 2169 // build the new format 2170 // with following vectors from above: 2171 // old values between the place holders: 2172 // --------- 2173 // | empty | (old.1) 2174 // --------- 2175 // | <b> | (old.2) 2176 // |-------- 2177 // | </b> | (old.3) 2178 // |-------- 2179 // | empty | (old.4) 2180 // |-------- 2181 // 2182 // new values between the place holders: 2183 // ------------| 2184 // | empty | (new.1) 2185 // ------------| 2186 // | <strong> | (new.2) 2187 // |-----------| 2188 // | </strong> | (new.3) 2189 // |-----------| 2190 // | empty | (new.4) 2191 // |-----------| 2192 // 2193 // content of the place holders: 2194 // ------| 2195 // | abc | (place.1) 2196 // ------| 2197 // | def | (place.2) 2198 // |-----| 2199 // | ghi | (place.3) 2200 // |-----| 2201 // 2202 // the result is calculated in that way: 2203 // new.1 + place.1 + new.2 + place.2 + new.3 + place.3 + new.4 2204 String newValue = ""; 2205 // take the values between the place holders and add the content of the place holders 2206 for (int buildCounter = 0; buildCounter < newNumber; buildCounter++) { 2207 newValue = newValue + newBetween.get(buildCounter) + placeHolders.get(buildCounter); 2208 } 2209 newValue = newValue + newBetween.get(newNumber); 2210 // return the changed value 2211 return newValue; 2212 } 2213 2214 /** 2215 * Translates all consecutive sequences of non-slash characters in a path using the given resource translator. 2216 * 2217 * @param translator the resource translator 2218 * @param path the path to translate 2219 * @return the translated path 2220 */ 2221 public static String translatePathComponents(CmsResourceTranslator translator, String path) { 2222 2223 String result = substitute(NOT_SLASHES, path, (text, matcher) -> { 2224 return translator.translateResource(matcher.group()); 2225 }); 2226 return result; 2227 } 2228 2229 /** 2230 * Returns a substring of the source, which is at most length characters long.<p> 2231 * 2232 * This is the same as calling {@link #trimToSize(String, int, String)} with the 2233 * parameters <code>(source, length, " ...")</code>.<p> 2234 * 2235 * @param source the string to trim 2236 * @param length the maximum length of the string to be returned 2237 * 2238 * @return a substring of the source, which is at most length characters long 2239 */ 2240 public static String trimToSize(String source, int length) { 2241 2242 return trimToSize(source, length, length, " ..."); 2243 } 2244 2245 /** 2246 * Returns a substring of the source, which is at most length characters long, cut 2247 * in the last <code>area</code> chars in the source at a sentence ending char or whitespace.<p> 2248 * 2249 * If a char is cut, the given <code>suffix</code> is appended to the result.<p> 2250 * 2251 * @param source the string to trim 2252 * @param length the maximum length of the string to be returned 2253 * @param area the area at the end of the string in which to find a sentence ender or whitespace 2254 * @param suffix the suffix to append in case the String was trimmed 2255 * 2256 * @return a substring of the source, which is at most length characters long 2257 */ 2258 public static String trimToSize(String source, int length, int area, String suffix) { 2259 2260 if ((source == null) || (source.length() <= length)) { 2261 // no operation is required 2262 return source; 2263 } 2264 if (CmsStringUtil.isEmpty(suffix)) { 2265 // we need an empty suffix 2266 suffix = ""; 2267 } 2268 // must remove the length from the after sequence chars since these are always added in the end 2269 int modLength = length - suffix.length(); 2270 if (modLength <= 0) { 2271 // we are to short, return beginning of the suffix 2272 return suffix.substring(0, length); 2273 } 2274 int modArea = area + suffix.length(); 2275 if ((modArea > modLength) || (modArea < 0)) { 2276 // area must not be longer then max length 2277 modArea = modLength; 2278 } 2279 2280 // first reduce the String to the maximum allowed length 2281 String findPointSource = source.substring(modLength - modArea, modLength); 2282 2283 String result; 2284 // try to find an "sentence ending" char in the text 2285 int pos = lastIndexOf(findPointSource, SENTENCE_ENDING_CHARS); 2286 if (pos >= 0) { 2287 // found a sentence ender in the lookup area, keep the sentence ender 2288 result = source.substring(0, (modLength - modArea) + pos + 1) + suffix; 2289 } else { 2290 // no sentence ender was found, try to find a whitespace 2291 pos = lastWhitespaceIn(findPointSource); 2292 if (pos >= 0) { 2293 // found a whitespace, don't keep the whitespace 2294 result = source.substring(0, (modLength - modArea) + pos) + suffix; 2295 } else { 2296 // not even a whitespace was found, just cut away what's to long 2297 result = source.substring(0, modLength) + suffix; 2298 } 2299 } 2300 2301 return result; 2302 } 2303 2304 /** 2305 * Returns a substring of the source, which is at most length characters long.<p> 2306 * 2307 * If a char is cut, the given <code>suffix</code> is appended to the result.<p> 2308 * 2309 * This is almost the same as calling {@link #trimToSize(String, int, int, String)} with the 2310 * parameters <code>(source, length, length*, suffix)</code>. If <code>length</code> 2311 * if larger then 100, then <code>length* = length / 2</code>, 2312 * otherwise <code>length* = length</code>.<p> 2313 * 2314 * @param source the string to trim 2315 * @param length the maximum length of the string to be returned 2316 * @param suffix the suffix to append in case the String was trimmed 2317 * 2318 * @return a substring of the source, which is at most length characters long 2319 */ 2320 public static String trimToSize(String source, int length, String suffix) { 2321 2322 int area = (length > 100) ? length / 2 : length; 2323 return trimToSize(source, length, area, suffix); 2324 } 2325 2326 /** 2327 * Validates a value against a regular expression.<p> 2328 * 2329 * @param value the value to test 2330 * @param regex the regular expression 2331 * @param allowEmpty if an empty value is allowed 2332 * 2333 * @return <code>true</code> if the value satisfies the validation 2334 */ 2335 public static boolean validateRegex(String value, String regex, boolean allowEmpty) { 2336 2337 if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) { 2338 return allowEmpty; 2339 } 2340 Pattern pattern = Pattern.compile(regex); 2341 Matcher matcher = pattern.matcher(value); 2342 return matcher.matches(); 2343 } 2344}