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.LinkedHashMap;
31  import java.util.Map;
32  
33  import org.apache.commons.io.IOUtils;
34  import org.apache.maven.doxia.macro.MacroExecutionException;
35  import org.apache.maven.doxia.macro.MacroRequest;
36  import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
37  import org.apache.maven.doxia.parser.ParseException;
38  import org.apache.maven.doxia.parser.Xhtml1BaseParser;
39  import org.apache.maven.doxia.sink.Sink;
40  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
41  import org.apache.maven.doxia.util.HtmlTools;
42  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
43  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  /**
48   * Parse an xdoc model and emit events into the specified doxia Sink.
49   *
50   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
51   * @since 1.0
52   */
53  @Singleton
54  @Named("xdoc")
55  public class XdocParser extends Xhtml1BaseParser implements XdocMarkup {
56      private static final Logger LOGGER = LoggerFactory.getLogger(XdocParser.class);
57  
58      /**
59       * The source content of the input reader. Used to pass into macros.
60       */
61      private String sourceContent;
62  
63      /**
64       * Empty elements don't write a closing tag.
65       */
66      private boolean isEmptyElement;
67  
68      /**
69       * A macro name.
70       */
71      private String macroName;
72  
73      /**
74       * The macro parameters.
75       */
76      private Map<String, Object> macroParameters = new LinkedHashMap<>();
77  
78      /**
79       * Indicates that we're inside &lt;properties&gt; or &lt;head&gt;.
80       */
81      private boolean inHead;
82  
83      /**
84       * Indicates that &lt;title&gt; was called from &lt;properties&gt; or &lt;head&gt;.
85       */
86      private boolean hasTitle;
87  
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     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 }