diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/feature/Features.java b/api/maven-api-core/src/main/java/org/apache/maven/api/feature/Features.java index 04ab33d8643c..5527ed70b7f4 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/feature/Features.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/feature/Features.java @@ -37,6 +37,8 @@ public final class Features { */ public static final String BUILDCONSUMER = "maven.experimental.buildconsumer"; + public static final String XINCLUDE = "maven.experimental.xinclude"; + private Features() {} /** @@ -60,6 +62,18 @@ public static boolean buildConsumer(@Nullable Session session) { return buildConsumer(session != null ? session.getUserProperties() : null); } + public static boolean xinclude(@Nullable Properties userProperties) { + return doGet(userProperties, XINCLUDE, true); + } + + public static boolean xinclude(@Nullable Map userProperties) { + return doGet(userProperties, XINCLUDE, true); + } + + public static boolean xinclude(@Nullable Session session) { + return xinclude(session != null ? session.getUserProperties() : null); + } + private static boolean doGet(Properties userProperties, String key, boolean def) { return doGet(userProperties != null ? userProperties.get(key) : null, def); } diff --git a/maven-bom/pom.xml b/maven-bom/pom.xml index 0e43dc90a987..478abd6fa863 100644 --- a/maven-bom/pom.xml +++ b/maven-bom/pom.xml @@ -127,6 +127,11 @@ under the License. maven-api-xml ${project.version} + + org.apache.maven + maven-stax-xinclude + ${project.version} + org.apache.maven maven-model-builder diff --git a/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java b/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java index 182df18faccf..805f22e6eb5b 100644 --- a/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java +++ b/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java @@ -234,7 +234,7 @@ void testReadInvalidPom() throws Exception { // single project build entry point Exception ex = assertThrows(Exception.class, () -> projectBuilder.build(pomFile, configuration)); - assertThat(ex.getMessage(), containsString("Received non-all-whitespace CHARACTERS or CDATA event")); + assertThat(ex.getMessage(), containsString("expected START_TAG or END_TAG not CHARACTERS")); // multi projects build entry point ProjectBuildingException pex = assertThrows( @@ -245,8 +245,7 @@ void testReadInvalidPom() throws Exception { assertThat(pex.getResults().get(0).getProblems().size(), greaterThan(0)); assertThat( pex.getResults(), - contains(projectBuildingResultWithProblemMessage( - "Received non-all-whitespace CHARACTERS or CDATA event in nextTag()"))); + contains(projectBuildingResultWithProblemMessage("expected START_TAG or END_TAG not CHARACTERS"))); } @Test diff --git a/maven-model-builder/pom.xml b/maven-model-builder/pom.xml index 35efe2800023..d730790e8de1 100644 --- a/maven-model-builder/pom.xml +++ b/maven-model-builder/pom.xml @@ -68,6 +68,10 @@ under the License. org.apache.maven maven-builder-support + + org.apache.maven + maven-stax-xinclude + org.eclipse.sisu diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java index 3f6265736ec9..5a6af4c75e62 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java @@ -1030,6 +1030,7 @@ private org.apache.maven.api.model.Model doReadFileModel( options.put(ModelProcessor.IS_STRICT, strict); options.put(ModelProcessor.SOURCE, modelSource); options.put(ModelReader.ROOT_DIRECTORY, request.getRootDirectory()); + options.put(ModelReader.XINCLUDE, Features.xinclude(request.getUserProperties())); InputSource source; if (request.isLocationTracking()) { @@ -1040,9 +1041,15 @@ private org.apache.maven.api.model.Model doReadFileModel( } try { - model = modelProcessor - .read(modelSource.getInputStream(), options) - .getDelegate(); + if (modelSource instanceof FileModelSource) { + model = modelProcessor + .read(((FileModelSource) modelSource).getFile(), options) + .getDelegate(); + } else { + model = modelProcessor + .read(modelSource.getInputStream(), options) + .getDelegate(); + } } catch (ModelParseException e) { if (!strict) { throw e; @@ -1051,9 +1058,15 @@ private org.apache.maven.api.model.Model doReadFileModel( options.put(ModelProcessor.IS_STRICT, Boolean.FALSE); try { - model = modelProcessor - .read(modelSource.getInputStream(), options) - .getDelegate(); + if (modelSource instanceof FileModelSource) { + model = modelProcessor + .read(((FileModelSource) modelSource).getFile(), options) + .getDelegate(); + } else { + model = modelProcessor + .read(modelSource.getInputStream(), options) + .getDelegate(); + } } catch (ModelParseException ne) { // still unreadable even in non-strict mode, rethrow original error throw e; diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/io/DefaultModelReader.java b/maven-model-builder/src/main/java/org/apache/maven/model/io/DefaultModelReader.java index 276856b1f93e..ab3120b3dff0 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/io/DefaultModelReader.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/io/DefaultModelReader.java @@ -25,6 +25,8 @@ import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; import java.io.File; import java.io.IOException; @@ -39,6 +41,8 @@ import org.apache.maven.model.Model; import org.apache.maven.model.building.ModelSourceTransformer; import org.apache.maven.model.v4.MavenStaxReader; +import org.apache.maven.stax.xinclude.XInclude; +import org.codehaus.stax2.io.Stax2FileSource; /** * Handles deserialization of a model from some kind of textual format like XML. @@ -100,14 +104,39 @@ private Path getRootDirectory(Map options) { return (Path) value; } + private boolean getXInclude(Map options) { + Object value = (options != null) ? options.get(XINCLUDE) : null; + return value instanceof Boolean && (Boolean) value; + } + private Model read(InputStream input, Path pomFile, Map options) throws IOException { try { - XMLInputFactory factory = new com.ctc.wstx.stax.WstxInputFactory(); - factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); - XMLStreamReader parser = factory.createXMLStreamReader(input); - InputSource source = getSource(options); boolean strict = isStrict(options); + Path rootDirectory = getRootDirectory(options); + + Source xmlSource; + if (pomFile != null) { + if (input != null) { + xmlSource = new StaxPathInputSource(pomFile, input); + } else { + xmlSource = new Stax2FileSource(pomFile.toFile()); + } + } else { + xmlSource = new StreamSource(input); + } + + XMLStreamReader parser; + // We only support xml entities and xinclude when reading a file in strict mode + if (pomFile != null && strict && getXInclude(options)) { + parser = XInclude.xinclude(xmlSource, new LocalXmlResolver(rootDirectory)); + } else { + XMLInputFactory factory = new com.ctc.wstx.stax.WstxInputFactory(); + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + parser = factory.createXMLStreamReader(xmlSource); + } + MavenStaxReader mr = new MavenStaxReader(); mr.setAddLocationInformation(source != null); Model model = new Model(mr.read( @@ -132,7 +161,6 @@ private Model read(InputStream input, Path pomFile, Map options) thro private Model read(Reader reader, Path pomFile, Map options) throws IOException { try { XMLInputFactory factory = new com.ctc.wstx.stax.WstxInputFactory(); - factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); XMLStreamReader parser = factory.createXMLStreamReader(reader); InputSource source = getSource(options); @@ -155,4 +183,18 @@ private Model read(Reader reader, Path pomFile, Map options) throws I throw new IOException("Unable to transform pom", e); } } + + private static class StaxPathInputSource extends Stax2FileSource { + private final InputStream input; + + StaxPathInputSource(Path pomFile, InputStream input) { + super(pomFile.toFile()); + this.input = input; + } + + @Override + public InputStream constructInputStream() throws IOException { + return input; + } + } } diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/io/LocalXmlResolver.java b/maven-model-builder/src/main/java/org/apache/maven/model/io/LocalXmlResolver.java new file mode 100644 index 000000000000..456437893f58 --- /dev/null +++ b/maven-model-builder/src/main/java/org/apache/maven/model/io/LocalXmlResolver.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.model.io; + +import javax.xml.stream.XMLResolver; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.stream.StreamSource; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class LocalXmlResolver implements XMLResolver { + + private final Path rootDirectory; + + public LocalXmlResolver(Path rootDirectory) { + this.rootDirectory = rootDirectory != null ? rootDirectory.normalize() : null; + } + + @Override + public Object resolveEntity(String publicID, String systemID, String baseURI, String namespace) + throws XMLStreamException { + if (rootDirectory == null) { + return null; + } + if (systemID == null) { + throw new XMLStreamException("systemID is null"); + } + if (baseURI == null) { + throw new XMLStreamException("baseURI is null"); + } + URI baseUri; + try { + baseUri = new URI(baseURI).normalize(); + } catch (URISyntaxException e) { + throw new XMLStreamException("Invalid syntax for baseURI URI: " + baseURI, e); + } + URI sysUri; + try { + sysUri = new URI(systemID).normalize(); + } catch (URISyntaxException e) { + throw new XMLStreamException("Invalid syntax for systemID URI: " + systemID, e); + } + if (sysUri.getScheme() != null) { + throw new XMLStreamException("systemID must be a relative URI: " + systemID); + } + Path base = Paths.get(baseUri).normalize(); + if (!base.startsWith(rootDirectory)) { + return null; + } + Path sys = Paths.get(sysUri.getSchemeSpecificPart()).normalize(); + if (sys.isAbsolute()) { + throw new XMLStreamException("systemID must be a relative path: " + systemID); + } + Path res = base.resolveSibling(sys).normalize(); + if (!res.startsWith(rootDirectory)) { + throw new XMLStreamException("systemID cannot refer to outside rootDirectory: " + systemID); + } + try { + return new StreamSource(Files.newInputStream(res), res.toUri().toASCIIString()); + } catch (IOException e) { + throw new XMLStreamException("Unable to create Source for " + systemID + ": " + e.getMessage(), e); + } + } +} diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/io/ModelReader.java b/maven-model-builder/src/main/java/org/apache/maven/model/io/ModelReader.java index 601160d59d29..988a3e853439 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/io/ModelReader.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/io/ModelReader.java @@ -45,6 +45,12 @@ public interface ModelReader { */ String INPUT_SOURCE = "org.apache.maven.model.io.inputSource"; + /** + * Name of the property used to store a boolean {@code true} if XInclude supports + * is needed. + */ + String XINCLUDE = "xinclude"; + /** * Name of the property used to store the project's root directory to use with * XInclude support. diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/io/LocalXmlResolverTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/io/LocalXmlResolverTest.java new file mode 100644 index 000000000000..9d80b114100b --- /dev/null +++ b/maven-model-builder/src/test/java/org/apache/maven/model/io/LocalXmlResolverTest.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.model.io; + +import javax.xml.stream.XMLStreamException; + +import java.io.File; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LocalXmlResolverTest { + + @Test + void testAbsoluteUriWithRelativePath() { + XMLStreamException exception = + assertThrows(XMLStreamException.class, () -> new LocalXmlResolver(Paths.get("/users/base")) + .resolveEntity(null, "file:foo/bar.xml", "file:/users/base/pom.xml", null)); + assertTrue(exception.getMessage().contains("systemID must be a relative URI"), exception.getMessage()); + } + + @Test + void testAbsoluteUriWithAbsolutePath() { + XMLStreamException exception = + assertThrows(XMLStreamException.class, () -> new LocalXmlResolver(Paths.get("/users/base")) + .resolveEntity(null, "file:/foo/bar.xml", "file:/users/base/pom.xml", null)); + assertTrue(exception.getMessage().contains("systemID must be a relative URI"), exception.getMessage()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void testRelativeUriWithDifferentAbsolutePath() { + XMLStreamException exception = + assertThrows(XMLStreamException.class, () -> new LocalXmlResolver(Paths.get("/users/base")) + .resolveEntity(null, "/foo/bar.xml", "file:/users/base/pom.xml", null)); + assertTrue(exception.getMessage().contains("systemID must be a relative path"), exception.getMessage()); + } + + @Test + void testRelativeUriWithDifferentAbsolutePathWin() { + // `C:/users` is parsed as a `C` scheme ! + XMLStreamException exception = + assertThrows(XMLStreamException.class, () -> new LocalXmlResolver(Paths.get("/users/base")) + .resolveEntity(null, "C:/foo/bar.xml", "file:/users/base/pom.xml", null)); + assertTrue(exception.getMessage().contains("systemID must be a relative URI"), exception.getMessage()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void testRelativeUriWithSameAbsolutePath() { + XMLStreamException exception = + assertThrows(XMLStreamException.class, () -> new LocalXmlResolver(Paths.get("/users/base")) + .resolveEntity(null, "/users/base/foo/bar.xml", "file:/users/base/pom.xml", null)); + assertTrue(exception.getMessage().contains("systemID must be a relative path"), exception.getMessage()); + } + + @Test + void testRelativeUriWithSameAbsolutePathWin() { + // `C:/users` is parsed as a `C` scheme ! + XMLStreamException exception = + assertThrows(XMLStreamException.class, () -> new LocalXmlResolver(Paths.get("/users/base")) + .resolveEntity(null, "C:/users/base/foo/bar.xml", "file:/users/base/pom.xml", null)); + assertTrue(exception.getMessage().contains("systemID must be a relative URI"), exception.getMessage()); + } + + @Test + void testRelativeUriWithRelativeUriToParentOutsideTree() { + XMLStreamException exception = + assertThrows(XMLStreamException.class, () -> new LocalXmlResolver(Paths.get("/users/base")) + .resolveEntity(null, "../../bar.xml", "file:/users/base/foo/pom.xml", null)); + assertTrue( + exception.getMessage().contains("systemID cannot refer to outside rootDirectory"), + exception.getMessage()); + } + + @Test + void testRelativeUriWithRelativeUriToParentInsideTree() { + XMLStreamException exception = + assertThrows(XMLStreamException.class, () -> new LocalXmlResolver(Paths.get("/users/base")) + .resolveEntity(null, "../bar.xml", "file:/users/base/foo/pom.xml", null)); + assertTrue( + exception.getMessage().contains("Unable to create Source") + && exception.getMessage().contains("/users/base/bar.xml".replace('/', File.separatorChar)), + exception.getMessage()); + } + + @Test + void testRelativeUriWithRelativePath() { + XMLStreamException exception = + assertThrows(XMLStreamException.class, () -> new LocalXmlResolver(Paths.get("/users/base")) + .resolveEntity(null, "foo/bar.xml", "file:/users/base/pom.xml", null)); + assertTrue( + exception.getMessage().contains("Unable to create Source") + && exception.getMessage().contains("/users/base/foo/bar.xml".replace('/', File.separatorChar)), + exception.getMessage()); + } +} diff --git a/maven-model/src/test/java/org/apache/maven/model/v4/ModelXmlTest.java b/maven-model/src/test/java/org/apache/maven/model/v4/ModelXmlTest.java index 71fc8e785946..95abd9eb0dce 100644 --- a/maven-model/src/test/java/org/apache/maven/model/v4/ModelXmlTest.java +++ b/maven-model/src/test/java/org/apache/maven/model/v4/ModelXmlTest.java @@ -30,9 +30,57 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; class ModelXmlTest { + @Test + void testExternalEntities() throws Exception { + String xml = "\n" + "\n" + + " ]>\n" + + "\n" + + "\n" + + " 4.0.0\n" + + "\n" + + " \n" + + " org.apache.maven\n" + + " maven-parent\n" + + " 40\n" + + " \n" + + " \n" + + "\n" + + " org.apache.maven.daemon\n" + + " mvnd\n" + + " 1.0-m7-SNAPSHOT\n" + + " &desc;\n" + + "\n"; + MavenStaxReader staxReader = new MavenStaxReader(); + staxReader.setXmlResolver((publicID, systemID, baseURI, namespace) -> { + if ("file:desc.xml".equals(systemID)) { + return "foo"; + } + return null; + }); + Model model = staxReader.read(new StringReader(xml)); + assertNotNull(model); + assertEquals("foo", model.getDescription()); + } + + @Test + void testDefaultEntities() throws Exception { + String xml = "\n" + + "\n" + + " 4.0.0\n" + + " org.apache.maven.daemon\n" + + " mvndœ\n" + + " 1.0-m7-SNAPSHOT\n" + + "\n"; + Model model = new MavenStaxReader().read(new StringReader(xml), false, null); + assertNotNull(model); + assertEquals("mvndœ", model.getArtifactId()); + } + @Test void testXmlRoundtripWithProperties() throws Exception { Map props = new LinkedHashMap<>(); diff --git a/maven-stax-xinclude/pom.xml b/maven-stax-xinclude/pom.xml new file mode 100644 index 000000000000..5ada18562597 --- /dev/null +++ b/maven-stax-xinclude/pom.xml @@ -0,0 +1,39 @@ + + + + 4.0.0 + + org.apache.maven + maven + 4.0.0-alpha-8-SNAPSHOT + + + maven-stax-xinclude + Implementation of Maven API XML + Provides the implementation classes for the Maven API XML + + + + com.fasterxml.woodstox + woodstox-core + + + + diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/DOMXMLElement.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/DOMXMLElement.java new file mode 100644 index 000000000000..ce80683b4bfb --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/DOMXMLElement.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import java.util.ArrayList; +import java.util.List; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * This class is based upon a class of the same name in Apache Woden. + */ +class DOMXMLElement implements XMLElement { + + private Element fSource; + + DOMXMLElement() {} + + DOMXMLElement(Element fSource) { + this.fSource = fSource; + } + + /* + * (non-Javadoc) + * @see org.apache.woden.XMLElement#getSource() + */ + public final Element getSource() { + return fSource; + } + + /* + * @see org.apache.woden.XMLElement#setSource(java.lang.Object) + */ + public void setSource(Element elem) { + fSource = elem; + } + + /* + * (non-Javadoc) + * @see org.apache.woden.XMLElement#getLocalName() + */ + public final String getLocalName() { + return fSource != null ? doGetLocalName() : null; + } + + /* + * (non-Javadoc) + * @see org.apache.woden.XMLElement#getNextSiblingElement() + */ + public final XMLElement getNextSiblingElement() { + return fSource != null ? doGetNextSiblingElement() : null; + } + + /* + * (non-Javadoc) + * @see org.apache.woden.XMLElement#getChildElements() + */ + public final List> getChildElements() { + return fSource != null ? doGetChildElements() : null; + } + + protected String doGetLocalName() { + return fSource.getLocalName(); + } + + protected XMLElement doGetFirstChildElement() { + for (Node node = fSource.getFirstChild(); node != null; node = node.getNextSibling()) { + if (node instanceof Element) { + return new DOMXMLElement((Element) node); + } + } + return null; // no child element found + } + + protected XMLElement doGetNextSiblingElement() { + for (Node node = fSource.getNextSibling(); node != null; node = node.getNextSibling()) { + if (node instanceof Element) { + return new DOMXMLElement((Element) node); + } + } + return null; // no sibling element found + } + + protected List> doGetChildElements() { + List> children = new ArrayList<>(); + XMLElement temp = doGetFirstChildElement(); + while (temp != null) { + children.add(temp); + temp = temp.getNextSiblingElement(); + } + return children; + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/DOMXMLElementEvaluator.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/DOMXMLElementEvaluator.java new file mode 100644 index 000000000000..edc1a444f61a --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/DOMXMLElementEvaluator.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import java.util.Map; +import java.util.Objects; + +import com.ctc.wstx.dtd.DTDAttribute; +import com.ctc.wstx.dtd.DTDElement; +import com.ctc.wstx.dtd.DTDSubset; +import com.ctc.wstx.util.PrefixedName; +import org.w3c.dom.Element; + +/** + * This class extends the XMLElementEvaluator to support the DOM implementation in XMLElement. + *

+ * This class is based upon a class of the same name in Apache Woden. + */ +class DOMXMLElementEvaluator extends XMLElementEvaluator { + + private DTDSubset dtd; + + /** + * Constructs a new DOMXMLElementEvaluator to evaluate a XPointer on a DOM Element. + * + * @param xpointer an XPointer to evaluate + * @param element an DOM Element to be evaluated + */ + DOMXMLElementEvaluator(XPointer xpointer, Element element, DTDSubset dtd) { + super(xpointer, createXMLElement(element)); + this.dtd = dtd; + } + + /* + * (non-Javadoc) + * @see org.apache.woden.internal.xpointer.XMLElementEvaluator#testElementShorthand(org.apache.woden.XMLElement, java.lang.String) + */ + public boolean testElementShorthand(XMLElement element, String shorthand) { + // Simple http://www.w3.org/TR/xml-id/ support for now until we support full scheme based ID's. + Element domElement = element.getSource(); + String attr = domElement.getAttributeNS("http://www.w3.org/XML/1998/namespace", "id"); + if (Objects.equals(attr, shorthand)) { + return true; + } + if (dtd != null) { + Map map = dtd.getElementMap(); + if (map != null) { + DTDElement dtdElement = map.get(new PrefixedName(domElement.getPrefix(), domElement.getLocalName())); + if (dtdElement != null) { + DTDAttribute dtdAttribute = dtdElement.getIdAttribute(); + if (dtdAttribute != null) { + attr = domElement.getAttribute(dtdAttribute.getName().getLocalName()); + if (Objects.equals(attr, shorthand)) { + return true; + } + } + } + } + } + return false; + } + + /** + * Evaluates the XPointer on the root Element and returns the resulting Element or null. + * + * @return an Element from the resultant evaluation of the root Element or null if evaluation fails + */ + public Element evaluateElement() { + XMLElement element = evaluate(); + if (element != null) { + return element.getSource(); + } + return null; + } + + // Private methods + private static XMLElement createXMLElement(Element element) { + DOMXMLElement domXMLElement = new DOMXMLElement(); + domXMLElement.setSource(element); + return domXMLElement; + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/ElementPointerPart.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/ElementPointerPart.java new file mode 100644 index 000000000000..a79c5ce8b2ce --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/ElementPointerPart.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * ElementPointerPart is a class which represents the element() scheme for the XPointer Framework. + * The specification is defined at http://www.w3.org/TR/xptr-element/ + *

+ * This class is immutable. + *

+ * This class is based upon a class of the same name in Apache Woden. + */ +class ElementPointerPart implements PointerPart { + private final String ncname; + private final List childSequence; + + /** + * Constructs an ElementPointerPart with only an elementID NCName. + * + * @param elementID an NCName of the elementID to reference. + * @throws NullPointerException is a null elementID is given. + */ + ElementPointerPart(String elementID) { + if (elementID == null) { + throw new NullPointerException("The elementID argument is null."); + } + this.ncname = elementID; + this.childSequence = null; + } + + /** + * Constructs an ElementPointerPart with only a childSequence. + * + * @param childSequence a List of Integers representing the child sequence. + * @throws NullPointerException if childSequence is null. + * @throws IllegalArgumentException if childSequence is empty or contains elements other than Integers. + */ + ElementPointerPart(List childSequence) { + if (childSequence == null) { + throw new NullPointerException("The childSequence argument is null."); + } + if (childSequence.isEmpty()) { + throw new IllegalArgumentException("The childSequence list is empty."); + } + this.ncname = null; + this.childSequence = childSequence; + } + + /** + * Constructs an ElementPointerPart with both an NCName and a childSequence. + * + * @param elementID an NCName of the elementID to reference. + * @param childSequence a List of Integers representing the child sequence. + * @throws NullPointerException if elementID or childSequence are null. + * @throws IllegalArgumentException if childSequence is empty or contains elements other than Integers. + */ + ElementPointerPart(String elementID, List childSequence) { + if (elementID == null) { + throw new NullPointerException("The elementID argument is null."); + } + if (childSequence == null) { + throw new NullPointerException("The childSequence argument is null."); + } + if (childSequence.isEmpty()) { + throw new IllegalArgumentException("The childSequence list is empty."); + } + if (childSequence.contains(0)) { + throw new IllegalArgumentException("the childSequence list must only contain Integers bigger than 0."); + } + + this.ncname = elementID; + this.childSequence = childSequence; + } + + /** + * Returns the NCName for this Element PointerPart. + * + * @return an NCName if it exists in this Element PointerPart, otherwise null. + */ + public String getNCName() { + return ncname; + } + + /** + * Returns the child sequence of this Element PointerPart. + * + * @return an Integer[] of the child sequence for this element pointer part, or an empty array if none exists. + */ + public List getChildSequence() { + return Collections.unmodifiableList(childSequence); + } + + /** + * Checks if this Element PointerPart has a NCName or not. + * + * @return a boolean, true if it has a NCName or false if not. + */ + public boolean hasNCName() { + return ncname != null; + } + + /** + * Checks if this Element PointerPart has a childSequence or not. + * + * @return a boolean, true if this has a childSequence or false if not. + */ + public boolean hasChildSequence() { + return childSequence != null; + } + + /* + *(non-Javadoc) + * @see org.apache.woden.xpointer.PointerPart#toString() + */ + public String toString() { + String schemeData; + if (childSequence == null) { + schemeData = ncname.toString(); + } else if (ncname == null) { + schemeData = serialiseChildSequence(); + } else { + schemeData = ncname.toString() + serialiseChildSequence(); + } + return "element(" + schemeData + ")"; + } + + /** + * Serialises the child sequence and returns it as a string. + * + * @return a String of the serialised child sequence. + */ + private String serialiseChildSequence() { + StringBuilder buffer = new StringBuilder(); + for (Integer child : childSequence) { + buffer.append("/").append(child.toString()); + } + return buffer.toString(); + } + + /** + * Deserialises the schemaData for an ElementPointerPart and constructs a new ElementPointerPart from it. + * + * @param schemeData a String of the schemeaData parsed from the string XPointer. + * @return an ElementPointerPart representing the parsed schemaData. + * @throws IllegalArgumentException if the schemeData has invalid scheme syntax. + */ + static ElementPointerPart parseFromString(final String schemeData) throws InvalidXPointerException { + List childSequence; + String elementID = null; + int startChar; + int endChar; + + // Find an NCName if it exists? + startChar = schemeData.indexOf("/"); + // -1 Only an NCName. 0 No NCName. > 1 An NCName. + + switch (startChar) { + case -1: // Only an NCName. + elementID = schemeData; + if (!NCName.isValid(elementID)) { + throw new InvalidXPointerException("Invalid NCName in the XPointer", schemeData); + } + return new ElementPointerPart(elementID); + case 0: // No NCName. + break; + default: // An NCName. + elementID = schemeData.substring(0, startChar); + if (!NCName.isValid(elementID)) { + throw new InvalidXPointerException("Invalid NCName in the XPointer", schemeData, 0, startChar); + } + break; + } + + // Find remaining child sequence. + childSequence = new ArrayList<>(); + + endChar = schemeData.indexOf("/", startChar + 1); + // -1 Only single child sequence element. > 0 A childSequence. + + if (endChar < 0) { // Only single child sequence element. + childSequence.add(parseIntegerFromChildSequence(schemeData, startChar + 1, schemeData.length())); + } else { // Multiple child sequence elements. + while (true) { + if (endChar < 0) { // Last integer. + childSequence.add(parseIntegerFromChildSequence(schemeData, startChar + 1, schemeData.length())); + break; + } else { // Inner sequence integer. + childSequence.add(parseIntegerFromChildSequence(schemeData, startChar + 1, endChar)); + startChar = endChar; + endChar = schemeData.indexOf("/", startChar + 1); + } + } + } + + if (elementID == null) { // Only a childSequence + return new ElementPointerPart(childSequence); + } else { // Both NCName and childSequence + return new ElementPointerPart(elementID, childSequence); + } + } + + /** + * Parses a String for an integer between two indices and returns this as an Integer. + * + * @param string a String to parse. + * @param start an int char index to the start of the Integer. + * @param end an int char index to the end of the Integer. + * @return an Integer resulting from parsing the given String in the index range. + * @throws IllegalArgumentException if the given char range does not contain an integer. + */ + private static Integer parseIntegerFromChildSequence(String string, int start, int end) + throws InvalidXPointerException { + if (start < end) { // Make sure sub string is not of zero length. + try { // Make sure the integer is valid. + return Integer.valueOf(string.substring(start, end)); + } catch (NumberFormatException e) { + throw new InvalidXPointerException( + "The child sequence part contained an invalid integer.", string, start, end); + } + } else { + throw new InvalidXPointerException( + "The child sequence part contained an empty item at " + String.valueOf(start), string, start, end); + } + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/FragmentReader.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/FragmentReader.java new file mode 100644 index 000000000000..b76aac7f4a3c --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/FragmentReader.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +class FragmentReader extends StreamReaderDelegate { + + XMLStreamReader delegate; + int depth; + int current = START_DOCUMENT; + int state = 0; + + FragmentReader(XMLStreamReader delegate) { + this.delegate = delegate; + this.depth = 1; + } + + @Override + public int next() throws XMLStreamException { + if (state == 0) { + current = getDelegate().getEventType(); + state++; + } else { + if (depth == 0) { + current = END_DOCUMENT; + } else { + current = super.next(); + if (current == START_ELEMENT) { + depth++; + } else if (current == END_ELEMENT) { + depth--; + } + } + } + return current; + } + + @Override + public int getEventType() { + return current; + } + + @Override + public boolean hasNext() throws XMLStreamException { + return current != END_DOCUMENT; + } + + @Override + protected XMLStreamReader getDelegate() { + return delegate; + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/InvalidXPointerException.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/InvalidXPointerException.java new file mode 100644 index 000000000000..d37c9b941d9e --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/InvalidXPointerException.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +/** + * This class represents Exceptions that can happen during parsing an XPointer Expression. + *

+ * This class is based upon a class of the same name in Apache Woden. + */ +public class InvalidXPointerException extends Exception { + + private final String fragment; + private final Integer startChar; + private final Integer endChar; + + /** + * Constructs an InvalidXPointerException with a message and fragment properties. + * + * @param message a String message of error + * @param fragment a String fragment of the cause + */ + public InvalidXPointerException(String message, String fragment) { + this(message, fragment, null, null, null); + } + + /** + * Constructs an InvalidXPointerException with a message and fragment properties. + *

+ * It also has a Throwable argument to support exception chaining. + * + * @param message a String message of error + * @param fragment a String fragment of the cause of the error + * @param cause a Throwable which caused this exception to be thrown + */ + public InvalidXPointerException(String message, String fragment, Throwable cause) { + this(message, fragment, null, null, cause); + } + + /** + * Constructs an InvalidXPointerException with a message and fragment properties, + * and index to the cause inside the fragment. + * + * @param message a String message of error + * @param fragment a String fragment of the cause of the error + * @param startChar a int char index to the start of the cause in the fragment + * @param endChar a int char index to the end of the cause in the fragment + */ + public InvalidXPointerException(String message, String fragment, int startChar, int endChar) { + this(message, fragment, Integer.valueOf(startChar), Integer.valueOf(endChar), null); + } + + /** + * Constructs an InvalidXPointerException with a message and fragment properties, + * and index to the cause inside the fragment. + *

+ * It also has a Throwable argument to support exception chaining. + * + * @param message a String message of error + * @param fragment a String fragment of the cause of the error + * @param startChar an int char index to the start of the cause in the fragment + * @param endChar an int char index to the end of the cause in the fragment + * @param cause a Throwable which caused the exception to be thrown + */ + public InvalidXPointerException(String message, String fragment, int startChar, int endChar, Throwable cause) { + this(message, fragment, Integer.valueOf(startChar), Integer.valueOf(endChar), cause); + } + + /** + * Constructs a new InvalidXPointerException. + * This constructor is called by all of the above constructors and stores the in indexes and Integers internally. + * + * @param message a String message of error + * @param fragment a String fragment of the cause of the error + * @param startChar an Integer char index to the start of the cause in the fragment + * @param endChar an Integer char index to the end of the cause in the fragment + * @param cause a Throwable which caused the exception to be thrown + */ + private InvalidXPointerException( + String message, String fragment, Integer startChar, Integer endChar, Throwable cause) { + super(message, cause); + this.fragment = fragment; + this.startChar = startChar; + this.endChar = endChar; + } + + /** + * Returns the fragment String stored inside this exception. + * + * @return a String fragment + */ + public String getFragment() { + return fragment; + } + + /** + * Returns the startChar index of the cause of this error in the fragment. + * + * @return an Integer of the startChar index if one exists, otherwise null + */ + public Integer getStartChar() { + return startChar; + } + + /** + * Returns the endChar index of the cause of this error in the fragment. + * + * @return an Integer of the startChar index if one exists, otherwise null + */ + public Integer getEndChar() { + return endChar; + } + + /* + * (non-Javadoc) + * @see java.lang.Throwable#toString() + */ + public String toString() { + String postString; + if (startChar != null && endChar != null) { + postString = "{XPointer: " + fragment + ", start: " + startChar + ", end: " + endChar + ", substr: " + + fragment.substring(startChar, endChar) + "}"; + } else { + postString = "{XPointer: " + fragment + "}"; + } + return "InvalidXPointerException: " + getMessage() + ". " + postString; + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/NCName.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/NCName.java new file mode 100644 index 000000000000..523f3d2b5a3f --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/NCName.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +/** + * This class represents the data type NCName use for XML non-colonized names. + */ +@SuppressWarnings("checkstyle:MagicNumber") +class NCName { + + public static boolean isValid(String stValue) { + for (int scan = 0; scan < stValue.length(); scan++) { + char c = stValue.charAt(scan); + boolean bValid = c != ':' && (scan == 0 ? is11NameStartChar(c, true) : is11NameChar(c, true)); + if (!bValid) { + return false; + } + } + return true; + } + + public static boolean is11NameStartChar(int c, boolean ncname) { + if (c <= 0x7A) { // 'z' or earlier + if (c >= 0x61) { // 'a' - 'z' are ok + return true; + } + if (c <= 0x5A) { + if (c >= 0x41) { // 'A' - 'Z' ok too + return true; + } + // As are 0-9, '.' and '-' + if ((c >= 0x30 && c <= 0x39) || (c == '.') || (c == '-')) { + return true; + } + // And finally, colon, in non-ns-aware mode + if (c == ':' && !ncname) { // ':' == 0x3A + return true; + } + } else if (c == 0x5F) { // '_' is ok too + return true; + } + } + + // Others are checked block-by-block: + if (c <= 0x2FEF) { + if (c < 0x300) { + if (c < 0x00C0) { // 8-bit ctrl chars + return false; + } + // most of the rest are fine... + return (c != 0xD7 && c != 0xF7); + } + if (c >= 0x2C00) { + // 0x2C00 - 0x2FEF are ok + return true; + } + if (c < 0x370 || c > 0x218F) { + // 0x300 - 0x36F, 0x2190 - 0x2BFF invalid + return false; + } + if (c < 0x2000) { + // 0x370 - 0x37D, 0x37F - 0x1FFF are ok + return (c != 0x37E); + } + if (c >= 0x2070) { + // 0x2070 - 0x218F are ok; 0x218F+ was covered above + return true; + } + // And finally, 0x200C - 0x200D + return (c == 0x200C || c == 0x200D); + } + + // 0x3000 and above: + if (c >= 0x3001) { + /* Hmmh, let's allow high surrogates here, without checking + * that they are properly followed... crude basic support, + * I know, but allows valid combinations, just doesn't catch + * invalid ones + */ + if (c <= 0xDBFF) { // 0x3001 - 0xD7FF (chars), + // 0xD800 - 0xDBFF (high surrogate) are ok (unlike DC00-DFFF) + return true; + } + if (c >= 0xF900 && c <= 0xFFFD) { + /* Check above removes low surrogate (since one can not + * START an identifier), and byte-order markers.. + */ + return (c <= 0xFDCF || c >= 0xFDF0); + } + } + + return false; + } + + public static boolean is11NameChar(int c, boolean ncname) { + if (c <= 0x7A) { // 'z' or earlier + if (c >= 0x61) { // 'a' - 'z' are ok + return true; + } + if (c <= 0x5A) { + if (c >= 0x41) { // 'A' - 'Z' ok too + return true; + } + // As are 0-9, '.' and '-' + if ((c >= 0x30 && c <= 0x39) || (c == '.') || (c == '-')) { + return true; + } + // And finally, colon, in non-ns-aware mode + if (c == ':' && !ncname) { // ':' == 0x3A + return true; + } + } else if (c == 0x5F) { // '_' is ok too + return true; + } + } + + // Others are checked block-by-block: + if (c <= 0x2FEF) { + if (c < 0x2000) { // only 8-bit ctrl chars and 0x37E to filter out + return (c >= 0x00C0 && c != 0x37E) || (c == 0xB7); + } + if (c >= 0x2C00) { + // 0x100 - 0x1FFF, 0x2C00 - 0x2FEF are ok + return true; + } + if (c < 0x200C || c > 0x218F) { + // 0x2000 - 0x200B, 0x2190 - 0x2BFF invalid + return false; + } + if (c >= 0x2070) { + // 0x2070 - 0x218F are ok + return true; + } + // And finally, 0x200C - 0x200D, 0x203F - 0x2040 are ok + return (c == 0x200C || c == 0x200D || c == 0x203F || c == 0x2040); + } + + // 0x3000 and above: + if (c >= 0x3001) { + /* Hmmh, let's allow surrogate heres, without checking that + * they have proper ordering. For non-first name chars, both are + * ok, for valid names. Crude basic support, + * I know, but allows valid combinations, just doesn't catch + * invalid ones + */ + if (c <= 0xDFFF) { // 0x3001 - 0xD7FF (chars), + // 0xD800 - 0xDFFF (high, low surrogate) are ok: + return true; + } + if (c >= 0xF900 && c <= 0xFFFD) { + /* Check above removes other invalid chars (below valid + * range), and byte-order markers (0xFFFE, 0xFFFF). + */ + return (c <= 0xFDCF || c >= 0xFDF0); + } + } + + return false; + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/PointerPart.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/PointerPart.java new file mode 100644 index 000000000000..e42fcca9a983 --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/PointerPart.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +/** + * Interface to be implemented by pointer parts (XPointer schemes). + */ +interface PointerPart { + + /** + * Returns a String serialisation of this xpointer PointerPart. + * + * @return a String containing the serialisation of this xpointer PointerPart + */ + String toString(); +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/StreamReaderDelegate.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/StreamReaderDelegate.java new file mode 100644 index 000000000000..68839920dea7 --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/StreamReaderDelegate.java @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import java.util.regex.Pattern; + +/** + * A {@code XMLStreamReader} delegating to the {@link #getDelegate()} method. + *

+ * This class is similar to {@link javax.xml.stream.util.StreamReaderDelegate} though + * it differs in using a method to delegate to instead of a field. This is needed as + * the {@link XIncludeStreamReader} needs to pick up the reader to delegate to from + * a stack. + * + * @see XIncludeStreamReader#getDelegate() + */ +abstract class StreamReaderDelegate implements XMLStreamReader { + + protected abstract XMLStreamReader getDelegate(); + + @Override + public Object getProperty(String name) throws IllegalArgumentException { + return getDelegate().getProperty(name); + } + + @Override + public int next() throws XMLStreamException { + return getDelegate().next(); + } + + @Override + public void require(int type, String namespaceURI, String localName) throws XMLStreamException { + getDelegate().require(type, namespaceURI, localName); + } + + @Override + public String getElementText() throws XMLStreamException { + return getDelegate().getElementText(); + } + + private static final String[] TYPES = new String[] { + "", + "START_ELEMENT", + "END_ELEMENT", + "PROCESSING_INSTRUCTION", + "CHARACTERS", + "COMMENT", + "SPACE", + "START_DOCUMENT", + "END_DOCUMENT", + "ENTITY_REFERENCE", + "ATTRIBUTE", + "DTD", + "CDATA", + "NAMESPACE", + "NOTATION_DECLARATION", + "ENTITY_DECLARATION" + }; + + private static final Pattern WHITESPACE_REGEX = Pattern.compile("[ \r\t\n]+"); + + public boolean isWhitespace() throws XMLStreamException { + if (getEventType() == CHARACTERS || getEventType() == CDATA) { + return WHITESPACE_REGEX.matcher(getText()).matches(); + } else if (getEventType() == SPACE) { + return true; + } else { + throw new XMLStreamException("no content available to check for whitespaces"); + } + } + + @Override + public int nextTag() throws XMLStreamException { + int eventType = next(); + while (eventType == CHARACTERS && isWhitespace() // skip whitespace + || eventType == COMMENT) { // skip comments + eventType = next(); + } + if (eventType != START_ELEMENT && eventType != END_ELEMENT) { + throw new XMLStreamException( + "expected START_TAG or END_TAG not " + TYPES[getEventType()], getLocation(), null); + } + return eventType; + } + + @Override + public boolean hasNext() throws XMLStreamException { + return getDelegate().hasNext(); + } + + @Override + public void close() throws XMLStreamException { + getDelegate().close(); + } + + @Override + public String getNamespaceURI(String prefix) { + return getDelegate().getNamespaceURI(prefix); + } + + @Override + public boolean isStartElement() { + return getDelegate().isStartElement(); + } + + @Override + public boolean isEndElement() { + return getDelegate().isEndElement(); + } + + @Override + public boolean isCharacters() { + return getDelegate().isCharacters(); + } + + @Override + public boolean isWhiteSpace() { + return getDelegate().isWhiteSpace(); + } + + @Override + public String getAttributeValue(String namespaceURI, String localName) { + return getDelegate().getAttributeValue(namespaceURI, localName); + } + + @Override + public int getAttributeCount() { + return getDelegate().getAttributeCount(); + } + + @Override + public QName getAttributeName(int index) { + return getDelegate().getAttributeName(index); + } + + @Override + public String getAttributeNamespace(int index) { + return getDelegate().getAttributeNamespace(index); + } + + @Override + public String getAttributeLocalName(int index) { + return getDelegate().getAttributeLocalName(index); + } + + @Override + public String getAttributePrefix(int index) { + return getDelegate().getAttributePrefix(index); + } + + @Override + public String getAttributeType(int index) { + return getDelegate().getAttributeType(index); + } + + @Override + public String getAttributeValue(int index) { + return getDelegate().getAttributeValue(index); + } + + @Override + public boolean isAttributeSpecified(int index) { + return getDelegate().isAttributeSpecified(index); + } + + @Override + public int getNamespaceCount() { + return getDelegate().getNamespaceCount(); + } + + @Override + public String getNamespacePrefix(int index) { + return getDelegate().getNamespacePrefix(index); + } + + @Override + public String getNamespaceURI(int index) { + return getDelegate().getNamespaceURI(index); + } + + @Override + public NamespaceContext getNamespaceContext() { + return getDelegate().getNamespaceContext(); + } + + @Override + public int getEventType() { + return getDelegate().getEventType(); + } + + @Override + public String getText() { + return getDelegate().getText(); + } + + @Override + public char[] getTextCharacters() { + return getDelegate().getTextCharacters(); + } + + @Override + public int getTextCharacters(int sourceStart, char[] target, int targetStart, int length) + throws XMLStreamException { + return getDelegate().getTextCharacters(sourceStart, target, targetStart, length); + } + + @Override + public int getTextStart() { + return getDelegate().getTextStart(); + } + + @Override + public int getTextLength() { + return getDelegate().getTextLength(); + } + + @Override + public String getEncoding() { + return getDelegate().getEncoding(); + } + + @Override + public boolean hasText() { + return getDelegate().hasText(); + } + + @Override + public Location getLocation() { + return getDelegate().getLocation(); + } + + @Override + public QName getName() { + return getDelegate().getName(); + } + + @Override + public String getLocalName() { + return getDelegate().getLocalName(); + } + + @Override + public boolean hasName() { + return getDelegate().hasName(); + } + + @Override + public String getNamespaceURI() { + return getDelegate().getNamespaceURI(); + } + + @Override + public String getPrefix() { + return getDelegate().getPrefix(); + } + + @Override + public String getVersion() { + return getDelegate().getVersion(); + } + + @Override + public boolean isStandalone() { + return getDelegate().isStandalone(); + } + + @Override + public boolean standaloneSet() { + return getDelegate().standaloneSet(); + } + + @Override + public String getCharacterEncodingScheme() { + return getDelegate().getCharacterEncodingScheme(); + } + + @Override + public String getPITarget() { + return getDelegate().getPITarget(); + } + + @Override + public String getPIData() { + return getDelegate().getPIData(); + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XInclude.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XInclude.java new file mode 100644 index 000000000000..b30d2d55543c --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XInclude.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLResolver; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.Source; + +import java.util.Objects; + +import com.ctc.wstx.api.WstxInputProperties; +import com.ctc.wstx.stax.WstxInputFactory; +import com.ctc.wstx.stax.WstxOutputFactory; +import org.codehaus.stax2.XMLInputFactory2; + +/** + * XInclude support + */ +public class XInclude { + + /** + * Creates a XML stream reader that supports XInclude. + *

+ * External files will be loaded using the given {@code XMLResolver} + * and called with {@code systemId} set to the URL to load and + * {@code baseURI} set to the current document location (the initial + * one will be the {@code systemId} of the input {@code Source}. + * + * @param source the XML source to parse + * @param resolver the XML resolver to use when resolving external files + * @return a XML stream reader that supports xinclude + * @throws XMLStreamException if the stream reader cannot be created + */ + public static XMLStreamReader xinclude(Source source, XMLResolver resolver) throws XMLStreamException { + XMLInputFactory2 factory = new WstxInputFactory(); + factory.configureForRoundTripping(); + factory.setProperty(WstxInputProperties.P_TREAT_CHAR_REFS_AS_ENTS, false); + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true); + factory.setProperty(WstxInputProperties.P_ENTITY_RESOLVER, Objects.requireNonNull(resolver)); + + XMLStreamReader reader = factory.createXMLStreamReader(source); + return xinclude(factory, new WstxOutputFactory(), source.getSystemId(), reader); + } + + public static XMLStreamReader xinclude( + XMLInputFactory factory, XMLOutputFactory outputFactory, String location, XMLStreamReader reader) { + return new XIncludeStreamReader(factory, outputFactory, location, reader); + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XIncludeStreamReader.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XIncludeStreamReader.java new file mode 100644 index 000000000000..b057a93b1550 --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XIncludeStreamReader.java @@ -0,0 +1,510 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.Location; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLResolver; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.events.DTD; +import javax.xml.stream.events.XMLEvent; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamSource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Objects; +import java.util.stream.Collectors; + +import com.ctc.wstx.dtd.DTDSubset; +import org.codehaus.stax2.XMLInputFactory2; +import org.codehaus.stax2.XMLStreamLocation2; +import org.codehaus.stax2.io.Stax2Source; +import org.codehaus.stax2.io.Stax2URLSource; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import static javax.xml.XMLConstants.XML_NS_URI; + +/** + * Main implementation class for XInclude support. + */ +@SuppressWarnings("checkstyle:MissingSwitchDefault") +class XIncludeStreamReader extends StreamReaderDelegate { + + private static final String XINCLUDE_NAMESPACE = "http://www.w3.org/2001/XInclude"; + private static final String XINCLUDE_INCLUDE = "include"; + private static final String XINCLUDE_FALLBACK = "fallback"; + + private final XMLInputFactory factory; + private final XMLOutputFactory outputFactory; + private final Deque contextStack = new ArrayDeque<>(); + private final Deque xmlLangs = new ArrayDeque<>(); + private final Deque xmlBases = new ArrayDeque<>(); + private boolean firstElementInContext; + + XIncludeStreamReader( + XMLInputFactory factory, XMLOutputFactory outputFactory, String location, XMLStreamReader reader) { + this.factory = factory; + this.outputFactory = outputFactory; + if (!(Boolean) reader.getProperty(XMLInputFactory.IS_NAMESPACE_AWARE)) { + throw new IllegalArgumentException("Namespace support should be enabled"); + } + pushContext(location, reader); + } + + @Override + public int next() throws XMLStreamException { + int event = super.next(); + if (event == START_ELEMENT) { + contextStack.peek().depth++; + String xmlLang = this.xmlLangs.peek(); + String xmlBase = firstElementInContext ? contextStack.peek().location : this.xmlBases.peek(); + firstElementInContext = false; + for (int i = 0; i < getAttributeCount(); i++) { + if ("xml".equals(getAttributePrefix(i))) { + switch (getAttributeLocalName(i)) { + case "lang": + xmlLang = getAttributeValue(i); + break; + case "base": + xmlBase = getAttributeValue(i); + break; + } + } + } + this.xmlLangs.push(xmlLang != null ? xmlLang : ""); + this.xmlBases.push(xmlBase != null ? xmlBase : ""); + String namespace = getNamespaceURI(); + String localName = getLocalName(); + if (XINCLUDE_NAMESPACE.equals(namespace) && XINCLUDE_INCLUDE.equals(localName)) { + processInclude(); + return next(); + } + } else if (event == END_ELEMENT) { + contextStack.peek().depth--; + this.xmlBases.pop(); + this.xmlLangs.pop(); + } else if (event == END_DOCUMENT) { + while (event == END_DOCUMENT) { + if (contextStack.size() > 1) { + contextStack.pop(); + event = next(); + } else { + break; + } + } + firstElementInContext = false; + } + return event; + } + + private void processInclude() throws XMLStreamException { + + Location startLocation = this.getLocation(); + + Element node; + try { + Document doc = + DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + DOMResult res = new DOMResult(doc); + XMLEventWriter xew = outputFactory.createXMLEventWriter(res); + XMLEventReader xer = factory.createXMLEventReader(new FragmentReader(getDelegate())); + xew.add(xer); + node = doc.getDocumentElement(); + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + } + + String href = getAttribute(node, "href"); + String parse = getAttribute(node, "parse"); + String xpointer = getAttribute(node, "xpointer"); + String fragid = getAttribute(node, "fragid"); + String setXmlId = getAttribute(node, "set-xml-id"); + String encoding = getAttribute(node, "encoding"); + + IOException resourceError = null; + + if (href == null) { + if (xpointer == null && fragid == null) { + throw new XMLStreamException("xpointer or fragid must be used as href is null"); + } + href = ""; + } else if (href.contains("#")) { + throw new XMLStreamException("fragment identifiers must not be used in href: " + href); + } + URI hrefUri; + try { + hrefUri = new URI(href); + } catch (URISyntaxException e) { + throw new XMLStreamException("invalid syntax for href: " + href, e); + } + + Source input; + String currentLocation = xmlBases.peek(); + XMLResolver r = factory.getXMLResolver(); + Object o = r != null ? r.resolveEntity(null, href, currentLocation, null) : null; + if (o instanceof URI) { + try { + o = new Stax2URLSource(((URI) o).toURL()); + } catch (MalformedURLException e) { + throw new XMLStreamException(e); + } + } + if (o != null && !(o instanceof Source)) { + throw new XMLStreamException( + "Unsupported input of class " + o.getClass().getName()); + } + if (o == null) { + resourceError = new IOException("Unable to load resource: " + href); + } + input = (Source) o; + + boolean isXml = false; + boolean isText = false; + if (resourceError == null) { + if (parse == null || "xml".equals(parse) || "application/xml".equals(parse) || parse.endsWith("+xml")) { + isXml = true; + } else if ("text".equals(parse) || parse.startsWith("text/")) { + isText = true; + if (xpointer != null) { + throw new XMLStreamException("xpointer cannot be used with text parsing"); + } + } else { + resourceError = new IOException("Unsupported media type: " + parse); + } + } + + boolean fallback = true; + if (resourceError == null && isXml) { + boolean reportPrologWs = (boolean) factory.getProperty(XMLInputFactory2.P_REPORT_PROLOG_WHITESPACE); + if (reportPrologWs) { + factory.setProperty(XMLInputFactory2.P_REPORT_PROLOG_WHITESPACE, false); + } + XMLStreamReader reader = factory.createXMLStreamReader(input); + String pointer = xpointer != null ? xpointer : fragid; + try { + Document doc = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .newDocument(); + DOMResult res = new DOMResult(doc); + XMLEventWriter xew = outputFactory.createXMLEventWriter(res); + XMLEventReader xer = factory.createXMLEventReader(reader); + DTD dtd = null; + while (xer.hasNext()) { + XMLEvent event = xer.nextEvent(); + if (event instanceof DTD) { + dtd = (DTD) event; + } else { + xew.add(event); + } + } + + Element resNode; + if (pointer != null) { + DOMXMLElementEvaluator evaluator = new DOMXMLElementEvaluator( + new XPointer(pointer), + doc.getDocumentElement(), + dtd != null ? (DTDSubset) dtd.getProcessedDTD() : null); + resNode = evaluator.evaluateElement(); + } else { + resNode = doc.getDocumentElement(); + } + + if (resNode != null) { + // xml:lang fix + String curLang = this.xmlLangs.peek(); + String impLang = ""; + + Element p = resNode; + while (p != null) { + Attr attr = p.getAttributeNodeNS(XML_NS_URI, "lang"); + if (attr != null) { + impLang = attr.getValue(); + break; + } + Node np = p.getParentNode(); + p = np instanceof Element ? (Element) np : null; + } + if (!Objects.equals(curLang, impLang)) { + resNode.setAttributeNS(XML_NS_URI, "xml:lang", impLang); + } + resNode.setAttributeNS(XML_NS_URI, "xml:base", input.getSystemId()); + + NamedNodeMap attrs = node.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Attr att = (Attr) attrs.item(i); + String ns = att.getNamespaceURI(); + if (ns != null && !XML_NS_URI.equals(ns)) { + if ("http://www.w3.org/2001/XInclude/local-attributes".equals(ns)) { + resNode.setAttribute(att.getLocalName(), att.getValue()); + } else { + resNode.setAttributeNS(ns, att.getPrefix() + ":" + att.getLocalName(), att.getValue()); + } + } + } + if (setXmlId != null) { + resNode.setAttributeNS(XML_NS_URI, "xml:id", setXmlId); + } + + XMLStreamReader sr = factory.createXMLStreamReader(new DOMSource(resNode)); + pushContext(input.getSystemId(), sr); + fallback = false; + } + } catch (InvalidXPointerException | ParserConfigurationException e) { + throw new XMLStreamException(e); + } + } else if (resourceError == null && isText) { + try { + StringWriter sw = new StringWriter(); + Reader reader; + if (input instanceof StreamSource) { + StreamSource ss = (StreamSource) input; + reader = ss.getReader(); + if (reader == null) { + InputStream is = ss.getInputStream(); + reader = new InputStreamReader(is, encoding != null ? encoding : "UTF-8"); + } + } else if (input instanceof Stax2Source) { + Stax2Source ss = (Stax2Source) input; + reader = ss.constructReader(); + if (reader == null) { + InputStream is = ss.constructInputStream(); + reader = new InputStreamReader(is, encoding != null ? encoding : "UTF-8"); + } + } else { + throw new XMLStreamException( + "Unsupported source of class " + input.getClass().getName()); + } + transferTo(reader, sw); + String include; + if (fragid != null) { + String scheme; + String integrity; + int scIdx = fragid.indexOf(';'); + if (scIdx > 0) { + scheme = fragid.substring(0, scIdx); + integrity = fragid.substring(scIdx + 1); + } else { + scheme = fragid; + integrity = ""; + } + if (scheme.startsWith("char=")) { + String str = scheme.substring("char=".length()); + int idx = str.indexOf(','); + int min, max; + if (idx >= 0) { + min = idx == 0 ? 0 : Integer.parseInt(str.substring(0, idx)); + max = idx == str.length() - 1 ? str.length() - 1 : Integer.parseInt(str.substring(idx + 1)); + } else { + min = Integer.parseInt(str); + max = min; + } + include = sw.toString().substring(min, max); + } else if (scheme.startsWith("line=")) { + String str = scheme.substring("line=".length()); + int idx = str.indexOf(','); + int min, max; + if (idx >= 0) { + min = idx == 0 ? 0 : Integer.parseInt(str.substring(0, idx)); + max = idx == str.length() - 1 ? str.length() - 1 : Integer.parseInt(str.substring(idx + 1)); + } else { + min = Integer.parseInt(str); + max = min; + } + BufferedReader br = new BufferedReader(new StringReader(sw.toString())); + include = br.lines().skip(min).limit(max - min).collect(Collectors.joining("\n", "", "\n")); + } else { + throw new XMLStreamException("Unsupported text scheme in fragid: " + fragid); + } + if (!integrity.isEmpty()) { + String charset = null; + int idx = integrity.indexOf(','); + if (idx >= 0) { + charset = integrity.substring(idx + 1); + integrity = integrity.substring(0, idx); + if (integrity.startsWith("length=")) { + // TODO: implement + } else if (integrity.startsWith("md5=")) { + // TODO: implement + } else { + throw new XMLStreamException("Unsupported text integrity in fragid: " + fragid); + } + } + } + } else { + include = sw.toString(); + } + + StreamReaderDelegate sr = new StreamReaderDelegate() { + int state = 0; + + @Override + protected XMLStreamReader getDelegate() { + return null; + } + + @Override + public int next() throws XMLStreamException { + state++; + return getEventType(); + } + + @Override + public int getEventType() { + switch (state) { + case 0: + return START_ELEMENT; + case 1: + return CHARACTERS; + } + return END_DOCUMENT; + } + + @Override + public String getText() { + return include; + } + + @Override + public Location getLocation() { + return XMLStreamLocation2.NOT_AVAILABLE; + } + }; + pushContext(input.getSystemId(), sr); + fallback = false; + } catch (IOException e) { + resourceError = e; + } + } + + // now skip fallback elements + boolean hasFallback = false; + for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { + if (!(child instanceof Element)) { + continue; + } + if (XINCLUDE_NAMESPACE.equals(child.getNamespaceURI())) { + if (XINCLUDE_FALLBACK.equals(child.getLocalName())) { + if (hasFallback) { + throw new XMLStreamException("One one xi:fallback element can be present", startLocation); + } + hasFallback = true; + if (fallback) { + XMLStreamReader sr = factory.createXMLStreamReader(new DOMSource(child)); + sr.nextTag(); // the fallback + sr.next(); // next + sr = new FragmentReader(sr); + pushContext(currentLocation, sr); + break; + } + } else { + throw new XMLStreamException( + "Element " + child.getLocalName() + " cannot be present inside the include element", + startLocation); + } + } + } + } + + private String getAttribute(Element node, String attr) { + Attr a = node.getAttributeNode(attr); + return a != null ? a.getValue() : null; + } + + private void pushContext(String location, XMLStreamReader reader) { + contextStack.push(new EventContext(location, reader)); + firstElementInContext = true; + } + + protected XMLStreamReader getDelegate() { + return contextStack.peek().getReader(); + } + + private static final int TRANSFER_BUFFER_SIZE = 8192; + + private static long transferTo(Reader in, Writer out) throws IOException { + Objects.requireNonNull(out, "out"); + long transferred = 0; + char[] buffer = new char[TRANSFER_BUFFER_SIZE]; + int read; + while ((read = in.read(buffer, 0, buffer.length)) >= 0) { + out.write(buffer, 0, read); + if (transferred < Long.MAX_VALUE) { + try { + transferred = Math.addExact(transferred, read); + } catch (ArithmeticException ignore) { + transferred = Long.MAX_VALUE; + } + } + } + return transferred; + } + + static class EventContext { + + final String location; + final XMLStreamReader reader; + final String input; + int depth; + int startDepth; + + EventContext(String location, XMLStreamReader reader) { + this.location = Objects.requireNonNull(location); + this.reader = Objects.requireNonNull(reader); + this.input = null; + } + + EventContext(String location, String input) { + this.location = Objects.requireNonNull(location); + this.input = Objects.requireNonNull(input); + this.reader = null; + } + + public String getLocation() { + return location; + } + + public XMLStreamReader getReader() { + return reader; + } + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XMLElement.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XMLElement.java new file mode 100644 index 000000000000..e871f7b47edd --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XMLElement.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import java.util.List; + +interface XMLElement { + T getSource(); + + String getLocalName(); + + List> getChildElements(); + + XMLElement getNextSiblingElement(); +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XMLElementEvaluator.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XMLElementEvaluator.java new file mode 100644 index 000000000000..a8e38acbacd1 --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XMLElementEvaluator.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * This class evaluates an XPointer on a XMLElement, using the XPointer model. + * It currently supports shorthand pointer and element() scheme based pointer part. + *

+ * This class is based upon a class of the same name in Apache Woden. + * + * @param the wrapped type + */ +abstract class XMLElementEvaluator { + private final XPointer xpointer; + private final XMLElement root; + + /** + * Constructs a new XMLElement abstract class for a XPointer and XMLElement. + * + * @param xpointer an XPointer which to evaluate. + * @param root an XMLElement which to evaluate the XPointer against. + */ + XMLElementEvaluator(XPointer xpointer, XMLElement root) { + this.xpointer = xpointer; + this.root = root; + } + + /** + * Evaluates the XPointer on the root XMLElement and returns the resulting XMLElement or null. + * + * @return an XMLElement from the resultant evaluation of the root XMLElement or null if evaluation fails. + */ + public XMLElement evaluate() { + if (xpointer.hasPointerParts()) { // Scheme based pointer. + // Take each pointer part at a time and evaluate it against the root element. The first result found will be + // returned. + XMLElement result = null; + for (PointerPart pointerPart : xpointer.getPointerParts()) { + // TODO Add extra pointer parts here once we support them. + if (pointerPart instanceof ElementPointerPart) { + result = evaluateElementPointerPart((ElementPointerPart) pointerPart); + } + if (result != null) { + return result; + } + } + } else if (xpointer.hasShorthandPointer()) { // Shorthand pointer + // Iterator for XMLElement from root in document order. See http://www.w3.org/TR/xpath#dt-document-order + return evaluateShorthandPointer(xpointer.getShorthandPointer()); + } + return null; + } + + /** + * Evaluates an element() XPointer scheme based pointer part to the specification at + * http://www.w3.org/TR/xptr-element/ + * + * @param elementPointerPart an ElementPointerPart to evaluate. + * @return an XMLElement pointed to by this Element pointer part, or null if none exists. + */ + private XMLElement evaluateElementPointerPart(ElementPointerPart elementPointerPart) { + if (elementPointerPart.hasChildSequence() && elementPointerPart.hasNCName()) { // Both NCName and childSequence. + // Find NCName. + XMLElement element = evaluateShorthandPointer(elementPointerPart.getNCName()); + if (element == null) { + return null; + } + // Walk through children. + return evaluateChildSequence(element, elementPointerPart.getChildSequence()); + } else if (elementPointerPart.hasNCName()) { // Only NCName + return evaluateShorthandPointer(elementPointerPart.getNCName()); + } else { // Only a childSequence + // XML must only have 1 root element so we can't evaluate it if its > 1 + List childSequence = elementPointerPart.getChildSequence(); + if (childSequence.get(0) > 1) { + return null; + } + return evaluateChildSequence(root, childSequence.subList(1, childSequence.size())); + } + } + + /** + * Evaluates an shorthand pointer in an XPointer based on the specification at + * http://www.w3.org/TR/xptr-framework/#shorthand + * + * @param shorthand an NCName to evaluate. + * @return an XMLElement pointed to by this shorthand name, or null if none exists. + */ + private XMLElement evaluateShorthandPointer(String shorthand) { + // Iterator for XMLElement from root in document order. See http://www.w3.org/TR/xpath#dt-document-order + for (Iterator> it = new DocumentOrderIterator<>(root); it.hasNext(); ) { + XMLElement element = it.next(); + if (testElementShorthand(element, shorthand)) { + return element; + } + } + return null; + } + + /** + * Evaluates a child sequence array of Integers to an XMLElement following XML Document Order. + * This is a helper method used by other evaluation methods in this class. + * + * @param element an XMLElement to start from. + * @param childSequence an Integer[] to evaluate from the start XMLElement. + * @return an XMLElement pointed to by this childSequence, or null if none exists. + */ + private XMLElement evaluateChildSequence(XMLElement element, List childSequence) { + for (Integer integer : childSequence) { + // does the iTh child exist? + List> children = element.getChildElements(); + children = filterNoneElementNodes(children); + if (integer > children.size()) { // childSequence int out of bounds of child array so does not exist. + return null; + } else { + element = element.getChildElements().get(integer - 1); + } + } + return element; + } + + // Utility classes + + /** + * Filters an XMLElement[] for nodes which are not xml tag elements. + * + * @param nodes an XMLElement[] of the nodes to filter. + * @return an XMLElement[] of the remaining nodes. + */ + private static List> filterNoneElementNodes(List> nodes) { + return nodes.stream().filter(n -> !n.getLocalName().contains("#")).collect(Collectors.toList()); + } + + // Abstract Methods + + /** + * Tests the element for an id according to the specification at + * http://www.w3.org/TR/xptr-framework/#term-sdi and returns a boolean answer. + * + * @param element An XMLElement to test for an id. + * @param id A String of the id to test for. + * @return boolean value of whether the id matches or not. + */ + public abstract boolean testElementShorthand(XMLElement element, String id); + + // Internal classes + + /** + * DocumentOrderIterator is a implementation of Iterator which iterates in Document Order from a root XMLElement object. + */ + private static class DocumentOrderIterator implements Iterator> { + private final Deque> stack; + + DocumentOrderIterator(XMLElement root) { + stack = new ArrayDeque<>(); + stack.add(root); + } + + public boolean hasNext() { + return !stack.isEmpty(); + } + + public XMLElement next() { + // Get next element. + XMLElement element = stack.pop(); + // Add children to top of stack in reverse order. + List> children = new ArrayList<>(element.getChildElements()); + Collections.reverse(children); + stack.addAll(children); + return element; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XPointer.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XPointer.java new file mode 100644 index 000000000000..185f14ae1d4b --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XPointer.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * XPointer is a class which represents an XPointer defined in the XPointer Framework. + * This is specified at http://www.w3.org/TR/xptr-framework/ + *

+ * This class is based upon a class of the same name in Apache Woden. + */ +class XPointer { + private static final String NS_URI_XML = "http://www.w3.org/XML/1998/namespace"; + private static final String NS_URI_XMLNS = "http://www.w3.org/2000/xmlns/"; + private static final String NS_PREFIX_XMLNS = "xmlns"; + + private final Map prefixBindingContex; + private final Map namespaceBindingContex; + private String shorthandPointer; + private final List pointerParts; + + /** + * Constructs a new XPointer. + */ + XPointer() { + pointerParts = new ArrayList<>(); + shorthandPointer = ""; + + // Setup prefix/namespace binding context. + prefixBindingContex = new HashMap<>(); + namespaceBindingContex = new HashMap<>(); + addPrefixNamespaceBinding("xml", NS_URI_XML); + } + + /** + * Constructs a new XPointer from the serialised string. + * + * @param xpointerString a String form of the XPointer to deserialise + */ + XPointer(String xpointerString) throws InvalidXPointerException { + this(); // Construct a new XPointer. + if (xpointerString == null || xpointerString.isEmpty()) { + throw new InvalidXPointerException("The XPointer string is either null or empty", ""); + } + XPointerParser.parseXPointer( + xpointerString, this); // Parse the string and add the Pointers to the new XPointer. + } + + /** + * Appends a pointer part to the end of this XPointer. + * + * @param pointerPart the Pointer Part to append + * @throws UnsupportedOperationException if a Shorthand Pointer is already set + */ + public void addPointerPart(PointerPart pointerPart) { + if (!shorthandPointer.isEmpty()) { + throw new UnsupportedOperationException("A Shortname Pointer already exists for this XPointer."); + } else { + pointerParts.add(pointerPart); + } + } + + /** + * Returns the pointer parts in this XPointer. + * + * @return a PointerPart[] of type Object[] containing the pointer parts in this XPointer + * @throws IllegalStateException if this XPointer has a shorthand pointer + */ + public List getPointerParts() { + if (hasPointerParts()) { + return Collections.unmodifiableList(pointerParts); + } else { + throw new IllegalStateException("This XPointer has a shorthand pointer."); + } + } + + /** + * Sets the Shorthand Pointer of this XPointer to the NCName given as an argument. + * + * @param shorthandPointer an NCName of the Shorthand Pointer to set + * @throws UnsupportedOperationException is a PointerPart Pointer is already set + */ + public void setShorthandPointer(String shorthandPointer) { + if (hasPointerParts()) { + throw new UnsupportedOperationException("A PointerPart Pointer already exists for this XPointer"); + } + if (shorthandPointer == null) { + throw new NullPointerException("The shorthandPointer argument is null"); + } + + this.shorthandPointer = shorthandPointer; + } + + /** + * Returns the shorthandPointer in this XPointer. + * + * @return an NCName containing the shorthand pointer for this XPointer + * @throws IllegalStateException if this XPointer has a shorthand pointer + */ + public String getShorthandPointer() { + if (hasShorthandPointer()) { + return shorthandPointer; + } else { + throw new IllegalStateException("This XPointer has scheme based pointers."); + } + } + + /** + * Adds a Prefix/Namespace binding to this XPointers contex. + * + * @param prefix a NCName of the prefix too bind to the namespace + * @param namespace a String of the namespace to bind to the prefix + * @throws NullPointerException if the prefix or namespace arguments are null + * @throws IllegalArgumentException if the prefix or namespace are invalid as specified at http://www.w3.org/TR/xptr-framework/#nsContext + */ + public void addPrefixNamespaceBinding(String prefix, String namespace) { + if (prefix == null) { + throw new NullPointerException("The prefix argument provided has a null pointer."); + } else if (namespace == null) { + throw new NullPointerException("The namespace argument provided has a null pointer."); + } else if (prefix.equals(NS_PREFIX_XMLNS)) { + throw new IllegalArgumentException("The xmlns prefix must not be bound to any namespace."); + } else if (namespace.equals(NS_URI_XMLNS)) { + throw new IllegalArgumentException("The " + NS_URI_XMLNS + " namespace must not be bound to any prefix."); + } else { + // It's a valid binding so add it to the binding contex. + prefixBindingContex.put(prefix, namespace); + namespaceBindingContex.put(namespace, prefix); + } + } + + /** + * Gets the Namespace the Prefix is bound to if the binding exists, + * otherwise it will return null. + * + * @param prefix a NCName of the prefix bound to the namespace + * @return a String of the namespace bound to this prefix or null if none exists + */ + public String getPrefixBinding(String prefix) { + return prefixBindingContex.get(prefix); + } + + /** + * Gets Prefix the Namespace is bound to if the binding exists, + * otherwise it will return null. + * + * @param namespace a String of the prefix bound to the prefix + * @return a NCName of the prefix bound to this namespace or null if none exists + */ + public String getNamespaceBinding(String namespace) { + return namespaceBindingContex.get(namespace); + } + + /** + * Checks whether a prefix is bound or not. + * + * @param prefix a NCName of the prefix to check + * @return a boolean value that is true if the binding exists, or false otherwise + */ + public boolean hasPrefixBinding(String prefix) { + return prefixBindingContex.containsKey(prefix); + } + + /** + * Checks whether a namespace is bound or not. + * + * @param namespace a String of the namespace to check + * @return a boolean value that is true if the binding exists, or false otherwise + */ + public boolean hasNamespaceBinding(String namespace) { + return namespaceBindingContex.containsKey(namespace); + } + + /** + * Tests whether this XPointer has a shorthand pointer or not. + * + * @return a boolean which is true if this XPointer contains a shorthand pointer, false otherwise + */ + public boolean hasShorthandPointer() { + return !shorthandPointer.isEmpty(); + } + + /** + * Tests whether this XPointer has scheme based pointers or not. + * + * @return a boolean which is true if this XPointer contains scheme based pointers, false otherwise + */ + public boolean hasPointerParts() { + return !pointerParts.isEmpty(); + } + + /** + * Returns a String serialisation of this XPointer. + * + * @return a String containing the serialisation of this XPointer + */ + public String toString() { + if (shorthandPointer.isEmpty()) { + return pointerParts.stream().map(PointerPart::toString).collect(Collectors.joining()); + } else { + return shorthandPointer; + } + } +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XPointerParser.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XPointerParser.java new file mode 100644 index 000000000000..1b94e5f6eac7 --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XPointerParser.java @@ -0,0 +1,621 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import java.util.Hashtable; +import java.util.Map; +import java.util.Objects; + +/** + * This class parses a String to the XPointer Framework specification for shorthand and scheme based pointers. + * For scheme based pointers each know pointer part + *

+ * See the XPointer Framework Recommendation for + * more information on the XPointer Framework, ShortHand and Scheme based Pointers. + *

+ * This class is based upon a class of the same name in Apache Woden. + */ +@SuppressWarnings({"checkstyle:MissingSwitchDefault", "checkstyle:AvoidNestedBlocks"}) +final class XPointerParser { + + private static final String ELEMENT_SCHEME_NAME = "element"; // Supported schemes + + /** + * Parses a String XPointer and stores the results into the given XPointer object. + * + * @throws InvalidXPointerException if the XPointer being parsed contains invalid syntax + */ + public static void parseXPointer(String xpointerString, XPointer xpointer) throws InvalidXPointerException { + + final Tokens tokens = new Tokens(); // tokens + + // scan the XPointer expression + int length = xpointerString.length(); + boolean success = Scanner.scanExpr(tokens, xpointerString, 0, length); + + if (!success) { + throw new InvalidXPointerException("Invalid XPointer expression", xpointerString); + } + + while (tokens.hasMore()) { + int token = tokens.nextToken(); + + switch (token) { + case Tokens.XPTRTOKEN_SHORTHAND: { + + // The shorthand name + token = tokens.nextToken(); + String shortHandPointerName = tokens.getTokenString(token); + + if (shortHandPointerName == null) { + throw new InvalidXPointerException("Invalid Shorthand XPointer", xpointerString); + } + if (!NCName.isValid(shortHandPointerName)) { + throw new InvalidXPointerException( + "Shorthand XPointer is not a valid NCName: " + shortHandPointerName, xpointerString); + } + + xpointer.setShorthandPointer(shortHandPointerName); + break; + } + case Tokens.XPTRTOKEN_SCHEMENAME: { + + // Retrieve the local name and prefix to form the scheme name + token = tokens.nextToken(); + String prefix = tokens.getTokenString(token); + token = tokens.nextToken(); + String localName = tokens.getTokenString(token); + + String schemeName = prefix + localName; + + // The next character should be an open parenthesis + int openParenCount = 0; + int closeParenCount = 0; + + token = tokens.nextToken(); + String openParen = tokens.getTokenString(token); + if (!Objects.equals(openParen, "XPTRTOKEN_OPEN_PAREN")) { + + // can not have more than one ShortHand Pointer + if (token == Tokens.XPTRTOKEN_SHORTHAND) { + throw new InvalidXPointerException("Multiple Shorthand pointers", xpointerString); + } else { + throw new InvalidXPointerException("Invalid XPointer expression", xpointerString); + } + } + openParenCount++; + + // followed by zero or more ( and the schemeData + String schemeData = null; + while (tokens.hasMore()) { + token = tokens.nextToken(); + schemeData = tokens.getTokenString(token); + if (!Objects.equals(schemeData, "XPTRTOKEN_OPEN_PAREN")) { + break; + } + openParenCount++; + } + token = tokens.nextToken(); + schemeData = tokens.getTokenString(token); + + // followed by the same number of ) + if (tokens.hasMore()) { + token = tokens.nextToken(); + String closeParen = tokens.getTokenString(token); + if (!Objects.equals(closeParen, "XPTRTOKEN_CLOSE_PAREN")) { + throw new InvalidXPointerException( + "SchemeData not followed by close parenthesis", xpointerString); + } + } else { + throw new InvalidXPointerException( + "SchemeData not followed by close parenthesis", xpointerString); + } + + closeParenCount++; + + while (tokens.hasMore()) { + if (!Objects.equals(tokens.getTokenString(tokens.peekToken()), "XPTRTOKEN_OPEN_PAREN")) { + break; + } + closeParenCount++; + } + + // check if the number of open parenthesis are equal to the number of close parenthesis + if (openParenCount != closeParenCount) { + throw new InvalidXPointerException( + "Unbalanced parenthesis in XPointer expression", xpointerString); + } + + // Perform scheme specific parsing of the pointer part, make this more generic for any pointer part? + if (schemeName.equals(ELEMENT_SCHEME_NAME)) { + PointerPart elementSchemePointer = ElementPointerPart.parseFromString(schemeData); + xpointer.addPointerPart(elementSchemePointer); + } // Else an unknown scheme. + break; + } + default: + throw new InvalidXPointerException("Invalid XPointer expression", xpointerString); + } + } + } + + /** + * List of XPointer Framework tokens. + */ + private static class Tokens { + + /** + * XPointer Framework tokens + * [1] Pointer ::= Shorthand | SchemeBased + * [2] Shorthand ::= NCName + * [3] SchemeBased ::= PointerPart (S? PointerPart)* + * [4] PointerPart ::= SchemeName '(' SchemeData ')' + * [5] SchemeName ::= QName + * [6] SchemeData ::= EscapedData* + * [7] EscapedData ::= NormalChar | '^(' | '^)' | '^^' | '(' SchemeData ')' + * [8] NormalChar ::= UnicodeChar - [()^] + * [9] UnicodeChar ::= [#x0-#x10FFFF] + */ + private static final int XPTRTOKEN_OPEN_PAREN = 0, + XPTRTOKEN_CLOSE_PAREN = 1, + XPTRTOKEN_SHORTHAND = 2, + XPTRTOKEN_SCHEMENAME = 3, + XPTRTOKEN_SCHEMEDATA = 4; + + // Token count + private static final int INITIAL_TOKEN_COUNT = 1 << 8; + + private int[] fTokens = new int[INITIAL_TOKEN_COUNT]; + + private int fTokenCount = 0; + + // Current token position + private int fCurrentTokenIndex; + + private Hashtable fTokenNames = new Hashtable<>(); + + /** + * Constructor + */ + private Tokens() { + fTokenNames.put(XPTRTOKEN_OPEN_PAREN, "XPTRTOKEN_OPEN_PAREN"); + fTokenNames.put(XPTRTOKEN_CLOSE_PAREN, "XPTRTOKEN_CLOSE_PAREN"); + fTokenNames.put(XPTRTOKEN_SHORTHAND, "XPTRTOKEN_SHORTHAND"); + fTokenNames.put(XPTRTOKEN_SCHEMENAME, "XPTRTOKEN_SCHEMENAME"); + fTokenNames.put(XPTRTOKEN_SCHEMEDATA, "XPTRTOKEN_SCHEMEDATA"); + } + + /** + * Returns the token String + * + * @param token The index of the token + * @return String The token string + */ + private String getTokenString(int token) { + return (String) fTokenNames.get(token); + } + + /** + * Add the specified string as a token + * + * @param token The token string + */ + private void addToken(String token) { + Integer tokenInt = fTokenNames.entrySet().stream() + .filter(e -> Objects.equals(e.getValue(), token)) + .findFirst() + .map(Map.Entry::getKey) + .orElse(null); + if (tokenInt == null) { + tokenInt = fTokenNames.size(); + fTokenNames.put(tokenInt, token); + } + addToken(tokenInt); + } + + /** + * Add the specified int token + * + * @param token The int specifying the token + */ + private void addToken(int token) { + try { + fTokens[fTokenCount] = token; + } catch (ArrayIndexOutOfBoundsException ex) { + int[] oldList = fTokens; + fTokens = new int[fTokenCount << 1]; + System.arraycopy(oldList, 0, fTokens, 0, fTokenCount); + fTokens[fTokenCount] = token; + } + fTokenCount++; + } + + /** + * Returns true if the {@link #nextToken()} method + * returns a valid token. + */ + private boolean hasMore() { + return fCurrentTokenIndex < fTokenCount; + } + + /** + * Obtains the token at the current position, then advance + * the current position by one. + *

+ * throws If there's no such next token, this method throws + * new XNIException("XPointerProcessingError");. + */ + private int nextToken() { + if (fCurrentTokenIndex == fTokenCount) { + throw new IndexOutOfBoundsException("There are no more tokens to return."); + } + return fTokens[fCurrentTokenIndex++]; + } + + /** + * Obtains the token at the current position, without advancing + * the current position. + *

+ * If there's no such next token, this method throws + * new XNIException("XPointerProcessingError");. + */ + private int peekToken() { + if (fCurrentTokenIndex == fTokenCount) { + throw new IndexOutOfBoundsException("There are no more tokens to return."); + } + return fTokens[fCurrentTokenIndex]; + } + } + + /** + * The XPointer expression scanner. Scans the XPointer framework expression. + */ + private static class Scanner { + + /** + * 7-bit ASCII subset + *

+ * 0 1 2 3 4 5 6 7 8 9 A B C D E F + * 0, 0, 0, 0, 0, 0, 0, 0, 0, HT, LF, 0, 0, CR, 0, 0, // 0 + * 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1 + * SP, !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, // 2 + * 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <, =, >, ?, // 3 + * + * @, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, // 4 + * P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, // 5 + * `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, // 6 + * p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~, DEL // 7 + */ + private static final byte CHARTYPE_INVALID = 0, // invalid XML character + CHARTYPE_OTHER = 1, // not special - one of "#%&;?\`{}~" or DEL + CHARTYPE_WHITESPACE = 2, // one of "\t\n\r " (0x09, 0x0A, 0x0D, 0x20) + CHARTYPE_CARRET = 3, // ^ + CHARTYPE_OPEN_PAREN = 4, // '(' (0x28) + CHARTYPE_CLOSE_PAREN = 5, // ')' (0x29) + CHARTYPE_MINUS = 6, // '-' (0x2D) + CHARTYPE_PERIOD = 7, // '.' (0x2E) + CHARTYPE_SLASH = 8, // '/' (0x2F) + CHARTYPE_DIGIT = 9, // '0'-'9' (0x30 to 0x39) + CHARTYPE_COLON = 10, // ':' (0x3A) + CHARTYPE_EQUAL = 11, // '=' (0x3D) + CHARTYPE_LETTER = 12, // 'A'-'Z' or 'a'-'z' (0x41 to 0x5A and 0x61 to 0x7A) + CHARTYPE_UNDERSCORE = 13, // '_' (0x5F) + CHARTYPE_NONASCII = 14; // Non-ASCII Unicode codepoint (>= 0x80) + + private static final byte[] ASCII_CHAR_MAP = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 1, + 1, 1, 1, 1, 4, 5, 1, 1, 1, 6, 7, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 1, 1, 11, 1, 1, 1, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 3, 13, 1, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, + 1, 1 + }; + + /** + * Scans the XPointer Expression + */ + private static boolean scanExpr(Tokens tokens, String data, int currentOffset, int endOffset) + throws InvalidXPointerException { + int ch; + int openParen = 0; + int closeParen = 0; + int nameOffset, dataOffset; + String name = null; + String prefix; + String schemeData; + StringBuffer schemeDataBuff = new StringBuffer(); + + while (true) { + + if (currentOffset == endOffset) { + break; + } + ch = data.charAt(currentOffset); + + // + while (ch == ' ' || ch == 0x0A || ch == 0x09 || ch == 0x0D) { + if (++currentOffset == endOffset) { + break; + } + ch = data.charAt(currentOffset); + } + if (currentOffset == endOffset) { + break; + } + + // + // [1] Pointer ::= Shorthand | SchemeBased + // [2] Shorthand ::= NCName + // [3] SchemeBased ::= PointerPart (S? PointerPart)* + // [4] PointerPart ::= SchemeName '(' SchemeData ')' + // [5] SchemeName ::= QName + // [6] SchemeData ::= EscapedData* + // [7] EscapedData ::= NormalChar | '^(' | '^)' | '^^' | '(' SchemeData ')' + // [8] NormalChar ::= UnicodeChar - [()^] + // [9] UnicodeChar ::= [#x0-#x10FFFF] + // [?] QName ::= (NCName ':')? NCName + // [?] NCName ::= (Letter | '_') (NCNameChar)* + // [?] NCNameChar ::= Letter | Digit | '.' | '-' | '_' (ascii subset of 'NCNameChar') + // [?] Letter ::= [A-Za-z] (ascii subset of 'Letter') + // [?] Digit ::= [0-9] (ascii subset of 'Digit') + // + byte chartype = (ch >= 0x80) ? CHARTYPE_NONASCII : ASCII_CHAR_MAP[ch]; + + switch (chartype) { + case CHARTYPE_OPEN_PAREN: // '(' + addToken(tokens, Tokens.XPTRTOKEN_OPEN_PAREN); + openParen++; + ++currentOffset; + break; + + case CHARTYPE_CLOSE_PAREN: // ')' + addToken(tokens, Tokens.XPTRTOKEN_CLOSE_PAREN); + closeParen++; + ++currentOffset; + break; + + case CHARTYPE_CARRET: + case CHARTYPE_COLON: + case CHARTYPE_DIGIT: + case CHARTYPE_EQUAL: + case CHARTYPE_LETTER: + case CHARTYPE_MINUS: + case CHARTYPE_NONASCII: + case CHARTYPE_OTHER: + case CHARTYPE_PERIOD: + case CHARTYPE_SLASH: + case CHARTYPE_UNDERSCORE: + case CHARTYPE_WHITESPACE: + // Scanning SchemeName | Shorthand + if (openParen == 0) { + nameOffset = currentOffset; + currentOffset = scanNCName(data, endOffset, currentOffset); + + if (currentOffset == nameOffset) { + throw new InvalidXPointerException("InvalidShortHandPointer", data); + } + + if (currentOffset < endOffset) { + ch = data.charAt(currentOffset); + } else { + ch = -1; + } + + name = data.substring(nameOffset, currentOffset).intern(); + prefix = ""; + + // The name is a QName => a SchemeName + if (ch == ':') { + if (++currentOffset == endOffset) { + return false; + } + + ch = data.charAt(currentOffset); + prefix = name; + nameOffset = currentOffset; + currentOffset = scanNCName(data, endOffset, currentOffset); + + if (currentOffset == nameOffset) { + return false; + } + + if (currentOffset < endOffset) { + ch = data.charAt(currentOffset); + } else { + ch = -1; + } + + name = data.substring(nameOffset, currentOffset).intern(); + } + + // REVISIT: + if (currentOffset != endOffset) { + addToken(tokens, Tokens.XPTRTOKEN_SCHEMENAME); + tokens.addToken(prefix); + tokens.addToken(name); + } else { + // NCName => Shorthand + addToken(tokens, Tokens.XPTRTOKEN_SHORTHAND); + tokens.addToken(name); + } + + // reset open/close paren for the next pointer part + closeParen = 0; + + break; + + } else if (openParen > 0 && closeParen == 0 && name != null) { + // Scanning SchemeData + dataOffset = currentOffset; + currentOffset = scanData(data, schemeDataBuff, endOffset, currentOffset); + + if (currentOffset == dataOffset) { + throw new InvalidXPointerException("InvalidSchemeDataInXPointer", data); + } + + if (currentOffset < endOffset) { + ch = data.charAt(currentOffset); + } else { + ch = -1; + } + + schemeData = schemeDataBuff.toString().intern(); + addToken(tokens, Tokens.XPTRTOKEN_SCHEMEDATA); + tokens.addToken(schemeData); + + // reset open/close paren for the next pointer part + openParen = 0; + schemeDataBuff.delete(0, schemeDataBuff.length()); + + } else { + // ex. schemeName() + // Should we throw an exception with a more suitable message instead?? + return false; + } + } + } // end while + return true; + } + + /** + * Scans a NCName. + * From Namespaces in XML + * [5] NCName ::= (Letter | '_') (NCNameChar)* + * [6] NCNameChar ::= Letter | Digit | '.' | '-' | '_' | CombiningChar | Extender + * + * @param data A String containing the XPointer expression + * @param endOffset The int XPointer expression length + * @param currentOffset An int representing the current position of the XPointer expression pointer + */ + private static int scanNCName(String data, int endOffset, int currentOffset) { + int ch = data.charAt(currentOffset); + if (ch >= 0x80) { + if (!NCName.is11NameStartChar(ch, true)) { + return currentOffset; + } + } else { + byte chartype = ASCII_CHAR_MAP[ch]; + if (chartype != CHARTYPE_LETTER && chartype != CHARTYPE_UNDERSCORE) { + return currentOffset; + } + } + + // while (currentOffset++ < endOffset) { + while (++currentOffset < endOffset) { + ch = data.charAt(currentOffset); + if (ch >= 0x80) { + if (!NCName.is11NameChar(ch, true)) { + break; + } + } else { + byte chartype = ASCII_CHAR_MAP[ch]; + if (chartype != CHARTYPE_LETTER + && chartype != CHARTYPE_DIGIT + && chartype != CHARTYPE_PERIOD + && chartype != CHARTYPE_MINUS + && chartype != CHARTYPE_UNDERSCORE) { + break; + } + } + } + return currentOffset; + } + + /** + * Scans the SchemeData. + * [6] SchemeData ::= EscapedData* + * [7] EscapedData ::= NormalChar | '^(' | '^)' | '^^' | '(' SchemeData ')' + * [8] NormalChar ::= UnicodeChar - [()^] + * [9] UnicodeChar ::= [#x0-#x10FFFF] + */ + private static int scanData(String data, StringBuffer schemeData, int endOffset, int currentOffset) { + while (true) { + + if (currentOffset == endOffset) { + break; + } + + int ch = data.charAt(currentOffset); + byte chartype = (ch >= 0x80) ? CHARTYPE_NONASCII : ASCII_CHAR_MAP[ch]; + + if (chartype == CHARTYPE_OPEN_PAREN) { + schemeData.append(ch); + // schemeData.append(Tokens.XPTRTOKEN_OPEN_PAREN); + currentOffset = scanData(data, schemeData, endOffset, ++currentOffset); + if (currentOffset == endOffset) { + return currentOffset; + } + + ch = data.charAt(currentOffset); + chartype = (ch >= 0x80) ? CHARTYPE_NONASCII : ASCII_CHAR_MAP[ch]; + + if (chartype != CHARTYPE_CLOSE_PAREN) { + return endOffset; + } + schemeData.append((char) ch); + ++currentOffset; // + + } else if (chartype == CHARTYPE_CLOSE_PAREN) { + return currentOffset; + + } else if (chartype == CHARTYPE_CARRET) { + ch = data.charAt(++currentOffset); + chartype = (ch >= 0x80) ? CHARTYPE_NONASCII : ASCII_CHAR_MAP[ch]; + + if (chartype != CHARTYPE_CARRET + && chartype != CHARTYPE_OPEN_PAREN + && chartype != CHARTYPE_CLOSE_PAREN) { + break; + } + schemeData.append((char) ch); + ++currentOffset; + + } else { + schemeData.append((char) ch); + ++currentOffset; // + } + } + + return currentOffset; + } + + // + // Protected methods + // + + /** + * This method adds the specified token to the token list. By + * default, this method allows all tokens. However, subclasses + * of the XPathExprScanner can override this method in order + * to disallow certain tokens from being used in the scanned + * XPath expression. This is a convenient way of allowing only + * a subset of XPath. + */ + protected static void addToken(Tokens tokens, int token) { + if (token == Tokens.XPTRTOKEN_OPEN_PAREN + || token == Tokens.XPTRTOKEN_CLOSE_PAREN + || token == Tokens.XPTRTOKEN_SCHEMENAME + || token == Tokens.XPTRTOKEN_SCHEMEDATA + || token == Tokens.XPTRTOKEN_SHORTHAND) { + tokens.addToken(token); + return; + } + throw new IllegalArgumentException("InvalidXPointerToken"); + } + } // class Scanner +} diff --git a/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XmlnsPointerPart.java b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XmlnsPointerPart.java new file mode 100644 index 000000000000..37d77dc98516 --- /dev/null +++ b/maven-stax-xinclude/src/main/java/org/apache/maven/stax/xinclude/XmlnsPointerPart.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +/** + * Represents a fragment identifier conforming to the XML Pointer Language Framework. + *

+ * This class is based upon a class of the same name in Apache Woden. + */ +class XmlnsPointerPart implements PointerPart { + private final String prefix; + private final String namespace; + + XmlnsPointerPart(String prefix, String namespace) { + if (prefix == null | namespace == null) { + throw new IllegalArgumentException(); + } + this.prefix = prefix; + this.namespace = namespace; + } + + public String toString() { + return "xmlns(" + prefix + "=" + namespace + ")"; + } + + public void prefixNamespaces(XPointer xpointer) { + // This PointerPart does not have any namespaces. + } +} diff --git a/maven-stax-xinclude/src/test/java/org/apache/maven/stax/xinclude/XIncludeTest.java b/maven-stax-xinclude/src/test/java/org/apache/maven/stax/xinclude/XIncludeTest.java new file mode 100644 index 000000000000..1b1b37e04993 --- /dev/null +++ b/maven-stax-xinclude/src/test/java/org/apache/maven/stax/xinclude/XIncludeTest.java @@ -0,0 +1,416 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.stream.StreamSource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.ctc.wstx.stax.WstxInputFactory; +import com.ctc.wstx.stax.WstxOutputFactory; +import org.codehaus.stax2.XMLInputFactory2; +import org.codehaus.stax2.XMLOutputFactory2; +import org.codehaus.stax2.io.EscapingWriterFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class XIncludeTest { + + @Test + void testBasicInclusion() throws Exception { + String input = "\n" + "\n" + + "

120 Mz is adequate for an average home user.

\n" + + " \n" + + ""; + Map includes = Collections.singletonMap( + "http://www.example.com/disclaimer.xml", + "\n" + "\n" + + "

The opinions represented herein represent those of the individual\n" + + " and should not be interpreted as official policy endorsed by this\n" + + " organization.

\n" + + "
"); + String expected = "\n" + "\n" + + "

120 Mz is adequate for an average home user.

\n" + + " \n" + + "

The opinions represented herein represent those of the individual\n" + + " and should not be interpreted as official policy endorsed by this\n" + + " organization.

\n" + + "
\n" + + "
"; + + assertXInclude(input, includes, expected); + } + + @Test + void testTextualInclusion() throws Exception { + String input = "\n" + "\n" + + "

This document has been accessed\n" + + " times.

\n" + + "
"; + Map includes = Collections.singletonMap("http://www.example.com/count.txt", "324387"); + String expected = "\n" + "\n" + + "

This document has been accessed\n" + + " 324387 times.

\n" + + "
"; + + assertXInclude(input, includes, expected); + } + + @Test + void testTextualInclusionOfXml() throws Exception { + String input = "\n" + "\n" + + "

The following is the source of the \"data.xml\" resource:

\n" + + " \n" + + "
"; + Map includes = Collections.singletonMap( + "http://www.example.com/data.xml", + "\n" + "\n" + " \n" + ""); + String expected = "\n" + "\n" + + "

The following is the source of the \"data.xml\" resource:

\n" + + " <?xml version='1.0'?>\n" + + "<data>\n" + + " <item><![CDATA[Brooks & Shields]]></item>\n" + + "</data>\n" + + "
"; + + assertXInclude(input, includes, expected); + } + + @Test + void testFragmentInclusion() throws Exception { + String input = "\n" + "\n" + + " Joe Smith\n" + + " 20040930\n" + + " \n" + + " 40\n" + + " \n" + + ""; + Map includes = new HashMap<>(); + includes.put( + "http://www.example.com/price-list.dtd", + " " + " " + + " "); + includes.put( + "http://www.example.com/price-list.xml", + "\n" + "\n" + + "\n" + + " \n" + + " \n" + + "

Normal Widget

\n" + + "
\n" + + " \n" + + " 39.95\n" + + " 34.95\n" + + " 29.95\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "

Super-sized widget with bells and whistles.

\n" + + "
\n" + + " \n" + + " 59.95\n" + + " 54.95\n" + + " 49.95\n" + + " \n" + + "
\n" + + "
"); + String expected = "\n" + "\n" + + " Joe Smith\n" + + " 20040930\n" + + " \n" + + "

Super-sized widget with bells and whistles.

\n" + + "
\n" + + " 40\n" + + " 54.95\n" + + "
"; + + assertXInclude(input, includes, expected); + } + + @Test + void testTextualInclusionWithFragment() throws Exception { + String input1 = "\n" + "\n" + + "

This example includes just the ‘use' lines from a Perl script.

\n" + + "
\n" + + "

There are four of them.

\n" + + "
"; + Map includes = Collections.singletonMap( + "http://www.example.com/code.pl", + "#!/usr/bin/perl -- # --*-Perl-*--\n" + "\n" + + "use strict;\n" + + "use English;\n" + + "use Getopt::Std;\n" + + "use vars qw($opt_p $opt_q $opt_u $opt_m);\n" + + "\n" + + "my $usage = \"Usage: $0 [-q] [-u|-p|-m] file [ file ... ]\\n\";\n" + + "\n" + + "die $usage if ! getopts('qupm');\n" + + "\n" + + "die $usage if ($opt_p + $opt_u + $opt_m) != 1;\n" + + "\n" + + "my $file = shift @ARGV || die $usage;\n" + + "\n" + + "my $opt = '-u' if $opt_u;\n" + + "$opt = '-p' if $opt_p;\n" + + "$opt = '-m' if $opt_m;\n" + + "\n" + + "while ($file) {\n" + + " print \"Converting $file to $opt linebreaks.\\n\" if !$opt_q;\n" + + " open (F, \"$file\");\n" + + " binmode F;\n" + + " read (F, $_, -s $file);\n" + + " close (F);\n" + + "\n" + + " s/\\r\\n/\\n/sg;\n" + + " s/\\r/\\n/sg;\n" + + "\n" + + " if ($opt eq '-p') {\n" + + "\ts/\\n/\\r\\n/sg;\n" + + " } elsif ($opt eq '-m') {\n" + + "\ts/\\n/\\r/sg;\n" + + " }\n" + + "\n" + + " open (F, \">$file\");\n" + + " binmode F;\n" + + " print F $_;\n" + + " close (F);\n" + + "\n" + + " $file = shift @ARGV;\n" + + "}"); + String expected1 = "\n" + "\n" + + "

This example includes just the ‘use' lines from a Perl script.

\n" + + "
use strict;\n"
+                + "use English;\n"
+                + "use Getopt::Std;\n"
+                + "use vars qw($opt_p $opt_q $opt_u $opt_m);\n"
+                + "
\n" + + "

There are four of them.

\n" + + "
"; + + assertXInclude(input1, includes, expected1); + + String input2 = "\n" + "\n" + + "

This example includes a range of characters.

\n" + + "
\n" + + "
"; + String expected2 = "\n" + "\n" + + "

This example includes a range of characters.

\n" + + "
_q $opt_u $opt_m);\n"
+                + "\n"
+                + "my $usage = \"Usage: $0 [-q] [-u|-p|-m] file [ file ... ]\\n\";\n"
+                + "\n"
+                + "die $usage if ! ge
\n" + + "
"; + + assertXInclude(input2, includes, expected2); + } + + @Test + void testAttributeCopying() throws Exception { + String input = "\n" + + "\n" + + "

This example includes a “definition” paragraph from some document\n" + + "twice using attribute copying.

\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
"; + Map includes = Collections.singletonMap( + "http://www.example.com/src.xml", + "\n" + " Some paragraph.\n" + + " Some definition.\n" + + " Some other paragraph.\n" + + ""); + String expected = "\n" + + "\n" + + "

This example includes a “definition” paragraph from some document\n" + + "twice using attribute copying.

\n" + + "\n" + + "Some definition.\n" + + "\n" + + "Some definition.\n" + + "\n" + + "
"; + + assertXInclude(input, includes, expected); + } + + @Test + void testAttributeCopying2() throws Exception { + String input = "\n" + + "\n" + + "

This example shows attribute replacement.

\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
"; + Map includes = Collections.singletonMap( + "http://www.example.com/src-2.xml", + "\n" + + "

Consider the Wombat.

\n" + + "
"); + String expected = "\n" + + "\n" + + "

This example shows attribute replacement.

\n" + + "\n" + + "

Consider the Wombat.

\n" + + "\n" + + "

Consider the Wombat.

\n" + + "\n" + + "
"; + + assertXInclude(input, includes, expected); + } + + @Test + void testFallback() throws Exception { + String input = "\n" + "
\n" + + " \n" + + " \n" + + " Report error\n" + + " \n" + + " \n" + + "
"; + Map includes = Collections.emptyMap(); + String expected = "\n" + "
\n" + + " Report error\n" + + "
"; + + assertXInclude(input, includes, expected); + } + + private void assertXInclude(String input, Map includes, String expected) throws Exception { + WstxInputFactory factory = new WstxInputFactory(); + WstxOutputFactory outputFactory = new WstxOutputFactory(); + factory.setProperty(XMLInputFactory2.P_REPORT_PROLOG_WHITESPACE, true); + factory.setXMLResolver((publicID, systemID, baseURI, namespace) -> { + String r = URI.create(baseURI).resolve(systemID).toString(); + String text = includes.get(r); + if (text == null) { + return null; + } + return new StreamSource(new ByteArrayInputStream(text.getBytes()), r); + }); + + XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(input)); + XMLStreamReader xiReader = + new XIncludeStreamReader(factory, outputFactory, "http://www.example.com/pom.xml", reader); + + XMLEventReader er = factory.createXMLEventReader(xiReader); + StringWriter sw = new StringWriter(); + outputFactory.setProperty(XMLOutputFactory2.P_TEXT_ESCAPER, new EscapingWriterFactory() { + @Override + public Writer createEscapingWriterFor(Writer writer, String s) throws UnsupportedEncodingException { + return new Writer() { + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + for (int i = 0; i < len; i++) { + char ch = cbuf[off + i]; + switch (ch) { + case '<': + writer.write("<"); + break; + case '>': + writer.write(">"); + break; + case '&': + writer.write("&"); + break; + default: + writer.write(ch); + break; + } + } + } + + @Override + public void flush() throws IOException { + writer.flush(); + } + + @Override + public void close() throws IOException { + writer.close(); + } + }; + } + + @Override + public Writer createEscapingWriterFor(OutputStream output, String s) throws UnsupportedEncodingException { + return new Writer() { + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + for (int i = 0; i < len; i++) { + char ch = cbuf[off + i]; + switch (ch) { + case '<': + output.write("<".getBytes()); + break; + case '>': + output.write(">".getBytes()); + break; + case '&': + output.write("&".getBytes()); + break; + default: + output.write(ch); + break; + } + } + } + + @Override + public void flush() throws IOException { + output.flush(); + } + + @Override + public void close() throws IOException { + output.close(); + } + }; + } + }); + XMLEventWriter ew = outputFactory.createXMLEventWriter(sw); + while (er.hasNext()) { + ew.add(er.nextEvent()); + } + + assertEquals(expected, sw.toString()); + } +} diff --git a/maven-stax-xinclude/src/test/java/org/apache/maven/stax/xinclude/XPointerTest.java b/maven-stax-xinclude/src/test/java/org/apache/maven/stax/xinclude/XPointerTest.java new file mode 100644 index 000000000000..3def756664f9 --- /dev/null +++ b/maven-stax-xinclude/src/test/java/org/apache/maven/stax/xinclude/XPointerTest.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.stax.xinclude; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class XPointerTest { + + @Test + void testXPointerString() { + XPointer xpointer; + // Good Tests. + String[] goodXPointers = new String[] { + // Shorthand + "justaShorthandPointer", + // element() scheme. + "element(AnNCName)", + "element(AnNCName/1)", + "element(AnNCName/1/2/34)", + "element(/1)", + "element(/1/4/43)" + }; + + // Test XPointers + for (String goodXPointer : goodXPointers) { + try { + xpointer = new XPointer(goodXPointer); + String result = xpointer.toString(); + assertEquals( + goodXPointer, + result, + "The serialisation of XPointer: " + goodXPointer + ", produced a different result: " + result); + } catch (InvalidXPointerException e) { + fail("XPointer: " + goodXPointer + ", is reported as being invalid when it actually is valid."); + } catch (Exception e) { + fail("Failed with unexpected exception: " + e); + } + } + + // Bad Tests. + String[] badXPointers = new String[] { + // Shorthand + "justaShorthand##Pointer", + // element() scheme. + "", + "element(/)", + "element(//)", + "element(/1/2/3//)", + "element(/1/b/3)", + "element(Not!AnNCNa-me)", + "element(/ncname)", + "element(AnNCName/)", + "element(AnNCName//)", + "element(AnNCName/1/b)", + "element(AnNCName/1/2//)" + }; + + // Test XPointers + for (String badXPointer : badXPointers) { + try { + xpointer = new XPointer(badXPointer); + fail("XPointer parser failed to thrown an exception for invalid XPointer: " + badXPointer); + } catch (Exception e) { // See if exception is anything other than InvalidXPointerException which we want. + if (!(e instanceof InvalidXPointerException)) { + fail("Parsing the XPointer threw an unexpected exception: " + e + ", On XPointer: " + badXPointer); + } + } + } + } +} diff --git a/pom.xml b/pom.xml index 5c93937fc067..d8c0b29b8e71 100644 --- a/pom.xml +++ b/pom.xml @@ -103,9 +103,10 @@ under the License. maven-bom maven-plugin-api maven-builder-support + maven-stax-xinclude + api maven-model maven-model-builder - api maven-xml-impl maven-core maven-settings diff --git a/src/mdo/reader-stax.vm b/src/mdo/reader-stax.vm index 61bd8428fe71..e17f43aba659 100644 --- a/src/mdo/reader-stax.vm +++ b/src/mdo/reader-stax.vm @@ -47,6 +47,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; + +import com.ctc.wstx.api.WstxInputProperties; import org.apache.maven.api.annotations.Generated; #if ( $locationTracking ) import ${packageModelV4}.InputSource; @@ -58,6 +60,7 @@ import ${packageModelV4}.${class.name}; import org.apache.maven.internal.xml.XmlNodeBuilder; import org.apache.maven.api.xml.XmlNode; import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLResolver; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.transform.stream.StreamSource; @@ -337,10 +340,11 @@ public class ${className} { private boolean addLocationInformation = true; #end - private final ContentTransformer contentTransformer; + private ContentTransformer contentTransformer = (s, f) -> s; + + private XMLResolver xmlResolver; public ${className}() { - this((s, f) -> s); } public ${className}(ContentTransformer contentTransformer) { @@ -385,6 +389,22 @@ public class ${className} { } //-- void setAddLocationInformation(boolean) #end + public ContentTransformer getContentTransformer() { + return contentTransformer; + } + + public void setContentTransformer(ContentTransformer contentTransformer) { + this.contentTransformer = contentTransformer; + } + + public XMLResolver getXmlResolver() { + return xmlResolver; + } + + public void setXmlResolver(XMLResolver xmlResolver) { + this.xmlResolver = xmlResolver; + } + public ${root.name} read(Reader reader) throws XMLStreamException { #if ( $locationTracking ) return read(reader, true, null); @@ -406,7 +426,11 @@ public class ${className} { public ${root.name} read(Reader reader, boolean strict) throws XMLStreamException { #end XMLInputFactory factory = new com.ctc.wstx.stax.WstxInputFactory(); - factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true); + factory.setProperty(WstxInputProperties.P_TREAT_CHAR_REFS_AS_ENTS, false); + factory.setProperty(WstxInputProperties.P_ENTITY_RESOLVER, xmlResolver); + factory.setProperty(WstxInputProperties.P_UNDECLARED_ENTITY_RESOLVER, getUndeclaredXmlResolver(strict)); #if ( $locationTracking ) StreamSource streamSource = new StreamSource(reader, source != null ? source.getLocation() : null); #else @@ -443,7 +467,11 @@ public class ${className} { public ${root.name} read(InputStream in, boolean strict) throws XMLStreamException { #end XMLInputFactory factory = new com.ctc.wstx.stax.WstxInputFactory(); - factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true); + factory.setProperty(WstxInputProperties.P_TREAT_CHAR_REFS_AS_ENTS, false); + factory.setProperty(WstxInputProperties.P_ENTITY_RESOLVER, xmlResolver); + factory.setProperty(WstxInputProperties.P_UNDECLARED_ENTITY_RESOLVER, getUndeclaredXmlResolver(strict)); #if ( $locationTracking ) StreamSource streamSource = new StreamSource(in, source != null ? source.getLocation() : null); #else @@ -727,6 +755,15 @@ public class ${className} { return tagName; } + private XMLResolver getUndeclaredXmlResolver(boolean strict) { + return (String publicID, String systemID, String baseURI, String namespace) -> { + if (publicID == null && !strict && addDefaultEntities) { + return DEFAULT_ENTITIES.get(namespace); + } + return null; + }; + } + /** * Method checkUnknownAttribute. * @@ -829,18 +866,6 @@ public class ${className} { while (true) { if (eventType == XMLStreamReader.CHARACTERS || eventType == XMLStreamReader.CDATA) { result.append(parser.getText()); - } else if (eventType == XMLStreamReader.ENTITY_REFERENCE) { - String val = null; - if (strict) { - throw new XMLStreamException("Entities are not supported in strict mode", parser.getLocation(), null); - } else if (addDefaultEntities) { - val = DEFAULT_ENTITIES.get(parser.getLocalName()); - } - if (val != null) { - result.append(val); - } else { - result.append("&").append(parser.getLocalName()).append(";"); - } } else if (eventType != XMLStreamReader.COMMENT) { break; }