View Javadoc
1   package org.apache.maven.doxia.module.fo;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.IOException;
23  import java.io.Writer;
24  
25  import java.util.Calendar;
26  import java.util.Date;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.ResourceBundle;
31  import java.util.Stack;
32  
33  import javax.swing.text.MutableAttributeSet;
34  import javax.swing.text.html.HTML.Tag;
35  
36  import org.apache.maven.doxia.document.DocumentCover;
37  import org.apache.maven.doxia.document.DocumentMeta;
38  import org.apache.maven.doxia.document.DocumentModel;
39  import org.apache.maven.doxia.document.DocumentTOC;
40  import org.apache.maven.doxia.document.DocumentTOCItem;
41  import org.apache.maven.doxia.sink.SinkEventAttributes;
42  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
43  import org.apache.maven.doxia.util.DoxiaUtils;
44  import org.apache.maven.doxia.util.HtmlTools;
45  
46  import org.codehaus.plexus.util.StringUtils;
47  
48  /**
49   * A Doxia Sink that produces an aggregated FO model. The usage is similar to the following:
50   * <p/>
51   * <pre>
52   * FoAggregateSink sink = new FoAggregateSink( writer );
53   * sink.setDocumentModel( documentModel );
54   * sink.beginDocument();
55   * sink.coverPage();
56   * sink.toc();
57   * ...
58   * sink.endDocument();
59   * </pre>
60   * <p/>
61   * <b>Note</b>: the documentModel object contains several
62   * <a href="http://maven.apache.org/doxia/doxia/doxia-core/document.html">document metadata</a>, but only a few
63   * of them are used in this sink (i.e. author, confidential, date and title), the others are ignored.
64   *
65   * @author ltheussl
66   * @version $Id$
67   * @since 1.1
68   */
69  public class FoAggregateSink
70      extends FoSink
71  {
72      /**
73       * No Table Of Content.
74       *
75       * @see #setDocumentModel(DocumentModel, int)
76       */
77      public static final int TOC_NONE = 0;
78  
79      /**
80       * Table Of Content at the start of the document.
81       *
82       * @see #setDocumentModel(DocumentModel, int)
83       */
84      public static final int TOC_START = 1;
85  
86      /**
87       * Table Of Content at the end of the document.
88       *
89       * @see #setDocumentModel(DocumentModel, int)
90       */
91      public static final int TOC_END = 2;
92  
93      // TODO: make configurable
94      private static final String COVER_HEADER_HEIGHT = "1.5in";
95  
96      /**
97       * The document model to be used by this sink.
98       */
99      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 = " &#8226; " + getBundle( Locale.US ).getString( "footer.rights" );
771         String companyName = "";
772 
773         if ( docModel != null && docModel.getMeta() != null && docModel.getMeta().isConfidential() )
774         {
775             add = add + " &#8226; " + 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 "&#169;" + 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 }