001package org.apache.maven.doxia.module.fml; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.IOException; 023import java.io.Reader; 024import java.io.StringReader; 025import java.io.StringWriter; 026 027import java.util.HashMap; 028import java.util.Iterator; 029import java.util.Map; 030import java.util.Set; 031import java.util.TreeSet; 032 033import javax.swing.text.html.HTML.Attribute; 034 035import org.apache.maven.doxia.macro.MacroExecutionException; 036import org.apache.maven.doxia.macro.MacroRequest; 037import org.apache.maven.doxia.macro.manager.MacroNotFoundException; 038import org.apache.maven.doxia.module.fml.model.Faq; 039import org.apache.maven.doxia.module.fml.model.Faqs; 040import org.apache.maven.doxia.module.fml.model.Part; 041import org.apache.maven.doxia.parser.AbstractXmlParser; 042import org.apache.maven.doxia.parser.ParseException; 043import org.apache.maven.doxia.parser.Parser; 044import org.apache.maven.doxia.sink.Sink; 045import org.apache.maven.doxia.sink.SinkEventAttributeSet; 046import org.apache.maven.doxia.sink.XhtmlBaseSink; 047import org.apache.maven.doxia.util.DoxiaUtils; 048import org.apache.maven.doxia.util.HtmlTools; 049 050import org.codehaus.plexus.component.annotations.Component; 051import org.codehaus.plexus.util.IOUtil; 052import org.codehaus.plexus.util.StringUtils; 053import org.codehaus.plexus.util.xml.pull.XmlPullParser; 054import org.codehaus.plexus.util.xml.pull.XmlPullParserException; 055 056/** 057 * Parse a fml model and emit events into the specified doxia Sink. 058 * 059 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a> 060 * @author ltheussl 061 * @version $Id$ 062 * @since 1.0 063 */ 064@Component( role = Parser.class, hint = "fml" ) 065public class FmlParser 066 extends AbstractXmlParser 067 implements FmlMarkup 068{ 069 /** Collect a faqs model. */ 070 private Faqs faqs; 071 072 /** Collect a part. */ 073 private Part currentPart; 074 075 /** Collect a single faq. */ 076 private Faq currentFaq; 077 078 /** Used to collect text events. */ 079 private StringBuilder buffer; 080 081 /** Map of warn messages with a String as key to describe the error type and a Set as value. 082 * Using to reduce warn messages. */ 083 private Map<String, Set<String>> warnMessages; 084 085 /** The source content of the input reader. Used to pass into macros. */ 086 private String sourceContent; 087 088 /** A macro name. */ 089 private String macroName; 090 091 /** The macro parameters. */ 092 private Map<String, Object> macroParameters = new HashMap<String, Object>(); 093 094 /** {@inheritDoc} */ 095 public void parse( Reader source, Sink sink ) 096 throws ParseException 097 { 098 this.faqs = null; 099 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 // (< -> <), 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}