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.xdoc; 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.LinkedHashMap; 031import java.util.Map; 032 033import org.apache.commons.io.IOUtils; 034import org.apache.maven.doxia.macro.MacroExecutionException; 035import org.apache.maven.doxia.macro.MacroRequest; 036import org.apache.maven.doxia.macro.manager.MacroNotFoundException; 037import org.apache.maven.doxia.parser.ParseException; 038import org.apache.maven.doxia.parser.Xhtml1BaseParser; 039import org.apache.maven.doxia.sink.Sink; 040import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 041import org.apache.maven.doxia.util.HtmlTools; 042import org.codehaus.plexus.util.xml.pull.XmlPullParser; 043import org.codehaus.plexus.util.xml.pull.XmlPullParserException; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047/** 048 * Parse an xdoc model and emit events into the specified doxia Sink. 049 * 050 * @author <a href="mailto:jason@maven.org">Jason van Zyl</a> 051 * @since 1.0 052 */ 053@Singleton 054@Named("xdoc") 055public class XdocParser extends Xhtml1BaseParser implements XdocMarkup { 056 private static final Logger LOGGER = LoggerFactory.getLogger(XdocParser.class); 057 058 /** 059 * The source content of the input reader. Used to pass into macros. 060 */ 061 private String sourceContent; 062 063 /** 064 * Empty elements don't write a closing tag. 065 */ 066 private boolean isEmptyElement; 067 068 /** 069 * A macro name. 070 */ 071 private String macroName; 072 073 /** 074 * The macro parameters. 075 */ 076 private Map<String, Object> macroParameters = new LinkedHashMap<>(); 077 078 /** 079 * Indicates that we're inside <properties> or <head>. 080 */ 081 private boolean inHead; 082 083 /** 084 * Indicates that <title> was called from <properties> or <head>. 085 */ 086 private boolean hasTitle; 087 088 public void parse(Reader source, Sink sink, String reference) throws ParseException { 089 this.sourceContent = null; 090 091 try (Reader reader = source) { 092 StringWriter contentWriter = new StringWriter(); 093 IOUtils.copy(reader, contentWriter); 094 sourceContent = contentWriter.toString(); 095 } catch (IOException ex) { 096 throw new ParseException("Error reading the input source", ex); 097 } 098 099 // leave this at default (false) until everything is properly implemented, see DOXIA-226 100 // setIgnorableWhitespace(true); 101 102 try { 103 super.parse(new StringReader(sourceContent), sink, reference); 104 } finally { 105 this.sourceContent = null; 106 } 107 } 108 109 protected void handleStartTag(XmlPullParser parser, Sink sink) 110 throws XmlPullParserException, MacroExecutionException { 111 isEmptyElement = parser.isEmptyElementTag(); 112 isBeginningOfLineInsideBlock = true; 113 SinkEventAttributeSet attribs = getAttributesFromParser(parser); 114 115 if (parser.getName().equals(DOCUMENT_TAG.toString())) { 116 // Do nothing 117 return; 118 } else if (parser.getName().equals(HEAD.toString())) { 119 if (!inHead) // we might be in head from a <properties> already 120 { 121 this.inHead = true; 122 123 sink.head(attribs); 124 } 125 } else if (parser.getName().equals(TITLE.toString())) { 126 if (hasTitle) { 127 LOGGER.warn("<title> was already defined in <properties>, ignored <title> in <head>."); 128 129 try { 130 parser.nextText(); // ignore next text event 131 } catch (IOException ex) { 132 throw new XmlPullParserException("Failed to parse text", parser, ex); 133 } 134 } else { 135 sink.title(attribs); 136 } 137 } else if (parser.getName().equals(AUTHOR_TAG.toString())) { 138 sink.author(attribs); 139 } else if (parser.getName().equals(DATE_TAG.toString())) { 140 sink.date(attribs); 141 } else if (parser.getName().equals(META.toString())) { 142 handleMetaStart(parser, sink, attribs); 143 } else if (parser.getName().equals(BODY.toString())) { 144 if (inHead) { 145 sink.head_(); 146 this.inHead = false; 147 } 148 sink.body(attribs); 149 } else if (parser.getName().equals(SECTION_TAG.toString())) { 150 handleSectionStart(Sink.SECTION_LEVEL_1, sink, attribs, parser); 151 } else if (parser.getName().equals(SUBSECTION_TAG.toString())) { 152 handleSectionStart(Sink.SECTION_LEVEL_2, sink, attribs, parser); 153 } else if (parser.getName().equals(SOURCE_TAG.toString())) { 154 verbatim(); 155 156 attribs.addAttributes(SinkEventAttributeSet.SOURCE); 157 158 sink.verbatim(attribs); 159 } else if (parser.getName().equals(PROPERTIES_TAG.toString())) { 160 if (!inHead) // we might be in head from a <head> already 161 { 162 this.inHead = true; 163 164 sink.head(attribs); 165 } 166 } 167 168 // ---------------------------------------------------------------------- 169 // Macro 170 // ---------------------------------------------------------------------- 171 172 else if (parser.getName().equals(MACRO_TAG.toString())) { 173 handleMacroStart(parser); 174 } else if (parser.getName().equals(PARAM.toString())) { 175 handleParamStart(parser, sink); 176 } else if (!baseStartTag(parser, sink)) { 177 if (isEmptyElement) { 178 handleUnknown(parser, sink, TAG_TYPE_SIMPLE); 179 } else { 180 handleUnknown(parser, sink, TAG_TYPE_START); 181 } 182 183 LOGGER.warn( 184 "Unrecognized xdoc tag <{}> at [{}:{}]", 185 parser.getName(), 186 parser.getLineNumber(), 187 parser.getColumnNumber()); 188 } 189 } 190 191 protected void handleEndTag(XmlPullParser parser, Sink sink) 192 throws XmlPullParserException, MacroExecutionException { 193 isBeginningOfLineInsideBlock = true; 194 if (parser.getName().equals(DOCUMENT_TAG.toString())) { 195 // Do nothing 196 return; 197 } else if (parser.getName().equals(HEAD.toString())) { 198 // Do nothing, head is closed with BODY start. 199 } else if (parser.getName().equals(BODY.toString())) { 200 consecutiveSections(0, sink); 201 202 sink.body_(); 203 } else if (parser.getName().equals(TITLE.toString())) { 204 if (!hasTitle) { 205 sink.title_(); 206 this.hasTitle = true; 207 } 208 } else if (parser.getName().equals(AUTHOR_TAG.toString())) { 209 sink.author_(); 210 } else if (parser.getName().equals(DATE_TAG.toString())) { 211 sink.date_(); 212 } else if (parser.getName().equals(SOURCE_TAG.toString())) { 213 verbatim_(); 214 215 sink.verbatim_(); 216 } else if (parser.getName().equals(PROPERTIES_TAG.toString())) { 217 // Do nothing, head is closed with BODY start. 218 } else if (parser.getName().equals(MACRO_TAG.toString())) { 219 handleMacroEnd(sink); 220 } else if (parser.getName().equals(PARAM.toString())) { 221 if (!(macroName != null && !macroName.isEmpty())) { 222 handleUnknown(parser, sink, TAG_TYPE_END); 223 } 224 } else if (parser.getName().equals(SECTION_TAG.toString())) { 225 consecutiveSections(0, sink); 226 227 sink.section1_(); 228 } else if (parser.getName().equals(SUBSECTION_TAG.toString())) { 229 consecutiveSections(Sink.SECTION_LEVEL_1, sink); 230 231 // sink.section2_() not necessary 232 } else if (!baseEndTag(parser, sink)) { 233 if (!isEmptyElement) { 234 handleUnknown(parser, sink, TAG_TYPE_END); 235 } 236 } 237 238 isEmptyElement = false; 239 } 240 241 protected void consecutiveSections(int newLevel, Sink sink) { 242 closeOpenSections(newLevel, sink); 243 openMissingSections(newLevel, sink); 244 245 setSectionLevel(newLevel); 246 } 247 248 /** 249 * {@inheritDoc} 250 */ 251 protected void init() { 252 super.init(); 253 254 this.isEmptyElement = false; 255 this.macroName = null; 256 this.macroParameters = null; 257 this.inHead = false; 258 this.hasTitle = false; 259 } 260 261 /** 262 * Close open h2, h3, h4, h5 sections. 263 */ 264 private void closeOpenSections(int newLevel, Sink sink) { 265 while (getSectionLevel() >= newLevel) { 266 if (getSectionLevel() > Sink.SECTION_LEVEL_1) { 267 sink.section_(getSectionLevel()); 268 } 269 270 setSectionLevel(getSectionLevel() - 1); 271 } 272 } 273 274 private void handleMacroEnd(Sink sink) throws MacroExecutionException { 275 if (!isSecondParsing() && (macroName != null && !macroName.isEmpty())) { 276 MacroRequest request = new MacroRequest(sourceContent, new XdocParser(), macroParameters, getBasedir()); 277 278 try { 279 executeMacro(macroName, request, sink); 280 } catch (MacroNotFoundException me) { 281 throw new MacroExecutionException("Macro not found: " + macroName, me); 282 } 283 } 284 285 // Reinit macro 286 macroName = null; 287 macroParameters = null; 288 } 289 290 private void handleMacroStart(XmlPullParser parser) throws MacroExecutionException { 291 if (!isSecondParsing()) { 292 macroName = parser.getAttributeValue(null, Attribute.NAME.toString()); 293 294 if (macroParameters == null) { 295 macroParameters = new HashMap<>(); 296 } 297 298 if (macroName == null || macroName.isEmpty()) { 299 throw new MacroExecutionException("The '" + Attribute.NAME.toString() + "' attribute for the '" 300 + MACRO_TAG.toString() + "' tag is required."); 301 } 302 } 303 } 304 305 private void handleMetaStart(XmlPullParser parser, Sink sink, SinkEventAttributeSet attribs) { 306 String name = parser.getAttributeValue(null, Attribute.NAME.toString()); 307 String content = parser.getAttributeValue(null, Attribute.CONTENT.toString()); 308 309 if ("author".equals(name)) { 310 sink.author(null); 311 sink.text(content); 312 sink.author_(); 313 } else if ("date".equals(name)) { 314 sink.date(null); 315 sink.text(content); 316 sink.date_(); 317 } else { 318 sink.unknown("meta", new Object[] {TAG_TYPE_SIMPLE}, attribs); 319 } 320 } 321 322 private void handleParamStart(XmlPullParser parser, Sink sink) throws MacroExecutionException { 323 if (!isSecondParsing()) { 324 if (macroName != null && !macroName.isEmpty()) { 325 String paramName = parser.getAttributeValue(null, Attribute.NAME.toString()); 326 String paramValue = parser.getAttributeValue(null, Attribute.VALUE.toString()); 327 328 if ((paramName == null || paramName.isEmpty()) || (paramValue == null || paramValue.isEmpty())) { 329 throw new MacroExecutionException( 330 "'" + Attribute.NAME.toString() + "' and '" + Attribute.VALUE.toString() 331 + "' attributes for the '" + PARAM.toString() + "' tag are required inside the '" 332 + MACRO_TAG.toString() + "' tag."); 333 } 334 335 macroParameters.put(paramName, paramValue); 336 } else { 337 // param tag from non-macro object, see MSITE-288 338 handleUnknown(parser, sink, TAG_TYPE_START); 339 } 340 } 341 } 342 343 private void handleSectionStart(int level, Sink sink, SinkEventAttributeSet attribs, XmlPullParser parser) { 344 consecutiveSections(level, sink); 345 346 Object id = attribs.getAttribute(Attribute.ID.toString()); 347 348 if (id != null) { 349 sink.anchor(id.toString()); 350 sink.anchor_(); 351 } 352 353 sink.section(level, attribs); 354 sink.sectionTitle(level, null); 355 sink.text(HtmlTools.unescapeHTML(parser.getAttributeValue(null, Attribute.NAME.toString()))); 356 sink.sectionTitle_(level); 357 } 358 359 /** 360 * Open missing h2, h3, h4, h5 sections. 361 */ 362 private void openMissingSections(int newLevel, Sink sink) { 363 while (getSectionLevel() < newLevel - 1) { 364 setSectionLevel(getSectionLevel() + 1); 365 366 if (getSectionLevel() == Sink.SECTION_LEVEL_5) { 367 sink.section5(); 368 } else if (getSectionLevel() == Sink.SECTION_LEVEL_4) { 369 sink.section4(); 370 } else if (getSectionLevel() == Sink.SECTION_LEVEL_3) { 371 sink.section3(); 372 } else if (getSectionLevel() == Sink.SECTION_LEVEL_2) { 373 sink.section2(); 374 } 375 } 376 } 377}