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.fml;
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.Iterator;
31  import java.util.Map;
32  
33  import org.apache.commons.io.IOUtils;
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.maven.doxia.macro.MacroExecutionException;
36  import org.apache.maven.doxia.macro.MacroRequest;
37  import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
38  import org.apache.maven.doxia.module.fml.model.Faq;
39  import org.apache.maven.doxia.module.fml.model.Faqs;
40  import org.apache.maven.doxia.module.fml.model.Part;
41  import org.apache.maven.doxia.parser.AbstractXmlParser;
42  import org.apache.maven.doxia.parser.ParseException;
43  import org.apache.maven.doxia.sink.Sink;
44  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
45  import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink;
46  import org.apache.maven.doxia.util.DoxiaUtils;
47  import org.apache.maven.doxia.util.HtmlTools;
48  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
49  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  /**
54   * Parse a fml model and emit events into the specified doxia Sink.
55   *
56   * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
57   * @author ltheussl
58   * @since 1.0
59   */
60  @Singleton
61  @Named("fml")
62  public class FmlParser extends AbstractXmlParser implements FmlMarkup {
63      private static final Logger LOGGER = LoggerFactory.getLogger(FmlParser.class);
64  
65      /** Collect a faqs model. */
66      private Faqs faqs;
67  
68      /** Collect a part. */
69      private Part currentPart;
70  
71      /** Collect a single faq. */
72      private Faq currentFaq;
73  
74      /** Used to collect text events. */
75      private StringBuilder buffer;
76  
77      /** The source content of the input reader. Used to pass into macros. */
78      private String sourceContent;
79  
80      /** A macro name. */
81      private String macroName;
82  
83      /** The macro parameters. */
84      private Map<String, Object> macroParameters = new HashMap<>();
85  
86      /** {@inheritDoc} */
87      public void parse(Reader source, Sink sink, String reference) throws ParseException {
88          this.faqs = null;
89          this.sourceContent = null;
90          init();
91  
92          try (Reader reader = source) {
93              StringWriter contentWriter = new StringWriter();
94              IOUtils.copy(reader, contentWriter);
95              sourceContent = contentWriter.toString();
96          } catch (IOException ex) {
97              throw new ParseException("Error reading the input source", ex);
98          }
99  
100         try {
101             Reader tmp = new StringReader(sourceContent);
102 
103             this.faqs = new Faqs();
104 
105             // this populates faqs
106             super.parse(tmp, sink, reference);
107 
108             writeFaqs(getWrappedSink(sink));
109         } finally {
110             this.faqs = null;
111             this.sourceContent = null;
112             setSecondParsing(false);
113             init();
114         }
115     }
116 
117     /** {@inheritDoc} */
118     protected void handleStartTag(XmlPullParser parser, Sink sink)
119             throws XmlPullParserException, MacroExecutionException {
120         if (parser.getName().equals(FAQS_TAG.toString())) {
121             String title = parser.getAttributeValue(null, "title");
122 
123             if (title != null) {
124                 faqs.setTitle(title);
125             }
126 
127             String toplink = parser.getAttributeValue(null, "toplink");
128 
129             if (toplink != null) {
130                 if (toplink.equalsIgnoreCase("true")) {
131                     faqs.setToplink(true);
132                 } else {
133                     faqs.setToplink(false);
134                 }
135             }
136         } else if (parser.getName().equals(PART_TAG.toString())) {
137             currentPart = new Part();
138 
139             currentPart.setId(parser.getAttributeValue(null, Attribute.ID.toString()));
140 
141             if (currentPart.getId() == null) {
142                 throw new XmlPullParserException("id attribute required for <part> at: (" + parser.getLineNumber() + ":"
143                         + parser.getColumnNumber() + ")");
144             } else if (!DoxiaUtils.isValidId(currentPart.getId())) {
145                 String linkAnchor = DoxiaUtils.encodeId(currentPart.getId());
146 
147                 LOGGER.debug("Modified invalid link '{}' to '{}'", currentPart.getId(), linkAnchor);
148 
149                 currentPart.setId(linkAnchor);
150             }
151         } else if (parser.getName().equals(TITLE.toString())) {
152             buffer = new StringBuilder();
153             buffer.append(LESS_THAN).append(parser.getName()).append(GREATER_THAN);
154         } else if (parser.getName().equals(FAQ_TAG.toString())) {
155             currentFaq = new Faq();
156 
157             currentFaq.setId(parser.getAttributeValue(null, Attribute.ID.toString()));
158 
159             if (currentFaq.getId() == null) {
160                 throw new XmlPullParserException("id attribute required for <faq> at: (" + parser.getLineNumber() + ":"
161                         + parser.getColumnNumber() + ")");
162             } else if (!DoxiaUtils.isValidId(currentFaq.getId())) {
163                 String linkAnchor = DoxiaUtils.encodeId(currentFaq.getId());
164 
165                 LOGGER.debug("Modified invalid link '{}' to '{}'", currentFaq.getId(), linkAnchor);
166 
167                 currentFaq.setId(linkAnchor);
168             }
169         } else if (parser.getName().equals(QUESTION_TAG.toString())) {
170             buffer = new StringBuilder();
171             buffer.append(LESS_THAN).append(parser.getName()).append(GREATER_THAN);
172         } else if (parser.getName().equals(ANSWER_TAG.toString())) {
173             buffer = new StringBuilder();
174             buffer.append(LESS_THAN).append(parser.getName()).append(GREATER_THAN);
175 
176         }
177 
178         // ----------------------------------------------------------------------
179         // Macro
180         // ----------------------------------------------------------------------
181 
182         else if (parser.getName().equals(MACRO_TAG.toString())) {
183             handleMacroStart(parser);
184         } else if (parser.getName().equals(PARAM.toString())) {
185             handleParamStart(parser, sink);
186         } else if (buffer != null) {
187             buffer.append(LESS_THAN).append(parser.getName());
188 
189             int count = parser.getAttributeCount();
190 
191             for (int i = 0; i < count; i++) {
192                 buffer.append(SPACE).append(parser.getAttributeName(i));
193 
194                 buffer.append(EQUAL).append(QUOTE);
195 
196                 // TODO: why are attribute values HTML-encoded?
197                 buffer.append(HtmlTools.escapeHTML(parser.getAttributeValue(i)));
198 
199                 buffer.append(QUOTE);
200             }
201 
202             buffer.append(GREATER_THAN);
203         }
204     }
205 
206     /** {@inheritDoc} */
207     protected void handleEndTag(XmlPullParser parser, Sink sink)
208             throws XmlPullParserException, MacroExecutionException {
209         if (parser.getName().equals(FAQS_TAG.toString())) {
210             // Do nothing
211             return;
212         } else if (parser.getName().equals(PART_TAG.toString())) {
213             faqs.addPart(currentPart);
214 
215             currentPart = null;
216         } else if (parser.getName().equals(FAQ_TAG.toString())) {
217             if (currentPart == null) {
218                 throw new XmlPullParserException(
219                         "Missing <part>  at: (" + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")");
220             }
221 
222             currentPart.addFaq(currentFaq);
223 
224             currentFaq = null;
225         } else if (parser.getName().equals(QUESTION_TAG.toString())) {
226             if (currentFaq == null) {
227                 throw new XmlPullParserException(
228                         "Missing <faq> at: (" + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")");
229             }
230 
231             buffer.append(LESS_THAN).append(SLASH).append(parser.getName()).append(GREATER_THAN);
232 
233             currentFaq.setQuestion(buffer.toString());
234 
235             buffer = null;
236         } else if (parser.getName().equals(ANSWER_TAG.toString())) {
237             if (currentFaq == null) {
238                 throw new XmlPullParserException(
239                         "Missing <faq> at: (" + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")");
240             }
241 
242             buffer.append(LESS_THAN).append(SLASH).append(parser.getName()).append(GREATER_THAN);
243 
244             currentFaq.setAnswer(buffer.toString());
245 
246             buffer = null;
247         } else if (parser.getName().equals(TITLE.toString())) {
248             if (currentPart == null) {
249                 throw new XmlPullParserException(
250                         "Missing <part> at: (" + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")");
251             }
252 
253             buffer.append(LESS_THAN).append(SLASH).append(parser.getName()).append(GREATER_THAN);
254 
255             currentPart.setTitle(buffer.toString());
256 
257             buffer = null;
258         }
259 
260         // ----------------------------------------------------------------------
261         // Macro
262         // ----------------------------------------------------------------------
263 
264         else if (parser.getName().equals(MACRO_TAG.toString())) {
265             handleMacroEnd(buffer);
266         } else if (parser.getName().equals(PARAM.toString())) {
267             if (!(macroName != null && !macroName.isEmpty())) {
268                 handleUnknown(parser, sink, TAG_TYPE_END);
269             }
270         } else if (buffer != null) {
271             if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == SPACE) {
272                 buffer.deleteCharAt(buffer.length() - 1);
273             }
274 
275             buffer.append(LESS_THAN).append(SLASH).append(parser.getName()).append(GREATER_THAN);
276         }
277     }
278 
279     /** {@inheritDoc} */
280     protected void handleText(XmlPullParser parser, Sink sink) throws XmlPullParserException {
281         if (buffer != null) {
282             buffer.append(parser.getText());
283         }
284         // only significant text content in fml files is in <question>, <answer> or <title>
285     }
286 
287     /** {@inheritDoc} */
288     protected void handleCdsect(XmlPullParser parser, Sink sink) throws XmlPullParserException {
289         String cdSection = parser.getText();
290 
291         if (buffer != null) {
292             buffer.append(LESS_THAN)
293                     .append(BANG)
294                     .append(LEFT_SQUARE_BRACKET)
295                     .append(CDATA)
296                     .append(LEFT_SQUARE_BRACKET)
297                     .append(cdSection)
298                     .append(RIGHT_SQUARE_BRACKET)
299                     .append(RIGHT_SQUARE_BRACKET)
300                     .append(GREATER_THAN);
301         } else {
302             sink.text(cdSection);
303         }
304     }
305 
306     /** {@inheritDoc} */
307     protected void handleComment(XmlPullParser parser, Sink sink) throws XmlPullParserException {
308         String comment = parser.getText();
309 
310         if (buffer != null) {
311             buffer.append(LESS_THAN)
312                     .append(BANG)
313                     .append(MINUS)
314                     .append(MINUS)
315                     .append(comment)
316                     .append(MINUS)
317                     .append(MINUS)
318                     .append(GREATER_THAN);
319         } else {
320             if (isEmitComments()) {
321                 sink.comment(comment);
322             }
323         }
324     }
325 
326     /** {@inheritDoc} */
327     protected void handleEntity(XmlPullParser parser, Sink sink) throws XmlPullParserException {
328         if (buffer != null) {
329             if (parser.getText() != null) {
330                 String text = parser.getText();
331 
332                 // parser.getText() returns the entity replacement text
333                 // (&lt; -> <), need to re-escape them
334                 if (text.length() == 1) {
335                     text = HtmlTools.escapeHTML(text);
336                 }
337 
338                 buffer.append(text);
339             }
340         } else {
341             super.handleEntity(parser, sink);
342         }
343     }
344 
345     /**
346      * {@inheritDoc}
347      */
348     protected void init() {
349         super.init();
350 
351         this.currentFaq = null;
352         this.currentPart = null;
353         this.buffer = null;
354         this.macroName = null;
355         this.macroParameters = null;
356     }
357 
358     /**
359      * TODO import from XdocParser, probably need to be generic.
360      *
361      * @param parser not null
362      * @throws MacroExecutionException if any
363      */
364     private void handleMacroStart(XmlPullParser parser) throws MacroExecutionException {
365         if (!isSecondParsing()) {
366             macroName = parser.getAttributeValue(null, Attribute.NAME.toString());
367 
368             if (macroParameters == null) {
369                 macroParameters = new HashMap<>();
370             }
371 
372             if (macroName == null || macroName.isEmpty()) {
373                 throw new MacroExecutionException("The '" + Attribute.NAME.toString() + "' attribute for the '"
374                         + MACRO_TAG.toString() + "' tag is required.");
375             }
376         }
377     }
378 
379     /**
380      * TODO import from XdocParser, probably need to be generic.
381      *
382      * @param buffer not null
383      * @throws MacroExecutionException if any
384      */
385     private void handleMacroEnd(StringBuilder buffer) throws MacroExecutionException {
386         if (!isSecondParsing()) {
387             if (macroName != null && !macroName.isEmpty()) {
388                 MacroRequest request = new MacroRequest(sourceContent, new FmlParser(), macroParameters, getBasedir());
389 
390                 try {
391                     StringWriter sw = new StringWriter();
392                     Xhtml5BaseSink sink = new Xhtml5BaseSink(sw);
393                     executeMacro(macroName, request, sink);
394                     sink.close();
395                     buffer.append(sw.toString());
396                 } catch (MacroNotFoundException me) {
397                     throw new MacroExecutionException("Macro not found: " + macroName, me);
398                 }
399             }
400         }
401 
402         // Reinit macro
403         macroName = null;
404         macroParameters = null;
405     }
406 
407     /**
408      * TODO import from XdocParser, probably need to be generic.
409      *
410      * @param parser not null
411      * @param sink not null
412      * @throws MacroExecutionException if any
413      */
414     private void handleParamStart(XmlPullParser parser, Sink sink) throws MacroExecutionException {
415         if (!isSecondParsing()) {
416             if (macroName != null && !macroName.isEmpty()) {
417                 String paramName = parser.getAttributeValue(null, Attribute.NAME.toString());
418                 String paramValue = parser.getAttributeValue(null, Attribute.VALUE.toString());
419 
420                 if ((paramName == null || paramName.isEmpty()) || (paramValue == null || paramValue.isEmpty())) {
421                     throw new MacroExecutionException("'" + Attribute.NAME.toString()
422                             + "' and '" + Attribute.VALUE.toString() + "' attributes for the '" + PARAM.toString()
423                             + "' tag are required inside the '" + MACRO_TAG.toString() + "' tag.");
424                 }
425 
426                 macroParameters.put(paramName, paramValue);
427             } else {
428                 // param tag from non-macro object, see MSITE-288
429                 handleUnknown(parser, sink, TAG_TYPE_START);
430             }
431         }
432     }
433 
434     /**
435      * Writes the faqs to the specified sink.
436      *
437      * @param sink The sink to consume the event.
438      * @throws ParseException if something goes wrong.
439      */
440     private void writeFaqs(Sink sink) throws ParseException {
441         FmlContentParser xdocParser = new FmlContentParser();
442 
443         sink.head();
444         sink.title();
445         sink.text(faqs.getTitle());
446         sink.title_();
447         sink.head_();
448 
449         sink.body();
450         sink.section1();
451         sink.anchor("top");
452         sink.anchor_();
453         sink.sectionTitle1();
454         sink.text(faqs.getTitle());
455         sink.sectionTitle1_();
456 
457         // ----------------------------------------------------------------------
458         // Write summary
459         // ----------------------------------------------------------------------
460 
461         for (Part part : faqs.getParts()) {
462             if (StringUtils.isNotEmpty(part.getTitle())) {
463                 sink.paragraph();
464                 sink.inline(SinkEventAttributeSet.Semantics.BOLD);
465                 xdocParser.parse(part.getTitle(), sink);
466                 sink.inline_();
467                 sink.paragraph_();
468             }
469 
470             sink.numberedList(Sink.NUMBERING_DECIMAL);
471 
472             for (Faq faq : part.getFaqs()) {
473                 sink.numberedListItem();
474                 sink.link("#" + faq.getId());
475 
476                 if (StringUtils.isNotEmpty(faq.getQuestion())) {
477                     xdocParser.parse(faq.getQuestion(), sink);
478                 } else {
479                     throw new ParseException("Missing <question> for FAQ '" + faq.getId() + "'");
480                 }
481 
482                 sink.link_();
483                 sink.numberedListItem_();
484             }
485 
486             sink.numberedList_();
487         }
488 
489         sink.section1_();
490 
491         // ----------------------------------------------------------------------
492         // Write content
493         // ----------------------------------------------------------------------
494 
495         for (Part part : faqs.getParts()) {
496             if (StringUtils.isNotEmpty(part.getTitle())) {
497                 sink.section1();
498                 sink.anchor(part.getId());
499                 sink.anchor_();
500                 sink.sectionTitle1();
501                 xdocParser.parse(part.getTitle(), sink);
502                 sink.sectionTitle1_();
503             }
504 
505             sink.definitionList();
506 
507             for (Iterator<Faq> faqIterator = part.getFaqs().iterator(); faqIterator.hasNext(); ) {
508                 Faq faq = faqIterator.next();
509 
510                 sink.anchor(faq.getId());
511                 sink.anchor_();
512 
513                 sink.definedTerm();
514 
515                 if (StringUtils.isNotEmpty(faq.getQuestion())) {
516                     xdocParser.parse(faq.getQuestion(), sink);
517                 } else {
518                     throw new ParseException("Missing <question> for FAQ '" + faq.getId() + "'");
519                 }
520 
521                 sink.definedTerm_();
522 
523                 sink.definition();
524 
525                 if (StringUtils.isNotEmpty(faq.getAnswer())) {
526                     xdocParser.parse(faq.getAnswer(), sink);
527                 } else {
528                     throw new ParseException("Missing <answer> for FAQ '" + faq.getId() + "'");
529                 }
530 
531                 if (faqs.isToplink()) {
532                     writeTopLink(sink);
533                 }
534 
535                 if (faqIterator.hasNext()) {
536                     sink.horizontalRule();
537                 }
538 
539                 sink.definition_();
540             }
541 
542             sink.definitionList_();
543 
544             if (StringUtils.isNotEmpty(part.getTitle())) {
545                 sink.section1_();
546             }
547         }
548 
549         sink.body_();
550     }
551 
552     /**
553      * Writes a toplink element.
554      *
555      * @param sink The sink to consume the event.
556      */
557     private void writeTopLink(Sink sink) {
558         SinkEventAttributeSet atts = new SinkEventAttributeSet();
559         atts.addAttribute(SinkEventAttributeSet.STYLE, "text-align: right;");
560         sink.paragraph(atts);
561         sink.link("#top");
562         sink.text("[top]");
563         sink.link_();
564         sink.paragraph_();
565     }
566 }