| /* |
| * Copyright (c) 1997, 2008, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| package javax.swing.text.rtf; |
| |
| import java.io.*; |
| import java.lang.*; |
| |
| /** |
| * <b>RTFParser</b> is a subclass of <b>AbstractFilter</b> which understands basic RTF syntax |
| * and passes a stream of control words, text, and begin/end group |
| * indications to its subclass. |
| * |
| * Normally programmers will only use <b>RTFFilter</b>, a subclass of this class that knows what to |
| * do with the tokens this class parses. |
| * |
| * @see AbstractFilter |
| * @see RTFFilter |
| */ |
| abstract class RTFParser extends AbstractFilter |
| { |
| /** The current RTF group nesting level. */ |
| public int level; |
| |
| private int state; |
| private StringBuffer currentCharacters; |
| private String pendingKeyword; // where keywords go while we |
| // read their parameters |
| private int pendingCharacter; // for the \'xx construct |
| |
| private long binaryBytesLeft; // in a \bin blob? |
| ByteArrayOutputStream binaryBuf; |
| private boolean[] savedSpecials; |
| |
| /** A stream to which to write warnings and debugging information |
| * while parsing. This is set to <code>System.out</code> to log |
| * any anomalous information to stdout. */ |
| protected PrintStream warnings; |
| |
| // value for the 'state' variable |
| private final int S_text = 0; // reading random text |
| private final int S_backslashed = 1; // read a backslash, waiting for next |
| private final int S_token = 2; // reading a multicharacter token |
| private final int S_parameter = 3; // reading a token's parameter |
| |
| private final int S_aftertick = 4; // after reading \' |
| private final int S_aftertickc = 5; // after reading \'x |
| |
| private final int S_inblob = 6; // in a \bin blob |
| |
| /** Implemented by subclasses to interpret a parameter-less RTF keyword. |
| * The keyword is passed without the leading '/' or any delimiting |
| * whitespace. */ |
| public abstract boolean handleKeyword(String keyword); |
| /** Implemented by subclasses to interpret a keyword with a parameter. |
| * @param keyword The keyword, as with <code>handleKeyword(String)</code>. |
| * @param parameter The parameter following the keyword. */ |
| public abstract boolean handleKeyword(String keyword, int parameter); |
| /** Implemented by subclasses to interpret text from the RTF stream. */ |
| public abstract void handleText(String text); |
| public void handleText(char ch) |
| { handleText(String.valueOf(ch)); } |
| /** Implemented by subclasses to handle the contents of the \bin keyword. */ |
| public abstract void handleBinaryBlob(byte[] data); |
| /** Implemented by subclasses to react to an increase |
| * in the nesting level. */ |
| public abstract void begingroup(); |
| /** Implemented by subclasses to react to the end of a group. */ |
| public abstract void endgroup(); |
| |
| // table of non-text characters in rtf |
| static final boolean rtfSpecialsTable[]; |
| static { |
| rtfSpecialsTable = noSpecialsTable.clone(); |
| rtfSpecialsTable['\n'] = true; |
| rtfSpecialsTable['\r'] = true; |
| rtfSpecialsTable['{'] = true; |
| rtfSpecialsTable['}'] = true; |
| rtfSpecialsTable['\\'] = true; |
| } |
| |
| public RTFParser() |
| { |
| currentCharacters = new StringBuffer(); |
| state = S_text; |
| pendingKeyword = null; |
| level = 0; |
| //warnings = System.out; |
| |
| specialsTable = rtfSpecialsTable; |
| } |
| |
| // TODO: Handle wrapup at end of file correctly. |
| |
| public void writeSpecial(int b) |
| throws IOException |
| { |
| write((char)b); |
| } |
| |
| protected void warning(String s) { |
| if (warnings != null) { |
| warnings.println(s); |
| } |
| } |
| |
| public void write(String s) |
| throws IOException |
| { |
| if (state != S_text) { |
| int index = 0; |
| int length = s.length(); |
| while(index < length && state != S_text) { |
| write(s.charAt(index)); |
| index ++; |
| } |
| |
| if(index >= length) |
| return; |
| |
| s = s.substring(index); |
| } |
| |
| if (currentCharacters.length() > 0) |
| currentCharacters.append(s); |
| else |
| handleText(s); |
| } |
| |
| public void write(char ch) |
| throws IOException |
| { |
| boolean ok; |
| |
| switch (state) |
| { |
| case S_text: |
| if (ch == '\n' || ch == '\r') { |
| break; // unadorned newlines are ignored |
| } else if (ch == '{') { |
| if (currentCharacters.length() > 0) { |
| handleText(currentCharacters.toString()); |
| currentCharacters = new StringBuffer(); |
| } |
| level ++; |
| begingroup(); |
| } else if(ch == '}') { |
| if (currentCharacters.length() > 0) { |
| handleText(currentCharacters.toString()); |
| currentCharacters = new StringBuffer(); |
| } |
| if (level == 0) |
| throw new IOException("Too many close-groups in RTF text"); |
| endgroup(); |
| level --; |
| } else if(ch == '\\') { |
| if (currentCharacters.length() > 0) { |
| handleText(currentCharacters.toString()); |
| currentCharacters = new StringBuffer(); |
| } |
| state = S_backslashed; |
| } else { |
| currentCharacters.append(ch); |
| } |
| break; |
| case S_backslashed: |
| if (ch == '\'') { |
| state = S_aftertick; |
| break; |
| } |
| if (!Character.isLetter(ch)) { |
| char newstring[] = new char[1]; |
| newstring[0] = ch; |
| if (!handleKeyword(new String(newstring))) { |
| warning("Unknown keyword: " + newstring + " (" + (byte)ch + ")"); |
| } |
| state = S_text; |
| pendingKeyword = null; |
| /* currentCharacters is already an empty stringBuffer */ |
| break; |
| } |
| |
| state = S_token; |
| /* FALL THROUGH */ |
| case S_token: |
| if (Character.isLetter(ch)) { |
| currentCharacters.append(ch); |
| } else { |
| pendingKeyword = currentCharacters.toString(); |
| currentCharacters = new StringBuffer(); |
| |
| // Parameter following? |
| if (Character.isDigit(ch) || (ch == '-')) { |
| state = S_parameter; |
| currentCharacters.append(ch); |
| } else { |
| ok = handleKeyword(pendingKeyword); |
| if (!ok) |
| warning("Unknown keyword: " + pendingKeyword); |
| pendingKeyword = null; |
| state = S_text; |
| |
| // Non-space delimiters get included in the text |
| if (!Character.isWhitespace(ch)) |
| write(ch); |
| } |
| } |
| break; |
| case S_parameter: |
| if (Character.isDigit(ch)) { |
| currentCharacters.append(ch); |
| } else { |
| /* TODO: Test correct behavior of \bin keyword */ |
| if (pendingKeyword.equals("bin")) { /* magic layer-breaking kwd */ |
| long parameter = Long.parseLong(currentCharacters.toString()); |
| pendingKeyword = null; |
| state = S_inblob; |
| binaryBytesLeft = parameter; |
| if (binaryBytesLeft > Integer.MAX_VALUE) |
| binaryBuf = new ByteArrayOutputStream(Integer.MAX_VALUE); |
| else |
| binaryBuf = new ByteArrayOutputStream((int)binaryBytesLeft); |
| savedSpecials = specialsTable; |
| specialsTable = allSpecialsTable; |
| break; |
| } |
| |
| int parameter = Integer.parseInt(currentCharacters.toString()); |
| ok = handleKeyword(pendingKeyword, parameter); |
| if (!ok) |
| warning("Unknown keyword: " + pendingKeyword + |
| " (param " + currentCharacters + ")"); |
| pendingKeyword = null; |
| currentCharacters = new StringBuffer(); |
| state = S_text; |
| |
| // Delimiters here are interpreted as text too |
| if (!Character.isWhitespace(ch)) |
| write(ch); |
| } |
| break; |
| case S_aftertick: |
| if (Character.digit(ch, 16) == -1) |
| state = S_text; |
| else { |
| pendingCharacter = Character.digit(ch, 16); |
| state = S_aftertickc; |
| } |
| break; |
| case S_aftertickc: |
| state = S_text; |
| if (Character.digit(ch, 16) != -1) |
| { |
| pendingCharacter = pendingCharacter * 16 + Character.digit(ch, 16); |
| ch = translationTable[pendingCharacter]; |
| if (ch != 0) |
| handleText(ch); |
| } |
| break; |
| case S_inblob: |
| binaryBuf.write(ch); |
| binaryBytesLeft --; |
| if (binaryBytesLeft == 0) { |
| state = S_text; |
| specialsTable = savedSpecials; |
| savedSpecials = null; |
| handleBinaryBlob(binaryBuf.toByteArray()); |
| binaryBuf = null; |
| } |
| } |
| } |
| |
| /** Flushes any buffered but not yet written characters. |
| * Subclasses which override this method should call this |
| * method <em>before</em> flushing |
| * any of their own buffers. */ |
| public void flush() |
| throws IOException |
| { |
| super.flush(); |
| |
| if (state == S_text && currentCharacters.length() > 0) { |
| handleText(currentCharacters.toString()); |
| currentCharacters = new StringBuffer(); |
| } |
| } |
| |
| /** Closes the parser. Currently, this simply does a <code>flush()</code>, |
| * followed by some minimal consistency checks. */ |
| public void close() |
| throws IOException |
| { |
| flush(); |
| |
| if (state != S_text || level > 0) { |
| warning("Truncated RTF file."); |
| |
| /* TODO: any sane way to handle termination in a non-S_text state? */ |
| /* probably not */ |
| |
| /* this will cause subclasses to behave more reasonably |
| some of the time */ |
| while (level > 0) { |
| endgroup(); |
| level --; |
| } |
| } |
| |
| super.close(); |
| } |
| |
| } |