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