1 package org.apache.maven.doxia.module.fml;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
58
59
60
61
62
63
64 @Component( role = Parser.class, hint = "fml" )
65 public class FmlParser
66 extends AbstractXmlParser
67 implements FmlMarkup
68 {
69
70 private Faqs faqs;
71
72
73 private Part currentPart;
74
75
76 private Faq currentFaq;
77
78
79 private StringBuilder buffer;
80
81
82
83 private Map<String, Set<String>> warnMessages;
84
85
86 private String sourceContent;
87
88
89 private String macroName;
90
91
92 private Map<String, Object> macroParameters = new HashMap<String, Object>();
93
94
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
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
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
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
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
266 protected void handleEndTag( XmlPullParser parser, Sink sink )
267 throws XmlPullParserException, MacroExecutionException
268 {
269 if ( parser.getName().equals( FAQS_TAG.toString() ) )
270 {
271
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
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
366 protected void handleText( XmlPullParser parser, Sink sink )
367 throws XmlPullParserException
368 {
369 if ( buffer != null )
370 {
371 buffer.append( parser.getText() );
372 }
373
374 }
375
376
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
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
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
422
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
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
452
453
454
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
478
479
480
481
482 private void handleMacroEnd( StringBuilder buffer )
483 throws MacroExecutionException
484 {
485 if ( !isSecondParsing() )
486 {
487 if ( StringUtils.isNotEmpty( macroName ) )
488 {
489
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
512 macroName = null;
513 macroParameters = null;
514 }
515
516
517
518
519
520
521
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
546 handleUnknown( parser, sink, TAG_TYPE_START );
547 }
548 }
549 }
550
551
552
553
554
555
556
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
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
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
691
692
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
707
708
709
710
711
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
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 }