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