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.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
56
57
58
59
60
61
62
63 public class FmlParser
64 extends AbstractXmlParser
65 implements FmlMarkup
66 {
67
68 private Faqs faqs;
69
70
71 private Part currentPart;
72
73
74 private Faq currentFaq;
75
76
77 private StringBuffer buffer;
78
79
80
81 private Map<String, Set<String>> warnMessages;
82
83
84 private String sourceContent;
85
86
87 private String macroName;
88
89
90 private Map<String, Object> macroParameters = new HashMap<String, Object>();
91
92
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
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
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
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
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
264 protected void handleEndTag( XmlPullParser parser, Sink sink )
265 throws XmlPullParserException, MacroExecutionException
266 {
267 if ( parser.getName().equals( FAQS_TAG.toString() ) )
268 {
269
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
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
364 protected void handleText( XmlPullParser parser, Sink sink )
365 throws XmlPullParserException
366 {
367 if ( buffer != null )
368 {
369 buffer.append( parser.getText() );
370 }
371
372 }
373
374
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
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
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
420
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
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
450
451
452
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
476
477
478
479
480 private void handleMacroEnd( StringBuffer buffer )
481 throws MacroExecutionException
482 {
483 if ( !isSecondParsing() )
484 {
485 if ( StringUtils.isNotEmpty( macroName ) )
486 {
487
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
510 macroName = null;
511 macroParameters = null;
512 }
513
514
515
516
517
518
519
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
544 handleUnknown( parser, sink, TAG_TYPE_START );
545 }
546 }
547 }
548
549
550
551
552
553
554
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
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
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
689
690
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
705
706
707
708
709
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
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 }