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.SinkEventAttributeSet;
46  import org.apache.maven.doxia.sink.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 1438269 2013-01-24 23:47:50Z olamy $
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             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 }