diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 7b525a5522ca0..6f28de3382cee 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -4817,11 +4817,6 @@ pom - - org.jboss.marshalling - jboss-marshalling - ${jboss-marshalling.version} - org.jboss.threads jboss-threads diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java index 83355e35ca946..6219d4c8ca510 100644 --- a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java @@ -7,6 +7,7 @@ import org.apache.maven.shared.invoker.MavenInvocationException; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; @@ -21,6 +22,7 @@ * mvn install -Dit.test=DevMojoIT#methodName */ @DisabledIfSystemProperty(named = "quarkus.test.native", matches = "true") +@Disabled("Needs https://github.com/junit-team/junit5/pull/3820 and #40601") public class TestParameterDevModeIT extends RunAndCheckMojoTestBase { protected int getPort() { diff --git a/test-framework/junit5/pom.xml b/test-framework/junit5/pom.xml index 449f8fda37df5..132c4db1b6531 100644 --- a/test-framework/junit5/pom.xml +++ b/test-framework/junit5/pom.xml @@ -49,8 +49,10 @@ quarkus-core - org.jboss.marshalling - jboss-marshalling + com.thoughtworks.xstream + xstream + + 1.4.20 diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 15fa6c360e67b..f2707e915346b 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -40,6 +40,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.regex.Pattern; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; @@ -51,6 +52,7 @@ import org.jboss.jandex.Type; import org.jboss.logging.Logger; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; @@ -104,7 +106,7 @@ import io.quarkus.test.junit.callback.QuarkusTestContext; import io.quarkus.test.junit.callback.QuarkusTestMethodContext; import io.quarkus.test.junit.internal.DeepClone; -import io.quarkus.test.junit.internal.NewSerializingDeepClone; +import io.quarkus.test.junit.internal.SerializationWithXStreamFallbackDeepClone; public class QuarkusTestExtension extends AbstractJvmQuarkusTestExtension implements BeforeEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterEachCallback, @@ -353,7 +355,7 @@ private void shutdownHangDetection() { } private void populateDeepCloneField(StartupAction startupAction) { - deepClone = new NewSerializingDeepClone(originalCl, startupAction.getClassLoader()); + deepClone = new SerializationWithXStreamFallbackDeepClone(startupAction.getClassLoader()); } private void populateTestMethodInvokers(ClassLoader quarkusClassLoader) { @@ -960,13 +962,49 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation Parameter[] parameters = invocationContext.getExecutable().getParameters(); for (int i = 0; i < originalArguments.size(); i++) { Object arg = originalArguments.get(i); + boolean cloneRequired = false; + Object replacement = null; Class argClass = parameters[i].getType(); + if (arg != null) { + Class theclass = argClass; + while (theclass.isArray()) { + theclass = theclass.getComponentType(); + } + if (theclass.isPrimitive()) { + cloneRequired = false; + } else if (TestInfo.class.isAssignableFrom(theclass)) { + TestInfo info = (TestInfo) arg; + Method newTestMethod = info.getTestMethod().isPresent() + ? determineTCCLExtensionMethod(info.getTestMethod().get(), testClassFromTCCL) + : null; + replacement = new TestInfoImpl(info.getDisplayName(), info.getTags(), + Optional.of(testClassFromTCCL), + Optional.ofNullable(newTestMethod)); + } else if (clonePattern.matcher(theclass.getName()).matches()) { + cloneRequired = true; + } else { + try { + cloneRequired = runningQuarkusApplication.getClassLoader() + .loadClass(theclass.getName()) != theclass; + } catch (ClassNotFoundException e) { + if (arg instanceof Supplier) { + cloneRequired = true; + } else { + throw e; + } + } + } + } - if (testMethodInvokerToUse != null) { + if (replacement != null) { + argumentsFromTccl.add(replacement); + } else if (cloneRequired) { + argumentsFromTccl.add(deepClone.clone(arg)); + } else if (testMethodInvokerToUse != null) { argumentsFromTccl.add(testMethodInvokerToUse.getClass().getMethod("methodParamInstance", String.class) .invoke(testMethodInvokerToUse, argClass.getName())); } else { - argumentsFromTccl.add(deepClone.clone(arg)); + argumentsFromTccl.add(arg); } } @@ -976,7 +1014,7 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation .invoke(testMethodInvokerToUse, effectiveTestInstance, newMethod, argumentsFromTccl, extensionContext.getRequiredTestClass().getName()); } else { - return newMethod.invoke(effectiveTestInstance, argumentsFromTccl.toArray(Object[]::new)); + return newMethod.invoke(effectiveTestInstance, argumentsFromTccl.toArray(new Object[0])); } } catch (InvocationTargetException e) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/TestInfoImpl.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestInfoImpl.java similarity index 95% rename from test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/TestInfoImpl.java rename to test-framework/junit5/src/main/java/io/quarkus/test/junit/TestInfoImpl.java index 7cc0be697b719..498cc5ff64447 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/TestInfoImpl.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestInfoImpl.java @@ -1,4 +1,4 @@ -package io.quarkus.test.junit.internal; +package io.quarkus.test.junit; import java.lang.reflect.Method; import java.util.Optional; diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomListConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomListConverter.java new file mode 100644 index 0000000000000..ddb8642d0056c --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomListConverter.java @@ -0,0 +1,63 @@ +package io.quarkus.test.junit.internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import com.thoughtworks.xstream.converters.collections.CollectionConverter; +import com.thoughtworks.xstream.mapper.Mapper; + +/** + * A custom List converter that always uses ArrayList for unmarshalling. + * This is probably not semantically correct 100% of the time, but it's likely fine + * for all the cases where we are using marshalling / unmarshalling. + * + * The reason for doing this is to avoid XStream causing illegal access issues + * for internal JDK lists + */ +public class CustomListConverter extends CollectionConverter { + + // if we wanted to be 100% sure, we'd list all the List.of methods, but I think it's pretty safe to say + // that the JDK won't add custom implementations for the other classes + + private final Predicate supported = new Predicate() { + + private final Set JDK_LIST_CLASS_NAMES = Set.of( + List.of().getClass().getName(), + List.of(Integer.MAX_VALUE).getClass().getName(), + Arrays.asList(Integer.MAX_VALUE).getClass().getName(), + Collections.unmodifiableList(List.of()).getClass().getName(), + Collections.emptyList().getClass().getName(), + List.of(Integer.MIN_VALUE, Integer.MAX_VALUE).subList(0, 1).getClass().getName()); + + @Override + public boolean test(String className) { + return JDK_LIST_CLASS_NAMES.contains(className); + } + }.or(new Predicate<>() { + + private static final String GUAVA_LISTS_PACKAGE = "com.google.common.collect.Lists"; + + @Override + public boolean test(String className) { + return className.startsWith(GUAVA_LISTS_PACKAGE); + } + }); + + public CustomListConverter(Mapper mapper) { + super(mapper); + } + + @Override + public boolean canConvert(Class type) { + return (type != null) && supported.test(type.getName()); + } + + @Override + protected Object createCollection(Class type) { + return new ArrayList<>(); + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapConverter.java new file mode 100644 index 0000000000000..fe93cb8594587 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapConverter.java @@ -0,0 +1,41 @@ +package io.quarkus.test.junit.internal; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import com.thoughtworks.xstream.converters.collections.MapConverter; +import com.thoughtworks.xstream.mapper.Mapper; + +/** + * A custom Map converter that always uses HashMap for unmarshalling. + * This is probably not semantically correct 100% of the time, but it's likely fine + * for all the cases where we are using marshalling / unmarshalling. + * + * The reason for doing this is to avoid XStream causing illegal access issues + * for internal JDK maps + */ +public class CustomMapConverter extends MapConverter { + + // if we wanted to be 100% sure, we'd list all the Set.of methods, but I think it's pretty safe to say + // that the JDK won't add custom implementations for the other classes + private final Set SUPPORTED_CLASS_NAMES = Set.of( + Map.of().getClass().getName(), + Map.of(Integer.MAX_VALUE, Integer.MAX_VALUE).getClass().getName(), + Collections.emptyMap().getClass().getName()); + + public CustomMapConverter(Mapper mapper) { + super(mapper); + } + + @Override + public boolean canConvert(Class type) { + return (type != null) && SUPPORTED_CLASS_NAMES.contains(type.getName()); + } + + @Override + protected Object createCollection(Class type) { + return new HashMap<>(); + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapEntryConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapEntryConverter.java new file mode 100644 index 0000000000000..f20a7fe3e3f36 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapEntryConverter.java @@ -0,0 +1,55 @@ +package io.quarkus.test.junit.internal; + +import java.util.AbstractMap; +import java.util.Map; +import java.util.Set; + +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.collections.MapConverter; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import com.thoughtworks.xstream.mapper.Mapper; + +/** + * A custom Map.Entry converter that always uses AbstractMap.SimpleEntry for unmarshalling. + * This is probably not semantically correct 100% of the time, but it's likely fine + * for all the cases where we are using marshalling / unmarshalling. + * + * The reason for doing this is to avoid XStream causing illegal access issues + * for internal JDK types + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class CustomMapEntryConverter extends MapConverter { + + private final Set SUPPORTED_CLASS_NAMES = Set + .of(Map.entry(Integer.MAX_VALUE, Integer.MAX_VALUE).getClass().getName()); + + public CustomMapEntryConverter(Mapper mapper) { + super(mapper); + } + + @Override + public boolean canConvert(Class type) { + return (type != null) && SUPPORTED_CLASS_NAMES.contains(type.getName()); + } + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + var entryName = mapper().serializedClass(Map.Entry.class); + var entry = (Map.Entry) source; + writer.startNode(entryName); + writeCompleteItem(entry.getKey(), context, writer); + writeCompleteItem(entry.getValue(), context, writer); + writer.endNode(); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + reader.moveDown(); + var key = readCompleteItem(reader, context, null); + var value = readCompleteItem(reader, context, null); + reader.moveUp(); + return new AbstractMap.SimpleEntry(key, value); + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomSetConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomSetConverter.java new file mode 100644 index 0000000000000..88d434cfaf34a --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomSetConverter.java @@ -0,0 +1,40 @@ +package io.quarkus.test.junit.internal; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.thoughtworks.xstream.converters.collections.CollectionConverter; +import com.thoughtworks.xstream.mapper.Mapper; + +/** + * A custom Set converter that always uses HashSet for unmarshalling. + * This is probably not semantically correct 100% of the time, but it's likely fine + * for all the cases where we are using marshalling / unmarshalling. + * + * The reason for doing this is to avoid XStream causing illegal access issues + * for internal JDK sets + */ +public class CustomSetConverter extends CollectionConverter { + + // if we wanted to be 100% sure, we'd list all the Set.of methods, but I think it's pretty safe to say + // that the JDK won't add custom implementations for the other classes + private final Set SUPPORTED_CLASS_NAMES = Set.of( + Set.of().getClass().getName(), + Set.of(Integer.MAX_VALUE).getClass().getName(), + Collections.emptySet().getClass().getName()); + + public CustomSetConverter(Mapper mapper) { + super(mapper); + } + + @Override + public boolean canConvert(Class type) { + return (type != null) && SUPPORTED_CLASS_NAMES.contains(type.getName()); + } + + @Override + protected Object createCollection(Class type) { + return new HashSet<>(); + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/NewSerializingDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/NewSerializingDeepClone.java deleted file mode 100644 index 682a196e00c71..0000000000000 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/NewSerializingDeepClone.java +++ /dev/null @@ -1,113 +0,0 @@ -package io.quarkus.test.junit.internal; - -import java.io.IOException; -import java.io.Serializable; -import java.io.UncheckedIOException; -import java.lang.reflect.Method; -import java.util.Set; -import java.util.function.Supplier; - -import org.jboss.marshalling.cloner.ClassCloner; -import org.jboss.marshalling.cloner.ClonerConfiguration; -import org.jboss.marshalling.cloner.ObjectCloner; -import org.jboss.marshalling.cloner.ObjectCloners; -import org.junit.jupiter.api.TestInfo; - -/** - * A deep-clone implementation using JBoss Marshalling's fast object cloner. - */ -public final class NewSerializingDeepClone implements DeepClone { - private final ObjectCloner cloner; - - public NewSerializingDeepClone(final ClassLoader sourceLoader, final ClassLoader targetLoader) { - ClonerConfiguration cc = new ClonerConfiguration(); - cc.setSerializabilityChecker(clazz -> clazz != Object.class); - cc.setClassCloner(new ClassCloner() { - public Class clone(final Class original) { - if (isUncloneable(original)) { - return original; - } - try { - return targetLoader.loadClass(original.getName()); - } catch (ClassNotFoundException ignored) { - return original; - } - } - - public Class cloneProxy(final Class proxyClass) { - // not really supported - return proxyClass; - } - }); - cc.setCloneTable( - (original, objectCloner, classCloner) -> { - if (EXTRA_IDENTITY_CLASSES.contains(original.getClass())) { - // avoid copying things that do not need to be copied - return original; - } else if (isUncloneable(original.getClass())) { - if (original instanceof Supplier s) { - // sneaky - return (Supplier) () -> clone(s.get()); - } else { - return original; - } - } else if (original instanceof TestInfo info) { - // copy the test info correctly - return new TestInfoImpl(info.getDisplayName(), info.getTags(), - info.getTestClass().map(this::cloneClass), - info.getTestMethod().map(this::cloneMethod)); - } else if (original == sourceLoader) { - return targetLoader; - } - // let the default cloner handle it - return null; - }); - cloner = ObjectCloners.getSerializingObjectClonerFactory().createCloner(cc); - } - - private static boolean isUncloneable(Class clazz) { - return clazz.isHidden() && !Serializable.class.isAssignableFrom(clazz); - } - - private Class cloneClass(Class clazz) { - try { - return (Class) cloner.clone(clazz); - } catch (IOException | ClassNotFoundException e) { - return null; - } - } - - private Method cloneMethod(Method method) { - try { - Class declaring = (Class) cloner.clone(method.getDeclaringClass()); - Class[] argTypes = (Class[]) cloner.clone(method.getParameterTypes()); - return declaring.getDeclaredMethod(method.getName(), argTypes); - } catch (Exception e) { - return null; - } - } - - public Object clone(final Object objectToClone) { - try { - return cloner.clone(objectToClone); - } catch (IOException e) { - throw new UncheckedIOException(e); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(e); - } - } - - /** - * Classes which do not need to be cloned. - */ - private static final Set> EXTRA_IDENTITY_CLASSES = Set.of( - Object.class, - byte[].class, - short[].class, - int[].class, - long[].class, - char[].class, - boolean[].class, - float[].class, - double[].class); -} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationDeepClone.java new file mode 100644 index 0000000000000..3da2c0c16e372 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationDeepClone.java @@ -0,0 +1,46 @@ +package io.quarkus.test.junit.internal; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; + +/** + * Cloning strategy that just serializes and deserializes using plain old java serialization. + */ +class SerializationDeepClone implements DeepClone { + + private final ClassLoader classLoader; + + SerializationDeepClone(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public Object clone(Object objectToClone) { + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(512); + try (ObjectOutputStream objOut = new ObjectOutputStream(byteOut)) { + objOut.writeObject(objectToClone); + try (ObjectInputStream objIn = new ClassLoaderAwareObjectInputStream(byteOut)) { + return objIn.readObject(); + } + } catch (IOException | ClassNotFoundException e) { + throw new IllegalStateException("Unable to deep clone object of type '" + objectToClone.getClass().getName() + + "'. Please report the issue on the Quarkus issue tracker.", e); + } + } + + private class ClassLoaderAwareObjectInputStream extends ObjectInputStream { + + public ClassLoaderAwareObjectInputStream(ByteArrayOutputStream byteOut) throws IOException { + super(new ByteArrayInputStream(byteOut.toByteArray())); + } + + @Override + protected Class resolveClass(ObjectStreamClass desc) throws ClassNotFoundException { + return Class.forName(desc.getName(), true, classLoader); + } + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationWithXStreamFallbackDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationWithXStreamFallbackDeepClone.java new file mode 100644 index 0000000000000..36da89a82e804 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationWithXStreamFallbackDeepClone.java @@ -0,0 +1,35 @@ +package io.quarkus.test.junit.internal; + +import java.io.Serializable; +import java.util.Optional; + +import org.jboss.logging.Logger; + +/** + * Cloning strategy delegating to {@link SerializationDeepClone}, falling back to {@link XStreamDeepClone} in case of error. + */ +public class SerializationWithXStreamFallbackDeepClone implements DeepClone { + + private static final Logger LOG = Logger.getLogger(SerializationWithXStreamFallbackDeepClone.class); + + private final SerializationDeepClone serializationDeepClone; + private final XStreamDeepClone xStreamDeepClone; + + public SerializationWithXStreamFallbackDeepClone(ClassLoader classLoader) { + this.serializationDeepClone = new SerializationDeepClone(classLoader); + this.xStreamDeepClone = new XStreamDeepClone(classLoader); + } + + @Override + public Object clone(Object objectToClone) { + if (objectToClone instanceof Serializable) { + try { + return serializationDeepClone.clone(objectToClone); + } catch (RuntimeException re) { + LOG.debugf("SerializationDeepClone failed (will fall back to XStream): %s", + Optional.ofNullable(re.getCause()).orElse(re)); + } + } + return xStreamDeepClone.clone(objectToClone); + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java new file mode 100644 index 0000000000000..9951f96734d44 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java @@ -0,0 +1,61 @@ +package io.quarkus.test.junit.internal; + +import java.util.function.Supplier; + +import com.thoughtworks.xstream.XStream; + +/** + * Super simple cloning strategy that just serializes to XML and deserializes it using xstream + */ +class XStreamDeepClone implements DeepClone { + + private final Supplier xStreamSupplier; + + XStreamDeepClone(ClassLoader classLoader) { + // avoid doing any work eagerly since the cloner is rarely used + xStreamSupplier = () -> { + XStream result = new XStream(); + result.allowTypesByRegExp(new String[] { ".*" }); + result.setClassLoader(classLoader); + result.registerConverter(new CustomListConverter(result.getMapper())); + result.registerConverter(new CustomSetConverter(result.getMapper())); + result.registerConverter(new CustomMapConverter(result.getMapper())); + result.registerConverter(new CustomMapEntryConverter(result.getMapper())); + + return result; + }; + } + + @Override + public Object clone(Object objectToClone) { + if (objectToClone == null) { + return null; + } + + if (objectToClone instanceof Supplier) { + return handleSupplier((Supplier) objectToClone); + } + + return doClone(objectToClone); + } + + private Supplier handleSupplier(final Supplier supplier) { + return new Supplier() { + @Override + public Object get() { + return doClone(supplier.get()); + } + }; + } + + private Object doClone(Object objectToClone) { + XStream xStream = xStreamSupplier.get(); + final String serialized = xStream.toXML(objectToClone); + final Object result = xStream.fromXML(serialized); + if (result == null) { + throw new IllegalStateException("Unable to deep clone object of type '" + objectToClone.getClass().getName() + + "'. Please report the issue on the Quarkus issue tracker."); + } + return result; + } +}