This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/extensions in repository https://gitbox.apache.org/repos/asf/cxf-xjc-utils.git
commit f596a9dff08e6fa8505ac56593cc315da0fd2f17 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Sun Oct 25 11:09:49 2026 +0000 Keep project dependencies off the classpath XJC loads code from --- README.md | 2 + .../cxf/maven_plugin/AbstractXSDToJavaMojo.java | 18 ++- .../apache/cxf/maven_plugin/XSDToJavaRunner.java | 47 ++++++- .../cxf/maven_plugin/ExtensionClasspathTest.java | 143 +++++++++++++++++++++ 4 files changed, 202 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d8bf2a5..c9bd82b 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,8 @@ Then activate the plugin in your `wsdl2java` configuration: </plugin> ``` +XJC extensions are loaded only from the artifacts listed under `<extensions>` and from the `<dependencies>` of the `cxf-xjc-plugin` itself, not from the dependencies of your project. If you previously relied on an XJC extension being found as a project dependency, add it to `<extensions>` instead. + ## Requirements - Java 17+ diff --git a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java index 9727d98..44f5453 100644 --- a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java +++ b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java @@ -81,6 +81,8 @@ public abstract class AbstractXSDToJavaMojo extends AbstractMojo { /** * The extension artifacts that will be retrieved and added to the classpath. + * XJC plugins are only loaded from these and from the dependencies of this Maven plugin, + * not from the dependencies of the project. */ @Parameter private List<String> extensions; @@ -518,19 +520,29 @@ public abstract class AbstractXSDToJavaMojo extends AbstractMojo { JarArchiver jar = new JarArchiver(); jar.setDestFile(file.getAbsoluteFile()); + // Only the plugin's own dependencies go on the class path of the forked process. The + // project classpath is passed separately, for resolving classpath: catalog entries, + // as XJC loads plugins (and JAXP implementations) from the class path, and so would + // run code from any dependency of the project that registered one. Manifest manifest = new Manifest(); Attribute attr = new Attribute(); attr.setName("Class-Path"); StringBuilder b = new StringBuilder(8000); + for (Artifact a : pluginArtifacts) { + b.append(a.getFile().toURI().toURL().toExternalForm()).append(' '); + } + attr.setValue(b.toString()); + manifest.getMainSection().addConfiguredAttribute(attr); + + attr = new Attribute(); + attr.setName(XSDToJavaRunner.PROJECT_CLASSPATH_ATTRIBUTE); + b = new StringBuilder(8000); for (String cp : getClasspathElements()) { URI uri = mapLocation(cp); if (uri != null) { b.append(uri.toString()).append(' '); } } - for (Artifact a : pluginArtifacts) { - b.append(a.getFile().toURI().toURL().toExternalForm()).append(' '); - } attr.setValue(b.toString()); manifest.getMainSection().addConfiguredAttribute(attr); diff --git a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XSDToJavaRunner.java b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XSDToJavaRunner.java index 2ee7ac9..2bb145a 100644 --- a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XSDToJavaRunner.java +++ b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XSDToJavaRunner.java @@ -28,6 +28,8 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; +import java.util.jar.JarFile; +import java.util.jar.Manifest; import javax.xml.XMLConstants; import javax.xml.parsers.ParserConfigurationException; @@ -66,6 +68,9 @@ import org.sonatype.plexus.build.incremental.BuildContext; * */ public class XSDToJavaRunner { + // The manifest attribute of the forked process's jar holding the project classpath + static final String PROJECT_CLASSPATH_ATTRIBUTE = "CXF-XJC-Project-Classpath"; + static Class<?> modelLoaderClass; // The JAXP system property (JAXP 1.5) that restricts which protocols may be used to resolve @@ -149,16 +154,21 @@ public class XSDToJavaRunner { urls.add(file.toURI().toURL()); } } + // The "-classpath" arguments are the configured extension artifacts. XJC plugins are only + // loaded from these (and the cxf-xjc-plugin's own dependencies), not from the project's + // classpath above: XJC instantiates every plugin registered in META-INF/services on the + // classpath it is given, so any dependency of the project could otherwise run code here. + List<URL> extensionUrls = new ArrayList<URL>(); for (int x = 0; x < args.length - 1; x++) { if ("-classpath".equals(args[x])) { File file = getFile(args[x + 1], listener); if (file != null && file.exists()) { - cpList.add(file.getAbsolutePath()); - urls.add(file.getAbsoluteFile().toURI().toURL()); + extensionUrls.add(file.getAbsoluteFile().toURI().toURL()); } x++; } } + urls.addAll(extensionUrls); try (URLClassLoader loader = new URLClassLoader(urls.toArray(new URL[0]), this.getClass().getClassLoader())) { @@ -203,7 +213,9 @@ public class XSDToJavaRunner { } }; - for (URL url : urls) { + // the project classpath stays available to the catalog resolver (for classpath: entries) + // through "loader", but only the extensions are on the classpath XJC loads plugins from + for (URL url : extensionUrls) { opt.classpaths.add(url); } if (checkXmlElementRef()) { @@ -411,12 +423,37 @@ public class XSDToJavaRunner { return false; } + /** + * Reads the project classpath that AbstractXSDToJavaMojo records in the manifest of the jar + * used to run a forked process. It is kept out of the manifest Class-Path, so that the + * project's dependencies are not on the class path XJC loads plugins from. + */ + static List<String> getProjectClasspath(File jar) throws IOException { + List<String> cplist = new ArrayList<String>(); + if (!jar.isFile()) { + return cplist; + } + try (JarFile jarFile = new JarFile(jar)) { + Manifest manifest = jarFile.getManifest(); + String cp = manifest == null ? null : manifest.getMainAttributes().getValue(PROJECT_CLASSPATH_ATTRIBUTE); + if (cp != null) { + for (String s : cp.split(" ")) { + if (!s.isEmpty()) { + cplist.add(s); + } + } + } + } + return cplist; + } + public static void main(String[] args) throws Exception { BuildContext context = new XJCBuildContext(); XJCErrorListener listener = new XJCErrorListener(context); - List<String> cplist = new ArrayList<String>(); - + // with "-jar" the class path is just the jar AbstractXSDToJavaMojo created for this run + List<String> cplist = getProjectClasspath(new File(System.getProperty("java.class.path"))); + File outputFile = getFile(args[args.length - 1], listener); if (outputFile == null) { outputFile = new File(args[args.length - 1]); diff --git a/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/ExtensionClasspathTest.java b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/ExtensionClasspathTest.java new file mode 100644 index 0000000..bdcadf0 --- /dev/null +++ b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/ExtensionClasspathTest.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.cxf.maven_plugin; + +import java.io.File; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import org.xml.sax.ErrorHandler; + +import com.sun.tools.xjc.Options; +import com.sun.tools.xjc.Plugin; +import com.sun.tools.xjc.outline.Outline; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * XJC instantiates every plugin registered in META-INF/services on the classpath it loads plugins + * from, so that must only hold the configured extensions, not the project's dependencies. + */ +public class ExtensionClasspathTest { + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + + /** + * A plugin that records whether XJC instantiated it. + */ + public static class ProbePlugin extends Plugin { + static volatile boolean instantiated; + + public ProbePlugin() { + instantiated = true; + } + + @Override + public String getOptionName() { + return "Xcxf-probe"; + } + + @Override + public String getUsage() { + return ""; + } + + @Override + public boolean run(Outline outline, Options opt, ErrorHandler errorHandler) { + return true; + } + } + + @Before + public void reset() { + ProbePlugin.instantiated = false; + } + + // a jar registering ProbePlugin, as a dependency with a hostile META-INF/services entry would + private File probeJar() throws Exception { + File jar = tmp.newFile("probe-" + System.nanoTime() + ".jar"); + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar.toPath()))) { + out.putNextEntry(new JarEntry("META-INF/services/" + Plugin.class.getName())); + out.write(ProbePlugin.class.getName().getBytes(StandardCharsets.UTF_8)); + out.closeEntry(); + } + return jar; + } + + private int compile(List<String> extraArgs, List<String> projectClasspath) throws Exception { + File xsd = tmp.newFile("schema-" + System.nanoTime() + ".xsd"); + Files.write(xsd.toPath(), ("<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'" + + " targetNamespace='urn:t'><xs:element name='root' type='xs:string'/></xs:schema>") + .getBytes(StandardCharsets.UTF_8)); + File out = tmp.newFolder("gen-" + System.nanoTime()); + List<String> args = new ArrayList<>(extraArgs); + // -Xlocator is a plugin built into XJC: enabling it makes XJC look up all plugins + args.addAll(Arrays.asList("-Xlocator", "-d", out.getAbsolutePath(), xsd.toURI().toString())); + XJCErrorListener listener = new XJCErrorListener(new XJCBuildContext()); + return new XSDToJavaRunner(args.toArray(new String[0]), listener, out, + new ArrayList<>(projectClasspath)).run(); + } + + @Test + public void pluginOnProjectClasspathIsNotLoaded() throws Exception { + File jar = probeJar(); + int rc = compile(Collections.<String>emptyList(), Collections.singletonList(jar.getAbsolutePath())); + Assert.assertEquals(0, rc); + Assert.assertFalse("a plugin registered by a project dependency must not be instantiated", + ProbePlugin.instantiated); + } + + @Test + public void pluginInExtensionIsLoaded() throws Exception { + File jar = probeJar(); + int rc = compile(Arrays.asList("-classpath", jar.getAbsolutePath(), "-Xcxf-probe"), + Collections.<String>emptyList()); + Assert.assertEquals(0, rc); + Assert.assertTrue("a plugin from a configured extension must be loaded", ProbePlugin.instantiated); + } + + @Test + public void projectClasspathIsReadFromManifest() throws Exception { + File jar = tmp.newFile("forked.jar"); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(new Attributes.Name(XSDToJavaRunner.PROJECT_CLASSPATH_ATTRIBUTE), + "file:/a/b.jar file:/c/d%20e/ "); + try (OutputStream out = new JarOutputStream(Files.newOutputStream(jar.toPath()), manifest)) { + // just the manifest + } + Assert.assertEquals(Arrays.asList("file:/a/b.jar", "file:/c/d%20e/"), + XSDToJavaRunner.getProjectClasspath(jar)); + Assert.assertEquals(Collections.emptyList(), + XSDToJavaRunner.getProjectClasspath(new File(tmp.getRoot(), "missing.jar"))); + } +}
