blob: 7eac94404a54ede80470b020f9905723e30c11d8 [file] [log] [blame]
/*
* 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.lang.*;
import java.util.*;
import java.io.*;
import java.awt.Font;
import java.awt.Color;
import javax.swing.text.*;
/**
* Takes a sequence of RTF tokens and text and appends the text
* described by the RTF to a <code>StyledDocument</code> (the <em>target</em>).
* The RTF is lexed
* from the character stream by the <code>RTFParser</code> which is this class's
* superclass.
*
* This class is an indirect subclass of OutputStream. It must be closed
* in order to guarantee that all of the text has been sent to
* the text acceptor.
*
* @see RTFParser
* @see java.io.OutputStream
*/
class RTFReader extends RTFParser
{
/** The object to which the parsed text is sent. */
StyledDocument target;
/** Miscellaneous information about the parser's state. This
* dictionary is saved and restored when an RTF group begins
* or ends. */
Dictionary<Object, Object> parserState; /* Current parser state */
/** This is the "dst" item from parserState. rtfDestination
* is the current rtf destination. It is cached in an instance
* variable for speed. */
Destination rtfDestination;
/** This holds the current document attributes. */
MutableAttributeSet documentAttributes;
/** This Dictionary maps Integer font numbers to String font names. */
Dictionary<Integer, String> fontTable;
/** This array maps color indices to Color objects. */
Color[] colorTable;
/** This array maps character style numbers to Style objects. */
Style[] characterStyles;
/** This array maps paragraph style numbers to Style objects. */
Style[] paragraphStyles;
/** This array maps section style numbers to Style objects. */
Style[] sectionStyles;
/** This is the RTF version number, extracted from the \rtf keyword.
* The version information is currently not used. */
int rtfversion;
/** <code>true</code> to indicate that if the next keyword is unknown,
* the containing group should be ignored. */
boolean ignoreGroupIfUnknownKeyword;
/** The parameter of the most recently parsed \\ucN keyword,
* used for skipping alternative representations after a
* Unicode character. */
int skippingCharacters;
static private Dictionary<String, RTFAttribute> straightforwardAttributes;
static {
straightforwardAttributes = RTFAttributes.attributesByKeyword();
}
private MockAttributeSet mockery;
/* this should be final, but there's a bug in javac... */
/** textKeywords maps RTF keywords to single-character strings,
* for those keywords which simply insert some text. */
static Dictionary<String, String> textKeywords = null;
static {
textKeywords = new Hashtable<String, String>();
textKeywords.put("\\", "\\");
textKeywords.put("{", "{");
textKeywords.put("}", "}");
textKeywords.put(" ", "\u00A0"); /* not in the spec... */
textKeywords.put("~", "\u00A0"); /* nonbreaking space */
textKeywords.put("_", "\u2011"); /* nonbreaking hyphen */
textKeywords.put("bullet", "\u2022");
textKeywords.put("emdash", "\u2014");
textKeywords.put("emspace", "\u2003");
textKeywords.put("endash", "\u2013");
textKeywords.put("enspace", "\u2002");
textKeywords.put("ldblquote", "\u201C");
textKeywords.put("lquote", "\u2018");
textKeywords.put("ltrmark", "\u200E");
textKeywords.put("rdblquote", "\u201D");
textKeywords.put("rquote", "\u2019");
textKeywords.put("rtlmark", "\u200F");
textKeywords.put("tab", "\u0009");
textKeywords.put("zwj", "\u200D");
textKeywords.put("zwnj", "\u200C");
/* There is no Unicode equivalent to an optional hyphen, as far as
I can tell. */
textKeywords.put("-", "\u2027"); /* TODO: optional hyphen */
}
/* some entries in parserState */
static final String TabAlignmentKey = "tab_alignment";
static final String TabLeaderKey = "tab_leader";
static Dictionary<String, char[]> characterSets;
static boolean useNeXTForAnsi = false;
static {
characterSets = new Hashtable<String, char[]>();
}
/* TODO: per-font font encodings ( \fcharset control word ) ? */
/**
* Creates a new RTFReader instance. Text will be sent to
* the specified TextAcceptor.
*
* @param destination The TextAcceptor which is to receive the text.
*/
public RTFReader(StyledDocument destination)
{
int i;
target = destination;
parserState = new Hashtable<Object, Object>();
fontTable = new Hashtable<Integer, String>();
rtfversion = -1;
mockery = new MockAttributeSet();
documentAttributes = new SimpleAttributeSet();
}
/** Called when the RTFParser encounters a bin keyword in the
* RTF stream.
*
* @see RTFParser
*/
public void handleBinaryBlob(byte[] data)
{
if (skippingCharacters > 0) {
/* a blob only counts as one character for skipping purposes */
skippingCharacters --;
return;
}
/* someday, someone will want to do something with blobs */
}
/**
* Handles any pure text (containing no control characters) in the input
* stream. Called by the superclass. */
public void handleText(String text)
{
if (skippingCharacters > 0) {
if (skippingCharacters >= text.length()) {
skippingCharacters -= text.length();
return;
} else {
text = text.substring(skippingCharacters);
skippingCharacters = 0;
}
}
if (rtfDestination != null) {
rtfDestination.handleText(text);
return;
}
warning("Text with no destination. oops.");
}
/** The default color for text which has no specified color. */
Color defaultColor()
{
return Color.black;
}
/** Called by the superclass when a new RTF group is begun.
* This implementation saves the current <code>parserState</code>, and gives
* the current destination a chance to save its own state.
* @see RTFParser#begingroup
*/
public void begingroup()
{
if (skippingCharacters > 0) {
/* TODO this indicates an error in the RTF. Log it? */
skippingCharacters = 0;
}
/* we do this little dance to avoid cloning the entire state stack and
immediately throwing it away. */
Object oldSaveState = parserState.get("_savedState");
if (oldSaveState != null)
parserState.remove("_savedState");
Dictionary<String, Object> saveState = (Dictionary<String, Object>)((Hashtable)parserState).clone();
if (oldSaveState != null)
saveState.put("_savedState", oldSaveState);
parserState.put("_savedState", saveState);
if (rtfDestination != null)
rtfDestination.begingroup();
}
/** Called by the superclass when the current RTF group is closed.
* This restores the parserState saved by <code>begingroup()</code>
* as well as invoking the endgroup method of the current
* destination.
* @see RTFParser#endgroup
*/
public void endgroup()
{
if (skippingCharacters > 0) {
/* NB this indicates an error in the RTF. Log it? */
skippingCharacters = 0;
}
Dictionary<Object, Object> restoredState = (Dictionary<Object, Object>)parserState.get("_savedState");
Destination restoredDestination = (Destination)restoredState.get("dst");
if (restoredDestination != rtfDestination) {
rtfDestination.close(); /* allow the destination to clean up */
rtfDestination = restoredDestination;
}
Dictionary oldParserState = parserState;
parserState = restoredState;
if (rtfDestination != null)
rtfDestination.endgroup(oldParserState);
}
protected void setRTFDestination(Destination newDestination)
{
/* Check that setting the destination won't close the
current destination (should never happen) */
Dictionary previousState = (Dictionary)parserState.get("_savedState");
if (previousState != null) {
if (rtfDestination != previousState.get("dst")) {
warning("Warning, RTF destination overridden, invalid RTF.");
rtfDestination.close();
}
}
rtfDestination = newDestination;
parserState.put("dst", rtfDestination);
}
/** Called by the user when there is no more input (<i>i.e.</i>,
* at the end of the RTF file.)
*
* @see OutputStream#close
*/
public void close()
throws IOException
{
Enumeration docProps = documentAttributes.getAttributeNames();
while(docProps.hasMoreElements()) {
Object propName = docProps.nextElement();
target.putProperty(propName,
documentAttributes.getAttribute(propName));
}
/* RTFParser should have ensured that all our groups are closed */
warning("RTF filter done.");
super.close();
}
/**
* Handles a parameterless RTF keyword. This is called by the superclass
* (RTFParser) when a keyword is found in the input stream.
*
* @returns <code>true</code> if the keyword is recognized and handled;
* <code>false</code> otherwise
* @see RTFParser#handleKeyword
*/
public boolean handleKeyword(String keyword)
{
String item;
boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;
if (skippingCharacters > 0) {
skippingCharacters --;
return true;
}
ignoreGroupIfUnknownKeyword = false;
if ((item = textKeywords.get(keyword)) != null) {
handleText(item);
return true;
}
if (keyword.equals("fonttbl")) {
setRTFDestination(new FonttblDestination());
return true;
}
if (keyword.equals("colortbl")) {
setRTFDestination(new ColortblDestination());
return true;
}
if (keyword.equals("stylesheet")) {
setRTFDestination(new StylesheetDestination());
return true;
}
if (keyword.equals("info")) {
setRTFDestination(new InfoDestination());
return false;
}
if (keyword.equals("mac")) {
setCharacterSet("mac");
return true;
}
if (keyword.equals("ansi")) {
if (useNeXTForAnsi)
setCharacterSet("NeXT");
else
setCharacterSet("ansi");
return true;
}
if (keyword.equals("next")) {
setCharacterSet("NeXT");
return true;
}
if (keyword.equals("pc")) {
setCharacterSet("cpg437"); /* IBM Code Page 437 */
return true;
}
if (keyword.equals("pca")) {
setCharacterSet("cpg850"); /* IBM Code Page 850 */
return true;
}
if (keyword.equals("*")) {
ignoreGroupIfUnknownKeyword = true;
return true;
}
if (rtfDestination != null) {
if(rtfDestination.handleKeyword(keyword))
return true;
}
/* this point is reached only if the keyword is unrecognized */
/* other destinations we don't understand and therefore ignore */
if (keyword.equals("aftncn") ||
keyword.equals("aftnsep") ||
keyword.equals("aftnsepc") ||
keyword.equals("annotation") ||
keyword.equals("atnauthor") ||
keyword.equals("atnicn") ||
keyword.equals("atnid") ||
keyword.equals("atnref") ||
keyword.equals("atntime") ||
keyword.equals("atrfend") ||
keyword.equals("atrfstart") ||
keyword.equals("bkmkend") ||
keyword.equals("bkmkstart") ||
keyword.equals("datafield") ||
keyword.equals("do") ||
keyword.equals("dptxbxtext") ||
keyword.equals("falt") ||
keyword.equals("field") ||
keyword.equals("file") ||
keyword.equals("filetbl") ||
keyword.equals("fname") ||
keyword.equals("fontemb") ||
keyword.equals("fontfile") ||
keyword.equals("footer") ||
keyword.equals("footerf") ||
keyword.equals("footerl") ||
keyword.equals("footerr") ||
keyword.equals("footnote") ||
keyword.equals("ftncn") ||
keyword.equals("ftnsep") ||
keyword.equals("ftnsepc") ||
keyword.equals("header") ||
keyword.equals("headerf") ||
keyword.equals("headerl") ||
keyword.equals("headerr") ||
keyword.equals("keycode") ||
keyword.equals("nextfile") ||
keyword.equals("object") ||
keyword.equals("pict") ||
keyword.equals("pn") ||
keyword.equals("pnseclvl") ||
keyword.equals("pntxtb") ||
keyword.equals("pntxta") ||
keyword.equals("revtbl") ||
keyword.equals("rxe") ||
keyword.equals("tc") ||
keyword.equals("template") ||
keyword.equals("txe") ||
keyword.equals("xe")) {
ignoreGroupIfUnknownKeywordSave = true;
}
if (ignoreGroupIfUnknownKeywordSave) {
setRTFDestination(new DiscardingDestination());
}
return false;
}
/**
* Handles an RTF keyword and its integer parameter.
* This is called by the superclass
* (RTFParser) when a keyword is found in the input stream.
*
* @returns <code>true</code> if the keyword is recognized and handled;
* <code>false</code> otherwise
* @see RTFParser#handleKeyword
*/
public boolean handleKeyword(String keyword, int parameter)
{
boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;
if (skippingCharacters > 0) {
skippingCharacters --;
return true;
}
ignoreGroupIfUnknownKeyword = false;
if (keyword.equals("uc")) {
/* count of characters to skip after a unicode character */
parserState.put("UnicodeSkip", Integer.valueOf(parameter));
return true;
}
if (keyword.equals("u")) {
if (parameter < 0)
parameter = parameter + 65536;
handleText((char)parameter);
Number skip = (Number)(parserState.get("UnicodeSkip"));
if (skip != null) {
skippingCharacters = skip.intValue();
} else {
skippingCharacters = 1;
}
return true;
}
if (keyword.equals("rtf")) {
rtfversion = parameter;
setRTFDestination(new DocumentDestination());
return true;
}
if (keyword.startsWith("NeXT") ||
keyword.equals("private"))
ignoreGroupIfUnknownKeywordSave = true;
if (rtfDestination != null) {
if(rtfDestination.handleKeyword(keyword, parameter))
return true;
}
/* this point is reached only if the keyword is unrecognized */
if (ignoreGroupIfUnknownKeywordSave) {
setRTFDestination(new DiscardingDestination());
}
return false;
}
private void setTargetAttribute(String name, Object value)
{
// target.changeAttributes(new LFDictionary(LFArray.arrayWithObject(value), LFArray.arrayWithObject(name)));
}
/**
* setCharacterSet sets the current translation table to correspond with
* the named character set. The character set is loaded if necessary.
*
* @see AbstractFilter
*/
public void setCharacterSet(String name)
{
Object set;
try {
set = getCharacterSet(name);
} catch (Exception e) {
warning("Exception loading RTF character set \"" + name + "\": " + e);
set = null;
}
if (set != null) {
translationTable = (char[])set;
} else {
warning("Unknown RTF character set \"" + name + "\"");
if (!name.equals("ansi")) {
try {
translationTable = (char[])getCharacterSet("ansi");
} catch (IOException e) {
throw new InternalError("RTFReader: Unable to find character set resources (" + e + ")");
}
}
}
setTargetAttribute(Constants.RTFCharacterSet, name);
}
/** Adds a character set to the RTFReader's list
* of known character sets */
public static void
defineCharacterSet(String name, char[] table)
{
if (table.length < 256)
throw new IllegalArgumentException("Translation table must have 256 entries.");
characterSets.put(name, table);
}
/** Looks up a named character set. A character set is a 256-entry
* array of characters, mapping unsigned byte values to their Unicode
* equivalents. The character set is loaded if necessary.
*
* @returns the character set
*/
public static Object
getCharacterSet(final String name)
throws IOException
{
char[] set = characterSets.get(name);
if (set == null) {
InputStream charsetStream;
charsetStream = java.security.AccessController.
doPrivileged(new java.security.PrivilegedAction<InputStream>() {
public InputStream run() {
return RTFReader.class.getResourceAsStream
("charsets/" + name + ".txt");
}
});
set = readCharset(charsetStream);
defineCharacterSet(name, set);
}
return set;
}
/** Parses a character set from an InputStream. The character set
* must contain 256 decimal integers, separated by whitespace, with
* no punctuation. B- and C- style comments are allowed.
*
* @returns the newly read character set
*/
static char[] readCharset(InputStream strm)
throws IOException
{
char[] values = new char[256];
int i;
StreamTokenizer in = new StreamTokenizer(new BufferedReader(
new InputStreamReader(strm, "ISO-8859-1")));
in.eolIsSignificant(false);
in.commentChar('#');
in.slashSlashComments(true);
in.slashStarComments(true);
i = 0;
while (i < 256) {
int ttype;
try {
ttype = in.nextToken();
} catch (Exception e) {
throw new IOException("Unable to read from character set file (" + e + ")");
}
if (ttype != in.TT_NUMBER) {
// System.out.println("Bad token: type=" + ttype + " tok=" + in.sval);
throw new IOException("Unexpected token in character set file");
// continue;
}
values[i] = (char)(in.nval);
i++;
}
return values;
}
static char[] readCharset(java.net.URL href)
throws IOException
{
return readCharset(href.openStream());
}
/** An interface (could be an entirely abstract class) describing
* a destination. The RTF reader always has a current destination
* which is where text is sent.
*
* @see RTFReader
*/
interface Destination {
void handleBinaryBlob(byte[] data);
void handleText(String text);
boolean handleKeyword(String keyword);
boolean handleKeyword(String keyword, int parameter);
void begingroup();
void endgroup(Dictionary oldState);
void close();
}
/** This data-sink class is used to implement ignored destinations
* (e.g. {\*\blegga blah blah blah} )
* It accepts all keywords and text but does nothing with them. */
class DiscardingDestination implements Destination
{
public void handleBinaryBlob(byte[] data)
{
/* Discard binary blobs. */
}
public void handleText(String text)
{
/* Discard text. */
}
public boolean handleKeyword(String text)
{
/* Accept and discard keywords. */
return true;
}
public boolean handleKeyword(String text, int parameter)
{
/* Accept and discard parameterized keywords. */
return true;
}
public void begingroup()
{
/* Ignore groups --- the RTFReader will keep track of the
current group level as necessary */
}
public void endgroup(Dictionary oldState)
{
/* Ignore groups */
}
public void close()
{
/* No end-of-destination cleanup needed */
}
}
/** Reads the fonttbl group, inserting fonts into the RTFReader's
* fontTable dictionary. */
class FonttblDestination implements Destination
{
int nextFontNumber;
Integer fontNumberKey = null;
String nextFontFamily;
public void handleBinaryBlob(byte[] data)
{ /* Discard binary blobs. */ }
public void handleText(String text)
{
int semicolon = text.indexOf(';');
String fontName;
if (semicolon > -1)
fontName = text.substring(0, semicolon);
else
fontName = text;
/* TODO: do something with the font family. */
if (nextFontNumber == -1
&& fontNumberKey != null) {
//font name might be broken across multiple calls
fontName = fontTable.get(fontNumberKey) + fontName;
} else {
fontNumberKey = Integer.valueOf(nextFontNumber);
}
fontTable.put(fontNumberKey, fontName);
nextFontNumber = -1;
nextFontFamily = null;
}
public boolean handleKeyword(String keyword)
{
if (keyword.charAt(0) == 'f') {
nextFontFamily = keyword.substring(1);
return true;
}
return false;
}
public boolean handleKeyword(String keyword, int parameter)
{
if (keyword.equals("f")) {
nextFontNumber = parameter;
return true;
}
return false;
}
/* Groups are irrelevant. */
public void begingroup() {}
public void endgroup(Dictionary oldState) {}
/* currently, the only thing we do when the font table ends is
dump its contents to the debugging log. */
public void close()
{
Enumeration<Integer> nums = fontTable.keys();
warning("Done reading font table.");
while(nums.hasMoreElements()) {
Integer num = nums.nextElement();
warning("Number " + num + ": " + fontTable.get(num));
}
}
}
/** Reads the colortbl group. Upon end-of-group, the RTFReader's
* color table is set to an array containing the read colors. */
class ColortblDestination implements Destination
{
int red, green, blue;
Vector<Color> proTemTable;
public ColortblDestination()
{
red = 0;
green = 0;
blue = 0;
proTemTable = new Vector<Color>();
}
public void handleText(String text)
{
int index;
for (index = 0; index < text.length(); index ++) {
if (text.charAt(index) == ';') {
Color newColor;
newColor = new Color(red, green, blue);
proTemTable.addElement(newColor);
}
}
}
public void close()
{
int count = proTemTable.size();
warning("Done reading color table, " + count + " entries.");
colorTable = new Color[count];
proTemTable.copyInto(colorTable);
}
public boolean handleKeyword(String keyword, int parameter)
{
if (keyword.equals("red"))
red = parameter;
else if (keyword.equals("green"))
green = parameter;
else if (keyword.equals("blue"))
blue = parameter;
else
return false;
return true;
}
/* Colortbls don't understand any parameterless keywords */
public boolean handleKeyword(String keyword) { return false; }
/* Groups are irrelevant. */
public void begingroup() {}
public void endgroup(Dictionary oldState) {}
/* Shouldn't see any binary blobs ... */
public void handleBinaryBlob(byte[] data) {}
}
/** Handles the stylesheet keyword. Styles are read and sorted
* into the three style arrays in the RTFReader. */
class StylesheetDestination
extends DiscardingDestination
implements Destination
{
Dictionary<Integer, StyleDefiningDestination> definedStyles;
public StylesheetDestination()
{
definedStyles = new Hashtable<Integer, StyleDefiningDestination>();
}
public void begingroup()
{
setRTFDestination(new StyleDefiningDestination());
}
public void close()
{
Vector<Style> chrStyles = new Vector<Style>();
Vector<Style> pgfStyles = new Vector<Style>();
Vector<Style> secStyles = new Vector<Style>();
Enumeration<StyleDefiningDestination> styles = definedStyles.elements();
while(styles.hasMoreElements()) {
StyleDefiningDestination style;
Style defined;
style = styles.nextElement();
defined = style.realize();
warning("Style "+style.number+" ("+style.styleName+"): "+defined);
String stype = (String)defined.getAttribute(Constants.StyleType);
Vector<Style> toSet;
if (stype.equals(Constants.STSection)) {
toSet = secStyles;
} else if (stype.equals(Constants.STCharacter)) {
toSet = chrStyles;
} else {
toSet = pgfStyles;
}
if (toSet.size() <= style.number)
toSet.setSize(style.number + 1);
toSet.setElementAt(defined, style.number);
}
if (!(chrStyles.isEmpty())) {
Style[] styleArray = new Style[chrStyles.size()];
chrStyles.copyInto(styleArray);
characterStyles = styleArray;
}
if (!(pgfStyles.isEmpty())) {
Style[] styleArray = new Style[pgfStyles.size()];
pgfStyles.copyInto(styleArray);
paragraphStyles = styleArray;
}
if (!(secStyles.isEmpty())) {
Style[] styleArray = new Style[secStyles.size()];
secStyles.copyInto(styleArray);
sectionStyles = styleArray;
}
/* (old debugging code)
int i, m;
if (characterStyles != null) {
m = characterStyles.length;
for(i=0;i<m;i++)
warnings.println("chrStyle["+i+"]="+characterStyles[i]);
} else warnings.println("No character styles.");
if (paragraphStyles != null) {
m = paragraphStyles.length;
for(i=0;i<m;i++)
warnings.println("pgfStyle["+i+"]="+paragraphStyles[i]);
} else warnings.println("No paragraph styles.");
if (sectionStyles != null) {
m = characterStyles.length;
for(i=0;i<m;i++)
warnings.println("secStyle["+i+"]="+sectionStyles[i]);
} else warnings.println("No section styles.");
*/
}
/** This subclass handles an individual style */
class StyleDefiningDestination
extends AttributeTrackingDestination
implements Destination
{
final int STYLENUMBER_NONE = 222;
boolean additive;
boolean characterStyle;
boolean sectionStyle;
public String styleName;
public int number;
int basedOn;
int nextStyle;
boolean hidden;
Style realizedStyle;
public StyleDefiningDestination()
{
additive = false;
characterStyle = false;
sectionStyle = false;
styleName = null;
number = 0;
basedOn = STYLENUMBER_NONE;
nextStyle = STYLENUMBER_NONE;
hidden = false;
}
public void handleText(String text)
{
if (styleName != null)
styleName = styleName + text;
else
styleName = text;
}
public void close() {
int semicolon = (styleName == null) ? 0 : styleName.indexOf(';');
if (semicolon > 0)
styleName = styleName.substring(0, semicolon);
definedStyles.put(Integer.valueOf(number), this);
super.close();
}
public boolean handleKeyword(String keyword)
{
if (keyword.equals("additive")) {
additive = true;
return true;
}
if (keyword.equals("shidden")) {
hidden = true;
return true;
}
return super.handleKeyword(keyword);
}
public boolean handleKeyword(String keyword, int parameter)
{
if (keyword.equals("s")) {
characterStyle = false;
sectionStyle = false;
number = parameter;
} else if (keyword.equals("cs")) {
characterStyle = true;
sectionStyle = false;
number = parameter;
} else if (keyword.equals("ds")) {
characterStyle = false;
sectionStyle = true;
number = parameter;
} else if (keyword.equals("sbasedon")) {
basedOn = parameter;
} else if (keyword.equals("snext")) {
nextStyle = parameter;
} else {
return super.handleKeyword(keyword, parameter);
}
return true;
}
public Style realize()
{
Style basis = null;
Style next = null;
if (realizedStyle != null)
return realizedStyle;
if (basedOn != STYLENUMBER_NONE) {
StyleDefiningDestination styleDest;
styleDest = definedStyles.get(Integer.valueOf(basedOn));
if (styleDest != null && styleDest != this) {
basis = styleDest.realize();
}
}
/* NB: Swing StyleContext doesn't allow distinct styles with
the same name; RTF apparently does. This may confuse the
user. */
realizedStyle = target.addStyle(styleName, basis);
if (characterStyle) {
realizedStyle.addAttributes(currentTextAttributes());
realizedStyle.addAttribute(Constants.StyleType,
Constants.STCharacter);
} else if (sectionStyle) {
realizedStyle.addAttributes(currentSectionAttributes());
realizedStyle.addAttribute(Constants.StyleType,
Constants.STSection);
} else { /* must be a paragraph style */
realizedStyle.addAttributes(currentParagraphAttributes());
realizedStyle.addAttribute(Constants.StyleType,
Constants.STParagraph);
}
if (nextStyle != STYLENUMBER_NONE) {
StyleDefiningDestination styleDest;
styleDest = definedStyles.get(Integer.valueOf(nextStyle));
if (styleDest != null) {
next = styleDest.realize();
}
}
if (next != null)
realizedStyle.addAttribute(Constants.StyleNext, next);
realizedStyle.addAttribute(Constants.StyleAdditive,
Boolean.valueOf(additive));
realizedStyle.addAttribute(Constants.StyleHidden,
Boolean.valueOf(hidden));
return realizedStyle;
}
}
}
/** Handles the info group. Currently no info keywords are recognized
* so this is a subclass of DiscardingDestination. */
class InfoDestination
extends DiscardingDestination
implements Destination
{
}
/** RTFReader.TextHandlingDestination is an abstract RTF destination
* which simply tracks the attributes specified by the RTF control words
* in internal form and can produce acceptable AttributeSets for the
* current character, paragraph, and section attributes. It is up
* to the subclasses to determine what is done with the actual text. */
abstract class AttributeTrackingDestination implements Destination
{
/** This is the "chr" element of parserState, cached for
* more efficient use */
MutableAttributeSet characterAttributes;
/** This is the "pgf" element of parserState, cached for
* more efficient use */
MutableAttributeSet paragraphAttributes;
/** This is the "sec" element of parserState, cached for
* more efficient use */
MutableAttributeSet sectionAttributes;
public AttributeTrackingDestination()
{
characterAttributes = rootCharacterAttributes();
parserState.put("chr", characterAttributes);
paragraphAttributes = rootParagraphAttributes();
parserState.put("pgf", paragraphAttributes);
sectionAttributes = rootSectionAttributes();
parserState.put("sec", sectionAttributes);
}
abstract public void handleText(String text);
public void handleBinaryBlob(byte[] data)
{
/* This should really be in TextHandlingDestination, but
* since *nobody* does anything with binary blobs, this
* is more convenient. */
warning("Unexpected binary data in RTF file.");
}
public void begingroup()
{
AttributeSet characterParent = currentTextAttributes();
AttributeSet paragraphParent = currentParagraphAttributes();
AttributeSet sectionParent = currentSectionAttributes();
/* It would probably be more efficient to use the
* resolver property of the attributes set for
* implementing rtf groups,
* but that's needed for styles. */
/* update the cached attribute dictionaries */
characterAttributes = new SimpleAttributeSet();
characterAttributes.addAttributes(characterParent);
parserState.put("chr", characterAttributes);
paragraphAttributes = new SimpleAttributeSet();
paragraphAttributes.addAttributes(paragraphParent);
parserState.put("pgf", paragraphAttributes);
sectionAttributes = new SimpleAttributeSet();
sectionAttributes.addAttributes(sectionParent);
parserState.put("sec", sectionAttributes);
}
public void endgroup(Dictionary oldState)
{
characterAttributes = (MutableAttributeSet)parserState.get("chr");
paragraphAttributes = (MutableAttributeSet)parserState.get("pgf");
sectionAttributes = (MutableAttributeSet)parserState.get("sec");
}
public void close()
{
}
public boolean handleKeyword(String keyword)
{
if (keyword.equals("ulnone")) {
return handleKeyword("ul", 0);
}
{
RTFAttribute attr = straightforwardAttributes.get(keyword);
if (attr != null) {
boolean ok;
switch(attr.domain()) {
case RTFAttribute.D_CHARACTER:
ok = attr.set(characterAttributes);
break;
case RTFAttribute.D_PARAGRAPH:
ok = attr.set(paragraphAttributes);
break;
case RTFAttribute.D_SECTION:
ok = attr.set(sectionAttributes);
break;
case RTFAttribute.D_META:
mockery.backing = parserState;
ok = attr.set(mockery);
mockery.backing = null;
break;
case RTFAttribute.D_DOCUMENT:
ok = attr.set(documentAttributes);
break;
default:
/* should never happen */
ok = false;
break;
}
if (ok)
return true;
}
}
if (keyword.equals("plain")) {
resetCharacterAttributes();
return true;
}
if (keyword.equals("pard")) {
resetParagraphAttributes();
return true;
}
if (keyword.equals("sectd")) {
resetSectionAttributes();
return true;
}
return false;
}
public boolean handleKeyword(String keyword, int parameter)
{
boolean booleanParameter = (parameter != 0);
if (keyword.equals("fc"))
keyword = "cf"; /* whatEVER, dude. */
if (keyword.equals("f")) {
parserState.put(keyword, Integer.valueOf(parameter));
return true;
}
if (keyword.equals("cf")) {
parserState.put(keyword, Integer.valueOf(parameter));
return true;
}
{
RTFAttribute attr = straightforwardAttributes.get(keyword);
if (attr != null) {
boolean ok;
switch(attr.domain()) {
case RTFAttribute.D_CHARACTER:
ok = attr.set(characterAttributes, parameter);
break;
case RTFAttribute.D_PARAGRAPH:
ok = attr.set(paragraphAttributes, parameter);
break;
case RTFAttribute.D_SECTION:
ok = attr.set(sectionAttributes, parameter);
break;
case RTFAttribute.D_META:
mockery.backing = parserState;
ok = attr.set(mockery, parameter);
mockery.backing = null;
break;
case RTFAttribute.D_DOCUMENT:
ok = attr.set(documentAttributes, parameter);
break;
default:
/* should never happen */
ok = false;
break;
}
if (ok)
return true;
}
}
if (keyword.equals("fs")) {
StyleConstants.setFontSize(characterAttributes, (parameter / 2));
return true;
}
/* TODO: superscript/subscript */
if (keyword.equals("sl")) {
if (parameter == 1000) { /* magic value! */
characterAttributes.removeAttribute(StyleConstants.LineSpacing);
} else {
/* TODO: The RTF sl attribute has special meaning if it's
negative. Make sure that SwingText has the same special
meaning, or find a way to imitate that. When SwingText
handles this, also recognize the slmult keyword. */
StyleConstants.setLineSpacing(characterAttributes,
parameter / 20f);
}
return true;
}
/* TODO: Other kinds of underlining */
if (keyword.equals("tx") || keyword.equals("tb")) {
float tabPosition = parameter / 20f;
int tabAlignment, tabLeader;
Number item;
tabAlignment = TabStop.ALIGN_LEFT;
item = (Number)(parserState.get("tab_alignment"));
if (item != null)
tabAlignment = item.intValue();
tabLeader = TabStop.LEAD_NONE;
item = (Number)(parserState.get("tab_leader"));
if (item != null)
tabLeader = item.intValue();
if (keyword.equals("tb"))
tabAlignment = TabStop.ALIGN_BAR;
parserState.remove("tab_alignment");
parserState.remove("tab_leader");
TabStop newStop = new TabStop(tabPosition, tabAlignment, tabLeader);
Dictionary<Object, Object> tabs;
Integer stopCount;
tabs = (Dictionary<Object, Object>)parserState.get("_tabs");
if (tabs == null) {
tabs = new Hashtable<Object, Object>();
parserState.put("_tabs", tabs);
stopCount = Integer.valueOf(1);
} else {
stopCount = (Integer)tabs.get("stop count");
stopCount = Integer.valueOf(1 + stopCount.intValue());
}
tabs.put(stopCount, newStop);
tabs.put("stop count", stopCount);
parserState.remove("_tabs_immutable");
return true;
}
if (keyword.equals("s") &&
paragraphStyles != null) {
parserState.put("paragraphStyle", paragraphStyles[parameter]);
return true;
}
if (keyword.equals("cs") &&
characterStyles != null) {
parserState.put("characterStyle", characterStyles[parameter]);
return true;
}
if (keyword.equals("ds") &&
sectionStyles != null) {
parserState.put("sectionStyle", sectionStyles[parameter]);
return true;
}
return false;
}
/** Returns a new MutableAttributeSet containing the
* default character attributes */
protected MutableAttributeSet rootCharacterAttributes()
{
MutableAttributeSet set = new SimpleAttributeSet();
/* TODO: default font */
StyleConstants.setItalic(set, false);
StyleConstants.setBold(set, false);
StyleConstants.setUnderline(set, false);
StyleConstants.setForeground(set, defaultColor());
return set;
}
/** Returns a new MutableAttributeSet containing the
* default paragraph attributes */
protected MutableAttributeSet rootParagraphAttributes()
{
MutableAttributeSet set = new SimpleAttributeSet();
StyleConstants.setLeftIndent(set, 0f);
StyleConstants.setRightIndent(set, 0f);
StyleConstants.setFirstLineIndent(set, 0f);
/* TODO: what should this be, really? */
set.setResolveParent(target.getStyle(StyleContext.DEFAULT_STYLE));
return set;
}
/** Returns a new MutableAttributeSet containing the
* default section attributes */
protected MutableAttributeSet rootSectionAttributes()
{
MutableAttributeSet set = new SimpleAttributeSet();
return set;
}
/**
* Calculates the current text (character) attributes in a form suitable
* for SwingText from the current parser state.
*
* @returns a new MutableAttributeSet containing the text attributes.
*/
MutableAttributeSet currentTextAttributes()
{
MutableAttributeSet attributes =
new SimpleAttributeSet(characterAttributes);
Integer fontnum;
Integer stateItem;
/* figure out the font name */
/* TODO: catch exceptions for undefined attributes,
bad font indices, etc.? (as it stands, it is the caller's
job to clean up after corrupt RTF) */
fontnum = (Integer)parserState.get("f");
/* note setFontFamily() can not handle a null font */
String fontFamily;
if (fontnum != null)
fontFamily = fontTable.get(fontnum);
else
fontFamily = null;
if (fontFamily != null)
StyleConstants.setFontFamily(attributes, fontFamily);
else
attributes.removeAttribute(StyleConstants.FontFamily);
if (colorTable != null) {
stateItem = (Integer)parserState.get("cf");
if (stateItem != null) {
Color fg = colorTable[stateItem.intValue()];
StyleConstants.setForeground(attributes, fg);
} else {
/* AttributeSet dies if you set a value to null */
attributes.removeAttribute(StyleConstants.Foreground);
}
}
if (colorTable != null) {
stateItem = (Integer)parserState.get("cb");
if (stateItem != null) {
Color bg = colorTable[stateItem.intValue()];
attributes.addAttribute(StyleConstants.Background,
bg);
} else {
/* AttributeSet dies if you set a value to null */
attributes.removeAttribute(StyleConstants.Background);
}
}
Style characterStyle = (Style)parserState.get("characterStyle");
if (characterStyle != null)
attributes.setResolveParent(characterStyle);
/* Other attributes are maintained directly in "attributes" */
return attributes;
}
/**
* Calculates the current paragraph attributes (with keys
* as given in StyleConstants) from the current parser state.
*
* @returns a newly created MutableAttributeSet.
* @see StyleConstants
*/
MutableAttributeSet currentParagraphAttributes()
{
/* NB if there were a mutableCopy() method we should use it */
MutableAttributeSet bld = new SimpleAttributeSet(paragraphAttributes);
Integer stateItem;
/*** Tab stops ***/
TabStop tabs[];
tabs = (TabStop[])parserState.get("_tabs_immutable");
if (tabs == null) {
Dictionary workingTabs = (Dictionary)parserState.get("_tabs");
if (workingTabs != null) {
int count = ((Integer)workingTabs.get("stop count")).intValue();
tabs = new TabStop[count];
for (int ix = 1; ix <= count; ix ++)
tabs[ix-1] = (TabStop)workingTabs.get(Integer.valueOf(ix));
parserState.put("_tabs_immutable", tabs);
}
}
if (tabs != null)
bld.addAttribute(Constants.Tabs, tabs);
Style paragraphStyle = (Style)parserState.get("paragraphStyle");
if (paragraphStyle != null)
bld.setResolveParent(paragraphStyle);
return bld;
}
/**
* Calculates the current section attributes
* from the current parser state.
*
* @returns a newly created MutableAttributeSet.
*/
public AttributeSet currentSectionAttributes()
{
MutableAttributeSet attributes = new SimpleAttributeSet(sectionAttributes);
Style sectionStyle = (Style)parserState.get("sectionStyle");
if (sectionStyle != null)
attributes.setResolveParent(sectionStyle);
return attributes;
}
/** Resets the filter's internal notion of the current character
* attributes to their default values. Invoked to handle the
* \plain keyword. */
protected void resetCharacterAttributes()
{
handleKeyword("f", 0);
handleKeyword("cf", 0);
handleKeyword("fs", 24); /* 12 pt. */
Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
while(attributes.hasMoreElements()) {
RTFAttribute attr = attributes.nextElement();
if (attr.domain() == RTFAttribute.D_CHARACTER)
attr.setDefault(characterAttributes);
}
handleKeyword("sl", 1000);
parserState.remove("characterStyle");
}
/** Resets the filter's internal notion of the current paragraph's
* attributes to their default values. Invoked to handle the
* \pard keyword. */
protected void resetParagraphAttributes()
{
parserState.remove("_tabs");
parserState.remove("_tabs_immutable");
parserState.remove("paragraphStyle");
StyleConstants.setAlignment(paragraphAttributes,
StyleConstants.ALIGN_LEFT);
Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
while(attributes.hasMoreElements()) {
RTFAttribute attr = attributes.nextElement();
if (attr.domain() == RTFAttribute.D_PARAGRAPH)
attr.setDefault(characterAttributes);
}
}
/** Resets the filter's internal notion of the current section's
* attributes to their default values. Invoked to handle the
* \sectd keyword. */
protected void resetSectionAttributes()
{
Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
while(attributes.hasMoreElements()) {
RTFAttribute attr = attributes.nextElement();
if (attr.domain() == RTFAttribute.D_SECTION)
attr.setDefault(characterAttributes);
}
parserState.remove("sectionStyle");
}
}
/** RTFReader.TextHandlingDestination provides basic text handling
* functionality. Subclasses must implement: <dl>
* <dt>deliverText()<dd>to handle a run of text with the same
* attributes
* <dt>finishParagraph()<dd>to end the current paragraph and
* set the paragraph's attributes
* <dt>endSection()<dd>to end the current section
* </dl>
*/
abstract class TextHandlingDestination
extends AttributeTrackingDestination
implements Destination
{
/** <code>true</code> if the reader has not just finished
* a paragraph; false upon startup */
boolean inParagraph;
public TextHandlingDestination()
{
super();
inParagraph = false;
}
public void handleText(String text)
{
if (! inParagraph)
beginParagraph();
deliverText(text, currentTextAttributes());
}
abstract void deliverText(String text, AttributeSet characterAttributes);
public void close()
{
if (inParagraph)
endParagraph();
super.close();
}
public boolean handleKeyword(String keyword)
{
if (keyword.equals("\r") || keyword.equals("\n")) {
keyword = "par";
}
if (keyword.equals("par")) {
// warnings.println("Ending paragraph.");
endParagraph();
return true;
}
if (keyword.equals("sect")) {
// warnings.println("Ending section.");
endSection();
return true;
}
return super.handleKeyword(keyword);
}
protected void beginParagraph()
{
inParagraph = true;
}
protected void endParagraph()
{
AttributeSet pgfAttributes = currentParagraphAttributes();
AttributeSet chrAttributes = currentTextAttributes();
finishParagraph(pgfAttributes, chrAttributes);
inParagraph = false;
}
abstract void finishParagraph(AttributeSet pgfA, AttributeSet chrA);
abstract void endSection();
}
/** RTFReader.DocumentDestination is a concrete subclass of
* TextHandlingDestination which appends the text to the
* StyledDocument given by the <code>target</code> ivar of the
* containing RTFReader.
*/
class DocumentDestination
extends TextHandlingDestination
implements Destination
{
public void deliverText(String text, AttributeSet characterAttributes)
{
try {
target.insertString(target.getLength(),
text,
currentTextAttributes());
} catch (BadLocationException ble) {
/* This shouldn't be able to happen, of course */
/* TODO is InternalError the correct error to throw? */
throw new InternalError(ble.getMessage());
}
}
public void finishParagraph(AttributeSet pgfAttributes,
AttributeSet chrAttributes)
{
int pgfEndPosition = target.getLength();
try {
target.insertString(pgfEndPosition, "\n", chrAttributes);
target.setParagraphAttributes(pgfEndPosition, 1, pgfAttributes, true);
} catch (BadLocationException ble) {
/* This shouldn't be able to happen, of course */
/* TODO is InternalError the correct error to throw? */
throw new InternalError(ble.getMessage());
}
}
public void endSection()
{
/* If we implemented sections, we'd end 'em here */
}
}
}