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; 023import javax.swing.text.html.HTML.Attribute; 024 025import java.io.IOException; 026import java.io.Reader; 027import java.io.StringReader; 028import java.io.StringWriter; 029import java.util.HashMap; 030import java.util.Iterator; 031import java.util.Map; 032 033import org.apache.commons.io.IOUtils; 034import org.apache.commons.lang3.StringUtils; 035import org.apache.maven.doxia.macro.MacroExecutionException; 036import org.apache.maven.doxia.macro.MacroRequest; 037import org.apache.maven.doxia.macro.manager.MacroNotFoundException; 038import org.apache.maven.doxia.module.fml.model.Faq; 039import org.apache.maven.doxia.module.fml.model.Faqs; 040import org.apache.maven.doxia.module.fml.model.Part; 041import org.apache.maven.doxia.parser.AbstractXmlParser; 042import org.apache.maven.doxia.parser.ParseException; 043import org.apache.maven.doxia.sink.Sink; 044import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 045import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink; 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 */ 060@Singleton 061@Named("fml") 062public class FmlParser extends AbstractXmlParser implements FmlMarkup { 063 private static final Logger LOGGER = LoggerFactory.getLogger(FmlParser.class); 064 065 /** Collect a faqs model. */ 066 private Faqs faqs; 067 068 /** Collect a part. */ 069 private Part currentPart; 070 071 /** Collect a single faq. */ 072 private Faq currentFaq; 073 074 /** Used to collect text events. */ 075 private StringBuilder buffer; 076 077 /** The source content of the input reader. Used to pass into macros. */ 078 private String sourceContent; 079 080 /** A macro name. */ 081 private String macroName; 082 083 /** The macro parameters. */ 084 private Map<String, Object> macroParameters = new HashMap<>(); 085 086 /** {@inheritDoc} */ 087 public void parse(Reader source, Sink sink, String reference) throws ParseException { 088 this.faqs = null; 089 this.sourceContent = null; 090 init(); 091 092 try (Reader reader = source) { 093 StringWriter contentWriter = new StringWriter(); 094 IOUtils.copy(reader, contentWriter); 095 sourceContent = contentWriter.toString(); 096 } catch (IOException ex) { 097 throw new ParseException("Error reading the input source", ex); 098 } 099 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 /** {@inheritDoc} */ 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 // Macro 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 // TODO: why are attribute values HTML-encoded? 197 buffer.append(HtmlTools.escapeHTML(parser.getAttributeValue(i))); 198 199 buffer.append(QUOTE); 200 } 201 202 buffer.append(GREATER_THAN); 203 } 204 } 205 206 /** {@inheritDoc} */ 207 protected void handleEndTag(XmlPullParser parser, Sink sink) 208 throws XmlPullParserException, MacroExecutionException { 209 if (parser.getName().equals(FAQS_TAG.toString())) { 210 // Do nothing 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 // Macro 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 /** {@inheritDoc} */ 280 protected void handleText(XmlPullParser parser, Sink sink) throws XmlPullParserException { 281 if (buffer != null) { 282 buffer.append(parser.getText()); 283 } 284 // only significant text content in fml files is in <question>, <answer> or <title> 285 } 286 287 /** {@inheritDoc} */ 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 /** {@inheritDoc} */ 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 /** {@inheritDoc} */ 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 // parser.getText() returns the entity replacement text 333 // (< -> <), need to re-escape them 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 * {@inheritDoc} 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 * TODO import from XdocParser, probably need to be generic. 360 * 361 * @param parser not null 362 * @throws MacroExecutionException if any 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 * TODO import from XdocParser, probably need to be generic. 381 * 382 * @param buffer not null 383 * @throws MacroExecutionException if any 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 // Reinit macro 403 macroName = null; 404 macroParameters = null; 405 } 406 407 /** 408 * TODO import from XdocParser, probably need to be generic. 409 * 410 * @param parser not null 411 * @param sink not null 412 * @throws MacroExecutionException if any 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 // param tag from non-macro object, see MSITE-288 429 handleUnknown(parser, sink, TAG_TYPE_START); 430 } 431 } 432 } 433 434 /** 435 * Writes the faqs to the specified sink. 436 * 437 * @param sink The sink to consume the event. 438 * @throws ParseException if something goes wrong. 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 // Write summary 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 // Write content 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 * Writes a toplink element. 554 * 555 * @param sink The sink to consume the event. 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}