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: FoAggregateSink.java 1726411 2016-01-23 16:34:09Z hboutemy $
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, "id", docName );
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         writeStartTag( BLOCK_TAG, "" );
868         writeStartTag( LIST_BLOCK_TAG, "" );
869         writeStartTag( LIST_ITEM_TAG, "" );
870         writeln( "<fo:list-item-label end-indent=\"6.375in\" start-indent=\"-1in\">" );
871         writeStartTag( BLOCK_TAG, "outdented.number.style" );
872 
873         if ( chapterNumber )
874         {
875             writeStartTag( BLOCK_TAG, "chapter.title" );
876             write( Integer.toString( chapter ) );
877             writeEndTag( BLOCK_TAG );
878         }
879 
880         writeEndTag( BLOCK_TAG );
881         writeEndTag( LIST_ITEM_LABEL_TAG );
882         writeln( "<fo:list-item-body end-indent=\"1in\" start-indent=\"0in\">" );
883         writeStartTag( BLOCK_TAG, "chapter.title" );
884 
885         if ( headerText == null )
886         {
887             text( docTitle );
888         }
889         else
890         {
891             text( headerText );
892         }
893 
894         writeEndTag( BLOCK_TAG );
895         writeEndTag( LIST_ITEM_BODY_TAG );
896         writeEndTag( LIST_ITEM_TAG );
897         writeEndTag( LIST_BLOCK_TAG );
898         writeEndTag( BLOCK_TAG );
899         writeStartTag( BLOCK_TAG, "space-after.optimum", "0em" );
900         writeEmptyTag( LEADER_TAG, "chapter.rule" );
901         writeEndTag( BLOCK_TAG );
902     }
903 
904     /**
905      * Writes a table of contents. The DocumentModel has to contain a DocumentTOC for this to work.
906      */
907     public void toc()
908     {
909         if ( docModel == null || docModel.getToc() == null || docModel.getToc().getItems() == null
910             || this.tocPosition == TOC_NONE )
911         {
912             return;
913         }
914 
915         DocumentTOC toc = docModel.getToc();
916 
917         writeln( "<fo:page-sequence master-reference=\"toc\" initial-page-number=\"1\" format=\"i\">" );
918         regionBefore( toc.getName() );
919         regionAfter( getFooterText() );
920         writeStartTag( FLOW_TAG, "flow-name", "xsl-region-body" );
921         writeStartTag( BLOCK_TAG, "id", "./toc" );
922         chapterHeading( toc.getName(), false );
923         writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" );
924         writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.45in" );
925         writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.4in" );
926         writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.4in" );
927         writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "5in" ); // TODO {$maxBodyWidth - 1.25}in
928         writeStartTag( TABLE_BODY_TAG );
929 
930         writeTocItems( toc.getItems(), 1 );
931 
932         writeEndTag( TABLE_BODY_TAG );
933         writeEndTag( TABLE_TAG );
934         writeEndTag( BLOCK_TAG );
935         writeEndTag( FLOW_TAG );
936         writeEndTag( PAGE_SEQUENCE_TAG );
937     }
938 
939     private void writeTocItems( List<DocumentTOCItem> tocItems, int level )
940     {
941         final int maxTocLevel = 4;
942 
943         if ( level < 1 || level > maxTocLevel )
944         {
945             return;
946         }
947 
948         tocStack.push( new NumberedListItem( NUMBERING_DECIMAL ) );
949 
950         for ( DocumentTOCItem tocItem : tocItems )
951         {
952             String ref = getIdName( tocItem.getRef() );
953 
954             writeStartTag( TABLE_ROW_TAG, "keep-with-next", "auto" );
955 
956             if ( level > 2 )
957             {
958                 for ( int i = 0; i < level - 2; i++ )
959                 {
960                     writeStartTag( TABLE_CELL_TAG );
961                     writeSimpleTag( BLOCK_TAG );
962                     writeEndTag( TABLE_CELL_TAG );
963                 }
964             }
965 
966             writeStartTag( TABLE_CELL_TAG, "toc.cell" );
967             writeStartTag( BLOCK_TAG, "toc.number.style" );
968 
969             NumberedListItem current = tocStack.peek();
970             current.next();
971             write( currentTocNumber() );
972 
973             writeEndTag( BLOCK_TAG );
974             writeEndTag( TABLE_CELL_TAG );
975 
976             String span = "3";
977 
978             if ( level > 2 )
979             {
980                 span = Integer.toString( 5 - level );
981             }
982 
983             writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", span, "toc.cell" );
984             MutableAttributeSet atts = getFoConfiguration().getAttributeSet( "toc.h" + level + ".style" );
985             atts.addAttribute( "text-align-last", "justify" );
986             writeStartTag( BLOCK_TAG, atts );
987             writeStartTag( BASIC_LINK_TAG, "internal-destination", ref );
988             text( tocItem.getName() );
989             writeEndTag( BASIC_LINK_TAG );
990             writeEmptyTag( LEADER_TAG, "toc.leader.style" );
991             writeStartTag( INLINE_TAG, "page.number" );
992             writeEmptyTag( PAGE_NUMBER_CITATION_TAG, "ref-id", ref );
993             writeEndTag( INLINE_TAG );
994             writeEndTag( BLOCK_TAG );
995             writeEndTag( TABLE_CELL_TAG );
996             writeEndTag( TABLE_ROW_TAG );
997 
998             if ( tocItem.getItems() != null )
999             {
1000                 writeTocItems( tocItem.getItems(), level + 1 );
1001             }
1002         }
1003 
1004         tocStack.pop();
1005     }
1006 
1007     private String currentTocNumber()
1008     {
1009         StringBuilder ch = new StringBuilder( tocStack.get( 0 ).getListItemSymbol() );
1010 
1011         for ( int i = 1; i < tocStack.size(); i++ )
1012         {
1013             ch.append( "." + tocStack.get( i ).getListItemSymbol() );
1014         }
1015 
1016         return ch.toString();
1017     }
1018 
1019     /**
1020      * {@inheritDoc}
1021      * <p/>
1022      * Writes a fo:bookmark-tree. The DocumentModel has to contain a DocumentTOC for this to work.
1023      */
1024     protected void pdfBookmarks()
1025     {
1026         if ( docModel == null || docModel.getToc() == null )
1027         {
1028             return;
1029         }
1030 
1031         writeStartTag( BOOKMARK_TREE_TAG );
1032 
1033         renderBookmarkItems( docModel.getToc().getItems() );
1034 
1035         writeEndTag( BOOKMARK_TREE_TAG );
1036     }
1037 
1038     private void renderBookmarkItems( List<DocumentTOCItem> items )
1039     {
1040         for ( DocumentTOCItem tocItem : items )
1041         {
1042             String ref = getIdName( tocItem.getRef() );
1043 
1044             writeStartTag( BOOKMARK_TAG, "internal-destination", ref );
1045             writeStartTag( BOOKMARK_TITLE_TAG );
1046             text( tocItem.getName() );
1047             writeEndTag( BOOKMARK_TITLE_TAG );
1048 
1049             if ( tocItem.getItems() != null )
1050             {
1051                 renderBookmarkItems( tocItem.getItems() );
1052             }
1053 
1054             writeEndTag( BOOKMARK_TAG );
1055         }
1056     }
1057 
1058     /**
1059      * Writes a cover page. The DocumentModel has to contain a DocumentMeta for this to work.
1060      */
1061     public void coverPage()
1062     {
1063         if ( this.docModel == null )
1064         {
1065             return;
1066         }
1067 
1068         DocumentCover cover = docModel.getCover();
1069         DocumentMeta meta = docModel.getMeta();
1070 
1071         if ( cover == null && meta == null )
1072         {
1073             return; // no information for cover page: ignore
1074         }
1075 
1076         // TODO: remove hard-coded settings
1077 
1078         writeStartTag( PAGE_SEQUENCE_TAG, "master-reference", "cover-page" );
1079         writeStartTag( FLOW_TAG, "flow-name", "xsl-region-body" );
1080         writeStartTag( BLOCK_TAG, "text-align", "center" );
1081         writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" );
1082         writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "3.125in" );
1083         writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "3.125in" );
1084         writeStartTag( TABLE_BODY_TAG );
1085 
1086         writeCoverHead( cover );
1087         writeCoverBody( cover, meta );
1088         writeCoverFooter( cover, meta );
1089 
1090         writeEndTag( TABLE_BODY_TAG );
1091         writeEndTag( TABLE_TAG );
1092         writeEndTag( BLOCK_TAG );
1093         writeEndTag( FLOW_TAG );
1094         writeEndTag( PAGE_SEQUENCE_TAG );
1095     }
1096 
1097     private void writeCoverHead( DocumentCover cover )
1098     {
1099         if ( cover == null )
1100         {
1101             return;
1102         }
1103 
1104         String compLogo = cover.getCompanyLogo();
1105         String projLogo = cover.getProjectLogo();
1106 
1107         writeStartTag( TABLE_ROW_TAG, "height", COVER_HEADER_HEIGHT );
1108         writeStartTag( TABLE_CELL_TAG );
1109 
1110         if ( StringUtils.isNotEmpty( compLogo ) )
1111         {
1112             SinkEventAttributeSet atts = new SinkEventAttributeSet();
1113             atts.addAttribute( "text-align", "left" );
1114             atts.addAttribute( "vertical-align", "top" );
1115             writeStartTag( BLOCK_TAG, atts );
1116             figureGraphics( compLogo, getGraphicsAttributes( compLogo ) );
1117             writeEndTag( BLOCK_TAG );
1118         }
1119 
1120         writeSimpleTag( BLOCK_TAG );
1121         writeEndTag( TABLE_CELL_TAG );
1122         writeStartTag( TABLE_CELL_TAG );
1123 
1124         if ( StringUtils.isNotEmpty( projLogo ) )
1125         {
1126             SinkEventAttributeSet atts = new SinkEventAttributeSet();
1127             atts.addAttribute( "text-align", "right" );
1128             atts.addAttribute( "vertical-align", "top" );
1129             writeStartTag( BLOCK_TAG, atts );
1130             figureGraphics( projLogo, getGraphicsAttributes( projLogo ) );
1131             writeEndTag( BLOCK_TAG );
1132         }
1133 
1134         writeSimpleTag( BLOCK_TAG );
1135         writeEndTag( TABLE_CELL_TAG );
1136         writeEndTag( TABLE_ROW_TAG );
1137     }
1138 
1139     private void writeCoverBody( DocumentCover cover, DocumentMeta meta )
1140     {
1141         if ( cover == null && meta == null )
1142         {
1143             return;
1144         }
1145 
1146         String subtitle = null;
1147         String title = null;
1148         String type = null;
1149         String version = null;
1150         if ( cover == null )
1151         {
1152             // aleady checked that meta != null
1153             getLog().debug( "The DocumentCover is not defined, using the DocumentMeta title as cover title." );
1154             title = meta.getTitle();
1155         }
1156         else
1157         {
1158             subtitle = cover.getCoverSubTitle();
1159             title = cover.getCoverTitle();
1160             type = cover.getCoverType();
1161             version = cover.getCoverVersion();
1162         }
1163 
1164         writeln( "<fo:table-row keep-with-previous=\"always\" height=\"0.014in\">" );
1165         writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" );
1166         writeStartTag( BLOCK_TAG, "line-height", "0.014in" );
1167         writeEmptyTag( LEADER_TAG, "chapter.rule" );
1168         writeEndTag( BLOCK_TAG );
1169         writeEndTag( TABLE_CELL_TAG );
1170         writeEndTag( TABLE_ROW_TAG );
1171 
1172         writeStartTag( TABLE_ROW_TAG, "height", "7.447in" );
1173         writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" );
1174         writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" );
1175         writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "2.083in" );
1176         writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "2.083in" );
1177         writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "2.083in" );
1178 
1179         writeStartTag( TABLE_BODY_TAG );
1180 
1181         writeStartTag( TABLE_ROW_TAG );
1182         writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "3" );
1183         writeSimpleTag( BLOCK_TAG );
1184         writeEmptyTag( BLOCK_TAG, "space-before", "3.2235in" );
1185         writeEndTag( TABLE_CELL_TAG );
1186         writeEndTag( TABLE_ROW_TAG );
1187 
1188         writeStartTag( TABLE_ROW_TAG );
1189         writeStartTag( TABLE_CELL_TAG );
1190         writeEmptyTag( BLOCK_TAG, "space-after", "0.5in" );
1191         writeEndTag( TABLE_CELL_TAG );
1192 
1193         writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2", "cover.border.left" );
1194         writeStartTag( BLOCK_TAG, "cover.title" );
1195         text( title == null ? "" : title );
1196         writeEndTag( BLOCK_TAG );
1197         writeEndTag( TABLE_CELL_TAG );
1198         writeEndTag( TABLE_ROW_TAG );
1199 
1200         writeStartTag( TABLE_ROW_TAG );
1201         writeStartTag( TABLE_CELL_TAG );
1202         writeSimpleTag( BLOCK_TAG );
1203         writeEndTag( TABLE_CELL_TAG );
1204 
1205         writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2", "cover.border.left.bottom" );
1206         writeStartTag( BLOCK_TAG, "cover.subtitle" );
1207         text( subtitle == null ? ( version == null ? "" : " v. " + version ) : subtitle );
1208         writeEndTag( BLOCK_TAG );
1209         writeStartTag( BLOCK_TAG, "cover.subtitle" );
1210         text( type == null ? "" : type );
1211         writeEndTag( BLOCK_TAG );
1212         writeEndTag( TABLE_CELL_TAG );
1213         writeEndTag( TABLE_ROW_TAG );
1214 
1215         writeEndTag( TABLE_BODY_TAG );
1216         writeEndTag( TABLE_TAG );
1217 
1218         writeEndTag( TABLE_CELL_TAG );
1219         writeEndTag( TABLE_ROW_TAG );
1220 
1221         writeStartTag( TABLE_ROW_TAG, "height", "0.014in" );
1222         writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" );
1223         writeln( "<fo:block space-after=\"0.2in\" line-height=\"0.014in\">" );
1224         writeEmptyTag( LEADER_TAG, "chapter.rule" );
1225         writeEndTag( BLOCK_TAG );
1226         writeEndTag( TABLE_CELL_TAG );
1227         writeEndTag( TABLE_ROW_TAG );
1228 
1229         writeStartTag( TABLE_ROW_TAG );
1230         writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" );
1231         writeSimpleTag( BLOCK_TAG );
1232         writeEmptyTag( BLOCK_TAG, "space-before", "0.2in" );
1233         writeEndTag( TABLE_CELL_TAG );
1234         writeEndTag( TABLE_ROW_TAG );
1235     }
1236 
1237     private void writeCoverFooter( DocumentCover cover, DocumentMeta meta )
1238     {
1239         if ( cover == null && meta == null )
1240         {
1241             return;
1242         }
1243 
1244         String date = null;
1245         String compName = null;
1246         if ( cover == null )
1247         {
1248             // aleady checked that meta != null
1249             getLog().debug( "The DocumentCover is not defined, using the DocumentMeta author as company name." );
1250             compName = meta.getAuthor();
1251         }
1252         else
1253         {
1254             compName = cover.getCompanyName();
1255 
1256             if ( cover.getCoverdate() == null )
1257             {
1258                 cover.setCoverDate( new Date() );
1259                 date = cover.getCoverdate();
1260                 cover.setCoverDate( null );
1261             }
1262             else
1263             {
1264                 date = cover.getCoverdate();
1265             }
1266         }
1267 
1268         writeStartTag( TABLE_ROW_TAG, "height", "0.3in" );
1269 
1270         writeStartTag( TABLE_CELL_TAG );
1271         MutableAttributeSet att = getFoConfiguration().getAttributeSet( "cover.subtitle" );
1272         att.addAttribute( "height", "0.3in" );
1273         att.addAttribute( "text-align", "left" );
1274         writeStartTag( BLOCK_TAG, att );
1275         text( compName == null ? ( cover.getAuthor() == null ? "" : cover.getAuthor() ) : compName );
1276         writeEndTag( BLOCK_TAG );
1277         writeEndTag( TABLE_CELL_TAG );
1278 
1279         writeStartTag( TABLE_CELL_TAG );
1280         att = getFoConfiguration().getAttributeSet( "cover.subtitle" );
1281         att.addAttribute( "height", "0.3in" );
1282         att.addAttribute( "text-align", "right" );
1283         writeStartTag( BLOCK_TAG, att );
1284         text( date == null ? "" : date );
1285         writeEndTag( BLOCK_TAG );
1286         writeEndTag( TABLE_CELL_TAG );
1287 
1288         writeEndTag( TABLE_ROW_TAG );
1289     }
1290 
1291     private ResourceBundle getBundle( Locale locale )
1292     {
1293         return ResourceBundle.getBundle( "doxia-fo", locale, this.getClass().getClassLoader() );
1294     }
1295 
1296     private SinkEventAttributeSet getGraphicsAttributes( String logo )
1297     {
1298         MutableAttributeSet atts = null;
1299 
1300         try
1301         {
1302             atts = DoxiaUtils.getImageAttributes( logo );
1303         }
1304         catch ( IOException e )
1305         {
1306             getLog().debug( e );
1307         }
1308 
1309         if ( atts == null )
1310         {
1311             return new SinkEventAttributeSet( new String[]{ SinkEventAttributes.HEIGHT, COVER_HEADER_HEIGHT } );
1312         }
1313 
1314         // FOP dpi: 72
1315         // Max width : 3.125 inch, table cell size, see #coverPage()
1316         final int maxWidth = 225; // 3.125 * 72
1317 
1318         if ( Integer.parseInt( atts.getAttribute( SinkEventAttributes.WIDTH ).toString() ) > maxWidth )
1319         {
1320             atts.addAttribute( "content-width", "3.125in" );
1321         }
1322 
1323         return new SinkEventAttributeSet( atts );
1324     }
1325 }