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