Skip to content

Commit

Permalink
[MNG-5862] Support XML entities and XInclude
Browse files Browse the repository at this point in the history
  • Loading branch information
gnodet committed Sep 14, 2023
1 parent 85130dd commit 267d4e1
Show file tree
Hide file tree
Showing 30 changed files with 3,820 additions and 31 deletions.
Expand Up @@ -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() {}

/**
Expand All @@ -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<String, String> 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);
}
Expand Down
5 changes: 5 additions & 0 deletions maven-bom/pom.xml
Expand Up @@ -127,6 +127,11 @@ under the License.
<artifactId>maven-api-xml</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-stax-xinclude</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model-builder</artifactId>
Expand Down
Expand Up @@ -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(
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions maven-model-builder/pom.xml
Expand Up @@ -68,6 +68,10 @@ under the License.
<groupId>org.apache.maven</groupId>
<artifactId>maven-builder-support</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-stax-xinclude</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.eclipse.sisu</groupId>
Expand Down
Expand Up @@ -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()) {
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -100,14 +104,39 @@ private Path getRootDirectory(Map<String, ?> options) {
return (Path) value;
}

private boolean getXInclude(Map<String, ?> options) {
Object value = (options != null) ? options.get(XINCLUDE) : null;
return value instanceof Boolean && (Boolean) value;
}

private Model read(InputStream input, Path pomFile, Map<String, ?> 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(
Expand All @@ -132,7 +161,6 @@ private Model read(InputStream input, Path pomFile, Map<String, ?> options) thro
private Model read(Reader reader, Path pomFile, Map<String, ?> 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);
Expand All @@ -155,4 +183,18 @@ private Model read(Reader reader, Path pomFile, Map<String, ?> 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;
}
}
}
@@ -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);
}
}
}
Expand Up @@ -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.
Expand Down

0 comments on commit 267d4e1

Please sign in to comment.