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.LinkedHashMap; 032import java.util.Map; 033 034import org.apache.commons.io.IOUtils; 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.DoxiaStringUtils; 047import org.apache.maven.doxia.util.DoxiaUtils; 048import org.apache.maven.doxia.util.HtmlTools; 049import org.codehaus.plexus.util.xml.pull.XmlPullParser; 050import org.codehaus.plexus.util.xml.pull.XmlPullParserException; 051import org.slf4j.Logger; 052import org.slf4j.LoggerFactory; 053 054/** 055 * Parse a fml model and emit events into the specified doxia Sink. 056 * 057 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a> 058 * @author ltheussl 059 * @since 1.0 060 */ 061@Singleton 062@Named("fml") 063public class FmlParser extends AbstractXmlParser implements FmlMarkup { 064 private static final Logger LOGGER = LoggerFactory.getLogger(FmlParser.class); 065 066 /** Collect a faqs model. */ 067 private Faqs faqs; 068 069 /** Collect a part. */ 070 private Part currentPart; 071 072 /** Collect a single faq. */ 073 private Faq currentFaq; 074 075 /** Used to collect text events. */ 076 private StringBuilder buffer; 077 078 /** The source content of the input reader. Used to pass into macros. */ 079 private String sourceContent; 080 081 /** A macro name. */ 082 private String macroName; 083 084 /** The macro parameters. */ 085 private Map<String, Object> macroParameters = new LinkedHashMap<>(); 086 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 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 // Macro 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 // TODO: why are attribute values HTML-encoded? 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 // Do nothing 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 // Macro 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 // only significant text content in fml files is in <question>, <answer> or <title> 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 // parser.getText() returns the entity replacement text 327 // (< -> <), need to re-escape them 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 * {@inheritDoc} 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 * TODO import from XdocParser, probably need to be generic. 354 * 355 * @param parser not null 356 * @throws MacroExecutionException if any 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 * TODO import from XdocParser, probably need to be generic. 375 * 376 * @param buffer not null 377 * @throws MacroExecutionException if any 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 // Reinit macro 397 macroName = null; 398 macroParameters = null; 399 } 400 401 /** 402 * TODO import from XdocParser, probably need to be generic. 403 * 404 * @param parser not null 405 * @param sink not null 406 * @throws MacroExecutionException if any 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 // param tag from non-macro object, see MSITE-288 423 handleUnknown(parser, sink, TAG_TYPE_START); 424 } 425 } 426 } 427 428 /** 429 * Writes the faqs to the specified sink. 430 * 431 * @param sink The sink to consume the event. 432 * @throws ParseException if something goes wrong. 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 // Write summary 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 // Write content 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 * Writes a toplink element. 548 * 549 * @param sink The sink to consume the event. 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}