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; 026import java.util.HashMap; 027import java.util.Iterator; 028import java.util.Map; 029import java.util.Set; 030import java.util.TreeSet; 031 032import javax.swing.text.html.HTML.Attribute; 033 034import org.apache.maven.doxia.macro.MacroExecutionException; 035import org.apache.maven.doxia.macro.MacroRequest; 036import org.apache.maven.doxia.macro.manager.MacroNotFoundException; 037import org.apache.maven.doxia.module.fml.model.Faq; 038import org.apache.maven.doxia.module.fml.model.Faqs; 039import org.apache.maven.doxia.module.fml.model.Part; 040import org.apache.maven.doxia.parser.AbstractXmlParser; 041import org.apache.maven.doxia.parser.ParseException; 042import org.apache.maven.doxia.parser.Parser; 043import org.apache.maven.doxia.sink.Sink; 044import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 045import org.apache.maven.doxia.sink.impl.XhtmlBaseSink; 046import org.apache.maven.doxia.util.DoxiaUtils; 047import org.apache.maven.doxia.util.HtmlTools; 048import org.codehaus.plexus.component.annotations.Component; 049import org.codehaus.plexus.util.IOUtil; 050import org.codehaus.plexus.util.StringUtils; 051import org.codehaus.plexus.util.xml.pull.XmlPullParser; 052import org.codehaus.plexus.util.xml.pull.XmlPullParserException; 053 054/** 055 * Parse a fml model and emit events into the specified doxia Sink. 056 * 057 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a> 058 * @author ltheussl 059 * @since 1.0 060 */ 061@Component( role = Parser.class, hint = "fml" ) 062public class FmlParser 063 extends AbstractXmlParser 064 implements FmlMarkup 065{ 066 /** Collect a faqs model. */ 067 private Faqs faqs; 068 069 /** Collect a part. */ 070 private Part currentPart; 071 072 /** Collect a single faq. */ 073 private Faq currentFaq; 074 075 /** Used to collect text events. */ 076 private StringBuilder buffer; 077 078 /** Map of warn messages with a String as key to describe the error type and a Set as value. 079 * Using to reduce warn messages. */ 080 private Map<String, Set<String>> warnMessages; 081 082 /** The source content of the input reader. Used to pass into macros. */ 083 private String sourceContent; 084 085 /** A macro name. */ 086 private String macroName; 087 088 /** The macro parameters. */ 089 private Map<String, Object> macroParameters = new HashMap<>(); 090 091 /** {@inheritDoc} */ 092 public void parse( Reader source, Sink sink, String reference ) 093 throws ParseException 094 { 095 this.faqs = null; 096 this.sourceContent = null; 097 init(); 098 099 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 // this populates faqs 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 /** {@inheritDoc} */ 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 // Macro 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 // TODO: why are attribute values HTML-encoded? 247 buffer.append( HtmlTools.escapeHTML( parser.getAttributeValue( i ) ) ); 248 249 buffer.append( QUOTE ); 250 } 251 252 buffer.append( GREATER_THAN ); 253 } 254 } 255 256 /** {@inheritDoc} */ 257 protected void handleEndTag( XmlPullParser parser, Sink sink ) 258 throws XmlPullParserException, MacroExecutionException 259 { 260 if ( parser.getName().equals( FAQS_TAG.toString() ) ) 261 { 262 // Do nothing 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 // Macro 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 /** {@inheritDoc} */ 353 protected void handleText( XmlPullParser parser, Sink sink ) 354 throws XmlPullParserException 355 { 356 if ( buffer != null ) 357 { 358 buffer.append( parser.getText() ); 359 } 360 // only significant text content in fml files is in <question>, <answer> or <title> 361 } 362 363 /** {@inheritDoc} */ 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 /** {@inheritDoc} */ 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 /** {@inheritDoc} */ 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 // parser.getText() returns the entity replacement text 412 // (< -> <), need to re-escape them 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 * {@inheritDoc} 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 * TODO import from XdocParser, probably need to be generic. 444 * 445 * @param parser not null 446 * @throws MacroExecutionException if any 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 * TODO import from XdocParser, probably need to be generic. 470 * 471 * @param buffer not null 472 * @throws MacroExecutionException if any 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 // Reinit macro 500 macroName = null; 501 macroParameters = null; 502 } 503 504 /** 505 * TODO import from XdocParser, probably need to be generic. 506 * 507 * @param parser not null 508 * @param sink not null 509 * @throws MacroExecutionException if any 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 // param tag from non-macro object, see MSITE-288 534 handleUnknown( parser, sink, TAG_TYPE_START ); 535 } 536 } 537 } 538 539 /** 540 * Writes the faqs to the specified sink. 541 * 542 * @param sink The sink to consume the event. 543 * @throws ParseException if something goes wrong. 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 // Write summary 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 // Write content 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 * Writes a toplink element. 678 * 679 * @param sink The sink to consume the event. 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 * If debug mode is enabled, log the <code>msg</code> as is, otherwise add unique msg in <code>warnMessages</code>. 694 * 695 * @param key not null 696 * @param msg not null 697 * @see #parse(Reader, Sink) 698 * @since 1.1.1 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 * @since 1.1.1 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}