/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package com.queplix.core.utils;

import com.queplix.core.error.GenericSystemException;
import org.apache.regexp.RE;
import org.apache.regexp.RESyntaxException;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;

/**
 * Helper class for string manipulations.
 *
 * @author [ONZ] Oleg N. Zhovtanyuk
 * @author [ALB] Baranov Andrey
 * @version $Revision: 1.5 $ $Date: 2006/06/13 19:30:08 $
 */
public final class StringHelper {

    // =============================================================== Constants

    /**
     * Default empty string value.
     */
    public static final String EMPTY_VALUE = "";

    /**
     * Default empty integer value.
     */
    public static final byte EMPTY_NUMBER = -1;

    /**
     * Default value for <b>null</b> strings.
     */
    public static final String NULL_VALUE = "null";

    /**
     * End line characters sequence.
     */
    public static final String CRLF = "\r\n";

    /**
     * Default delimeter.
     */
    public static final String DEFAULT_DELIMETER = ",;\n\r";

    public static final String HTML_INDICATOR = "<!--html-->";

    // ========================================================== Initialization

    // Private constructor - blocks instantiation.

    private StringHelper() {
    }

    // ================================================================= Methods

    /**
     * Trims the string, returns <b>null</b> if empty.
     *
     * @param s the string to trim
     * @return the trimmed string or <b>null</b>, if empty
     */
    public static String trim(String s) {
        if(s != null) {
            s = s.trim();
            if(s.length() == 0) {
                s = null;
            }
        }
        return s;
    }

    /**
     * Trims the <code>StringBuffer</code>, returns <b>null</b> if empty.
     *
     * @param sb the <code>StringBuffer</code> to trim
     * @return the trimmed <code>StringBuffer</code> or <b>null</b>, if empty
     */
    public static String trim(StringBuffer sb) {
        if(sb == null || sb.length() == 0) {
            return null;
        } else {
            return trim(sb.toString());
        }
    }

    /**
     * Checks the string for emptiness.
     * <b>Null</b> and the string of space characters are treated as empty.
     *
     * @param s the string to check
     * @return <b>true</b> if empty
     */
    public static boolean isEmpty(String s) {
        return (s == null) ? true:(s.trim().length() == 0);
    }

    /**
     * Checks the character array for emptiness.
     *
     * @param cc the array of characters to check
     * @return <b>true</b> if empty
     */
    public static boolean isEmpty(char[] cc) {
        return (cc == null) ? true:isEmpty(String.valueOf(cc));
    }

    /**
     * Checks the integer value for emptiness.
     *
     * @param l integer to check
     * @return <b>true</b> if <code>i == EMPTY_NUMBER</code>
     */
    public static boolean isEmpty(long l) {
        return l == EMPTY_NUMBER;
    }

    /**
     * Compares two strings.
     * Makes no difference between the <b>null</b> values and empty strings.
     *
     * @param s1 the first string
     * @param s2 the second string
     * @return <b>true</b> if they're equal
     */
    public static boolean cmp(String s1, String s2) {
        if(isEmpty(s1) && isEmpty(s2)) {
            return true;
        } else {
            if(s1 == null || s2 == null) {
                return false;
            } else {
                return s1.equals(s2);
            }
        }
    }

    /**
     * Converts the string into boolean.
     * <p/>
     * <p/>
     * Rules:
     * <ul>
     * <li><code>true</code> | <code>1</code> - <b>true</b></li>
     * <li><code>false</code> | <code>0</code> | null string - <b>false</b></li>
     * <li>all the rest - exception</li>
     * </ul>
     * </p>
     *
     * @param s the string to convert
     * @return boolean value
     * @throws ParseException
     */
    public static boolean str2bool(String s)
            throws ParseException {

        boolean value = false;
        if(!isEmpty(s)) {
            if((s.length() == 1 && s.equals("1")) || (s.length() == 4 && s
                    .equalsIgnoreCase("true"))) {
                value = true;
            } else if((s.length() == 1 && s.equals("0")) || (s.length() == 5
                    && s.equalsIgnoreCase("false"))) {
                return false;
            } else {
                throw new ParseException("Can't convert the string to boolean",
                        0);
            }
        }
        return value;
    }

    /**
     * Converts the string into Integer removing all none-digits from the String
     *
     * @param s the string to convert
     * @return Integer value
     * @throws ParseException
     */
    public static Integer str2integer(String s)
            throws ParseException {

        Integer value = new Integer(0);

        if(!isEmpty(s)) {
            try {
                // Removing everything but digits from the String
                RE re = new RE("[^0-9]", RE.REPLACE_ALL);
                s = re.subst(s, "");

                value = new Integer(s);
            } catch (NumberFormatException e) {
                throw new ParseException(
                        "Can't convert string to integer: " + e.getMessage(),
                        0);
            } catch (RESyntaxException rex) {
                throw new GenericSystemException(
                        "Can't convert string to integer, regexp exception: "
                                + rex.getMessage(), rex);
            }
        }
        return value;
    }

    /**
     * Splits the string by the given delimiter.
     *
     * @param s            the string to split
     * @param delim        delimiter
     * @param returnDelims should it return delims or not.
     * @return tokens array
     */
    public static String[] split(String s, String delim, boolean returnDelims) {
        StringTokenizer st = new StringTokenizer(s, delim, returnDelims);
        List<String> tokens = new ArrayList<String>(st.countTokens());
        while(st.hasMoreTokens()) {
            tokens.add(st.nextToken());
        }

        String[] rc = new String[tokens.size()];
        if(rc.length > 0) {
            rc = tokens.toArray(rc);
        }
        return tokens.toArray(new String[tokens.size()]);
    }

    /**
     * Splits the string by the given delimiter.
     *
     * @param s            the string to split
     * @param delim        delimiter
     * @param returnDelims should it return delims or not.
     * @return tokens array
     */
    public static String[] split(String s, String delim) {
        return split(s, delim, false);
    }

    /**
     * Splits the string by the default delimiter.
     *
     * @param s the string to split
     * @return collection of tokens (might be empty)
     */
    public static Collection split(String s) {

        // Check for empty string.
        if(isEmpty(s)) {
            return Collections.EMPTY_LIST;
        }

        // Is HTML?
        if(isHTML(s)) {
            s = html2text(s);
        }

        // Just split.
        StringTokenizer st = new StringTokenizer(s, DEFAULT_DELIMETER);
        List tokens = new ArrayList(st.countTokens());
        while(st.hasMoreTokens()) {
            String token = st.nextToken();
            if(!isEmpty(token)) {
                tokens.add(token.trim());
            }
        }

        // Ok.
        return tokens.size() == 0 ? Collections.EMPTY_LIST:tokens;

    }

    /**
     * Splits the string (presented as character array) by the default delimiter.
     *
     * @param cc character array to split
     * @return collection of tokens (might be empty)
     */
    public static Collection split(char[] cc) {
        String s = (cc != null ? String.valueOf(cc):EMPTY_VALUE);
        return split(s);
    }

    /**
     * Joins the array of strings into one string with the delimiter.
     * Empty string are omitted.
     *
     * @param s     the strings to join
     * @param delim delimiter
     * @return joined strings
     */
    public static String join(String[] s, String delim) {
        if(s == null || s.length == 0) {
            return null;
        }

        StringBuffer sb = new StringBuffer();
        int max = s.length - 1;
        if(delim == null || delim.length() == 0) {
            for(int i = 0; i <= max; i++) {
                if(!isEmpty(s[i])) {
                    sb.append(s[i]);
                }
            }
        } else {
            for(int i = 0; i <= max; i++) {
                if(!isEmpty(s[i])) {
                    sb.append(s[i]);
                    if(i < max) {
                        sb.append(delim);
                    }
                }
            }
        }
        return sb.toString();
    }

    /**
     * Joins the array of strings into one string with the delimiter.
     * Empty strings are included.
     *
     * @param s     the strings to join
     * @param delim delimiter
     * @return joined strings
     */
    public static String fullJoin(String[] s, String delim) {

        // Data checks.
        if(s == null || s.length == 0) {
            return null;
        } else if(delim == null || delim.length() == 0) {
            throw new IllegalArgumentException("Delimiter is empty");
        }

        // Joining...
        StringBuffer sb = new StringBuffer();
        int max = s.length - 1;
        for(int i = 0; i <= max; i++) {
            if(s[i] == null) {
                sb.append(NULL_VALUE);
            } else {
                sb.append('\'').append(s[i]).append('\'');
            }
            if(i < max) {
                sb.append(delim);
            }
        }
        return sb.toString();
    }

    /**
     * Escapes the string.
     * <p/>
     * <p/>
     * Changes the entity characters (currently &lt;,&gt;, &quot;,&amp; and &apos;)
     * with the XML entities. Empty strings are returned unchanged.
     * </p>
     *
     * @param s the string to escape
     * @return XML-safe string
     */
    public static String escape(String s) {

        if(isEmpty(s)) {
            return s;
        }

        StringBuffer sb = new StringBuffer();
        int size = s.length();
        for(int i = 0; i < size; i++) {
            char c = s.charAt(i);
            switch(c) {
                case '<':
                    sb.append("&lt;");
                    break;
                case '>':
                    sb.append("&gt;");
                    break;
                case '"':
                    sb.append("&quot;");
                    break;
                case '\'':
                    sb.append("&apos;");
                    break;
                case '&':
                    sb.append("&amp;");
                    break;
                default:
                    if(c != 0) {
                        sb.append((char) c);
                    }
            }
        }

        return sb.toString();

    }

    /**
     * Unescapes the string.
     * <p/>
     * <p/>
     * Changes the XML entities (currently &lt;, &gt;, &quot;,&amp; and &apos;)
     * with the entity characters. Empty strings are returned unchanged.
     * </p>
     *
     * @param s the string to unescape
     * @return XML-unsafe string
     */
    public static String unEscape(String s) {

        if(isEmpty(s)) {
            return s;
        }

        int len = s.length();
        StringBuffer sb = new StringBuffer();
        int i = 0;
        while(i < s.length()) {
            char c = s.charAt(i);
            if(c == '&') {
                if(len >= (i + 4) && s.substring(i, i + 4).equalsIgnoreCase(
                        "&lt;")) {
                    sb.append('<');
                    i += 4;
                } else if(len >= (i + 4) && s.substring(i, i + 4)
                        .equalsIgnoreCase("&gt;")) {
                    sb.append('>');
                    i += 4;
                } else if(len >= (i + 6) && s.substring(i, i + 6)
                        .equalsIgnoreCase("&quot;")) {
                    sb.append('"');
                    i += 6;
                } else if(len >= (i + 6) && s.substring(i, i + 6)
                        .equalsIgnoreCase("&apos;")) {
                    i += 6;
                    sb.append('\\');
                } else if(len >= (i + 5) && s.substring(i, i + 5)
                        .equalsIgnoreCase("&amp;")) {
                    sb.append('&');
                    i += 5;
                } else {
                    sb.append(c);
                    i++;
                }

            } else if(c != 0) {
                sb.append(c);
                i++;
            }
        }

        return sb.toString();

    }

    /**
     * Unescapes the string with Unicode characters entities (%uXXXX;)
     * Empty strings are returned unchanged.
     *
     * @param s the string to unescape
     * @return Unicode string
     */
    public static String unEscapeUnicode(String s) {

        if(isEmpty(s)) {
            return s;
        }

        int len = s.length();
        StringBuffer sb = new StringBuffer();
        int i = 0;
        while(i < s.length()) {
            char c = s.charAt(i);
            if(len >= (i + 6) && s.substring(i, i + 2).equalsIgnoreCase("%u")) {
                String hex = s.substring(i + 2, i + 6);
                try {
                    int _c = Integer.parseInt(hex, 16);
                    sb.append((char) _c);
                    i += 6;
                } catch (NumberFormatException ex) {
                    sb.append(c);
                    i++;
                }
            } else if(c != 0) {
                sb.append(c);
                i++;
            }
        }

        return sb.toString();

    }

    /**
     * Converts the string from HTML to plain text.
     *
     * @param s the string to convert
     * @return plain-text string
     */
    public static String html2text(String s) {

        if(isEmpty(s)) {
            return s;
        }

        String result = s;
        try {

            // Clear HTML.
            result = clearHtml(result);

            // Platform-dependent linefeeds.
            RE re = new RE("\\r\\n|\\r|\\n", RE.REPLACE_ALL);
            result = re.subst(result, "");

            // <BR> -> linefeeds.
            re = new RE("<br.*?[\\/]??>",
                    RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "\n");

            // <P> -> double linefeeds.
            re = new RE("<p.*?[\\/]??>",
                    RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "\n\n");

            // IMG tag.
            re = new RE("<img\\s.*src=\"([^'\"]+)\">", RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "");

            // Removes all other tags.
            re = new RE("<[^>]+>", RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "");

            // Removes all Special Characters.
            re = new RE("&[#0-9a-z]*;", RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "");

            // Removes all XML entities.
            result = replaceXMLEntities(result);

        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }

        return result;

        
    }

    /**
     * Converts the string from XML to plain text.
     *
     * @param s the string to convert
     * @return plain-text string
     */
    public static String xml2text(String s) {

        if(isEmpty(s)) {
            return s;
        }

        String result = s;
        try {

            // Clear XML.
            result = clearXml(result);

            // Removes all tags.
            RE re = new RE("<[^>]+>",
                    RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "");

            // Removes all XML entities.
            result = replaceXMLEntities(result);

        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }

        return result;

    }

    /**
     * Converts the string from plain text to HTML.
     *
     * @param s the string to convert
     * @return HTML string
     */
    public static String text2html(String s) {
        String result = s;
        if(!isEmpty(result)) {
            try {
                RE re = new RE("\\r\\n|\\r|\\n", RE.REPLACE_ALL);
                result = re.subst(result, "<br>");
            } catch (RESyntaxException rex) {
                throw new GenericSystemException(
                        "Regexp exception: " + rex.getMessage(), rex);
            }
        }
        return "<html><body>" + result + "</body></html>";
    }

    /**
     * Converts the string from plain text to HTML.
     *
     * @param s the string to convert
     * @return HTML string
     */
    public static String text2htmlNoTag(String s) {
        String result = s;
        if(!isEmpty(result)) {
            try {
                RE re = new RE("\\r\\n|\\r|\\n", RE.REPLACE_ALL);
                result = re.subst(result, "<br>");
            } catch (RESyntaxException rex) {
                throw new GenericSystemException(
                        "Regexp exception: " + rex.getMessage(), rex);
            }
        }
        return result;
    }

    /**
     * Removes "bad" characters from XML.
     * Valid symbols: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
     * (| [#x10000-#x10FFFF])???
     *
     * @param s the string to convert
     * @return cleared XML
     */
    public static String clear(String s) {

        // [ALB] don't use StringHelper#isEmpty here!
        if(s == null) {
            return s;
        }

        StringBuffer sb = null;
        String result = s;
        int length = result.length();

        for(int i = 0; i < length; i++) {
            char c = result.charAt(i);

            if((c >= '\u0020' && c <= '\ud7ff')
                    || (c == 9)
                    || (c == 10)
                    || (c == 13)
                    || (c >= '\ue000' && c <= '\ufffd')) {

                // Symbol is valid
                if(sb != null) {
                    // StringBuffer is created - just append valid symbol...
                    sb.append(c);
                }

            } else {
                // Symbol is not valid
                if(sb == null) {
                    // first occurance - create new StringBuffer
                    sb = new StringBuffer(result.substring(0, i));
                }
            }
        }

        if(sb != null) {
            // if StringBuffer is created - take value from it
            result = sb.toString();
        }

        return result;
    }

    /**
     * Removes system XML tag <?xml .. ?>, XMS schema and DTD references from XML.
     *
     * @param s the string to convert
     * @return cleared XML
     */
    public static String clearXml(String s) {
        return clearXml(s, true);
    }

    /**
     * Removes system XML tag <?xml .. ?>, XMS schema and DTD references from XML.
     *
     * @param s         the string to convert
     * @param clearText clear bad characters
     * @return cleared XML
     */
    public static String clearXml(String s, boolean clearText) {

        if(isEmpty(s)) {
            return s;
        }

        // Remove special xml tag.
        String result = s;
        int pos = s.indexOf("?>");
        if(pos > 0) {
            result = s.substring(pos + 2).trim();
        }

        if(clearText) {
            // Removes "bad" characters.
            return clear(result);
        } else {
            return result;
        }
    }

    /**
     * Removes system HTML tags (<html>,<body>,<head>...) from HTML.
     *
     * @param s the string to convert
     * @return cleared HTML
     */
    public static String clearHtml(String s) {
        return clearHtml(s, true);
    }

    /**
     * Removes system HTML tags (<html>,<body>,<head>...) from HTML.
     *
     * @param s         the string to convert
     * @param clearText clear bad characters
     * @return cleared HTML
     */
    public static String clearHtml(String s, boolean clearText) {

        if(isEmpty(s)) {
            return s;
        }

        String result = s;
        try {
            // Removes <!DOCTYPE>, <HTML> and <BODY> tags.
            RE re = new RE(
                    "<!DOCTYPE[^>]*>|<HTML[^>]*>|<BODY[^>]*>|<\\/HTML>|<\\/BODY>",
                    RE.REPLACE_ALL | RE.MATCH_CASEINDEPENDENT | RE
                            .MATCH_MULTILINE);
            result = re.subst(result, "\n");

            // Removes <HEAD> <SCRIPT> ... - with content.
            // [MVT] Excludes <STYLE> from processing to provide proper email body display.
            String[] _ss = new String[]{
                    "HEAD",
                    "SCRIPT"
            };
            for(int i = 0; i < _ss.length; i++) {
                String _s = _ss[i];
                re = new RE("<" + _s + "[^>]*>",
                        RE.MATCH_CASEINDEPENDENT | RE.MATCH_MULTILINE);

                if(re.match(result)) {
                    int start = re.getParenStart(0);
                    re = new RE("<\\/" + _s + ">", RE.MATCH_CASEINDEPENDENT);
                    if(re.match(result, start)) {
                        int end = re.getParenEnd(0);
                        result = result.substring(0, start) + result.substring(
                                end);
                    }
                }
            }

        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }

        if(clearText) {
            // Removes "bad" characters.
            return clear(result);
        } else {
            return result;
        }
    }

    /**
     * Checks whether the given string is XML.
     *
     * @param s the string to check
     * @return <b>true</b> if XML
     */
    public static boolean isXML(String s) {
        if(isEmpty(s)) {
            return false;
        }
        try {
            RE re = new RE("<\\?xml((\\s+[^>]*)|(>))",
                    RE.MATCH_CASEINDEPENDENT);
            return re.match(s);
        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }
    }

    /**
     * Checks whether the given character array is XML.
     *
     * @param chs the character array to check
     * @return <b>true</b> if XML
     */
    public static boolean isXML(char[] chs) {
        if(chs == null) {
            return false;
        }

        return isXML(String.valueOf(chs));
    }

    /**
     * Checks whether the given string is HTML.
     *
     * @param s the string to check
     * @return <b>true</b> if HTML
     */
    public static boolean isHTML(String s) {
        if(isEmpty(s)) {
            return false;
        }
        try {
            RE re = new RE("<html((\\s+[^>]*)|(>))", RE.MATCH_CASEINDEPENDENT);
            if(re.match(s)) {
                return true;
            }
            if(s.indexOf(HTML_INDICATOR) != -1) {
                return true;
            } else {
                return false;
            }
        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }
    }

    /**
     * Checks whether the given character array is HTML.
     *
     * @param chs the character array to check
     * @return <b>true</b> if HTML
     */
    public static boolean isHTML(char[] chs) {
        if(chs == null) {
            return false;
        }

        return isHTML(String.valueOf(chs));
    }

    /**
     * Performs a case-independent search for the substring in the given string.
     * Returns <code>false</code>, if any parameter is empty.
     *
     * @param s         the string to search in
     * @param substring the substring to search for
     * @return <b>true</b> if found
     */
    public static boolean find(String s, String substring) {
        boolean result = false;
        if(isEmpty(s) || isEmpty(substring)) {
            result = false;
        } else {
            result = (s.toLowerCase().indexOf(substring.toLowerCase()) >= 0);
        }
        return result;
    }

    /**
     * Converts the string from SQL to Java format.
     * <p/>
     * <p/>
     * If the string is empty or contains just 1 character it is not changed.
     * Otherwise all double quotas are replaced with the single ones.
     * </p>
     *
     * @param s the string to convert
     * @return converted string
     * @see #java2sql(String)
     */
    public static String sql2java(String s) {
        if(isEmpty(s) || s.length() < 2) {
            return s;
        }
        s = s.substring(1, s.length() - 1);
        String result = s;
        try {
            RE re = new RE("''", RE.REPLACE_ALL);
            result = re.subst(result, "'");
        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }
        return result;
    }

    /**
     * Converts the string from Java to SQL format.
     *
     * @param s the string to convert
     * @return converted string
     * @see #sql2java(String)
     */
    public static String java2sql(String s) {
        if(isEmpty(s)) {
            return "''";
        }
        String result = s;
        try {
            RE re = new RE("'", RE.REPLACE_ALL);
            result = re.subst(result, "''");
        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }
        return "'" + result + "'";
    }

    /**
     * Formats the message using <code>MessageFormat</code> class
     *
     * @param pattern the message pattern to format
     * @param args    arguments
     * @return formatted string
     */
    public static String format(String pattern, Object[] args) {
        if(args != null) {
            Object[] __args = new Object[args.length];
            for(int i = 0; i < args.length; i++) {
                Object o = args[i];
                if(o == null) {
                    o = NULL_VALUE;
                }
                __args[i] = o;
            }
            pattern = MessageFormat.format(pattern, __args);
        }
        return pattern;
    }

    /**
     * Make the printable string (e. g. <code>[e1, e2, ...]</code>)
     * from the integer array.
     *
     * @param l the integer array
     * @return the printable string
     */
    public static String toString(long[] l) {
        String str = NULL_VALUE;
        if(l != null && l.length > 0) {
            StringBuffer sb = new StringBuffer();
            sb.append("[");
            sb.append(l[0]);
            for(int i = 0; i < l.length; i++) {
                sb.append(", ");
                sb.append(l[i]);
            }
            sb.append("]");
            str = sb.toString();
        }
        return str;
    }

    /**
     * Make the printable string (e. g. <code>[e1, e2, ...]</code>)
     * from the string array.
     *
     * @param s the string array
     * @return the printable string
     */
    public static String toString(String[] s) {
        String str = NULL_VALUE;
        if(s != null && s.length > 0) {
            str = "[" + fullJoin(s, ", ") + "]";
        }
        return str;
    }

    /**
     * Replaces all regular expression matches with the given replacement text.
     *
     * @param s           string to process
     * @param regex       regular expression
     * @param replacement replacement text
     * @return modified string
     */
    public static String replaceFirst(String s, String regex,
                                      String replacement) {
        if(s == null) {
            return null;
        }
        try {
            RE re = new RE(regex, RE.REPLACE_FIRSTONLY);
            return re.subst(s, replacement);
        } catch (RESyntaxException ex) {
            throw new GenericSystemException("RE exception: " + ex.getMessage(),
                    ex);
        }
    }

    /**
     * Replaces first regular expression match with the given replacement text.
     *
     * @param s           string to process
     * @param regex       regular expression
     * @param replacement replacement text
     * @return modified string
     */
    public static String replaceAll(String s, String regex,
                                    String replacement) {
        if(s == null) {
            return null;
        }
        try {
            RE re = new RE(regex, RE.REPLACE_ALL);
            return re.subst(s, replacement);
        } catch (RESyntaxException ex) {
            throw new GenericSystemException("RE exception: " + ex.getMessage(),
                    ex);
        }
    }

    /**
     * Converts the XML entities to ANSI symbols.
     *
     * @param s the string to convert
     * @return new string
     */
    public static String replaceXMLEntities(String s) {

        if(isEmpty(s)) {
            return s;
        }

        String result = s;
        try {

            // Non-blanking spaces.
            RE re = new RE("&nbsp;", RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, " ");

            // Entity for 'greater' sign.
            re = new RE("&gt;", RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, ">");

            // Entity for 'lesser' sign.
            re = new RE("&lt;", RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "<");

            // Entity for 'quotation' sign.
            re = new RE("&quot;", RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "\"");

            // Entity for 'apostrof' sign.
            re = new RE("&apos;", RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "'");

            // Entity for 'lesser' sign.
            re = new RE("&amp;", RE.MATCH_CASEINDEPENDENT + RE.REPLACE_ALL);
            result = re.subst(result, "&");

        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }

        return result;
    }

    public static String getExceptionStacktrace(Throwable cause) {
        StringWriter stringWriter = new StringWriter();
        cause.printStackTrace(new PrintWriter(stringWriter, true));
        return stringWriter.toString();
    }
}
