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