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