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