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. &#38;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;#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>&lt;tr&gt; &lt;td/&gt; &lt;/tr&gt;</pre>
432     * is equivalent to
433     * <pre>&lt;tr&gt;&lt;td/&gt;&lt;/tr&gt;</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>&lt;p&gt; Text &lt;/p&gt;</pre>
480     * is equivalent to
481     * <pre>&lt;p&gt;Text&lt;/p&gt;</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     * &lt;!DOCTYPE foo [
526     *   &lt;!ENTITY bar "&#38;#x160;"&gt;
527     *   &lt;!ENTITY bar1 "&#38;#x161;"&gt;
528     * ]&gt;
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: &#38;amp;, &#38;lt;, &#38;gt;, &#38;quot; and &#38;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     * &lt;!DOCTYPE foo [
609     *   &lt;!ENTITY bar "&#38;#x160;"&gt;
610     *   &lt;!ENTITY bar1 "&#38;#x161;"&gt;
611     * ]&gt;
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     * &lt;!DOCTYPE foo [
634     *   &lt;!-- These are the entity sets for ISO Latin 1 characters for the XHTML --&gt;
635     *   &lt;!ENTITY % HTMLlat1 PUBLIC "-//W3C//ENTITIES Latin 1 for XHTML//EN"
636     *          "http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent"&gt;
637     *   %HTMLlat1;
638     * ]&gt;
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}