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.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
55
56
57
58
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
66 private Faqs faqs;
67
68
69 private Part currentPart;
70
71
72 private Faq currentFaq;
73
74
75 private StringBuilder buffer;
76
77
78 private String sourceContent;
79
80
81 private String macroName;
82
83
84 private Map<String, Object> macroParameters = new HashMap<>();
85
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
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
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
197 buffer.append(HtmlTools.escapeHTML(parser.getAttributeValue(i)));
198
199 buffer.append(QUOTE);
200 }
201
202 buffer.append(GREATER_THAN);
203 }
204 }
205
206
207 protected void handleEndTag(XmlPullParser parser, Sink sink)
208 throws XmlPullParserException, MacroExecutionException {
209 if (parser.getName().equals(FAQS_TAG.toString())) {
210
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
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
280 protected void handleText(XmlPullParser parser, Sink sink) throws XmlPullParserException {
281 if (buffer != null) {
282 buffer.append(parser.getText());
283 }
284
285 }
286
287
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
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
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
333
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
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
360
361
362
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
381
382
383
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
403 macroName = null;
404 macroParameters = null;
405 }
406
407
408
409
410
411
412
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
429 handleUnknown(parser, sink, TAG_TYPE_START);
430 }
431 }
432 }
433
434
435
436
437
438
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
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
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
554
555
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 }