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