001package org.apache.maven.doxia.module.fo;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.IOException;
023import java.io.Writer;
024
025import java.util.Calendar;
026import java.util.Date;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Locale;
030import java.util.ResourceBundle;
031import java.util.Stack;
032
033import javax.swing.text.MutableAttributeSet;
034import javax.swing.text.html.HTML.Tag;
035
036import org.apache.maven.doxia.document.DocumentCover;
037import org.apache.maven.doxia.document.DocumentMeta;
038import org.apache.maven.doxia.document.DocumentModel;
039import org.apache.maven.doxia.document.DocumentTOC;
040import org.apache.maven.doxia.document.DocumentTOCItem;
041import org.apache.maven.doxia.sink.SinkEventAttributes;
042import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
043import org.apache.maven.doxia.util.DoxiaUtils;
044import org.apache.maven.doxia.util.HtmlTools;
045
046import org.codehaus.plexus.util.StringUtils;
047
048/**
049 * A Doxia Sink that produces an aggregated FO model. The usage is similar to the following:
050 * <pre>
051 * FoAggregateSink sink = new FoAggregateSink( writer );
052 * sink.setDocumentModel( documentModel );
053 * sink.beginDocument();
054 * sink.coverPage();
055 * sink.toc();
056 * ...
057 * sink.endDocument();
058 * </pre>
059 * <b>Note</b>: the documentModel object contains several
060 * <a href="https://maven.apache.org/doxia/doxia/doxia-core/document.html">document metadata</a>, but only a few
061 * of them are used in this sink (i.e. author, confidential, date and title), the others are ignored.
062 *
063 * @author ltheussl
064 * @since 1.1
065 */
066public class FoAggregateSink
067    extends FoSink
068{
069    /**
070     * No Table Of Content.
071     *
072     * @see #setDocumentModel(DocumentModel, int)
073     */
074    public static final int TOC_NONE = 0;
075
076    /**
077     * Table Of Content at the start of the document.
078     *
079     * @see #setDocumentModel(DocumentModel, int)
080     */
081    public static final int TOC_START = 1;
082
083    /**
084     * Table Of Content at the end of the document.
085     *
086     * @see #setDocumentModel(DocumentModel, int)
087     */
088    public static final int TOC_END = 2;
089
090    // TODO: make configurable
091    private static final String COVER_HEADER_HEIGHT = "1.5in";
092
093    /**
094     * The document model to be used by this sink.
095     */
096    private DocumentModel docModel;
097
098    /**
099     * 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}