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.impl.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 1726411 2016-01-23 16:34:09Z 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  
97          try
98          {
99              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 }