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