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