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