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.parser; 020 021import java.io.BufferedReader; 022import java.io.ByteArrayInputStream; 023import java.io.ByteArrayOutputStream; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.Reader; 027import java.io.StringReader; 028import java.io.StringWriter; 029import java.net.URI; 030import java.net.URL; 031import java.nio.file.Paths; 032import java.util.HashMap; 033import java.util.Hashtable; 034import java.util.LinkedHashMap; 035import java.util.Map; 036import java.util.regex.Matcher; 037import java.util.regex.Pattern; 038 039import org.apache.maven.doxia.macro.MacroExecutionException; 040import org.apache.maven.doxia.markup.XmlMarkup; 041import org.apache.maven.doxia.sink.Sink; 042import org.apache.maven.doxia.sink.impl.AbstractLocator; 043import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 044import org.apache.maven.doxia.util.DoxiaStringUtils; 045import org.apache.maven.doxia.util.HtmlTools; 046import org.apache.maven.doxia.util.XmlValidator; 047import org.codehaus.plexus.util.xml.pull.EntityReplacementMap; 048import org.codehaus.plexus.util.xml.pull.MXParser; 049import org.codehaus.plexus.util.xml.pull.XmlPullParser; 050import org.codehaus.plexus.util.xml.pull.XmlPullParserException; 051import org.slf4j.Logger; 052import org.slf4j.LoggerFactory; 053import org.xml.sax.EntityResolver; 054import org.xml.sax.InputSource; 055import org.xml.sax.SAXException; 056 057/** 058 * An abstract class that defines some convenience methods for <code>XML</code> parsers. 059 * 060 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a> 061 * @since 1.0 062 */ 063public abstract class AbstractXmlParser extends AbstractParser implements XmlMarkup { 064 /** 065 * Entity pattern for HTML entity, i.e. &nbsp; 066 * "<!ENTITY(\\s)+([^>|^\\s]+)(\\s)+\"(\\s)*(&[a-zA-Z]{2,6};)(\\s)*\"(\\s)*> 067 * <br> 068 * see <a href="http://www.w3.org/TR/REC-xml/#NT-EntityDecl">http://www.w3.org/TR/REC-xml/#NT-EntityDecl</a>. 069 */ 070 private static final Pattern PATTERN_ENTITY_1 = 071 Pattern.compile(ENTITY_START + "(\\s)+([^>|^\\s]+)(\\s)+\"(\\s)*(&[a-zA-Z]{2,6};)(\\s)*\"(\\s)*>"); 072 073 /** 074 * Entity pattern for Unicode entity, i.e. &#38; 075 * "<!ENTITY(\\s)+([^>|^\\s]+)(\\s)+\"(\\s)*(&(#x?[0-9a-fA-F]{1,5};)*)(\\s)*\"(\\s)*>" 076 * <br> 077 * see <a href="http://www.w3.org/TR/REC-xml/#NT-EntityDecl">http://www.w3.org/TR/REC-xml/#NT-EntityDecl</a>. 078 */ 079 private static final Pattern PATTERN_ENTITY_2 = 080 Pattern.compile(ENTITY_START + "(\\s)+([^>|^\\s]+)(\\s)+\"(\\s)*(&(#x?[0-9a-fA-F]{1,5};)*)(\\s)*\"(\\s)*>"); 081 082 private boolean ignorableWhitespace; 083 084 private boolean collapsibleWhitespace; 085 086 private boolean trimmableWhitespace; 087 088 private Map<String, String> entities; 089 090 private boolean validate = false; 091 092 /** 093 * If set the parser will be loaded with all single characters 094 * from the XHTML specification. 095 * The entities used: 096 * <ul> 097 * <li>http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent</li> 098 * <li>http://www.w3.org/TR/xhtml1/DTD/xhtml-special.ent</li> 099 * <li>http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent</li> 100 * </ul> 101 */ 102 private boolean addDefaultEntities = true; 103 104 public void parse(Reader source, Sink sink, String reference) throws ParseException { 105 init(); 106 107 Reader src = source; 108 109 // 1 first parsing if validation is required 110 if (isValidate()) { 111 String content; 112 try { 113 StringWriter contentWriter = new StringWriter(); 114 char[] buffer = new char[8192]; 115 int n; 116 while ((n = src.read(buffer)) != -1) { 117 contentWriter.write(buffer, 0, n); 118 } 119 content = contentWriter.toString(); 120 } catch (IOException e) { 121 throw new ParseException("Error reading the model", e); 122 } 123 124 XmlValidator validator = new XmlValidator(); 125 validator.setDefaultHandler(new XmlValidator.MessagesErrorHandler()); 126 validator.setEntityResolver(new CachedFileEntityResolver()); 127 128 validator.validate(content); 129 130 src = new StringReader(content); 131 } 132 133 // 2 second parsing to process 134 try { 135 XmlPullParser parser = addDefaultEntities 136 ? new MXParser(EntityReplacementMap.defaultEntityReplacementMap) 137 : new MXParser(); 138 139 parser.setInput(src); 140 141 // allow parser initialization, e.g. for additional entities in XHTML 142 // Note: do it after input is set, otherwise values are reset 143 initXmlParser(parser); 144 145 parseXml(parser, getWrappedSink(sink), reference); 146 } catch (XmlPullParserException ex) { 147 throw new ParseException("Error parsing the model", ex, ex.getLineNumber(), ex.getColumnNumber()); 148 } catch (MacroExecutionException ex) { 149 throw new ParseException("Macro execution failed", ex); 150 } 151 152 setSecondParsing(false); 153 init(); 154 } 155 156 /** 157 * Initializes the parser with custom entities or other options. 158 * 159 * @param parser A parser, not null. 160 * @throws org.codehaus.plexus.util.xml.pull.XmlPullParserException if there's a problem initializing the parser 161 */ 162 protected void initXmlParser(XmlPullParser parser) throws XmlPullParserException { 163 // nop 164 } 165 166 @Override 167 public final int getType() { 168 return XML_TYPE; 169 } 170 171 /** 172 * Converts the attributes of the current start tag of the given parser to a SinkEventAttributeSet. 173 * 174 * @param parser A parser, not null. 175 * @return a SinkEventAttributeSet or null if the current parser event is not a start tag. 176 * @since 1.1 177 * @deprecated The return type will become {@link org.apache.maven.doxia.sink.SinkEventAttributes} in 2.3.0. 178 * Callers are unaffected if they declare the result as that interface; every use in Doxia 179 * itself already needs nothing more than the interface. An override of this method will stop being called 180 * once the return type changes, with no error at build or run time. See <a href="https://github.com/apache/maven-doxia/issues/1074">issue 1074</a>. 181 */ 182 @Deprecated 183 protected SinkEventAttributeSet getAttributesFromParser(XmlPullParser parser) { 184 int count = parser.getAttributeCount(); 185 186 if (count < 0) { 187 return null; 188 } 189 190 SinkEventAttributeSet atts = new SinkEventAttributeSet(count); 191 192 for (int i = 0; i < count; i++) { 193 atts.addAttribute(parser.getAttributeName(i), parser.getAttributeValue(i)); 194 } 195 196 return atts; 197 } 198 199 private static final class XmlPullParserLocator extends AbstractLocator { 200 201 private final XmlPullParser parser; 202 203 XmlPullParserLocator(XmlPullParser parser, String reference) { 204 super(reference); 205 this.parser = parser; 206 } 207 208 @Override 209 public int getLineNumber() { 210 return parser.getLineNumber(); 211 } 212 213 @Override 214 public int getColumnNumber() { 215 return parser.getColumnNumber() != -1 ? parser.getColumnNumber() + 1 : -1; 216 } 217 } 218 /** 219 * Parse the model from the XmlPullParser into the given sink. 220 * 221 * @param parser A parser, not null. 222 * @param sink the sink to receive the events. 223 * @param reference the reference (usually the file path of the parsed document) 224 * @throws org.codehaus.plexus.util.xml.pull.XmlPullParserException if there's a problem parsing the model 225 * @throws org.apache.maven.doxia.macro.MacroExecutionException if there's a problem executing a macro 226 */ 227 private void parseXml(XmlPullParser parser, Sink sink, String reference) 228 throws XmlPullParserException, MacroExecutionException { 229 sink.setDocumentLocator(new XmlPullParserLocator(parser, reference)); 230 int eventType = parser.getEventType(); 231 232 while (eventType != XmlPullParser.END_DOCUMENT) { 233 if (eventType == XmlPullParser.START_TAG) { 234 handleStartTag(parser, sink); 235 } else if (eventType == XmlPullParser.END_TAG) { 236 handleEndTag(parser, sink); 237 } else if (eventType == XmlPullParser.TEXT) { 238 String text = getText(parser); 239 240 if (isIgnorableWhitespace()) { 241 if (text.trim().length() != 0) { 242 handleText(parser, sink); 243 } 244 } else { 245 handleText(parser, sink); 246 } 247 } else if (eventType == XmlPullParser.CDSECT) { 248 handleCdsect(parser, sink); 249 } else if (eventType == XmlPullParser.COMMENT) { 250 handleComment(parser, sink); 251 } else if (eventType == XmlPullParser.ENTITY_REF) { 252 handleEntity(parser, sink); 253 } else if (eventType == XmlPullParser.IGNORABLE_WHITESPACE) { 254 // nop 255 } else if (eventType == XmlPullParser.PROCESSING_INSTRUCTION) { 256 // nop 257 } else if (eventType == XmlPullParser.DOCDECL) { 258 addLocalEntities(parser, parser.getText()); 259 260 for (byte[] res : CachedFileEntityResolver.ENTITY_CACHE.values()) { 261 addDTDEntities(parser, new String(res)); 262 } 263 } 264 265 try { 266 eventType = parser.nextToken(); 267 } catch (IOException io) { 268 // Does not have a cause arg 269 throw new XmlPullParserException("Failed to parse next token", parser, io); 270 } 271 } 272 } 273 274 /** 275 * Goes through the possible start tags. 276 * 277 * @param parser A parser, not null. 278 * @param sink the sink to receive the events. 279 * @throws org.codehaus.plexus.util.xml.pull.XmlPullParserException if there's a problem parsing the model 280 * @throws org.apache.maven.doxia.macro.MacroExecutionException if there's a problem executing a macro 281 */ 282 protected abstract void handleStartTag(XmlPullParser parser, Sink sink) 283 throws XmlPullParserException, MacroExecutionException; 284 285 /** 286 * Goes through the possible end tags. 287 * 288 * @param parser A parser, not null. 289 * @param sink the sink to receive the events. 290 * @throws org.codehaus.plexus.util.xml.pull.XmlPullParserException if there's a problem parsing the model 291 * @throws org.apache.maven.doxia.macro.MacroExecutionException if there's a problem executing a macro 292 */ 293 protected abstract void handleEndTag(XmlPullParser parser, Sink sink) 294 throws XmlPullParserException, MacroExecutionException; 295 296 /** 297 * Handles text events. 298 * 299 * <p>This is a default implementation, if the parser points to a non-empty text element, 300 * it is emitted as a text event into the specified sink.</p> 301 * 302 * @param parser A parser, not null. 303 * @param sink the sink to receive the events. Not null. 304 * @throws org.codehaus.plexus.util.xml.pull.XmlPullParserException if there's a problem parsing the model 305 */ 306 protected void handleText(XmlPullParser parser, Sink sink) throws XmlPullParserException { 307 String text = getText(parser); 308 309 /* 310 * NOTE: Don't do any whitespace trimming here. Whitespace normalization has already been performed by the 311 * parser so any whitespace that makes it here is significant. 312 */ 313 if (text != null && !text.isEmpty()) { 314 sink.text(text); 315 } 316 } 317 318 /** 319 * Handles CDATA sections. 320 * 321 * <p>This is a default implementation, all data are emitted as text 322 * events into the specified sink.</p> 323 * 324 * @param parser A parser, not null. 325 * @param sink the sink to receive the events. Not null. 326 * @throws org.codehaus.plexus.util.xml.pull.XmlPullParserException if there's a problem parsing the model 327 */ 328 protected void handleCdsect(XmlPullParser parser, Sink sink) throws XmlPullParserException { 329 sink.text(getText(parser)); 330 } 331 332 /** 333 * Handles comments. 334 * 335 * <p>This is a default implementation, all data are emitted as comment 336 * events into the specified sink.</p> 337 * 338 * @param parser A parser, not null. 339 * @param sink the sink to receive the events. Not null. 340 * @throws org.codehaus.plexus.util.xml.pull.XmlPullParserException if there's a problem parsing the model 341 */ 342 protected void handleComment(XmlPullParser parser, Sink sink) throws XmlPullParserException { 343 if (isEmitComments()) { 344 sink.comment(getText(parser)); 345 } 346 } 347 348 /** 349 * Handles entities. 350 * 351 * <p>This is a default implementation, all entities are resolved and emitted as text 352 * events into the specified sink, except:</p> 353 * <ul> 354 * <li>the entities with names <code>#160</code>, <code>nbsp</code> and <code>#x00A0</code> 355 * are emitted as <code>nonBreakingSpace()</code> events.</li> 356 * </ul> 357 * 358 * @param parser A parser, not null. 359 * @param sink the sink to receive the events. Not null. 360 * @throws org.codehaus.plexus.util.xml.pull.XmlPullParserException if there's a problem parsing the model 361 */ 362 protected void handleEntity(XmlPullParser parser, Sink sink) throws XmlPullParserException { 363 String text = getText(parser); 364 365 String name = parser.getName(); 366 367 if ("#160".equals(name) || "nbsp".equals(name) || "#x00A0".equals(name)) { 368 sink.nonBreakingSpace(); 369 } else { 370 String unescaped = HtmlTools.unescapeHTML(text); 371 372 sink.text(unescaped); 373 } 374 } 375 376 /** 377 * Handles an unknown event. 378 * 379 * <p>This is a default implementation, all events are emitted as unknown 380 * events into the specified sink.</p> 381 * 382 * @param parser the parser to get the event from. 383 * @param sink the sink to receive the event. 384 * @param type the tag event type. This should be one of HtmlMarkup.TAG_TYPE_SIMPLE, 385 * HtmlMarkup.TAG_TYPE_START, HtmlMarkup.TAG_TYPE_END or HtmlMarkup.ENTITY_TYPE. 386 * It will be passed as the first argument of the required parameters to the Sink 387 * {@link 388 * org.apache.maven.doxia.sink.Sink#unknown(String, Object[], org.apache.maven.doxia.sink.SinkEventAttributes)} 389 * method. 390 */ 391 protected void handleUnknown(XmlPullParser parser, Sink sink, int type) { 392 SinkEventAttributeSet attribs = getAttributesFromParser(parser); 393 394 handleUnknown(parser.getName(), attribs, sink, type); 395 } 396 397 /** 398 * Emits an element that has no dedicated Sink event as 399 * {@link Sink#unknown(String, Object[], org.apache.maven.doxia.sink.SinkEventAttributes)}. 400 * 401 * @param elementName the name of the element. 402 * @param attribs the attributes of the element, may be null. 403 * @param sink the sink to receive the event. 404 * @param type the tag event type, passed as the first required parameter of the Sink event. This should be one 405 * of HtmlMarkup.TAG_TYPE_SIMPLE, HtmlMarkup.TAG_TYPE_START, HtmlMarkup.TAG_TYPE_END or 406 * HtmlMarkup.ENTITY_TYPE. 407 * @deprecated The {@code attribs} parameter will become 408 * {@link org.apache.maven.doxia.sink.SinkEventAttributes} in 2.3.0. 409 * An override of this signature will stop being called once the parameter type changes, with no error 410 * at build or run time, so move the override to the new signature when it lands. See <a href="https://github.com/apache/maven-doxia/issues/1074">issue 1074</a>. 411 */ 412 @Deprecated 413 protected void handleUnknown(String elementName, SinkEventAttributeSet attribs, Sink sink, int type) { 414 Object[] required = new Object[] {type}; 415 sink.unknown(elementName, required, attribs); 416 } 417 418 /** 419 * <p>isIgnorableWhitespace.</p> 420 * 421 * @return <code>true</code> if whitespace will be ignored, <code>false</code> otherwise. 422 * @see #setIgnorableWhitespace(boolean) 423 * @since 1.1 424 */ 425 protected boolean isIgnorableWhitespace() { 426 return ignorableWhitespace; 427 } 428 429 /** 430 * Specify that whitespace will be ignored. I.e.: 431 * <pre><tr> <td/> </tr></pre> 432 * is equivalent to 433 * <pre><tr><td/></tr></pre> 434 * 435 * @param ignorable <code>true</code> to ignore whitespace, <code>false</code> otherwise. 436 * @since 1.1 437 */ 438 protected void setIgnorableWhitespace(boolean ignorable) { 439 this.ignorableWhitespace = ignorable; 440 } 441 442 /** 443 * <p>isCollapsibleWhitespace.</p> 444 * 445 * @return <code>true</code> if text will collapse, <code>false</code> otherwise. 446 * @see #setCollapsibleWhitespace(boolean) 447 * @since 1.1 448 */ 449 protected boolean isCollapsibleWhitespace() { 450 return collapsibleWhitespace; 451 } 452 453 /** 454 * Specify that text will be collapsed. I.e.: 455 * <pre>Text Text</pre> 456 * is equivalent to 457 * <pre>Text Text</pre> 458 * 459 * @param collapsible <code>true</code> to allow collapsible text, <code>false</code> otherwise. 460 * @since 1.1 461 */ 462 protected void setCollapsibleWhitespace(boolean collapsible) { 463 this.collapsibleWhitespace = collapsible; 464 } 465 466 /** 467 * <p>isTrimmableWhitespace.</p> 468 * 469 * @return <code>true</code> if text will be trim, <code>false</code> otherwise. 470 * @see #setTrimmableWhitespace(boolean) 471 * @since 1.1 472 */ 473 protected boolean isTrimmableWhitespace() { 474 return trimmableWhitespace; 475 } 476 477 /** 478 * Specify that text will be collapsed. I.e.: 479 * <pre><p> Text </p></pre> 480 * is equivalent to 481 * <pre><p>Text</p></pre> 482 * 483 * @param trimmable <code>true</code> to allow trimmable text, <code>false</code> otherwise. 484 * @since 1.1 485 */ 486 protected void setTrimmableWhitespace(boolean trimmable) { 487 this.trimmableWhitespace = trimmable; 488 } 489 490 /** 491 * <p>getText.</p> 492 * 493 * @param parser A parser, not null. 494 * @return the {@link XmlPullParser#getText()} taking care of trimmable or collapsible configuration. 495 * @see XmlPullParser#getText() 496 * @see #isCollapsibleWhitespace() 497 * @see #isTrimmableWhitespace() 498 * @since 1.1 499 */ 500 protected String getText(XmlPullParser parser) { 501 String text = parser.getText(); 502 503 if (isTrimmableWhitespace()) { 504 text = text.trim(); 505 } 506 507 if (isCollapsibleWhitespace()) { 508 StringBuilder newText = new StringBuilder(); 509 String[] elts = DoxiaStringUtils.split(text, " \r\n"); 510 for (int i = 0; i < elts.length; i++) { 511 newText.append(elts[i]); 512 if ((i + 1) < elts.length) { 513 newText.append(" "); 514 } 515 } 516 text = newText.toString(); 517 } 518 519 return text; 520 } 521 522 /** 523 * Return the defined entities in a local doctype. I.e.: 524 * <pre> 525 * <!DOCTYPE foo [ 526 * <!ENTITY bar "&#x160;"> 527 * <!ENTITY bar1 "&#x161;"> 528 * ]> 529 * </pre> 530 * 531 * @return a map of the defined entities in a local doctype. 532 * @since 1.1 533 */ 534 protected Map<String, String> getLocalEntities() { 535 if (entities == null) { 536 entities = new LinkedHashMap<>(); 537 } 538 539 return entities; 540 } 541 542 /** 543 * <p>isValidate.</p> 544 * 545 * @return <code>true</code> if XML content will be validate, <code>false</code> otherwise. 546 * @since 1.1 547 */ 548 public boolean isValidate() { 549 return validate; 550 } 551 552 /** 553 * Specify a flag to validate or not the XML content. 554 * 555 * @param validate the validate to set 556 * @see #parse(Reader, Sink) 557 * @since 1.1 558 */ 559 public void setValidate(boolean validate) { 560 this.validate = validate; 561 } 562 563 /** 564 * @since 2.0.0-M4 565 */ 566 public boolean getAddDefaultEntities() { 567 return addDefaultEntities; 568 } 569 570 /** 571 * @since 2.0.0-M4 572 */ 573 public void setAddDefaultEntities(boolean addDefaultEntities) { 574 this.addDefaultEntities = addDefaultEntities; 575 } 576 577 // ---------------------------------------------------------------------- 578 // Private methods 579 // ---------------------------------------------------------------------- 580 581 /** 582 * Add an entity given by <code>entityName</code> and <code>entityValue</code> to {@link #entities}. 583 * <br> 584 * By default, we exclude the default XML entities: &amp;, &lt;, &gt;, &quot; and &apos;. 585 * 586 * @param parser not null 587 * @param entityName not null 588 * @param entityValue not null 589 * @throws XmlPullParserException if any 590 * @see XmlPullParser#defineEntityReplacementText(String, String) 591 */ 592 private void addEntity(XmlPullParser parser, String entityName, String entityValue) throws XmlPullParserException { 593 if (entityName.endsWith("amp") 594 || entityName.endsWith("lt") 595 || entityName.endsWith("gt") 596 || entityName.endsWith("quot") 597 || entityName.endsWith("apos")) { 598 return; 599 } 600 601 parser.defineEntityReplacementText(entityName, entityValue); 602 getLocalEntities().put(entityName, entityValue); 603 } 604 605 /** 606 * Handle entities defined in a local doctype as the following: 607 * <pre> 608 * <!DOCTYPE foo [ 609 * <!ENTITY bar "&#x160;"> 610 * <!ENTITY bar1 "&#x161;"> 611 * ]> 612 * </pre> 613 * 614 * @param parser not null 615 * @param text not null 616 * @throws XmlPullParserException if any 617 */ 618 private void addLocalEntities(XmlPullParser parser, String text) throws XmlPullParserException { 619 int entitiesCount = DoxiaStringUtils.countMatches(text, ENTITY_START); 620 if (entitiesCount > 0) { 621 // text should be foo [...] 622 int start = text.indexOf('['); 623 int end = text.lastIndexOf(']'); 624 if (start != -1 && end != -1) { 625 addDTDEntities(parser, text.substring(start + 1, end)); 626 } 627 } 628 } 629 630 /** 631 * Handle entities defined in external doctypes as the following: 632 * <pre> 633 * <!DOCTYPE foo [ 634 * <!-- These are the entity sets for ISO Latin 1 characters for the XHTML --> 635 * <!ENTITY % HTMLlat1 PUBLIC "-//W3C//ENTITIES Latin 1 for XHTML//EN" 636 * "http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent"> 637 * %HTMLlat1; 638 * ]> 639 * </pre> 640 * 641 * @param parser not null 642 * @param text not null 643 * @throws XmlPullParserException if any 644 */ 645 private void addDTDEntities(XmlPullParser parser, String text) throws XmlPullParserException { 646 int entitiesCount = DoxiaStringUtils.countMatches(text, ENTITY_START); 647 if (entitiesCount > 0) { 648 final String txt = DoxiaStringUtils.replace(text, ENTITY_START, "\n" + ENTITY_START); 649 try (BufferedReader reader = new BufferedReader(new StringReader(txt))) { 650 String line; 651 String tmpLine = ""; 652 Matcher matcher; 653 while ((line = reader.readLine()) != null) { 654 tmpLine += "\n" + line; 655 matcher = PATTERN_ENTITY_1.matcher(tmpLine); 656 if (matcher.find() && matcher.groupCount() == 7) { 657 String entityName = matcher.group(2); 658 String entityValue = matcher.group(5); 659 660 addEntity(parser, entityName, entityValue); 661 tmpLine = ""; 662 } else { 663 matcher = PATTERN_ENTITY_2.matcher(tmpLine); 664 if (matcher.find() && matcher.groupCount() == 8) { 665 String entityName = matcher.group(2); 666 String entityValue = matcher.group(5); 667 668 addEntity(parser, entityName, entityValue); 669 tmpLine = ""; 670 } 671 } 672 } 673 } catch (IOException e) { 674 // nop 675 } 676 } 677 } 678 679 /** 680 * Implementation of the callback mechanism <code>EntityResolver</code>. 681 * Using a mechanism of cached files in temp dir to improve performance when using the <code>XMLReader</code>. 682 */ 683 public static class CachedFileEntityResolver implements EntityResolver { 684 private static final Logger LOGGER = LoggerFactory.getLogger(CachedFileEntityResolver.class); 685 686 /** Map with systemId as key and the content of systemId as byte[]. */ 687 protected static final Map<String, byte[]> ENTITY_CACHE = new Hashtable<>(); 688 689 private static final Map<String, String> WELL_KNOWN_SYSTEM_IDS = new HashMap<>(); 690 691 static { 692 WELL_KNOWN_SYSTEM_IDS.put("http://www.w3.org/2001/xml.xsd", "xml.xsd"); 693 WELL_KNOWN_SYSTEM_IDS.put("https://www.w3.org/2001/xml.xsd", "xml.xsd"); 694 WELL_KNOWN_SYSTEM_IDS.put("http://maven.apache.org/xsd/xdoc-2.0.xsd", "xdoc-2.0.xsd"); 695 WELL_KNOWN_SYSTEM_IDS.put("https://maven.apache.org/xsd/xdoc-2.0.xsd", "xdoc-2.0.xsd"); 696 WELL_KNOWN_SYSTEM_IDS.put("http://maven.apache.org/xsd/fml-1.0.1.xsd", "fml-1.0.1.xsd"); 697 WELL_KNOWN_SYSTEM_IDS.put("https://maven.apache.org/xsd/fml-1.0.1.xsd", "fml-1.0.1.xsd"); 698 WELL_KNOWN_SYSTEM_IDS.put("http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent", "xhtml-lat1.ent"); 699 WELL_KNOWN_SYSTEM_IDS.put("https://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent", "xhtml-lat1.ent"); 700 } 701 702 public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { 703 byte[] res = ENTITY_CACHE.get(systemId); 704 // already cached? 705 if (res == null) { 706 if (WELL_KNOWN_SYSTEM_IDS.containsKey(systemId)) { 707 String resource = "/" + WELL_KNOWN_SYSTEM_IDS.get(systemId); 708 URL url = getClass().getResource(resource); 709 if (url != null) { 710 LOGGER.debug( 711 "Resolving SYSTEM '{}' from well-known classpath resource '{}'", systemId, resource); 712 res = toByteArray(url); 713 } 714 } 715 716 if (res == null) { 717 URI uri = URI.create(systemId); 718 if (uri.getScheme() == null) { 719 uri = Paths.get(systemId).toUri(); 720 } 721 722 LOGGER.debug("Resolving SYSTEM '{}' from URI resource '{}'", systemId, uri); 723 res = toByteArray(uri.toURL()); 724 } 725 726 ENTITY_CACHE.put(systemId, res); 727 } else { 728 LOGGER.debug("Resolved SYSTEM '{}' from cache", systemId); 729 } 730 731 InputSource is = new InputSource(new ByteArrayInputStream(res)); 732 is.setPublicId(publicId); 733 is.setSystemId(systemId); 734 735 return is; 736 } 737 738 /** 739 * @param url not null 740 * @return return an array of byte 741 * @throws SAXException if any 742 */ 743 private static byte[] toByteArray(URL url) throws SAXException { 744 try (InputStream is = url.openStream()) { 745 if (is == null) { 746 throw new SAXException("Cannot open stream from the url: " + url); 747 } 748 ByteArrayOutputStream out = new ByteArrayOutputStream(); 749 byte[] buffer = new byte[8192]; 750 int n; 751 while ((n = is.read(buffer)) != -1) { 752 out.write(buffer, 0, n); 753 } 754 return out.toByteArray(); 755 } catch (IOException e) { 756 throw new SAXException(e); 757 } 758 } 759 } 760}