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.Xhtml5BaseParser;
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 Xhtml5BaseParser 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     /** {@inheritDoc} */
244     protected void consecutiveSections(int newLevel, Sink sink) {
245         closeOpenSections(newLevel, sink);
246         openMissingSections(newLevel, sink);
247 
248         setSectionLevel(newLevel);
249     }
250 
251     /**
252      * {@inheritDoc}
253      */
254     protected void init() {
255         super.init();
256 
257         this.isEmptyElement = false;
258         this.macroName = null;
259         this.macroParameters = null;
260         this.inHead = false;
261         this.hasTitle = false;
262     }
263 
264     /**
265      * Close open h2, h3, h4, h5 sections.
266      */
267     private void closeOpenSections(int newLevel, Sink sink) {
268         while (getSectionLevel() >= newLevel) {
269             if (getSectionLevel() == Sink.SECTION_LEVEL_5) {
270                 sink.section5_();
271             } else if (getSectionLevel() == Sink.SECTION_LEVEL_4) {
272                 sink.section4_();
273             } else if (getSectionLevel() == Sink.SECTION_LEVEL_3) {
274                 sink.section3_();
275             } else if (getSectionLevel() == Sink.SECTION_LEVEL_2) {
276                 sink.section2_();
277             }
278 
279             setSectionLevel(getSectionLevel() - 1);
280         }
281     }
282 
283     private void handleMacroEnd(Sink sink) throws MacroExecutionException {
284         if (!isSecondParsing() && (macroName != null && !macroName.isEmpty())) {
285             MacroRequest request = new MacroRequest(sourceContent, new XdocParser(), macroParameters, getBasedir());
286 
287             try {
288                 executeMacro(macroName, request, sink);
289             } catch (MacroNotFoundException me) {
290                 throw new MacroExecutionException("Macro not found: " + macroName, me);
291             }
292         }
293 
294         // Reinit macro
295         macroName = null;
296         macroParameters = null;
297     }
298 
299     private void handleMacroStart(XmlPullParser parser) throws MacroExecutionException {
300         if (!isSecondParsing()) {
301             macroName = parser.getAttributeValue(null, Attribute.NAME.toString());
302 
303             if (macroParameters == null) {
304                 macroParameters = new HashMap<>();
305             }
306 
307             if (macroName == null || macroName.isEmpty()) {
308                 throw new MacroExecutionException("The '" + Attribute.NAME.toString() + "' attribute for the '"
309                         + MACRO_TAG.toString() + "' tag is required.");
310             }
311         }
312     }
313 
314     private void handleMetaStart(XmlPullParser parser, Sink sink, SinkEventAttributeSet attribs) {
315         String name = parser.getAttributeValue(null, Attribute.NAME.toString());
316         String content = parser.getAttributeValue(null, Attribute.CONTENT.toString());
317 
318         if ("author".equals(name)) {
319             sink.author(null);
320             sink.text(content);
321             sink.author_();
322         } else if ("date".equals(name)) {
323             sink.date(null);
324             sink.text(content);
325             sink.date_();
326         } else {
327             sink.unknown("meta", new Object[] {TAG_TYPE_SIMPLE}, attribs);
328         }
329     }
330 
331     private void handleParamStart(XmlPullParser parser, Sink sink) throws MacroExecutionException {
332         if (!isSecondParsing()) {
333             if (macroName != null && !macroName.isEmpty()) {
334                 String paramName = parser.getAttributeValue(null, Attribute.NAME.toString());
335                 String paramValue = parser.getAttributeValue(null, Attribute.VALUE.toString());
336 
337                 if ((paramName == null || paramName.isEmpty()) || (paramValue == null || paramValue.isEmpty())) {
338                     throw new MacroExecutionException(
339                             "'" + Attribute.NAME.toString() + "' and '" + Attribute.VALUE.toString()
340                                     + "' attributes for the '" + PARAM.toString() + "' tag are required inside the '"
341                                     + MACRO_TAG.toString() + "' tag.");
342                 }
343 
344                 macroParameters.put(paramName, paramValue);
345             } else {
346                 // param tag from non-macro object, see MSITE-288
347                 handleUnknown(parser, sink, TAG_TYPE_START);
348             }
349         }
350     }
351 
352     private void handleSectionStart(int level, Sink sink, SinkEventAttributeSet attribs, XmlPullParser parser) {
353         consecutiveSections(level, sink);
354 
355         Object id = attribs.getAttribute(Attribute.ID.toString());
356 
357         if (id != null) {
358             sink.anchor(id.toString());
359             sink.anchor_();
360         }
361 
362         sink.section(level, attribs);
363         sink.sectionTitle(level, null);
364         sink.text(HtmlTools.unescapeHTML(parser.getAttributeValue(null, Attribute.NAME.toString())));
365         sink.sectionTitle_(level);
366     }
367 
368     /**
369      * Open missing h2, h3, h4, h5 sections.
370      */
371     private void openMissingSections(int newLevel, Sink sink) {
372         while (getSectionLevel() < newLevel - 1) {
373             setSectionLevel(getSectionLevel() + 1);
374 
375             if (getSectionLevel() == Sink.SECTION_LEVEL_5) {
376                 sink.section5();
377             } else if (getSectionLevel() == Sink.SECTION_LEVEL_4) {
378                 sink.section4();
379             } else if (getSectionLevel() == Sink.SECTION_LEVEL_3) {
380                 sink.section3();
381             } else if (getSectionLevel() == Sink.SECTION_LEVEL_2) {
382                 sink.section2();
383             }
384         }
385     }
386 }