ca.mcgill.cs.swevo.qualyzer.editors.RTFDocumentProvider2.java Source code

Java tutorial

Introduction

Here is the source code for ca.mcgill.cs.swevo.qualyzer.editors.RTFDocumentProvider2.java

Source

/*******************************************************************************
 * Copyright (c) 2011 McGill University
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Barthelemy Dagenais
 *******************************************************************************/
package ca.mcgill.cs.swevo.qualyzer.editors;

import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.BACKSLASH;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.BOLD_END;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.BOLD_END_TAG;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.BOLD_START;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.BOLD_START_TAG;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.BULLET_POINT;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.ESCAPE_8BIT;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.ESCAPE_CONTROLS;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.FOOTER;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.HEADER;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.IGNORE_GROUPS;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.ITALIC_END;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.ITALIC_END_TAG;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.ITALIC_START;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.ITALIC_START_TAG;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.LEFT_BRACE;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.MINUS;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.NEW_LINE;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.NEW_LINE_CHAR;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.NEW_LINE_TAG;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.NULL_CHAR;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.PLAIN;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.RESET;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.RIGHT_BRACE;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.SPACES;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.SPACE_CHAR;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.TAB;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.TAB_CHAR;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.TAB_TAG;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.UNDERLINE_END;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.UNDERLINE_END_TAG;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.UNDERLINE_START;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.UNDERLINE_START_TAG;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.UNICODE;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.UNICODE_COUNT;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.UNICODE_COUNT_FULL;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.UNICODE_END_TAG;
import static ca.mcgill.cs.swevo.qualyzer.editors.RTFTags.UNICODE_START_TAG;
import static ca.mcgill.cs.swevo.qualyzer.util.ParserUtil.equal;
import static ca.mcgill.cs.swevo.qualyzer.util.ParserUtil.getDefault;
import static ca.mcgill.cs.swevo.qualyzer.util.ParserUtil.in;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;

import org.apache.commons.lang.CharUtils;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.ui.editors.text.FileDocumentProvider;
import org.eclipse.ui.part.FileEditorInput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ca.mcgill.cs.swevo.qualyzer.editors.inputs.RTFEditorInput;
import ca.mcgill.cs.swevo.qualyzer.model.Facade;
import ca.mcgill.cs.swevo.qualyzer.model.Fragment;
import ca.mcgill.cs.swevo.qualyzer.model.IAnnotatedDocument;

/**
 * The DocumentProvider for our editor. If you need to parse a document for some other task (without opening the editor)
 * do the following.
 * 
 * RTFDocumentProvider provider = new RTFDocumentProvider(); RTFEditorInput input = new RTFEditorInput(file, document);
 * IDocument parsedDocument = provider.getCreatedDocument(input);
 * 
 * String paresedText = parsedDocument.getText();
 * 
 */
public class RTFDocumentProvider2 extends FileDocumentProvider {

    private static Logger gLogger = LoggerFactory.getLogger(RTFDocumentProvider2.class);

    private static final String EMPTY = ""; //$NON-NLS-1$

    private static final int HEX_RADIX = 16;

    private static final String SPACE = " "; //$NON-NLS-1$

    private static final String UNICOUNT = "UNICOUNT";

    /**
     * Exists only for use by things that need parsed documents, but that don't want to open the editor.
     * 
     * @param element
     *            The editor input that will be used to create the document.
     * @return The parsed document.
     */
    public IDocument getCreatedDocument(Object element) {
        try {
            return createDocument(element);
        } catch (CoreException e) {
            gLogger.error("DocumentProvider: Failed to create document.", e); //$NON-NLS-1$
        }
        return null;
    }

    /**
     * Given the RTFEditorInput creates the document and then attaches all the fragments to the document.
     */
    @Override
    protected IDocument createDocument(Object element) throws CoreException {
        RTFDocument doc = (RTFDocument) super.createDocument(element);

        IAnnotatedDocument document = ((RTFEditorInput) element).getDocument();

        for (Fragment fragment : document.getFragments().values()) {
            Position position = new Position(fragment.getOffset(), fragment.getLength());
            doc.addAnnotation(position, new FragmentAnnotation(fragment));
        }

        return doc;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.editors.text.StorageDocumentProvider#createEmptyDocument()
     */
    @Override
    protected IDocument createEmptyDocument() {
        return new RTFDocument();
    }

    /**
     * This is the main loop of the RTF parser.
     */
    //CSOFF:
    @Override
    protected void setDocumentContent(IDocument document, InputStream contentStream, String encoding)
            throws CoreException {
        RTFDocument rtfDocument = (RTFDocument) document;
        StringBuilder text = new StringBuilder();
        Map<String, Integer> currentTags = new HashMap<String, Integer>();
        currentTags.put(UNICOUNT, 1);
        Stack<Map<String, Integer>> state = new Stack<Map<String, Integer>>();
        state.push(currentTags);

        try {
            int c = contentStream.read();
            while (c != -1) {
                char ch = (char) c;
                if (ch == BACKSLASH) {
                    ParserPair pair = handleControl(contentStream, safeState(state));
                    handleControlCommand(pair.fString, text, safeState(state), rtfDocument);
                    c = pair.fChar;
                } else if (ch == LEFT_BRACE) {
                    ParserPair pair = handleGroup(contentStream, state, text, rtfDocument);
                    c = pair.fChar;
                } else if (ch == RIGHT_BRACE) {
                    handleEndGroup(contentStream, state, text, rtfDocument);
                    c = contentStream.read();
                } else {
                    if (!in(ch, SPACES)) {
                        text.append(ch);
                    } else if (equal(ch, SPACE_CHAR)) {
                        text.append(ch);
                    }
                    c = contentStream.read();
                }

            }

        } catch (Exception e) {
            gLogger.error("Error while parsing a rtf file.", e); //$NON-NLS-1$
        }

        rtfDocument.set(text.toString());
    }
    //CSON:

    private void handleEndGroup(InputStream contentStream, Stack<Map<String, Integer>> state, StringBuilder text,
            RTFDocument document) {
        Map<String, Integer> oldState = safeState(state, true);
        reset(oldState, text, document);
        Map<String, Integer> currentState = safeState(state);
        resetNew(text, currentState, document);
    }

    private ParserPair handleGroup(InputStream contentStream, Stack<Map<String, Integer>> state, StringBuilder text,
            RTFDocument document) throws IOException {
        int c = contentStream.read();
        char ch = NULL_CHAR;
        ParserPair pair = null;
        String groupName = null;
        while (c != -1) {
            ch = (char) c;
            if (!Character.isWhitespace(ch)) {
                break;
            } else {
                c = contentStream.read();
            }
        }

        if (ch == BACKSLASH) {
            pair = handleControl(contentStream, safeState(state));
            c = pair.fChar;
            groupName = pair.fString;
        } else {
            groupName = " ";
        }

        if (in(groupName, IGNORE_GROUPS)) {
            c = skipGroup(contentStream, c);
        } else {
            Map<String, Integer> oldState = safeState(state);
            reset(oldState, text, document);
            Map<String, Integer> newState = new HashMap<String, Integer>(oldState);
            newState.put(UNICOUNT, oldState.get(UNICOUNT));
            state.push(newState);
            resetNew(text, newState, document);

            handleControlCommand(groupName, text, newState, document);
        }
        return new ParserPair(c, groupName);
    }

    private void resetNew(StringBuilder text, Map<String, Integer> state, RTFDocument document) {
        for (String command : state.keySet()) {
            if (command.equals(UNICOUNT)) {
                continue;
            }
            state.remove(command);
            handleControlCommand(command, text, state, document);
        }

    }

    private void reset(Map<String, Integer> state, StringBuilder text, RTFDocument document) {
        handleControlCommand(PLAIN, text, state, document);
    }

    private int skipGroup(InputStream contentStream, int inputC) throws IOException {
        int c = inputC;
        int count = 1;
        while (c != -1 && count > 0) {
            char ch = (char) c;
            if (ch == LEFT_BRACE) {
                count++;
            } else if (ch == RIGHT_BRACE) {
                count--;
            }
            c = contentStream.read();
        }
        return c;
    }

    //CSOFF:
    private void handleControlCommand(String control, StringBuilder text, Map<String, Integer> state,
            RTFDocument document) {
        if (control.equals(BOLD_START) && !state.containsKey(BOLD_START)) {
            startBold(document, text, state);
        } else if (control.equals(BOLD_END) && state.containsKey(BOLD_START)) {
            endBold(document, text, state);
        } else if (control.equals(ITALIC_START) && !state.containsKey(ITALIC_START)) {
            startItalic(document, text, state);
        } else if (control.equals(ITALIC_END) && state.containsKey(ITALIC_START)) {
            endItalic(document, text, state);
        } else if (control.equals(UNDERLINE_START) && !state.containsKey(UNDERLINE_START)) {
            startUnderline(document, text, state);
        } else if (control.equals(UNDERLINE_END) && state.containsKey(UNDERLINE_START)) {
            endUnderline(document, text, state);
        } else if (control.equals(NEW_LINE)) {
            text.append(NEW_LINE_CHAR);
        } else if (control.equals(TAB)) {
            text.append(TAB_CHAR);
        } else if (in(control, RESET)) {
            handleControlCommand(BOLD_END, text, state, document);
            handleControlCommand(ITALIC_END, text, state, document);
            handleControlCommand(UNDERLINE_END, text, state, document);
        } else if (in(control, ESCAPE_CONTROLS)) {
            text.append(control);
        } else if (control.charAt(0) == ESCAPE_8BIT) {
            text.append(get8bit(control.substring(1)));
        } else if (isUnicodeCount(control)) {
            int number = Integer.parseInt(control.substring(2));
            state.put(UNICOUNT, number);
        } else if (isUnicode(control)) {
            ParserPair unicode = parseUnicode(control.substring(1));
            int unicodeNumber = Integer.parseInt(unicode.fString);
            char unicodeChar = (char) unicodeNumber;
            text.append(unicodeChar);
        }
    }
    //CSON:

    private boolean isUnicodeCount(String control) {
        return control.length() > 2 && control.startsWith(UNICODE_COUNT_FULL)
                && Character.isDigit(control.charAt(2));
    }

    private ParserPair parseUnicode(String unicodeStr) {
        StringBuilder number = new StringBuilder();
        int size = unicodeStr.length();
        for (int i = 0; i < size; i++) {
            char ch = unicodeStr.charAt(i);
            if (Character.isDigit(ch)) {
                number.append(ch);
            } else {
                // For now, we don't care about the replacement...
                break;
            }
        }

        return new ParserPair(-1, number.toString());
    }

    private boolean isUnicode(String control) {
        return control.length() > 1 && control.charAt(0) == UNICODE && Character.isDigit(control.charAt(1));
    }

    private char get8bit(String numberStr) {
        int c = Integer.parseInt(numberStr, HEX_RADIX);
        // Replace weird bullet point by dashes.
        if (c == BULLET_POINT) {
            c = '-';
        }
        return (char) c;
    }

    private ParserPair handleControl(InputStream contentStream, Map<String, Integer> state) throws IOException {
        int c = contentStream.read();
        return handleControl(contentStream, c, EMPTY, state);
    }

    //CSOFF:
    private ParserPair handleControl(InputStream contentStream, int startChar, String startControl,
            Map<String, Integer> state) throws IOException {
        StringBuilder controlWord = new StringBuilder(startControl);
        int c = startChar;
        char ch;

        while (c != -1) {
            ch = (char) c;
            if (ch == UNICODE && isEmpty(controlWord)) {
                // This is potentially an unicode char
                ParserPair pair = getUnicode(contentStream, state);
                c = pair.fChar;
                controlWord = new StringBuilder(pair.fString);
                break;
            } else if (Character.isLetter(ch)) {
                // Start of a control word
                controlWord.append(ch);
            } else if (ch == ESCAPE_8BIT && isEmpty(controlWord)) {
                // This is an escaped 8bit char
                ParserPair pair = get8Bit(contentStream);
                c = pair.fChar;
                controlWord = new StringBuilder(pair.fString);
                break;
            } else if (Character.isDigit(ch)) {
                // Unit of control word
                controlWord.append(ch);
            } else if (ch == MINUS) {
                controlWord.append(ch);
            } else {
                if (isEmpty(controlWord)) {
                    controlWord.append(ch);
                    c = contentStream.read();
                } else if (Character.isWhitespace(ch)) {
                    // This is a delimiter. Skip it
                    c = contentStream.read();
                }
                break;
            }
            c = contentStream.read();
        }

        return new ParserPair(c, controlWord.toString());
    }
    //CSON:

    private ParserPair get8Bit(InputStream contentStream) throws IOException {
        StringBuilder sBuilder = new StringBuilder();
        sBuilder.append(ESCAPE_8BIT);
        sBuilder.append((char) contentStream.read());
        sBuilder.append((char) contentStream.read());
        int c = contentStream.read();
        return new ParserPair(c, sBuilder.toString());
    }

    private ParserPair getUnicode(InputStream contentStream, Map<String, Integer> state) throws IOException {

        StringBuilder control = new StringBuilder();
        int c = contentStream.read();
        if (c != -1) {
            char ch = (char) c;
            if (ch == UNICODE_COUNT) {
                ParserPair number = getNumber(contentStream);
                control.append(UNICODE_COUNT_FULL);
                control.append(number.fString);
                c = number.fChar;
                // This is a control so a space is a delimiter.
                if (Character.isWhitespace((char) c)) {
                    c = contentStream.read();
                }
            } else if (!Character.isDigit(ch)) {
                ParserPair result = handleControl(contentStream, c, String.valueOf(UNICODE), state);
                c = result.fChar;
                control = new StringBuilder(result.fString);
            } else {
                ParserPair number = getNumber(contentStream, Integer.parseInt(String.valueOf(ch)));
                int replacement = number.fChar;
                ParserPair replPair = getUnicodeReplacement(contentStream, replacement, state);
                c = replPair.fChar;

                control.append(UNICODE);
                control.append(number.fString);
                control.append(replPair.fString);
            }
        }

        return new ParserPair(c, control.toString());
    }

    private ParserPair getUnicodeReplacement(InputStream contentStream, int replC, Map<String, Integer> state)
            throws IOException {
        int count = state.get(UNICOUNT);
        int currentCount = 0;
        int c = replC;
        StringBuilder replString = new StringBuilder();
        while (currentCount < count) {
            String replch = String.valueOf((char) c);
            if (equal(BACKSLASH, replch)) {
                // This is a 8 bit
                ParserPair repl8bit = handleControl(contentStream, state);
                replString.append(repl8bit.fString);
                c = repl8bit.fChar;
            } else {
                // This was a 7 bit character
                replString.append(replch);
                c = contentStream.read();
            }
            currentCount++;
        }
        return new ParserPair(c, replString.toString());
    }

    private ParserPair getNumber(InputStream contentStream) throws IOException {
        return getNumber(contentStream, -1);
    }

    private ParserPair getNumber(InputStream contentStream, int initial) throws IOException {
        StringBuilder number = new StringBuilder();
        if (initial > -1) {
            number.append(initial);
        }
        int c = contentStream.read();
        while (c != -1) {
            char ch = (char) c;
            if (Character.isDigit(ch)) {
                number.append(ch);
                c = contentStream.read();
            } else {
                break;
            }
        }
        return new ParserPair(c, number.toString());
    }

    private boolean isEmpty(StringBuilder builder) {
        return builder.toString().isEmpty();
    }

    private Map<String, Integer> safeState(Stack<Map<String, Integer>> state) {
        return safeState(state, false);
    }

    /**
     * Returns the set of tags at the top of the stack. Return an empty set if the stack is empty. This should never
     * occur, but some badly-formatted RTF documents seem to lead to this situation.
     * 
     * @param state
     * @param pop
     * @return
     */
    private Map<String, Integer> safeState(Stack<Map<String, Integer>> state, boolean pop) {
        if (!state.isEmpty()) {
            if (pop) {
                return state.pop();
            } else {
                return state.peek();
            }
        } else {
            gLogger.error("State was empty.");
            return new HashMap<String, Integer>();
        }
    }

    /**
     * @param document
     * @param currentText
     */
    private void startBold(RTFDocument document, StringBuilder currentText, Map<String, Integer> state) {
        if (state.containsKey(BOLD_START)) {
            return;
        }

        int boldPos = currentText.length();
        int italicPos = getDefault(state, ITALIC_START, -1);
        int underlinePos = getDefault(state, UNDERLINE_START, -1);

        if (italicPos != -1 && underlinePos != -1 && italicPos != boldPos) {
            Annotation annotation = new Annotation(RTFConstants.ITALIC_UNDERLINE_TYPE, true, EMPTY);
            Position position = new Position(italicPos, boldPos - italicPos);

            document.addAnnotation(position, annotation);
            italicPos = boldPos;
            underlinePos = boldPos;
        } else if (italicPos != -1 && italicPos != boldPos) {
            Annotation annotation = new Annotation(RTFConstants.ITALIC_TYPE, true, EMPTY);
            Position position = new Position(italicPos, boldPos - italicPos);

            document.addAnnotation(position, annotation);
            italicPos = boldPos;
        } else if (underlinePos != -1 && underlinePos != boldPos) {
            Annotation annotation = new Annotation(RTFConstants.UNDERLINE_TYPE, true, EMPTY);
            Position position = new Position(underlinePos, boldPos - underlinePos);

            document.addAnnotation(position, annotation);
            underlinePos = boldPos;
        }

        // Save state
        state.put(BOLD_START, boldPos);
        if (italicPos != -1) {
            state.put(ITALIC_START, italicPos);
        }
        if (underlinePos != -1) {
            state.put(UNDERLINE_START, underlinePos);
        }
    }

    /**
     * @param document
     * @param currentText
     */
    private void endBold(RTFDocument document, StringBuilder currentText, Map<String, Integer> state) {
        if (!state.containsKey(BOLD_START)) {
            return;
        }

        int boldPos = state.get(BOLD_START);
        int italicPos = getDefault(state, ITALIC_START, -1);
        int underlinePos = getDefault(state, UNDERLINE_START, -1);

        Annotation annotation;
        int curPos = currentText.length();
        Position position = new Position(boldPos, curPos - boldPos);

        if (italicPos != -1 && underlinePos != -1) {
            annotation = new Annotation(RTFConstants.BOLD_ITALIC_UNDERLINE_TYPE, true, EMPTY);
            italicPos = curPos;
            underlinePos = curPos;
            state.put(ITALIC_START, italicPos);
            state.put(UNDERLINE_START, underlinePos);
        } else if (italicPos != -1) {
            annotation = new Annotation(RTFConstants.BOLD_ITALIC_TYPE, true, EMPTY);
            italicPos = curPos;
            state.put(ITALIC_START, italicPos);
        } else if (underlinePos != -1) {
            annotation = new Annotation(RTFConstants.BOLD_UNDERLINE_TYPE, true, EMPTY);
            underlinePos = curPos;
            state.put(UNDERLINE_START, underlinePos);
        } else {
            annotation = new Annotation(RTFConstants.BOLD_TYPE, true, EMPTY);
        }

        if (position.length > 0) {
            document.addAnnotation(position, annotation);
        }

        state.remove(BOLD_START);
    }

    /**
     * @param document
     * @param currentText
     */
    private void endUnderline(RTFDocument document, StringBuilder currentText, Map<String, Integer> state) {
        if (!state.containsKey(UNDERLINE_START)) {
            return;
        }

        int underlinePos = state.get(UNDERLINE_START);
        int italicPos = getDefault(state, ITALIC_START, -1);
        int boldPos = getDefault(state, BOLD_START, -1);

        Annotation annotation;
        int curPos = currentText.length();
        Position position = new Position(underlinePos, curPos - underlinePos);

        if (boldPos != -1 && italicPos != -1) {
            annotation = new Annotation(RTFConstants.BOLD_ITALIC_UNDERLINE_TYPE, true, EMPTY);
            boldPos = curPos;
            italicPos = curPos;
            state.put(BOLD_START, boldPos);
            state.put(ITALIC_START, italicPos);
        } else if (boldPos != -1) {
            annotation = new Annotation(RTFConstants.BOLD_UNDERLINE_TYPE, true, EMPTY);
            boldPos = curPos;
            state.put(BOLD_START, boldPos);
        } else if (italicPos != -1) {
            annotation = new Annotation(RTFConstants.ITALIC_UNDERLINE_TYPE, true, EMPTY);
            italicPos = curPos;
            state.put(ITALIC_START, italicPos);
        } else {
            annotation = new Annotation(RTFConstants.UNDERLINE_TYPE, true, EMPTY);
        }

        if (position.length > 0) {
            document.addAnnotation(position, annotation);
        }

        state.remove(UNDERLINE_START);
    }

    /**
     * @param document
     * @param currentText
     */
    private void startUnderline(RTFDocument document, StringBuilder currentText, Map<String, Integer> state) {
        if (state.containsKey(UNDERLINE_START)) {
            return;
        }

        int underlinePos = currentText.length();
        int italicPos = getDefault(state, ITALIC_START, -1);
        int boldPos = getDefault(state, BOLD_START, -1);

        if (boldPos != -1 && italicPos != -1 && boldPos != underlinePos) {
            Annotation annotation = new Annotation(RTFConstants.BOLD_ITALIC_TYPE, true, EMPTY);
            Position position = new Position(boldPos, underlinePos - boldPos);

            document.addAnnotation(position, annotation);
            boldPos = underlinePos;
            italicPos = underlinePos;
        } else if (boldPos != -1 && boldPos != underlinePos) {
            Annotation annotation = new Annotation(RTFConstants.BOLD_TYPE, true, EMPTY);
            Position position = new Position(boldPos, underlinePos - boldPos);

            document.addAnnotation(position, annotation);
            boldPos = underlinePos;
        } else if (italicPos != -1 && italicPos != underlinePos) {
            Annotation annotation = new Annotation(RTFConstants.ITALIC_TYPE, true, EMPTY);
            Position position = new Position(italicPos, underlinePos - italicPos);

            document.addAnnotation(position, annotation);
            italicPos = underlinePos;
        }

        // Save state
        state.put(UNDERLINE_START, underlinePos);
        if (italicPos != -1) {
            state.put(ITALIC_START, italicPos);
        }
        if (boldPos != -1) {
            state.put(BOLD_START, boldPos);
        }
    }

    /**
     * @param document
     * @param currentText
     */
    private void endItalic(RTFDocument document, StringBuilder currentText, Map<String, Integer> state) {
        if (!state.containsKey(ITALIC_START)) {
            return;
        }

        int italicPos = state.get(ITALIC_START);
        int underlinePos = getDefault(state, UNDERLINE_START, -1);
        int boldPos = getDefault(state, BOLD_START, -1);

        Annotation annotation;
        int curPos = currentText.length();
        Position position = new Position(italicPos, curPos - italicPos);

        if (boldPos != -1 && underlinePos != -1) {
            annotation = new Annotation(RTFConstants.BOLD_ITALIC_UNDERLINE_TYPE, true, EMPTY);
            boldPos = curPos;
            underlinePos = curPos;
            state.put(BOLD_START, boldPos);
            state.put(UNDERLINE_START, underlinePos);
        } else if (boldPos != -1) {
            annotation = new Annotation(RTFConstants.BOLD_ITALIC_TYPE, true, EMPTY);
            boldPos = curPos;
            state.put(BOLD_START, boldPos);
        } else if (underlinePos != -1) {
            annotation = new Annotation(RTFConstants.ITALIC_UNDERLINE_TYPE, true, EMPTY);
            underlinePos = curPos;
            state.put(UNDERLINE_START, underlinePos);
        } else {
            annotation = new Annotation(RTFConstants.ITALIC_TYPE, true, EMPTY);
        }

        if (position.length > 0) {
            document.addAnnotation(position, annotation);
        }

        state.remove(ITALIC_START);
    }

    /**
     * @param document
     * @param currentText
     */
    private void startItalic(RTFDocument document, StringBuilder currentText, Map<String, Integer> state) {
        if (state.containsKey(ITALIC_START)) {
            return;
        }

        int italicPos = currentText.length();
        int underlinePos = getDefault(state, UNDERLINE_START, -1);
        int boldPos = getDefault(state, BOLD_START, -1);

        if (boldPos != -1 && underlinePos != -1 && boldPos != italicPos) {
            Annotation annotation = new Annotation(RTFConstants.BOLD_UNDERLINE_TYPE, true, EMPTY);
            Position position = new Position(boldPos, italicPos - boldPos);

            document.addAnnotation(position, annotation);
            boldPos = italicPos;
            underlinePos = italicPos;
        } else if (boldPos != -1 && boldPos != italicPos) {
            Annotation annotation = new Annotation(RTFConstants.BOLD_TYPE, true, EMPTY);
            Position position = new Position(boldPos, italicPos - boldPos);

            document.addAnnotation(position, annotation);
            boldPos = italicPos;
        } else if (underlinePos != -1 && underlinePos != italicPos) {
            Annotation annotation = new Annotation(RTFConstants.UNDERLINE_TYPE, true, EMPTY);
            Position position = new Position(underlinePos, italicPos - underlinePos);

            document.addAnnotation(position, annotation);
            underlinePos = italicPos;
        }

        // Save state
        state.put(ITALIC_START, italicPos);
        if (underlinePos != -1) {
            state.put(UNDERLINE_START, underlinePos);
        }
        if (boldPos != -1) {
            state.put(BOLD_START, boldPos);
        }
    }

    /**
     * Converts the contents of the document back into rtf so that it can be saved to disk.
     * 
     * @see org.eclipse.ui.editors.text.FileDocumentProvider#doSaveDocument(org.eclipse.core.runtime.IProgressMonitor,
     *      java.lang.Object, org.eclipse.jface.text.IDocument, boolean)
     */
    @SuppressWarnings("unchecked")
    @Override
    protected void doSaveDocument(IProgressMonitor monitor, Object element, IDocument document, boolean overwrite)
            throws CoreException {
        FileEditorInput input = (FileEditorInput) element;
        IAnnotationModel model = getAnnotationModel(element);

        StringBuilder contents = new StringBuilder(document.get());
        StringBuilder toWrite = new StringBuilder(EMPTY);

        toWrite = buildRTFString(contents, model);

        InputStream stream = new ByteArrayInputStream(toWrite.toString().getBytes());
        try {
            input.getFile().setContents(stream, IResource.FORCE, new NullProgressMonitor());
        } catch (CoreException e) {

        }

        // This seems to be necessary for the timestamp markers to persist across saves.
        FileInfo info = (FileInfo) getElementInfo(element);
        if (info != null) {
            RTFAnnotationModel rtfModel = (RTFAnnotationModel) info.fModel;
            rtfModel.updateMarkers(info.fDocument);
        }

        // Updates all of the fragment positions, and removes any annotations that have length 0.
        IAnnotatedDocument rtfDoc = ((RTFEditorInput) element).getDocument();
        Iterator<Annotation> iter = model.getAnnotationIterator();
        while (iter.hasNext()) {
            Annotation annotation = iter.next();
            if (annotation instanceof FragmentAnnotation) {
                updateFragment(model, rtfDoc, annotation);
            } else {
                if (model.getPosition(annotation).length == 0) {
                    model.removeAnnotation(annotation);
                }
            }
        }

        Facade.getInstance().saveDocument(rtfDoc);
    }

    /**
     * Updates the given annotation's fragment to match it's new offset and length. Then updates the map key so that it
     * matches the new offset. If the fragment has a length of 0 it gets removed from the model (and the DB).
     * 
     * @param model
     * @param rtfDoc
     * @param annotation
     */
    private void updateFragment(IAnnotationModel model, IAnnotatedDocument rtfDoc, Annotation annotation) {
        Fragment fragment = ((FragmentAnnotation) annotation).getFragment();
        Position position = model.getPosition(annotation);
        if (position.length == 0) {
            model.removeAnnotation(annotation);
        } else {
            int oldOffset = fragment.getOffset();
            fragment.setOffset(position.offset);
            fragment.setLength(position.length);
            rtfDoc.getFragments().remove(oldOffset);
            rtfDoc.getFragments().put(position.offset, fragment);
        }
    }

    /**
     * Goes through the editor text and all the annotations to build the string that will be written to the disk.
     * Converts any special characters to their RTF tags as well.
     * 
     * @param contents
     * @param model
     * @return
     */
    private StringBuilder buildRTFString(StringBuilder contents, IAnnotationModel model) {
        StringBuilder output = new StringBuilder(HEADER);
        ArrayList<Position> positions = new ArrayList<Position>();
        ArrayList<Annotation> annotations = new ArrayList<Annotation>();
        prepareAnnotationLists(model, positions, annotations);

        Position position = null;
        Annotation annotation = null;

        for (int i = 0; i < contents.length(); i++) {
            if (position == null) {
                for (int j = 0; j < positions.size(); j++) {
                    if (positions.get(j).offset == i) {
                        position = positions.remove(j);
                        annotation = annotations.remove(j);
                        break;
                    } else if (positions.get(j).offset > i) {
                        break;
                    }
                }
                if (position != null) {
                    output.append(getStartTagFromAnnotation(annotation));
                }
            }

            char c = contents.charAt(i);
            output.append(getMiddleChar(c));

            if (position != null && i == position.offset + position.length - 1) {
                output.append(getEndTagFromAnnotation(annotation));

                position = null;
                annotation = null;
            }

            output.append(getEndChar(c));
        }

        return output.append(FOOTER);
    }

    /**
     * Gets the annotations and their positions from the model and sorts them by position. Sets them into the two
     * provided arraylists.
     * 
     * @param model
     * @param positions
     * @param annotations
     */
    @SuppressWarnings("unchecked")
    private void prepareAnnotationLists(IAnnotationModel model, ArrayList<Position> positions,
            ArrayList<Annotation> annotations) {
        Iterator<Annotation> iter = model.getAnnotationIterator();
        while (iter.hasNext()) {
            Annotation annotation = iter.next();
            String type = annotation.getType();
            if (!(annotation instanceof FragmentAnnotation) && !type.equals(RTFConstants.TIMESTAMP_TYPE)) {
                if (positions.isEmpty()) {
                    annotations.add(annotation);
                    positions.add(model.getPosition(annotation));
                } else {
                    Position position = model.getPosition(annotation);
                    int i;
                    for (i = 0; i < positions.size(); i++) {
                        Position curPos = positions.get(i);
                        if (position.offset < curPos.offset) {
                            annotations.add(i, annotation);
                            positions.add(i, position);
                            break;
                        }
                    }

                    if (i >= positions.size()) {
                        annotations.add(annotation);
                        positions.add(position);
                    }
                }

            }
        }
    }

    /**
     * Gets the rtf representations of newline and tab if the current character is one of those.
     * 
     * @param c
     * @return
     */
    private String getEndChar(char c) {
        StringBuilder output = new StringBuilder(EMPTY);
        if (c == '\n') {
            output.append(NEW_LINE_TAG);
        } else if (c == '\t') {
            output.append(TAB_TAG);
        }
        return output.toString();
    }

    /**
     * Stops newlines tabs and EOF from being written, adds an escape to brackets and backslash, converts non-ascii
     * characters to their RTF tag and lets all other characters through.
     * 
     * @param c
     * @return
     */
    private String getMiddleChar(char c) {
        StringBuilder output = new StringBuilder(EMPTY);
        if (c != '\n' && c != '\t' && c != '\0') {
            if (c == '{' || c == '}' || c == '\\') {
                output.append(BACKSLASH);
            }

            if (CharUtils.isAscii(c)) {
                output.append(c);
            } else {
                int unicode = (int) c;
                output = new StringBuilder(UNICODE_START_TAG + unicode + UNICODE_END_TAG);
            }
        }
        return output.toString();
    }

    /**
     * @param annotation
     * @return
     */
    private String getEndTagFromAnnotation(Annotation annotation) {
        String tag = EMPTY;
        String type = annotation.getType();

        if (type.equals(RTFConstants.BOLD_TYPE)) {
            tag = BOLD_END_TAG + SPACE;
        } else if (type.equals(RTFConstants.ITALIC_TYPE)) {
            tag = ITALIC_END_TAG + SPACE;
        } else if (type.equals(RTFConstants.UNDERLINE_TYPE)) {
            tag = UNDERLINE_END_TAG + SPACE;
        } else if (type.equals(RTFConstants.BOLD_ITALIC_TYPE)) {
            tag = BOLD_END_TAG + ITALIC_END_TAG + SPACE;
        } else if (type.equals(RTFConstants.BOLD_UNDERLINE_TYPE)) {
            tag = BOLD_END_TAG + UNDERLINE_END_TAG + SPACE;
        } else if (type.equals(RTFConstants.ITALIC_UNDERLINE_TYPE)) {
            tag = ITALIC_END_TAG + UNDERLINE_END_TAG + SPACE;
        } else if (type.equals(RTFConstants.BOLD_ITALIC_UNDERLINE_TYPE)) {
            tag = BOLD_END_TAG + ITALIC_END_TAG + UNDERLINE_END_TAG + SPACE;
        }

        return tag;
    }

    /**
     * @param annotation
     * @return
     */
    private String getStartTagFromAnnotation(Annotation annotation) {
        String tag = EMPTY;
        String type = annotation.getType();

        if (type.equals(RTFConstants.BOLD_TYPE)) {
            tag = BOLD_START_TAG + SPACE;
        } else if (type.equals(RTFConstants.ITALIC_TYPE)) {
            tag = ITALIC_START_TAG + SPACE;
        } else if (type.equals(RTFConstants.UNDERLINE_TYPE)) {
            tag = UNDERLINE_START_TAG + SPACE;
        } else if (type.equals(RTFConstants.BOLD_ITALIC_TYPE)) {
            tag = BOLD_START_TAG + ITALIC_START_TAG + SPACE;
        } else if (type.equals(RTFConstants.BOLD_UNDERLINE_TYPE)) {
            tag = BOLD_START_TAG + UNDERLINE_START_TAG + SPACE;
        } else if (type.equals(RTFConstants.ITALIC_UNDERLINE_TYPE)) {
            tag = ITALIC_START_TAG + UNDERLINE_START_TAG + SPACE;
        } else if (type.equals(RTFConstants.BOLD_ITALIC_UNDERLINE_TYPE)) {
            tag = BOLD_START_TAG + ITALIC_START_TAG + UNDERLINE_START_TAG + SPACE;
        }

        return tag;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.editors.text.FileDocumentProvider#createAnnotationModel(java.lang.Object)
     */
    @Override
    protected IAnnotationModel createAnnotationModel(Object element) throws CoreException {
        if (element instanceof RTFEditorInput) {
            return new RTFAnnotationModel((RTFEditorInput) element);
        }

        return super.createAnnotationModel(element);
    }
}

/**
 * Used by the parser to return a string and the last read character (similar to peek).
 *
 */
//CSOFF:
class ParserPair {
    public final int fChar;

    public final String fString;

    public ParserPair(int ch, String str) {
        fChar = ch;
        fString = str;
    }
}
//CSON: