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.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        init();
097
098        try
099        {
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}