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.impl.SinkEventAttributeSet; 046import org.apache.maven.doxia.sink.impl.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: FmlParser.html 979316 2016-02-02 21:51:43Z hboutemy $ 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 if ( isEmitComments() ) 408 { 409 sink.comment( comment ); 410 } 411 } 412 } 413 414 /** {@inheritDoc} */ 415 protected void handleEntity( XmlPullParser parser, Sink sink ) 416 throws XmlPullParserException 417 { 418 if ( buffer != null ) 419 { 420 if ( parser.getText() != null ) 421 { 422 String text = parser.getText(); 423 424 // parser.getText() returns the entity replacement text 425 // (< -> <), need to re-escape them 426 if ( text.length() == 1 ) 427 { 428 text = HtmlTools.escapeHTML( text ); 429 } 430 431 buffer.append( text ); 432 } 433 } 434 else 435 { 436 super.handleEntity( parser, sink ); 437 } 438 } 439 440 /** {@inheritDoc} */ 441 protected void init() 442 { 443 super.init(); 444 445 this.currentFaq = null; 446 this.currentPart = null; 447 this.buffer = null; 448 this.warnMessages = null; 449 this.macroName = null; 450 this.macroParameters = null; 451 } 452 453 /** 454 * TODO import from XdocParser, probably need to be generic. 455 * 456 * @param parser not null 457 * @throws MacroExecutionException if any 458 */ 459 private void handleMacroStart( XmlPullParser parser ) 460 throws MacroExecutionException 461 { 462 if ( !isSecondParsing() ) 463 { 464 macroName = parser.getAttributeValue( null, Attribute.NAME.toString() ); 465 466 if ( macroParameters == null ) 467 { 468 macroParameters = new HashMap<String, Object>(); 469 } 470 471 if ( StringUtils.isEmpty( macroName ) ) 472 { 473 throw new MacroExecutionException( "The '" + Attribute.NAME.toString() 474 + "' attribute for the '" + MACRO_TAG.toString() + "' tag is required." ); 475 } 476 } 477 } 478 479 /** 480 * TODO import from XdocParser, probably need to be generic. 481 * 482 * @param buffer not null 483 * @throws MacroExecutionException if any 484 */ 485 private void handleMacroEnd( StringBuilder buffer ) 486 throws MacroExecutionException 487 { 488 if ( !isSecondParsing() ) 489 { 490 if ( StringUtils.isNotEmpty( macroName ) ) 491 { 492 MacroRequest request = 493 new MacroRequest( sourceContent, new FmlParser(), 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 } 503 catch ( MacroNotFoundException me ) 504 { 505 throw new MacroExecutionException( "Macro not found: " + macroName, me ); 506 } 507 } 508 } 509 510 // Reinit macro 511 macroName = null; 512 macroParameters = null; 513 } 514 515 /** 516 * TODO import from XdocParser, probably need to be generic. 517 * 518 * @param parser not null 519 * @param sink not null 520 * @throws MacroExecutionException if any 521 */ 522 private void handleParamStart( XmlPullParser parser, Sink sink ) 523 throws MacroExecutionException 524 { 525 if ( !isSecondParsing() ) 526 { 527 if ( StringUtils.isNotEmpty( macroName ) ) 528 { 529 String paramName = parser.getAttributeValue( null, Attribute.NAME.toString() ); 530 String paramValue = parser.getAttributeValue( null, 531 Attribute.VALUE.toString() ); 532 533 if ( StringUtils.isEmpty( paramName ) || StringUtils.isEmpty( paramValue ) ) 534 { 535 throw new MacroExecutionException( "'" + Attribute.NAME.toString() 536 + "' and '" + Attribute.VALUE.toString() + "' attributes for the '" + PARAM.toString() 537 + "' tag are required inside the '" + MACRO_TAG.toString() + "' tag." ); 538 } 539 540 macroParameters.put( paramName, paramValue ); 541 } 542 else 543 { 544 // param tag from non-macro object, see MSITE-288 545 handleUnknown( parser, sink, TAG_TYPE_START ); 546 } 547 } 548 } 549 550 /** 551 * Writes the faqs to the specified sink. 552 * 553 * @param faqs The faqs to emit. 554 * @param sink The sink to consume the event. 555 * @throws ParseException if something goes wrong. 556 */ 557 private void writeFaqs( Sink sink ) 558 throws ParseException 559 { 560 FmlContentParser xdocParser = new FmlContentParser(); 561 xdocParser.enableLogging( getLog() ); 562 563 sink.head(); 564 sink.title(); 565 sink.text( faqs.getTitle() ); 566 sink.title_(); 567 sink.head_(); 568 569 sink.body(); 570 sink.section1(); 571 sink.sectionTitle1(); 572 sink.anchor( "top" ); 573 sink.text( faqs.getTitle() ); 574 sink.anchor_(); 575 sink.sectionTitle1_(); 576 577 // ---------------------------------------------------------------------- 578 // Write summary 579 // ---------------------------------------------------------------------- 580 581 for ( Part part : faqs.getParts() ) 582 { 583 if ( StringUtils.isNotEmpty( part.getTitle() ) ) 584 { 585 sink.paragraph(); 586 sink.bold(); 587 xdocParser.parse( part.getTitle(), sink ); 588 sink.bold_(); 589 sink.paragraph_(); 590 } 591 592 sink.numberedList( Sink.NUMBERING_DECIMAL ); 593 594 for ( Faq faq : part.getFaqs() ) 595 { 596 sink.numberedListItem(); 597 sink.link( "#" + faq.getId() ); 598 599 if ( StringUtils.isNotEmpty( faq.getQuestion() ) ) 600 { 601 xdocParser.parse( faq.getQuestion(), sink ); 602 } 603 else 604 { 605 throw new ParseException( "Missing <question> for FAQ '" + faq.getId() + "'" ); 606 } 607 608 sink.link_(); 609 sink.numberedListItem_(); 610 } 611 612 sink.numberedList_(); 613 } 614 615 sink.section1_(); 616 617 // ---------------------------------------------------------------------- 618 // Write content 619 // ---------------------------------------------------------------------- 620 621 for ( Part part : faqs.getParts() ) 622 { 623 if ( StringUtils.isNotEmpty( part.getTitle() ) ) 624 { 625 sink.section1(); 626 627 sink.sectionTitle1(); 628 xdocParser.parse( part.getTitle(), sink ); 629 sink.sectionTitle1_(); 630 } 631 632 sink.definitionList(); 633 634 for ( Iterator<Faq> faqIterator = part.getFaqs().iterator(); faqIterator.hasNext(); ) 635 { 636 Faq faq = faqIterator.next(); 637 638 sink.definedTerm(); 639 sink.anchor( faq.getId() ); 640 641 if ( StringUtils.isNotEmpty( faq.getQuestion() ) ) 642 { 643 xdocParser.parse( faq.getQuestion(), sink ); 644 } 645 else 646 { 647 throw new ParseException( "Missing <question> for FAQ '" + faq.getId() + "'" ); 648 } 649 650 sink.anchor_(); 651 sink.definedTerm_(); 652 653 sink.definition(); 654 655 if ( StringUtils.isNotEmpty( faq.getAnswer() ) ) 656 { 657 xdocParser.parse( faq.getAnswer(), sink ); 658 } 659 else 660 { 661 throw new ParseException( "Missing <answer> for FAQ '" + faq.getId() + "'" ); 662 } 663 664 if ( faqs.isToplink() ) 665 { 666 writeTopLink( sink ); 667 } 668 669 if ( faqIterator.hasNext() ) 670 { 671 sink.horizontalRule(); 672 } 673 674 sink.definition_(); 675 } 676 677 sink.definitionList_(); 678 679 if ( StringUtils.isNotEmpty( part.getTitle() ) ) 680 { 681 sink.section1_(); 682 } 683 } 684 685 sink.body_(); 686 } 687 688 /** 689 * Writes a toplink element. 690 * 691 * @param sink The sink to consume the event. 692 */ 693 private void writeTopLink( Sink sink ) 694 { 695 SinkEventAttributeSet atts = new SinkEventAttributeSet(); 696 atts.addAttribute( SinkEventAttributeSet.ALIGN, "right" ); 697 sink.paragraph( atts ); 698 sink.link( "#top" ); 699 sink.text( "[top]" ); 700 sink.link_(); 701 sink.paragraph_(); 702 } 703 704 /** 705 * If debug mode is enabled, log the <code>msg</code> as is, otherwise add unique msg in <code>warnMessages</code>. 706 * 707 * @param key not null 708 * @param msg not null 709 * @see #parse(Reader, Sink) 710 * @since 1.1.1 711 */ 712 private void logMessage( String key, String msg ) 713 { 714 msg = "[FML Parser] " + msg; 715 if ( getLog().isDebugEnabled() ) 716 { 717 getLog().debug( msg ); 718 719 return; 720 } 721 722 if ( warnMessages == null ) 723 { 724 warnMessages = new HashMap<String, Set<String>>(); 725 } 726 727 Set<String> set = warnMessages.get( key ); 728 if ( set == null ) 729 { 730 set = new TreeSet<String>(); 731 } 732 set.add( msg ); 733 warnMessages.put( key, set ); 734 } 735 736 /** 737 * @since 1.1.1 738 */ 739 private void logWarnings() 740 { 741 if ( getLog().isWarnEnabled() && this.warnMessages != null && !isSecondParsing() ) 742 { 743 for ( Map.Entry<String, Set<String>> entry : this.warnMessages.entrySet() ) 744 { 745 for ( String msg : entry.getValue() ) 746 { 747 getLog().warn( msg ); 748 } 749 } 750 751 this.warnMessages = null; 752 } 753 } 754}