View Javadoc

1   package org.apache.maven.doxia.module.xdoc;
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  import java.util.HashMap;
27  import java.util.Map;
28  
29  import javax.swing.text.html.HTML.Attribute;
30  
31  import org.apache.maven.doxia.macro.MacroExecutionException;
32  import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
33  import org.apache.maven.doxia.macro.MacroRequest;
34  import org.apache.maven.doxia.parser.ParseException;
35  import org.apache.maven.doxia.parser.XhtmlBaseParser;
36  import org.apache.maven.doxia.sink.Sink;
37  import org.apache.maven.doxia.sink.SinkEventAttributeSet;
38  import org.apache.maven.doxia.util.HtmlTools;
39  
40  import org.codehaus.plexus.util.IOUtil;
41  import org.codehaus.plexus.util.StringUtils;
42  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
43  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
44  
45  /**
46   * Parse an xdoc model and emit events into the specified doxia Sink.
47   *
48   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
49   * @version $Id: XdocParser.java 1185529 2011-10-18 08:27:44Z ltheussl $
50   * @since 1.0
51   * @plexus.component role="org.apache.maven.doxia.parser.Parser" role-hint="xdoc"
52   */
53  public class XdocParser
54      extends XhtmlBaseParser
55      implements XdocMarkup
56  {
57      /** The source content of the input reader. Used to pass into macros. */
58      private String sourceContent;
59  
60      /** Empty elements don't write a closing tag. */
61      private boolean isEmptyElement;
62  
63      /** A macro name. */
64      private String macroName;
65  
66      /** The macro parameters. */
67      private Map<String, Object> macroParameters = new HashMap<String, Object>();
68  
69      /** Indicates that we're inside &lt;properties&gt; or &lt;head&gt;.*/
70      private boolean inHead;
71  
72      /** Indicates that &lt;title&gt; was called from &lt;properties&gt; or &lt;head&gt;.*/
73      private boolean hasTitle;
74  
75      /** {@inheritDoc} */
76      public void parse( Reader source, Sink sink )
77          throws ParseException
78      {
79          this.sourceContent = null;
80          init();
81  
82          try
83          {
84              StringWriter contentWriter = new StringWriter();
85              IOUtil.copy( source, contentWriter );
86              sourceContent = contentWriter.toString();
87          }
88          catch ( IOException ex )
89          {
90              throw new ParseException( "Error reading the input source: " + ex.getMessage(), ex );
91          }
92          finally
93          {
94              IOUtil.close( source );
95          }
96  
97          Reader tmp = new StringReader( sourceContent );
98  
99          // leave this at default (false) until everything is properly implemented, see DOXIA-226
100         //setIgnorableWhitespace( true );
101 
102         try
103         {
104             super.parse( tmp, sink );
105         }
106         finally
107         {
108             this.sourceContent = null;
109 
110             setSecondParsing( false );
111             init();
112         }
113     }
114 
115     /** {@inheritDoc} */
116     protected void handleStartTag( XmlPullParser parser, Sink sink )
117         throws XmlPullParserException, MacroExecutionException
118     {
119         isEmptyElement = parser.isEmptyElementTag();
120 
121         SinkEventAttributeSet attribs = getAttributesFromParser( parser );
122 
123         if ( parser.getName().equals( DOCUMENT_TAG.toString() ) )
124         {
125             //Do nothing
126             return;
127         }
128         else if ( parser.getName().equals( HEAD.toString() ) )
129         {
130             if ( !inHead ) // we might be in head from a <properties> already
131             {
132                 this.inHead = true;
133 
134                 sink.head( attribs );
135             }
136         }
137         else if ( parser.getName().equals( TITLE.toString() ) )
138         {
139             if ( hasTitle )
140             {
141                 getLog().warn( "<title> was already defined in <properties>, ignored <title> in <head>." );
142 
143                 try
144                 {
145                     parser.nextText(); // ignore next text event
146                 }
147                 catch ( IOException ex )
148                 {
149                     throw new XmlPullParserException( "Failed to parse text", parser, ex );
150                 }
151             }
152             else
153             {
154                 sink.title( attribs );
155             }
156         }
157         else if ( parser.getName().equals( AUTHOR_TAG.toString() ) )
158         {
159             sink.author( attribs );
160         }
161         else if ( parser.getName().equals( DATE_TAG.toString() ) )
162         {
163             sink.date( attribs );
164         }
165         else if ( parser.getName().equals( META.toString() ) )
166         {
167             handleMetaStart( parser, sink, attribs );
168         }
169         else if ( parser.getName().equals( BODY.toString() ) )
170         {
171             if ( inHead )
172             {
173                 sink.head_();
174                 this.inHead = false;
175             }
176 
177             sink.body( attribs );
178         }
179         else if ( parser.getName().equals( SECTION_TAG.toString() ) )
180         {
181             handleSectionStart( Sink.SECTION_LEVEL_1, sink, attribs, parser );
182         }
183         else if ( parser.getName().equals( SUBSECTION_TAG.toString() ) )
184         {
185             handleSectionStart( Sink.SECTION_LEVEL_2, sink, attribs, parser );
186         }
187         else if ( parser.getName().equals( SOURCE_TAG.toString() ) )
188         {
189             verbatim();
190 
191             attribs.addAttributes( SinkEventAttributeSet.BOXED );
192 
193             sink.verbatim( attribs );
194         }
195         else if ( parser.getName().equals( PROPERTIES_TAG.toString() ) )
196         {
197             if ( !inHead ) // we might be in head from a <head> already
198             {
199                 this.inHead = true;
200 
201                 sink.head( attribs );
202             }
203         }
204 
205         // ----------------------------------------------------------------------
206         // Macro
207         // ----------------------------------------------------------------------
208 
209         else if ( parser.getName().equals( MACRO_TAG.toString() ) )
210         {
211             handleMacroStart( parser );
212         }
213         else if ( parser.getName().equals( PARAM.toString() ) )
214         {
215             handleParamStart( parser, sink );
216         }
217         else if ( !baseStartTag( parser, sink ) )
218         {
219             if ( isEmptyElement )
220             {
221                 handleUnknown( parser, sink, TAG_TYPE_SIMPLE );
222             }
223             else
224             {
225                 handleUnknown( parser, sink, TAG_TYPE_START );
226             }
227 
228             if ( getLog().isDebugEnabled() )
229             {
230                 String position = "[" + parser.getLineNumber() + ":"
231                     + parser.getColumnNumber() + "]";
232                 String tag = "<" + parser.getName() + ">";
233 
234                 getLog().debug( "Unrecognized xdoc tag: " + tag + " at " + position );
235             }
236         }
237     }
238 
239     /** {@inheritDoc} */
240     protected void handleEndTag( XmlPullParser parser, Sink sink )
241         throws XmlPullParserException, MacroExecutionException
242     {
243         if ( parser.getName().equals( DOCUMENT_TAG.toString() ) )
244         {
245             //Do nothing
246             return;
247         }
248         else if ( parser.getName().equals( HEAD.toString() ) )
249         {
250             //Do nothing, head is closed with BODY start.
251         }
252         else if ( parser.getName().equals( BODY.toString() ) )
253         {
254             consecutiveSections( 0, sink );
255 
256             sink.body_();
257         }
258         else if ( parser.getName().equals( TITLE.toString() ) )
259         {
260             if ( !hasTitle )
261             {
262                 sink.title_();
263                 this.hasTitle = true;
264             }
265         }
266         else if ( parser.getName().equals( AUTHOR_TAG.toString() ) )
267         {
268             sink.author_();
269         }
270         else if ( parser.getName().equals( DATE_TAG.toString() ) )
271         {
272             sink.date_();
273         }
274         else if ( parser.getName().equals( SOURCE_TAG.toString() ) )
275         {
276             verbatim_();
277 
278             sink.verbatim_();
279         }
280         else if ( parser.getName().equals( PROPERTIES_TAG.toString() ) )
281         {
282             //Do nothing, head is closed with BODY start.
283         }
284         else if ( parser.getName().equals( MACRO_TAG.toString() ) )
285         {
286             handleMacroEnd( sink );
287         }
288         else if ( parser.getName().equals( PARAM.toString() ) )
289         {
290             if ( !StringUtils.isNotEmpty( macroName ) )
291             {
292                 handleUnknown( parser, sink, TAG_TYPE_END );
293             }
294         }
295         else if ( parser.getName().equals( SECTION_TAG.toString() ) )
296         {
297             consecutiveSections( 0, sink );
298 
299             sink.section1_();
300         }
301         else if ( parser.getName().equals( SUBSECTION_TAG.toString() ) )
302         {
303             consecutiveSections( Sink.SECTION_LEVEL_1, sink );
304         }
305         else if ( !baseEndTag( parser, sink ) )
306         {
307             if ( !isEmptyElement )
308             {
309                 handleUnknown( parser, sink, TAG_TYPE_END );
310             }
311         }
312 
313         isEmptyElement = false;
314     }
315 
316     /** {@inheritDoc} */
317     protected void consecutiveSections( int newLevel, Sink sink )
318     {
319         closeOpenSections( newLevel, sink );
320         openMissingSections( newLevel, sink );
321 
322         setSectionLevel( newLevel );
323     }
324 
325     /** {@inheritDoc} */
326     protected void init()
327     {
328         super.init();
329 
330         this.isEmptyElement = false;
331         this.macroName = null;
332         this.macroParameters = null;
333         this.inHead = false;
334         this.hasTitle = false;
335     }
336 
337     /**
338      * Close open h4, h5, h6 sections.
339      */
340     private void closeOpenSections( int newLevel, Sink sink )
341     {
342         while ( getSectionLevel() >= newLevel )
343         {
344             if ( getSectionLevel() == Sink.SECTION_LEVEL_5 )
345             {
346                 sink.section5_();
347             }
348             else if ( getSectionLevel() == Sink.SECTION_LEVEL_4 )
349             {
350                 sink.section4_();
351             }
352             else if ( getSectionLevel() == Sink.SECTION_LEVEL_3 )
353             {
354                 sink.section3_();
355             }
356             else if ( getSectionLevel() == Sink.SECTION_LEVEL_2 )
357             {
358                 sink.section2_();
359             }
360 
361             setSectionLevel( getSectionLevel() - 1 );
362         }
363     }
364 
365     private void handleMacroEnd( Sink sink )
366             throws MacroExecutionException
367     {
368         if ( !isSecondParsing() )
369         {
370             if ( StringUtils.isNotEmpty( macroName ) )
371             {
372                 // TODO handles specific macro attributes
373                 macroParameters.put( "sourceContent", sourceContent );
374                 XdocParser xdocParser = new XdocParser();
375                 xdocParser.setSecondParsing( true );
376                 macroParameters.put( "parser", xdocParser );
377 
378                 MacroRequest request = new MacroRequest( macroParameters, getBasedir() );
379 
380                 try
381                 {
382                     executeMacro( macroName, request, sink );
383                 } catch ( MacroNotFoundException me )
384                 {
385                     throw new MacroExecutionException( "Macro not found: " + macroName, me );
386                 }
387             }
388         }
389 
390         // Reinit macro
391         macroName = null;
392         macroParameters = null;
393     }
394 
395     private void handleMacroStart( XmlPullParser parser )
396             throws MacroExecutionException
397     {
398         if ( !isSecondParsing() )
399         {
400             macroName = parser.getAttributeValue( null, Attribute.NAME.toString() );
401 
402             if ( macroParameters == null )
403             {
404                 macroParameters = new HashMap<String, Object>();
405             }
406 
407             if ( StringUtils.isEmpty( macroName ) )
408             {
409                 throw new MacroExecutionException( "The '" + Attribute.NAME.toString()
410                         + "' attribute for the '" + MACRO_TAG.toString() + "' tag is required." );
411             }
412         }
413     }
414 
415     private void handleMetaStart( XmlPullParser parser, Sink sink, SinkEventAttributeSet attribs )
416     {
417         String name = parser.getAttributeValue( null, Attribute.NAME.toString() );
418         String content = parser.getAttributeValue( null, Attribute.CONTENT.toString() );
419 
420         if ( "author".equals( name ) )
421         {
422             sink.author( null );
423             sink.text( content );
424             sink.author_();
425         }
426         else if ( "date".equals( name ) )
427         {
428             sink.date( null );
429             sink.text( content );
430             sink.date_();
431         }
432         else
433         {
434             sink.unknown( "meta", new Object[] {new Integer( TAG_TYPE_SIMPLE )}, attribs );
435         }
436     }
437 
438     private void handleParamStart( XmlPullParser parser, Sink sink )
439             throws MacroExecutionException
440     {
441         if ( !isSecondParsing() )
442         {
443             if ( StringUtils.isNotEmpty( macroName ) )
444             {
445                 String paramName = parser.getAttributeValue( null, Attribute.NAME.toString() );
446                 String paramValue = parser.getAttributeValue( null,
447                         Attribute.VALUE.toString() );
448 
449                 if ( StringUtils.isEmpty( paramName ) || StringUtils.isEmpty( paramValue ) )
450                 {
451                     throw new MacroExecutionException( "'" + Attribute.NAME.toString()
452                             + "' and '" + Attribute.VALUE.toString() + "' attributes for the '" + PARAM.toString()
453                             + "' tag are required inside the '" + MACRO_TAG.toString() + "' tag." );
454                 }
455 
456                 macroParameters.put( paramName, paramValue );
457             }
458             else
459             {
460                 // param tag from non-macro object, see MSITE-288
461                 handleUnknown( parser, sink, TAG_TYPE_START );
462             }
463         }
464     }
465 
466     private void handleSectionStart( int level, Sink sink, SinkEventAttributeSet attribs, XmlPullParser parser )
467     {
468         consecutiveSections( level, sink );
469 
470         Object id = attribs.getAttribute( Attribute.ID.toString() );
471 
472         if ( id != null )
473         {
474             sink.anchor( id.toString() );
475             sink.anchor_();
476         }
477 
478         sink.section( level, attribs );
479         sink.sectionTitle( level, null );
480         sink.text( HtmlTools.unescapeHTML( parser.getAttributeValue( null, Attribute.NAME.toString() ) ) );
481         sink.sectionTitle_( level );
482     }
483 
484     /**
485      * Open missing h4, h5, h6 sections.
486      */
487     private void openMissingSections( int newLevel, Sink sink )
488     {
489         while ( getSectionLevel() < newLevel - 1 )
490         {
491             setSectionLevel( getSectionLevel() + 1 );
492 
493             if ( getSectionLevel() == Sink.SECTION_LEVEL_5 )
494             {
495                 sink.section5();
496             }
497             else if ( getSectionLevel() == Sink.SECTION_LEVEL_4 )
498             {
499                 sink.section4();
500             }
501             else if ( getSectionLevel() == Sink.SECTION_LEVEL_3 )
502             {
503                 sink.section3();
504             }
505             else if ( getSectionLevel() == Sink.SECTION_LEVEL_2 )
506             {
507                 sink.section2();
508             }
509         }
510     }
511 }