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: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
SEIanonymous{@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: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
SEIanonymous{@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"); + } + +}