View Javadoc
1   package org.apache.maven.doxia.module.fml;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.IOException;
23  import java.io.Reader;
24  import java.io.StringReader;
25  import java.io.StringWriter;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.TreeSet;
31  
32  import javax.swing.text.html.HTML.Attribute;
33  
34  import org.apache.maven.doxia.macro.MacroExecutionException;
35  import org.apache.maven.doxia.macro.MacroRequest;
36  import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
37  import org.apache.maven.doxia.module.fml.model.Faq;
38  import org.apache.maven.doxia.module.fml.model.Faqs;
39  import org.apache.maven.doxia.module.fml.model.Part;
40  import org.apache.maven.doxia.parser.AbstractXmlParser;
41  import org.apache.maven.doxia.parser.ParseException;
42  import org.apache.maven.doxia.parser.Parser;
43  import org.apache.maven.doxia.sink.Sink;
44  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
45  import org.apache.maven.doxia.sink.impl.XhtmlBaseSink;
46  import org.apache.maven.doxia.util.DoxiaUtils;
47  import org.apache.maven.doxia.util.HtmlTools;
48  import org.codehaus.plexus.component.annotations.Component;
49  import org.codehaus.plexus.util.IOUtil;
50  import org.codehaus.plexus.util.StringUtils;
51  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
52  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
53  
54  /**
55   * Parse a fml model and emit events into the specified doxia Sink.
56   *
57   * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
58   * @author ltheussl
59   * @since 1.0
60   */
61  @Component( role = Parser.class, hint = "fml" )
62  public class FmlParser
63      extends AbstractXmlParser
64      implements FmlMarkup
65  {
66      /** Collect a faqs model. */
67      private Faqs faqs;
68  
69      /** Collect a part. */
70      private Part currentPart;
71  
72      /** Collect a single faq. */
73      private Faq currentFaq;
74  
75      /** Used to collect text events. */
76      private StringBuilder buffer;
77  
78      /** Map of warn messages with a String as key to describe the error type and a Set as value.
79       * Using to reduce warn messages. */
80      private Map<String, Set<String>> warnMessages;
81  
82      /** The source content of the input reader. Used to pass into macros. */
83      private String sourceContent;
84  
85      /** A macro name. */
86      private String macroName;
87  
88      /** The macro parameters. */
89      private Map<String, Object> macroParameters = new HashMap<>();
90  
91      /** {@inheritDoc} */
92      public void parse( Reader source, Sink sink, String reference )
93          throws ParseException
94      {
95          this.faqs = null;
96          this.sourceContent = null;
97          init();
98  
99          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 }