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  
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.TreeSet;
32  
33  import javax.swing.text.html.HTML.Attribute;
34  
35  import org.apache.maven.doxia.macro.MacroExecutionException;
36  import org.apache.maven.doxia.macro.MacroRequest;
37  import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
38  import org.apache.maven.doxia.module.fml.model.Faq;
39  import org.apache.maven.doxia.module.fml.model.Faqs;
40  import org.apache.maven.doxia.module.fml.model.Part;
41  import org.apache.maven.doxia.parser.AbstractXmlParser;
42  import org.apache.maven.doxia.parser.ParseException;
43  import org.apache.maven.doxia.parser.Parser;
44  import org.apache.maven.doxia.sink.Sink;
45  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
46  import org.apache.maven.doxia.sink.impl.XhtmlBaseSink;
47  import org.apache.maven.doxia.util.DoxiaUtils;
48  import org.apache.maven.doxia.util.HtmlTools;
49  
50  import org.codehaus.plexus.component.annotations.Component;
51  import org.codehaus.plexus.util.IOUtil;
52  import org.codehaus.plexus.util.StringUtils;
53  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
54  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
55  
56  /**
57   * Parse a fml model and emit events into the specified doxia Sink.
58   *
59   * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
60   * @author ltheussl
61   * @version $Id: FmlParser.java 1726411 2016-01-23 16:34:09Z hboutemy $
62   * @since 1.0
63   */
64  @Component( role = Parser.class, hint = "fml" )
65  public class FmlParser
66      extends AbstractXmlParser
67      implements FmlMarkup
68  {
69      /** Collect a faqs model. */
70      private Faqs faqs;
71  
72      /** Collect a part. */
73      private Part currentPart;
74  
75      /** Collect a single faq. */
76      private Faq currentFaq;
77  
78      /** Used to collect text events. */
79      private StringBuilder buffer;
80  
81      /** Map of warn messages with a String as key to describe the error type and a Set as value.
82       * Using to reduce warn messages. */
83      private Map<String, Set<String>> warnMessages;
84  
85      /** The source content of the input reader. Used to pass into macros. */
86      private String sourceContent;
87  
88      /** A macro name. */
89      private String macroName;
90  
91      /** The macro parameters. */
92      private Map<String, Object> macroParameters = new HashMap<String, Object>();
93  
94      /** {@inheritDoc} */
95      public void parse( Reader source, Sink sink )
96          throws ParseException
97      {
98          this.faqs = null;
99          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             if ( isEmitComments() )
408             {
409                 sink.comment( comment );
410             }
411         }
412     }
413 
414     /** {@inheritDoc} */
415     protected void handleEntity( XmlPullParser parser, Sink sink )
416         throws XmlPullParserException
417     {
418         if ( buffer != null )
419         {
420             if ( parser.getText() != null )
421             {
422                 String text = parser.getText();
423 
424                 // parser.getText() returns the entity replacement text
425                 // (&lt; -> <), need to re-escape them
426                 if ( text.length() == 1 )
427                 {
428                     text = HtmlTools.escapeHTML( text );
429                 }
430 
431                 buffer.append( text );
432             }
433         }
434         else
435         {
436             super.handleEntity( parser, sink );
437         }
438     }
439 
440     /** {@inheritDoc} */
441     protected void init()
442     {
443         super.init();
444 
445         this.currentFaq = null;
446         this.currentPart = null;
447         this.buffer = null;
448         this.warnMessages = null;
449         this.macroName = null;
450         this.macroParameters = null;
451     }
452 
453     /**
454      * TODO import from XdocParser, probably need to be generic.
455      *
456      * @param parser not null
457      * @throws MacroExecutionException if any
458      */
459     private void handleMacroStart( XmlPullParser parser )
460             throws MacroExecutionException
461     {
462         if ( !isSecondParsing() )
463         {
464             macroName = parser.getAttributeValue( null, Attribute.NAME.toString() );
465 
466             if ( macroParameters == null )
467             {
468                 macroParameters = new HashMap<String, Object>();
469             }
470 
471             if ( StringUtils.isEmpty( macroName ) )
472             {
473                 throw new MacroExecutionException( "The '" + Attribute.NAME.toString()
474                         + "' attribute for the '" + MACRO_TAG.toString() + "' tag is required." );
475             }
476         }
477     }
478 
479     /**
480      * TODO import from XdocParser, probably need to be generic.
481      *
482      * @param buffer not null
483      * @throws MacroExecutionException if any
484      */
485     private void handleMacroEnd( StringBuilder buffer )
486             throws MacroExecutionException
487     {
488         if ( !isSecondParsing() )
489         {
490             if ( StringUtils.isNotEmpty( macroName ) )
491             {
492                 MacroRequest request =
493                     new MacroRequest( sourceContent, new FmlParser(), macroParameters, getBasedir() );
494 
495                 try
496                 {
497                     StringWriter sw = new StringWriter();
498                     XhtmlBaseSink sink = new XhtmlBaseSink( sw );
499                     executeMacro( macroName, request, sink );
500                     sink.close();
501                     buffer.append( sw.toString() );
502                 }
503                 catch ( MacroNotFoundException me )
504                 {
505                     throw new MacroExecutionException( "Macro not found: " + macroName, me );
506                 }
507             }
508         }
509 
510         // Reinit macro
511         macroName = null;
512         macroParameters = null;
513     }
514 
515     /**
516      * TODO import from XdocParser, probably need to be generic.
517      *
518      * @param parser not null
519      * @param sink not null
520      * @throws MacroExecutionException if any
521      */
522     private void handleParamStart( XmlPullParser parser, Sink sink )
523             throws MacroExecutionException
524     {
525         if ( !isSecondParsing() )
526         {
527             if ( StringUtils.isNotEmpty( macroName ) )
528             {
529                 String paramName = parser.getAttributeValue( null, Attribute.NAME.toString() );
530                 String paramValue = parser.getAttributeValue( null,
531                         Attribute.VALUE.toString() );
532 
533                 if ( StringUtils.isEmpty( paramName ) || StringUtils.isEmpty( paramValue ) )
534                 {
535                     throw new MacroExecutionException( "'" + Attribute.NAME.toString()
536                             + "' and '" + Attribute.VALUE.toString() + "' attributes for the '" + PARAM.toString()
537                             + "' tag are required inside the '" + MACRO_TAG.toString() + "' tag." );
538                 }
539 
540                 macroParameters.put( paramName, paramValue );
541             }
542             else
543             {
544                 // param tag from non-macro object, see MSITE-288
545                 handleUnknown( parser, sink, TAG_TYPE_START );
546             }
547         }
548     }
549 
550     /**
551      * Writes the faqs to the specified sink.
552      *
553      * @param faqs The faqs to emit.
554      * @param sink The sink to consume the event.
555      * @throws ParseException if something goes wrong.
556      */
557     private void writeFaqs( Sink sink )
558         throws ParseException
559     {
560         FmlContentParser xdocParser = new FmlContentParser();
561         xdocParser.enableLogging( getLog() );
562 
563         sink.head();
564         sink.title();
565         sink.text( faqs.getTitle() );
566         sink.title_();
567         sink.head_();
568 
569         sink.body();
570         sink.section1();
571         sink.sectionTitle1();
572         sink.anchor( "top" );
573         sink.text( faqs.getTitle() );
574         sink.anchor_();
575         sink.sectionTitle1_();
576 
577         // ----------------------------------------------------------------------
578         // Write summary
579         // ----------------------------------------------------------------------
580 
581         for ( Part part : faqs.getParts() )
582         {
583             if ( StringUtils.isNotEmpty( part.getTitle() ) )
584             {
585                 sink.paragraph();
586                 sink.bold();
587                 xdocParser.parse( part.getTitle(), sink );
588                 sink.bold_();
589                 sink.paragraph_();
590             }
591 
592             sink.numberedList( Sink.NUMBERING_DECIMAL );
593 
594             for ( Faq faq : part.getFaqs() )
595             {
596                 sink.numberedListItem();
597                 sink.link( "#" + faq.getId() );
598 
599                 if ( StringUtils.isNotEmpty( faq.getQuestion() ) )
600                 {
601                     xdocParser.parse( faq.getQuestion(), sink );
602                 }
603                 else
604                 {
605                     throw new ParseException( "Missing <question> for FAQ '" + faq.getId() + "'" );
606                 }
607 
608                 sink.link_();
609                 sink.numberedListItem_();
610             }
611 
612             sink.numberedList_();
613         }
614 
615         sink.section1_();
616 
617         // ----------------------------------------------------------------------
618         // Write content
619         // ----------------------------------------------------------------------
620 
621         for ( Part part : faqs.getParts() )
622         {
623             if ( StringUtils.isNotEmpty( part.getTitle() ) )
624             {
625                 sink.section1();
626 
627                 sink.sectionTitle1();
628                 xdocParser.parse( part.getTitle(), sink );
629                 sink.sectionTitle1_();
630             }
631 
632             sink.definitionList();
633 
634             for ( Iterator<Faq> faqIterator = part.getFaqs().iterator(); faqIterator.hasNext(); )
635             {
636                 Faq faq = faqIterator.next();
637 
638                 sink.definedTerm();
639                 sink.anchor( faq.getId() );
640 
641                 if ( StringUtils.isNotEmpty( faq.getQuestion() ) )
642                 {
643                     xdocParser.parse( faq.getQuestion(), sink );
644                 }
645                 else
646                 {
647                     throw new ParseException( "Missing <question> for FAQ '" + faq.getId() + "'" );
648                 }
649 
650                 sink.anchor_();
651                 sink.definedTerm_();
652 
653                 sink.definition();
654 
655                 if ( StringUtils.isNotEmpty( faq.getAnswer() ) )
656                 {
657                     xdocParser.parse( faq.getAnswer(), sink );
658                 }
659                 else
660                 {
661                     throw new ParseException( "Missing <answer> for FAQ '" + faq.getId() + "'" );
662                 }
663 
664                 if ( faqs.isToplink() )
665                 {
666                     writeTopLink( sink );
667                 }
668 
669                 if ( faqIterator.hasNext() )
670                 {
671                     sink.horizontalRule();
672                 }
673 
674                 sink.definition_();
675             }
676 
677             sink.definitionList_();
678 
679             if ( StringUtils.isNotEmpty( part.getTitle() ) )
680             {
681                 sink.section1_();
682             }
683         }
684 
685         sink.body_();
686     }
687 
688     /**
689      * Writes a toplink element.
690      *
691      * @param sink The sink to consume the event.
692      */
693     private void writeTopLink( Sink sink )
694     {
695         SinkEventAttributeSet atts = new SinkEventAttributeSet();
696         atts.addAttribute( SinkEventAttributeSet.ALIGN, "right" );
697         sink.paragraph( atts );
698         sink.link( "#top" );
699         sink.text( "[top]" );
700         sink.link_();
701         sink.paragraph_();
702     }
703 
704     /**
705      * If debug mode is enabled, log the <code>msg</code> as is, otherwise add unique msg in <code>warnMessages</code>.
706      *
707      * @param key not null
708      * @param msg not null
709      * @see #parse(Reader, Sink)
710      * @since 1.1.1
711      */
712     private void logMessage( String key, String msg )
713     {
714         msg = "[FML Parser] " + msg;
715         if ( getLog().isDebugEnabled() )
716         {
717             getLog().debug( msg );
718 
719             return;
720         }
721 
722         if ( warnMessages == null )
723         {
724             warnMessages = new HashMap<String, Set<String>>();
725         }
726 
727         Set<String> set = warnMessages.get( key );
728         if ( set == null )
729         {
730             set = new TreeSet<String>();
731         }
732         set.add( msg );
733         warnMessages.put( key, set );
734     }
735 
736     /**
737      * @since 1.1.1
738      */
739     private void logWarnings()
740     {
741         if ( getLog().isWarnEnabled() && this.warnMessages != null && !isSecondParsing() )
742         {
743             for ( Map.Entry<String, Set<String>> entry : this.warnMessages.entrySet() )
744             {
745                 for ( String msg : entry.getValue() )
746                 {
747                     getLog().warn( msg );
748                 }
749             }
750 
751             this.warnMessages = null;
752         }
753     }
754 }