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