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