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, new Character(c), new Integer(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", new Byte(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     * Substitutes a pattern in a string using a {@link I_CmsRegexSubstitution}.<p>
1653     *
1654     * @param pattern the pattern to substitute
1655     * @param text the text in which the pattern should be substituted
1656     * @param sub the substitution handler
1657     *
1658     * @return the transformed string
1659     */
1660    public static String substitute(Pattern pattern, String text, I_CmsRegexSubstitution sub) {
1661
1662        if (text == null) {
1663            return null;
1664        }
1665        StringBuffer buffer = new StringBuffer();
1666        Matcher matcher = pattern.matcher(text);
1667        while (matcher.find()) {
1668            matcher.appendReplacement(buffer, sub.substituteMatch(text, matcher));
1669        }
1670        matcher.appendTail(buffer);
1671        return buffer.toString();
1672    }
1673
1674    /**
1675     * Replaces a set of <code>searchString</code> and <code>replaceString</code> pairs,
1676     * given by the <code>substitutions</code> Map parameter.<p>
1677     *
1678     * @param source the string to scan
1679     * @param substitions the map of substitutions
1680     *
1681     * @return the substituted String
1682     *
1683     * @see #substitute(String, String, String)
1684     */
1685    public static String substitute(String source, Map<String, String> substitions) {
1686
1687        String result = source;
1688        Iterator<Map.Entry<String, String>> it = substitions.entrySet().iterator();
1689        while (it.hasNext()) {
1690            Map.Entry<String, String> entry = it.next();
1691            result = substitute(result, entry.getKey(), entry.getValue().toString());
1692        }
1693        return result;
1694    }
1695
1696    /**
1697     * Substitutes <code>searchString</code> in the given source String with <code>replaceString</code>.<p>
1698     *
1699     * This is a high-performance implementation which should be used as a replacement for
1700     * <code>{@link String#replaceAll(java.lang.String, java.lang.String)}</code> in case no
1701     * regular expression evaluation is required.<p>
1702     *
1703     * @param source the content which is scanned
1704     * @param searchString the String which is searched in content
1705     * @param replaceString the String which replaces <code>searchString</code>
1706     *
1707     * @return the substituted String
1708     */
1709    public static String substitute(String source, String searchString, String replaceString) {
1710
1711        if (source == null) {
1712            return null;
1713        }
1714
1715        if (isEmpty(searchString)) {
1716            return source;
1717        }
1718
1719        if (replaceString == null) {
1720            replaceString = "";
1721        }
1722        int len = source.length();
1723        int sl = searchString.length();
1724        int rl = replaceString.length();
1725        int length;
1726        if (sl == rl) {
1727            length = len;
1728        } else {
1729            int c = 0;
1730            int s = 0;
1731            int e;
1732            while ((e = source.indexOf(searchString, s)) != -1) {
1733                c++;
1734                s = e + sl;
1735            }
1736            if (c == 0) {
1737                return source;
1738            }
1739            length = len - (c * (sl - rl));
1740        }
1741
1742        int s = 0;
1743        int e = source.indexOf(searchString, s);
1744        if (e == -1) {
1745            return source;
1746        }
1747        StringBuffer sb = new StringBuffer(length);
1748        while (e != -1) {
1749            sb.append(source.substring(s, e));
1750            sb.append(replaceString);
1751            s = e + sl;
1752            e = source.indexOf(searchString, s);
1753        }
1754        e = len;
1755        sb.append(source.substring(s, e));
1756        return sb.toString();
1757    }
1758
1759    /**
1760     * Substitutes the OpenCms context path (e.g. /opencms/opencms/) in a HTML page with a
1761     * special variable so that the content also runs if the context path of the server changes.<p>
1762     *
1763     * @param htmlContent the HTML to replace the context path in
1764     * @param context the context path of the server
1765     *
1766     * @return the HTML with the replaced context path
1767     */
1768    public static String substituteContextPath(String htmlContent, String context) {
1769
1770        if (m_contextSearch == null) {
1771            m_contextSearch = "([^\\w/])" + context;
1772            m_contextReplace = "$1" + CmsStringUtil.escapePattern(CmsStringUtil.MACRO_OPENCMS_CONTEXT) + "/";
1773        }
1774        return substitutePerl(htmlContent, m_contextSearch, m_contextReplace, "g");
1775    }
1776
1777    /**
1778     * Substitutes searchString in content with replaceItem.<p>
1779     *
1780     * @param content the content which is scanned
1781     * @param searchString the String which is searched in content
1782     * @param replaceItem the new String which replaces searchString
1783     * @param occurences must be a "g" if all occurrences of searchString shall be replaced
1784     *
1785     * @return String the substituted String
1786     */
1787    public static String substitutePerl(String content, String searchString, String replaceItem, String occurences) {
1788
1789        String translationRule = "s#" + searchString + "#" + replaceItem + "#" + occurences;
1790        Perl5Util perlUtil = new Perl5Util();
1791        try {
1792            return perlUtil.substitute(translationRule, content);
1793        } catch (MalformedPerl5PatternException e) {
1794            if (LOG.isDebugEnabled()) {
1795                LOG.debug(
1796                    Messages.get().getBundle().key(Messages.LOG_MALFORMED_TRANSLATION_RULE_1, translationRule),
1797                    e);
1798            }
1799        }
1800        return content;
1801    }
1802
1803    /**
1804     * Returns the java String literal for the given String. <p>
1805     *
1806     * This is the form of the String that had to be written into source code
1807     * using the unicode escape sequence for special characters. <p>
1808     *
1809     * Example: "&Auml" would be transformed to "\\u00C4".<p>
1810     *
1811     * @param s a string that may contain non-ascii characters
1812     *
1813     * @return the java unicode escaped string Literal of the given input string
1814     */
1815    public static String toUnicodeLiteral(String s) {
1816
1817        StringBuffer result = new StringBuffer();
1818        char[] carr = s.toCharArray();
1819
1820        String unicode;
1821        for (int i = 0; i < carr.length; i++) {
1822            result.append("\\u");
1823            // append leading zeros
1824            unicode = Integer.toHexString(carr[i]).toUpperCase();
1825            for (int j = 4 - unicode.length(); j > 0; j--) {
1826                result.append("0");
1827            }
1828            result.append(unicode);
1829        }
1830        return result.toString();
1831    }
1832
1833    /**
1834     * This method transformes a string which matched a format with one or more place holders into another format. The
1835     * other format also includes the same number of place holders. Place holders start with
1836     * {@link org.opencms.util.CmsStringUtil#PLACEHOLDER_START} and end with {@link org.opencms.util.CmsStringUtil#PLACEHOLDER_END}.<p>
1837     *
1838     * @param oldFormat the original format
1839     * @param newFormat the new format
1840     * @param value the value which matched the original format and which shall be transformed into the new format
1841     *
1842     * @return the new value with the filled place holder with the information in the parameter value
1843     */
1844    public static String transformValues(String oldFormat, String newFormat, String value) {
1845
1846        if (!oldFormat.contains(CmsStringUtil.PLACEHOLDER_START)
1847            || !oldFormat.contains(CmsStringUtil.PLACEHOLDER_END)
1848            || !newFormat.contains(CmsStringUtil.PLACEHOLDER_START)
1849            || !newFormat.contains(CmsStringUtil.PLACEHOLDER_END)) {
1850            // no place holders are set in correct format
1851            // that is why there is nothing to calculate and the value is the new format
1852            return newFormat;
1853        }
1854        //initialize the arrays with the values where the place holders starts
1855        ArrayList<Integer> oldValues = new ArrayList<Integer>();
1856        ArrayList<Integer> newValues = new ArrayList<Integer>();
1857
1858        // count the number of placeholders
1859        // for example these are three pairs:
1860        // old format: {.*}<b>{.*}</b>{.*}
1861        // new format: {}<strong>{}</strong>{}
1862        // get the number of place holders in the old format
1863        int oldNumber = 0;
1864        try {
1865            int counter = 0;
1866            Pattern pattern = Pattern.compile("\\{\\.\\*\\}");
1867            Matcher matcher = pattern.matcher(oldFormat);
1868            // get the number of matches
1869            while (matcher.find()) {
1870                counter += 1;
1871            }
1872            oldValues = new ArrayList<Integer>(counter);
1873            matcher = pattern.matcher(oldFormat);
1874            while (matcher.find()) {
1875                int start = matcher.start() + 1;
1876                oldValues.add(oldNumber, new Integer(start));
1877                oldNumber += 1;
1878            }
1879        } catch (PatternSyntaxException e) {
1880            // do nothing
1881        }
1882        // get the number of place holders in the new format
1883        int newNumber = 0;
1884        try {
1885            int counter = 0;
1886            Pattern pattern = Pattern.compile("\\{\\}");
1887            Matcher matcher = pattern.matcher(newFormat);
1888            // get the number of matches
1889            while (matcher.find()) {
1890                counter += 1;
1891            }
1892            newValues = new ArrayList<Integer>(counter);
1893            matcher = pattern.matcher(newFormat);
1894            while (matcher.find()) {
1895                int start = matcher.start() + 1;
1896                newValues.add(newNumber, new Integer(start));
1897                newNumber += 1;
1898            }
1899        } catch (PatternSyntaxException e) {
1900            // do nothing
1901        }
1902        // prove the numbers of place holders
1903        if (oldNumber != newNumber) {
1904            // not the same number of place holders in the old and in the new format
1905            return newFormat;
1906        }
1907
1908        // initialize the arrays with the values between the place holders
1909        ArrayList<String> oldBetween = new ArrayList<String>(oldNumber + 1);
1910        ArrayList<String> newBetween = new ArrayList<String>(newNumber + 1);
1911
1912        // get the values between the place holders for the old format
1913        // for this example with oldFormat: {.*}<b>{.*}</b>{.*}
1914        // this array is that:
1915        // ---------
1916        // | empty |
1917        // ---------
1918        // | <b>   |
1919        // |--------
1920        // | </b>  |
1921        // |--------
1922        // | empty |
1923        // |--------
1924        int counter = 0;
1925        Iterator<Integer> iter = oldValues.iterator();
1926        while (iter.hasNext()) {
1927            int start = iter.next().intValue();
1928            if (counter == 0) {
1929                // the first entry
1930                if (start == 1) {
1931                    // the first place holder starts at the beginning of the old format
1932                    // for example: {.*}<b>...
1933                    oldBetween.add(counter, "");
1934                } else {
1935                    // the first place holder starts NOT at the beginning of the old format
1936                    // for example: <a>{.*}<b>...
1937                    String part = oldFormat.substring(0, start - 1);
1938                    oldBetween.add(counter, part);
1939                }
1940            } else {
1941                // the entries between the first and the last entry
1942                int lastStart = oldValues.get(counter - 1).intValue();
1943                String part = oldFormat.substring(lastStart + 3, start - 1);
1944                oldBetween.add(counter, part);
1945            }
1946            counter += 1;
1947        }
1948        // the last element
1949        int lastElstart = oldValues.get(counter - 1).intValue();
1950        if ((lastElstart + 2) == (oldFormat.length() - 1)) {
1951            // the last place holder ends at the end of the old format
1952            // for example: ...</b>{.*}
1953            oldBetween.add(counter, "");
1954        } else {
1955            // the last place holder ends NOT at the end of the old format
1956            // for example: ...</b>{.*}</a>
1957            String part = oldFormat.substring(lastElstart + 3);
1958            oldBetween.add(counter, part);
1959        }
1960
1961        // get the values between the place holders for the new format
1962        // for this example with newFormat: {}<strong>{}</strong>{}
1963        // this array is that:
1964        // ------------|
1965        // | empty     |
1966        // ------------|
1967        // | <strong>  |
1968        // |-----------|
1969        // | </strong> |
1970        // |-----------|
1971        // | empty     |
1972        // |-----------|
1973        counter = 0;
1974        iter = newValues.iterator();
1975        while (iter.hasNext()) {
1976            int start = iter.next().intValue();
1977            if (counter == 0) {
1978                // the first entry
1979                if (start == 1) {
1980                    // the first place holder starts at the beginning of the new format
1981                    // for example: {.*}<b>...
1982                    newBetween.add(counter, "");
1983                } else {
1984                    // the first place holder starts NOT at the beginning of the new format
1985                    // for example: <a>{.*}<b>...
1986                    String part = newFormat.substring(0, start - 1);
1987                    newBetween.add(counter, part);
1988                }
1989            } else {
1990                // the entries between the first and the last entry
1991                int lastStart = newValues.get(counter - 1).intValue();
1992                String part = newFormat.substring(lastStart + 1, start - 1);
1993                newBetween.add(counter, part);
1994            }
1995            counter += 1;
1996        }
1997        // the last element
1998        lastElstart = newValues.get(counter - 1).intValue();
1999        if ((lastElstart + 2) == (newFormat.length() - 1)) {
2000            // the last place holder ends at the end of the old format
2001            // for example: ...</b>{.*}
2002            newBetween.add(counter, "");
2003        } else {
2004            // the last place holder ends NOT at the end of the old format
2005            // for example: ...</b>{.*}</a>
2006            String part = newFormat.substring(lastElstart + 1);
2007            newBetween.add(counter, part);
2008        }
2009
2010        // get the values in the place holders
2011        // for the example with:
2012        //   oldFormat: {.*}<b>{.*}</b>{.*}
2013        //   newFormat: {}<strong>{}</strong>{}
2014        //   value: abc<b>def</b>ghi
2015        // it is used the array with the old values between the place holders to get the content in the place holders
2016        // this result array is that:
2017        // ------|
2018        // | abc |
2019        // ------|
2020        // | def |
2021        // |-----|
2022        // | ghi |
2023        // |-----|
2024        ArrayList<String> placeHolders = new ArrayList<String>(oldNumber);
2025        String tmpValue = value;
2026        // loop over all rows with the old values between the place holders and take the values between them in the
2027        // current property value
2028        for (int placeCounter = 0; placeCounter < (oldBetween.size() - 1); placeCounter++) {
2029            // get the two next values with the old values between the place holders
2030            String content = oldBetween.get(placeCounter);
2031            String nextContent = oldBetween.get(placeCounter + 1);
2032            // check the position of the first of the next values in the current property value
2033            int contPos = 0;
2034            int nextContPos = 0;
2035            if ((placeCounter == 0) && CmsStringUtil.isEmpty(content)) {
2036                // the first value in the values between the place holders is empty
2037                // for example: {.*}<p>...
2038                contPos = 0;
2039            } else {
2040                // the first value in the values between the place holders is NOT empty
2041                // for example: bla{.*}<p>...
2042                contPos = tmpValue.indexOf(content);
2043            }
2044            // check the position of the second of the next values in the current property value
2045            if (((placeCounter + 1) == (oldBetween.size() - 1)) && CmsStringUtil.isEmpty(nextContent)) {
2046                // the last value in the values between the place holders is empty
2047                // for example: ...<p>{.*}
2048                nextContPos = tmpValue.length();
2049            } else {
2050                // the last value in the values between the place holders is NOT empty
2051                // for example: ...<p>{.*}bla
2052                nextContPos = tmpValue.indexOf(nextContent);
2053            }
2054            // every value must match the current value
2055            if ((contPos < 0) || (nextContPos < 0)) {
2056                return value;
2057            }
2058            // get the content of the current place holder
2059            String placeContent = tmpValue.substring(contPos + content.length(), nextContPos);
2060            placeHolders.add(placeCounter, placeContent);
2061            // cut off the currently visited part of the value
2062            tmpValue = tmpValue.substring(nextContPos);
2063        }
2064
2065        // build the new format
2066        // with following vectors from above:
2067        // old values between the place holders:
2068        // ---------
2069        // | empty | (old.1)
2070        // ---------
2071        // | <b>   | (old.2)
2072        // |--------
2073        // | </b>  | (old.3)
2074        // |--------
2075        // | empty | (old.4)
2076        // |--------
2077        //
2078        // new values between the place holders:
2079        // ------------|
2080        // | empty     | (new.1)
2081        // ------------|
2082        // | <strong>  | (new.2)
2083        // |-----------|
2084        // | </strong> | (new.3)
2085        // |-----------|
2086        // | empty     | (new.4)
2087        // |-----------|
2088        //
2089        // content of the place holders:
2090        // ------|
2091        // | abc | (place.1)
2092        // ------|
2093        // | def | (place.2)
2094        // |-----|
2095        // | ghi | (place.3)
2096        // |-----|
2097        //
2098        // the result is calculated in that way:
2099        // new.1 + place.1 + new.2 + place.2 + new.3 + place.3 + new.4
2100        String newValue = "";
2101        // take the values between the place holders and add the content of the place holders
2102        for (int buildCounter = 0; buildCounter < newNumber; buildCounter++) {
2103            newValue = newValue + newBetween.get(buildCounter) + placeHolders.get(buildCounter);
2104        }
2105        newValue = newValue + newBetween.get(newNumber);
2106        // return the changed value
2107        return newValue;
2108    }
2109
2110    /**
2111     * Translates all consecutive sequences of non-slash characters in a path using the given resource translator.
2112     *
2113     * @param translator the resource translator
2114     * @param path the path to translate
2115     * @return the translated path
2116     */
2117    public static String translatePathComponents(CmsResourceTranslator translator, String path) {
2118
2119        String result = substitute(NOT_SLASHES, path, (text, matcher) -> {
2120            return translator.translateResource(matcher.group());
2121        });
2122        return result;
2123    }
2124
2125    /**
2126     * Returns a substring of the source, which is at most length characters long.<p>
2127     *
2128     * This is the same as calling {@link #trimToSize(String, int, String)} with the
2129     * parameters <code>(source, length, " ...")</code>.<p>
2130     *
2131     * @param source the string to trim
2132     * @param length the maximum length of the string to be returned
2133     *
2134     * @return a substring of the source, which is at most length characters long
2135     */
2136    public static String trimToSize(String source, int length) {
2137
2138        return trimToSize(source, length, length, " ...");
2139    }
2140
2141    /**
2142     * Returns a substring of the source, which is at most length characters long, cut
2143     * in the last <code>area</code> chars in the source at a sentence ending char or whitespace.<p>
2144     *
2145     * If a char is cut, the given <code>suffix</code> is appended to the result.<p>
2146     *
2147     * @param source the string to trim
2148     * @param length the maximum length of the string to be returned
2149     * @param area the area at the end of the string in which to find a sentence ender or whitespace
2150     * @param suffix the suffix to append in case the String was trimmed
2151     *
2152     * @return a substring of the source, which is at most length characters long
2153     */
2154    public static String trimToSize(String source, int length, int area, String suffix) {
2155
2156        if ((source == null) || (source.length() <= length)) {
2157            // no operation is required
2158            return source;
2159        }
2160        if (CmsStringUtil.isEmpty(suffix)) {
2161            // we need an empty suffix
2162            suffix = "";
2163        }
2164        // must remove the length from the after sequence chars since these are always added in the end
2165        int modLength = length - suffix.length();
2166        if (modLength <= 0) {
2167            // we are to short, return beginning of the suffix
2168            return suffix.substring(0, length);
2169        }
2170        int modArea = area + suffix.length();
2171        if ((modArea > modLength) || (modArea < 0)) {
2172            // area must not be longer then max length
2173            modArea = modLength;
2174        }
2175
2176        // first reduce the String to the maximum allowed length
2177        String findPointSource = source.substring(modLength - modArea, modLength);
2178
2179        String result;
2180        // try to find an "sentence ending" char in the text
2181        int pos = lastIndexOf(findPointSource, SENTENCE_ENDING_CHARS);
2182        if (pos >= 0) {
2183            // found a sentence ender in the lookup area, keep the sentence ender
2184            result = source.substring(0, (modLength - modArea) + pos + 1) + suffix;
2185        } else {
2186            // no sentence ender was found, try to find a whitespace
2187            pos = lastWhitespaceIn(findPointSource);
2188            if (pos >= 0) {
2189                // found a whitespace, don't keep the whitespace
2190                result = source.substring(0, (modLength - modArea) + pos) + suffix;
2191            } else {
2192                // not even a whitespace was found, just cut away what's to long
2193                result = source.substring(0, modLength) + suffix;
2194            }
2195        }
2196
2197        return result;
2198    }
2199
2200    /**
2201     * Returns a substring of the source, which is at most length characters long.<p>
2202     *
2203     * If a char is cut, the given <code>suffix</code> is appended to the result.<p>
2204     *
2205     * This is almost the same as calling {@link #trimToSize(String, int, int, String)} with the
2206     * parameters <code>(source, length, length*, suffix)</code>. If <code>length</code>
2207     * if larger then 100, then <code>length* = length / 2</code>,
2208     * otherwise <code>length* = length</code>.<p>
2209     *
2210     * @param source the string to trim
2211     * @param length the maximum length of the string to be returned
2212     * @param suffix the suffix to append in case the String was trimmed
2213     *
2214     * @return a substring of the source, which is at most length characters long
2215     */
2216    public static String trimToSize(String source, int length, String suffix) {
2217
2218        int area = (length > 100) ? length / 2 : length;
2219        return trimToSize(source, length, area, suffix);
2220    }
2221
2222    /**
2223     * Validates a value against a regular expression.<p>
2224     *
2225     * @param value the value to test
2226     * @param regex the regular expression
2227     * @param allowEmpty if an empty value is allowed
2228     *
2229     * @return <code>true</code> if the value satisfies the validation
2230     */
2231    public static boolean validateRegex(String value, String regex, boolean allowEmpty) {
2232
2233        if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) {
2234            return allowEmpty;
2235        }
2236        Pattern pattern = Pattern.compile(regex);
2237        Matcher matcher = pattern.matcher(value);
2238        return matcher.matches();
2239    }
2240}