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