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 // (< -> <), 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}