001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.module.fml;
020
021import javax.inject.Named;
022import javax.inject.Singleton;
023
024import java.io.IOException;
025import java.io.Reader;
026import java.io.StringReader;
027import java.io.StringWriter;
028import java.util.HashMap;
029import java.util.Iterator;
030import java.util.LinkedHashMap;
031import java.util.Map;
032
033import org.apache.maven.doxia.macro.MacroExecutionException;
034import org.apache.maven.doxia.macro.MacroRequest;
035import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
036import org.apache.maven.doxia.module.fml.model.Faq;
037import org.apache.maven.doxia.module.fml.model.Faqs;
038import org.apache.maven.doxia.module.fml.model.Part;
039import org.apache.maven.doxia.parser.AbstractXmlParser;
040import org.apache.maven.doxia.parser.ParseException;
041import org.apache.maven.doxia.sink.Sink;
042import org.apache.maven.doxia.sink.SinkEventAttributeSet;
043import org.apache.maven.doxia.sink.SinkEventAttributes;
044import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink;
045import org.apache.maven.doxia.util.DoxiaStringUtils;
046import org.apache.maven.doxia.util.DoxiaUtils;
047import org.apache.maven.doxia.util.HtmlTools;
048import org.codehaus.plexus.util.xml.pull.XmlPullParser;
049import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053/**
054 * Parse a fml model and emit events into the specified doxia Sink.
055 *
056 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
057 * @author ltheussl
058 * @since 1.0
059 * @deprecated FML is deprecated since 2.2.0 and scheduled for removal in the next major version.
060 *     Convert FAQ pages to Markdown or XDoc, for example with doxia-converter; see
061 *     https://github.com/apache/maven-doxia/issues/1101.
062 */
063@Deprecated
064@Singleton
065@Named("fml")
066public class FmlParser extends AbstractXmlParser implements FmlMarkup {
067    private static final Logger LOGGER = LoggerFactory.getLogger(FmlParser.class);
068
069    /** Collect a faqs model. */
070    private Faqs faqs;
071
072    /** Collect a part. */
073    private Part currentPart;
074
075    /** Collect a single faq. */
076    private Faq currentFaq;
077
078    /** Used to collect text events. */
079    private StringBuilder buffer;
080
081    /** The source content of the input reader. Used to pass into macros. */
082    private String sourceContent;
083
084    /** A macro name. */
085    private String macroName;
086
087    /** The macro parameters. */
088    private Map<String, Object> macroParameters = new LinkedHashMap<>();
089
090    public void parse(Reader source, Sink sink, String reference) throws ParseException {
091        LOGGER.warn(
092                "FML is deprecated and will be removed in the next major Doxia version; convert {} to Markdown or XDoc",
093                reference == null ? "this document" : "'" + reference + "'");
094        this.faqs = null;
095        this.sourceContent = null;
096        init();
097
098        try (Reader reader = source) {
099            StringWriter contentWriter = new StringWriter();
100            char[] buffer = new char[8192];
101            int n;
102            while ((n = reader.read(buffer)) != -1) {
103                contentWriter.write(buffer, 0, n);
104            }
105            sourceContent = contentWriter.toString();
106        } catch (IOException ex) {
107            throw new ParseException("Error reading the input source", ex);
108        }
109
110        try {
111            Reader tmp = new StringReader(sourceContent);
112
113            this.faqs = new Faqs();
114
115            // this populates faqs
116            super.parse(tmp, sink, reference);
117
118            writeFaqs(getWrappedSink(sink));
119        } finally {
120            this.faqs = null;
121            this.sourceContent = null;
122            setSecondParsing(false);
123            init();
124        }
125    }
126
127    protected void handleStartTag(XmlPullParser parser, Sink sink)
128            throws XmlPullParserException, MacroExecutionException {
129        if (parser.getName().equals(FAQS_TAG.toString())) {
130            String title = parser.getAttributeValue(null, "title");
131
132            if (title != null) {
133                faqs.setTitle(title);
134            }
135
136            String toplink = parser.getAttributeValue(null, "toplink");
137
138            if (toplink != null) {
139                if (toplink.equalsIgnoreCase("true")) {
140                    faqs.setToplink(true);
141                } else {
142                    faqs.setToplink(false);
143                }
144            }
145        } else if (parser.getName().equals(PART_TAG.toString())) {
146            currentPart = new Part();
147
148            currentPart.setId(parser.getAttributeValue(null, SinkEventAttributes.ID));
149
150            if (currentPart.getId() == null) {
151                throw new XmlPullParserException("id attribute required for <part> at: (" + parser.getLineNumber() + ":"
152                        + parser.getColumnNumber() + ")");
153            } else if (!DoxiaUtils.isValidId(currentPart.getId())) {
154                String linkAnchor = DoxiaUtils.encodeId(currentPart.getId());
155
156                LOGGER.debug("Modified invalid link '{}' to '{}'", currentPart.getId(), linkAnchor);
157
158                currentPart.setId(linkAnchor);
159            }
160        } else if (parser.getName().equals(TITLE.toString())) {
161            buffer = new StringBuilder();
162            buffer.append(LESS_THAN).append(parser.getName()).append(GREATER_THAN);
163        } else if (parser.getName().equals(FAQ_TAG.toString())) {
164            currentFaq = new Faq();
165
166            currentFaq.setId(parser.getAttributeValue(null, SinkEventAttributes.ID));
167
168            if (currentFaq.getId() == null) {
169                throw new XmlPullParserException("id attribute required for <faq> at: (" + parser.getLineNumber() + ":"
170                        + parser.getColumnNumber() + ")");
171            } else if (!DoxiaUtils.isValidId(currentFaq.getId())) {
172                String linkAnchor = DoxiaUtils.encodeId(currentFaq.getId());
173
174                LOGGER.debug("Modified invalid link '{}' to '{}'", currentFaq.getId(), linkAnchor);
175
176                currentFaq.setId(linkAnchor);
177            }
178        } else if (parser.getName().equals(QUESTION_TAG.toString())) {
179            buffer = new StringBuilder();
180            buffer.append(LESS_THAN).append(parser.getName()).append(GREATER_THAN);
181        } else if (parser.getName().equals(ANSWER_TAG.toString())) {
182            buffer = new StringBuilder();
183            buffer.append(LESS_THAN).append(parser.getName()).append(GREATER_THAN);
184
185        }
186
187        // ----------------------------------------------------------------------
188        // Macro
189        // ----------------------------------------------------------------------
190
191        else if (parser.getName().equals(MACRO_TAG.toString())) {
192            handleMacroStart(parser);
193        } else if (parser.getName().equals(PARAM.toString())) {
194            handleParamStart(parser, sink);
195        } else if (buffer != null) {
196            buffer.append(LESS_THAN).append(parser.getName());
197
198            int count = parser.getAttributeCount();
199
200            for (int i = 0; i < count; i++) {
201                buffer.append(SPACE).append(parser.getAttributeName(i));
202
203                buffer.append(EQUAL).append(QUOTE);
204
205                // TODO: why are attribute values HTML-encoded?
206                buffer.append(HtmlTools.escapeHTML(parser.getAttributeValue(i)));
207
208                buffer.append(QUOTE);
209            }
210
211            buffer.append(GREATER_THAN);
212        }
213    }
214
215    protected void handleEndTag(XmlPullParser parser, Sink sink)
216            throws XmlPullParserException, MacroExecutionException {
217        if (parser.getName().equals(FAQS_TAG.toString())) {
218            // Do nothing
219            return;
220        } else if (parser.getName().equals(PART_TAG.toString())) {
221            faqs.addPart(currentPart);
222
223            currentPart = null;
224        } else if (parser.getName().equals(FAQ_TAG.toString())) {
225            if (currentPart == null) {
226                throw new XmlPullParserException(
227                        "Missing <part>  at: (" + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")");
228            }
229
230            currentPart.addFaq(currentFaq);
231
232            currentFaq = null;
233        } else if (parser.getName().equals(QUESTION_TAG.toString())) {
234            if (currentFaq == null) {
235                throw new XmlPullParserException(
236                        "Missing <faq> at: (" + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")");
237            }
238
239            buffer.append(LESS_THAN).append(SLASH).append(parser.getName()).append(GREATER_THAN);
240
241            currentFaq.setQuestion(buffer.toString());
242
243            buffer = null;
244        } else if (parser.getName().equals(ANSWER_TAG.toString())) {
245            if (currentFaq == null) {
246                throw new XmlPullParserException(
247                        "Missing <faq> at: (" + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")");
248            }
249
250            buffer.append(LESS_THAN).append(SLASH).append(parser.getName()).append(GREATER_THAN);
251
252            currentFaq.setAnswer(buffer.toString());
253
254            buffer = null;
255        } else if (parser.getName().equals(TITLE.toString())) {
256            if (currentPart == null) {
257                throw new XmlPullParserException(
258                        "Missing <part> at: (" + parser.getLineNumber() + ":" + parser.getColumnNumber() + ")");
259            }
260
261            buffer.append(LESS_THAN).append(SLASH).append(parser.getName()).append(GREATER_THAN);
262
263            currentPart.setTitle(buffer.toString());
264
265            buffer = null;
266        }
267
268        // ----------------------------------------------------------------------
269        // Macro
270        // ----------------------------------------------------------------------
271
272        else if (parser.getName().equals(MACRO_TAG.toString())) {
273            handleMacroEnd(buffer);
274        } else if (parser.getName().equals(PARAM.toString())) {
275            if (!(macroName != null && !macroName.isEmpty())) {
276                handleUnknown(parser, sink, TAG_TYPE_END);
277            }
278        } else if (buffer != null) {
279            if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == SPACE) {
280                buffer.deleteCharAt(buffer.length() - 1);
281            }
282
283            buffer.append(LESS_THAN).append(SLASH).append(parser.getName()).append(GREATER_THAN);
284        }
285    }
286
287    protected void handleText(XmlPullParser parser, Sink sink) throws XmlPullParserException {
288        if (buffer != null) {
289            buffer.append(parser.getText());
290        }
291        // only significant text content in fml files is in <question>, <answer> or <title>
292    }
293
294    protected void handleCdsect(XmlPullParser parser, Sink sink) throws XmlPullParserException {
295        String cdSection = parser.getText();
296
297        if (buffer != null) {
298            buffer.append(LESS_THAN)
299                    .append(BANG)
300                    .append(LEFT_SQUARE_BRACKET)
301                    .append(CDATA)
302                    .append(LEFT_SQUARE_BRACKET)
303                    .append(cdSection)
304                    .append(RIGHT_SQUARE_BRACKET)
305                    .append(RIGHT_SQUARE_BRACKET)
306                    .append(GREATER_THAN);
307        } else {
308            sink.text(cdSection);
309        }
310    }
311
312    protected void handleComment(XmlPullParser parser, Sink sink) throws XmlPullParserException {
313        String comment = parser.getText();
314
315        if (buffer != null) {
316            buffer.append(LESS_THAN)
317                    .append(BANG)
318                    .append(MINUS)
319                    .append(MINUS)
320                    .append(comment)
321                    .append(MINUS)
322                    .append(MINUS)
323                    .append(GREATER_THAN);
324        } else {
325            if (isEmitComments()) {
326                sink.comment(comment);
327            }
328        }
329    }
330
331    protected void handleEntity(XmlPullParser parser, Sink sink) throws XmlPullParserException {
332        if (buffer != null) {
333            if (parser.getText() != null) {
334                String text = parser.getText();
335
336                // parser.getText() returns the entity replacement text
337                // (&lt; -> <), need to re-escape them
338                if (text.length() == 1) {
339                    text = HtmlTools.escapeHTML(text);
340                }
341
342                buffer.append(text);
343            }
344        } else {
345            super.handleEntity(parser, sink);
346        }
347    }
348
349    /**
350     * {@inheritDoc}
351     */
352    protected void init() {
353        super.init();
354
355        this.currentFaq = null;
356        this.currentPart = null;
357        this.buffer = null;
358        this.macroName = null;
359        this.macroParameters = null;
360    }
361
362    /**
363     * TODO import from XdocParser, probably need to be generic.
364     *
365     * @param parser not null
366     * @throws MacroExecutionException if any
367     */
368    private void handleMacroStart(XmlPullParser parser) throws MacroExecutionException {
369        if (!isSecondParsing()) {
370            macroName = parser.getAttributeValue(null, SinkEventAttributes.NAME);
371
372            if (macroParameters == null) {
373                macroParameters = new HashMap<>();
374            }
375
376            if (macroName == null || macroName.isEmpty()) {
377                throw new MacroExecutionException("The '" + SinkEventAttributes.NAME + "' attribute for the '"
378                        + MACRO_TAG.toString() + "' tag is required.");
379            }
380        }
381    }
382
383    /**
384     * TODO import from XdocParser, probably need to be generic.
385     *
386     * @param buffer not null
387     * @throws MacroExecutionException if any
388     */
389    private void handleMacroEnd(StringBuilder buffer) throws MacroExecutionException {
390        if (!isSecondParsing()) {
391            if (macroName != null && !macroName.isEmpty()) {
392                MacroRequest request = new MacroRequest(sourceContent, new FmlParser(), macroParameters, getBasedir());
393
394                try {
395                    StringWriter sw = new StringWriter();
396                    Xhtml5BaseSink sink = new Xhtml5BaseSink(sw);
397                    executeMacro(macroName, request, sink);
398                    sink.close();
399                    buffer.append(sw.toString());
400                } catch (MacroNotFoundException me) {
401                    throw new MacroExecutionException("Macro not found: " + macroName, me);
402                }
403            }
404        }
405
406        // Reinit macro
407        macroName = null;
408        macroParameters = null;
409    }
410
411    /**
412     * TODO import from XdocParser, probably need to be generic.
413     *
414     * @param parser not null
415     * @param sink not null
416     * @throws MacroExecutionException if any
417     */
418    private void handleParamStart(XmlPullParser parser, Sink sink) throws MacroExecutionException {
419        if (!isSecondParsing()) {
420            if (macroName != null && !macroName.isEmpty()) {
421                String paramName = parser.getAttributeValue(null, SinkEventAttributes.NAME);
422                String paramValue = parser.getAttributeValue(null, SinkEventAttributes.VALUE);
423
424                if ((paramName == null || paramName.isEmpty()) || (paramValue == null || paramValue.isEmpty())) {
425                    throw new MacroExecutionException("'" + SinkEventAttributes.NAME
426                            + "' and '" + SinkEventAttributes.VALUE + "' attributes for the '" + PARAM.toString()
427                            + "' tag are required inside the '" + MACRO_TAG.toString() + "' tag.");
428                }
429
430                macroParameters.put(paramName, paramValue);
431            } else {
432                // param tag from non-macro object, see MSITE-288
433                handleUnknown(parser, sink, TAG_TYPE_START);
434            }
435        }
436    }
437
438    /**
439     * Writes the faqs to the specified sink.
440     *
441     * @param sink The sink to consume the event.
442     * @throws ParseException if something goes wrong.
443     */
444    private void writeFaqs(Sink sink) throws ParseException {
445        FmlContentParser xdocParser = new FmlContentParser();
446
447        sink.head();
448        sink.title();
449        sink.text(faqs.getTitle());
450        sink.title_();
451        sink.head_();
452
453        sink.body();
454        sink.section1();
455        sink.anchor("top");
456        sink.anchor_();
457        sink.sectionTitle1();
458        sink.text(faqs.getTitle());
459        sink.sectionTitle1_();
460
461        // ----------------------------------------------------------------------
462        // Write summary
463        // ----------------------------------------------------------------------
464
465        for (Part part : faqs.getParts()) {
466            if (DoxiaStringUtils.isNotEmpty(part.getTitle())) {
467                sink.paragraph();
468                sink.inline(SinkEventAttributeSet.Semantics.BOLD);
469                xdocParser.parse(part.getTitle(), sink);
470                sink.inline_();
471                sink.paragraph_();
472            }
473
474            sink.numberedList(Sink.NUMBERING_DECIMAL);
475
476            for (Faq faq : part.getFaqs()) {
477                sink.numberedListItem();
478                sink.link("#" + faq.getId());
479
480                if (DoxiaStringUtils.isNotEmpty(faq.getQuestion())) {
481                    xdocParser.parse(faq.getQuestion(), sink);
482                } else {
483                    throw new ParseException("Missing <question> for FAQ '" + faq.getId() + "'");
484                }
485
486                sink.link_();
487                sink.numberedListItem_();
488            }
489
490            sink.numberedList_();
491        }
492
493        sink.section1_();
494
495        // ----------------------------------------------------------------------
496        // Write content
497        // ----------------------------------------------------------------------
498
499        for (Part part : faqs.getParts()) {
500            if (DoxiaStringUtils.isNotEmpty(part.getTitle())) {
501                sink.section1();
502                sink.anchor(part.getId());
503                sink.anchor_();
504                sink.sectionTitle1();
505                xdocParser.parse(part.getTitle(), sink);
506                sink.sectionTitle1_();
507            }
508
509            sink.definitionList();
510
511            for (Iterator<Faq> faqIterator = part.getFaqs().iterator(); faqIterator.hasNext(); ) {
512                Faq faq = faqIterator.next();
513
514                sink.anchor(faq.getId());
515                sink.anchor_();
516
517                sink.definedTerm();
518
519                if (DoxiaStringUtils.isNotEmpty(faq.getQuestion())) {
520                    xdocParser.parse(faq.getQuestion(), sink);
521                } else {
522                    throw new ParseException("Missing <question> for FAQ '" + faq.getId() + "'");
523                }
524
525                sink.definedTerm_();
526
527                sink.definition();
528
529                if (DoxiaStringUtils.isNotEmpty(faq.getAnswer())) {
530                    xdocParser.parse(faq.getAnswer(), sink);
531                } else {
532                    throw new ParseException("Missing <answer> for FAQ '" + faq.getId() + "'");
533                }
534
535                if (faqs.isToplink()) {
536                    writeTopLink(sink);
537                }
538
539                if (faqIterator.hasNext()) {
540                    sink.horizontalRule();
541                }
542
543                sink.definition_();
544            }
545
546            sink.definitionList_();
547
548            if (DoxiaStringUtils.isNotEmpty(part.getTitle())) {
549                sink.section1_();
550            }
551        }
552
553        sink.body_();
554    }
555
556    /**
557     * Writes a toplink element.
558     *
559     * @param sink The sink to consume the event.
560     */
561    private void writeTopLink(Sink sink) {
562        SinkEventAttributeSet atts = new SinkEventAttributeSet();
563        atts.addAttribute(SinkEventAttributeSet.STYLE, "text-align: right;");
564        sink.paragraph(atts);
565        sink.link("#top");
566        sink.text("[top]");
567        sink.link_();
568        sink.paragraph_();
569    }
570}