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