From af2dea7e5dadb3ed02031162e88a28a6e3d7dbb8 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Thu, 26 Feb 2026 08:35:40 +0100 Subject: [PATCH 1/2] Fix #11715: preserve 4.1.0 namespace/schema in help:effective-pom When generating the effective POM for a modelVersion 4.1.0 project, preserve the root namespace and schemaLocation as 4.1.0 instead of falling back to 4.0.0. Add/keep coverage with MavenITgh11715EffectivePomNamespaceTest to verify the effective POM header contains: - xmlns http://maven.apache.org/POM/4.1.0 - schemaLocation .../maven-4.1.0.xsd --- .../org/apache/maven/api/xml/XmlService.java | 9 +++- .../maven/model/io/xpp3/MavenXpp3Writer.java | 28 ++++++++++ .../org/apache/maven/model/ModelTest.java | 22 ++++++++ .../internal/xml/XmlServiceLoadingTest.java | 44 ++++++++++++++++ ...venITgh11715EffectivePomNamespaceTest.java | 52 +++++++++++++++++++ .../src/test/resources/gh-11715/pom.xml | 10 ++++ 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlServiceLoadingTest.java create mode 100644 its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11715EffectivePomNamespaceTest.java create mode 100644 its/core-it-suite/src/test/resources/gh-11715/pom.xml diff --git a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlService.java b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlService.java index e6735e255f54..f8a9742b3faf 100644 --- a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlService.java +++ b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlService.java @@ -239,8 +239,15 @@ private static XmlService getService() { /** Holder class for lazy initialization of the default instance */ private static final class Holder { - static final XmlService INSTANCE = ServiceLoader.load(XmlService.class) + static final XmlService INSTANCE = ServiceLoader.load(XmlService.class, XmlService.class.getClassLoader()) .findFirst() + .or(() -> { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + return contextClassLoader != null + ? ServiceLoader.load(XmlService.class, contextClassLoader) + .findFirst() + : java.util.Optional.empty(); + }) .orElseThrow(() -> new IllegalStateException("No XmlService implementation found")); private Holder() {} diff --git a/compat/maven-model/src/main/java/org/apache/maven/model/io/xpp3/MavenXpp3Writer.java b/compat/maven-model/src/main/java/org/apache/maven/model/io/xpp3/MavenXpp3Writer.java index 66328256d9df..56dc6c4c24db 100644 --- a/compat/maven-model/src/main/java/org/apache/maven/model/io/xpp3/MavenXpp3Writer.java +++ b/compat/maven-model/src/main/java/org/apache/maven/model/io/xpp3/MavenXpp3Writer.java @@ -33,6 +33,14 @@ */ @Deprecated public class MavenXpp3Writer { + private static final String NAMESPACE_PREFIX = "http://maven.apache.org/POM/"; + + private static final String SCHEMA_LOCATION_PREFIX = "https://maven.apache.org/xsd/maven-"; + + private static final String SCHEMA_LOCATION_SUFFIX = ".xsd"; + + private static final String DEFAULT_MODEL_VERSION = "4.0.0"; + // --------------------------/ // - Class/Member Variables -/ // --------------------------/ @@ -79,6 +87,7 @@ public void setStringFormatter(InputLocation.StringFormatter stringFormatter) { */ public void write(Writer writer, Model model) throws IOException { try { + configureDelegate(model); delegate.write(writer, model.getDelegate()); } catch (XMLStreamException e) { throw new IOException(e); @@ -94,9 +103,28 @@ public void write(Writer writer, Model model) throws IOException { */ public void write(OutputStream stream, Model model) throws IOException { try { + configureDelegate(model); delegate.write(stream, model.getDelegate()); } catch (XMLStreamException e) { throw new IOException(e); } } // -- void write( OutputStream, Model ) + + private void configureDelegate(Model model) { + String version = model.getModelVersion(); + if (version != null) { + version = version.trim(); + } + if (version == null || version.isBlank()) { + String namespaceUri = model.getDelegate().getNamespaceUri(); + if (namespaceUri != null && namespaceUri.startsWith(NAMESPACE_PREFIX)) { + version = namespaceUri.substring(NAMESPACE_PREFIX.length()).trim(); + } + } + if (version == null || version.isBlank()) { + version = DEFAULT_MODEL_VERSION; + } + delegate.setNamespace(NAMESPACE_PREFIX + version); + delegate.setSchemaLocation(SCHEMA_LOCATION_PREFIX + version + SCHEMA_LOCATION_SUFFIX); + } } diff --git a/compat/maven-model/src/test/java/org/apache/maven/model/ModelTest.java b/compat/maven-model/src/test/java/org/apache/maven/model/ModelTest.java index 967af237c5cf..3dcd2d9ff3f5 100644 --- a/compat/maven-model/src/test/java/org/apache/maven/model/ModelTest.java +++ b/compat/maven-model/src/test/java/org/apache/maven/model/ModelTest.java @@ -18,12 +18,16 @@ */ package org.apache.maven.model; +import java.io.StringWriter; + +import org.apache.maven.model.io.xpp3.MavenXpp3Writer; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests {@code Model}. @@ -67,6 +71,24 @@ void testToStringNullSafe() { assertNotNull(new Model().toString()); } + @Test + void testWritePreservesModelVersionNamespace() throws Exception { + Model model = new Model(); + model.setModelVersion("4.1.0"); + model.setGroupId("g"); + model.setArtifactId("a"); + model.setVersion("1"); + + StringWriter output = new StringWriter(); + new MavenXpp3Writer().write(output, model); + + String xml = output.toString(); + assertTrue(xml.contains("xmlns=\"http://maven.apache.org/POM/4.1.0\"")); + assertTrue( + xml.contains( + "xsi:schemaLocation=\"http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd\"")); + } + @Test void testPropertiesClear() { // Test for issue #11552: NullPointerException when clearing properties diff --git a/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlServiceLoadingTest.java b/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlServiceLoadingTest.java new file mode 100644 index 000000000000..f51107426394 --- /dev/null +++ b/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlServiceLoadingTest.java @@ -0,0 +1,44 @@ +/* + * 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.internal.xml; + +import java.io.StringReader; +import java.net.URL; +import java.net.URLClassLoader; + +import org.apache.maven.api.xml.XmlNode; +import org.apache.maven.api.xml.XmlService; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class XmlServiceLoadingTest { + + @Test + void testServiceLoaderFallbackWhenContextClassLoaderCannotSeeProvider() throws Exception { + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + try (URLClassLoader contextClassLoader = new URLClassLoader(new URL[0], null)) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + XmlNode node = XmlService.read(new StringReader("")); + assertEquals("configuration", node.name()); + } finally { + Thread.currentThread().setContextClassLoader(previous); + } + } +} diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11715EffectivePomNamespaceTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11715EffectivePomNamespaceTest.java new file mode 100644 index 000000000000..7bbcc571f789 --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11715EffectivePomNamespaceTest.java @@ -0,0 +1,52 @@ +/* + * 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.it; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +/** + * This is a test set for GH-11715. + * + * Verifies that help:effective-pom preserves the 4.1.0 root namespace/schema for a 4.1.0 POM. + * + * @since 4.0.0-rc-5 + */ +class MavenITgh11715EffectivePomNamespaceTest extends AbstractMavenIntegrationTestCase { + + MavenITgh11715EffectivePomNamespaceTest() { + super("(4.0.0-rc-5,)"); + } + + @Test + void testIt() throws Exception { + Path basedir = extractResources("/gh-11715").getAbsoluteFile().toPath(); + + Verifier verifier = newVerifier(basedir.toString()); + verifier.addCliArgument("help:effective-pom"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + verifier.verifyTextInLog("4.1.0"); + verifier.verifyTextInLog(" + + 4.1.0 + org.apache.maven.its.gh11715 + effective-pom-namespace + 1.0.0 + pom + From a082687d840bb122f2ed1ead4b7dd35053e9c997 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 5 Jun 2026 01:56:46 +0200 Subject: [PATCH 2/2] Extract XmlService classloader fix and use MavenModelVersion - Remove XmlService classloader fallback (extracted to PR #12237) - Use MavenModelVersion to compute the minimum model version instead of hardcoding 4.0.0 as the fallback - Align namespace/schemaLocation format strings with TransformerSupport conventions Co-Authored-By: Claude Opus 4.6 --- .../org/apache/maven/api/xml/XmlService.java | 9 +--- .../maven/model/io/xpp3/MavenXpp3Writer.java | 27 +++--------- .../org/apache/maven/model/ModelTest.java | 27 ++++++++++-- .../internal/xml/XmlServiceLoadingTest.java | 44 ------------------- 4 files changed, 30 insertions(+), 77 deletions(-) delete mode 100644 impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlServiceLoadingTest.java diff --git a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlService.java b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlService.java index f8a9742b3faf..e6735e255f54 100644 --- a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlService.java +++ b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlService.java @@ -239,15 +239,8 @@ private static XmlService getService() { /** Holder class for lazy initialization of the default instance */ private static final class Holder { - static final XmlService INSTANCE = ServiceLoader.load(XmlService.class, XmlService.class.getClassLoader()) + static final XmlService INSTANCE = ServiceLoader.load(XmlService.class) .findFirst() - .or(() -> { - ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); - return contextClassLoader != null - ? ServiceLoader.load(XmlService.class, contextClassLoader) - .findFirst() - : java.util.Optional.empty(); - }) .orElseThrow(() -> new IllegalStateException("No XmlService implementation found")); private Holder() {} diff --git a/compat/maven-model/src/main/java/org/apache/maven/model/io/xpp3/MavenXpp3Writer.java b/compat/maven-model/src/main/java/org/apache/maven/model/io/xpp3/MavenXpp3Writer.java index 56dc6c4c24db..3b95f08cb0c4 100644 --- a/compat/maven-model/src/main/java/org/apache/maven/model/io/xpp3/MavenXpp3Writer.java +++ b/compat/maven-model/src/main/java/org/apache/maven/model/io/xpp3/MavenXpp3Writer.java @@ -26,6 +26,7 @@ import org.apache.maven.model.InputLocation; import org.apache.maven.model.Model; +import org.apache.maven.model.v4.MavenModelVersion; import org.apache.maven.model.v4.MavenStaxWriter; /** @@ -33,13 +34,9 @@ */ @Deprecated public class MavenXpp3Writer { - private static final String NAMESPACE_PREFIX = "http://maven.apache.org/POM/"; + private static final String NAMESPACE_FORMAT = "http://maven.apache.org/POM/%s"; - private static final String SCHEMA_LOCATION_PREFIX = "https://maven.apache.org/xsd/maven-"; - - private static final String SCHEMA_LOCATION_SUFFIX = ".xsd"; - - private static final String DEFAULT_MODEL_VERSION = "4.0.0"; + private static final String SCHEMA_LOCATION_FORMAT = "https://maven.apache.org/xsd/maven-%s.xsd"; // --------------------------/ // - Class/Member Variables -/ @@ -111,20 +108,8 @@ public void write(OutputStream stream, Model model) throws IOException { } // -- void write( OutputStream, Model ) private void configureDelegate(Model model) { - String version = model.getModelVersion(); - if (version != null) { - version = version.trim(); - } - if (version == null || version.isBlank()) { - String namespaceUri = model.getDelegate().getNamespaceUri(); - if (namespaceUri != null && namespaceUri.startsWith(NAMESPACE_PREFIX)) { - version = namespaceUri.substring(NAMESPACE_PREFIX.length()).trim(); - } - } - if (version == null || version.isBlank()) { - version = DEFAULT_MODEL_VERSION; - } - delegate.setNamespace(NAMESPACE_PREFIX + version); - delegate.setSchemaLocation(SCHEMA_LOCATION_PREFIX + version + SCHEMA_LOCATION_SUFFIX); + String version = new MavenModelVersion().getModelVersion(model.getDelegate()); + delegate.setNamespace(String.format(NAMESPACE_FORMAT, version)); + delegate.setSchemaLocation(String.format(SCHEMA_LOCATION_FORMAT, version)); } } diff --git a/compat/maven-model/src/test/java/org/apache/maven/model/ModelTest.java b/compat/maven-model/src/test/java/org/apache/maven/model/ModelTest.java index 3dcd2d9ff3f5..652ff9535430 100644 --- a/compat/maven-model/src/test/java/org/apache/maven/model/ModelTest.java +++ b/compat/maven-model/src/test/java/org/apache/maven/model/ModelTest.java @@ -72,9 +72,28 @@ void testToStringNullSafe() { } @Test - void testWritePreservesModelVersionNamespace() throws Exception { + void testWriteUsesMinimumModelVersionNamespace() throws Exception { + Model model = new Model(org.apache.maven.api.model.Model.newBuilder() + .modelVersion("4.1.0") + .root(true) + .groupId("g") + .artifactId("a") + .version("1") + .build()); + + StringWriter output = new StringWriter(); + new MavenXpp3Writer().write(output, model); + + String xml = output.toString(); + assertTrue(xml.contains("xmlns=\"http://maven.apache.org/POM/4.1.0\"")); + assertTrue( + xml.contains( + "xsi:schemaLocation=\"http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd\"")); + } + + @Test + void testWriteDefaultsTo400Namespace() throws Exception { Model model = new Model(); - model.setModelVersion("4.1.0"); model.setGroupId("g"); model.setArtifactId("a"); model.setVersion("1"); @@ -83,10 +102,10 @@ void testWritePreservesModelVersionNamespace() throws Exception { new MavenXpp3Writer().write(output, model); String xml = output.toString(); - assertTrue(xml.contains("xmlns=\"http://maven.apache.org/POM/4.1.0\"")); + assertTrue(xml.contains("xmlns=\"http://maven.apache.org/POM/4.0.0\"")); assertTrue( xml.contains( - "xsi:schemaLocation=\"http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd\"")); + "xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\"")); } @Test diff --git a/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlServiceLoadingTest.java b/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlServiceLoadingTest.java deleted file mode 100644 index f51107426394..000000000000 --- a/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlServiceLoadingTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.internal.xml; - -import java.io.StringReader; -import java.net.URL; -import java.net.URLClassLoader; - -import org.apache.maven.api.xml.XmlNode; -import org.apache.maven.api.xml.XmlService; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class XmlServiceLoadingTest { - - @Test - void testServiceLoaderFallbackWhenContextClassLoaderCannotSeeProvider() throws Exception { - ClassLoader previous = Thread.currentThread().getContextClassLoader(); - try (URLClassLoader contextClassLoader = new URLClassLoader(new URL[0], null)) { - Thread.currentThread().setContextClassLoader(contextClassLoader); - XmlNode node = XmlService.read(new StringReader("")); - assertEquals("configuration", node.name()); - } finally { - Thread.currentThread().setContextClassLoader(previous); - } - } -}