View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.module.xdoc;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  import javax.swing.text.html.HTML.Attribute;
24  
25  import java.io.IOException;
26  import java.io.Reader;
27  import java.io.StringReader;
28  import java.io.StringWriter;
29  import java.util.HashMap;
30  import java.util.Map;
31  
32  import org.apache.commons.io.IOUtils;
33  import org.apache.maven.doxia.macro.MacroExecutionException;
34  import org.apache.maven.doxia.macro.MacroRequest;
35  import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
36  import org.apache.maven.doxia.parser.ParseException;
37  import org.apache.maven.doxia.parser.Xhtml1BaseParser;
38  import org.apache.maven.doxia.sink.Sink;
39  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
40  import org.apache.maven.doxia.util.HtmlTools;
41  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
42  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
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  @Singleton
53  @Named("xdoc")
54  public class XdocParser extends Xhtml1BaseParser implements XdocMarkup {
55      private static final Logger LOGGER = LoggerFactory.getLogger(XdocParser.class);
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) throws ParseException {
89          this.sourceContent = null;
90  
91          try (Reader reader = source) {
92              StringWriter contentWriter = new StringWriter();
93              IOUtils.copy(reader, contentWriter);
94              sourceContent = contentWriter.toString();
95          } catch (IOException ex) {
96              throw new ParseException("Error reading the input source", ex);
97          }
98  
99          // 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     /** {@inheritDoc} */
110     protected void handleStartTag(XmlPullParser parser, Sink sink)
111             throws XmlPullParserException, MacroExecutionException {
112         isEmptyElement = parser.isEmptyElementTag();
113 
114         SinkEventAttributeSet attribs = getAttributesFromParser(parser);
115 
116         if (parser.getName().equals(DOCUMENT_TAG.toString())) {
117             // Do nothing
118             return;
119         } else if (parser.getName().equals(HEAD.toString())) {
120             if (!inHead) // we might be in head from a <properties> already
121             {
122                 this.inHead = true;
123 
124                 sink.head(attribs);
125             }
126         } else if (parser.getName().equals(TITLE.toString())) {
127             if (hasTitle) {
128                 LOGGER.warn("<title> was already defined in <properties>, ignored <title> in <head>.");
129 
130                 try {
131                     parser.nextText(); // ignore next text event
132                 } catch (IOException ex) {
133                     throw new XmlPullParserException("Failed to parse text", parser, ex);
134                 }
135             } else {
136                 sink.title(attribs);
137             }
138         } else if (parser.getName().equals(AUTHOR_TAG.toString())) {
139             sink.author(attribs);
140         } else if (parser.getName().equals(DATE_TAG.toString())) {
141             sink.date(attribs);
142         } else if (parser.getName().equals(META.toString())) {
143             handleMetaStart(parser, sink, attribs);
144         } else if (parser.getName().equals(BODY.toString())) {
145             if (inHead) {
146                 sink.head_();
147                 this.inHead = false;
148             }
149 
150             sink.body(attribs);
151         } else if (parser.getName().equals(SECTION_TAG.toString())) {
152             handleSectionStart(Sink.SECTION_LEVEL_1, sink, attribs, parser);
153         } else if (parser.getName().equals(SUBSECTION_TAG.toString())) {
154             handleSectionStart(Sink.SECTION_LEVEL_2, sink, attribs, parser);
155         } else if (parser.getName().equals(SOURCE_TAG.toString())) {
156             verbatim();
157 
158             attribs.addAttributes(SinkEventAttributeSet.SOURCE);
159 
160             sink.verbatim(attribs);
161         } else if (parser.getName().equals(PROPERTIES_TAG.toString())) {
162             if (!inHead) // we might be in head from a <head> already
163             {
164                 this.inHead = true;
165 
166                 sink.head(attribs);
167             }
168         }
169 
170         // ----------------------------------------------------------------------
171         // Macro
172         // ----------------------------------------------------------------------
173 
174         else if (parser.getName().equals(MACRO_TAG.toString())) {
175             handleMacroStart(parser);
176         } else if (parser.getName().equals(PARAM.toString())) {
177             handleParamStart(parser, sink);
178         } else if (!baseStartTag(parser, sink)) {
179             if (isEmptyElement) {
180                 handleUnknown(parser, sink, TAG_TYPE_SIMPLE);
181             } else {
182                 handleUnknown(parser, sink, TAG_TYPE_START);
183             }
184 
185             LOGGER.warn(
186                     "Unrecognized xdoc tag <{}> at [{}:{}]",
187                     parser.getName(),
188                     parser.getLineNumber(),
189                     parser.getColumnNumber());
190         }
191     }
192 
193     /** {@inheritDoc} */
194     protected void handleEndTag(XmlPullParser parser, Sink sink)
195             throws XmlPullParserException, MacroExecutionException {
196         if (parser.getName().equals(DOCUMENT_TAG.toString())) {
197             // Do nothing
198             return;
199         } else if (parser.getName().equals(HEAD.toString())) {
200             // Do nothing, head is closed with BODY start.
201         } else if (parser.getName().equals(BODY.toString())) {
202             consecutiveSections(0, sink);
203 
204             sink.body_();
205         } else if (parser.getName().equals(TITLE.toString())) {
206             if (!hasTitle) {
207                 sink.title_();
208                 this.hasTitle = true;
209             }
210         } else if (parser.getName().equals(AUTHOR_TAG.toString())) {
211             sink.author_();
212         } else if (parser.getName().equals(DATE_TAG.toString())) {
213             sink.date_();
214         } else if (parser.getName().equals(SOURCE_TAG.toString())) {
215             verbatim_();
216 
217             sink.verbatim_();
218         } else if (parser.getName().equals(PROPERTIES_TAG.toString())) {
219             // Do nothing, head is closed with BODY start.
220         } else if (parser.getName().equals(MACRO_TAG.toString())) {
221             handleMacroEnd(sink);
222         } else if (parser.getName().equals(PARAM.toString())) {
223             if (!(macroName != null && !macroName.isEmpty())) {
224                 handleUnknown(parser, sink, TAG_TYPE_END);
225             }
226         } else if (parser.getName().equals(SECTION_TAG.toString())) {
227             consecutiveSections(0, sink);
228 
229             sink.section1_();
230         } else if (parser.getName().equals(SUBSECTION_TAG.toString())) {
231             consecutiveSections(Sink.SECTION_LEVEL_1, sink);
232 
233             // sink.section2_() not necessary
234         } else if (!baseEndTag(parser, sink)) {
235             if (!isEmptyElement) {
236                 handleUnknown(parser, sink, TAG_TYPE_END);
237             }
238         }
239 
240         isEmptyElement = false;
241     }
242 
243     protected void consecutiveSections(int newLevel, Sink sink) {
244         closeOpenSections(newLevel, sink);
245         openMissingSections(newLevel, sink);
246 
247         setSectionLevel(newLevel);
248     }
249 
250     /**
251      * {@inheritDoc}
252      */
253     protected void init() {
254         super.init();
255 
256         this.isEmptyElement = false;
257         this.macroName = null;
258         this.macroParameters = null;
259         this.inHead = false;
260         this.hasTitle = false;
261     }
262 
263     /**
264      * Close open h2, h3, h4, h5 sections.
265      */
266     private void closeOpenSections(int newLevel, Sink sink) {
267         while (getSectionLevel() >= newLevel) {
268             if (getSectionLevel() > Sink.SECTION_LEVEL_1) {
269                 sink.section_(getSectionLevel());
270             }
271 
272             setSectionLevel(getSectionLevel() - 1);
273         }
274     }
275 
276     private void handleMacroEnd(Sink sink) throws MacroExecutionException {
277         if (!isSecondParsing() && (macroName != null && !macroName.isEmpty())) {
278             MacroRequest request = new MacroRequest(sourceContent, new XdocParser(), macroParameters, getBasedir());
279 
280             try {
281                 executeMacro(macroName, request, sink);
282             } catch (MacroNotFoundException me) {
283                 throw new MacroExecutionException("Macro not found: " + macroName, me);
284             }
285         }
286 
287         // Reinit macro
288         macroName = null;
289         macroParameters = null;
290     }
291 
292     private void handleMacroStart(XmlPullParser parser) throws MacroExecutionException {
293         if (!isSecondParsing()) {
294             macroName = parser.getAttributeValue(null, Attribute.NAME.toString());
295 
296             if (macroParameters == null) {
297                 macroParameters = new HashMap<>();
298             }
299 
300             if (macroName == null || macroName.isEmpty()) {
301                 throw new MacroExecutionException("The '" + Attribute.NAME.toString() + "' attribute for the '"
302                         + MACRO_TAG.toString() + "' tag is required.");
303             }
304         }
305     }
306 
307     private void handleMetaStart(XmlPullParser parser, Sink sink, SinkEventAttributeSet attribs) {
308         String name = parser.getAttributeValue(null, Attribute.NAME.toString());
309         String content = parser.getAttributeValue(null, Attribute.CONTENT.toString());
310 
311         if ("author".equals(name)) {
312             sink.author(null);
313             sink.text(content);
314             sink.author_();
315         } else if ("date".equals(name)) {
316             sink.date(null);
317             sink.text(content);
318             sink.date_();
319         } else {
320             sink.unknown("meta", new Object[] {TAG_TYPE_SIMPLE}, attribs);
321         }
322     }
323 
324     private void handleParamStart(XmlPullParser parser, Sink sink) throws MacroExecutionException {
325         if (!isSecondParsing()) {
326             if (macroName != null && !macroName.isEmpty()) {
327                 String paramName = parser.getAttributeValue(null, Attribute.NAME.toString());
328                 String paramValue = parser.getAttributeValue(null, Attribute.VALUE.toString());
329 
330                 if ((paramName == null || paramName.isEmpty()) || (paramValue == null || paramValue.isEmpty())) {
331                     throw new MacroExecutionException(
332                             "'" + Attribute.NAME.toString() + "' and '" + Attribute.VALUE.toString()
333                                     + "' attributes for the '" + PARAM.toString() + "' tag are required inside the '"
334                                     + MACRO_TAG.toString() + "' tag.");
335                 }
336 
337                 macroParameters.put(paramName, paramValue);
338             } else {
339                 // param tag from non-macro object, see MSITE-288
340                 handleUnknown(parser, sink, TAG_TYPE_START);
341             }
342         }
343     }
344 
345     private void handleSectionStart(int level, Sink sink, SinkEventAttributeSet attribs, XmlPullParser parser) {
346         consecutiveSections(level, sink);
347 
348         Object id = attribs.getAttribute(Attribute.ID.toString());
349 
350         if (id != null) {
351             sink.anchor(id.toString());
352             sink.anchor_();
353         }
354 
355         sink.section(level, attribs);
356         sink.sectionTitle(level, null);
357         sink.text(HtmlTools.unescapeHTML(parser.getAttributeValue(null, Attribute.NAME.toString())));
358         sink.sectionTitle_(level);
359     }
360 
361     /**
362      * Open missing h2, h3, h4, h5 sections.
363      */
364     private void openMissingSections(int newLevel, Sink sink) {
365         while (getSectionLevel() < newLevel - 1) {
366             setSectionLevel(getSectionLevel() + 1);
367 
368             if (getSectionLevel() == Sink.SECTION_LEVEL_5) {
369                 sink.section5();
370             } else if (getSectionLevel() == Sink.SECTION_LEVEL_4) {
371                 sink.section4();
372             } else if (getSectionLevel() == Sink.SECTION_LEVEL_3) {
373                 sink.section3();
374             } else if (getSectionLevel() == Sink.SECTION_LEVEL_2) {
375                 sink.section2();
376             }
377         }
378     }
379 }