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.Parser;
36  import org.apache.maven.doxia.parser.XhtmlBaseParser;
37  import org.apache.maven.doxia.sink.Sink;
38  import org.apache.maven.doxia.sink.SinkEventAttributeSet;
39  import org.apache.maven.doxia.util.HtmlTools;
40  
41  import org.codehaus.plexus.component.annotations.Component;
42  import org.codehaus.plexus.util.IOUtil;
43  import org.codehaus.plexus.util.StringUtils;
44  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
45  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
46  
47  /**
48   * Parse an xdoc model and emit events into the specified doxia Sink.
49   *
50   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
51   * @version $Id: XdocParser.java 1465336 2013-04-07 07:39:00Z hboutemy $
52   * @since 1.0
53   */
54  @Component( role = Parser.class, hint = "xdoc" )
55  public class XdocParser
56      extends XhtmlBaseParser
57      implements XdocMarkup
58  {
59      /**
60       * The source content of the input reader. Used to pass into macros.
61       */
62      private String sourceContent;
63  
64      /**
65       * Empty elements don't write a closing tag.
66       */
67      private boolean isEmptyElement;
68  
69      /**
70       * A macro name.
71       */
72      private String macroName;
73  
74      /**
75       * The macro parameters.
76       */
77      private Map<String, Object> macroParameters = new HashMap<String, Object>();
78  
79      /**
80       * Indicates that we're inside &lt;properties&gt; or &lt;head&gt;.
81       */
82      private boolean inHead;
83  
84      /**
85       * Indicates that &lt;title&gt; was called from &lt;properties&gt; or &lt;head&gt;.
86       */
87      private boolean hasTitle;
88  
89      /**
90       * {@inheritDoc}
91       */
92      public void parse( Reader source, Sink sink )
93          throws ParseException
94      {
95          this.sourceContent = null;
96          init();
97  
98          try
99          {
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 }