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 &lt;properties&gt; or &lt;head&gt;.
081     */
082    private boolean inHead;
083
084    /**
085     * Indicates that &lt;title&gt; was called from &lt;properties&gt; or &lt;head&gt;.
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}