1 /* 2 * Copyright (c) 1997, 2018, 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 }