1 /*
   2  * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.text.rtf;
  26 
  27 import java.lang.*;
  28 import java.util.*;
  29 import java.io.*;
  30 import java.awt.Color;
  31 import java.security.AccessController;
  32 import java.security.PrivilegedAction;
  33 import javax.swing.text.*;
  34 
  35 /**
  36  * Takes a sequence of RTF tokens and text and appends the text
  37  * described by the RTF to a <code>StyledDocument</code> (the <em>target</em>).
  38  * The RTF is lexed
  39  * from the character stream by the <code>RTFParser</code> which is this class's
  40  * superclass.
  41  *
  42  * This class is an indirect subclass of OutputStream. It must be closed
  43  * in order to guarantee that all of the text has been sent to
  44  * the text acceptor.
  45  *
  46  *   @see RTFParser
  47  *   @see java.io.OutputStream
  48  */
  49 class RTFReader extends RTFParser
  50 {
  51   /** The object to which the parsed text is sent. */
  52   StyledDocument target;
  53 
  54   /** Miscellaneous information about the parser's state. This
  55    *  dictionary is saved and restored when an RTF group begins
  56    *  or ends. */
  57   Dictionary<Object, Object> parserState;   /* Current parser state */
  58   /** This is the "dst" item from parserState. rtfDestination
  59    *  is the current rtf destination. It is cached in an instance
  60    *  variable for speed. */
  61   Destination rtfDestination;
  62   /** This holds the current document attributes. */
  63   MutableAttributeSet documentAttributes;
  64 
  65   /** This Dictionary maps Integer font numbers to String font names. */
  66   Dictionary<Integer, String> fontTable;
  67   /** This array maps color indices to Color objects. */
  68   Color[] colorTable;
  69   /** This array maps character style numbers to Style objects. */
  70   Style[] characterStyles;
  71   /** This array maps paragraph style numbers to Style objects. */
  72   Style[] paragraphStyles;
  73   /** This array maps section style numbers to Style objects. */
  74   Style[] sectionStyles;
  75 
  76   /** This is the RTF version number, extracted from the \rtf keyword.
  77    *  The version information is currently not used. */
  78   int rtfversion;
  79 
  80   /** <code>true</code> to indicate that if the next keyword is unknown,
  81    *  the containing group should be ignored. */
  82   boolean ignoreGroupIfUnknownKeyword;
  83 
  84   /** The parameter of the most recently parsed \\ucN keyword,
  85    *  used for skipping alternative representations after a
  86    *  Unicode character. */
  87   int skippingCharacters;
  88 
  89   private static Dictionary<String, RTFAttribute> straightforwardAttributes;
  90   static {
  91       straightforwardAttributes = RTFAttributes.attributesByKeyword();
  92   }
  93 
  94   private MockAttributeSet mockery;
  95 
  96   /* this should be final, but there's a bug in javac... */
  97   /** textKeywords maps RTF keywords to single-character strings,
  98    *  for those keywords which simply insert some text. */
  99   static Dictionary<String, String> textKeywords = null;
 100   static {
 101       textKeywords = new Hashtable<String, String>();
 102       textKeywords.put("\\",         "\\");
 103       textKeywords.put("{",          "{");
 104       textKeywords.put("}",          "}");
 105       textKeywords.put(" ",          "\u00A0");  /* not in the spec... */
 106       textKeywords.put("~",          "\u00A0");  /* nonbreaking space */
 107       textKeywords.put("_",          "\u2011");  /* nonbreaking hyphen */
 108       textKeywords.put("bullet",     "\u2022");
 109       textKeywords.put("emdash",     "\u2014");
 110       textKeywords.put("emspace",    "\u2003");
 111       textKeywords.put("endash",     "\u2013");
 112       textKeywords.put("enspace",    "\u2002");
 113       textKeywords.put("ldblquote",  "\u201C");
 114       textKeywords.put("lquote",     "\u2018");
 115       textKeywords.put("ltrmark",    "\u200E");
 116       textKeywords.put("rdblquote",  "\u201D");
 117       textKeywords.put("rquote",     "\u2019");
 118       textKeywords.put("rtlmark",    "\u200F");
 119       textKeywords.put("tab",        "\u0009");
 120       textKeywords.put("zwj",        "\u200D");
 121       textKeywords.put("zwnj",       "\u200C");
 122 
 123       /* There is no Unicode equivalent to an optional hyphen, as far as
 124          I can tell. */
 125       textKeywords.put("-",          "\u2027");  /* TODO: optional hyphen */
 126   }
 127 
 128   /* some entries in parserState */
 129   static final String TabAlignmentKey = "tab_alignment";
 130   static final String TabLeaderKey = "tab_leader";
 131 
 132   static Dictionary<String, char[]> characterSets;
 133   static boolean useNeXTForAnsi = false;
 134   static {
 135       characterSets = new Hashtable<String, char[]>();
 136   }
 137 
 138 /* TODO: per-font font encodings ( \fcharset control word ) ? */
 139 
 140 /**
 141  * Creates a new RTFReader instance. Text will be sent to
 142  * the specified TextAcceptor.
 143  *
 144  * @param destination The TextAcceptor which is to receive the text.
 145  */
 146 public RTFReader(StyledDocument destination)
 147 {
 148     int i;
 149 
 150     target = destination;
 151     parserState = new Hashtable<Object, Object>();
 152     fontTable = new Hashtable<Integer, String>();
 153 
 154     rtfversion = -1;
 155 
 156     mockery = new MockAttributeSet();
 157     documentAttributes = new SimpleAttributeSet();
 158 }
 159 
 160 /** Called when the RTFParser encounters a bin keyword in the
 161  *  RTF stream.
 162  *
 163  *  @see RTFParser
 164  */
 165 public void handleBinaryBlob(byte[] data)
 166 {
 167     if (skippingCharacters > 0) {
 168         /* a blob only counts as one character for skipping purposes */
 169         skippingCharacters --;
 170         return;
 171     }
 172 
 173     /* someday, someone will want to do something with blobs */
 174 }
 175 
 176 
 177 /**
 178  * Handles any pure text (containing no control characters) in the input
 179  * stream. Called by the superclass. */
 180 public void handleText(String text)
 181 {
 182     if (skippingCharacters > 0) {
 183         if (skippingCharacters >= text.length()) {
 184             skippingCharacters -= text.length();
 185             return;
 186         } else {
 187             text = text.substring(skippingCharacters);
 188             skippingCharacters = 0;
 189         }
 190     }
 191 
 192     if (rtfDestination != null) {
 193         rtfDestination.handleText(text);
 194         return;
 195     }
 196 
 197     warning("Text with no destination. oops.");
 198 }
 199 
 200 /** The default color for text which has no specified color. */
 201 Color defaultColor()
 202 {
 203     return Color.black;
 204 }
 205 
 206 /** Called by the superclass when a new RTF group is begun.
 207  *  This implementation saves the current <code>parserState</code>, and gives
 208  *  the current destination a chance to save its own state.
 209  * @see RTFParser#begingroup
 210  */
 211 public void begingroup()
 212 {
 213     if (skippingCharacters > 0) {
 214         /* TODO this indicates an error in the RTF. Log it? */
 215         skippingCharacters = 0;
 216     }
 217 
 218     /* we do this little dance to avoid cloning the entire state stack and
 219        immediately throwing it away. */
 220     Object oldSaveState = parserState.get("_savedState");
 221     if (oldSaveState != null)
 222         parserState.remove("_savedState");
 223     @SuppressWarnings("unchecked")
 224     Dictionary<String, Object> saveState = (Dictionary<String, Object>)((Hashtable)parserState).clone();
 225     if (oldSaveState != null)
 226         saveState.put("_savedState", oldSaveState);
 227     parserState.put("_savedState", saveState);
 228 
 229     if (rtfDestination != null)
 230         rtfDestination.begingroup();
 231 }
 232 
 233 /** Called by the superclass when the current RTF group is closed.
 234  *  This restores the parserState saved by <code>begingroup()</code>
 235  *  as well as invoking the endgroup method of the current
 236  *  destination.
 237  * @see RTFParser#endgroup
 238  */
 239 public void endgroup()
 240 {
 241     if (skippingCharacters > 0) {
 242         /* NB this indicates an error in the RTF. Log it? */
 243         skippingCharacters = 0;
 244     }
 245 
 246     @SuppressWarnings("unchecked")
 247     Dictionary<Object, Object> restoredState = (Dictionary<Object, Object>)parserState.get("_savedState");
 248     Destination restoredDestination = (Destination)restoredState.get("dst");
 249     if (restoredDestination != rtfDestination) {
 250         rtfDestination.close(); /* allow the destination to clean up */
 251         rtfDestination = restoredDestination;
 252     }
 253     Dictionary<Object, Object> oldParserState = parserState;
 254     parserState = restoredState;
 255     if (rtfDestination != null)
 256         rtfDestination.endgroup(oldParserState);
 257 }
 258 
 259 protected void setRTFDestination(Destination newDestination)
 260 {
 261     /* Check that setting the destination won't close the
 262        current destination (should never happen) */
 263     @SuppressWarnings("unchecked")
 264     Dictionary<Object, Object> previousState = (Dictionary)parserState.get("_savedState");
 265     if (previousState != null) {
 266         if (rtfDestination != previousState.get("dst")) {
 267             warning("Warning, RTF destination overridden, invalid RTF.");
 268             rtfDestination.close();
 269         }
 270     }
 271     rtfDestination = newDestination;
 272     parserState.put("dst", rtfDestination);
 273 }
 274 
 275 /** Called by the user when there is no more input (<i>i.e.</i>,
 276  * at the end of the RTF file.)
 277  *
 278  * @see OutputStream#close
 279  */
 280 public void close()
 281     throws IOException
 282 {
 283     Enumeration<?> docProps = documentAttributes.getAttributeNames();
 284     while(docProps.hasMoreElements()) {
 285         Object propName = docProps.nextElement();
 286         target.putProperty(propName,
 287                            documentAttributes.getAttribute(propName));
 288     }
 289 
 290     /* RTFParser should have ensured that all our groups are closed */
 291 
 292     warning("RTF filter done.");
 293 
 294     super.close();
 295 }
 296 
 297 /**
 298  * Handles a parameterless RTF keyword. This is called by the superclass
 299  * (RTFParser) when a keyword is found in the input stream.
 300  *
 301  * @return true if the keyword is recognized and handled;
 302  *         false otherwise
 303  * @see RTFParser#handleKeyword
 304  */
 305 public boolean handleKeyword(String keyword)
 306 {
 307     String item;
 308     boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;
 309 
 310     if (skippingCharacters > 0) {
 311         skippingCharacters --;
 312         return true;
 313     }
 314 
 315     ignoreGroupIfUnknownKeyword = false;
 316 
 317     if ((item = textKeywords.get(keyword)) != null) {
 318         handleText(item);
 319         return true;
 320     }
 321 
 322     if (keyword.equals("fonttbl")) {
 323         setRTFDestination(new FonttblDestination());
 324         return true;
 325     }
 326 
 327     if (keyword.equals("colortbl")) {
 328         setRTFDestination(new ColortblDestination());
 329         return true;
 330     }
 331 
 332     if (keyword.equals("stylesheet")) {
 333         setRTFDestination(new StylesheetDestination());
 334         return true;
 335     }
 336 
 337     if (keyword.equals("info")) {
 338         setRTFDestination(new InfoDestination());
 339         return false;
 340     }
 341 
 342     if (keyword.equals("mac")) {
 343         setCharacterSet("mac");
 344         return true;
 345     }
 346 
 347     if (keyword.equals("ansi")) {
 348         if (useNeXTForAnsi)
 349             setCharacterSet("NeXT");
 350         else
 351             setCharacterSet("ansi");
 352         return true;
 353     }
 354 
 355     if (keyword.equals("next")) {
 356         setCharacterSet("NeXT");
 357         return true;
 358     }
 359 
 360     if (keyword.equals("pc")) {
 361         setCharacterSet("cpg437"); /* IBM Code Page 437 */
 362         return true;
 363     }
 364 
 365     if (keyword.equals("pca")) {
 366         setCharacterSet("cpg850"); /* IBM Code Page 850 */
 367         return true;
 368     }
 369 
 370     if (keyword.equals("*")) {
 371         ignoreGroupIfUnknownKeyword = true;
 372         return true;
 373     }
 374 
 375     if (rtfDestination != null) {
 376         if(rtfDestination.handleKeyword(keyword))
 377             return true;
 378     }
 379 
 380     /* this point is reached only if the keyword is unrecognized */
 381 
 382     /* other destinations we don't understand and therefore ignore */
 383     if (keyword.equals("aftncn") ||
 384         keyword.equals("aftnsep") ||
 385         keyword.equals("aftnsepc") ||
 386         keyword.equals("annotation") ||
 387         keyword.equals("atnauthor") ||
 388         keyword.equals("atnicn") ||
 389         keyword.equals("atnid") ||
 390         keyword.equals("atnref") ||
 391         keyword.equals("atntime") ||
 392         keyword.equals("atrfend") ||
 393         keyword.equals("atrfstart") ||
 394         keyword.equals("bkmkend") ||
 395         keyword.equals("bkmkstart") ||
 396         keyword.equals("datafield") ||
 397         keyword.equals("do") ||
 398         keyword.equals("dptxbxtext") ||
 399         keyword.equals("falt") ||
 400         keyword.equals("field") ||
 401         keyword.equals("file") ||
 402         keyword.equals("filetbl") ||
 403         keyword.equals("fname") ||
 404         keyword.equals("fontemb") ||
 405         keyword.equals("fontfile") ||
 406         keyword.equals("footer") ||
 407         keyword.equals("footerf") ||
 408         keyword.equals("footerl") ||
 409         keyword.equals("footerr") ||
 410         keyword.equals("footnote") ||
 411         keyword.equals("ftncn") ||
 412         keyword.equals("ftnsep") ||
 413         keyword.equals("ftnsepc") ||
 414         keyword.equals("header") ||
 415         keyword.equals("headerf") ||
 416         keyword.equals("headerl") ||
 417         keyword.equals("headerr") ||
 418         keyword.equals("keycode") ||
 419         keyword.equals("nextfile") ||
 420         keyword.equals("object") ||
 421         keyword.equals("pict") ||
 422         keyword.equals("pn") ||
 423         keyword.equals("pnseclvl") ||
 424         keyword.equals("pntxtb") ||
 425         keyword.equals("pntxta") ||
 426         keyword.equals("revtbl") ||
 427         keyword.equals("rxe") ||
 428         keyword.equals("tc") ||
 429         keyword.equals("template") ||
 430         keyword.equals("txe") ||
 431         keyword.equals("xe")) {
 432         ignoreGroupIfUnknownKeywordSave = true;
 433     }
 434 
 435     if (ignoreGroupIfUnknownKeywordSave) {
 436         setRTFDestination(new DiscardingDestination());
 437     }
 438 
 439     return false;
 440 }
 441 
 442 /**
 443  * Handles an RTF keyword and its integer parameter.
 444  * This is called by the superclass
 445  * (RTFParser) when a keyword is found in the input stream.
 446  *
 447  * @return true if the keyword is recognized and handled;
 448  *         false otherwise
 449  * @see RTFParser#handleKeyword
 450  */
 451 public boolean handleKeyword(String keyword, int parameter)
 452 {
 453     boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;
 454 
 455     if (skippingCharacters > 0) {
 456         skippingCharacters --;
 457         return true;
 458     }
 459 
 460     ignoreGroupIfUnknownKeyword = false;
 461 
 462     if (keyword.equals("uc")) {
 463         /* count of characters to skip after a unicode character */
 464         parserState.put("UnicodeSkip", Integer.valueOf(parameter));
 465         return true;
 466     }
 467     if (keyword.equals("u")) {
 468         if (parameter < 0)
 469             parameter = parameter + 65536;
 470         handleText((char)parameter);
 471         Number skip = (Number)(parserState.get("UnicodeSkip"));
 472         if (skip != null) {
 473             skippingCharacters = skip.intValue();
 474         } else {
 475             skippingCharacters = 1;
 476         }
 477         return true;
 478     }
 479 
 480     if (keyword.equals("rtf")) {
 481         rtfversion = parameter;
 482         setRTFDestination(new DocumentDestination());
 483         return true;
 484     }
 485 
 486     if (keyword.startsWith("NeXT") ||
 487         keyword.equals("private"))
 488         ignoreGroupIfUnknownKeywordSave = true;
 489 
 490     if (rtfDestination != null) {
 491         if(rtfDestination.handleKeyword(keyword, parameter))
 492             return true;
 493     }
 494 
 495     /* this point is reached only if the keyword is unrecognized */
 496 
 497     if (ignoreGroupIfUnknownKeywordSave) {
 498         setRTFDestination(new DiscardingDestination());
 499     }
 500 
 501     return false;
 502 }
 503 
 504 private void setTargetAttribute(String name, Object value)
 505 {
 506 //    target.changeAttributes(new LFDictionary(LFArray.arrayWithObject(value), LFArray.arrayWithObject(name)));
 507 }
 508 
 509 /**
 510  * setCharacterSet sets the current translation table to correspond with
 511  * the named character set. The character set is loaded if necessary.
 512  *
 513  * @see AbstractFilter
 514  */
 515 public void setCharacterSet(String name)
 516 {
 517     Object set;
 518 
 519     try {
 520         set = getCharacterSet(name);
 521     } catch (Exception e) {
 522         warning("Exception loading RTF character set \"" + name + "\": " + e);
 523         set = null;
 524     }
 525 
 526     if (set != null) {
 527         translationTable = (char[])set;
 528     } else {
 529         warning("Unknown RTF character set \"" + name + "\"");
 530         if (!name.equals("ansi")) {
 531             try {
 532                 translationTable = (char[])getCharacterSet("ansi");
 533             } catch (IOException e) {
 534                 throw new InternalError("RTFReader: Unable to find character set resources (" + e + ")", e);
 535             }
 536         }
 537     }
 538 
 539     setTargetAttribute(Constants.RTFCharacterSet, name);
 540 }
 541 
 542 /** Adds a character set to the RTFReader's list
 543  *  of known character sets */
 544 public static void
 545 defineCharacterSet(String name, char[] table)
 546 {
 547     if (table.length < 256)
 548         throw new IllegalArgumentException("Translation table must have 256 entries.");
 549     characterSets.put(name, table);
 550 }
 551 
 552 /** Looks up a named character set. A character set is a 256-entry
 553  *  array of characters, mapping unsigned byte values to their Unicode
 554  *  equivalents. The character set is loaded if necessary.
 555  *
 556  *  @return the character set
 557  */
 558 public static Object
 559 getCharacterSet(final String name)
 560     throws IOException
 561 {
 562     char[] set = characterSets.get(name);
 563     if (set == null) {
 564         InputStream charsetStream = AccessController.doPrivileged(
 565                 new PrivilegedAction<InputStream>() {
 566                     public InputStream run() {
 567                         return RTFReader.class.getResourceAsStream("charsets/" + name + ".txt");
 568                     }
 569                 });
 570         set = readCharset(charsetStream);
 571         defineCharacterSet(name, set);
 572     }
 573     return set;
 574 }
 575 
 576 /** Parses a character set from an InputStream. The character set
 577  * must contain 256 decimal integers, separated by whitespace, with
 578  * no punctuation. B- and C- style comments are allowed.
 579  *
 580  * @return the newly read character set
 581  */
 582 static char[] readCharset(InputStream strm)
 583      throws IOException
 584 {
 585     char[] values = new char[256];
 586     int i;
 587     StreamTokenizer in = new StreamTokenizer(new BufferedReader(
 588             new InputStreamReader(strm, "ISO-8859-1")));
 589 
 590     in.eolIsSignificant(false);
 591     in.commentChar('#');
 592     in.slashSlashComments(true);
 593     in.slashStarComments(true);
 594 
 595     i = 0;
 596     while (i < 256) {
 597         int ttype;
 598         try {
 599             ttype = in.nextToken();
 600         } catch (Exception e) {
 601             throw new IOException("Unable to read from character set file (" + e + ")");
 602         }
 603         if (ttype != StreamTokenizer.TT_NUMBER) {
 604 //          System.out.println("Bad token: type=" + ttype + " tok=" + in.sval);
 605             throw new IOException("Unexpected token in character set file");
 606 //          continue;
 607         }
 608         values[i] = (char)(in.nval);
 609         i++;
 610     }
 611 
 612     return values;
 613 }
 614 
 615 static char[] readCharset(java.net.URL href)
 616      throws IOException
 617 {
 618     return readCharset(href.openStream());
 619 }
 620 
 621 /** An interface (could be an entirely abstract class) describing
 622  *  a destination. The RTF reader always has a current destination
 623  *  which is where text is sent.
 624  *
 625  *  @see RTFReader
 626  */
 627 interface Destination {
 628     void handleBinaryBlob(byte[] data);
 629     void handleText(String text);
 630     boolean handleKeyword(String keyword);
 631     boolean handleKeyword(String keyword, int parameter);
 632 
 633     void begingroup();
 634     void endgroup(Dictionary<Object, Object> oldState);
 635 
 636     void close();
 637 }
 638 
 639 /** This data-sink class is used to implement ignored destinations
 640  *  (e.g. {\*\blegga blah blah blah} )
 641  *  It accepts all keywords and text but does nothing with them. */
 642 class DiscardingDestination implements Destination
 643 {
 644     public void handleBinaryBlob(byte[] data)
 645     {
 646         /* Discard binary blobs. */
 647     }
 648 
 649     public void handleText(String text)
 650     {
 651         /* Discard text. */
 652     }
 653 
 654     public boolean handleKeyword(String text)
 655     {
 656         /* Accept and discard keywords. */
 657         return true;
 658     }
 659 
 660     public boolean handleKeyword(String text, int parameter)
 661     {
 662         /* Accept and discard parameterized keywords. */
 663         return true;
 664     }
 665 
 666     public void begingroup()
 667     {
 668         /* Ignore groups --- the RTFReader will keep track of the
 669            current group level as necessary */
 670     }
 671 
 672     public void endgroup(Dictionary<Object, Object> oldState)
 673     {
 674         /* Ignore groups */
 675     }
 676 
 677     public void close()
 678     {
 679         /* No end-of-destination cleanup needed */
 680     }
 681 }
 682 
 683 /** Reads the fonttbl group, inserting fonts into the RTFReader's
 684  *  fontTable dictionary. */
 685 class FonttblDestination implements Destination
 686 {
 687     int nextFontNumber;
 688     Integer fontNumberKey = null;
 689     String nextFontFamily;
 690 
 691     public void handleBinaryBlob(byte[] data)
 692     { /* Discard binary blobs. */ }
 693 
 694     public void handleText(String text)
 695     {
 696         int semicolon = text.indexOf(';');
 697         String fontName;
 698 
 699         if (semicolon > -1)
 700             fontName = text.substring(0, semicolon);
 701         else
 702             fontName = text;
 703 
 704 
 705         /* TODO: do something with the font family. */
 706 
 707         if (nextFontNumber == -1
 708             && fontNumberKey != null) {
 709             //font name might be broken across multiple calls
 710             fontName = fontTable.get(fontNumberKey) + fontName;
 711         } else {
 712             fontNumberKey = Integer.valueOf(nextFontNumber);
 713         }
 714         fontTable.put(fontNumberKey, fontName);
 715 
 716         nextFontNumber = -1;
 717         nextFontFamily = null;
 718     }
 719 
 720     public boolean handleKeyword(String keyword)
 721     {
 722         if (keyword.charAt(0) == 'f') {
 723             nextFontFamily = keyword.substring(1);
 724             return true;
 725         }
 726 
 727         return false;
 728     }
 729 
 730     public boolean handleKeyword(String keyword, int parameter)
 731     {
 732         if (keyword.equals("f")) {
 733             nextFontNumber = parameter;
 734             return true;
 735         }
 736 
 737         return false;
 738     }
 739 
 740     /* Groups are irrelevant. */
 741     public void begingroup() {}
 742     public void endgroup(Dictionary<Object, Object> oldState) {}
 743 
 744     /* currently, the only thing we do when the font table ends is
 745        dump its contents to the debugging log. */
 746     public void close()
 747     {
 748         Enumeration<Integer> nums = fontTable.keys();
 749         warning("Done reading font table.");
 750         while(nums.hasMoreElements()) {
 751             Integer num = nums.nextElement();
 752             warning("Number " + num + ": " + fontTable.get(num));
 753         }
 754     }
 755 }
 756 
 757 /** Reads the colortbl group. Upon end-of-group, the RTFReader's
 758  *  color table is set to an array containing the read colors. */
 759 class ColortblDestination implements Destination
 760 {
 761     int red, green, blue;
 762     Vector<Color> proTemTable;
 763 
 764     public ColortblDestination()
 765     {
 766         red = 0;
 767         green = 0;
 768         blue = 0;
 769         proTemTable = new Vector<Color>();
 770     }
 771 
 772     public void handleText(String text)
 773     {
 774         int index;
 775 
 776         for (index = 0; index < text.length(); index ++) {
 777             if (text.charAt(index) == ';') {
 778                 Color newColor;
 779                 newColor = new Color(red, green, blue);
 780                 proTemTable.addElement(newColor);
 781             }
 782         }
 783     }
 784 
 785     public void close()
 786     {
 787         int count = proTemTable.size();
 788         warning("Done reading color table, " + count + " entries.");
 789         colorTable = new Color[count];
 790         proTemTable.copyInto(colorTable);
 791     }
 792 
 793     public boolean handleKeyword(String keyword, int parameter)
 794     {
 795         if (keyword.equals("red"))
 796             red = parameter;
 797         else if (keyword.equals("green"))
 798             green = parameter;
 799         else if (keyword.equals("blue"))
 800             blue = parameter;
 801         else
 802             return false;
 803 
 804         return true;
 805     }
 806 
 807     /* Colortbls don't understand any parameterless keywords */
 808     public boolean handleKeyword(String keyword) { return false; }
 809 
 810     /* Groups are irrelevant. */
 811     public void begingroup() {}
 812     public void endgroup(Dictionary<Object, Object> oldState) {}
 813 
 814     /* Shouldn't see any binary blobs ... */
 815     public void handleBinaryBlob(byte[] data) {}
 816 }
 817 
 818 /** Handles the stylesheet keyword. Styles are read and sorted
 819  *  into the three style arrays in the RTFReader. */
 820 class StylesheetDestination
 821     extends DiscardingDestination
 822     implements Destination
 823 {
 824     Dictionary<Integer, StyleDefiningDestination> definedStyles;
 825 
 826     public StylesheetDestination()
 827     {
 828         definedStyles = new Hashtable<Integer, StyleDefiningDestination>();
 829     }
 830 
 831     public void begingroup()
 832     {
 833         setRTFDestination(new StyleDefiningDestination());
 834     }
 835 
 836     public void close()
 837     {
 838         Vector<Style> chrStyles = new Vector<Style>();
 839         Vector<Style> pgfStyles = new Vector<Style>();
 840         Vector<Style> secStyles = new Vector<Style>();
 841         Enumeration<StyleDefiningDestination> styles = definedStyles.elements();
 842         while(styles.hasMoreElements()) {
 843             StyleDefiningDestination style;
 844             Style defined;
 845             style = styles.nextElement();
 846             defined = style.realize();
 847             warning("Style "+style.number+" ("+style.styleName+"): "+defined);
 848             String stype = (String)defined.getAttribute(Constants.StyleType);
 849             Vector<Style> toSet;
 850             if (stype.equals(Constants.STSection)) {
 851                 toSet = secStyles;
 852             } else if (stype.equals(Constants.STCharacter)) {
 853                 toSet = chrStyles;
 854             } else {
 855                 toSet = pgfStyles;
 856             }
 857             if (toSet.size() <= style.number)
 858                 toSet.setSize(style.number + 1);
 859             toSet.setElementAt(defined, style.number);
 860         }
 861         if (!(chrStyles.isEmpty())) {
 862             Style[] styleArray = new Style[chrStyles.size()];
 863             chrStyles.copyInto(styleArray);
 864             characterStyles = styleArray;
 865         }
 866         if (!(pgfStyles.isEmpty())) {
 867             Style[] styleArray = new Style[pgfStyles.size()];
 868             pgfStyles.copyInto(styleArray);
 869             paragraphStyles = styleArray;
 870         }
 871         if (!(secStyles.isEmpty())) {
 872             Style[] styleArray = new Style[secStyles.size()];
 873             secStyles.copyInto(styleArray);
 874             sectionStyles = styleArray;
 875         }
 876 
 877 /* (old debugging code)
 878         int i, m;
 879         if (characterStyles != null) {
 880           m = characterStyles.length;
 881           for(i=0;i<m;i++)
 882             warnings.println("chrStyle["+i+"]="+characterStyles[i]);
 883         } else warnings.println("No character styles.");
 884         if (paragraphStyles != null) {
 885           m = paragraphStyles.length;
 886           for(i=0;i<m;i++)
 887             warnings.println("pgfStyle["+i+"]="+paragraphStyles[i]);
 888         } else warnings.println("No paragraph styles.");
 889         if (sectionStyles != null) {
 890           m = characterStyles.length;
 891           for(i=0;i<m;i++)
 892             warnings.println("secStyle["+i+"]="+sectionStyles[i]);
 893         } else warnings.println("No section styles.");
 894 */
 895     }
 896 
 897     /** This subclass handles an individual style */
 898     class StyleDefiningDestination
 899         extends AttributeTrackingDestination
 900         implements Destination
 901     {
 902         final int STYLENUMBER_NONE = 222;
 903         boolean additive;
 904         boolean characterStyle;
 905         boolean sectionStyle;
 906         public String styleName;
 907         public int number;
 908         int basedOn;
 909         int nextStyle;
 910         boolean hidden;
 911 
 912         Style realizedStyle;
 913 
 914         public StyleDefiningDestination()
 915         {
 916             additive = false;
 917             characterStyle = false;
 918             sectionStyle = false;
 919             styleName = null;
 920             number = 0;
 921             basedOn = STYLENUMBER_NONE;
 922             nextStyle = STYLENUMBER_NONE;
 923             hidden = false;
 924         }
 925 
 926         public void handleText(String text)
 927         {
 928             if (styleName != null)
 929                 styleName = styleName + text;
 930             else
 931                 styleName = text;
 932         }
 933 
 934         public void close() {
 935             int semicolon = (styleName == null) ? 0 : styleName.indexOf(';');
 936             if (semicolon > 0)
 937                 styleName = styleName.substring(0, semicolon);
 938             definedStyles.put(Integer.valueOf(number), this);
 939             super.close();
 940         }
 941 
 942         public boolean handleKeyword(String keyword)
 943         {
 944             if (keyword.equals("additive")) {
 945                 additive = true;
 946                 return true;
 947             }
 948             if (keyword.equals("shidden")) {
 949                 hidden = true;
 950                 return true;
 951             }
 952             return super.handleKeyword(keyword);
 953         }
 954 
 955         public boolean handleKeyword(String keyword, int parameter)
 956         {
 957             if (keyword.equals("s")) {
 958                 characterStyle = false;
 959                 sectionStyle = false;
 960                 number = parameter;
 961             } else if (keyword.equals("cs")) {
 962                 characterStyle = true;
 963                 sectionStyle = false;
 964                 number = parameter;
 965             } else if (keyword.equals("ds")) {
 966                 characterStyle = false;
 967                 sectionStyle = true;
 968                 number = parameter;
 969             } else if (keyword.equals("sbasedon")) {
 970                 basedOn = parameter;
 971             } else if (keyword.equals("snext")) {
 972                 nextStyle = parameter;
 973             } else {
 974                 return super.handleKeyword(keyword, parameter);
 975             }
 976             return true;
 977         }
 978 
 979         public Style realize()
 980         {
 981             Style basis = null;
 982             Style next = null;
 983 
 984             if (realizedStyle != null)
 985                 return realizedStyle;
 986 
 987             if (basedOn != STYLENUMBER_NONE) {
 988                 StyleDefiningDestination styleDest;
 989                 styleDest = definedStyles.get(Integer.valueOf(basedOn));
 990                 if (styleDest != null && styleDest != this) {
 991                     basis = styleDest.realize();
 992                 }
 993             }
 994 
 995             /* NB: Swing StyleContext doesn't allow distinct styles with
 996                the same name; RTF apparently does. This may confuse the
 997                user. */
 998             realizedStyle = target.addStyle(styleName, basis);
 999 
1000             if (characterStyle) {
1001                 realizedStyle.addAttributes(currentTextAttributes());
1002                 realizedStyle.addAttribute(Constants.StyleType,
1003                                            Constants.STCharacter);
1004             } else if (sectionStyle) {
1005                 realizedStyle.addAttributes(currentSectionAttributes());
1006                 realizedStyle.addAttribute(Constants.StyleType,
1007                                            Constants.STSection);
1008             } else { /* must be a paragraph style */
1009                 realizedStyle.addAttributes(currentParagraphAttributes());
1010                 realizedStyle.addAttribute(Constants.StyleType,
1011                                            Constants.STParagraph);
1012             }
1013 
1014             if (nextStyle != STYLENUMBER_NONE) {
1015                 StyleDefiningDestination styleDest;
1016                 styleDest = definedStyles.get(Integer.valueOf(nextStyle));
1017                 if (styleDest != null) {
1018                     next = styleDest.realize();
1019                 }
1020             }
1021 
1022             if (next != null)
1023                 realizedStyle.addAttribute(Constants.StyleNext, next);
1024             realizedStyle.addAttribute(Constants.StyleAdditive,
1025                                        Boolean.valueOf(additive));
1026             realizedStyle.addAttribute(Constants.StyleHidden,
1027                                        Boolean.valueOf(hidden));
1028 
1029             return realizedStyle;
1030         }
1031     }
1032 }
1033 
1034 /** Handles the info group. Currently no info keywords are recognized
1035  *  so this is a subclass of DiscardingDestination. */
1036 class InfoDestination
1037     extends DiscardingDestination
1038     implements Destination
1039 {
1040 }
1041 
1042 /** RTFReader.TextHandlingDestination is an abstract RTF destination
1043  *  which simply tracks the attributes specified by the RTF control words
1044  *  in internal form and can produce acceptable AttributeSets for the
1045  *  current character, paragraph, and section attributes. It is up
1046  *  to the subclasses to determine what is done with the actual text. */
1047 abstract class AttributeTrackingDestination implements Destination
1048 {
1049     /** This is the "chr" element of parserState, cached for
1050      *  more efficient use */
1051     MutableAttributeSet characterAttributes;
1052     /** This is the "pgf" element of parserState, cached for
1053      *  more efficient use */
1054     MutableAttributeSet paragraphAttributes;
1055     /** This is the "sec" element of parserState, cached for
1056      *  more efficient use */
1057     MutableAttributeSet sectionAttributes;
1058 
1059     public AttributeTrackingDestination()
1060     {
1061         characterAttributes = rootCharacterAttributes();
1062         parserState.put("chr", characterAttributes);
1063         paragraphAttributes = rootParagraphAttributes();
1064         parserState.put("pgf", paragraphAttributes);
1065         sectionAttributes = rootSectionAttributes();
1066         parserState.put("sec", sectionAttributes);
1067     }
1068 
1069     public abstract void handleText(String text);
1070 
1071     public void handleBinaryBlob(byte[] data)
1072     {
1073         /* This should really be in TextHandlingDestination, but
1074          * since *nobody* does anything with binary blobs, this
1075          * is more convenient. */
1076         warning("Unexpected binary data in RTF file.");
1077     }
1078 
1079     public void begingroup()
1080     {
1081         AttributeSet characterParent = currentTextAttributes();
1082         AttributeSet paragraphParent = currentParagraphAttributes();
1083         AttributeSet sectionParent = currentSectionAttributes();
1084 
1085         /* It would probably be more efficient to use the
1086          * resolver property of the attributes set for
1087          * implementing rtf groups,
1088          * but that's needed for styles. */
1089 
1090         /* update the cached attribute dictionaries */
1091         characterAttributes = new SimpleAttributeSet();
1092         characterAttributes.addAttributes(characterParent);
1093         parserState.put("chr", characterAttributes);
1094 
1095         paragraphAttributes = new SimpleAttributeSet();
1096         paragraphAttributes.addAttributes(paragraphParent);
1097         parserState.put("pgf", paragraphAttributes);
1098 
1099         sectionAttributes = new SimpleAttributeSet();
1100         sectionAttributes.addAttributes(sectionParent);
1101         parserState.put("sec", sectionAttributes);
1102     }
1103 
1104     public void endgroup(Dictionary<Object, Object> oldState)
1105     {
1106         characterAttributes = (MutableAttributeSet)parserState.get("chr");
1107         paragraphAttributes = (MutableAttributeSet)parserState.get("pgf");
1108         sectionAttributes   = (MutableAttributeSet)parserState.get("sec");
1109     }
1110 
1111     public void close()
1112     {
1113     }
1114 
1115     public boolean handleKeyword(String keyword)
1116     {
1117         if (keyword.equals("ulnone")) {
1118             return handleKeyword("ul", 0);
1119         }
1120 
1121         {
1122             RTFAttribute attr = straightforwardAttributes.get(keyword);
1123             if (attr != null) {
1124                 boolean ok;
1125 
1126                 switch(attr.domain()) {
1127                   case RTFAttribute.D_CHARACTER:
1128                     ok = attr.set(characterAttributes);
1129                     break;
1130                   case RTFAttribute.D_PARAGRAPH:
1131                     ok = attr.set(paragraphAttributes);
1132                     break;
1133                   case RTFAttribute.D_SECTION:
1134                     ok = attr.set(sectionAttributes);
1135                     break;
1136                   case RTFAttribute.D_META:
1137                     mockery.backing = parserState;
1138                     ok = attr.set(mockery);
1139                     mockery.backing = null;
1140                     break;
1141                   case RTFAttribute.D_DOCUMENT:
1142                     ok = attr.set(documentAttributes);
1143                     break;
1144                   default:
1145                     /* should never happen */
1146                     ok = false;
1147                     break;
1148                 }
1149                 if (ok)
1150                     return true;
1151             }
1152         }
1153 
1154 
1155         if (keyword.equals("plain")) {
1156             resetCharacterAttributes();
1157             return true;
1158         }
1159 
1160         if (keyword.equals("pard")) {
1161             resetParagraphAttributes();
1162             return true;
1163         }
1164 
1165         if (keyword.equals("sectd")) {
1166             resetSectionAttributes();
1167             return true;
1168         }
1169 
1170         return false;
1171     }
1172 
1173     public boolean handleKeyword(String keyword, int parameter)
1174     {
1175         boolean booleanParameter = (parameter != 0);
1176 
1177         if (keyword.equals("fc"))
1178             keyword = "cf"; /* whatEVER, dude. */
1179 
1180         if (keyword.equals("f")) {
1181             parserState.put(keyword, Integer.valueOf(parameter));
1182             return true;
1183         }
1184         if (keyword.equals("cf")) {
1185             parserState.put(keyword, Integer.valueOf(parameter));
1186             return true;
1187         }
1188 
1189         {
1190             RTFAttribute attr = straightforwardAttributes.get(keyword);
1191             if (attr != null) {
1192                 boolean ok;
1193 
1194                 switch(attr.domain()) {
1195                   case RTFAttribute.D_CHARACTER:
1196                     ok = attr.set(characterAttributes, parameter);
1197                     break;
1198                   case RTFAttribute.D_PARAGRAPH:
1199                     ok = attr.set(paragraphAttributes, parameter);
1200                     break;
1201                   case RTFAttribute.D_SECTION:
1202                     ok = attr.set(sectionAttributes, parameter);
1203                     break;
1204                   case RTFAttribute.D_META:
1205                     mockery.backing = parserState;
1206                     ok = attr.set(mockery, parameter);
1207                     mockery.backing = null;
1208                     break;
1209                   case RTFAttribute.D_DOCUMENT:
1210                     ok = attr.set(documentAttributes, parameter);
1211                     break;
1212                   default:
1213                     /* should never happen */
1214                     ok = false;
1215                     break;
1216                 }
1217                 if (ok)
1218                     return true;
1219             }
1220         }
1221 
1222         if (keyword.equals("fs")) {
1223             StyleConstants.setFontSize(characterAttributes, (parameter / 2));
1224             return true;
1225         }
1226 
1227         /* TODO: superscript/subscript */
1228 
1229         if (keyword.equals("sl")) {
1230             if (parameter == 1000) {  /* magic value! */
1231                 characterAttributes.removeAttribute(StyleConstants.LineSpacing);
1232             } else {
1233                 /* TODO: The RTF sl attribute has special meaning if it's
1234                    negative. Make sure that SwingText has the same special
1235                    meaning, or find a way to imitate that. When SwingText
1236                    handles this, also recognize the slmult keyword. */
1237                 StyleConstants.setLineSpacing(characterAttributes,
1238                                               parameter / 20f);
1239             }
1240             return true;
1241         }
1242 
1243         /* TODO: Other kinds of underlining */
1244 
1245         if (keyword.equals("tx") || keyword.equals("tb")) {
1246             float tabPosition = parameter / 20f;
1247             int tabAlignment, tabLeader;
1248             Number item;
1249 
1250             tabAlignment = TabStop.ALIGN_LEFT;
1251             item = (Number)(parserState.get("tab_alignment"));
1252             if (item != null)
1253                 tabAlignment = item.intValue();
1254             tabLeader = TabStop.LEAD_NONE;
1255             item = (Number)(parserState.get("tab_leader"));
1256             if (item != null)
1257                 tabLeader = item.intValue();
1258             if (keyword.equals("tb"))
1259                 tabAlignment = TabStop.ALIGN_BAR;
1260 
1261             parserState.remove("tab_alignment");
1262             parserState.remove("tab_leader");
1263 
1264             TabStop newStop = new TabStop(tabPosition, tabAlignment, tabLeader);
1265             Dictionary<Object, Object> tabs;
1266             Integer stopCount;
1267 
1268             @SuppressWarnings("unchecked")
1269             Dictionary<Object, Object>tmp = (Dictionary)parserState.get("_tabs");
1270             tabs = tmp;
1271             if (tabs == null) {
1272                 tabs = new Hashtable<Object, Object>();
1273                 parserState.put("_tabs", tabs);
1274                 stopCount = Integer.valueOf(1);
1275             } else {
1276                 stopCount = (Integer)tabs.get("stop count");
1277                 stopCount = Integer.valueOf(1 + stopCount.intValue());
1278             }
1279             tabs.put(stopCount, newStop);
1280             tabs.put("stop count", stopCount);
1281             parserState.remove("_tabs_immutable");
1282 
1283             return true;
1284         }
1285 
1286         if (keyword.equals("s") &&
1287             paragraphStyles != null) {
1288             parserState.put("paragraphStyle", paragraphStyles[parameter]);
1289             return true;
1290         }
1291 
1292         if (keyword.equals("cs") &&
1293             characterStyles != null) {
1294             parserState.put("characterStyle", characterStyles[parameter]);
1295             return true;
1296         }
1297 
1298         if (keyword.equals("ds") &&
1299             sectionStyles != null) {
1300             parserState.put("sectionStyle", sectionStyles[parameter]);
1301             return true;
1302         }
1303 
1304         return false;
1305     }
1306 
1307     /** Returns a new MutableAttributeSet containing the
1308      *  default character attributes */
1309     protected MutableAttributeSet rootCharacterAttributes()
1310     {
1311         MutableAttributeSet set = new SimpleAttributeSet();
1312 
1313         /* TODO: default font */
1314 
1315         StyleConstants.setItalic(set, false);
1316         StyleConstants.setBold(set, false);
1317         StyleConstants.setUnderline(set, false);
1318         StyleConstants.setForeground(set, defaultColor());
1319 
1320         return set;
1321     }
1322 
1323     /** Returns a new MutableAttributeSet containing the
1324      *  default paragraph attributes */
1325     protected MutableAttributeSet rootParagraphAttributes()
1326     {
1327         MutableAttributeSet set = new SimpleAttributeSet();
1328 
1329         StyleConstants.setLeftIndent(set, 0f);
1330         StyleConstants.setRightIndent(set, 0f);
1331         StyleConstants.setFirstLineIndent(set, 0f);
1332 
1333         /* TODO: what should this be, really? */
1334         set.setResolveParent(target.getStyle(StyleContext.DEFAULT_STYLE));
1335 
1336         return set;
1337     }
1338 
1339     /** Returns a new MutableAttributeSet containing the
1340      *  default section attributes */
1341     protected MutableAttributeSet rootSectionAttributes()
1342     {
1343         MutableAttributeSet set = new SimpleAttributeSet();
1344 
1345         return set;
1346     }
1347 
1348     /**
1349      * Calculates the current text (character) attributes in a form suitable
1350      * for SwingText from the current parser state.
1351      *
1352      * @return a new MutableAttributeSet containing the text attributes.
1353      */
1354     MutableAttributeSet currentTextAttributes()
1355     {
1356         MutableAttributeSet attributes =
1357             new SimpleAttributeSet(characterAttributes);
1358         Integer fontnum;
1359         Integer stateItem;
1360 
1361         /* figure out the font name */
1362         /* TODO: catch exceptions for undefined attributes,
1363            bad font indices, etc.? (as it stands, it is the caller's
1364            job to clean up after corrupt RTF) */
1365         fontnum = (Integer)parserState.get("f");
1366         /* note setFontFamily() can not handle a null font */
1367         String fontFamily;
1368         if (fontnum != null)
1369             fontFamily = fontTable.get(fontnum);
1370         else
1371             fontFamily = null;
1372         if (fontFamily != null)
1373             StyleConstants.setFontFamily(attributes, fontFamily);
1374         else
1375             attributes.removeAttribute(StyleConstants.FontFamily);
1376 
1377         if (colorTable != null) {
1378             stateItem = (Integer)parserState.get("cf");
1379             if (stateItem != null) {
1380                 Color fg = colorTable[stateItem.intValue()];
1381                 StyleConstants.setForeground(attributes, fg);
1382             } else {
1383                 /* AttributeSet dies if you set a value to null */
1384                 attributes.removeAttribute(StyleConstants.Foreground);
1385             }
1386         }
1387 
1388         if (colorTable != null) {
1389             stateItem = (Integer)parserState.get("cb");
1390             if (stateItem != null) {
1391                 Color bg = colorTable[stateItem.intValue()];
1392                 attributes.addAttribute(StyleConstants.Background,
1393                                         bg);
1394             } else {
1395                 /* AttributeSet dies if you set a value to null */
1396                 attributes.removeAttribute(StyleConstants.Background);
1397             }
1398         }
1399 
1400         Style characterStyle = (Style)parserState.get("characterStyle");
1401         if (characterStyle != null)
1402             attributes.setResolveParent(characterStyle);
1403 
1404         /* Other attributes are maintained directly in "attributes" */
1405 
1406         return attributes;
1407     }
1408 
1409     /**
1410      * Calculates the current paragraph attributes (with keys
1411      * as given in StyleConstants) from the current parser state.
1412      *
1413      * @return a newly created MutableAttributeSet.
1414      * @see StyleConstants
1415      */
1416     MutableAttributeSet currentParagraphAttributes()
1417     {
1418         /* NB if there were a mutableCopy() method we should use it */
1419         MutableAttributeSet bld = new SimpleAttributeSet(paragraphAttributes);
1420 
1421         Integer stateItem;
1422 
1423         /*** Tab stops ***/
1424         TabStop tabs[];
1425 
1426         tabs = (TabStop[])parserState.get("_tabs_immutable");
1427         if (tabs == null) {
1428             @SuppressWarnings("unchecked")
1429             Dictionary<Object, Object> workingTabs = (Dictionary)parserState.get("_tabs");
1430             if (workingTabs != null) {
1431                 int count = ((Integer)workingTabs.get("stop count")).intValue();
1432                 tabs = new TabStop[count];
1433                 for (int ix = 1; ix <= count; ix ++)
1434                     tabs[ix-1] = (TabStop)workingTabs.get(Integer.valueOf(ix));
1435                 parserState.put("_tabs_immutable", tabs);
1436             }
1437         }
1438         if (tabs != null)
1439             bld.addAttribute(Constants.Tabs, tabs);
1440 
1441         Style paragraphStyle = (Style)parserState.get("paragraphStyle");
1442         if (paragraphStyle != null)
1443             bld.setResolveParent(paragraphStyle);
1444 
1445         return bld;
1446     }
1447 
1448     /**
1449      * Calculates the current section attributes
1450      * from the current parser state.
1451      *
1452      * @return a newly created MutableAttributeSet.
1453      */
1454     public AttributeSet currentSectionAttributes()
1455     {
1456         MutableAttributeSet attributes = new SimpleAttributeSet(sectionAttributes);
1457 
1458         Style sectionStyle = (Style)parserState.get("sectionStyle");
1459         if (sectionStyle != null)
1460             attributes.setResolveParent(sectionStyle);
1461 
1462         return attributes;
1463     }
1464 
1465     /** Resets the filter's internal notion of the current character
1466      *  attributes to their default values. Invoked to handle the
1467      *  \plain keyword. */
1468     protected void resetCharacterAttributes()
1469     {
1470         handleKeyword("f", 0);
1471         handleKeyword("cf", 0);
1472 
1473         handleKeyword("fs", 24);  /* 12 pt. */
1474 
1475         Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
1476         while(attributes.hasMoreElements()) {
1477             RTFAttribute attr = attributes.nextElement();
1478             if (attr.domain() == RTFAttribute.D_CHARACTER)
1479                 attr.setDefault(characterAttributes);
1480         }
1481 
1482         handleKeyword("sl", 1000);
1483 
1484         parserState.remove("characterStyle");
1485     }
1486 
1487     /** Resets the filter's internal notion of the current paragraph's
1488      *  attributes to their default values. Invoked to handle the
1489      *  \pard keyword. */
1490     protected void resetParagraphAttributes()
1491     {
1492         parserState.remove("_tabs");
1493         parserState.remove("_tabs_immutable");
1494         parserState.remove("paragraphStyle");
1495 
1496         StyleConstants.setAlignment(paragraphAttributes,
1497                                     StyleConstants.ALIGN_LEFT);
1498 
1499         Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
1500         while(attributes.hasMoreElements()) {
1501             RTFAttribute attr = attributes.nextElement();
1502             if (attr.domain() == RTFAttribute.D_PARAGRAPH)
1503                 attr.setDefault(characterAttributes);
1504         }
1505     }
1506 
1507     /** Resets the filter's internal notion of the current section's
1508      *  attributes to their default values. Invoked to handle the
1509      *  \sectd keyword. */
1510     protected void resetSectionAttributes()
1511     {
1512         Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
1513         while(attributes.hasMoreElements()) {
1514             RTFAttribute attr = attributes.nextElement();
1515             if (attr.domain() == RTFAttribute.D_SECTION)
1516                 attr.setDefault(characterAttributes);
1517         }
1518 
1519         parserState.remove("sectionStyle");
1520     }
1521 }
1522 
1523 /** RTFReader.TextHandlingDestination provides basic text handling
1524  *  functionality. Subclasses must implement: <dl>
1525  *  <dt>deliverText()<dd>to handle a run of text with the same
1526  *                       attributes
1527  *  <dt>finishParagraph()<dd>to end the current paragraph and
1528  *                           set the paragraph's attributes
1529  *  <dt>endSection()<dd>to end the current section
1530  *  </dl>
1531  */
1532 abstract class TextHandlingDestination
1533     extends AttributeTrackingDestination
1534     implements Destination
1535 {
1536     /** <code>true</code> if the reader has not just finished
1537      *  a paragraph; false upon startup */
1538     boolean inParagraph;
1539 
1540     public TextHandlingDestination()
1541     {
1542         super();
1543         inParagraph = false;
1544     }
1545 
1546     public void handleText(String text)
1547     {
1548         if (! inParagraph)
1549             beginParagraph();
1550 
1551         deliverText(text, currentTextAttributes());
1552     }
1553 
1554     abstract void deliverText(String text, AttributeSet characterAttributes);
1555 
1556     public void close()
1557     {
1558         if (inParagraph)
1559             endParagraph();
1560 
1561         super.close();
1562     }
1563 
1564     public boolean handleKeyword(String keyword)
1565     {
1566         if (keyword.equals("\r") || keyword.equals("\n")) {
1567             keyword = "par";
1568         }
1569 
1570         if (keyword.equals("par")) {
1571 //          warnings.println("Ending paragraph.");
1572             endParagraph();
1573             return true;
1574         }
1575 
1576         if (keyword.equals("sect")) {
1577 //          warnings.println("Ending section.");
1578             endSection();
1579             return true;
1580         }
1581 
1582         return super.handleKeyword(keyword);
1583     }
1584 
1585     protected void beginParagraph()
1586     {
1587         inParagraph = true;
1588     }
1589 
1590     protected void endParagraph()
1591     {
1592         AttributeSet pgfAttributes = currentParagraphAttributes();
1593         AttributeSet chrAttributes = currentTextAttributes();
1594         finishParagraph(pgfAttributes, chrAttributes);
1595         inParagraph = false;
1596     }
1597 
1598     abstract void finishParagraph(AttributeSet pgfA, AttributeSet chrA);
1599 
1600     abstract void endSection();
1601 }
1602 
1603 /** RTFReader.DocumentDestination is a concrete subclass of
1604  *  TextHandlingDestination which appends the text to the
1605  *  StyledDocument given by the <code>target</code> ivar of the
1606  *  containing RTFReader.
1607  */
1608 class DocumentDestination
1609     extends TextHandlingDestination
1610     implements Destination
1611 {
1612     public void deliverText(String text, AttributeSet characterAttributes)
1613     {
1614         try {
1615             target.insertString(target.getLength(),
1616                                 text,
1617                                 currentTextAttributes());
1618         } catch (BadLocationException ble) {
1619             /* This shouldn't be able to happen, of course */
1620             /* TODO is InternalError the correct error to throw? */
1621             throw new InternalError(ble.getMessage(), ble);
1622         }
1623     }
1624 
1625     public void finishParagraph(AttributeSet pgfAttributes,
1626                                 AttributeSet chrAttributes)
1627     {
1628         int pgfEndPosition = target.getLength();
1629         try {
1630             target.insertString(pgfEndPosition, "\n", chrAttributes);
1631             target.setParagraphAttributes(pgfEndPosition, 1, pgfAttributes, true);
1632         } catch (BadLocationException ble) {
1633             /* This shouldn't be able to happen, of course */
1634             /* TODO is InternalError the correct error to throw? */
1635             throw new InternalError(ble.getMessage(), ble);
1636         }
1637     }
1638 
1639     public void endSection()
1640     {
1641         /* If we implemented sections, we'd end 'em here */
1642     }
1643 }
1644 
1645 }