001package org.apache.maven.doxia.module.xdoc; 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.Map; 028 029import javax.swing.text.html.HTML.Attribute; 030 031import org.apache.maven.doxia.macro.MacroExecutionException; 032import org.apache.maven.doxia.macro.manager.MacroNotFoundException; 033import org.apache.maven.doxia.macro.MacroRequest; 034import org.apache.maven.doxia.parser.ParseException; 035import org.apache.maven.doxia.parser.Parser; 036import org.apache.maven.doxia.parser.XhtmlBaseParser; 037import org.apache.maven.doxia.sink.Sink; 038import org.apache.maven.doxia.sink.SinkEventAttributeSet; 039import org.apache.maven.doxia.util.HtmlTools; 040 041import org.codehaus.plexus.component.annotations.Component; 042import org.codehaus.plexus.util.IOUtil; 043import org.codehaus.plexus.util.StringUtils; 044import org.codehaus.plexus.util.xml.pull.XmlPullParser; 045import org.codehaus.plexus.util.xml.pull.XmlPullParserException; 046 047/** 048 * Parse an xdoc model and emit events into the specified doxia Sink. 049 * 050 * @author <a href="mailto:jason@maven.org">Jason van Zyl</a> 051 * @version $Id$ 052 * @since 1.0 053 */ 054@Component( role = Parser.class, hint = "xdoc" ) 055public class XdocParser 056 extends XhtmlBaseParser 057 implements XdocMarkup 058{ 059 /** 060 * The source content of the input reader. Used to pass into macros. 061 */ 062 private String sourceContent; 063 064 /** 065 * Empty elements don't write a closing tag. 066 */ 067 private boolean isEmptyElement; 068 069 /** 070 * A macro name. 071 */ 072 private String macroName; 073 074 /** 075 * The macro parameters. 076 */ 077 private Map<String, Object> macroParameters = new HashMap<String, Object>(); 078 079 /** 080 * Indicates that we're inside <properties> or <head>. 081 */ 082 private boolean inHead; 083 084 /** 085 * Indicates that <title> was called from <properties> or <head>. 086 */ 087 private boolean hasTitle; 088 089 /** 090 * {@inheritDoc} 091 */ 092 public void parse( Reader source, Sink sink ) 093 throws ParseException 094 { 095 this.sourceContent = null; 096 init(); 097 098 try 099 { 100 StringWriter contentWriter = new StringWriter(); 101 IOUtil.copy( source, contentWriter ); 102 sourceContent = contentWriter.toString(); 103 } 104 catch ( IOException ex ) 105 { 106 throw new ParseException( "Error reading the input source: " + ex.getMessage(), ex ); 107 } 108 finally 109 { 110 IOUtil.close( source ); 111 } 112 113 Reader tmp = new StringReader( sourceContent ); 114 115 // leave this at default (false) until everything is properly implemented, see DOXIA-226 116 //setIgnorableWhitespace( true ); 117 118 try 119 { 120 super.parse( tmp, sink ); 121 } 122 finally 123 { 124 this.sourceContent = null; 125 126 setSecondParsing( false ); 127 init(); 128 } 129 } 130 131 /** 132 * {@inheritDoc} 133 */ 134 protected void handleStartTag( XmlPullParser parser, Sink sink ) 135 throws XmlPullParserException, MacroExecutionException 136 { 137 isEmptyElement = parser.isEmptyElementTag(); 138 139 SinkEventAttributeSet attribs = getAttributesFromParser( parser ); 140 141 if ( parser.getName().equals( DOCUMENT_TAG.toString() ) ) 142 { 143 //Do nothing 144 return; 145 } 146 else if ( parser.getName().equals( HEAD.toString() ) ) 147 { 148 if ( !inHead ) // we might be in head from a <properties> already 149 { 150 this.inHead = true; 151 152 sink.head( attribs ); 153 } 154 } 155 else if ( parser.getName().equals( TITLE.toString() ) ) 156 { 157 if ( hasTitle ) 158 { 159 getLog().warn( "<title> was already defined in <properties>, ignored <title> in <head>." ); 160 161 try 162 { 163 parser.nextText(); // ignore next text event 164 } 165 catch ( IOException ex ) 166 { 167 throw new XmlPullParserException( "Failed to parse text", parser, ex ); 168 } 169 } 170 else 171 { 172 sink.title( attribs ); 173 } 174 } 175 else if ( parser.getName().equals( AUTHOR_TAG.toString() ) ) 176 { 177 sink.author( attribs ); 178 } 179 else if ( parser.getName().equals( DATE_TAG.toString() ) ) 180 { 181 sink.date( attribs ); 182 } 183 else if ( parser.getName().equals( META.toString() ) ) 184 { 185 handleMetaStart( parser, sink, attribs ); 186 } 187 else if ( parser.getName().equals( BODY.toString() ) ) 188 { 189 if ( inHead ) 190 { 191 sink.head_(); 192 this.inHead = false; 193 } 194 195 sink.body( attribs ); 196 } 197 else if ( parser.getName().equals( SECTION_TAG.toString() ) ) 198 { 199 handleSectionStart( Sink.SECTION_LEVEL_1, sink, attribs, parser ); 200 } 201 else if ( parser.getName().equals( SUBSECTION_TAG.toString() ) ) 202 { 203 handleSectionStart( Sink.SECTION_LEVEL_2, sink, attribs, parser ); 204 } 205 else if ( parser.getName().equals( SOURCE_TAG.toString() ) ) 206 { 207 verbatim(); 208 209 attribs.addAttributes( SinkEventAttributeSet.BOXED ); 210 211 sink.verbatim( attribs ); 212 } 213 else if ( parser.getName().equals( PROPERTIES_TAG.toString() ) ) 214 { 215 if ( !inHead ) // we might be in head from a <head> already 216 { 217 this.inHead = true; 218 219 sink.head( attribs ); 220 } 221 } 222 223 // ---------------------------------------------------------------------- 224 // Macro 225 // ---------------------------------------------------------------------- 226 227 else if ( parser.getName().equals( MACRO_TAG.toString() ) ) 228 { 229 handleMacroStart( parser ); 230 } 231 else if ( parser.getName().equals( PARAM.toString() ) ) 232 { 233 handleParamStart( parser, sink ); 234 } 235 else if ( !baseStartTag( parser, sink ) ) 236 { 237 if ( isEmptyElement ) 238 { 239 handleUnknown( parser, sink, TAG_TYPE_SIMPLE ); 240 } 241 else 242 { 243 handleUnknown( parser, sink, TAG_TYPE_START ); 244 } 245 246 if ( getLog().isDebugEnabled() ) 247 { 248 String position = "[" + parser.getLineNumber() + ":" + parser.getColumnNumber() + "]"; 249 String tag = "<" + parser.getName() + ">"; 250 251 getLog().debug( "Unrecognized xdoc tag: " + tag + " at " + position ); 252 } 253 } 254 } 255 256 /** 257 * {@inheritDoc} 258 */ 259 protected void handleEndTag( XmlPullParser parser, Sink sink ) 260 throws XmlPullParserException, MacroExecutionException 261 { 262 if ( parser.getName().equals( DOCUMENT_TAG.toString() ) ) 263 { 264 //Do nothing 265 return; 266 } 267 else if ( parser.getName().equals( HEAD.toString() ) ) 268 { 269 //Do nothing, head is closed with BODY start. 270 } 271 else if ( parser.getName().equals( BODY.toString() ) ) 272 { 273 consecutiveSections( 0, sink ); 274 275 sink.body_(); 276 } 277 else if ( parser.getName().equals( TITLE.toString() ) ) 278 { 279 if ( !hasTitle ) 280 { 281 sink.title_(); 282 this.hasTitle = true; 283 } 284 } 285 else if ( parser.getName().equals( AUTHOR_TAG.toString() ) ) 286 { 287 sink.author_(); 288 } 289 else if ( parser.getName().equals( DATE_TAG.toString() ) ) 290 { 291 sink.date_(); 292 } 293 else if ( parser.getName().equals( SOURCE_TAG.toString() ) ) 294 { 295 verbatim_(); 296 297 sink.verbatim_(); 298 } 299 else if ( parser.getName().equals( PROPERTIES_TAG.toString() ) ) 300 { 301 //Do nothing, head is closed with BODY start. 302 } 303 else if ( parser.getName().equals( MACRO_TAG.toString() ) ) 304 { 305 handleMacroEnd( sink ); 306 } 307 else if ( parser.getName().equals( PARAM.toString() ) ) 308 { 309 if ( !StringUtils.isNotEmpty( macroName ) ) 310 { 311 handleUnknown( parser, sink, TAG_TYPE_END ); 312 } 313 } 314 else if ( parser.getName().equals( SECTION_TAG.toString() ) ) 315 { 316 consecutiveSections( 0, sink ); 317 318 sink.section1_(); 319 } 320 else if ( parser.getName().equals( SUBSECTION_TAG.toString() ) ) 321 { 322 consecutiveSections( Sink.SECTION_LEVEL_1, sink ); 323 } 324 else if ( !baseEndTag( parser, sink ) ) 325 { 326 if ( !isEmptyElement ) 327 { 328 handleUnknown( parser, sink, TAG_TYPE_END ); 329 } 330 } 331 332 isEmptyElement = false; 333 } 334 335 /** 336 * {@inheritDoc} 337 */ 338 protected void consecutiveSections( int newLevel, Sink sink ) 339 { 340 closeOpenSections( newLevel, sink ); 341 openMissingSections( newLevel, sink ); 342 343 setSectionLevel( newLevel ); 344 } 345 346 /** 347 * {@inheritDoc} 348 */ 349 protected void init() 350 { 351 super.init(); 352 353 this.isEmptyElement = false; 354 this.macroName = null; 355 this.macroParameters = null; 356 this.inHead = false; 357 this.hasTitle = false; 358 } 359 360 /** 361 * Close open h4, h5, h6 sections. 362 */ 363 private void closeOpenSections( int newLevel, Sink sink ) 364 { 365 while ( getSectionLevel() >= newLevel ) 366 { 367 if ( getSectionLevel() == Sink.SECTION_LEVEL_5 ) 368 { 369 sink.section5_(); 370 } 371 else if ( getSectionLevel() == Sink.SECTION_LEVEL_4 ) 372 { 373 sink.section4_(); 374 } 375 else if ( getSectionLevel() == Sink.SECTION_LEVEL_3 ) 376 { 377 sink.section3_(); 378 } 379 else if ( getSectionLevel() == Sink.SECTION_LEVEL_2 ) 380 { 381 sink.section2_(); 382 } 383 384 setSectionLevel( getSectionLevel() - 1 ); 385 } 386 } 387 388 private void handleMacroEnd( Sink sink ) 389 throws MacroExecutionException 390 { 391 if ( !isSecondParsing() ) 392 { 393 if ( StringUtils.isNotEmpty( macroName ) ) 394 { 395 // TODO handles specific macro attributes 396 macroParameters.put( "sourceContent", sourceContent ); 397 XdocParser xdocParser = new XdocParser(); 398 xdocParser.setSecondParsing( true ); 399 macroParameters.put( "parser", xdocParser ); 400 401 MacroRequest request = new MacroRequest( macroParameters, getBasedir() ); 402 403 try 404 { 405 executeMacro( macroName, request, sink ); 406 } 407 catch ( MacroNotFoundException me ) 408 { 409 throw new MacroExecutionException( "Macro not found: " + macroName, me ); 410 } 411 } 412 } 413 414 // Reinit macro 415 macroName = null; 416 macroParameters = null; 417 } 418 419 private void handleMacroStart( XmlPullParser parser ) 420 throws MacroExecutionException 421 { 422 if ( !isSecondParsing() ) 423 { 424 macroName = parser.getAttributeValue( null, Attribute.NAME.toString() ); 425 426 if ( macroParameters == null ) 427 { 428 macroParameters = new HashMap<String, Object>(); 429 } 430 431 if ( StringUtils.isEmpty( macroName ) ) 432 { 433 throw new MacroExecutionException( 434 "The '" + Attribute.NAME.toString() + "' attribute for the '" + MACRO_TAG.toString() 435 + "' tag is required." ); 436 } 437 } 438 } 439 440 private void handleMetaStart( XmlPullParser parser, Sink sink, SinkEventAttributeSet attribs ) 441 { 442 String name = parser.getAttributeValue( null, Attribute.NAME.toString() ); 443 String content = parser.getAttributeValue( null, Attribute.CONTENT.toString() ); 444 445 if ( "author".equals( name ) ) 446 { 447 sink.author( null ); 448 sink.text( content ); 449 sink.author_(); 450 } 451 else if ( "date".equals( name ) ) 452 { 453 sink.date( null ); 454 sink.text( content ); 455 sink.date_(); 456 } 457 else 458 { 459 sink.unknown( "meta", new Object[]{ Integer.valueOf( TAG_TYPE_SIMPLE ) }, attribs ); 460 } 461 } 462 463 private void handleParamStart( XmlPullParser parser, Sink sink ) 464 throws MacroExecutionException 465 { 466 if ( !isSecondParsing() ) 467 { 468 if ( StringUtils.isNotEmpty( macroName ) ) 469 { 470 String paramName = parser.getAttributeValue( null, Attribute.NAME.toString() ); 471 String paramValue = parser.getAttributeValue( null, Attribute.VALUE.toString() ); 472 473 if ( StringUtils.isEmpty( paramName ) || StringUtils.isEmpty( paramValue ) ) 474 { 475 throw new MacroExecutionException( 476 "'" + Attribute.NAME.toString() + "' and '" + Attribute.VALUE.toString() 477 + "' attributes for the '" + PARAM.toString() + "' tag are required inside the '" 478 + MACRO_TAG.toString() + "' tag." ); 479 } 480 481 macroParameters.put( paramName, paramValue ); 482 } 483 else 484 { 485 // param tag from non-macro object, see MSITE-288 486 handleUnknown( parser, sink, TAG_TYPE_START ); 487 } 488 } 489 } 490 491 private void handleSectionStart( int level, Sink sink, SinkEventAttributeSet attribs, XmlPullParser parser ) 492 { 493 consecutiveSections( level, sink ); 494 495 Object id = attribs.getAttribute( Attribute.ID.toString() ); 496 497 if ( id != null ) 498 { 499 sink.anchor( id.toString() ); 500 sink.anchor_(); 501 } 502 503 sink.section( level, attribs ); 504 sink.sectionTitle( level, null ); 505 sink.text( HtmlTools.unescapeHTML( parser.getAttributeValue( null, Attribute.NAME.toString() ) ) ); 506 sink.sectionTitle_( level ); 507 } 508 509 /** 510 * Open missing h4, h5, h6 sections. 511 */ 512 private void openMissingSections( int newLevel, Sink sink ) 513 { 514 while ( getSectionLevel() < newLevel - 1 ) 515 { 516 setSectionLevel( getSectionLevel() + 1 ); 517 518 if ( getSectionLevel() == Sink.SECTION_LEVEL_5 ) 519 { 520 sink.section5(); 521 } 522 else if ( getSectionLevel() == Sink.SECTION_LEVEL_4 ) 523 { 524 sink.section4(); 525 } 526 else if ( getSectionLevel() == Sink.SECTION_LEVEL_3 ) 527 { 528 sink.section3(); 529 } 530 else if ( getSectionLevel() == Sink.SECTION_LEVEL_2 ) 531 { 532 sink.section2(); 533 } 534 } 535 } 536}