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