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