1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
56
57
58
59
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
67 private Faqs faqs;
68
69
70 private Part currentPart;
71
72
73 private Faq currentFaq;
74
75
76 private StringBuilder buffer;
77
78
79 private String sourceContent;
80
81
82 private String macroName;
83
84
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
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
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
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
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
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
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
327
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
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
354
355
356
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
375
376
377
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
397 macroName = null;
398 macroParameters = null;
399 }
400
401
402
403
404
405
406
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
423 handleUnknown(parser, sink, TAG_TYPE_START);
424 }
425 }
426 }
427
428
429
430
431
432
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
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
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
548
549
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 }