001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.module.xdoc;
020
021import javax.inject.Named;
022import javax.inject.Singleton;
023import javax.swing.text.html.HTML.Attribute;
024
025import java.io.IOException;
026import java.io.Reader;
027import java.io.StringReader;
028import java.io.StringWriter;
029import java.util.HashMap;
030import java.util.LinkedHashMap;
031import java.util.Map;
032
033import org.apache.commons.io.IOUtils;
034import org.apache.maven.doxia.macro.MacroExecutionException;
035import org.apache.maven.doxia.macro.MacroRequest;
036import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
037import org.apache.maven.doxia.parser.ParseException;
038import org.apache.maven.doxia.parser.Xhtml1BaseParser;
039import org.apache.maven.doxia.sink.Sink;
040import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
041import org.apache.maven.doxia.util.HtmlTools;
042import org.codehaus.plexus.util.xml.pull.XmlPullParser;
043import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
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 * @since 1.0
052 */
053@Singleton
054@Named("xdoc")
055public class XdocParser extends Xhtml1BaseParser implements XdocMarkup {
056    private static final Logger LOGGER = LoggerFactory.getLogger(XdocParser.class);
057
058    /**
059     * The source content of the input reader. Used to pass into macros.
060     */
061    private String sourceContent;
062
063    /**
064     * Empty elements don't write a closing tag.
065     */
066    private boolean isEmptyElement;
067
068    /**
069     * A macro name.
070     */
071    private String macroName;
072
073    /**
074     * The macro parameters.
075     */
076    private Map<String, Object> macroParameters = new LinkedHashMap<>();
077
078    /**
079     * Indicates that we're inside &lt;properties&gt; or &lt;head&gt;.
080     */
081    private boolean inHead;
082
083    /**
084     * Indicates that &lt;title&gt; was called from &lt;properties&gt; or &lt;head&gt;.
085     */
086    private boolean hasTitle;
087
088    public void parse(Reader source, Sink sink, String reference) throws ParseException {
089        this.sourceContent = null;
090
091        try (Reader reader = source) {
092            StringWriter contentWriter = new StringWriter();
093            IOUtils.copy(reader, contentWriter);
094            sourceContent = contentWriter.toString();
095        } catch (IOException ex) {
096            throw new ParseException("Error reading the input source", ex);
097        }
098
099        // leave this at default (false) until everything is properly implemented, see DOXIA-226
100        // setIgnorableWhitespace(true);
101
102        try {
103            super.parse(new StringReader(sourceContent), sink, reference);
104        } finally {
105            this.sourceContent = null;
106        }
107    }
108
109    protected void handleStartTag(XmlPullParser parser, Sink sink)
110            throws XmlPullParserException, MacroExecutionException {
111        isEmptyElement = parser.isEmptyElementTag();
112        isBeginningOfLineInsideBlock = true;
113        SinkEventAttributeSet attribs = getAttributesFromParser(parser);
114
115        if (parser.getName().equals(DOCUMENT_TAG.toString())) {
116            // Do nothing
117            return;
118        } else if (parser.getName().equals(HEAD.toString())) {
119            if (!inHead) // we might be in head from a <properties> already
120            {
121                this.inHead = true;
122
123                sink.head(attribs);
124            }
125        } else if (parser.getName().equals(TITLE.toString())) {
126            if (hasTitle) {
127                LOGGER.warn("<title> was already defined in <properties>, ignored <title> in <head>.");
128
129                try {
130                    parser.nextText(); // ignore next text event
131                } catch (IOException ex) {
132                    throw new XmlPullParserException("Failed to parse text", parser, ex);
133                }
134            } else {
135                sink.title(attribs);
136            }
137        } else if (parser.getName().equals(AUTHOR_TAG.toString())) {
138            sink.author(attribs);
139        } else if (parser.getName().equals(DATE_TAG.toString())) {
140            sink.date(attribs);
141        } else if (parser.getName().equals(META.toString())) {
142            handleMetaStart(parser, sink, attribs);
143        } else if (parser.getName().equals(BODY.toString())) {
144            if (inHead) {
145                sink.head_();
146                this.inHead = false;
147            }
148            sink.body(attribs);
149        } else if (parser.getName().equals(SECTION_TAG.toString())) {
150            handleSectionStart(Sink.SECTION_LEVEL_1, sink, attribs, parser);
151        } else if (parser.getName().equals(SUBSECTION_TAG.toString())) {
152            handleSectionStart(Sink.SECTION_LEVEL_2, sink, attribs, parser);
153        } else if (parser.getName().equals(SOURCE_TAG.toString())) {
154            verbatim();
155
156            attribs.addAttributes(SinkEventAttributeSet.SOURCE);
157
158            sink.verbatim(attribs);
159        } else if (parser.getName().equals(PROPERTIES_TAG.toString())) {
160            if (!inHead) // we might be in head from a <head> already
161            {
162                this.inHead = true;
163
164                sink.head(attribs);
165            }
166        }
167
168        // ----------------------------------------------------------------------
169        // Macro
170        // ----------------------------------------------------------------------
171
172        else if (parser.getName().equals(MACRO_TAG.toString())) {
173            handleMacroStart(parser);
174        } else if (parser.getName().equals(PARAM.toString())) {
175            handleParamStart(parser, sink);
176        } else if (!baseStartTag(parser, sink)) {
177            if (isEmptyElement) {
178                handleUnknown(parser, sink, TAG_TYPE_SIMPLE);
179            } else {
180                handleUnknown(parser, sink, TAG_TYPE_START);
181            }
182
183            LOGGER.warn(
184                    "Unrecognized xdoc tag <{}> at [{}:{}]",
185                    parser.getName(),
186                    parser.getLineNumber(),
187                    parser.getColumnNumber());
188        }
189    }
190
191    protected void handleEndTag(XmlPullParser parser, Sink sink)
192            throws XmlPullParserException, MacroExecutionException {
193        isBeginningOfLineInsideBlock = true;
194        if (parser.getName().equals(DOCUMENT_TAG.toString())) {
195            // Do nothing
196            return;
197        } else if (parser.getName().equals(HEAD.toString())) {
198            // Do nothing, head is closed with BODY start.
199        } else if (parser.getName().equals(BODY.toString())) {
200            consecutiveSections(0, sink);
201
202            sink.body_();
203        } else if (parser.getName().equals(TITLE.toString())) {
204            if (!hasTitle) {
205                sink.title_();
206                this.hasTitle = true;
207            }
208        } else if (parser.getName().equals(AUTHOR_TAG.toString())) {
209            sink.author_();
210        } else if (parser.getName().equals(DATE_TAG.toString())) {
211            sink.date_();
212        } else if (parser.getName().equals(SOURCE_TAG.toString())) {
213            verbatim_();
214
215            sink.verbatim_();
216        } else if (parser.getName().equals(PROPERTIES_TAG.toString())) {
217            // Do nothing, head is closed with BODY start.
218        } else if (parser.getName().equals(MACRO_TAG.toString())) {
219            handleMacroEnd(sink);
220        } else if (parser.getName().equals(PARAM.toString())) {
221            if (!(macroName != null && !macroName.isEmpty())) {
222                handleUnknown(parser, sink, TAG_TYPE_END);
223            }
224        } else if (parser.getName().equals(SECTION_TAG.toString())) {
225            consecutiveSections(0, sink);
226
227            sink.section1_();
228        } else if (parser.getName().equals(SUBSECTION_TAG.toString())) {
229            consecutiveSections(Sink.SECTION_LEVEL_1, sink);
230
231            // sink.section2_() not necessary
232        } else if (!baseEndTag(parser, sink)) {
233            if (!isEmptyElement) {
234                handleUnknown(parser, sink, TAG_TYPE_END);
235            }
236        }
237
238        isEmptyElement = false;
239    }
240
241    protected void consecutiveSections(int newLevel, Sink sink) {
242        closeOpenSections(newLevel, sink);
243        openMissingSections(newLevel, sink);
244
245        setSectionLevel(newLevel);
246    }
247
248    /**
249     * {@inheritDoc}
250     */
251    protected void init() {
252        super.init();
253
254        this.isEmptyElement = false;
255        this.macroName = null;
256        this.macroParameters = null;
257        this.inHead = false;
258        this.hasTitle = false;
259    }
260
261    /**
262     * Close open h2, h3, h4, h5 sections.
263     */
264    private void closeOpenSections(int newLevel, Sink sink) {
265        while (getSectionLevel() >= newLevel) {
266            if (getSectionLevel() > Sink.SECTION_LEVEL_1) {
267                sink.section_(getSectionLevel());
268            }
269
270            setSectionLevel(getSectionLevel() - 1);
271        }
272    }
273
274    private void handleMacroEnd(Sink sink) throws MacroExecutionException {
275        if (!isSecondParsing() && (macroName != null && !macroName.isEmpty())) {
276            MacroRequest request = new MacroRequest(sourceContent, new XdocParser(), macroParameters, getBasedir());
277
278            try {
279                executeMacro(macroName, request, sink);
280            } catch (MacroNotFoundException me) {
281                throw new MacroExecutionException("Macro not found: " + macroName, me);
282            }
283        }
284
285        // Reinit macro
286        macroName = null;
287        macroParameters = null;
288    }
289
290    private void handleMacroStart(XmlPullParser parser) throws MacroExecutionException {
291        if (!isSecondParsing()) {
292            macroName = parser.getAttributeValue(null, Attribute.NAME.toString());
293
294            if (macroParameters == null) {
295                macroParameters = new HashMap<>();
296            }
297
298            if (macroName == null || macroName.isEmpty()) {
299                throw new MacroExecutionException("The '" + Attribute.NAME.toString() + "' attribute for the '"
300                        + MACRO_TAG.toString() + "' tag is required.");
301            }
302        }
303    }
304
305    private void handleMetaStart(XmlPullParser parser, Sink sink, SinkEventAttributeSet attribs) {
306        String name = parser.getAttributeValue(null, Attribute.NAME.toString());
307        String content = parser.getAttributeValue(null, Attribute.CONTENT.toString());
308
309        if ("author".equals(name)) {
310            sink.author(null);
311            sink.text(content);
312            sink.author_();
313        } else if ("date".equals(name)) {
314            sink.date(null);
315            sink.text(content);
316            sink.date_();
317        } else {
318            sink.unknown("meta", new Object[] {TAG_TYPE_SIMPLE}, attribs);
319        }
320    }
321
322    private void handleParamStart(XmlPullParser parser, Sink sink) throws MacroExecutionException {
323        if (!isSecondParsing()) {
324            if (macroName != null && !macroName.isEmpty()) {
325                String paramName = parser.getAttributeValue(null, Attribute.NAME.toString());
326                String paramValue = parser.getAttributeValue(null, Attribute.VALUE.toString());
327
328                if ((paramName == null || paramName.isEmpty()) || (paramValue == null || paramValue.isEmpty())) {
329                    throw new MacroExecutionException(
330                            "'" + Attribute.NAME.toString() + "' and '" + Attribute.VALUE.toString()
331                                    + "' attributes for the '" + PARAM.toString() + "' tag are required inside the '"
332                                    + MACRO_TAG.toString() + "' tag.");
333                }
334
335                macroParameters.put(paramName, paramValue);
336            } else {
337                // param tag from non-macro object, see MSITE-288
338                handleUnknown(parser, sink, TAG_TYPE_START);
339            }
340        }
341    }
342
343    private void handleSectionStart(int level, Sink sink, SinkEventAttributeSet attribs, XmlPullParser parser) {
344        consecutiveSections(level, sink);
345
346        Object id = attribs.getAttribute(Attribute.ID.toString());
347
348        if (id != null) {
349            sink.anchor(id.toString());
350            sink.anchor_();
351        }
352
353        sink.section(level, attribs);
354        sink.sectionTitle(level, null);
355        sink.text(HtmlTools.unescapeHTML(parser.getAttributeValue(null, Attribute.NAME.toString())));
356        sink.sectionTitle_(level);
357    }
358
359    /**
360     * Open missing h2, h3, h4, h5 sections.
361     */
362    private void openMissingSections(int newLevel, Sink sink) {
363        while (getSectionLevel() < newLevel - 1) {
364            setSectionLevel(getSectionLevel() + 1);
365
366            if (getSectionLevel() == Sink.SECTION_LEVEL_5) {
367                sink.section5();
368            } else if (getSectionLevel() == Sink.SECTION_LEVEL_4) {
369                sink.section4();
370            } else if (getSectionLevel() == Sink.SECTION_LEVEL_3) {
371                sink.section3();
372            } else if (getSectionLevel() == Sink.SECTION_LEVEL_2) {
373                sink.section2();
374            }
375        }
376    }
377}