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