au.org.ala.delta.util.Utils.java Source code

Java tutorial

Introduction

Here is the source code for au.org.ala.delta.util.Utils.java

Source

/*******************************************************************************
 * Copyright (C) 2011 Atlas of Living Australia
 * All Rights Reserved.
 * 
 * The contents of this file are subject to the Mozilla Public
 * License Version 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.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 ******************************************************************************/
package au.org.ala.delta.util;

import au.org.ala.delta.rtf.RTFUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.FloatRange;

import javax.swing.*;
import java.awt.*;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class Utils {

    public static void centreWindow(Window c, JFrame frame) {
        Dimension app = frame.getSize();
        int x = frame.getX() + (app.width - c.getWidth()) / 2;
        int y = frame.getY() + (app.height - c.getHeight()) / 3;
        if (y < frame.getY()) {
            y = frame.getY();
        }
        c.setLocation(x, y);
    }

    public static String fixedWidth(String str, int length) {
        if (str == null) {
            str = "";
        }

        if (str.length() > length) {
            return ".." + str.substring(str.length() - (length - 2));
        } else {
            // Pad out to 15 chars.
            StringBuilder result = new StringBuilder();
            result.append(str);
            for (int i = result.length(); i < length; i++) {
                result.append(' ');
            }
            return result.toString();
        }
    }

    public static short LOWORD(int dword) {
        return (short) (dword & 0x0000ffff);
    }

    public static short HIWORD(int dword) {
        return (short) ((dword & 0xffff0000) >> 16);
    }

    public static long dateToFILETIME(Date d) {
        return (d.getTime() + 11644473600000L) * 10000L;
    }

    public static Date FILETIMEToDate(long FILETIME) {
        return new Date((FILETIME / 10000L) - 11644473600000L);
    }

    public static int strtol(String buf) {
        return strtol(buf, null, 10);
    }

    public static int strtol(String buf, int[] endpos) {
        return strtol(buf, endpos, 10);
    }

    public static int strtol(String buf, int[] endpos, int radix) {
        StringBuffer digits = new StringBuffer();
        int i = 0;
        for (; i < buf.length(); ++i) {
            char ch = buf.charAt(i);
            if (Character.isDigit(ch) || (i == 0 && ch == '-')
                    || ((radix > 10) && ((ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122)))) {
                digits.append(ch);
            } else {
                break;
            }
        }

        if (endpos != null && endpos.length > 0) {
            endpos[0] = i;
        }

        if (digits.length() > 0) {
            return Integer.parseInt(digits.toString(), radix);
        } else {
            return 0;
        }

    }

    public static <T extends Comparable<T>> int lowerBound(List<T> list, int startPos, int endPos, T n) {
        if (startPos >= list.size()) {
            return list.size();
        }

        if (endPos > list.size() - 1) {
            endPos = list.size() - 1;
        }

        for (int i = startPos; i <= endPos; ++i) {
            if (list.get(i).compareTo(n) >= 0) {
                return i;
            }
        }

        return list.size();
    }

    /**
     * A slightly more tolerant version of the BigDecimal constructor - we allow
     * the valid number to be followed by non-numeric characters at the end of
     * the string.
     * 
     * @param src
     *            the String to parse into a BigDecimal.
     * @return the length of the portion of the string containing a parsable
     *         number.
     */
    public static BigDecimal stringToBigDecimal(String src, int[] endPos) {

        int endIndex = src.length();
        while (!Character.isDigit(src.charAt(endIndex - 1))) {
            endIndex--;
        }
        BigDecimal value = new BigDecimal(src.substring(0, endIndex));

        endPos[0] = endIndex;
        return value;
    }

    /**
     * @deprecated Use {@link RTFUtils#stripFormatting(String)}
     * @param text
     * @return
     */
    @Deprecated
    public static byte[] RTFToUTF8(String text) {
        // Same as RTFToANSI, overall. But returns a string of UTF8 encoded
        // Unicode,
        // rather than ANSI.
        // We first build up a UCS2 "wide" string, then convert it to UTF8
        boolean hadControl = false;

        char[] wideBuf = new char[text.length()];

        int outPos = 0;
        if (text.length() > 0) {
            int[] RTFstart = new int[] { 0 };
            int[] RTFnext = new int[] { 0 };
            int prevStart;
            do {
                prevStart = RTFstart[0] = RTFnext[0];
                char aChar = GetNextChar(text, RTFstart, RTFnext);
                if (aChar != 0) {
                    wideBuf[outPos++] = aChar;
                }
                if (!hadControl && RTFstart[0] != prevStart) {
                    hadControl = true;
                }
            } while (RTFnext[0] < (int) text.length());
        }

        StringBuilder b = new StringBuilder();
        for (char ch : wideBuf) {
            if (ch == 0) {
                break; // simulate null terminated
            }
            b.append(ch);
        }
        String str = b.toString();
        try {
            return str.getBytes("UTF-8");
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static String RTFToANSI(String text) {

        boolean hadControl = false;

        StringBuffer out = new StringBuffer();

        if (text.length() > 0) {
            int[] RTFstart = new int[] { 0 };
            int[] RTFnext = new int[] { 0 };
            // int outPos = 0;
            int prevStart;
            do {
                prevStart = RTFstart[0] = RTFnext[0];
                char aChar = (char) LOWORD(GetNextChar(text, RTFstart, RTFnext));
                if (aChar != 0) {
                    out.append(aChar);
                }
                if (!hadControl && RTFstart[0] != prevStart)
                    hadControl = true;
            } while (RTFnext[0] < (int) text.length());
        }
        return out.toString();
    }

    public static class NextCharResult {
        public int retVal;
        public int startPos;
        public int endPos;
    }

    public static class RTFcmdReplace {
        public RTFcmdReplace(String a, String b, char c) {
            cmdString = a;
            repString = b;
            unicodeValue = c;
        }

        public String cmdString;
        public String repString;
        public char unicodeValue;
    }

    static RTFcmdReplace[] RTFreps = new RTFcmdReplace[] { new RTFcmdReplace("par", "\r\n", (char) 0x0d),
            new RTFcmdReplace("line", "\r\n", (char) 0x0b), new RTFcmdReplace("tab", "\t", (char) 0x09),
            new RTFcmdReplace("page", "\f", (char) 0x0c), new RTFcmdReplace("lquote", "\221", (char) 0x2018), // 145
            // ANSI
            new RTFcmdReplace("rquote", "\222", (char) 0x2019), // 146
            new RTFcmdReplace("ldblquote", "\223", (char) 0x201c), // 147
            new RTFcmdReplace("rdblquote", "\224", (char) 0x201d), // 148
            new RTFcmdReplace("bullet", "\225", (char) 0x2022), // 149
            new RTFcmdReplace("endash", "\226", (char) 0x2013), // 150 UGH!
            // Microsoft got
            // it
            new RTFcmdReplace("emdash", "\227", (char) 0x2014), // 151 wrong
            // again! When
            // Vers. 2
            new RTFcmdReplace("enspace", " ", (char) 0x2002), // RichEdit
            // controls
            // stream these
            // in with this
            // form,
            new RTFcmdReplace("emspace", " ", (char) 0x2003) // they're
                                                             // converted to
                                                             // plain ol'
                                                             // dashes and
                                                             // spaces
    };

    static int nRTFCmds = RTFreps.length;

    static String[] skipWords = new String[] { "author",
            // "buptim",
            "colortbl",
            // "comment",
            // "company",
            // "creatim",
            // "doccomm",
            "fonttbl",
            // "footer",
            // "footerf",
            // "footerl",
            // "footerr",
            // "footnote",
            // "ftncn",
            // "ftnsep",
            // "header",
            // "headerf",
            // "headerl",
            // "heaerr",
            // "info",
            // "keywords",
            // "listtable",
            // "operator",
            // "pict",
            // "printim",
            // "private1",
            // "revtim",
            // "rxe",
            // "stylesheet",
            "subject",
            // "tc",
            "title",
            // "txe",
            // "xe"
    };

    static int nSkipWords = skipWords.length;

    // Obtain the next single printable character encoded in an RTF string
    // Begins the search at the position passed in "startPos".
    // Returns the value of the character. If the character was in Unicode,
    // the low word of the result gives its Unicode value, and the high word
    // its "substitution" value.
    // On return, "startPos" points to the position at which the encoding of
    // the character began (which might be on an RTF \), and "endPos" returns
    // the position immediately after the end of the character's encoding.
    // This will be trivial for most text, (where "startPos" will point to the
    // character, and "endPos" to the next position), but for Unicode characters
    // or other
    // "special" characters (like \lquote), it gets more complicated.
    // Note that the calling function must be careful about setting startPos
    // correctly.
    // If it gets things wrong, parts of RTF command strings might look like
    // text.
    public static char GetNextChar(String RTFString, int[] startPos, int[] endPos) {
        char result = 0;
        int skipLevel = 0;
        endPos[0] = RTFString.length();
        while (result == 0 && startPos[0] < endPos[0]) {
            char ch = RTFString.charAt(startPos[0]);
            if (ch == '{' || ch == '}') {
                ++startPos[0];
                if (skipLevel != 0) {
                    if (ch == '{') {
                        ++skipLevel;
                    } else {
                        --skipLevel;
                    }
                }
            } else if (skipLevel != 0) {
                ++startPos[0];
            } else if (ch == '\\') {
                int cmdStart = startPos[0] + 1;
                if (cmdStart >= endPos[0]) {
                    // A pathological case - not actually good RTF

                    result = ch;
                } else {
                    ch = RTFString.charAt(cmdStart);
                    if (Character.isLetter(ch)) {
                        int[] curPos = new int[] { cmdStart };
                        while (++curPos[0] < endPos[0] && Character.isLetter(RTFString.charAt(curPos[0]))) {
                        }

                        String test = RTFString.substring(cmdStart, cmdStart + curPos[0] - cmdStart);

                        int numStart = curPos[0];
                        boolean hasParam = false;
                        if (curPos[0] < endPos[0] && (RTFString.charAt(curPos[0]) == '-'
                                || Character.isDigit(RTFString.charAt(curPos[0])))) {
                            hasParam = true;
                            while (++curPos[0] < endPos[0] && Character.isDigit(RTFString.charAt(curPos[0]))) {
                            }
                        }

                        if (curPos[0] < endPos[0] && RTFString.charAt(curPos[0]) == ' ') {
                            ++curPos[0];
                        }

                        for (int i = 0; i < nSkipWords; ++i) {
                            if (skipWords[i] == test) {
                                skipLevel = 1;
                                break;
                            }
                        }
                        if (skipLevel != 0) {

                        } else if (test == "u") {
                            // Actually had RTF unicode...
                            result = (char) Integer.parseInt(RTFString.substring(numStart, curPos[0] - numStart));
                            char ansiVal = GetNextChar(RTFString, curPos, endPos);
                            curPos[0] = endPos[0];
                            result |= ansiVal << 16;
                        } else if (!hasParam) {
                            // Currently match only parameter-less commands
                            for (int i = 0; i < nRTFCmds; ++i) {
                                if (RTFreps[i].cmdString == test) {
                                    result = RTFreps[i].unicodeValue;
                                    if (result > 0x100)
                                        result |= (char) RTFreps[i].repString.charAt(0) << 16;
                                }
                            }
                        }
                        if (result != 0) {
                            // && endPos == RTFString.size())

                            endPos[0] = curPos[0];
                        } else {
                            startPos[0] = curPos[0];
                        }
                    } else if (ch == '{' || ch == '}' || ch == '\\') {
                        result = ch;
                        endPos[0] = cmdStart + 1;
                    } else if (ch == '~') {
                        result = 0xa0;
                        endPos[0] = cmdStart + 1;
                    } else if (ch == '-') {
                        result = 0xad;
                        endPos[0] = cmdStart + 1;
                    } else if (ch == '\'' && cmdStart + 2 < endPos[0]) {
                        char[] buff = new char[2];
                        buff[0] = RTFString.charAt(cmdStart + 1);
                        buff[1] = RTFString.charAt(cmdStart + 2);

                        result = (char) Integer.parseInt(new String(buff), 16);
                        endPos[0] = cmdStart + 1 + 2;
                    } else {
                        result = ch;
                        endPos[0] = cmdStart + 1;
                    }
                }
            } else if (!Character.isISOControl(ch) || ch >= 0x80) {
                if (ch >= 0x80 && ch < 0xa0) {
                    result = (char) (winANSIChars[ch - 0x80] | ch << 16);
                } else {
                    result = ch;
                }
                endPos[0] = startPos[0] + 1;
            } else
                ++startPos[0];
        }

        if ((result >> 16) == 0)
            result |= (result << 16);

        return result;
    }

    static char[] winANSIChars = new char[] { 0x20AC, // 
            0x81, // ?
            0x201A, // 
            0x192, // 
            0x201E, // 
            0x2026, // 
            0x2020, // 
            0x2021, // 
            0x2C6, // 
            0x2030, // 
            0x160, // 
            0x2039, // 
            0x152, // 
            0x8D, // ?
            0x17D, // 
            0x8F, // ?
            0x90, // ?
            0x2018, // 
            0x2019, // 
            0x201C, // 
            0x201D, // ?
            0x2022, // 
            0x2013, // 
            0x2014, // 
            0x2DC, // 
            0x2122, // 
            0x161, // 
            0x203A, // 
            0x153, // 
            0x9D, // ?
            0x17E, // 
            0x178 // 
    };

    /**
     * Calls removeComments with the supplied level and all other parameters =
     * false.
     * 
     * @param text
     *            the text to remove comments from.
     * @param level
     *            identifies the extent of the removal operation.
     * @return the supplied text without comments
     */
    public static String removeComments(String text, int level) {
        return removeComments(text, level, false, false, false, false);
    }

    /**
     * Removes DELTA style <> comments from the supplied string.
     * 
     * @param text
     *            the string to remove comments from.
     * @param level
     *            0 = don't remove, 1 = remove all, 2 = remove only if other
     *            text, 3 = same as 2, but outer brackets are removed if
     *            commented text is used.
     * @return the string with comments removed
     */
    public static String removeComments(String text, int level, boolean convertCommentsToBrackets,
            boolean removeInnerComments, boolean stripSpaces, boolean removeBrackets) {

        int mode = level;

        int commentLevel = 0;
        boolean hasText = mode == 1;
        boolean hadInner = false;
        char ch;
        int i, curStart = -1, start = -1, end = -1;
        int innerStart = -1;
        boolean wasSpace = true;
        boolean wasBrace = false;
        // TODO despaceRTF(text);
        if (stripSpaces) {
            text = stripExtraSpaces(text);
        }
        StringBuilder result = new StringBuilder(text);

        for (i = 0; i < result.length(); ++i) { // Work through string
            // Is character an opening bracket?
            if (result.charAt(i) == '<'
                    && (wasSpace || wasBrace || (ch = result.charAt(i - 1)) == ' ' || ch == '<' || ch == '>')) {
                wasBrace = true;
                if (convertCommentsToBrackets) {
                    result.setCharAt(i, ')');
                }
                if (removeBrackets || (mode == 3 && commentLevel == 0)) {
                    result.deleteCharAt(i--);
                }
                if (commentLevel == 0) {
                    curStart = i;
                    if (start == -1)
                        start = i;
                } else if (commentLevel == 1) {
                    innerStart = i;
                    hadInner = true;
                }
                // Keep track of nesting level
                commentLevel++;
            }
            // Was it a closing bracket?
            else if (result.charAt(i) == '>' && commentLevel > 0 && result.charAt(i - 1) != '|'
                    && (i + 1 == result.length() || (ch = result.charAt(i + 1)) == ' ' || ch == '<' || ch == '>')) {
                // Keep track of nesting level
                commentLevel--;
                wasBrace = true;
                if (convertCommentsToBrackets)
                    result.setCharAt(i, ')');
                if (removeBrackets || (mode == 3 && commentLevel == 0))
                    result.deleteCharAt(i--);
                if (commentLevel == 0) {
                    if (start != -1) {
                        end = i;
                        if (removeInnerComments && hadInner) // In this case,
                                                             // check for
                                                             // and remove an empty
                                                             // comment...
                        {
                            int leng = end - curStart - 1;
                            String contents = result.substring(curStart + 1, end - 1);
                            contents = stripExtraSpaces(contents);
                            if (contents.isEmpty() || contents == " ") {
                                result.delete(curStart, end - 1);
                                i = curStart;
                            } else if (stripSpaces && contents.length() != leng) {
                                result.replace(curStart + 1, curStart + leng, contents);
                                i -= leng - contents.length();
                            }
                        }
                    }
                    hadInner = false;
                } else if (commentLevel == 1 && removeInnerComments) {
                    // If we're removing inner comments, get rid of this
                    // part of the string, and any space before it.
                    int leng = i - innerStart + 1;
                    result.delete(innerStart, innerStart + leng);
                    i = innerStart - 1;
                    while (result.length() > i && result.charAt(i) == ' ')
                        result.deleteCharAt(i--);
                }
            } else if (commentLevel == 0 && (hasText || result.charAt(i) != ' ')) {
                hasText = true;
                wasBrace = false;
                wasSpace = (end == i - 1 && i > 0);
                if (end != -1 && mode > 0) {
                    result.delete(start, end + 1);
                    i -= end - start + 2;
                    // Hmm. How SHOULD spaces around the removed comments
                    // be treated? This erases the spaces BEFORE the comment
                    while (i >= 0 && result.length() > i && result.charAt(i) == ' ')
                        result.deleteCharAt(i--);
                    start = -1;
                    end = -1;
                }
            } else
                wasBrace = false;
        }
        if (end != -1 && hasText && mode > 0) {
            result.delete(start, end + 1);
            for (i = result.length() - 1; i >= 0 && result.charAt(i) == ' '; --i)
                result.deleteCharAt(i);
        }
        return result.toString();
    }

    // Strip extra spaces from a string. This means reducing multiple spaces to
    // a
    // single space AND stripping leading and trailing spaces from comments
    public static String stripExtraSpaces(String str) {
        // TODO Needs to be done properly!
        String tmp = str.replaceAll("  ", " ");
        return tmp.trim();

    }

    public static String getVersionFromManifest() {
        String versionString = Utils.class.getPackage().getImplementationVersion();
        return versionString;
    }

    private static final int BYTES_IN_MEGABTYE = 1048576;

    public static String generateSystemInfo(String applicationTitle) {

        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzzz", Locale.ENGLISH);
        Calendar cal = Calendar.getInstance();
        Date currentTime = cal.getTime();

        // Free, max and total memory should be written out in megabytes
        long freeMemory = Runtime.getRuntime().freeMemory() / BYTES_IN_MEGABTYE;
        long maxMemory = Runtime.getRuntime().maxMemory() / BYTES_IN_MEGABTYE;
        long totalMemory = Runtime.getRuntime().totalMemory() / BYTES_IN_MEGABTYE;

        StringBuilder versionInfo = new StringBuilder();
        versionInfo.append(applicationTitle + " " + getVersionFromManifest());
        versionInfo.append("\n");
        versionInfo.append("date: ");
        versionInfo.append(df.format(currentTime));
        versionInfo.append("\n");
        versionInfo.append("free memory: ");
        versionInfo.append(freeMemory);
        versionInfo.append(" MB \n");
        versionInfo.append("total memory: ");
        versionInfo.append(totalMemory);
        versionInfo.append(" MB \n");
        versionInfo.append("max memory: ");
        versionInfo.append(maxMemory);
        versionInfo.append(" MB\n");
        versionInfo.append("java.version: ");
        versionInfo.append(System.getProperty("java.version"));
        versionInfo.append("\n");
        versionInfo.append("java.vendor: ");
        versionInfo.append(System.getProperty("java.vendor"));
        versionInfo.append("\n");
        versionInfo.append("os.name: ");
        versionInfo.append(System.getProperty("os.name"));
        versionInfo.append("\n");
        versionInfo.append("os.arch: ");
        versionInfo.append(System.getProperty("os.arch"));
        versionInfo.append("\n");
        versionInfo.append("os.version: ");
        versionInfo.append(System.getProperty("os.version"));
        versionInfo.append("\n");
        versionInfo.append("user.language: ");
        versionInfo.append(System.getProperty("user.language"));
        versionInfo.append("\n");
        versionInfo.append("user.region: ");
        versionInfo.append(System.getProperty("user.region"));
        versionInfo.append("\n");
        versionInfo.append("user.dir: ");
        versionInfo.append(System.getProperty("user.dir"));

        return versionInfo.toString();
    }

    /**
     * The main job of this method is to terminate RTF control words with {}
     * instead of a space.
     */
    // Not all cases are handled correctly in the current code.
    // For example, text with \bin might not always give correct results
    // A few other things, such as \'xx, should perhaps also be given
    // explicit treatment, but should not substantially affect the outcome.
    public static String despaceRtf(String text, boolean quoteDelims) {
        if (StringUtils.isEmpty(text)) {
            return "";
        }
        int srcPos;
        boolean inRTF = false;
        boolean inParam = false;
        boolean inUnicode = false;
        boolean bracketed = text.charAt(0) == '<' && text.charAt(text.length() - 1) == '>';

        StringBuilder outputText = new StringBuilder(text);
        if (bracketed) // If a "comment", temporarily chop off the terminating
                       // bracket
            outputText.setLength(outputText.length() - 1);
        for (srcPos = 0; srcPos < outputText.length(); ++srcPos) {
            char ch = outputText.charAt(srcPos);
            // Always convert a tab character into a \tab control word
            if (ch == '\t') {
                outputText.replace(srcPos, srcPos + 1, "\\tab{}");
                ch = '\\';
            }
            if (inRTF) {
                if (Character.isDigit(ch) || (!inParam && ch == '-')) {
                    if (!inParam && outputText.charAt(srcPos - 1) == 'u' && outputText.charAt(srcPos - 2) == '\\')
                        inUnicode = true;
                    inParam = true;
                } else if (inParam || !Character.isLetter(ch)) {
                    boolean wasInUnicode = inUnicode;
                    inUnicode = inParam = inRTF = false;
                    if (Character.isSpaceChar(ch)) {
                        // Check for the absence of a control; when this
                        // happens,
                        // the terminating character IS the control word!
                        if (srcPos > 0 && outputText.charAt(srcPos - 1) == '\\') {
                            // \<NEWLINE> is treated as a \par control. We make
                            // this
                            // change here explicitly, to make it more apparent.
                            // But should we keep the <NEWLINE> character around
                            // as well,
                            // as a clue for breaking lines during output?
                            if (ch == '\n' || ch == '\r') {
                                // text.replace(--srcPos, 2, "\\par{}");
                                outputText.insert(srcPos, "par{}");
                                srcPos += 5;
                            }
                            // (Note that if we don't catch this here, replacing
                            // "\ " could yield
                            // "\{}" which is WRONG. But rather than just get
                            // rid of this, it
                            // is probably better to replace with {} to ensure
                            // that any preceding
                            // RTF is terminated)
                            else if (ch == ' ') {
                                outputText.replace(srcPos - 1, 2, "{}");
                            }
                        }
                        // This is the chief condition we are trying to fix.
                        // Terminate the RTF
                        // control phrase with {} instead of white space...
                        // But if the terminator is a new line, we keep it
                        // around
                        // for assistance in wrapping output lines.
                        // else if (ch == '\n')
                        // {
                        // text.insert(srcPos, "{}");
                        // srcPos += 2;
                        // }
                        else if (ch != '\n') {
                            outputText.setCharAt(srcPos, '{');
                            outputText.insert(++srcPos, '}');
                        }
                    }
                    // No reason to do the following. Probably better to leave
                    // the
                    // character quoted.
                    // Reinstated 8 December 1999 because we need to be sure
                    // all text is in a consistent state when linking characters
                    // One exception - if the quoted character is a Unicode
                    // "replacement"
                    // character, we'd better leave it quoted.
                    else if (ch == '\'' && !wasInUnicode && srcPos + 2 < outputText.length()) {
                        char[] buff = new char[3];
                        buff[0] = outputText.charAt(srcPos + 1);
                        buff[1] = outputText.charAt(srcPos + 2);
                        buff[2] = 0;

                        int[] endPos = new int[1];
                        int value = strtol(new String(buff), endPos, 16);
                        if ((endPos[0] == 2) && value > 127 && outputText.charAt(srcPos - 1) == '\\') {

                            srcPos--;
                            outputText.replace(srcPos, srcPos + 4, new String(new char[] { (char) value }));

                        }
                    } else if (ch == '\\' && outputText.charAt(srcPos - 1) != '\\') // Terminates
                                                                                    // RTF,
                                                                                    // but
                                                                                    // starts
                                                                                    // new
                                                                                    // RTF
                    {
                        inRTF = true;
                        if (wasInUnicode && srcPos + 1 < outputText.length()
                                && outputText.charAt(srcPos + 1) == '\'')
                            inUnicode = true;
                    } else if (ch == '>') {
                        // Append a space after the RTF (it was probably
                        // stripped by the attribute parsing)
                        outputText.insert(srcPos, "{}");
                    }
                }
            } else if (ch == '\\')
                inRTF = true;
            // TEST - to allow outputting of a "*" or "#" character in arbitrary
            // text...
            else if (quoteDelims && (ch == '*' || ch == '#')
                    && (srcPos == 0 || Character.isSpaceChar(outputText.charAt(srcPos - 1)))) {
                // //char buffer[5];
                // Always build a 4-character replacement string, like:
                // \'20
                // //sprintf(buffer, "\\\'%2.2x", (int)ch);
                // //text.replace(srcPos, buffer, 4);
                // //srcPos += 3;
                outputText.insert(srcPos, "{}");
                srcPos += 2;
            }
        }
        if (inRTF)
            outputText.append("{}");
        if (bracketed)
            outputText.append('>');
        return outputText.toString();
    }

    /**
     * Capitalises the first word in the supplied text (which may contain RTF
     * markup) the first letter of the word is preceded by a '|'.
     * 
     * @param text
     *            the text to capitalise.
     * @return the text with the first word capitalised.
     */
    public static String capitaliseFirstWord(String text) {
        if (StringUtils.isEmpty(text)) {
            return text;
        }

        StringBuilder tmp = new StringBuilder();
        tmp.append(text);
        int index = 0;
        while (index >= 0 && index < text.length() && !Character.isLetterOrDigit(tmp.charAt(index))) {
            if (tmp.charAt(index) == '\\') {
                index = RTFUtils.skipKeyword(text, index);

                if (index < 0 || index >= tmp.length() || Character.isLetterOrDigit(tmp.charAt(index))) {
                    break;
                }
            }
            index++;
        }

        if (index >= 0 && index < text.length() && Character.isLetter(tmp.charAt(index))) {
            if ((index == 0) || (tmp.charAt(index - 1) != '|')) {
                tmp.setCharAt(index, Character.toUpperCase(tmp.charAt(index)));
            } else if (tmp.charAt(index - 1) == '|') {
                tmp.deleteCharAt(index - 1);
            }
        }
        return tmp.toString();
    }

    /**
     * Unzips the supplied zip file
     * 
     * @param zip
     *            The zip file to extract
     * @param destinationDir
     *            the directory to extract the zip to
     * @throws IOException
     *             if an error occurred while extracting the zip file.
     */
    public static void extractZipFile(File zip, File destinationDir) throws IOException {
        if (!zip.exists()) {
            throw new IllegalArgumentException("zip file does not exist");
        }

        if (!zip.isFile()) {
            throw new IllegalArgumentException("supplied zip file is not a file");
        }

        if (!destinationDir.exists()) {
            throw new IllegalArgumentException("destination does not exist");
        }

        if (!destinationDir.isDirectory()) {
            throw new IllegalArgumentException("destination is not a directory");
        }

        ZipFile zipFile = new ZipFile(zip);
        Enumeration<? extends ZipEntry> entries = zipFile.entries();

        while (entries.hasMoreElements()) {
            ZipEntry entry = entries.nextElement();
            String entryName = entry.getName();

            File fileForEntry = new File(destinationDir, entryName);
            if (entry.isDirectory() && !fileForEntry.exists()) {
                fileForEntry.mkdirs();
            } else {
                InputStream is = new BufferedInputStream(zipFile.getInputStream(entry));
                FileUtils.copyInputStreamToFile(is, fileForEntry);
            }
        }
    }

    /**
     * Adjust the supplied font size. Apply scaling based on the 72 dpi assumed
     * by java and the current screen resolution
     * 
     * Need to adjust the font size as Java 2D assumes 72 dpi. From the Java 2D
     * FAQ:
     * 
     * Q: Why does (eg) a 10 pt font in Java applications appear to have a
     * different size from the same font at 10pt in a native application?
     * 
     * A: Conversion from the size in points into device pixels depends on
     * device resolution as reported by the platform APIs. Java 2D defaults to
     * assuming 72 dpi. Platform defaults vary. Mac OS also uses 72 dpi. Linux
     * desktops based on GTK (Gnome) or Qt (KDE) typically default to 96 dpi and
     * let the end-user customise what they want to use. Windows defaults to 96
     * dpi (VGA resolution) and also offers 120 dpi (large fonts size) and lets
     * users further specify a custom resolution. So a couple of things can now
     * be seen
     * 
     * The DPI reported by platform APIs likely has no correspondence to the
     * true DPI of the display device Its unlikely that Java 2D's default
     * matches the platform default. So a typical results is that for Window's
     * default 96 DPI that a 10 pt font in a Java application is 72/96 of the
     * size of the native counterpart.
     * 
     * Note that Swing's Windows and GTK L&Fs do scale fonts based on the system
     * DPI to match the desktop. If you want to do the same in your application
     * you can call java.awt.Toolkit.getScreenResolution() and use this to apply
     * a simple scale to the size you specify for fonts.
     * 
     * 
     * @param fontSize
     *            the font size
     * @return the font size, adjusted from the default 72 dpi assumed by java
     *         2d to the screen resolution. See comment above.
     */
    public static int adjustFontSize(int fontSize) {
        int screenRes = Toolkit.getDefaultToolkit().getScreenResolution();
        return adjustFontSize(fontSize, screenRes);
    }

    /**
     * Adjust the supplied font size. Apply scaling based on the 72 dpi assumed
     * by java and the current screen resolution
     * 
     * Need to adjust the font size as Java 2D assumes 72 dpi. From the Java 2D
     * FAQ:
     * 
     * Q: Why does (eg) a 10 pt font in Java applications appear to have a
     * different size from the same font at 10pt in a native application?
     * 
     * A: Conversion from the size in points into device pixels depends on
     * device resolution as reported by the platform APIs. Java 2D defaults to
     * assuming 72 dpi. Platform defaults vary. Mac OS also uses 72 dpi. Linux
     * desktops based on GTK (Gnome) or Qt (KDE) typically default to 96 dpi and
     * let the end-user customise what they want to use. Windows defaults to 96
     * dpi (VGA resolution) and also offers 120 dpi (large fonts size) and lets
     * users further specify a custom resolution. So a couple of things can now
     * be seen
     * 
     * The DPI reported by platform APIs likely has no correspondence to the
     * true DPI of the display device Its unlikely that Java 2D's default
     * matches the platform default. So a typical results is that for Window's
     * default 96 DPI that a 10 pt font in a Java application is 72/96 of the
     * size of the native counterpart.
     * 
     * Note that Swing's Windows and GTK L&Fs do scale fonts based on the system
     * DPI to match the desktop. If you want to do the same in your application
     * you can call java.awt.Toolkit.getScreenResolution() and use this to apply
     * a simple scale to the size you specify for fonts.
     * 
     * 
     * @param fontSize
     *            the font size
     * @param targetDPI the resolution at which the font will be displayed           
     * @return the font size, adjusted from the default 72 dpi assumed by java
     *         2d to the target DPI. See comment above.
     */
    public static int adjustFontSize(int fontSize, int targetDPI) {
        int adjustedFontSize = (int) Math.round(Math.abs(fontSize) * (double) targetDPI / 72.0);
        return adjustedFontSize;
    }

    /**
     * Performs the opposite operation to adjustFontSize.
     */
    public static int adjustFontInfoSize(int fontInfoSize, int targetDPI) {
        return (int) Math.round(Math.abs(fontInfoSize) * 72.0 / (double) targetDPI);
    }

    /**
     * Format a list of integers as a set of ranges, with spaces as the list
     * item separator and "-" as the range symbol
     * 
     * @param ints
     *            the list of integers, this is assumed to be already sorted.
     * @return The formated list of ranges
     */
    public static String formatIntegersAsListOfRanges(List<Integer> ints) {
        return formatIntegersAsListOfRanges(ints, " ", "-");
    }

    /**
     * Format a list of integers as a set of ranges, with spaces as the list
     * item separator and the supplied string as the range symbol
     * 
     * @param ints
     *            the list of integers, this is assumed to be already sorted.
     * @param rangeSymbol
     *            the symbol to use as the range symbol
     * @return the formatted list of ranges
     */
    public static String formatIntegersAsListOfRanges(List<Integer> ints, String rangeSymbol) {
        return formatIntegersAsListOfRanges(ints, " ", rangeSymbol);
    }

    /**
     * Format a list of integers as a set of ranges, with supplied strings used
     * for the list item separator and range symbol
     * 
     * @param ints
     *            the list of integers, this is assumed to be already sorted.
     * @param itemSeparator
     *            the symbol to use as the list item separator.
     * @param rangeSymbol
     *            the symbol to use as the range symbol
     * @return the formatted list of ranges
     */
    public static String formatIntegersAsListOfRanges(List<Integer> ints, String itemSeparator,
            String rangeSymbol) {
        StringBuilder builder = new StringBuilder();

        int startRange = 0;
        int previousValue = 0;

        if (ints.size() == 0) {
            return StringUtils.EMPTY;
        } else if (ints.size() == 1) {
            return Integer.toString(ints.get(0));
        } else {
            for (int i = 0; i < ints.size(); i++) {
                int val = ints.get(i);

                if (i == 0) {
                    startRange = val;
                } else {
                    if (previousValue < val - 1) {
                        builder.append(itemSeparator);
                        builder.append(startRange);

                        if (previousValue != startRange) {
                            builder.append(rangeSymbol);
                            builder.append(previousValue);
                        }

                        startRange = val;

                    }

                    if (i == ints.size() - 1) {
                        builder.append(itemSeparator);
                        builder.append(startRange);

                        if (val != startRange) {
                            builder.append(rangeSymbol);
                            builder.append(val);
                        }

                        startRange = val;
                    }
                }

                previousValue = val;
            }

            String retStr = builder.toString();

            // Remove any leading or trailing space symbol
            if (retStr.startsWith(itemSeparator)) {
                retStr = retStr.substring(1);
            }

            if (retStr.endsWith(itemSeparator)) {
                retStr = retStr.substring(0, retStr.length() - 1);
            }

            return retStr;
        }
    }

    public static String formatFloatRangeAsString(FloatRange range) {
        StringBuilder builder = new StringBuilder();
        float minimumValue = range.getMinimumFloat();
        float maximumValue = range.getMaximumFloat();
        if (minimumValue == maximumValue) {
            if (minimumValue == Math.round(minimumValue)) {
                builder.append((int) minimumValue);
            } else {
                builder.append(minimumValue);
            }
        } else {
            if (minimumValue == Math.round(minimumValue)) {
                builder.append((int) minimumValue);
            } else {
                builder.append(minimumValue);
            }
            builder.append("-");
            if (maximumValue == Math.round(maximumValue)) {
                builder.append((int) maximumValue);
            } else {
                builder.append(maximumValue);
            }
        }

        return builder.toString();
    }

    /**
     * Return a file object for the file at the supplied path (may be a relative
     * path)
     * 
     * @param filePath
     *            the path of the file - may be a relative path
     * @param defaultDirectory
     *            the default parent directory - this directory will be used as
     *            the parent directory if the filePath is not absolute
     * @return
     */
    public static File createFileFromPath(String filePath, File defaultDirectory) {
        File file = null;
        // If the supplied file path starts with one of the file system
        // roots, then it is absolute. Otherwise, assume that
        // it is relative to the directory in which the dataset is located.
        filePath = FilenameUtils.separatorsToSystem(filePath);
        boolean fileAbsolute = false;
        for (File root : File.listRoots()) {
            if (filePath.toLowerCase().startsWith(root.getAbsolutePath().toLowerCase())) {
                fileAbsolute = true;
                break;
            }
        }

        if (fileAbsolute) {
            file = new File(filePath);
        } else {
            file = new File(defaultDirectory, filePath);
        }

        return file;
    }

    /**
     * Use the values of the bits in the supplied array of bytes to create a
     * single array of boolean values
     */
    public static boolean[] byteArrayToBooleanArray(byte[] bArray) {
        boolean[] boolArray = new boolean[bArray.length * Byte.SIZE];

        for (int i = 0; i < bArray.length; i++) {
            byte b = bArray[i];
            for (int j = 0; j < Byte.SIZE; j++) {
                if ((b & (1 << j)) > 0) {
                    boolArray[i * Byte.SIZE + j] = true;
                } else {
                    boolArray[i * Byte.SIZE + j] = false;
                }
            }
        }

        return boolArray;
    }

    public static void launchIntkeyInSeparateProcess(String startupDirectory, String inputFile) throws Exception {
        if (!launchIntkeyViaExe(startupDirectory, inputFile)) {
            if (!launchIntkeyViaScript(startupDirectory, inputFile)) {
                launchIntkeyViaClassLoader(inputFile);
            }
        }
    }

    private static boolean launchIntkeyViaExe(String startupDirectory, String inputFile) {
        if (Platform.isWindows()) {
            String exeFile = "Intkey.exe";
            String fullpath = String.format("%s%s%s", startupDirectory, File.separator, exeFile);
            if (launchFile(fullpath, inputFile)) {
                return true;
            }
        }
        return false;
    }

    private static boolean launchIntkeyViaScript(String startupDirectory, String inputFile) {
        String scriptFile = (Platform.isWindows() ? "Intkey.bat" : "Intkey.sh");
        // The DELTA scripts set a System property "basedir" - this is more
        // reliable than the current directory
        // as if the script is on the path the current directory won't be the
        // script directory.
        if (StringUtils.isNotBlank(System.getProperty("basedir"))) {
            String scriptDir = System.getProperty("basedir") + File.separator + "bin" + File.separator + scriptFile;
            if (launchFile(scriptDir, inputFile)) {
                return true;
            }
        }
        String fullpath = String.format("%s%s%s", startupDirectory, File.separator, scriptFile);
        if (!launchFile(fullpath, inputFile)) {
            String path = System.getenv("PATH");
            String[] elements = path.split("\\Q" + File.pathSeparator + "\\E");
            for (String element : elements) {
                fullpath = String.format("%s%s%s", element, File.separator, scriptFile);
                if (launchFile(fullpath, inputFile)) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

    private static boolean launchFile(String path, String args) {
        File f = new File(path);
        if (f.exists()) {
            try {
                Runtime.getRuntime().exec(new String[] { path, args });
                return true;
            } catch (Exception ex) {
            }
        }
        return false;
    }

    private static void launchIntkeyViaClassLoader(String inputFile) throws Exception {
        // Gah.... this is a horrible work around for the fact that
        // the swing application framework relies on a static
        // Application instance so we can't have the Editor and
        // Intkey playing together nicely in the same JVM.
        // It doesn't really work properly anyway, the swing application
        // framework generates exceptions during loading and saving
        // state due to failing instanceof checks.
        String classPath = System.getProperty("java.class.path");
        String[] path = classPath.split(File.pathSeparator);
        List<URL> urls = new ArrayList<URL>();
        for (String pathEntry : path) {
            urls.add(new File(pathEntry).toURI().toURL());
        }
        ClassLoader intkeyLoader = new URLClassLoader(urls.toArray(new URL[0]),
                ClassLoader.getSystemClassLoader().getParent());
        Class<?> intkey = intkeyLoader.loadClass("au.org.ala.delta.intkey.Intkey");
        Method main = intkey.getMethod("main", String[].class);
        main.invoke(null, (Object) new String[] { inputFile });
    }

    /**
     * Parse a string containing a URL or a file path and return a URL object.
     * 
     * @param input
     *            - the URL or file path
     * @return a URL object. If a file path was supplied, this will be a file
     *         protocol URL pointing to the file.
     * @throws IOException
     */
    public static URL parseURLOrFilePath(String input) throws IOException {
        try {
            URL url = new URL(input);
            return url;
        } catch (MalformedURLException ex) {
            // do nothing - assume this is a regular system file path
        }

        File file = new File(input);
        if (!(file.exists() && file.isFile())) {
            throw new IllegalArgumentException("Invalid input or file does not exist");
        }
        return file.toURI().toURL();
    }

    /**
     * Saves the content of the supplied URL to a temporary file. If the url is
     * a file url, the underlying file is simply returned without creating a
     * temporary file.
     * 
     * @param url
     *            the url
     * @param tempFilePrefix
     *            the prefix to use for the temporary file
     * @param timeout
     *            timeout to use when saving the URL's content to the temporary
     *            file
     * @return If the supplied url is a file url, the underlying file is
     *         returned. Otherwise, a temporary file containing the content is
     *         returned
     * @throws IOException
     */
    public static File saveURLToTempFile(URL url, String tempFilePrefix, int timeout) throws IOException {
        // If the URL is a file protocol url, just return the underlying
        // file.
        if (isFileURL(url)) {
            try {
                return new File(url.toURI());
            } catch (URISyntaxException ex) {
                throw new IllegalArgumentException("Invalid URL", ex);
            }
        }

        // Save the file to a temporary file
        File tempFile = File.createTempFile(tempFilePrefix, null);
        FileUtils.copyURLToFile(url, tempFile, timeout, timeout);
        return tempFile;
    }

    /**
     * Returns true if the supplied URL is using one of the formats supported by
     * open-delta - the supported formats are http, ftp and file.
     * 
     * @param url
     *            the url
     * @return true if the url is one of the supported formats
     */
    public static boolean checkURLValidProtocol(URL url) {
        return (url.getProtocol().equalsIgnoreCase("http")
                || (url.getProtocol().equalsIgnoreCase("ftp") || (url.getProtocol().equalsIgnoreCase("file"))));
    }

    /**
     * Returns true if the URL is a file URL
     * 
     * @param url
     *            the file URL
     * @return true if the supplied url is a file url
     */
    public static boolean isFileURL(URL url) {
        return url.getProtocol().equalsIgnoreCase("file");
    }

    /**
     * Use this method when you want to save a file to a directory but do not want to overwrite an existing file with the same name.
     * @param saveDir The directory that you want to save to
     * @param saveFileName The name that you want to use for the file
     * @return If no file with the specified name exists in the directory, a file with the exact name in the specified directory will be returned. Otherwise,
     * a file with the name specified, but with a number in brackets between the base file name and the extension.
     */
    public static File getSaveFileForDirectory(File saveDir, String saveFileName) {
        if (!saveDir.exists() || !saveDir.isDirectory()) {
            throw new IllegalArgumentException("Save directory does not exist or is not a directory");
        }

        // If a file with the exact name specified does not exist in the directory, use a file with the exact name
        File fileWithExactName = new File(saveDir, saveFileName);
        if (!fileWithExactName.exists()) {
            return fileWithExactName;
        }

        // Otherwise, look for existing files with the name specified, but with a number in brackets between the
        // base file name and the extension. Use a file with a number in brackets one higher than the highest number appended to 
        // existing files with the same base file name/extension.

        String fileExtension = FilenameUtils.getExtension(saveFileName);
        String filenameWithoutExtension = FilenameUtils.getBaseName(saveFileName);

        Pattern pattern = Pattern
                .compile(".*" + filenameWithoutExtension + "\\((\\d+)\\)\\." + fileExtension + "$");

        int highestAppendedNumber = 0;

        for (File f : saveDir.listFiles()) {
            if (f.isFile()) {
                Matcher matcher = pattern.matcher(f.getAbsolutePath());
                if (matcher.matches()) {
                    String strAppendedNumber = matcher.group(1);
                    highestAppendedNumber = Integer.parseInt(strAppendedNumber);
                }
            }
        }

        String modifiedSaveFileName = String.format("%s(%s).%s", filenameWithoutExtension,
                highestAppendedNumber + 1, fileExtension);

        return new File(saveDir, modifiedSaveFileName);
    }

}