001package org.apache.maven.doxia.module.fo; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.IOException; 023import java.io.Writer; 024 025import java.util.Calendar; 026import java.util.Date; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Locale; 030import java.util.ResourceBundle; 031import java.util.Stack; 032 033import javax.swing.text.MutableAttributeSet; 034import javax.swing.text.html.HTML.Tag; 035 036import org.apache.maven.doxia.document.DocumentCover; 037import org.apache.maven.doxia.document.DocumentMeta; 038import org.apache.maven.doxia.document.DocumentModel; 039import org.apache.maven.doxia.document.DocumentTOC; 040import org.apache.maven.doxia.document.DocumentTOCItem; 041import org.apache.maven.doxia.sink.SinkEventAttributes; 042import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 043import org.apache.maven.doxia.util.DoxiaUtils; 044import org.apache.maven.doxia.util.HtmlTools; 045 046import org.codehaus.plexus.util.StringUtils; 047 048/** 049 * A Doxia Sink that produces an aggregated FO model. The usage is similar to the following: 050 * <p/> 051 * <pre> 052 * FoAggregateSink sink = new FoAggregateSink( writer ); 053 * sink.setDocumentModel( documentModel ); 054 * sink.beginDocument(); 055 * sink.coverPage(); 056 * sink.toc(); 057 * ... 058 * sink.endDocument(); 059 * </pre> 060 * <p/> 061 * <b>Note</b>: the documentModel object contains several 062 * <a href="http://maven.apache.org/doxia/doxia/doxia-core/document.html">document metadata</a>, but only a few 063 * of them are used in this sink (i.e. author, confidential, date and title), the others are ignored. 064 * 065 * @author ltheussl 066 * @version $Id$ 067 * @since 1.1 068 */ 069public class FoAggregateSink 070 extends FoSink 071{ 072 /** 073 * No Table Of Content. 074 * 075 * @see #setDocumentModel(DocumentModel, int) 076 */ 077 public static final int TOC_NONE = 0; 078 079 /** 080 * Table Of Content at the start of the document. 081 * 082 * @see #setDocumentModel(DocumentModel, int) 083 */ 084 public static final int TOC_START = 1; 085 086 /** 087 * Table Of Content at the end of the document. 088 * 089 * @see #setDocumentModel(DocumentModel, int) 090 */ 091 public static final int TOC_END = 2; 092 093 // TODO: make configurable 094 private static final String COVER_HEADER_HEIGHT = "1.5in"; 095 096 /** 097 * The document model to be used by this sink. 098 */ 099 private DocumentModel docModel; 100 101 /** 102 * Counts the current chapter level. 103 */ 104 private int chapter = 0; 105 106 /** 107 * Name of the source file of the current document, relative to the source root. 108 */ 109 private String docName; 110 111 /** 112 * Title of the chapter, used in the page header. 113 */ 114 private String docTitle = ""; 115 116 /** 117 * Content in head is ignored in aggregated documents. 118 */ 119 private boolean ignoreText; 120 121 /** 122 * Current position of the TOC, see {@link #TOC_POSITION} 123 */ 124 private int tocPosition; 125 126 /** 127 * Used to get the current position in the TOC. 128 */ 129 private final Stack<NumberedListItem> tocStack = new Stack<NumberedListItem>(); 130 131 /** 132 * Constructor. 133 * 134 * @param writer The writer for writing the result. 135 */ 136 public FoAggregateSink( Writer writer ) 137 { 138 super( writer ); 139 } 140 141 /** 142 * {@inheritDoc} 143 */ 144 public void head() 145 { 146 head( null ); 147 } 148 149 /** 150 * {@inheritDoc} 151 */ 152 public void head( SinkEventAttributes attributes ) 153 { 154 init(); 155 156 ignoreText = true; 157 } 158 159 /** 160 * {@inheritDoc} 161 */ 162 public void head_() 163 { 164 ignoreText = false; 165 writeEOL(); 166 } 167 168 /** 169 * {@inheritDoc} 170 */ 171 public void title() 172 { 173 title( null ); 174 } 175 176 /** 177 * {@inheritDoc} 178 */ 179 public void title( SinkEventAttributes attributes ) 180 { 181 // ignored 182 } 183 184 /** 185 * {@inheritDoc} 186 */ 187 public void title_() 188 { 189 // ignored 190 } 191 192 /** 193 * {@inheritDoc} 194 */ 195 public void author() 196 { 197 author( null ); 198 } 199 200 /** 201 * {@inheritDoc} 202 */ 203 public void author( SinkEventAttributes attributes ) 204 { 205 // ignored 206 } 207 208 /** 209 * {@inheritDoc} 210 */ 211 public void author_() 212 { 213 // ignored 214 } 215 216 /** 217 * {@inheritDoc} 218 */ 219 public void date() 220 { 221 date( null ); 222 } 223 224 /** 225 * {@inheritDoc} 226 */ 227 public void date( SinkEventAttributes attributes ) 228 { 229 // ignored 230 } 231 232 /** 233 * {@inheritDoc} 234 */ 235 public void date_() 236 { 237 // ignored 238 } 239 240 /** 241 * {@inheritDoc} 242 */ 243 public void body() 244 { 245 body( null ); 246 } 247 248 /** 249 * {@inheritDoc} 250 */ 251 public void body( SinkEventAttributes attributes ) 252 { 253 chapter++; 254 255 resetSectionCounter(); 256 257 startPageSequence( getHeaderText(), getFooterText() ); 258 259 if ( docName == null ) 260 { 261 getLog().warn( "No document root specified, local links will not be resolved correctly!" ); 262 } 263 else 264 { 265 writeStartTag( BLOCK_TAG, "" ); 266 } 267 268 } 269 270 /** 271 * {@inheritDoc} 272 */ 273 public void body_() 274 { 275 writeEOL(); 276 writeEndTag( BLOCK_TAG ); 277 writeEndTag( FLOW_TAG ); 278 writeEndTag( PAGE_SEQUENCE_TAG ); 279 280 // reset document name 281 docName = null; 282 } 283 284 /** 285 * Sets the title of the current document. This is used as a chapter title in the page header. 286 * 287 * @param title the title of the current document. 288 */ 289 public void setDocumentTitle( String title ) 290 { 291 this.docTitle = title; 292 293 if ( title == null ) 294 { 295 this.docTitle = ""; 296 } 297 } 298 299 /** 300 * Sets the name of the current source document, relative to the source root. 301 * Used to resolve links to other source documents. 302 * 303 * @param name the name for the current document. 304 */ 305 public void setDocumentName( String name ) 306 { 307 this.docName = getIdName( name ); 308 } 309 310 /** 311 * Sets the DocumentModel to be used by this sink. The DocumentModel provides all the meta-information 312 * required to render a document, eg settings for the cover page, table of contents, etc. 313 * <br/> 314 * By default, a TOC will be added at the beginning of the document. 315 * 316 * @param model the DocumentModel. 317 * @see #setDocumentModel(DocumentModel, String) 318 * @see #TOC_START 319 */ 320 public void setDocumentModel( DocumentModel model ) 321 { 322 setDocumentModel( model, TOC_START ); 323 } 324 325 /** 326 * Sets the DocumentModel to be used by this sink. The DocumentModel provides all the meta-information 327 * required to render a document, eg settings for the cover page, table of contents, etc. 328 * 329 * @param model the DocumentModel, could be null. 330 * @param tocPos should be one of these values: {@link #TOC_NONE}, {@link #TOC_START} and {@link #TOC_END}. 331 * @since 1.1.2 332 */ 333 public void setDocumentModel( DocumentModel model, int tocPos ) 334 { 335 this.docModel = model; 336 if ( !( tocPos == TOC_NONE || tocPos == TOC_START || tocPos == TOC_END ) ) 337 { 338 if ( getLog().isDebugEnabled() ) 339 { 340 getLog().debug( "Unrecognized value for tocPosition: " + tocPos + ", using no toc." ); 341 } 342 tocPos = TOC_NONE; 343 } 344 this.tocPosition = tocPos; 345 346 if ( this.docModel != null && this.docModel.getToc() != null && this.tocPosition != TOC_NONE ) 347 { 348 DocumentTOCItem tocItem = new DocumentTOCItem(); 349 tocItem.setName( this.docModel.getToc().getName() ); 350 tocItem.setRef( "./toc" ); 351 List<DocumentTOCItem> items = new LinkedList<DocumentTOCItem>(); 352 if ( this.tocPosition == TOC_START ) 353 { 354 items.add( tocItem ); 355 } 356 items.addAll( this.docModel.getToc().getItems() ); 357 if ( this.tocPosition == TOC_END ) 358 { 359 items.add( tocItem ); 360 } 361 362 this.docModel.getToc().setItems( items ); 363 } 364 } 365 366 /** 367 * Translates the given name to a usable id. 368 * Prepends "./" and strips any extension. 369 * 370 * @param name the name for the current document. 371 * @return String 372 */ 373 private String getIdName( String name ) 374 { 375 if ( StringUtils.isEmpty( name ) ) 376 { 377 getLog().warn( "Empty document reference, links will not be resolved correctly!" ); 378 return ""; 379 } 380 381 String idName = name.replace( '\\', '/' ); 382 383 // prepend "./" and strip extension 384 if ( !idName.startsWith( "./" ) ) 385 { 386 idName = "./" + idName; 387 } 388 389 if ( idName.substring( 2 ).lastIndexOf( "." ) != -1 ) 390 { 391 idName = idName.substring( 0, idName.lastIndexOf( "." ) ); 392 } 393 394 while ( idName.indexOf( "//" ) != -1 ) 395 { 396 idName = StringUtils.replace( idName, "//", "/" ); 397 } 398 399 return idName; 400 } 401 402 // ----------------------------------------------------------------------- 403 // 404 // ----------------------------------------------------------------------- 405 406 /** 407 * {@inheritDoc} 408 */ 409 public void figureGraphics( String name ) 410 { 411 figureGraphics( name, null ); 412 } 413 414 /** 415 * {@inheritDoc} 416 */ 417 public void figureGraphics( String src, SinkEventAttributes attributes ) 418 { 419 String anchor = src; 420 421 while ( anchor.startsWith( "./" ) ) 422 { 423 anchor = anchor.substring( 2 ); 424 } 425 426 if ( anchor.startsWith( "../" ) && docName != null ) 427 { 428 anchor = resolveLinkRelativeToBase( anchor ); 429 } 430 431 super.figureGraphics( anchor, attributes ); 432 } 433 434 /** 435 * {@inheritDoc} 436 */ 437 public void anchor( String name ) 438 { 439 anchor( name, null ); 440 } 441 442 /** 443 * {@inheritDoc} 444 */ 445 public void anchor( String name, SinkEventAttributes attributes ) 446 { 447 if ( name == null ) 448 { 449 throw new NullPointerException( "Anchor name cannot be null!" ); 450 } 451 452 String anchor = name; 453 454 if ( !DoxiaUtils.isValidId( anchor ) ) 455 { 456 anchor = DoxiaUtils.encodeId( name, true ); 457 458 String msg = "Modified invalid anchor name: '" + name + "' to '" + anchor + "'"; 459 logMessage( "modifiedLink", msg ); 460 } 461 462 anchor = "#" + anchor; 463 464 if ( docName != null ) 465 { 466 anchor = docName + anchor; 467 } 468 469 writeStartTag( INLINE_TAG, "id", anchor ); 470 } 471 472 /** 473 * {@inheritDoc} 474 */ 475 public void link( String name ) 476 { 477 link( name, null ); 478 } 479 480 /** 481 * {@inheritDoc} 482 */ 483 public void link( String name, SinkEventAttributes attributes ) 484 { 485 if ( name == null ) 486 { 487 throw new NullPointerException( "Link name cannot be null!" ); 488 } 489 490 if ( DoxiaUtils.isExternalLink( name ) ) 491 { 492 // external links 493 writeStartTag( BASIC_LINK_TAG, "external-destination", HtmlTools.escapeHTML( name ) ); 494 writeStartTag( INLINE_TAG, "href.external" ); 495 return; 496 } 497 498 while ( name.indexOf( "//" ) != -1 ) 499 { 500 name = StringUtils.replace( name, "//", "/" ); 501 } 502 503 if ( DoxiaUtils.isInternalLink( name ) ) 504 { 505 // internal link (ie anchor is in the same source document) 506 String anchor = name.substring( 1 ); 507 508 if ( !DoxiaUtils.isValidId( anchor ) ) 509 { 510 String tmp = anchor; 511 anchor = DoxiaUtils.encodeId( anchor, true ); 512 513 String msg = "Modified invalid anchor name: '" + tmp + "' to '" + anchor + "'"; 514 logMessage( "modifiedLink", msg ); 515 } 516 517 if ( docName != null ) 518 { 519 anchor = docName + "#" + anchor; 520 } 521 522 writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) ); 523 writeStartTag( INLINE_TAG, "href.internal" ); 524 } 525 else if ( name.startsWith( "../" ) ) 526 { 527 // local link (ie anchor is not in the same source document) 528 529 if ( docName == null ) 530 { 531 // can't resolve link without base, fop will issue a warning 532 writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( name ) ); 533 writeStartTag( INLINE_TAG, "href.internal" ); 534 535 return; 536 } 537 538 String anchor = resolveLinkRelativeToBase( chopExtension( name ) ); 539 540 writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) ); 541 writeStartTag( INLINE_TAG, "href.internal" ); 542 } 543 else 544 { 545 // local link (ie anchor is not in the same source document) 546 547 String anchor = name; 548 549 if ( anchor.startsWith( "./" ) ) 550 { 551 this.link( anchor.substring( 2 ) ); 552 return; 553 } 554 555 anchor = chopExtension( anchor ); 556 557 String base = docName.substring( 0, docName.lastIndexOf( "/" ) ); 558 anchor = base + "/" + anchor; 559 560 writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) ); 561 writeStartTag( INLINE_TAG, "href.internal" ); 562 } 563 } 564 565 // only call this if docName != null !!! 566 private String resolveLinkRelativeToBase( String name ) 567 { 568 String anchor = name; 569 570 String base = docName.substring( 0, docName.lastIndexOf( "/" ) ); 571 572 if ( base.indexOf( "/" ) != -1 ) 573 { 574 while ( anchor.startsWith( "../" ) ) 575 { 576 base = base.substring( 0, base.lastIndexOf( "/" ) ); 577 578 anchor = anchor.substring( 3 ); 579 580 if ( base.lastIndexOf( "/" ) == -1 ) 581 { 582 while ( anchor.startsWith( "../" ) ) 583 { 584 anchor = anchor.substring( 3 ); 585 } 586 break; 587 } 588 } 589 } 590 591 return base + "/" + anchor; 592 } 593 594 private String chopExtension( String name ) 595 { 596 String anchor = name; 597 598 int dot = anchor.lastIndexOf( "." ); 599 600 if ( dot != -1 && dot != anchor.length() && anchor.charAt( dot + 1 ) != '/' ) 601 { 602 int hash = anchor.indexOf( "#", dot ); 603 604 if ( hash != -1 ) 605 { 606 int dot2 = anchor.indexOf( ".", hash ); 607 608 if ( dot2 != -1 ) 609 { 610 anchor = 611 anchor.substring( 0, dot ) + "#" + HtmlTools.encodeId( anchor.substring( hash + 1, dot2 ) ); 612 } 613 else 614 { 615 anchor = anchor.substring( 0, dot ) + "#" + HtmlTools.encodeId( 616 anchor.substring( hash + 1, anchor.length() ) ); 617 } 618 } 619 else 620 { 621 anchor = anchor.substring( 0, dot ); 622 } 623 } 624 625 return anchor; 626 } 627 628 // ---------------------------------------------------------------------- 629 // 630 // ---------------------------------------------------------------------- 631 632 /** 633 * {@inheritDoc} 634 * <p/> 635 * Writes a start tag, prepending EOL. 636 */ 637 protected void writeStartTag( Tag tag, String attributeId ) 638 { 639 if ( !ignoreText ) 640 { 641 super.writeStartTag( tag, attributeId ); 642 } 643 } 644 645 /** 646 * {@inheritDoc} 647 * <p/> 648 * Writes a start tag, prepending EOL. 649 */ 650 protected void writeStartTag( Tag tag, String id, String name ) 651 { 652 if ( !ignoreText ) 653 { 654 super.writeStartTag( tag, id, name ); 655 } 656 } 657 658 /** 659 * {@inheritDoc} 660 * <p/> 661 * Writes an end tag, appending EOL. 662 */ 663 protected void writeEndTag( Tag t ) 664 { 665 if ( !ignoreText ) 666 { 667 super.writeEndTag( t ); 668 } 669 } 670 671 /** 672 * {@inheritDoc} 673 * <p/> 674 * Writes a simple tag, appending EOL. 675 */ 676 protected void writeEmptyTag( Tag tag, String attributeId ) 677 { 678 if ( !ignoreText ) 679 { 680 super.writeEmptyTag( tag, attributeId ); 681 } 682 } 683 684 /** 685 * {@inheritDoc} 686 * <p/> 687 * Writes a text, swallowing any exceptions. 688 */ 689 protected void write( String text ) 690 { 691 if ( !ignoreText ) 692 { 693 super.write( text ); 694 } 695 } 696 697 /** 698 * {@inheritDoc} 699 * <p/> 700 * Writes a text, appending EOL. 701 */ 702 protected void writeln( String text ) 703 { 704 if ( !ignoreText ) 705 { 706 super.writeln( text ); 707 } 708 } 709 710 /** 711 * {@inheritDoc} 712 * <p/> 713 * Writes content, escaping special characters. 714 */ 715 protected void content( String text ) 716 { 717 if ( !ignoreText ) 718 { 719 super.content( text ); 720 } 721 } 722 723 /** 724 * Writes EOL. 725 */ 726 protected void newline() 727 { 728 if ( !ignoreText ) 729 { 730 writeEOL(); 731 } 732 } 733 734 /** 735 * Starts a page sequence, depending on the current chapter. 736 * 737 * @param headerText The text to write in the header, if null, nothing is written. 738 * @param footerText The text to write in the footer, if null, nothing is written. 739 */ 740 protected void startPageSequence( String headerText, String footerText ) 741 { 742 if ( chapter == 1 ) 743 { 744 startPageSequence( "0", headerText, footerText ); 745 } 746 else 747 { 748 startPageSequence( "auto", headerText, footerText ); 749 } 750 } 751 752 /** 753 * Returns the text to write in the header of each page. 754 * 755 * @return String 756 */ 757 protected String getHeaderText() 758 { 759 return Integer.toString( chapter ) + " " + docTitle; 760 } 761 762 /** 763 * Returns the text to write in the footer of each page. 764 * 765 * @return String 766 */ 767 protected String getFooterText() 768 { 769 int actualYear; 770 String add = " • " + getBundle( Locale.US ).getString( "footer.rights" ); 771 String companyName = ""; 772 773 if ( docModel != null && docModel.getMeta() != null && docModel.getMeta().isConfidential() ) 774 { 775 add = add + " • " + getBundle( Locale.US ).getString( "footer.confidential" ); 776 } 777 778 if ( docModel != null && docModel.getCover() != null && docModel.getCover().getCompanyName() != null ) 779 { 780 companyName = docModel.getCover().getCompanyName(); 781 } 782 783 if ( docModel != null && docModel.getMeta() != null && docModel.getMeta().getDate() != null ) 784 { 785 Calendar date = Calendar.getInstance(); 786 date.setTime( docModel.getMeta().getDate() ); 787 actualYear = date.get( Calendar.YEAR ); 788 } 789 else 790 { 791 actualYear = Calendar.getInstance().get( Calendar.YEAR ); 792 } 793 794 return "©" + actualYear + ", " + escaped( companyName, false ) + add; 795 } 796 797 /** 798 * {@inheritDoc} 799 * <p/> 800 * Returns the current chapter number as a string. 801 */ 802 protected String getChapterString() 803 { 804 return Integer.toString( chapter ) + "."; 805 } 806 807 /** 808 * {@inheritDoc} 809 * <p/> 810 * Writes a 'xsl-region-before' block. 811 */ 812 protected void regionBefore( String headerText ) 813 { 814 writeStartTag( STATIC_CONTENT_TAG, "flow-name", "xsl-region-before" ); 815 writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" ); 816 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "5.625in" ); 817 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.625in" ); 818 writeStartTag( TABLE_BODY_TAG, "" ); 819 writeStartTag( TABLE_ROW_TAG, "" ); 820 writeStartTag( TABLE_CELL_TAG, "" ); 821 writeStartTag( BLOCK_TAG, "header.style" ); 822 823 if ( headerText != null ) 824 { 825 write( headerText ); 826 } 827 828 writeEndTag( BLOCK_TAG ); 829 writeEndTag( TABLE_CELL_TAG ); 830 writeStartTag( TABLE_CELL_TAG, "" ); 831 writeStartTag( BLOCK_TAG, "page.number" ); 832 writeEmptyTag( PAGE_NUMBER_TAG, "" ); 833 writeEndTag( BLOCK_TAG ); 834 writeEndTag( TABLE_CELL_TAG ); 835 writeEndTag( TABLE_ROW_TAG ); 836 writeEndTag( TABLE_BODY_TAG ); 837 writeEndTag( TABLE_TAG ); 838 writeEndTag( STATIC_CONTENT_TAG ); 839 } 840 841 /** 842 * {@inheritDoc} 843 * <p/> 844 * Writes a 'xsl-region-after' block. 845 */ 846 protected void regionAfter( String footerText ) 847 { 848 writeStartTag( STATIC_CONTENT_TAG, "flow-name", "xsl-region-after" ); 849 writeStartTag( BLOCK_TAG, "footer.style" ); 850 851 if ( footerText != null ) 852 { 853 write( footerText ); 854 } 855 856 writeEndTag( BLOCK_TAG ); 857 writeEndTag( STATIC_CONTENT_TAG ); 858 } 859 860 /** 861 * {@inheritDoc} 862 * <p/> 863 * Writes a chapter heading. 864 */ 865 protected void chapterHeading( String headerText, boolean chapterNumber ) 866 { 867 if ( docName == null ) 868 { 869 getLog().warn( "No document root specified, local links will not be resolved correctly!" ); 870 writeStartTag( BLOCK_TAG, "" ); 871 } 872 else 873 { 874 writeStartTag( BLOCK_TAG, "id", docName ); 875 } 876 877 writeStartTag( LIST_BLOCK_TAG, "" ); 878 writeStartTag( LIST_ITEM_TAG, "" ); 879 writeln( "<fo:list-item-label end-indent=\"6.375in\" start-indent=\"-1in\">" ); 880 writeStartTag( BLOCK_TAG, "outdented.number.style" ); 881 882 if ( chapterNumber ) 883 { 884 writeStartTag( BLOCK_TAG, "chapter.title" ); 885 write( Integer.toString( chapter ) ); 886 writeEndTag( BLOCK_TAG ); 887 } 888 889 writeEndTag( BLOCK_TAG ); 890 writeEndTag( LIST_ITEM_LABEL_TAG ); 891 writeln( "<fo:list-item-body end-indent=\"1in\" start-indent=\"0in\">" ); 892 writeStartTag( BLOCK_TAG, "chapter.title" ); 893 894 if ( headerText == null ) 895 { 896 text( docTitle ); 897 } 898 else 899 { 900 text( headerText ); 901 } 902 903 writeEndTag( BLOCK_TAG ); 904 writeEndTag( LIST_ITEM_BODY_TAG ); 905 writeEndTag( LIST_ITEM_TAG ); 906 writeEndTag( LIST_BLOCK_TAG ); 907 writeEndTag( BLOCK_TAG ); 908 writeStartTag( BLOCK_TAG, "space-after.optimum", "0em" ); 909 writeEmptyTag( LEADER_TAG, "chapter.rule" ); 910 writeEndTag( BLOCK_TAG ); 911 } 912 913 /** 914 * Writes a table of contents. The DocumentModel has to contain a DocumentTOC for this to work. 915 */ 916 public void toc() 917 { 918 if ( docModel == null || docModel.getToc() == null || docModel.getToc().getItems() == null 919 || this.tocPosition == TOC_NONE ) 920 { 921 return; 922 } 923 924 DocumentTOC toc = docModel.getToc(); 925 926 writeln( "<fo:page-sequence master-reference=\"toc\" initial-page-number=\"1\" format=\"i\">" ); 927 regionBefore( toc.getName() ); 928 regionAfter( getFooterText() ); 929 writeStartTag( FLOW_TAG, "flow-name", "xsl-region-body" ); 930 writeStartTag( BLOCK_TAG, "id", "./toc" ); 931 chapterHeading( toc.getName(), false ); 932 writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" ); 933 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.45in" ); 934 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.4in" ); 935 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.4in" ); 936 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "5in" ); // TODO {$maxBodyWidth - 1.25}in 937 writeStartTag( TABLE_BODY_TAG ); 938 939 writeTocItems( toc.getItems(), 1 ); 940 941 writeEndTag( TABLE_BODY_TAG ); 942 writeEndTag( TABLE_TAG ); 943 writeEndTag( BLOCK_TAG ); 944 writeEndTag( FLOW_TAG ); 945 writeEndTag( PAGE_SEQUENCE_TAG ); 946 } 947 948 private void writeTocItems( List<DocumentTOCItem> tocItems, int level ) 949 { 950 final int maxTocLevel = 4; 951 952 if ( level < 1 || level > maxTocLevel ) 953 { 954 return; 955 } 956 957 tocStack.push( new NumberedListItem( NUMBERING_DECIMAL ) ); 958 959 boolean printToc = ( level == 1 ); 960 961 for ( DocumentTOCItem tocItem : tocItems ) 962 { 963 String ref = getIdName( tocItem.getRef() ); 964 965 writeStartTag( TABLE_ROW_TAG, "keep-with-next", "auto" ); 966 967 if ( level > 2 ) 968 { 969 for ( int i = 0; i < level - 2; i++ ) 970 { 971 writeStartTag( TABLE_CELL_TAG ); 972 writeSimpleTag( BLOCK_TAG ); 973 writeEndTag( TABLE_CELL_TAG ); 974 } 975 } 976 977 writeStartTag( TABLE_CELL_TAG, "toc.cell" ); 978 writeStartTag( BLOCK_TAG, "toc.number.style" ); 979 980 NumberedListItem current = tocStack.peek(); 981 if ( printToc ) 982 { 983 // MPDF-59: no entry numbering for first, since it's the "Table of Contents" 984 printToc = false; 985 } 986 else 987 { 988 current.next(); 989 write( currentTocNumber() ); 990 } 991 992 writeEndTag( BLOCK_TAG ); 993 writeEndTag( TABLE_CELL_TAG ); 994 995 String span = "3"; 996 997 if ( level > 2 ) 998 { 999 span = Integer.toString( 5 - level ); 1000 } 1001 1002 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", span, "toc.cell" ); 1003 MutableAttributeSet atts = getFoConfiguration().getAttributeSet( "toc.h" + level + ".style" ); 1004 atts.addAttribute( "text-align-last", "justify" ); 1005 writeStartTag( BLOCK_TAG, atts ); 1006 writeStartTag( BASIC_LINK_TAG, "internal-destination", ref ); 1007 text( tocItem.getName() ); 1008 writeEndTag( BASIC_LINK_TAG ); 1009 writeEmptyTag( LEADER_TAG, "toc.leader.style" ); 1010 writeStartTag( INLINE_TAG, "page.number" ); 1011 writeEmptyTag( PAGE_NUMBER_CITATION_TAG, "ref-id", ref ); 1012 writeEndTag( INLINE_TAG ); 1013 writeEndTag( BLOCK_TAG ); 1014 writeEndTag( TABLE_CELL_TAG ); 1015 writeEndTag( TABLE_ROW_TAG ); 1016 1017 if ( tocItem.getItems() != null ) 1018 { 1019 writeTocItems( tocItem.getItems(), level + 1 ); 1020 } 1021 } 1022 1023 tocStack.pop(); 1024 } 1025 1026 private String currentTocNumber() 1027 { 1028 StringBuilder ch = new StringBuilder( tocStack.get( 0 ).getListItemSymbol() ); 1029 1030 for ( int i = 1; i < tocStack.size(); i++ ) 1031 { 1032 ch.append( tocStack.get( i ).getListItemSymbol() ); 1033 } 1034 1035 return ch.toString(); 1036 } 1037 1038 /** 1039 * {@inheritDoc} 1040 * <p/> 1041 * Writes a fo:bookmark-tree. The DocumentModel has to contain a DocumentTOC for this to work. 1042 */ 1043 protected void pdfBookmarks() 1044 { 1045 if ( docModel == null || docModel.getToc() == null ) 1046 { 1047 return; 1048 } 1049 1050 writeStartTag( BOOKMARK_TREE_TAG ); 1051 1052 renderBookmarkItems( docModel.getToc().getItems() ); 1053 1054 writeEndTag( BOOKMARK_TREE_TAG ); 1055 } 1056 1057 private void renderBookmarkItems( List<DocumentTOCItem> items ) 1058 { 1059 for ( DocumentTOCItem tocItem : items ) 1060 { 1061 String ref = getIdName( tocItem.getRef() ); 1062 1063 writeStartTag( BOOKMARK_TAG, "internal-destination", ref ); 1064 writeStartTag( BOOKMARK_TITLE_TAG ); 1065 text( tocItem.getName() ); 1066 writeEndTag( BOOKMARK_TITLE_TAG ); 1067 1068 if ( tocItem.getItems() != null ) 1069 { 1070 renderBookmarkItems( tocItem.getItems() ); 1071 } 1072 1073 writeEndTag( BOOKMARK_TAG ); 1074 } 1075 } 1076 1077 /** 1078 * Writes a cover page. The DocumentModel has to contain a DocumentMeta for this to work. 1079 */ 1080 public void coverPage() 1081 { 1082 if ( this.docModel == null ) 1083 { 1084 return; 1085 } 1086 1087 DocumentCover cover = docModel.getCover(); 1088 DocumentMeta meta = docModel.getMeta(); 1089 1090 if ( cover == null && meta == null ) 1091 { 1092 return; // no information for cover page: ignore 1093 } 1094 1095 // TODO: remove hard-coded settings 1096 1097 writeStartTag( PAGE_SEQUENCE_TAG, "master-reference", "cover-page" ); 1098 writeStartTag( FLOW_TAG, "flow-name", "xsl-region-body" ); 1099 writeStartTag( BLOCK_TAG, "text-align", "center" ); 1100 writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" ); 1101 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "3.125in" ); 1102 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "3.125in" ); 1103 writeStartTag( TABLE_BODY_TAG ); 1104 1105 writeCoverHead( cover ); 1106 writeCoverBody( cover, meta ); 1107 writeCoverFooter( cover, meta ); 1108 1109 writeEndTag( TABLE_BODY_TAG ); 1110 writeEndTag( TABLE_TAG ); 1111 writeEndTag( BLOCK_TAG ); 1112 writeEndTag( FLOW_TAG ); 1113 writeEndTag( PAGE_SEQUENCE_TAG ); 1114 } 1115 1116 private void writeCoverHead( DocumentCover cover ) 1117 { 1118 if ( cover == null ) 1119 { 1120 return; 1121 } 1122 1123 String compLogo = cover.getCompanyLogo(); 1124 String projLogo = cover.getProjectLogo(); 1125 1126 writeStartTag( TABLE_ROW_TAG, "height", COVER_HEADER_HEIGHT ); 1127 writeStartTag( TABLE_CELL_TAG ); 1128 1129 if ( StringUtils.isNotEmpty( compLogo ) ) 1130 { 1131 SinkEventAttributeSet atts = new SinkEventAttributeSet(); 1132 atts.addAttribute( "text-align", "left" ); 1133 atts.addAttribute( "vertical-align", "top" ); 1134 writeStartTag( BLOCK_TAG, atts ); 1135 figureGraphics( compLogo, getGraphicsAttributes( compLogo ) ); 1136 writeEndTag( BLOCK_TAG ); 1137 } 1138 1139 writeSimpleTag( BLOCK_TAG ); 1140 writeEndTag( TABLE_CELL_TAG ); 1141 writeStartTag( TABLE_CELL_TAG ); 1142 1143 if ( StringUtils.isNotEmpty( projLogo ) ) 1144 { 1145 SinkEventAttributeSet atts = new SinkEventAttributeSet(); 1146 atts.addAttribute( "text-align", "right" ); 1147 atts.addAttribute( "vertical-align", "top" ); 1148 writeStartTag( BLOCK_TAG, atts ); 1149 figureGraphics( projLogo, getGraphicsAttributes( projLogo ) ); 1150 writeEndTag( BLOCK_TAG ); 1151 } 1152 1153 writeSimpleTag( BLOCK_TAG ); 1154 writeEndTag( TABLE_CELL_TAG ); 1155 writeEndTag( TABLE_ROW_TAG ); 1156 } 1157 1158 private void writeCoverBody( DocumentCover cover, DocumentMeta meta ) 1159 { 1160 if ( cover == null && meta == null ) 1161 { 1162 return; 1163 } 1164 1165 String subtitle = null; 1166 String title = null; 1167 String type = null; 1168 String version = null; 1169 if ( cover == null ) 1170 { 1171 // aleady checked that meta != null 1172 getLog().debug( "The DocumentCover is not defined, using the DocumentMeta title as cover title." ); 1173 title = meta.getTitle(); 1174 } 1175 else 1176 { 1177 subtitle = cover.getCoverSubTitle(); 1178 title = cover.getCoverTitle(); 1179 type = cover.getCoverType(); 1180 version = cover.getCoverVersion(); 1181 } 1182 1183 writeln( "<fo:table-row keep-with-previous=\"always\" height=\"0.014in\">" ); 1184 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" ); 1185 writeStartTag( BLOCK_TAG, "line-height", "0.014in" ); 1186 writeEmptyTag( LEADER_TAG, "chapter.rule" ); 1187 writeEndTag( BLOCK_TAG ); 1188 writeEndTag( TABLE_CELL_TAG ); 1189 writeEndTag( TABLE_ROW_TAG ); 1190 1191 writeStartTag( TABLE_ROW_TAG, "height", "7.447in" ); 1192 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" ); 1193 writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" ); 1194 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "2.083in" ); 1195 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "2.083in" ); 1196 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "2.083in" ); 1197 1198 writeStartTag( TABLE_BODY_TAG ); 1199 1200 writeStartTag( TABLE_ROW_TAG ); 1201 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "3" ); 1202 writeSimpleTag( BLOCK_TAG ); 1203 writeEmptyTag( BLOCK_TAG, "space-before", "3.2235in" ); 1204 writeEndTag( TABLE_CELL_TAG ); 1205 writeEndTag( TABLE_ROW_TAG ); 1206 1207 writeStartTag( TABLE_ROW_TAG ); 1208 writeStartTag( TABLE_CELL_TAG ); 1209 writeEmptyTag( BLOCK_TAG, "space-after", "0.5in" ); 1210 writeEndTag( TABLE_CELL_TAG ); 1211 1212 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2", "cover.border.left" ); 1213 writeStartTag( BLOCK_TAG, "cover.title" ); 1214 text( title == null ? "" : title ); 1215 writeEndTag( BLOCK_TAG ); 1216 writeEndTag( TABLE_CELL_TAG ); 1217 writeEndTag( TABLE_ROW_TAG ); 1218 1219 writeStartTag( TABLE_ROW_TAG ); 1220 writeStartTag( TABLE_CELL_TAG ); 1221 writeSimpleTag( BLOCK_TAG ); 1222 writeEndTag( TABLE_CELL_TAG ); 1223 1224 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2", "cover.border.left.bottom" ); 1225 writeStartTag( BLOCK_TAG, "cover.subtitle" ); 1226 text( subtitle == null ? ( version == null ? "" : " v. " + version ) : subtitle ); 1227 writeEndTag( BLOCK_TAG ); 1228 writeStartTag( BLOCK_TAG, "cover.subtitle" ); 1229 text( type == null ? "" : type ); 1230 writeEndTag( BLOCK_TAG ); 1231 writeEndTag( TABLE_CELL_TAG ); 1232 writeEndTag( TABLE_ROW_TAG ); 1233 1234 writeEndTag( TABLE_BODY_TAG ); 1235 writeEndTag( TABLE_TAG ); 1236 1237 writeEndTag( TABLE_CELL_TAG ); 1238 writeEndTag( TABLE_ROW_TAG ); 1239 1240 writeStartTag( TABLE_ROW_TAG, "height", "0.014in" ); 1241 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" ); 1242 writeln( "<fo:block space-after=\"0.2in\" line-height=\"0.014in\">" ); 1243 writeEmptyTag( LEADER_TAG, "chapter.rule" ); 1244 writeEndTag( BLOCK_TAG ); 1245 writeEndTag( TABLE_CELL_TAG ); 1246 writeEndTag( TABLE_ROW_TAG ); 1247 1248 writeStartTag( TABLE_ROW_TAG ); 1249 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" ); 1250 writeSimpleTag( BLOCK_TAG ); 1251 writeEmptyTag( BLOCK_TAG, "space-before", "0.2in" ); 1252 writeEndTag( TABLE_CELL_TAG ); 1253 writeEndTag( TABLE_ROW_TAG ); 1254 } 1255 1256 private void writeCoverFooter( DocumentCover cover, DocumentMeta meta ) 1257 { 1258 if ( cover == null && meta == null ) 1259 { 1260 return; 1261 } 1262 1263 String date = null; 1264 String compName = null; 1265 if ( cover == null ) 1266 { 1267 // aleady checked that meta != null 1268 getLog().debug( "The DocumentCover is not defined, using the DocumentMeta author as company name." ); 1269 compName = meta.getAuthor(); 1270 } 1271 else 1272 { 1273 compName = cover.getCompanyName(); 1274 1275 if ( cover.getCoverdate() == null ) 1276 { 1277 cover.setCoverDate( new Date() ); 1278 date = cover.getCoverdate(); 1279 cover.setCoverDate( null ); 1280 } 1281 else 1282 { 1283 date = cover.getCoverdate(); 1284 } 1285 } 1286 1287 writeStartTag( TABLE_ROW_TAG, "height", "0.3in" ); 1288 1289 writeStartTag( TABLE_CELL_TAG ); 1290 MutableAttributeSet att = getFoConfiguration().getAttributeSet( "cover.subtitle" ); 1291 att.addAttribute( "height", "0.3in" ); 1292 att.addAttribute( "text-align", "left" ); 1293 writeStartTag( BLOCK_TAG, att ); 1294 text( compName == null ? ( cover.getAuthor() == null ? "" : cover.getAuthor() ) : compName ); 1295 writeEndTag( BLOCK_TAG ); 1296 writeEndTag( TABLE_CELL_TAG ); 1297 1298 writeStartTag( TABLE_CELL_TAG ); 1299 att = getFoConfiguration().getAttributeSet( "cover.subtitle" ); 1300 att.addAttribute( "height", "0.3in" ); 1301 att.addAttribute( "text-align", "right" ); 1302 writeStartTag( BLOCK_TAG, att ); 1303 text( date == null ? "" : date ); 1304 writeEndTag( BLOCK_TAG ); 1305 writeEndTag( TABLE_CELL_TAG ); 1306 1307 writeEndTag( TABLE_ROW_TAG ); 1308 } 1309 1310 private ResourceBundle getBundle( Locale locale ) 1311 { 1312 return ResourceBundle.getBundle( "doxia-fo", locale, this.getClass().getClassLoader() ); 1313 } 1314 1315 private SinkEventAttributeSet getGraphicsAttributes( String logo ) 1316 { 1317 MutableAttributeSet atts = null; 1318 1319 try 1320 { 1321 atts = DoxiaUtils.getImageAttributes( logo ); 1322 } 1323 catch ( IOException e ) 1324 { 1325 getLog().debug( e ); 1326 } 1327 1328 if ( atts == null ) 1329 { 1330 return new SinkEventAttributeSet( new String[]{ SinkEventAttributes.HEIGHT, COVER_HEADER_HEIGHT } ); 1331 } 1332 1333 // FOP dpi: 72 1334 // Max width : 3.125 inch, table cell size, see #coverPage() 1335 final int maxWidth = 225; // 3.125 * 72 1336 1337 if ( Integer.parseInt( atts.getAttribute( SinkEventAttributes.WIDTH ).toString() ) > maxWidth ) 1338 { 1339 atts.addAttribute( "content-width", "3.125in" ); 1340 } 1341 1342 return new SinkEventAttributeSet( atts ); 1343 } 1344}