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