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