001package org.apache.maven.doxia.module.fml;
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.Reader;
024import java.io.StringReader;
025import java.io.StringWriter;
026
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.Map;
030import java.util.Set;
031import java.util.TreeSet;
032
033import javax.swing.text.html.HTML.Attribute;
034
035import org.apache.maven.doxia.macro.MacroExecutionException;
036import org.apache.maven.doxia.macro.MacroRequest;
037import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
038import org.apache.maven.doxia.module.fml.model.Faq;
039import org.apache.maven.doxia.module.fml.model.Faqs;
040import org.apache.maven.doxia.module.fml.model.Part;
041import org.apache.maven.doxia.parser.AbstractXmlParser;
042import org.apache.maven.doxia.parser.ParseException;
043import org.apache.maven.doxia.parser.Parser;
044import org.apache.maven.doxia.sink.Sink;
045import org.apache.maven.doxia.sink.SinkEventAttributeSet;
046import org.apache.maven.doxia.sink.XhtmlBaseSink;
047import org.apache.maven.doxia.util.DoxiaUtils;
048import org.apache.maven.doxia.util.HtmlTools;
049
050import org.codehaus.plexus.component.annotations.Component;
051import org.codehaus.plexus.util.IOUtil;
052import org.codehaus.plexus.util.StringUtils;
053import org.codehaus.plexus.util.xml.pull.XmlPullParser;
054import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
055
056/**
057 * Parse a fml model and emit events into the specified doxia Sink.
058 *
059 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
060 * @author ltheussl
061 * @version $Id$
062 * @since 1.0
063 */
064@Component( role = Parser.class, hint = "fml" )
065public class FmlParser
066    extends AbstractXmlParser
067    implements FmlMarkup
068{
069    /** Collect a faqs model. */
070    private Faqs faqs;
071
072    /** Collect a part. */
073    private Part currentPart;
074
075    /** Collect a single faq. */
076    private Faq currentFaq;
077
078    /** Used to collect text events. */
079    private StringBuilder buffer;
080
081    /** Map of warn messages with a String as key to describe the error type and a Set as value.
082     * Using to reduce warn messages. */
083    private Map<String, Set<String>> warnMessages;
084
085    /** The source content of the input reader. Used to pass into macros. */
086    private String sourceContent;
087
088    /** A macro name. */
089    private String macroName;
090
091    /** The macro parameters. */
092    private Map<String, Object> macroParameters = new HashMap<String, Object>();
093
094    /** {@inheritDoc} */
095    public void parse( Reader source, Sink sink )
096        throws ParseException
097    {
098        this.faqs = null;
099        this.sourceContent = null;
100        init();
101
102        try
103        {
104            StringWriter contentWriter = new StringWriter();
105            IOUtil.copy( source, contentWriter );
106            sourceContent = contentWriter.toString();
107        }
108        catch ( IOException ex )
109        {
110            throw new ParseException( "Error reading the input source: " + ex.getMessage(), ex );
111        }
112        finally
113        {
114            IOUtil.close( source );
115        }
116
117        try
118        {
119            Reader tmp = new StringReader( sourceContent );
120
121            this.faqs = new Faqs();
122
123            // this populates faqs
124            super.parse( tmp, sink );
125
126            writeFaqs( sink );
127        }
128        finally
129        {
130            logWarnings();
131
132            this.faqs = null;
133            this.sourceContent = null;
134            setSecondParsing( false );
135            init();
136        }
137    }
138
139    /** {@inheritDoc} */
140    protected void handleStartTag( XmlPullParser parser, Sink sink )
141        throws XmlPullParserException, MacroExecutionException
142    {
143        if ( parser.getName().equals( FAQS_TAG.toString() ) )
144        {
145            String title = parser.getAttributeValue( null, "title" );
146
147            if ( title != null )
148            {
149                faqs.setTitle( title );
150            }
151
152            String toplink = parser.getAttributeValue( null, "toplink" );
153
154            if ( toplink != null )
155            {
156                if ( toplink.equalsIgnoreCase( "true" ) )
157                {
158                    faqs.setToplink( true );
159                }
160                else
161                {
162                    faqs.setToplink( false );
163                }
164            }
165        }
166        else if ( parser.getName().equals( PART_TAG.toString() ) )
167        {
168            currentPart = new Part();
169
170            currentPart.setId( parser.getAttributeValue( null, Attribute.ID.toString() ) );
171
172            if ( currentPart.getId() == null )
173            {
174                throw new XmlPullParserException( "id attribute required for <part> at: ("
175                    + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
176            }
177            else if ( !DoxiaUtils.isValidId( currentPart.getId() ) )
178            {
179                String linkAnchor = DoxiaUtils.encodeId( currentPart.getId(), true );
180
181                String msg = "Modified invalid link: '" + currentPart.getId() + "' to '" + linkAnchor + "'";
182                logMessage( "modifiedLink", msg );
183
184                currentPart.setId( linkAnchor );
185            }
186        }
187        else if ( parser.getName().equals( TITLE.toString() ) )
188        {
189            buffer = new StringBuilder();
190
191            buffer.append( String.valueOf( LESS_THAN ) ).append( parser.getName() )
192                .append( String.valueOf( GREATER_THAN ) );
193        }
194        else if ( parser.getName().equals( FAQ_TAG.toString() ) )
195        {
196            currentFaq = new Faq();
197
198            currentFaq.setId( parser.getAttributeValue( null, Attribute.ID.toString() ) );
199
200            if ( currentFaq.getId() == null )
201            {
202                throw new XmlPullParserException( "id attribute required for <faq> at: ("
203                    + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
204            }
205            else if ( !DoxiaUtils.isValidId( currentFaq.getId() ) )
206            {
207                String linkAnchor = DoxiaUtils.encodeId( currentFaq.getId(), true );
208
209                String msg = "Modified invalid link: '" + currentFaq.getId() + "' to '" + linkAnchor + "'";
210                logMessage( "modifiedLink", msg );
211
212                currentFaq.setId( linkAnchor );
213            }
214        }
215        else if ( parser.getName().equals( QUESTION_TAG.toString() ) )
216        {
217            buffer = new StringBuilder();
218
219            buffer.append( String.valueOf( LESS_THAN ) ).append( parser.getName() )
220                .append( String.valueOf( GREATER_THAN ) );
221        }
222        else if ( parser.getName().equals( ANSWER_TAG.toString() ) )
223        {
224            buffer = new StringBuilder();
225
226            buffer.append( String.valueOf( LESS_THAN ) ).append( parser.getName() )
227                .append( String.valueOf( GREATER_THAN ) );
228
229        }
230
231        // ----------------------------------------------------------------------
232        // Macro
233        // ----------------------------------------------------------------------
234
235        else if ( parser.getName().equals( MACRO_TAG.toString() ) )
236        {
237            handleMacroStart( parser );
238        }
239        else if ( parser.getName().equals( PARAM.toString() ) )
240        {
241            handleParamStart( parser, sink );
242        }
243        else if ( buffer != null )
244        {
245            buffer.append( String.valueOf( LESS_THAN ) ).append( parser.getName() );
246
247            int count = parser.getAttributeCount();
248
249            for ( int i = 0; i < count; i++ )
250            {
251                buffer.append( String.valueOf( SPACE ) ).append( parser.getAttributeName( i ) );
252
253                buffer.append( String.valueOf( EQUAL ) ).append( String.valueOf( QUOTE ) );
254
255                // TODO: why are attribute values HTML-encoded?
256                buffer.append( HtmlTools.escapeHTML( parser.getAttributeValue( i ) ) );
257
258                buffer.append( String.valueOf( QUOTE ) );
259            }
260
261            buffer.append( String.valueOf( GREATER_THAN ) );
262        }
263    }
264
265    /** {@inheritDoc} */
266    protected void handleEndTag( XmlPullParser parser, Sink sink )
267        throws XmlPullParserException, MacroExecutionException
268    {
269        if ( parser.getName().equals( FAQS_TAG.toString() ) )
270        {
271            // Do nothing
272            return;
273        }
274        else if ( parser.getName().equals( PART_TAG.toString() ) )
275        {
276            faqs.addPart( currentPart );
277
278            currentPart = null;
279        }
280        else if ( parser.getName().equals( FAQ_TAG.toString() ) )
281        {
282            if ( currentPart == null )
283            {
284                throw new XmlPullParserException( "Missing <part>  at: ("
285                    + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
286            }
287
288            currentPart.addFaq( currentFaq );
289
290            currentFaq = null;
291        }
292        else if ( parser.getName().equals( QUESTION_TAG.toString() ) )
293        {
294            if ( currentFaq == null )
295            {
296                throw new XmlPullParserException( "Missing <faq> at: ("
297                    + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
298            }
299
300            buffer.append( String.valueOf( LESS_THAN ) ).append( String.valueOf( SLASH ) )
301                .append( parser.getName() ).append( String.valueOf( GREATER_THAN ) );
302
303            currentFaq.setQuestion( buffer.toString() );
304
305            buffer = null;
306        }
307        else if ( parser.getName().equals( ANSWER_TAG.toString() ) )
308        {
309            if ( currentFaq == null )
310            {
311                throw new XmlPullParserException( "Missing <faq> at: ("
312                    + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
313            }
314
315            buffer.append( String.valueOf( LESS_THAN ) ).append( String.valueOf( SLASH ) )
316                .append( parser.getName() ).append( String.valueOf( GREATER_THAN ) );
317
318            currentFaq.setAnswer( buffer.toString() );
319
320            buffer = null;
321        }
322        else if ( parser.getName().equals( TITLE.toString() ) )
323        {
324            if ( currentPart == null )
325            {
326                throw new XmlPullParserException( "Missing <part> at: ("
327                    + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
328            }
329
330            buffer.append( String.valueOf( LESS_THAN ) ).append( String.valueOf( SLASH ) )
331                .append( parser.getName() ).append( String.valueOf( GREATER_THAN ) );
332
333            currentPart.setTitle( buffer.toString() );
334
335            buffer = null;
336        }
337
338        // ----------------------------------------------------------------------
339        // Macro
340        // ----------------------------------------------------------------------
341
342        else if ( parser.getName().equals( MACRO_TAG.toString() ) )
343        {
344            handleMacroEnd( buffer );
345        }
346        else if ( parser.getName().equals( PARAM.toString() ) )
347        {
348            if ( !StringUtils.isNotEmpty( macroName ) )
349            {
350                handleUnknown( parser, sink, TAG_TYPE_END );
351            }
352        }
353        else if ( buffer != null )
354        {
355            if ( buffer.length() > 0 && buffer.charAt( buffer.length() - 1 ) == SPACE )
356            {
357                buffer.deleteCharAt( buffer.length() - 1 );
358            }
359
360            buffer.append( String.valueOf( LESS_THAN ) ).append( String.valueOf( SLASH ) )
361                .append( parser.getName() ).append( String.valueOf( GREATER_THAN ) );
362        }
363    }
364
365    /** {@inheritDoc} */
366    protected void handleText( XmlPullParser parser, Sink sink )
367        throws XmlPullParserException
368    {
369        if ( buffer != null )
370        {
371            buffer.append( parser.getText() );
372        }
373        // only significant text content in fml files is in <question>, <answer> or <title>
374    }
375
376    /** {@inheritDoc} */
377    protected void handleCdsect( XmlPullParser parser, Sink sink )
378        throws XmlPullParserException
379    {
380        String cdSection = parser.getText();
381
382        if ( buffer != null )
383        {
384            buffer.append( LESS_THAN ).append( BANG ).append( LEFT_SQUARE_BRACKET ).append( CDATA )
385                    .append( LEFT_SQUARE_BRACKET ).append( cdSection ).append( RIGHT_SQUARE_BRACKET )
386                    .append( RIGHT_SQUARE_BRACKET ).append( GREATER_THAN );
387        }
388        else
389        {
390            sink.text( cdSection );
391        }
392    }
393
394    /** {@inheritDoc} */
395    protected void handleComment( XmlPullParser parser, Sink sink )
396        throws XmlPullParserException
397    {
398        String comment = parser.getText();
399
400        if ( buffer != null )
401        {
402            buffer.append( LESS_THAN ).append( BANG ).append( MINUS ).append( MINUS )
403                    .append( comment ).append( MINUS ).append( MINUS ).append( GREATER_THAN );
404        }
405        else
406        {
407            sink.comment( comment.trim() );
408        }
409    }
410
411    /** {@inheritDoc} */
412    protected void handleEntity( XmlPullParser parser, Sink sink )
413        throws XmlPullParserException
414    {
415        if ( buffer != null )
416        {
417            if ( parser.getText() != null )
418            {
419                String text = parser.getText();
420
421                // parser.getText() returns the entity replacement text
422                // (&lt; -> <), need to re-escape them
423                if ( text.length() == 1 )
424                {
425                    text = HtmlTools.escapeHTML( text );
426                }
427
428                buffer.append( text );
429            }
430        }
431        else
432        {
433            super.handleEntity( parser, sink );
434        }
435    }
436
437    /** {@inheritDoc} */
438    protected void init()
439    {
440        super.init();
441
442        this.currentFaq = null;
443        this.currentPart = null;
444        this.buffer = null;
445        this.warnMessages = null;
446        this.macroName = null;
447        this.macroParameters = null;
448    }
449
450    /**
451     * TODO import from XdocParser, probably need to be generic.
452     *
453     * @param parser not null
454     * @throws MacroExecutionException if any
455     */
456    private void handleMacroStart( XmlPullParser parser )
457            throws MacroExecutionException
458    {
459        if ( !isSecondParsing() )
460        {
461            macroName = parser.getAttributeValue( null, Attribute.NAME.toString() );
462
463            if ( macroParameters == null )
464            {
465                macroParameters = new HashMap<String, Object>();
466            }
467
468            if ( StringUtils.isEmpty( macroName ) )
469            {
470                throw new MacroExecutionException( "The '" + Attribute.NAME.toString()
471                        + "' attribute for the '" + MACRO_TAG.toString() + "' tag is required." );
472            }
473        }
474    }
475
476    /**
477     * TODO import from XdocParser, probably need to be generic.
478     *
479     * @param buffer not null
480     * @throws MacroExecutionException if any
481     */
482    private void handleMacroEnd( StringBuilder buffer )
483            throws MacroExecutionException
484    {
485        if ( !isSecondParsing() )
486        {
487            if ( StringUtils.isNotEmpty( macroName ) )
488            {
489                // TODO handles specific macro attributes
490                macroParameters.put( "sourceContent", sourceContent );
491                FmlParser fmlParser = new FmlParser();
492                fmlParser.setSecondParsing( true );
493                macroParameters.put( "parser", fmlParser );
494
495                MacroRequest request = new MacroRequest( macroParameters, getBasedir() );
496
497                try
498                {
499                    StringWriter sw = new StringWriter();
500                    XhtmlBaseSink sink = new XhtmlBaseSink( sw );
501                    executeMacro( macroName, request, sink );
502                    sink.close();
503                    buffer.append( sw.toString() );
504                } catch ( MacroNotFoundException me )
505                {
506                    throw new MacroExecutionException( "Macro not found: " + macroName, me );
507                }
508            }
509        }
510
511        // Reinit macro
512        macroName = null;
513        macroParameters = null;
514    }
515
516    /**
517     * TODO import from XdocParser, probably need to be generic.
518     *
519     * @param parser not null
520     * @param sink not null
521     * @throws MacroExecutionException if any
522     */
523    private void handleParamStart( XmlPullParser parser, Sink sink )
524            throws MacroExecutionException
525    {
526        if ( !isSecondParsing() )
527        {
528            if ( StringUtils.isNotEmpty( macroName ) )
529            {
530                String paramName = parser.getAttributeValue( null, Attribute.NAME.toString() );
531                String paramValue = parser.getAttributeValue( null,
532                        Attribute.VALUE.toString() );
533
534                if ( StringUtils.isEmpty( paramName ) || StringUtils.isEmpty( paramValue ) )
535                {
536                    throw new MacroExecutionException( "'" + Attribute.NAME.toString()
537                            + "' and '" + Attribute.VALUE.toString() + "' attributes for the '" + PARAM.toString()
538                            + "' tag are required inside the '" + MACRO_TAG.toString() + "' tag." );
539                }
540
541                macroParameters.put( paramName, paramValue );
542            }
543            else
544            {
545                // param tag from non-macro object, see MSITE-288
546                handleUnknown( parser, sink, TAG_TYPE_START );
547            }
548        }
549    }
550
551    /**
552     * Writes the faqs to the specified sink.
553     *
554     * @param faqs The faqs to emit.
555     * @param sink The sink to consume the event.
556     * @throws ParseException if something goes wrong.
557     */
558    private void writeFaqs( Sink sink )
559        throws ParseException
560    {
561        FmlContentParser xdocParser = new FmlContentParser();
562        xdocParser.enableLogging( getLog() );
563
564        sink.head();
565        sink.title();
566        sink.text( faqs.getTitle() );
567        sink.title_();
568        sink.head_();
569
570        sink.body();
571        sink.section1();
572        sink.sectionTitle1();
573        sink.anchor( "top" );
574        sink.text( faqs.getTitle() );
575        sink.anchor_();
576        sink.sectionTitle1_();
577
578        // ----------------------------------------------------------------------
579        // Write summary
580        // ----------------------------------------------------------------------
581
582        for ( Part part : faqs.getParts() )
583        {
584            if ( StringUtils.isNotEmpty( part.getTitle() ) )
585            {
586                sink.paragraph();
587                sink.bold();
588                xdocParser.parse( part.getTitle(), sink );
589                sink.bold_();
590                sink.paragraph_();
591            }
592
593            sink.numberedList( Sink.NUMBERING_DECIMAL );
594
595            for ( Faq faq : part.getFaqs() )
596            {
597                sink.numberedListItem();
598                sink.link( "#" + faq.getId() );
599
600                if ( StringUtils.isNotEmpty( faq.getQuestion() ) )
601                {
602                    xdocParser.parse( faq.getQuestion(), sink );
603                }
604                else
605                {
606                    throw new ParseException( "Missing <question> for FAQ '" + faq.getId() + "'" );
607                }
608
609                sink.link_();
610                sink.numberedListItem_();
611            }
612
613            sink.numberedList_();
614        }
615
616        sink.section1_();
617
618        // ----------------------------------------------------------------------
619        // Write content
620        // ----------------------------------------------------------------------
621
622        for ( Part part : faqs.getParts() )
623        {
624            if ( StringUtils.isNotEmpty( part.getTitle() ) )
625            {
626                sink.section1();
627
628                sink.sectionTitle1();
629                xdocParser.parse( part.getTitle(), sink );
630                sink.sectionTitle1_();
631            }
632
633            sink.definitionList();
634
635            for ( Iterator<Faq> faqIterator = part.getFaqs().iterator(); faqIterator.hasNext(); )
636            {
637                Faq faq = faqIterator.next();
638
639                sink.definedTerm();
640                sink.anchor( faq.getId() );
641
642                if ( StringUtils.isNotEmpty( faq.getQuestion() ) )
643                {
644                    xdocParser.parse( faq.getQuestion(), sink );
645                }
646                else
647                {
648                    throw new ParseException( "Missing <question> for FAQ '" + faq.getId() + "'" );
649                }
650
651                sink.anchor_();
652                sink.definedTerm_();
653
654                sink.definition();
655
656                if ( StringUtils.isNotEmpty( faq.getAnswer() ) )
657                {
658                    xdocParser.parse( faq.getAnswer(), sink );
659                }
660                else
661                {
662                    throw new ParseException( "Missing <answer> for FAQ '" + faq.getId() + "'" );
663                }
664
665                if ( faqs.isToplink() )
666                {
667                    writeTopLink( sink );
668                }
669
670                if ( faqIterator.hasNext() )
671                {
672                    sink.horizontalRule();
673                }
674
675                sink.definition_();
676            }
677
678            sink.definitionList_();
679
680            if ( StringUtils.isNotEmpty( part.getTitle() ) )
681            {
682                sink.section1_();
683            }
684        }
685
686        sink.body_();
687    }
688
689    /**
690     * Writes a toplink element.
691     *
692     * @param sink The sink to consume the event.
693     */
694    private void writeTopLink( Sink sink )
695    {
696        SinkEventAttributeSet atts = new SinkEventAttributeSet();
697        atts.addAttribute( SinkEventAttributeSet.ALIGN, "right" );
698        sink.paragraph( atts );
699        sink.link( "#top" );
700        sink.text( "[top]" );
701        sink.link_();
702        sink.paragraph_();
703    }
704
705    /**
706     * If debug mode is enabled, log the <code>msg</code> as is, otherwise add unique msg in <code>warnMessages</code>.
707     *
708     * @param key not null
709     * @param msg not null
710     * @see #parse(Reader, Sink)
711     * @since 1.1.1
712     */
713    private void logMessage( String key, String msg )
714    {
715        msg = "[FML Parser] " + msg;
716        if ( getLog().isDebugEnabled() )
717        {
718            getLog().debug( msg );
719
720            return;
721        }
722
723        if ( warnMessages == null )
724        {
725            warnMessages = new HashMap<String, Set<String>>();
726        }
727
728        Set<String> set = warnMessages.get( key );
729        if ( set == null )
730        {
731            set = new TreeSet<String>();
732        }
733        set.add( msg );
734        warnMessages.put( key, set );
735    }
736
737    /**
738     * @since 1.1.1
739     */
740    private void logWarnings()
741    {
742        if ( getLog().isWarnEnabled() && this.warnMessages != null && !isSecondParsing() )
743        {
744            for ( Map.Entry<String, Set<String>> entry : this.warnMessages.entrySet() )
745            {
746                for ( String msg : entry.getValue() )
747                {
748                    getLog().warn( msg );
749                }
750            }
751
752            this.warnMessages = null;
753        }
754    }
755}