You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2023/02/28 10:08:08 UTC

[causeway] branch master updated: CAUSEWAY-3304: JaxbUtils: don't use JAXBContext#newInstance

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/master by this push:
     new 15d7311f15 CAUSEWAY-3304: JaxbUtils: don't use JAXBContext#newInstance
15d7311f15 is described below

commit 15d7311f15352f8562b85fd157f154fc62455eb2
Author: Andi Huber <ah...@apache.org>
AuthorDate: Tue Feb 28 11:08:02 2023 +0100

    CAUSEWAY-3304: JaxbUtils: don't use JAXBContext#newInstance
    
    - instead of using JAXBContext#newInstance(Class...), which does lookup
    the JaxbContextFactory on each call, and which - depending on
    system-properties - could change during the lifetime of an application,
    we rather utilize the com.sun.xml.bind.v2.ContextFactory directly
---
 commons/src/main/java/module-info.java             |  8 +--
 .../org/apache/causeway/commons/io/JaxbUtils.java  | 83 ++++++++++++++--------
 .../internal/resources/XmlRoundTripTest.java       | 30 +-------
 .../apache/causeway/commons/io/JaxbUtilsTest.java  | 10 ++-
 4 files changed, 68 insertions(+), 63 deletions(-)

diff --git a/commons/src/main/java/module-info.java b/commons/src/main/java/module-info.java
index 64d6499f39..be3c4b56be 100644
--- a/commons/src/main/java/module-info.java
+++ b/commons/src/main/java/module-info.java
@@ -74,10 +74,10 @@ module org.apache.causeway.commons {
     requires transitive spring.core;
     requires java.inject;
     requires java.annotation;
-    requires org.eclipse.persistence.moxy;
+    requires com.sun.xml.bind;
+    requires org.apache.causeway.core.privileged;
 
-    // JAXB JUnit test
-    opens org.apache.causeway.commons.internal.resources to java.xml.bind;
-    opens org.apache.causeway.commons.io to java.xml.bind;
+    opens org.apache.causeway.commons.internal.resources to java.xml.bind, com.sun.xml.bind; // JUnit test
+    opens org.apache.causeway.commons.io to java.xml.bind, com.sun.xml.bind;
 
 }
\ No newline at end of file
diff --git a/commons/src/main/java/org/apache/causeway/commons/io/JaxbUtils.java b/commons/src/main/java/org/apache/causeway/commons/io/JaxbUtils.java
index 193893e9df..f049a9b690 100644
--- a/commons/src/main/java/org/apache/causeway/commons/io/JaxbUtils.java
+++ b/commons/src/main/java/org/apache/causeway/commons/io/JaxbUtils.java
@@ -20,6 +20,10 @@ package org.apache.causeway.commons.io;
 
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -28,7 +32,9 @@ import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
 
 import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBContextFactory;
 import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
 import javax.xml.bind.Marshaller;
 import javax.xml.bind.Unmarshaller;
 import javax.xml.namespace.QName;
@@ -40,7 +46,6 @@ import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.base._NullSafe;
 import org.apache.causeway.commons.internal.codec._DocumentFactories;
 import org.apache.causeway.commons.internal.collections._Arrays;
-import org.apache.causeway.commons.internal.collections._Lists;
 import org.apache.causeway.commons.internal.collections._Maps;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.commons.internal.functions._Functions;
@@ -56,32 +61,25 @@ import lombok.experimental.UtilityClass;
 
 /**
  * Utilities to convert from and to JAXB-XML format.
+ * @implNote instead of using {@link JAXBContext#newInstance(Class...)},
+ *      which does lookup the JaxbContextFactory on each call,
+ *      and which - depending on system-properties - could change during the lifetime of an application,
+ *      we rather utilize the com.sun.xml.bind.v2.ContextFactory directly.
  *
  * @since 2.0 {@index}
  */
 @UtilityClass
 public class JaxbUtils {
 
-    /** uses given factory as default */
-    public void setDefaultJAXBContextFactory(final Class<?> jaxbContextFactoryClass, final boolean force) {
-        if(force
-                || System.getProperty(JAXBContext.JAXB_CONTEXT_FACTORY)==null) {
-            if(jaxbContextFactoryClass!=null) {
-                System.setProperty(JAXBContext.JAXB_CONTEXT_FACTORY, jaxbContextFactoryClass.getName());
-            } else {
-                System.clearProperty(JAXBContext.JAXB_CONTEXT_FACTORY);
-            }
-        }
-    }
-
-    /** uses MOXy */
-    public void useMoxy() {
-        setDefaultJAXBContextFactory(org.eclipse.persistence.jaxb.JAXBContextFactory.class, true);
-    }
+    private final static Class<?> JAXB_CONTEXT_FACTORY = com.sun.xml.bind.v2.ContextFactory.class;
+    private final static Map<String,Object> JAXB_CONTEXT_FACTORY_PROPS = Collections.<String,Object>emptyMap();
+    private final static MethodHandle JAXB_CONTEXT_FACTORY_METHOD_HANDLE = jaxbContextFactoryMethodHandle();
 
-    /** clears the system property override */
-    public static void usePlatformDefault() {
-        setDefaultJAXBContextFactory(null, true);
+    @SneakyThrows
+    private static MethodHandle jaxbContextFactoryMethodHandle() {
+        return MethodHandles.publicLookup()
+                .findStatic(JAXB_CONTEXT_FACTORY, "createContext",
+                        MethodType.methodType(JAXBContext.class, Class[].class, Map.class));
     }
 
     @Data @Builder
@@ -249,7 +247,7 @@ public class JaxbUtils {
         if(pojo==null) return;
         val opts = createOptions(customizers);
         try {
-            sink.writeAll(os->Try.run(()->opts.marshal(pojo, os)));
+            sink.writeAll(os->opts.marshal(pojo, os));
         } catch (Exception cause) {
             throw verboseException("marshalling domain object to XML", pojo.getClass(), cause);
         }
@@ -264,9 +262,9 @@ public class JaxbUtils {
             final @Nullable T pojo,
             final JaxbUtils.JaxbCustomizer ... customizers) {
         if(pojo==null) return null;
-        val sh = _Lists.<String>newArrayList(1);
-        write(pojo, DataSink.ofStringUtf8Consumer(sh::add), customizers);
-        return sh.stream().findFirst().orElse(null);
+        val sb = new StringBuilder();
+        write(pojo, DataSink.ofStringUtf8Consumer(sb), customizers);
+        return sb.toString();
     }
 
     // -- CUSTOMIZERS
@@ -301,7 +299,7 @@ public class JaxbUtils {
     @SneakyThrows
     private static <T> JAXBContext contextOf(final Class<?> ... classesToBeBound) {
         try {
-            return JAXBContext.newInstance(classesToBeBound);
+            return (JAXBContext) JAXB_CONTEXT_FACTORY_METHOD_HANDLE.invoke(open(classesToBeBound), JAXB_CONTEXT_FACTORY_PROPS);
         } catch (Exception e) {
             val msg = String.format("obtaining JAXBContext for classes (to be bound) {%s}", _NullSafe.stream(classesToBeBound)
                     .map(Class::getName)
@@ -310,6 +308,38 @@ public class JaxbUtils {
         }
     }
 
+    /**
+     * Clone of org.glassfish.jaxb.runtime.v2.MUtils.open(Class[]).
+     * @param classes used to resolve module for {@linkplain Module#addOpens(String, Module)}
+     * @throws JAXBException if any of a classes package is not open to our module.
+     */
+    private static Class<?>[] open(final Class<?>[] classes) throws JAXBException {
+        final Module coreModule = JAXB_CONTEXT_FACTORY.getClass().getModule();
+        final Module rtModule = JAXBContextFactory.class.getModule();
+        if (rtModule == coreModule || !rtModule.isNamed()) {
+            //we're either in a bundle or on the classpath
+            return classes;
+        }
+        for (Class<?> cls : classes) {
+            Class<?> jaxbClass = cls.isArray() ? cls.getComponentType() : cls;
+            final Module classModule = jaxbClass.getModule();
+            //no need for unnamed and java.base types
+            if (!classModule.isNamed() || "java.base".equals(classModule.getName())) {
+                continue;
+            }
+            final String packageName = jaxbClass.getPackageName();
+
+            if (classModule.isOpen(packageName, rtModule)) {
+                classModule.addOpens(packageName, coreModule);
+            } else {
+                throw new JAXBException(java.text.MessageFormat.format(
+                        "Package {0} with class {1} defined in a module {2} must be open to at least {3} module.",
+                        packageName, jaxbClass.getName(), classModule.getName(), rtModule.getName()));
+            }
+        }
+        return classes;
+    }
+
     // -- ENHANCE EXCEPTION MESSAGE IF POSSIBLE
 
     private static RuntimeException verboseException(final String doingWhat, @Nullable final Class<?> dtoClass, final Throwable cause) {
@@ -353,7 +383,4 @@ public class JaxbUtils {
         /*sonar-ignore-off*/
     }
 
-
-
-
 }
diff --git a/commons/src/test/java/org/apache/causeway/commons/internal/resources/XmlRoundTripTest.java b/commons/src/test/java/org/apache/causeway/commons/internal/resources/XmlRoundTripTest.java
index d391b1195f..392aaa1ae0 100644
--- a/commons/src/test/java/org/apache/causeway/commons/internal/resources/XmlRoundTripTest.java
+++ b/commons/src/test/java/org/apache/causeway/commons/internal/resources/XmlRoundTripTest.java
@@ -18,17 +18,14 @@
  */
 package org.apache.causeway.commons.internal.resources;
 
-import javax.xml.bind.JAXBContext;
 import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlType;
 
-import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import org.apache.causeway.commons.io.JaxbUtils;
 
@@ -39,33 +36,8 @@ import lombok.val;
 
 class XmlRoundTripTest {
 
-    @AfterEach
-    void cleanUp() {
-        JaxbUtils.usePlatformDefault();
-    }
-
     @Test @SneakyThrows
-    void testMoxy() {
-
-        JaxbUtils.useMoxy();
-
-        // test prerequisites
-        assertNotNull(JAXBContext.newInstance(SampleDto.class));
-
-        val dto = getSample();
-        val mapper = JaxbUtils
-                .mapperFor(SampleDto.class, opts->opts.allowMissingRootElement(true));
-        assertEquals(dto, mapper.clone(dto));
-    }
-
-    @Test @SneakyThrows
-    void testPlatformDefault() {
-
-        JaxbUtils.usePlatformDefault();
-
-        // test prerequisites
-        assertNotNull(JAXBContext.newInstance(SampleDto.class));
-
+    void testFrameworkDefault() {
         val dto = getSample();
         val mapper = JaxbUtils
                 .mapperFor(SampleDto.class, opts->opts.allowMissingRootElement(true));
diff --git a/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java b/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java
index 7243412394..588e4e193b 100644
--- a/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java
+++ b/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java
@@ -28,6 +28,9 @@ import javax.xml.bind.annotation.XmlType;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.causeway.commons.internal.base._Strings;
 
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -76,8 +79,11 @@ class JaxbUtilsTest {
         val aXml = JaxbUtils.toStringUtf8(a);
         val bXml = JaxbUtils.toStringUtf8(b);
 
-        val aRecovered = JaxbUtils.tryRead(A.class, aXml).ifFailureFail().ifAbsentFail().getValue().get();
-        val bRecovered = JaxbUtils.tryRead(B.class, bXml).ifFailureFail().ifAbsentFail().getValue().get();
+        assertTrue(_Strings.isNotEmpty(aXml));
+        assertTrue(_Strings.isNotEmpty(bXml));
+
+        val aRecovered = JaxbUtils.tryRead(A.class, aXml).valueAsNonNullElseFail();
+        val bRecovered = JaxbUtils.tryRead(B.class, bXml).valueAsNonNullElseFail();
 
         // then
         assertEquals(a, aRecovered);