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