diff --git a/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/WrapperClassGenerator.java b/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/WrapperClassGenerator.java
index c47338e4e56..ec36e6af0dc 100644
--- a/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/WrapperClassGenerator.java
+++ b/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/WrapperClassGenerator.java
@@ -47,10 +47,11 @@
import org.apache.cxf.common.spi.ClassGeneratorClassLoader;
import org.apache.cxf.common.util.ASMHelper;
import org.apache.cxf.common.util.OpcodesProxy;
-import org.apache.cxf.common.util.PackageUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.helpers.JavaUtils;
import org.apache.cxf.jaxws.spi.WrapperClassCreator;
+import org.apache.cxf.jaxws.spi.WrapperClassNamingConvention;
+import org.apache.cxf.jaxws.spi.WrapperClassNamingConvention.DefaultWrapperClassNamingConvention;
import org.apache.cxf.jaxws.support.JaxWsServiceFactoryBean;
import org.apache.cxf.service.model.InterfaceInfo;
import org.apache.cxf.service.model.MessageInfo;
@@ -60,19 +61,22 @@
import org.apache.cxf.wsdl.service.factory.ReflectionServiceFactoryBean;
public final class WrapperClassGenerator extends ClassGeneratorClassLoader implements WrapperClassCreator {
- public static final String DEFAULT_PACKAGE_NAME = "defaultnamespace";
+ /**
+ * Kept for backwards compatibility only
+ *
+ * @deprecated use {@link WrapperClassNamingConvention#DEFAULT_PACKAGE_NAME} instead
+ */
+ @Deprecated
+ public static final String DEFAULT_PACKAGE_NAME = DefaultWrapperClassNamingConvention.DEFAULT_PACKAGE_NAME;
private static final Logger LOG = LogUtils.getL7dLogger(WrapperClassGenerator.class);
private final ASMHelper helper;
+ private final WrapperClassNamingConvention wrapperClassNaming;
public WrapperClassGenerator(Bus bus) {
super(bus);
- helper = bus.getExtension(ASMHelper.class);
- }
-
- private String getPackageName(Method method) {
- String pkg = PackageUtils.getPackageName(method.getDeclaringClass());
- return pkg.length() == 0 ? DEFAULT_PACKAGE_NAME : pkg;
+ this.helper = bus.getExtension(ASMHelper.class);
+ this.wrapperClassNaming = bus.getExtension(WrapperClassNamingConvention.class);
}
private Annotation[] getMethodParameterAnnotations(final MessagePartInfo mpi) {
@@ -169,7 +173,7 @@ private void createWrapperClass(MessagePartInfo wrapperPart,
QName wrapperElement = messageInfo.getName();
boolean anonymous = factory.getAnonymousWrapperTypes();
- String pkg = getPackageName(method) + ".jaxws_asm" + (anonymous ? "_an" : "");
+ String pkg = wrapperClassNaming.getWrapperClassPackageName(method.getDeclaringClass(), anonymous);
String className = pkg + "."
+ StringUtils.capitalize(op.getName().getLocalPart());
if (!isRequest) {
diff --git a/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/spi/WrapperClassLoader.java b/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/spi/WrapperClassLoader.java
index dfaf1959963..9a993272ac8 100644
--- a/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/spi/WrapperClassLoader.java
+++ b/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/spi/WrapperClassLoader.java
@@ -24,9 +24,7 @@
import org.apache.cxf.Bus;
import org.apache.cxf.common.spi.GeneratedClassClassLoader;
-import org.apache.cxf.common.util.PackageUtils;
import org.apache.cxf.common.util.StringUtils;
-import org.apache.cxf.jaxws.WrapperClassGenerator;
import org.apache.cxf.jaxws.support.JaxWsServiceFactoryBean;
import org.apache.cxf.service.model.InterfaceInfo;
import org.apache.cxf.service.model.MessageInfo;
@@ -41,8 +39,11 @@
* @author olivier dufour
*/
public class WrapperClassLoader extends GeneratedClassClassLoader implements WrapperClassCreator {
+ private final WrapperClassNamingConvention wrapperClassNaming;
+
public WrapperClassLoader(Bus bus) {
super(bus);
+ wrapperClassNaming = bus.getExtension(WrapperClassNamingConvention.class);
}
@Override
@@ -87,9 +88,10 @@ private Class> createWrapperClass(MessagePartInfo wrapperPart,
Method method,
boolean isRequest,
JaxWsServiceFactoryBean factory) {
- boolean anonymous = factory.getAnonymousWrapperTypes();
- String pkg = getPackageName(method) + ".jaxws_asm" + (anonymous ? "_an" : "");
+ String pkg = wrapperClassNaming.getWrapperClassPackageName(
+ method.getDeclaringClass(),
+ factory.getAnonymousWrapperTypes());
String className = pkg + "."
+ StringUtils.capitalize(op.getName().getLocalPart());
if (!isRequest) {
@@ -112,8 +114,4 @@ private Class> createWrapperClass(MessagePartInfo wrapperPart,
//throw new ClassNotFoundException(origClassName);
return null;
}
- private String getPackageName(Method method) {
- String pkg = PackageUtils.getPackageName(method.getDeclaringClass());
- return pkg.length() == 0 ? WrapperClassGenerator.DEFAULT_PACKAGE_NAME : pkg;
- }
}
diff --git a/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/spi/WrapperClassNamingConvention.java b/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/spi/WrapperClassNamingConvention.java
new file mode 100644
index 00000000000..ae1b544c549
--- /dev/null
+++ b/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/spi/WrapperClassNamingConvention.java
@@ -0,0 +1,148 @@
+/**
+ * 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.jaxws.spi;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+import org.apache.cxf.common.util.PackageUtils;
+/**
+ * Provides names for storing generated wrapper classes.
+ *
+ * @since 4.1.1
+ */
+public interface WrapperClassNamingConvention {
+
+ /**
+ * Returns a package name unique for the given {@code sei} and {@code anonymous}
+ * parameters suitable for storing generated wrapper classes.
+ *
+ * @param sei the service endpoint interface for which the package name should be created
+ * @param anonymous whether the generated wrapper types are anonymous
+ * @return a valid Java package name
+ */
+ String getWrapperClassPackageName(Class> sei, boolean anonymous);
+
+ /**
+ * Default naming scheme since CXF 4.2.0.
+ *
+ * The package name returned by {@link #getWrapperClassPackageName(Class, boolean)} are unique
+ * per given {@code sei}.
+ *
+ * Examples:
+ *
+ *
+ * SEI | anonymous |
+ * {@code getWrapperClassPackageName()} return value |
+ *
+ *
+ * {@code org.example.Service} |
+ * {@code false} |
+ * {@code org.example.jaxws_asm.service} |
+ *
+ *
+ * {@code org.example.OuterClass$Service} |
+ * {@code false} |
+ * {@code org.example.jaxws_asm.outerclass_service} |
+ *
+ *
+ * {@code org.example.Service} |
+ * {@code true} |
+ * {@code org.example.jaxws_asm_an.service} |
+ *
+ *
+ *
+ * @since 4.1.1
+ */
+ class DefaultWrapperClassNamingConvention implements WrapperClassNamingConvention {
+ public static final String DEFAULT_PACKAGE_NAME = "defaultnamespace";
+ private static final Pattern JAVA_PACKAGE_NAME_SANITIZER_PATTERN = Pattern.compile("[^a-zA-Z0-9_]");
+
+ @Override
+ public String getWrapperClassPackageName(Class> sei, boolean anonymous) {
+ final String className = sei.getName();
+ final int start = className.startsWith("[L") ? 2 : 0;
+ final int end = className.lastIndexOf('.');
+ final String pkg;
+ final String cl;
+ if (end >= 0) {
+ pkg = className.substring(start, end);
+ cl = className.substring(end + 1);
+ } else {
+ pkg = DefaultWrapperClassNamingConvention.DEFAULT_PACKAGE_NAME;
+ cl = className;
+ }
+
+ return pkg
+ + (anonymous ? ".jaxws_asm_an." : ".jaxws_asm.")
+ + JAVA_PACKAGE_NAME_SANITIZER_PATTERN.matcher(cl).replaceAll("_").toLowerCase(Locale.ROOT);
+ }
+
+ }
+
+ /**
+ * An implementation restoring the behavior of CXF before version 4.2.0.
+ *
+ * Unlike with {@link DefaultWrapperClassNamingConvention}, this implementation's
+ * {@link #getWrapperClassPackageName(Class, boolean)} takes only package name
+ * of the given {@code sei} into account.
+ * Therefore naming clashes may occur if two SEIs are in the same package
+ * and both of them have a method with the same name but possibly different signature.
+ *
+ * Examples:
+ *
+ *
+ * SEI |
+ * anonymous |
+ * {@code getWrapperClassPackageName()} return value |
+ *
+ *
+ * {@code org.example.Service} |
+ * {@code false} |
+ * {@code org.example.jaxws_asm} |
+ *
+ *
+ * {@code org.example.OuterClass$Service} |
+ * {@code false} |
+ * {@code org.example.jaxws_asm} |
+ *
+ *
+ * {@code org.example.Service} |
+ * {@code true} |
+ * {@code org.example.jaxws_asm_an} |
+ *
+ *
+ *
+ * @since 4.1.1
+ */
+ class LegacyWrapperClassNamingConvention implements WrapperClassNamingConvention {
+
+ @Override
+ public String getWrapperClassPackageName(Class> sei, boolean anonymous) {
+ return getPackageName(sei) + ".jaxws_asm" + (anonymous ? "_an" : "");
+ }
+
+ private String getPackageName(Class> sei) {
+ String pkg = PackageUtils.getPackageName(sei);
+ return pkg.length() == 0 ? DefaultWrapperClassNamingConvention.DEFAULT_PACKAGE_NAME : pkg;
+ }
+
+ }
+
+}
diff --git a/rt/frontend/jaxws/src/main/resources/META-INF/cxf/bus-extensions.txt b/rt/frontend/jaxws/src/main/resources/META-INF/cxf/bus-extensions.txt
index ea4dc9d2bc1..f91f28cf0f7 100644
--- a/rt/frontend/jaxws/src/main/resources/META-INF/cxf/bus-extensions.txt
+++ b/rt/frontend/jaxws/src/main/resources/META-INF/cxf/bus-extensions.txt
@@ -1,2 +1,3 @@
org.apache.cxf.jaxws.context.WebServiceContextResourceResolver::true
-org.apache.cxf.jaxws.spi.WrapperClassCreatorProxyService:org.apache.cxf.jaxws.spi.WrapperClassCreator:true
\ No newline at end of file
+org.apache.cxf.jaxws.spi.WrapperClassCreatorProxyService:org.apache.cxf.jaxws.spi.WrapperClassCreator:true
+org.apache.cxf.jaxws.spi.WrapperClassNamingConvention$DefaultWrapperClassNamingConvention:org.apache.cxf.jaxws.spi.WrapperClassNamingConvention:true
diff --git a/rt/frontend/jaxws/src/test/java/org/apache/cxf/jaxws/spi/WrapperClassLoaderTest.java b/rt/frontend/jaxws/src/test/java/org/apache/cxf/jaxws/spi/WrapperClassLoaderTest.java
index 16ba943bf2f..e17ffb65a85 100644
--- a/rt/frontend/jaxws/src/test/java/org/apache/cxf/jaxws/spi/WrapperClassLoaderTest.java
+++ b/rt/frontend/jaxws/src/test/java/org/apache/cxf/jaxws/spi/WrapperClassLoaderTest.java
@@ -21,8 +21,12 @@
import java.lang.reflect.Method;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
import javax.xml.namespace.QName;
@@ -31,6 +35,8 @@
import org.apache.cxf.binding.soap.SoapBindingFactory;
import org.apache.cxf.binding.soap.SoapTransportFactory;
import org.apache.cxf.common.spi.GeneratedClassClassLoader;
+import org.apache.cxf.common.spi.GeneratedClassClassLoaderCapture;
+import org.apache.cxf.jaxws.WrapperClassGenerator;
import org.apache.cxf.jaxws.service.SayHi;
import org.apache.cxf.jaxws.support.JaxWsServiceFactoryBean;
import org.apache.cxf.service.model.DescriptionInfo;
@@ -59,10 +65,8 @@ public class WrapperClassLoaderTest extends AbstractCXFTest {
public void setUpBus() throws Exception {
super.setUpBus();
- GeneratedClassClassLoader.TypeHelperClassLoader mockClassLoader =
- Mockito.mock(GeneratedClassClassLoader.TypeHelperClassLoader.class);
- doReturn(SayHi.class).when(mockClassLoader).loadClass("org.apache.cxf.jaxws.service.jaxws_asm.SayHi");
- bus.setExtension(mockClassLoader, GeneratedClassClassLoader.TypeHelperClassLoader.class);
+ Capture c = new Capture();
+ bus.setExtension(c, GeneratedClassClassLoaderCapture.class);
SoapBindingFactory bindingFactory = new SoapBindingFactory();
bindingFactory.setBus(bus);
@@ -115,8 +119,31 @@ public void testWrapperClassLoaderWhenNoWrappedOperations() throws Exception {
}
@org.junit.Test
- public void testWrapperClassLoaderWithWrappedOperations() throws Exception {
- WrapperClassLoader wrapperClassLoader = new WrapperClassLoader(bus);
+ public void testWrapperClassLoaderWithWrappedOperationsAndDefaultConvention() throws Exception {
+ final List loadedClassNames = testWrapperClassLoaderWithNamingConvention(
+ new WrapperClassNamingConvention.DefaultWrapperClassNamingConvention());
+ assertEquals(
+ List.of(
+ "org.apache.cxf.jaxws.service.jaxws_asm.sayhi.SayHi",
+ "org.apache.cxf.jaxws.service.jaxws_asm.sayhi.SayHiResponse"),
+ loadedClassNames);
+ }
+
+ @org.junit.Test
+ public void testWrapperClassLoaderWithWrappedOperationsAndLegacyConvention() throws Exception {
+ final List loadedClassNames = testWrapperClassLoaderWithNamingConvention(
+ new WrapperClassNamingConvention.LegacyWrapperClassNamingConvention());
+ assertEquals(
+ List.of(
+ "org.apache.cxf.jaxws.service.jaxws_asm.SayHi",
+ "org.apache.cxf.jaxws.service.jaxws_asm.SayHiResponse"),
+ loadedClassNames);
+ }
+
+ public List testWrapperClassLoaderWithNamingConvention(WrapperClassNamingConvention convention)
+ throws Exception {
+ bus.setExtension(convention, WrapperClassNamingConvention.class);
+ WrapperClassGenerator wrapperClassGenerator = new WrapperClassGenerator(bus);
JaxWsServiceFactoryBean factory = new JaxWsServiceFactoryBean();
QName serviceName = new QName(
@@ -153,10 +180,53 @@ public void testWrapperClassLoaderWithWrappedOperations() throws Exception {
outputInfo.addMessagePart(sayHi);
operationInfo.setOutput("sayHi", outputInfo);
- Set> result = wrapperClassLoader.generate(factory, interfaceInfo, false);
-
//Both Input and Output Messages will be found on Classpath
//org.apache.cxf.jaxws.service.jaxws_asm.SayHi
- assertEquals(2, result.size());
+ Set> generatedClasses = wrapperClassGenerator.generate(factory, interfaceInfo, false);
+ assertEquals(2, generatedClasses.size());
+
+
+ operationInfo.getInput().getFirstMessagePart().setTypeClass(null);
+ operationInfo.getOutput().getFirstMessagePart().setTypeClass(null);
+
+ WrapperClassLoader wrapperClassLoader = new WrapperClassLoader(bus);
+ GeneratedClassClassLoader.TypeHelperClassLoader cl = wrapperClassLoader.getClassLoader();
+ Capture c = (Capture)bus.getExtension(GeneratedClassClassLoaderCapture.class);
+ c.restore(cl);
+
+ Set> loadedClasses = wrapperClassLoader.generate(factory, interfaceInfo, false);
+ assertEquals(2, loadedClasses.size());
+
+ // The class names must match, but the classes themselves are different because they were loaded
+ // using different class loaders
+ final List generatedClassNames = generatedClasses.stream()
+ .map(Class::getName)
+ .collect(Collectors.toList());
+ final List loadedClassNames = loadedClasses.stream()
+ .map(Class::getName)
+ .collect(Collectors.toList());
+ assertEquals(
+ generatedClassNames,
+ loadedClassNames);
+ return loadedClassNames;
}
+
+ static class Capture implements GeneratedClassClassLoaderCapture {
+
+ private final Map sources = new HashMap<>();
+
+ public void capture(String className, byte[] bytes) {
+ if (sources.containsKey(className)) {
+ throw new IllegalStateException("Class " + className + " defined twice");
+ }
+ sources.put(className, bytes);
+ }
+
+ public void restore(org.apache.cxf.common.spi.GeneratedClassClassLoader.TypeHelperClassLoader cl) {
+ for (Map.Entry cls : sources.entrySet()) {
+ cl.defineClass(cls.getKey(), cls.getValue());
+ }
+ }
+ }
+
}
diff --git a/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/CXF9003Test.java b/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/CXF9003Test.java
new file mode 100644
index 00000000000..907c43ea2c7
--- /dev/null
+++ b/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/CXF9003Test.java
@@ -0,0 +1,155 @@
+/**
+ * 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.systest.jaxws;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.namespace.QName;
+
+import jakarta.jws.WebMethod;
+import jakarta.jws.WebService;
+import jakarta.xml.ws.Endpoint;
+import jakarta.xml.ws.Service;
+import org.apache.cxf.Bus;
+import org.apache.cxf.BusFactory;
+import org.apache.cxf.common.spi.GeneratedClassClassLoaderCapture;
+import org.apache.cxf.testutil.common.AbstractBusTestServerBase;
+import org.apache.cxf.testutil.common.AbstractClientServerTestBase;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class CXF9003Test extends AbstractClientServerTestBase {
+ static final String PORT = allocatePort(Server9003.class);
+
+ @WebService(targetNamespace = "urn:hello")
+ interface Hello1Service {
+ @WebMethod
+ String hello(String name);
+ }
+
+ @WebService(targetNamespace = "urn:hello")
+ public static class Hello1ServiceImpl implements Hello1Service {
+ @Override
+ public String hello(String person) {
+ return "Hello " + person;
+ }
+ }
+
+ @WebService(targetNamespace = "urn:hello")
+ interface Hello2Service {
+ @WebMethod
+ String hello(Name name);
+ }
+
+ @WebService(targetNamespace = "urn:hello")
+ public static class Hello2ServiceImpl implements Hello2Service {
+ @Override
+ public String hello(Name person) {
+ return "Hello " + person.getName();
+ }
+ }
+ public static class Name {
+ private String name;
+
+ public Name() {
+
+ }
+ public Name(String name) {
+ super();
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
+ static class Capture implements GeneratedClassClassLoaderCapture {
+
+ private final Map sources = new HashMap<>();
+
+ public void capture(String className, byte[] bytes) {
+ if (sources.containsKey(className)) {
+ throw new IllegalStateException("Class " + className + " defined twice");
+ }
+ sources.put(className, bytes);
+ }
+ }
+
+
+ public static class Server9003 extends AbstractBusTestServerBase {
+ protected void run() {
+ Object implementor = new Hello1ServiceImpl();
+ String address = "http://localhost:" + PORT + "/hello1/service";
+ Endpoint.publish(address, implementor);
+
+ Object proxyImpl = new Hello2ServiceImpl();
+ String address2 = "http://localhost:" + PORT + "/hello2/service";
+ Endpoint.publish(address2, proxyImpl);
+ }
+
+ public static void main(String[] args) {
+ try {
+ Server9003 s = new Server9003();
+ s.start();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ System.exit(-1);
+ } finally {
+ System.out.println("done!");
+ }
+ }
+ }
+
+ @BeforeClass
+ public static void startServers() throws Exception {
+ Bus bus = BusFactory.getDefaultBus();
+ Capture c = new Capture();
+ bus.setExtension(c, GeneratedClassClassLoaderCapture.class);
+ assertTrue("server did not launch correctly", launchServer(Server9003.class, true));
+ }
+
+ @Test
+ public void generatedNamingClash() throws Exception {
+
+ QName serviceName1 = new QName("urn:hello", "Hello1ServiceImplService");
+ URL wsdlURL1 = new URL("http://localhost:" + PORT + "/hello1/service?wsdl");
+ Service service1 = Service.create(wsdlURL1, serviceName1);
+ Hello1Service helloService1 = service1.getPort(Hello1Service.class);
+
+ QName serviceName2 = new QName("urn:hello", "Hello2ServiceImplService");
+ URL wsdlURL2 = new URL("http://localhost:" + PORT + "/hello2/service?wsdl");
+ Service service2 = Service.create(wsdlURL2, serviceName2);
+ Hello2Service helloService2 = service2.getPort(Hello2Service.class);
+
+ assertEquals(helloService1.hello("Dolly"), "Hello Dolly");
+ assertEquals(helloService2.hello(new Name("Joe")), "Hello Joe");
+ }
+
+}