Coverage Report - org.apache.maven.doxia.module.fml.FmlParser
 
Classes in this File Line Coverage Branch Coverage Complexity
FmlParser
81%
221/270
63%
86/136
6,667
 
 1  
 package org.apache.maven.doxia.module.fml;
 2  
 
 3  
 /*
 4  
  * Licensed to the Apache Software Foundation (ASF) under one
 5  
  * or more contributor license agreements.  See the NOTICE file
 6  
  * distributed with this work for additional information
 7  
  * regarding copyright ownership.  The ASF licenses this file
 8  
  * to you under the Apache License, Version 2.0 (the
 9  
  * "License"); you may not use this file except in compliance
 10  
  * with the License.  You may obtain a copy of the License at
 11  
  *
 12  
  *   http://www.apache.org/licenses/LICENSE-2.0
 13  
  *
 14  
  * Unless required by applicable law or agreed to in writing,
 15  
  * software distributed under the License is distributed on an
 16  
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 17  
  * KIND, either express or implied.  See the License for the
 18  
  * specific language governing permissions and limitations
 19  
  * under the License.
 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  
  * Parse a fml model and emit events into the specified doxia Sink.
 56  
  *
 57  
  * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
 58  
  * @author ltheussl
 59  
  * @version $Id: FmlParser.java 1090706 2011-04-09 23:15:28Z hboutemy $
 60  
  * @since 1.0
 61  
  * @plexus.component role="org.apache.maven.doxia.parser.Parser" role-hint="fml"
 62  
  */
 63  12
 public class FmlParser
 64  
     extends AbstractXmlParser
 65  
     implements FmlMarkup
 66  
 {
 67  
     /** Collect a faqs model. */
 68  
     private Faqs faqs;
 69  
 
 70  
     /** Collect a part. */
 71  
     private Part currentPart;
 72  
 
 73  
     /** Collect a single faq. */
 74  
     private Faq currentFaq;
 75  
 
 76  
     /** Used to collect text events. */
 77  
     private StringBuffer buffer;
 78  
 
 79  
     /** Map of warn messages with a String as key to describe the error type and a Set as value.
 80  
      * Using to reduce warn messages. */
 81  
     private Map<String, Set<String>> warnMessages;
 82  
 
 83  
     /** The source content of the input reader. Used to pass into macros. */
 84  
     private String sourceContent;
 85  
 
 86  
     /** A macro name. */
 87  
     private String macroName;
 88  
 
 89  
     /** The macro parameters. */
 90  12
     private Map<String, Object> macroParameters = new HashMap<String, Object>();
 91  
 
 92  
     /** {@inheritDoc} */
 93  
     public void parse( Reader source, Sink sink )
 94  
         throws ParseException
 95  
     {
 96  10
         this.faqs = null;
 97  10
         this.sourceContent = null;
 98  10
         init();
 99  
 
 100  
         try
 101  
         {
 102  10
             StringWriter contentWriter = new StringWriter();
 103  10
             IOUtil.copy( source, contentWriter );
 104  10
             sourceContent = contentWriter.toString();
 105  
         }
 106  0
         catch ( IOException ex )
 107  
         {
 108  0
             throw new ParseException( "Error reading the input source: " + ex.getMessage(), ex );
 109  
         }
 110  
         finally
 111  
         {
 112  10
             IOUtil.close( source );
 113  10
         }
 114  
 
 115  
         try
 116  
         {
 117  10
             Reader tmp = new StringReader( sourceContent );
 118  
 
 119  10
             this.faqs = new Faqs();
 120  
 
 121  
             // this populates faqs
 122  10
             super.parse( tmp, sink );
 123  
 
 124  10
             writeFaqs( sink );
 125  
         }
 126  
         finally
 127  
         {
 128  10
             logWarnings();
 129  
 
 130  10
             this.faqs = null;
 131  10
             this.sourceContent = null;
 132  10
             setSecondParsing( false );
 133  10
             init();
 134  10
         }
 135  10
     }
 136  
 
 137  
     /** {@inheritDoc} */
 138  
     protected void handleStartTag( XmlPullParser parser, Sink sink )
 139  
         throws XmlPullParserException, MacroExecutionException
 140  
     {
 141  1382
         if ( parser.getName().equals( FAQS_TAG.toString() ) )
 142  
         {
 143  10
             String title = parser.getAttributeValue( null, "title" );
 144  
 
 145  10
             if ( title != null )
 146  
             {
 147  10
                 faqs.setTitle( title );
 148  
             }
 149  
 
 150  10
             String toplink = parser.getAttributeValue( null, "toplink" );
 151  
 
 152  10
             if ( toplink != null )
 153  
             {
 154  0
                 if ( toplink.equalsIgnoreCase( "true" ) )
 155  
                 {
 156  0
                     faqs.setToplink( true );
 157  
                 }
 158  
                 else
 159  
                 {
 160  0
                     faqs.setToplink( false );
 161  
                 }
 162  
             }
 163  10
         }
 164  1372
         else if ( parser.getName().equals( PART_TAG.toString() ) )
 165  
         {
 166  42
             currentPart = new Part();
 167  
 
 168  42
             currentPart.setId( parser.getAttributeValue( null, Attribute.ID.toString() ) );
 169  
 
 170  42
             if ( currentPart.getId() == null )
 171  
             {
 172  0
                 throw new XmlPullParserException( "id attribute required for <part> at: ("
 173  
                     + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
 174  
             }
 175  42
             else if ( !DoxiaUtils.isValidId( currentPart.getId() ) )
 176  
             {
 177  0
                 String linkAnchor = DoxiaUtils.encodeId( currentPart.getId(), true );
 178  
 
 179  0
                 String msg = "Modified invalid link: '" + currentPart.getId() + "' to '" + linkAnchor + "'";
 180  0
                 logMessage( "modifiedLink", msg );
 181  
 
 182  0
                 currentPart.setId( linkAnchor );
 183  0
             }
 184  
         }
 185  1330
         else if ( parser.getName().equals( TITLE.toString() ) )
 186  
         {
 187  42
             buffer = new StringBuffer();
 188  
 
 189  42
             buffer.append( String.valueOf( LESS_THAN ) ).append( parser.getName() )
 190  
                 .append( String.valueOf( GREATER_THAN ) );
 191  
         }
 192  1288
         else if ( parser.getName().equals( FAQ_TAG.toString() ) )
 193  
         {
 194  168
             currentFaq = new Faq();
 195  
 
 196  168
             currentFaq.setId( parser.getAttributeValue( null, Attribute.ID.toString() ) );
 197  
 
 198  168
             if ( currentFaq.getId() == null )
 199  
             {
 200  0
                 throw new XmlPullParserException( "id attribute required for <faq> at: ("
 201  
                     + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
 202  
             }
 203  168
             else if ( !DoxiaUtils.isValidId( currentFaq.getId() ) )
 204  
             {
 205  0
                 String linkAnchor = DoxiaUtils.encodeId( currentFaq.getId(), true );
 206  
 
 207  0
                 String msg = "Modified invalid link: '" + currentFaq.getId() + "' to '" + linkAnchor + "'";
 208  0
                 logMessage( "modifiedLink", msg );
 209  
 
 210  0
                 currentFaq.setId( linkAnchor );
 211  0
             }
 212  
         }
 213  1120
         else if ( parser.getName().equals( QUESTION_TAG.toString() ) )
 214  
         {
 215  168
             buffer = new StringBuffer();
 216  
 
 217  168
             buffer.append( String.valueOf( LESS_THAN ) ).append( parser.getName() )
 218  
                 .append( String.valueOf( GREATER_THAN ) );
 219  
         }
 220  952
         else if ( parser.getName().equals( ANSWER_TAG.toString() ) )
 221  
         {
 222  168
             buffer = new StringBuffer();
 223  
 
 224  168
             buffer.append( String.valueOf( LESS_THAN ) ).append( parser.getName() )
 225  
                 .append( String.valueOf( GREATER_THAN ) );
 226  
 
 227  
         }
 228  
 
 229  
         // ----------------------------------------------------------------------
 230  
         // Macro
 231  
         // ----------------------------------------------------------------------
 232  
 
 233  784
         else if ( parser.getName().equals( MACRO_TAG.toString() ) )
 234  
         {
 235  2
             handleMacroStart( parser );
 236  
         }
 237  782
         else if ( parser.getName().equals( PARAM.toString() ) )
 238  
         {
 239  4
             handleParamStart( parser, sink );
 240  
         }
 241  778
         else if ( buffer != null )
 242  
         {
 243  778
             buffer.append( String.valueOf( LESS_THAN ) ).append( parser.getName() );
 244  
 
 245  778
             int count = parser.getAttributeCount();
 246  
 
 247  906
             for ( int i = 0; i < count; i++ )
 248  
             {
 249  128
                 buffer.append( String.valueOf( SPACE ) ).append( parser.getAttributeName( i ) );
 250  
 
 251  128
                 buffer.append( String.valueOf( EQUAL ) ).append( String.valueOf( QUOTE ) );
 252  
 
 253  
                 // TODO: why are attribute values HTML-encoded?
 254  128
                 buffer.append( HtmlTools.escapeHTML( parser.getAttributeValue( i ) ) );
 255  
 
 256  128
                 buffer.append( String.valueOf( QUOTE ) );
 257  
             }
 258  
 
 259  778
             buffer.append( String.valueOf( GREATER_THAN ) );
 260  
         }
 261  1382
     }
 262  
 
 263  
     /** {@inheritDoc} */
 264  
     protected void handleEndTag( XmlPullParser parser, Sink sink )
 265  
         throws XmlPullParserException, MacroExecutionException
 266  
     {
 267  1382
         if ( parser.getName().equals( FAQS_TAG.toString() ) )
 268  
         {
 269  
             // Do nothing
 270  10
             return;
 271  
         }
 272  1372
         else if ( parser.getName().equals( PART_TAG.toString() ) )
 273  
         {
 274  42
             faqs.addPart( currentPart );
 275  
 
 276  42
             currentPart = null;
 277  
         }
 278  1330
         else if ( parser.getName().equals( FAQ_TAG.toString() ) )
 279  
         {
 280  168
             if ( currentPart == null )
 281  
             {
 282  0
                 throw new XmlPullParserException( "Missing <part>  at: ("
 283  
                     + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
 284  
             }
 285  
 
 286  168
             currentPart.addFaq( currentFaq );
 287  
 
 288  168
             currentFaq = null;
 289  
         }
 290  1162
         else if ( parser.getName().equals( QUESTION_TAG.toString() ) )
 291  
         {
 292  168
             if ( currentFaq == null )
 293  
             {
 294  0
                 throw new XmlPullParserException( "Missing <faq> at: ("
 295  
                     + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
 296  
             }
 297  
 
 298  168
             buffer.append( String.valueOf( LESS_THAN ) ).append( String.valueOf( SLASH ) )
 299  
                 .append( parser.getName() ).append( String.valueOf( GREATER_THAN ) );
 300  
 
 301  168
             currentFaq.setQuestion( buffer.toString() );
 302  
 
 303  168
             buffer = null;
 304  
         }
 305  994
         else if ( parser.getName().equals( ANSWER_TAG.toString() ) )
 306  
         {
 307  168
             if ( currentFaq == null )
 308  
             {
 309  0
                 throw new XmlPullParserException( "Missing <faq> at: ("
 310  
                     + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
 311  
             }
 312  
 
 313  168
             buffer.append( String.valueOf( LESS_THAN ) ).append( String.valueOf( SLASH ) )
 314  
                 .append( parser.getName() ).append( String.valueOf( GREATER_THAN ) );
 315  
 
 316  168
             currentFaq.setAnswer( buffer.toString() );
 317  
 
 318  168
             buffer = null;
 319  
         }
 320  826
         else if ( parser.getName().equals( TITLE.toString() ) )
 321  
         {
 322  42
             if ( currentPart == null )
 323  
             {
 324  0
                 throw new XmlPullParserException( "Missing <part> at: ("
 325  
                     + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")" );
 326  
             }
 327  
 
 328  42
             buffer.append( String.valueOf( LESS_THAN ) ).append( String.valueOf( SLASH ) )
 329  
                 .append( parser.getName() ).append( String.valueOf( GREATER_THAN ) );
 330  
 
 331  42
             currentPart.setTitle( buffer.toString() );
 332  
 
 333  42
             buffer = null;
 334  
         }
 335  
 
 336  
         // ----------------------------------------------------------------------
 337  
         // Macro
 338  
         // ----------------------------------------------------------------------
 339  
 
 340  784
         else if ( parser.getName().equals( MACRO_TAG.toString() ) )
 341  
         {
 342  2
             handleMacroEnd( buffer );
 343  
         }
 344  782
         else if ( parser.getName().equals( PARAM.toString() ) )
 345  
         {
 346  4
             if ( !StringUtils.isNotEmpty( macroName ) )
 347  
             {
 348  0
                 handleUnknown( parser, sink, TAG_TYPE_END );
 349  
             }
 350  
         }
 351  778
         else if ( buffer != null )
 352  
         {
 353  778
             if ( buffer.length() > 0 && buffer.charAt( buffer.length() - 1 ) == SPACE )
 354  
             {
 355  288
                 buffer.deleteCharAt( buffer.length() - 1 );
 356  
             }
 357  
 
 358  778
             buffer.append( String.valueOf( LESS_THAN ) ).append( String.valueOf( SLASH ) )
 359  
                 .append( parser.getName() ).append( String.valueOf( GREATER_THAN ) );
 360  
         }
 361  1372
     }
 362  
 
 363  
     /** {@inheritDoc} */
 364  
     protected void handleText( XmlPullParser parser, Sink sink )
 365  
         throws XmlPullParserException
 366  
     {
 367  2678
         if ( buffer != null )
 368  
         {
 369  1878
             buffer.append( parser.getText() );
 370  
         }
 371  
         // only significant text content in fml files is in <question>, <answer> or <title>
 372  2678
     }
 373  
 
 374  
     /** {@inheritDoc} */
 375  
     protected void handleCdsect( XmlPullParser parser, Sink sink )
 376  
         throws XmlPullParserException
 377  
     {
 378  40
         String cdSection = parser.getText();
 379  
 
 380  40
         if ( buffer != null )
 381  
         {
 382  40
             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  0
             sink.text( cdSection );
 389  
         }
 390  40
     }
 391  
 
 392  
     /** {@inheritDoc} */
 393  
     protected void handleComment( XmlPullParser parser, Sink sink )
 394  
         throws XmlPullParserException
 395  
     {
 396  20
         String comment = parser.getText();
 397  
 
 398  20
         if ( buffer != null )
 399  
         {
 400  0
             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  20
             sink.comment( comment.trim() );
 406  
         }
 407  20
     }
 408  
 
 409  
     /** {@inheritDoc} */
 410  
     protected void handleEntity( XmlPullParser parser, Sink sink )
 411  
         throws XmlPullParserException
 412  
     {
 413  32
         if ( buffer != null )
 414  
         {
 415  32
             if ( parser.getText() != null )
 416  
             {
 417  32
                 String text = parser.getText();
 418  
 
 419  
                 // parser.getText() returns the entity replacement text
 420  
                 // (&lt; -> <), need to re-escape them
 421  32
                 if ( text.length() == 1 )
 422  
                 {
 423  26
                     text = HtmlTools.escapeHTML( text );
 424  
                 }
 425  
 
 426  32
                 buffer.append( text );
 427  32
             }
 428  
         }
 429  
         else
 430  
         {
 431  0
             super.handleEntity( parser, sink );
 432  
         }
 433  32
     }
 434  
 
 435  
     /** {@inheritDoc} */
 436  
     protected void init()
 437  
     {
 438  40
         super.init();
 439  
 
 440  40
         this.currentFaq = null;
 441  40
         this.currentPart = null;
 442  40
         this.buffer = null;
 443  40
         this.warnMessages = null;
 444  40
         this.macroName = null;
 445  40
         this.macroParameters = null;
 446  40
     }
 447  
 
 448  
     /**
 449  
      * TODO import from XdocParser, probably need to be generic.
 450  
      *
 451  
      * @param parser not null
 452  
      * @throws MacroExecutionException if any
 453  
      */
 454  
     private void handleMacroStart( XmlPullParser parser )
 455  
             throws MacroExecutionException
 456  
     {
 457  2
         if ( !isSecondParsing() )
 458  
         {
 459  2
             macroName = parser.getAttributeValue( null, Attribute.NAME.toString() );
 460  
 
 461  2
             if ( macroParameters == null )
 462  
             {
 463  2
                 macroParameters = new HashMap<String, Object>();
 464  
             }
 465  
 
 466  2
             if ( StringUtils.isEmpty( macroName ) )
 467  
             {
 468  0
                 throw new MacroExecutionException( "The '" + Attribute.NAME.toString()
 469  
                         + "' attribute for the '" + MACRO_TAG.toString() + "' tag is required." );
 470  
             }
 471  
         }
 472  2
     }
 473  
 
 474  
     /**
 475  
      * TODO import from XdocParser, probably need to be generic.
 476  
      *
 477  
      * @param buffer not null
 478  
      * @throws MacroExecutionException if any
 479  
      */
 480  
     private void handleMacroEnd( StringBuffer buffer )
 481  
             throws MacroExecutionException
 482  
     {
 483  2
         if ( !isSecondParsing() )
 484  
         {
 485  2
             if ( StringUtils.isNotEmpty( macroName ) )
 486  
             {
 487  
                 // TODO handles specific macro attributes
 488  2
                 macroParameters.put( "sourceContent", sourceContent );
 489  2
                 FmlParser fmlParser = new FmlParser();
 490  2
                 fmlParser.setSecondParsing( true );
 491  2
                 macroParameters.put( "parser", fmlParser );
 492  
 
 493  2
                 MacroRequest request = new MacroRequest( macroParameters, getBasedir() );
 494  
 
 495  
                 try
 496  
                 {
 497  2
                     StringWriter sw = new StringWriter();
 498  2
                     XhtmlBaseSink sink = new XhtmlBaseSink(sw);
 499  2
                     executeMacro( macroName, request, sink );
 500  2
                     sink.close();
 501  2
                     buffer.append( sw.toString() );
 502  0
                 } catch ( MacroNotFoundException me )
 503  
                 {
 504  0
                     throw new MacroExecutionException( "Macro not found: " + macroName, me );
 505  2
                 }
 506  
             }
 507  
         }
 508  
 
 509  
         // Reinit macro
 510  2
         macroName = null;
 511  2
         macroParameters = null;
 512  2
     }
 513  
 
 514  
     /**
 515  
      * TODO import from XdocParser, probably need to be generic.
 516  
      *
 517  
      * @param parser not null
 518  
      * @param sink not null
 519  
      * @throws MacroExecutionException if any
 520  
      */
 521  
     private void handleParamStart( XmlPullParser parser, Sink sink )
 522  
             throws MacroExecutionException
 523  
     {
 524  4
         if ( !isSecondParsing() )
 525  
         {
 526  4
             if ( StringUtils.isNotEmpty( macroName ) )
 527  
             {
 528  4
                 String paramName = parser.getAttributeValue( null, Attribute.NAME.toString() );
 529  4
                 String paramValue = parser.getAttributeValue( null,
 530  
                         Attribute.VALUE.toString() );
 531  
 
 532  4
                 if ( StringUtils.isEmpty( paramName ) || StringUtils.isEmpty( paramValue ) )
 533  
                 {
 534  0
                     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  4
                 macroParameters.put( paramName, paramValue );
 540  4
             }
 541  
             else
 542  
             {
 543  
                 // param tag from non-macro object, see MSITE-288
 544  0
                 handleUnknown( parser, sink, TAG_TYPE_START );
 545  
             }
 546  
         }
 547  4
     }
 548  
 
 549  
     /**
 550  
      * Writes the faqs to the specified sink.
 551  
      *
 552  
      * @param faqs The faqs to emit.
 553  
      * @param sink The sink to consume the event.
 554  
      * @throws ParseException if something goes wrong.
 555  
      */
 556  
     private void writeFaqs( Sink sink )
 557  
         throws ParseException
 558  
     {
 559  10
         FmlContentParser xdocParser = new FmlContentParser();
 560  10
         xdocParser.enableLogging( getLog() );
 561  
 
 562  10
         sink.head();
 563  10
         sink.title();
 564  10
         sink.text( faqs.getTitle() );
 565  10
         sink.title_();
 566  10
         sink.head_();
 567  
 
 568  10
         sink.body();
 569  10
         sink.section1();
 570  10
         sink.sectionTitle1();
 571  10
         sink.anchor( "top" );
 572  10
         sink.text( faqs.getTitle() );
 573  10
         sink.anchor_();
 574  10
         sink.sectionTitle1_();
 575  
 
 576  
         // ----------------------------------------------------------------------
 577  
         // Write summary
 578  
         // ----------------------------------------------------------------------
 579  
 
 580  10
         for ( Part part : faqs.getParts() )
 581  
         {
 582  42
             if ( StringUtils.isNotEmpty( part.getTitle() ) )
 583  
             {
 584  42
                 sink.paragraph();
 585  42
                 sink.bold();
 586  42
                 xdocParser.parse( part.getTitle(), sink );
 587  42
                 sink.bold_();
 588  42
                 sink.paragraph_();
 589  
             }
 590  
 
 591  42
             sink.numberedList( Sink.NUMBERING_DECIMAL );
 592  
 
 593  42
             for ( Faq faq : part.getFaqs() )
 594  
             {
 595  168
                 sink.numberedListItem();
 596  168
                 sink.link( "#" + faq.getId() );
 597  
 
 598  168
                 if ( StringUtils.isNotEmpty( faq.getQuestion() ) )
 599  
                 {
 600  168
                     xdocParser.parse( faq.getQuestion(), sink );
 601  
                 }
 602  
                 else
 603  
                 {
 604  0
                     throw new ParseException( "Missing <question> for FAQ '" + faq.getId() + "'" );
 605  
                 }
 606  
 
 607  168
                 sink.link_();
 608  168
                 sink.numberedListItem_();
 609  
             }
 610  
 
 611  42
             sink.numberedList_();
 612  
         }
 613  
 
 614  10
         sink.section1_();
 615  
 
 616  
         // ----------------------------------------------------------------------
 617  
         // Write content
 618  
         // ----------------------------------------------------------------------
 619  
 
 620  10
         for ( Part part : faqs.getParts() )
 621  
         {
 622  42
             if ( StringUtils.isNotEmpty( part.getTitle() ) )
 623  
             {
 624  42
                 sink.section1();
 625  
 
 626  42
                 sink.sectionTitle1();
 627  42
                 xdocParser.parse( part.getTitle(), sink );
 628  42
                 sink.sectionTitle1_();
 629  
             }
 630  
 
 631  42
             sink.definitionList();
 632  
 
 633  42
             for ( Iterator<Faq> faqIterator = part.getFaqs().iterator(); faqIterator.hasNext(); )
 634  
             {
 635  168
                 Faq faq = faqIterator.next();
 636  
 
 637  168
                 sink.definedTerm();
 638  168
                 sink.anchor( faq.getId() );
 639  
 
 640  168
                 if ( StringUtils.isNotEmpty( faq.getQuestion() ) )
 641  
                 {
 642  168
                     xdocParser.parse( faq.getQuestion(), sink );
 643  
                 }
 644  
                 else
 645  
                 {
 646  0
                     throw new ParseException( "Missing <question> for FAQ '" + faq.getId() + "'" );
 647  
                 }
 648  
 
 649  168
                 sink.anchor_();
 650  168
                 sink.definedTerm_();
 651  
 
 652  168
                 sink.definition();
 653  
 
 654  168
                 if ( StringUtils.isNotEmpty( faq.getAnswer() ) )
 655  
                 {
 656  168
                     xdocParser.parse( faq.getAnswer(), sink );
 657  
                 }
 658  
                 else
 659  
                 {
 660  0
                     throw new ParseException( "Missing <answer> for FAQ '" + faq.getId() + "'" );
 661  
                 }
 662  
 
 663  168
                 if ( faqs.isToplink() )
 664  
                 {
 665  168
                     writeTopLink( sink );
 666  
                 }
 667  
 
 668  168
                 if ( faqIterator.hasNext() )
 669  
                 {
 670  126
                     sink.horizontalRule();
 671  
                 }
 672  
 
 673  168
                 sink.definition_();
 674  168
             }
 675  
 
 676  42
             sink.definitionList_();
 677  
 
 678  42
             if ( StringUtils.isNotEmpty( part.getTitle() ) )
 679  
             {
 680  42
                 sink.section1_();
 681  
             }
 682  
         }
 683  
 
 684  10
         sink.body_();
 685  10
     }
 686  
 
 687  
     /**
 688  
      * Writes a toplink element.
 689  
      *
 690  
      * @param sink The sink to consume the event.
 691  
      */
 692  
     private void writeTopLink( Sink sink )
 693  
     {
 694  168
         SinkEventAttributeSet atts = new SinkEventAttributeSet();
 695  168
         atts.addAttribute( SinkEventAttributeSet.ALIGN, "right" );
 696  168
         sink.paragraph( atts );
 697  168
         sink.link( "#top" );
 698  168
         sink.text( "[top]" );
 699  168
         sink.link_();
 700  168
         sink.paragraph_();
 701  168
     }
 702  
 
 703  
     /**
 704  
      * If debug mode is enabled, log the <code>msg</code> as is, otherwise add unique msg in <code>warnMessages</code>.
 705  
      *
 706  
      * @param key not null
 707  
      * @param msg not null
 708  
      * @see #parse(Reader, Sink)
 709  
      * @since 1.1.1
 710  
      */
 711  
     private void logMessage( String key, String msg )
 712  
     {
 713  0
         msg = "[FML Parser] " + msg;
 714  0
         if ( getLog().isDebugEnabled() )
 715  
         {
 716  0
             getLog().debug( msg );
 717  
 
 718  0
             return;
 719  
         }
 720  
 
 721  0
         if ( warnMessages == null )
 722  
         {
 723  0
             warnMessages = new HashMap<String, Set<String>>();
 724  
         }
 725  
 
 726  0
         Set<String> set = warnMessages.get( key );
 727  0
         if ( set == null )
 728  
         {
 729  0
             set = new TreeSet<String>();
 730  
         }
 731  0
         set.add( msg );
 732  0
         warnMessages.put( key, set );
 733  0
     }
 734  
 
 735  
     /**
 736  
      * @since 1.1.1
 737  
      */
 738  
     private void logWarnings()
 739  
     {
 740  10
         if ( getLog().isWarnEnabled() && this.warnMessages != null && !isSecondParsing() )
 741  
         {
 742  0
             for ( Map.Entry<String, Set<String>> entry : this.warnMessages.entrySet() )
 743  
             {
 744  0
                 for ( String msg : entry.getValue() )
 745  
                 {
 746  0
                     getLog().warn( msg );
 747  
                 }
 748  
             }
 749  
 
 750  0
             this.warnMessages = null;
 751  
         }
 752  10
     }
 753  
 }