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