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