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>&lt;br/&gt;</code> and HTML control characters
396     * like <code>&lt; &gt; &amp; &quot;</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>&lt;body&gt;</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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> Locale[en_EN]</code>
935     * <li><code>rabbit_en.html&nbsp;&nbsp;&nbsp;&nbsp;-> Locale[en]</code>
936     * <li><code>rabbit_en&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> Locale[en]</code>
937     * <li><code>rabbit_en.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> Locale[en]</code>
938     * <li><code>rabbit_enr&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> null</code>
939     * <li><code>rabbit_en.tar.gz&nbsp;&nbsp;-> 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: "&amp;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}