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