diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96e7d2712..d097d6c60 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,10 @@ okhttp = "5.0.0-alpha.7" jmh = "1.37" reactor = "3.6.2" nullaway-core = "0.10.21" -jaxb = "2.3.1" +jaxb-api = "2.3.1" +jaxb-impl = "2.3.8" +jaxb-jakarta-api = "4.0.1" +jaxb-jakarta-impl = "4.0.3" moxy = "2.7.14" jsoup = "1.17.2" testng = "7.9.0" @@ -61,7 +64,10 @@ okhttp-tls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref = "okhttp okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } reactor-core = { module = "io.projectreactor:reactor-core", version.ref = "reactor" } nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway-core" } -jaxb-api = { module = "javax.xml.bind:jaxb-api", version.ref = "jaxb" } +jaxb-api = { module = "javax.xml.bind:jaxb-api", version.ref = "jaxb-api" } +jaxb-impl = { module = "com.sun.xml.bind:jaxb-impl", version.ref = "jaxb-impl" } +jaxb-jakarta-api = { module = "jakarta.xml.bind:jakarta.xml.bind-api", version.ref = "jaxb-jakarta-api" } +jaxb-jakarta-impl = { module = "com.sun.xml.bind:jaxb-impl", version.ref = "jaxb-jakarta-impl" } moxy = { module = "org.eclipse.persistence:org.eclipse.persistence.moxy", version.ref = "moxy" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } testng = { module = "org.testng:testng", version.ref = "testng" } diff --git a/methanol-blackbox/build.gradle.kts b/methanol-blackbox/build.gradle.kts index d4855d202..dba95ac78 100644 --- a/methanol-blackbox/build.gradle.kts +++ b/methanol-blackbox/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { testImplementation(project(":methanol-jackson-flux")) testImplementation(project(":methanol-protobuf")) testImplementation(project(":methanol-jaxb")) + testImplementation(project(":methanol-jaxb-jakarta")) testImplementation(project(":methanol-brotli")) testImplementation(project(":methanol-testing")) testImplementation(libs.reactor.core) @@ -20,6 +21,7 @@ dependencies { testImplementation(libs.brotli.dec) testImplementation(libs.reactivestreams) testImplementation(libs.moxy) + testImplementation(libs.jaxb.jakarta.impl) } tasks.test { @@ -30,7 +32,7 @@ tasks.test { } extraJavaModuleInfo { - failOnMissingModuleInfo.set(false) + failOnMissingModuleInfo = false automaticModule(libs.moxy.get().module.toString(), "org.eclipse.persistence.moxy") } diff --git a/methanol-blackbox/src/test/java/com/github/mizosoft/methanol/blackbox/IntegrationTest.java b/methanol-blackbox/src/test/java/com/github/mizosoft/methanol/blackbox/IntegrationTest.java index 840d01a97..1681286cd 100644 --- a/methanol-blackbox/src/test/java/com/github/mizosoft/methanol/blackbox/IntegrationTest.java +++ b/methanol-blackbox/src/test/java/com/github/mizosoft/methanol/blackbox/IntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -55,6 +55,7 @@ import com.github.mizosoft.methanol.BodyDecoder; import com.github.mizosoft.methanol.HttpReadTimeoutException; import com.github.mizosoft.methanol.MediaType; +import com.github.mizosoft.methanol.Methanol; import com.github.mizosoft.methanol.MoreBodyPublishers; import com.github.mizosoft.methanol.MultipartBodyPublisher; import com.github.mizosoft.methanol.MultipartBodyPublisher.Part; @@ -64,8 +65,13 @@ import com.github.mizosoft.methanol.blackbox.Bruh.BruhMoment; import com.github.mizosoft.methanol.blackbox.Bruh.BruhMoments; import com.github.mizosoft.methanol.blackbox.support.JacksonMapper; -import com.github.mizosoft.methanol.testing.*; +import com.github.mizosoft.methanol.testing.ByteBufferIterator; +import com.github.mizosoft.methanol.testing.IterablePublisher; +import com.github.mizosoft.methanol.testing.Logging; +import com.github.mizosoft.methanol.testing.MockGzipMember; import com.github.mizosoft.methanol.testing.MockGzipMember.CorruptionMode; +import com.github.mizosoft.methanol.testing.RegistryFileTypeDetector; +import com.github.mizosoft.methanol.testing.TestUtils; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -98,13 +104,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPOutputStream; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBContextFactory; -import javax.xml.bind.JAXBException; -import javax.xml.bind.annotation.XmlAttribute; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; import okio.Buffer; @@ -117,21 +116,24 @@ import org.junit.jupiter.api.io.TempDir; import reactor.core.publisher.Flux; -@Timeout(60) +@Timeout(10) class IntegrationTest { static { Logging.disable("com.github.mizosoft.methanol.internal.spi.ServiceCache"); } - private static final Base64.Decoder BASE64_DEC = Base64.getDecoder(); + private static final Base64.Decoder base64Decoder = Base64.getDecoder(); - private static final String poem = + private static final String POEM = "Roses are red,\n" + "Violets are blue,\n" + "I hope my tests pass\n" + "I really hope they do"; - private static final String epicArtCourseXmlUtf8 = + // There's a slight distinction between how moxy's implementation of JavaEE's JAXB & glassfish's + // implementation of Jakarta's JAXB generate the XML, particularly in the XML declaration. + + private static final String EPIC_ART_COURSE_JAVAX_XML_UTF_8 = "" + "" + "" @@ -144,8 +146,28 @@ class IntegrationTest { + "" + ""; - private static final Course epicArtCourse = - new Course(Type.ART, List.of(new Student("Leonardo Da Vinci"), new Student("Michelangelo"))); + private static final String EPIC_ART_COURSE_JAKARTA_XML_UTF_8 = + "" + + "" + + "" + + "" + + "Leonardo Da Vinci" + + "" + + "" + + "Michelangelo" + + "" + + "" + + ""; + + private static final CourseJavax EPIC_ART_COURSE_JAVAX = + new CourseJavax( + Type.ART, + List.of(new StudentJavax("Leonardo Da Vinci"), new StudentJavax("Michelangelo"))); + + private static final CourseJakarta EPIC_ART_COURSE_JAKARTA = + new CourseJakarta( + Type.ART, + List.of(new StudentJakarta("Leonardo Da Vinci"), new StudentJakarta("Michelangelo"))); private static String lotsOfText; private static Map poemEncodings; @@ -160,8 +182,8 @@ class IntegrationTest { private ScheduledExecutorService scheduler; @BeforeAll - static void readTestData() throws IOException { - Class cls = IntegrationTest.class; + static void readTestData() { + var cls = IntegrationTest.class; lotsOfText = loadUtf8(cls, "/payload/alice.txt"); lotsOfJson = loadUtf8(cls, "/payload/lots_of_json.json"); @@ -183,10 +205,12 @@ static void readTestData() throws IOException { "br", load(cls, "/payload/alice.br"), "badzip", new byte[0]); - var mapper = new JsonMapper(); - var type = new TypeRef>>() {}; try { - lotsOfJsonDecoded = mapper.readerFor(mapper.constructType(type.type())).readValue(lotsOfJson); + var mapper = new JsonMapper(); + lotsOfJsonDecoded = + mapper + .readerFor(mapper.constructType(new TypeRef>>() {}.type())) + .readValue(lotsOfJson); } catch (IOException ioe) { throw new UncheckedIOException(ioe); } @@ -201,15 +225,15 @@ static void readTestData() throws IOException { @BeforeAll static void registerJaxbImplementation() { - System.setProperty(JAXBContext.JAXB_CONTEXT_FACTORY, MoxyJaxbContextFactory.class.getName()); + System.setProperty( + javax.xml.bind.JAXBContext.JAXB_CONTEXT_FACTORY, MoxyJaxbContextFactory.class.getName()); } @BeforeEach void setUpLifecycle() throws IOException { server = new MockWebServer(); server.start(); - var builder = HttpClient.newBuilder(); - client = builder.build(); + client = Methanol.newBuilder().autoAcceptEncoding(false).build(); executor = Executors.newFixedThreadPool(8); scheduler = Executors.newSingleThreadScheduledExecutor(); } @@ -229,11 +253,11 @@ private void assertDecodes(String encoding, String expected, byte[] compressed) } private void assertDecodesSmall(String encoding) throws Exception { - var compressed = BASE64_DEC.decode(poemEncodings.get(encoding)); - assertDecodes(encoding, poem, compressed); + var compressed = base64Decoder.decode(poemEncodings.get(encoding)); + assertDecodes(encoding, POEM, compressed); // Test deflate body without zlib wrapping if (encoding.equals("deflate")) { - assertDecodes(encoding, poem, zlibUnwrap(compressed)); + assertDecodes(encoding, POEM, zlibUnwrap(compressed)); } } @@ -268,7 +292,7 @@ void decoding_brotli() throws Exception { @Test void decoding_concatenatedGzip() throws Exception { var firstMember = lotsOfTextEncodings.get("gzip"); - var secondMember = BASE64_DEC.decode(poemEncodings.get("gzip")); + var secondMember = base64Decoder.decode(poemEncodings.get("gzip")); var thirdMember = MockGzipMember.newBuilder() .addComment(55) @@ -283,7 +307,7 @@ void decoding_concatenatedGzip() throws Exception { server.enqueue(new MockResponse().setBody(buffer).setHeader("Content-Encoding", "gzip")); var request = HttpRequest.newBuilder(server.url("/").uri()).build(); var response = client.send(request, decoding(ofString())); - assertLinesMatch(lines(lotsOfText + poem + lotsOfText), lines(response.body())); + assertLinesMatch(lines(lotsOfText + POEM + lotsOfText), lines(response.body())); } @Test @@ -291,7 +315,7 @@ void decoding_corruptConcatenatedGzip() { var firstMember = lotsOfTextEncodings.get("gzip"); var secondMember = MockGzipMember.newBuilder() - .data(poem.getBytes(US_ASCII)) + .data(POEM.getBytes(US_ASCII)) .corrupt(CorruptionMode.FLG, 0xE0) // add reserved flag .build() .getBytes(); @@ -325,7 +349,7 @@ void decoding_unsupported() { void decoding_nestedHandlerGetsNoLengthOrEncoding() throws Exception { server.enqueue( new MockResponse() - .setBody(okBuffer(BASE64_DEC.decode(poemEncodings.get("gzip")))) + .setBody(okBuffer(base64Decoder.decode(poemEncodings.get("gzip")))) .setHeader("Content-Encoding", "gzip")); var request = HttpRequest.newBuilder(server.url("/").uri()).build(); var headers = new AtomicReference(); @@ -337,7 +361,7 @@ void decoding_nestedHandlerGetsNoLengthOrEncoding() throws Exception { headers.set(info.headers()); return BodyHandlers.ofString().apply(info); })); - assertEquals(poem, response.body()); + assertEquals(POEM, response.body()); var headersMap = headers.get().map(); assertFalse(headersMap.containsKey("Content-Encoding")); assertFalse(headersMap.containsKey("Content-Length")); @@ -345,10 +369,10 @@ void decoding_nestedHandlerGetsNoLengthOrEncoding() throws Exception { @Test void decoding_noEncoding() throws Exception { - server.enqueue(new MockResponse().setBody(poem)); + server.enqueue(new MockResponse().setBody(POEM)); var request = HttpRequest.newBuilder(server.url("/").uri()).build(); var response = client.send(request, decoding(ofString())); - assertEquals(poem, response.body()); + assertEquals(POEM, response.body()); } @Test @@ -475,10 +499,10 @@ void ofObject_unsupported() { @Test void ofObject_stringDecoder() throws Exception { - server.enqueue(new MockResponse().setBody(poem).addHeader("Content-Type", "text/plain")); + server.enqueue(new MockResponse().setBody(POEM).addHeader("Content-Type", "text/plain")); var request = HttpRequest.newBuilder(server.url("/").uri()).build(); var response = client.send(request, ofObject(String.class)); - assertEquals(poem, response.body()); + assertEquals(POEM, response.body()); } @Test @@ -515,12 +539,24 @@ void ofObject_uploadFlux() throws Exception { void ofObject_downloadXml() throws Exception { server.enqueue( new MockResponse() - .setBody(okBuffer(gzip(epicArtCourseXmlUtf8.getBytes(UTF_8)))) + .setBody(okBuffer(gzip(EPIC_ART_COURSE_JAVAX_XML_UTF_8.getBytes(UTF_8)))) .addHeader("Content-Encoding", "gzip") .addHeader("Content-Type", "application/xml")); var request = MutableRequest.GET(server.url("/").uri()); - var response = client.send(request, decoding(ofObject(new TypeRef() {}))); - assertEquals(epicArtCourse, response.body()); + var response = client.send(request, decoding(ofObject(new TypeRef() {}))); + assertEquals(EPIC_ART_COURSE_JAVAX, response.body()); + } + + @Test + void ofObject_downloadXmlJakarta() throws Exception { + server.enqueue( + new MockResponse() + .setBody(okBuffer(gzip(EPIC_ART_COURSE_JAKARTA_XML_UTF_8.getBytes(UTF_8)))) + .addHeader("Content-Encoding", "gzip") + .addHeader("Content-Type", "application/xml")); + var request = MutableRequest.GET(server.url("/").uri()); + var response = client.send(request, decoding(ofObject(new TypeRef() {}))); + assertEquals(EPIC_ART_COURSE_JAKARTA, response.body()); } @Test @@ -529,12 +565,28 @@ void ofObject_uploadXml() throws Exception { var request = MutableRequest.POST( server.url("/").uri(), - MoreBodyPublishers.ofObject(epicArtCourse, MediaType.TEXT_XML.withCharset(UTF_8))); + MoreBodyPublishers.ofObject( + EPIC_ART_COURSE_JAVAX, MediaType.TEXT_XML.withCharset(UTF_8))); client.sendAsync(request, discarding()); var recordedRequest = server.takeRequest(); var uploaded = recordedRequest.getBody().readUtf8(); - assertEquals(epicArtCourseXmlUtf8, uploaded); + assertEquals(EPIC_ART_COURSE_JAVAX_XML_UTF_8, uploaded); + } + + @Test + void ofObject_uploadXmlJakarta() throws Exception { + server.enqueue(new MockResponse()); + var request = + MutableRequest.POST( + server.url("/").uri(), + MoreBodyPublishers.ofObject( + EPIC_ART_COURSE_JAKARTA, MediaType.TEXT_XML.withCharset(UTF_8))); + client.sendAsync(request, discarding()); + + var recordedRequest = server.takeRequest(); + var uploaded = recordedRequest.getBody().readUtf8(); + assertEquals(EPIC_ART_COURSE_JAKARTA_XML_UTF_8, uploaded); } @Test @@ -575,7 +627,7 @@ void withReadTimeout_readThroughByteChannel() throws Exception { var timeoutMillis = 50L; server.enqueue( new MockResponse() - .setBody(poem) + .setBody(POEM) .throttleBody(0, timeoutMillis * 10, TimeUnit.MILLISECONDS)); var request = HttpRequest.newBuilder(server.url("/").uri()).build(); var response = @@ -768,29 +820,83 @@ static final class Tweet { public Tweet() {} } - @XmlRootElement(name = "course") - private static final class Course { - @XmlAttribute(required = true) + @jakarta.xml.bind.annotation.XmlRootElement(name = "course") + private static final class CourseJakarta { + @jakarta.xml.bind.annotation.XmlAttribute(required = true) + private final Type type; + + @jakarta.xml.bind.annotation.XmlElementWrapper(name = "enrolled-students") + @jakarta.xml.bind.annotation.XmlElement(name = "student") + private final List enrolledStudents; + + CourseJakarta() { + this(Type.UNKNOWN, new ArrayList<>()); + } + + CourseJakarta(Type type, List enrolledStudents) { + this.type = type; + this.enrolledStudents = enrolledStudents; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof CourseJakarta + && type == ((CourseJakarta) obj).type + && enrolledStudents.equals(((CourseJakarta) obj).enrolledStudents); + } + + @Override + public String toString() { + return "CourseJakarta[type=" + type + ", enrolledStudents=" + enrolledStudents + "]"; + } + } + + private static final class StudentJakarta { + @jakarta.xml.bind.annotation.XmlElement(required = true) + private final String name; + + StudentJakarta() { + this(""); + } + + StudentJakarta(String name) { + this.name = name; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof StudentJakarta && name.equals(((StudentJakarta) obj).name); + } + + @Override + public String toString() { + return "StudentJakarta[" + name + "]"; + } + } + + @javax.xml.bind.annotation.XmlRootElement(name = "course") + private static final class CourseJavax { + @javax.xml.bind.annotation.XmlAttribute(required = true) private final Type type; - @XmlElementWrapper(name = "enrolled-students") - @XmlElement(name = "student") - private final List enrolledStudents; + @javax.xml.bind.annotation.XmlElementWrapper(name = "enrolled-students") + @javax.xml.bind.annotation.XmlElement(name = "student") + private final List enrolledStudents; - private Course() { + CourseJavax() { this(Type.UNKNOWN, new ArrayList<>()); } - Course(Type type, List enrolledStudents) { + CourseJavax(Type type, List enrolledStudents) { this.type = type; this.enrolledStudents = enrolledStudents; } @Override public boolean equals(Object obj) { - return obj instanceof Course - && type == ((Course) obj).type - && enrolledStudents.equals(((Course) obj).enrolledStudents); + return obj instanceof CourseJavax + && type == ((CourseJavax) obj).type + && enrolledStudents.equals(((CourseJavax) obj).enrolledStudents); } @Override @@ -799,21 +905,21 @@ public String toString() { } } - private static final class Student { - @XmlElement(required = true) + private static final class StudentJavax { + @javax.xml.bind.annotation.XmlElement(required = true) private final String name; - private Student() { + StudentJavax() { this(""); } - Student(String name) { + StudentJavax(String name) { this.name = name; } @Override public boolean equals(Object obj) { - return obj instanceof Student && name.equals(((Student) obj).name); + return obj instanceof StudentJavax && name.equals(((StudentJavax) obj).name); } @Override @@ -828,20 +934,21 @@ private enum Type { CALCULUS } - public static class MoxyJaxbContextFactory implements JAXBContextFactory { + public static final class MoxyJaxbContextFactory implements javax.xml.bind.JAXBContextFactory { public MoxyJaxbContextFactory() {} @Override - public JAXBContext createContext(Class[] classesToBeBound, Map properties) - throws JAXBException { + public javax.xml.bind.JAXBContext createContext( + Class[] classesToBeBound, Map properties) + throws javax.xml.bind.JAXBException { return org.eclipse.persistence.jaxb.JAXBContextFactory.createContext( classesToBeBound, properties); } @Override - public JAXBContext createContext( + public javax.xml.bind.JAXBContext createContext( String contextPath, ClassLoader classLoader, Map properties) - throws JAXBException { + throws javax.xml.bind.JAXBException { return org.eclipse.persistence.jaxb.JAXBContextFactory.createContext( contextPath, classLoader, properties); } diff --git a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbUtils.java b/methanol-blackbox/src/test/java/com/github/mizosoft/methanol/blackbox/support/JaxbJakartaProviders.java similarity index 50% rename from methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbUtils.java rename to methanol-blackbox/src/test/java/com/github/mizosoft/methanol/blackbox/support/JaxbJakartaProviders.java index d1df15e1e..c2b51c614 100644 --- a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbUtils.java +++ b/methanol-blackbox/src/test/java/com/github/mizosoft/methanol/blackbox/support/JaxbJakartaProviders.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,35 +20,27 @@ * SOFTWARE. */ -package com.github.mizosoft.methanol.adapter.jaxb; +package com.github.mizosoft.methanol.blackbox.support; -import java.util.Map; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBContextFactory; -import javax.xml.bind.JAXBException; +import com.github.mizosoft.methanol.BodyAdapter; +import com.github.mizosoft.methanol.adapter.jaxb.jakarta.JaxbAdapterFactory; -class JaxbUtils { - static void registerImplementation() { - System.setProperty(JAXBContext.JAXB_CONTEXT_FACTORY, MoxyJaxbContextFactory.class.getName()); - } +public class JaxbJakartaProviders { + private JaxbJakartaProviders() {} - // Make the factory accessible to JAXB - public static final class MoxyJaxbContextFactory implements JAXBContextFactory { - public MoxyJaxbContextFactory() {} + public static class EncoderProvider { + private EncoderProvider() {} - @Override - public JAXBContext createContext(Class[] classesToBeBound, Map properties) - throws JAXBException { - return org.eclipse.persistence.jaxb.JAXBContextFactory.createContext( - classesToBeBound, properties); + public static BodyAdapter.Encoder provider() { + return JaxbAdapterFactory.createEncoder(); } + } + + public static class DecoderProvider { + private DecoderProvider() {} - @Override - public JAXBContext createContext( - String contextPath, ClassLoader classLoader, Map properties) - throws JAXBException { - return org.eclipse.persistence.jaxb.JAXBContextFactory.createContext( - contextPath, classLoader, properties); + public static BodyAdapter.Decoder provider() { + return JaxbAdapterFactory.createDecoder(); } } } diff --git a/methanol-blackbox/src/test/java/module-info.java b/methanol-blackbox/src/test/java/module-info.java index a9a4c227a..5b015c9ce 100644 --- a/methanol-blackbox/src/test/java/module-info.java +++ b/methanol-blackbox/src/test/java/module-info.java @@ -5,6 +5,7 @@ import com.github.mizosoft.methanol.blackbox.support.FailingBodyDecoderFactory; import com.github.mizosoft.methanol.blackbox.support.JacksonFluxProviders; import com.github.mizosoft.methanol.blackbox.support.JacksonProviders; +import com.github.mizosoft.methanol.blackbox.support.JaxbJakartaProviders; import com.github.mizosoft.methanol.blackbox.support.JaxbProviders; import com.github.mizosoft.methanol.blackbox.support.MyBodyDecoderFactory; import com.github.mizosoft.methanol.blackbox.support.ProtobufProviders; @@ -18,6 +19,7 @@ requires methanol.adapter.jackson.flux; requires methanol.adapter.protobuf; requires methanol.adapter.jaxb; + requires methanol.adapter.jaxb.jakarta; requires methanol.brotli; requires methanol.testing; requires com.google.protobuf; @@ -26,8 +28,11 @@ requires okio; requires reactor.core; requires org.reactivestreams; - requires org.eclipse.persistence.moxy; - requires java.sql; // Required by org.eclipse.persistence.moxy + requires java.xml.bind; + requires jakarta.xml.bind; + requires org.eclipse.persistence.moxy; // Used for Javax JAXB. + requires com.sun.xml.bind; // Used for Jakarta JAXB. + requires java.sql; // Required by org.eclipse.persistence.moxy. requires static org.checkerframework.checker.qual; provides BodyDecoder.Factory with @@ -40,12 +45,14 @@ JacksonProviders.EncoderProvider, ProtobufProviders.EncoderProvider, JaxbProviders.EncoderProvider, + JaxbJakartaProviders.EncoderProvider, CharSequenceEncoderProvider; provides BodyAdapter.Decoder with JacksonFluxProviders.DecoderProvider, JacksonProviders.DecoderProvider, ProtobufProviders.DecoderProvider, JaxbProviders.DecoderProvider, + JaxbJakartaProviders.DecoderProvider, StringDecoderProvider; provides FileTypeDetector with RegistryFileTypeDetectorProvider; diff --git a/methanol-jaxb-jakarta/README.md b/methanol-jaxb-jakarta/README.md new file mode 100644 index 000000000..9f4bdc4ea --- /dev/null +++ b/methanol-jaxb-jakarta/README.md @@ -0,0 +1,184 @@ +# methanol-jaxb-jakarta + +Adapters for XML using Jakarta EE's [JAXB][jaxb]. + +## Installation + +### Gradle + +```gradle +implementation 'com.github.mizosoft.methanol:methanol-jaxb-jakarta:1.7.0' +``` + +### Maven + +```xml + + com.github.mizosoft.methanol + methanol-jaxb-jakarta + 1.7.0 + +``` + +The adapters need to be registered as [service providers][serviceloader_javadoc] so Methanol knows +they're there. +The way this is done depends on your project setup. + +### Module Path + +Follow these steps if your project uses the Java module system. + +1. Add this class to your module: + + ```java + public class JaxbProviders { + public static class EncoderProvider { + public static BodyAdapter.Encoder provider() { + return JaxbAdapterFactory.createEncoder(); + } + } + + public static class DecoderProvider { + public static BodyAdapter.Decoder provider() { + return JaxbAdapterFactory.createDecoder(); + } + } + } + ``` + +2. Add the corresponding provider declarations in your `module-info.java` file. + + ```java + requires methanol.adapter.jaxb.jakarta; + + provides BodyAdapter.Encoder with JaxbProviders.EncoderProvider; + provides BodyAdapter.Decoder with JaxbProviders.DecoderProvider; + ``` + +### Classpath + +Registering adapters from the classpath requires declaring the implementation classes in +provider-configuration +files that are bundled with your JAR. You'll first need to implement +delegating `Encoder` & `Decoder` +that forward to the instances created by `JaxbAdapterFactory`. Extending from `ForwardingEncoder` & +`ForwardingDecoder` makes this easier. + +You can use Google's [AutoService][autoservice] to generate the provider-configuration files +automatically, +so you won't bother writing them. + +#### Using AutoService + +First, [install AutoService][autoservice_getting_started]. + +##### Gradle + +```gradle +implementation "com.google.auto.service:auto-service-annotations:$autoServiceVersion" +annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion" +``` + +##### Maven + +```xml + + com.google.auto.service + auto-service-annotations + ${autoServiceVersion} + +``` + +Configure the annotation processor with the compiler plugin. + +```xml + + maven-compiler-plugin + + + + com.google.auto.service + auto-service + ${autoServiceVersion} + + + + +``` + +Next, add this class to your project: + +```java +public class JaxbAdapters { + @AutoService(BodyAdapter.Encoder.class) + public static class Encoder extends ForwardingEncoder { + public Encoder() { + super(JaxbAdapterFactory.createEncoder()); + } + } + + @AutoService(BodyAdapter.Decoder.class) + public static class Decoder extends ForwardingDecoder { + public Decoder() { + super(JaxbAdapterFactory.createDecoder()); + } + } +} +``` + +#### Manual Configuration + +You can also write the configuration files manually. First, add this class to your project: + +```java +public class JaxbAdapters { + public static class Encoder extends ForwardingEncoder { + public Encoder() { + super(JaxbAdapterFactory.createEncoder()); + } + } + + public static class Decoder extends ForwardingDecoder { + public Decoder() { + super(JaxbAdapterFactory.createDecoder()); + } + } +} +``` + +Next, create two provider-configuration files in the resource directory: `META-INF/services`, +one for the encoder and the other for the decoder. Each file must contain the fully qualified +name of the implementation class. + +Let's say the above class is in a package named `com.example`. You'll want to have one file for the +encoder named: + +``` +META-INF/services/com.github.mizosoft.methanol.BodyAdapter$Encoder +``` + +and contains the following line: + +``` +com.example.JaxbAdapters$Encoder +``` + +Similarly, the decoder's file is named: + +``` +META-INF/services/com.github.mizosoft.methanol.BodyAdapter$Decoder +``` + +and contains: + +``` +com.example.JaxbAdapters$Decoder +``` + +[jaxb]: https://eclipse-ee4j.github.io/jaxb-ri/ + +[autoservice]: https://github.com/google/auto/tree/master/service + +[autoservice_getting_started]: https://github.com/google/auto/tree/master/service#getting-started + +[serviceloader_javadoc]: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ServiceLoader.html diff --git a/methanol-jaxb-jakarta/build.gradle.kts b/methanol-jaxb-jakarta/build.gradle.kts new file mode 100644 index 000000000..842e53451 --- /dev/null +++ b/methanol-jaxb-jakarta/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("conventions.java-library") + id("conventions.static-analysis") + id("conventions.testing") + id("conventions.coverage") + id("conventions.publishing") +} + +dependencies { + api(project(":methanol")) + api(libs.jaxb.jakarta.api) + + testImplementation(project(":methanol-testing")) + testImplementation(libs.jaxb.jakarta.impl) +} diff --git a/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/CachingJaxbBindingFactory.java b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/CachingJaxbBindingFactory.java new file mode 100644 index 000000000..aacbf7649 --- /dev/null +++ b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/CachingJaxbBindingFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +final class CachingJaxbBindingFactory implements JaxbBindingFactory { + private final ConcurrentMap, JAXBContext> cachedContexts = new ConcurrentHashMap<>(); + + CachingJaxbBindingFactory() {} + + @Override + public Marshaller createMarshaller(Class boundClass) throws JAXBException { + return getOrCreateContext(boundClass).createMarshaller(); + } + + @Override + public Unmarshaller createUnmarshaller(Class boundClass) throws JAXBException { + return getOrCreateContext(boundClass).createUnmarshaller(); + } + + // Visible for testing. + JAXBContext getOrCreateContext(Class boundClass) { + return cachedContexts.computeIfAbsent( + boundClass, + c -> { + try { + return JAXBContext.newInstance(c); + } catch (JAXBException e) { + throw new UncheckedJaxbException(e); + } + }); + } +} diff --git a/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbAdapter.java b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbAdapter.java new file mode 100644 index 000000000..fabe6e9de --- /dev/null +++ b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbAdapter.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import static java.util.Objects.requireNonNull; + +import com.github.mizosoft.methanol.BodyAdapter; +import com.github.mizosoft.methanol.MediaType; +import com.github.mizosoft.methanol.TypeRef; +import com.github.mizosoft.methanol.adapter.AbstractBodyAdapter; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; +import jakarta.xml.bind.annotation.XmlEnum; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.XmlType; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.BodySubscribers; +import java.nio.charset.Charset; +import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; + +abstract class JaxbAdapter extends AbstractBodyAdapter { + final JaxbBindingFactory jaxbFactory; + + JaxbAdapter(JaxbBindingFactory jaxbFactory) { + super(MediaType.APPLICATION_XML, MediaType.TEXT_XML); + this.jaxbFactory = requireNonNull(jaxbFactory); + } + + @Override + public boolean supportsType(TypeRef type) { + if (!(type.type() instanceof Class)) { + return false; + } + + var clazz = type.rawType(); + return clazz.isAnnotationPresent(XmlRootElement.class) + || clazz.isAnnotationPresent(XmlType.class) + || clazz.isAnnotationPresent(XmlEnum.class); + } + + static final class Encoder extends JaxbAdapter implements BodyAdapter.Encoder { + Encoder(JaxbBindingFactory factory) { + super(factory); + } + + @Override + public BodyPublisher toBody(Object object, @Nullable MediaType mediaType) { + requireNonNull(object); + requireSupport(object.getClass()); + requireCompatibleOrNull(mediaType); + var outputBuffer = new ByteArrayOutputStream(); + try { + var marshaller = jaxbFactory.createMarshaller(object.getClass()); + String encoding; + if (mediaType != null && (encoding = mediaType.parameters().get("charset")) != null) { + marshaller.setProperty(Marshaller.JAXB_ENCODING, encoding); + } + marshaller.marshal(object, outputBuffer); + } catch (JAXBException e) { + throw new UncheckedJaxbException(e); + } + return attachMediaType(BodyPublishers.ofByteArray(outputBuffer.toByteArray()), mediaType); + } + } + + static final class Decoder extends JaxbAdapter implements BodyAdapter.Decoder { + Decoder(JaxbBindingFactory factory) { + super(factory); + } + + @Override + public BodySubscriber toObject(TypeRef objectType, @Nullable MediaType mediaType) { + requireNonNull(objectType); + requireSupport(objectType); + requireCompatibleOrNull(mediaType); + var elementClass = objectType.exactRawType(); + var charset = charsetOrNull(mediaType); + var unmarshaller = createUnmarshallerUnchecked(elementClass); + return BodySubscribers.mapping( + BodySubscribers.ofByteArray(), + bytes -> + unmarshalValue(elementClass, unmarshaller, new ByteArrayInputStream(bytes), charset)); + } + + @Override + public BodySubscriber> toDeferredObject( + TypeRef objectType, @Nullable MediaType mediaType) { + requireNonNull(objectType); + requireSupport(objectType); + requireCompatibleOrNull(mediaType); + var elementClass = objectType.exactRawType(); + var charset = charsetOrNull(mediaType); + var unmarshaller = createUnmarshallerUnchecked(elementClass); + return BodySubscribers.mapping( + BodySubscribers.ofInputStream(), + in -> () -> unmarshalValue(elementClass, unmarshaller, in, charset)); + } + + private T unmarshalValue( + Class elementClass, + Unmarshaller unmarshaller, + InputStream in, + @Nullable Charset charset) { + try { + // If the charset is known from the media type, use it for a Reader + // to avoid the overhead of having to infer it from the document. + return elementClass.cast( + charset != null + ? unmarshaller.unmarshal(new InputStreamReader(in, charset)) + : unmarshaller.unmarshal(in)); + } catch (JAXBException e) { + throw new UncheckedJaxbException(e); + } + } + + private Unmarshaller createUnmarshallerUnchecked(Class elementClass) { + try { + return jaxbFactory.createUnmarshaller(elementClass); + } catch (JAXBException e) { + throw new UncheckedJaxbException(e); + } + } + + private static @Nullable Charset charsetOrNull(@Nullable MediaType mediaType) { + return mediaType != null ? mediaType.charset().orElse(null) : null; + } + } +} diff --git a/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbAdapterFactory.java b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbAdapterFactory.java new file mode 100644 index 000000000..e01910340 --- /dev/null +++ b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbAdapterFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import com.github.mizosoft.methanol.BodyAdapter.Decoder; +import com.github.mizosoft.methanol.BodyAdapter.Encoder; + +/** Creates {@link com.github.mizosoft.methanol.BodyAdapter} implementations for XML using JAXB. */ +public class JaxbAdapterFactory { + private JaxbAdapterFactory() {} + + /** Returns a new {@code Encoder} using the default caching factory. */ + public static Encoder createEncoder() { + return createEncoder(JaxbBindingFactory.create()); + } + + /** Returns a new {@code Encoder} using the given factory. */ + public static Encoder createEncoder(JaxbBindingFactory jaxbFactory) { + return new JaxbAdapter.Encoder(jaxbFactory); + } + + /** Returns a new {@code Decoder} using the default caching factory. */ + public static Decoder createDecoder() { + return createDecoder(JaxbBindingFactory.create()); + } + + /** Returns a new {@code Decoder} using the given factory. */ + public static Decoder createDecoder(JaxbBindingFactory jaxbFactory) { + return new JaxbAdapter.Decoder(jaxbFactory); + } +} diff --git a/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbBindingFactory.java b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbBindingFactory.java new file mode 100644 index 000000000..467499c58 --- /dev/null +++ b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbBindingFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; + +/** + * Creates new {@link Marshaller} or {@link Unmarshaller} objects on demand for use by an adapter. + */ +public interface JaxbBindingFactory { + /** Returns a new {@code Marshaller} for encoding an object of the given class. */ + Marshaller createMarshaller(Class boundClass) throws JAXBException; + + /** Returns a new {@code Unmarshaller} for decoding to an object of the given class. */ + Unmarshaller createUnmarshaller(Class boundClass) throws JAXBException; + + /** + * Returns a new {@code JaxbBindingFactory} that creates and caches {@code JAXBContexts} for each + * requested type. + */ + static JaxbBindingFactory create() { + return new CachingJaxbBindingFactory(); + } +} diff --git a/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/UncheckedJaxbException.java b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/UncheckedJaxbException.java new file mode 100644 index 000000000..649cf39ec --- /dev/null +++ b/methanol-jaxb-jakarta/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/UncheckedJaxbException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import jakarta.xml.bind.JAXBException; + +/** Unchecked wrapper over a {@link JAXBException}. */ +public class UncheckedJaxbException extends RuntimeException { + /** Creates a new {@code UncheckedJaxbException} with the given cause. */ + public UncheckedJaxbException(JAXBException cause) { + super(cause); + } + + @Override + public JAXBException getCause() { + return (JAXBException) super.getCause(); + } +} diff --git a/methanol-jaxb-jakarta/src/main/java/module-info.java b/methanol-jaxb-jakarta/src/main/java/module-info.java new file mode 100644 index 000000000..2b1d04305 --- /dev/null +++ b/methanol-jaxb-jakarta/src/main/java/module-info.java @@ -0,0 +1,16 @@ +import com.github.mizosoft.methanol.BodyAdapter; +import com.github.mizosoft.methanol.adapter.jaxb.jakarta.JaxbAdapterFactory; + +/** + * Provides {@link BodyAdapter.Encoder} and {@link BodyAdapter.Decoder} implementations for XML + * using JAXB. Note that, for the sake of configurability, the adapters are not service-provided by + * default. You will need to explicitly declare service-providers that delegate to the instances + * created by {@link JaxbAdapterFactory}. + */ +module methanol.adapter.jaxb.jakarta { + requires transitive methanol; + requires transitive jakarta.xml.bind; + requires static org.checkerframework.checker.qual; + + exports com.github.mizosoft.methanol.adapter.jaxb.jakarta; +} diff --git a/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/CachingJaxbBindingFactoryTest.java b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/CachingJaxbBindingFactoryTest.java new file mode 100644 index 000000000..436e76283 --- /dev/null +++ b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/CachingJaxbBindingFactoryTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class CachingJaxbBindingFactoryTest { + @Test + void cachesContextsForSameType() { + var factory = new CachingJaxbBindingFactory(); + assertThat(factory.getOrCreateContext(Point.class)) + .isSameAs(factory.getOrCreateContext(Point.class)); + } +} diff --git a/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbDecoderTest.java b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbDecoderTest.java new file mode 100644 index 000000000..a6e45b018 --- /dev/null +++ b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbDecoderTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import static com.github.mizosoft.methanol.adapter.jaxb.jakarta.JaxbAdapterFactory.createDecoder; +import static com.github.mizosoft.methanol.testing.verifiers.Verifiers.verifyThat; +import static java.nio.charset.StandardCharsets.UTF_16; + +import com.github.mizosoft.methanol.testing.TestException; +import org.junit.jupiter.api.Test; + +class JaxbDecoderTest { + @Test + void compatibleMediaTypes() { + verifyThat(createDecoder()) + .isCompatibleWith("application/xml") + .isCompatibleWith("text/xml") + .isCompatibleWith("application/*") + .isCompatibleWith("text/*") + .isCompatibleWith("*/*"); + } + + @Test + void incompatibleMediaTypes() { + verifyThat(createDecoder()) + .isNotCompatibleWith("text/html") + .isNotCompatibleWith("application/json") + .isNotCompatibleWith("image/*"); + } + + @Test + void deserialize() { + verifyThat(createDecoder()) + .converting(Point.class) + .withBody("") + .succeedsWith(new Point(1, 2)); + } + + @Test + void deserializeWithUtf16_inferredFromMediaType() { + verifyThat(createDecoder()) + .converting(Point.class) + .withMediaType("application/xml; charset=utf-16") + .withBody("", UTF_16) + .succeedsWith(new Point(1, 2)); + } + + @Test + void deserializeWithUtf16_inferredFromXmlDocument() { + verifyThat(createDecoder()) + .converting(Point.class) + .withBody("", UTF_16) + .succeedsWith(new Point(1, 2)); + } + + @Test + void deserializeList() { + verifyThat(createDecoder()) + .converting(PointList.class) + .withBody( + "" + + "" + + "" + + "" + + "") + .succeedsWith(new PointList(new Point(1, 2), new Point(3, 4))); + } + + @Test + void deserializeBadXml() { + verifyThat(createDecoder()) + .converting(Point.class) + .withBody("") // Missing forward slash + .failsWith( + UncheckedJaxbException.class); // JAXBExceptions are rethrown as UncheckedJaxbExceptions + } + + @Test + void deserializeWithError() { + verifyThat(createDecoder()) + .converting(Point.class) + .withFailure(new TestException()) + .failsWith(TestException.class); + } + + @Test + void deserializeWithUnsupportedType() { + class NotAXmlRootElement {} + + verifyThat(createDecoder()).converting(NotAXmlRootElement.class).isNotSupported(); + } + + @Test + void deserializeWithUnsupportedMediaType() { + verifyThat(createDecoder()) + .converting(Point.class) + .withMediaType("application/json") + .isNotSupported(); + } +} diff --git a/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbDeferredDecoderTest.java b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbDeferredDecoderTest.java new file mode 100644 index 000000000..6082dd05c --- /dev/null +++ b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbDeferredDecoderTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import static com.github.mizosoft.methanol.adapter.jaxb.jakarta.JaxbAdapterFactory.createDecoder; +import static com.github.mizosoft.methanol.testing.verifiers.Verifiers.verifyThat; +import static java.nio.charset.StandardCharsets.UTF_16; + +import com.github.mizosoft.methanol.testing.TestException; +import org.junit.jupiter.api.Test; + +class JaxbDeferredDecoderTest { + @Test + void deserialize() { + verifyThat(createDecoder()) + .converting(Point.class) + .withDeferredBody("") + .succeedsWith(new Point(1, 2)); + } + + @Test + void deserializeWithUtf16_inferredFromMediaType() { + verifyThat(createDecoder()) + .converting(Point.class) + .withMediaType("application/xml; charset=utf-16") + .withDeferredBody("", UTF_16) + .succeedsWith(new Point(1, 2)); + } + + @Test + void deserializeWithUtf16_inferredFromXmlDocument() { + verifyThat(createDecoder()) + .converting(Point.class) + .withDeferredBody( + "", UTF_16) + .succeedsWith(new Point(1, 2)); + } + + @Test + void deserializeList() { + verifyThat(createDecoder()) + .converting(PointList.class) + .withDeferredBody( + "" + + "" + + "" + + "" + + "") + .succeedsWith(new PointList(new Point(1, 2), new Point(3, 4))); + } + + @Test + void deserializeBadXml() { + verifyThat(createDecoder()) + .converting(Point.class) + .withDeferredBody( + "") // No enclosing forward slash + .failsWith(UncheckedJaxbException.class); + } + + @Test + void deserializeWithError() { + verifyThat(createDecoder()) + .converting(Point.class) + .withDeferredFailure(new TestException()) + .failsWith(UncheckedJaxbException.class); + } +} diff --git a/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbEncoderTest.java b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbEncoderTest.java new file mode 100644 index 000000000..a8f94e5c5 --- /dev/null +++ b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/JaxbEncoderTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import static com.github.mizosoft.methanol.adapter.jaxb.jakarta.JaxbAdapterFactory.createEncoder; +import static com.github.mizosoft.methanol.testing.verifiers.Verifiers.verifyThat; +import static java.nio.charset.StandardCharsets.UTF_16; +import static org.junit.jupiter.api.Assertions.fail; + +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; +import org.junit.jupiter.api.Test; + +class JaxbEncoderTest { + @Test + void compatibleMediaTypes() { + verifyThat(createEncoder()) + .isCompatibleWith("application/xml") + .isCompatibleWith("text/xml") + .isCompatibleWith("application/*") + .isCompatibleWith("text/*") + .isCompatibleWith("*/*"); + } + + @Test + void incompatibleMediaTypes() { + verifyThat(createEncoder()) + .isNotCompatibleWith("application/json") + .isNotCompatibleWith("text/html") + .isNotCompatibleWith("image/*"); + } + + @Test + void serialize() { + verifyThat(createEncoder()) + .converting(new Point(1, 2)) + .succeedsWith( + ""); + } + + @Test + void serializeWithUtf16() { + verifyThat(createEncoder()) + .converting(new Point(1, 2)) + .withMediaType("application/xml; charset=utf-16") + .succeedsWith( + "", + UTF_16); + } + + @Test + void serializeList() { + verifyThat(createEncoder()) + .converting(new PointList(new Point(1, 2), new Point(3, 4))) + .succeedsWith( + "" + + "" + + "" + + "" + + ""); + } + + @Test + void serializeWithFormattedXml() { + var customFactory = + new JaxbBindingFactory() { + private final JaxbBindingFactory delegate = JaxbBindingFactory.create(); + + /** Create a Marshaller with pretty printing. */ + @Override + public Marshaller createMarshaller(Class boundClass) throws JAXBException { + var marshaller = delegate.createMarshaller(boundClass); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + return marshaller; + } + + @Override + public Unmarshaller createUnmarshaller(Class boundClass) { + return fail("unexpected unmarshalling"); + } + }; + + verifyThat(createEncoder(customFactory)) + .converting(new PointList(new Point(1, 2), new Point(3, 4))) + .succeedsWithNormalizingLineEndings( + "\n" + + "\n" + + " \n" + + " \n" + + "\n"); + } + + @Test + void serializeWithUnsupportedType() { + class NotAXmlRootElement {} + + verifyThat(createEncoder()).converting(new NotAXmlRootElement()).isNotSupported(); + } + + @Test + void serializeWithUnsupportedMediaType() { + verifyThat(createEncoder()) + .converting(new Point(1, 2)) + .withMediaType("application/json") + .isNotSupported(); + } + + @Test + void mediaTypeIsAttached() { + verifyThat(createEncoder()) + .converting(new Point(1, 2)) + .withMediaType("application/xml") + .asBodyPublisher() + .hasMediaType("application/xml"); + + verifyThat(createEncoder()) + .converting(new Point(1, 2)) + .withMediaType("text/xml") + .asBodyPublisher() + .hasMediaType("text/xml"); + } + + @Test + void mediaTypeWithCharsetIsAttached() { + verifyThat(createEncoder()) + .converting(new Point(1, 2)) + .withMediaType("application/xml; charset=utf-16") + .asBodyPublisher() + .hasMediaType("application/xml; charset=utf-16"); + + verifyThat(createEncoder()) + .converting(new Point(1, 2)) + .withMediaType("text/xml; charset=utf-16") + .asBodyPublisher() + .hasMediaType("text/xml; charset=utf-16"); + } +} diff --git a/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/Point.java b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/Point.java new file mode 100644 index 000000000..dd3c614f2 --- /dev/null +++ b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/Point.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlRootElement; +import java.util.Objects; + +@XmlRootElement(name = "point") +public class Point { + @XmlAttribute(required = true) + private final int x; + + @XmlAttribute(required = true) + private final int y; + + // Provide a no-arg constructor for JAXB + public Point() { + this(0, 0); + } + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Point)) { + return false; + } + var other = (Point) obj; + return x == other.x && y == other.y; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } + + @Override + public String toString() { + return "Point[" + x + ", " + y + "]"; + } +} diff --git a/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/PointList.java b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/PointList.java new file mode 100644 index 000000000..ed32628df --- /dev/null +++ b/methanol-jaxb-jakarta/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/jakarta/PointList.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 Moataz Abdelnasser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.mizosoft.methanol.adapter.jaxb.jakarta; + +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; + +@XmlRootElement(name = "points") +public class PointList { + @XmlElement(name = "point") + private final List points; + + // Provide a no-arg constructor for JAXB + public PointList() { + this(new ArrayList<>()); + } + + public PointList(Point... points) { + this.points = List.of(points); + } + + public PointList(List points) { + this.points = points; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PointList)) { + return false; + } + return points.equals(((PointList) obj).points); + } + + @Override + public int hashCode() { + return points.hashCode(); + } + + @Override + public String toString() { + return "PointList" + points.toString(); + } +} diff --git a/methanol-jaxb/README.md b/methanol-jaxb/README.md index c3f42e160..0621a54f7 100644 --- a/methanol-jaxb/README.md +++ b/methanol-jaxb/README.md @@ -1,6 +1,6 @@ # methanol-jaxb -Adapters for XML using [JAXB][jaxb]. +Adapters for XML using Java EE's [JAXB][jaxb]. ## Installation diff --git a/methanol-jaxb/build.gradle.kts b/methanol-jaxb/build.gradle.kts index 45f7ef4d3..6347e2647 100644 --- a/methanol-jaxb/build.gradle.kts +++ b/methanol-jaxb/build.gradle.kts @@ -11,5 +11,5 @@ dependencies { api(libs.jaxb.api) testImplementation(project(":methanol-testing")) - testImplementation(libs.moxy) + testImplementation(libs.jaxb.impl) } diff --git a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/CachingJaxbBindingFactory.java b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/CachingJaxbBindingFactory.java index d33b91507..72f6e3097 100644 --- a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/CachingJaxbBindingFactory.java +++ b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/CachingJaxbBindingFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -30,12 +30,9 @@ import javax.xml.bind.Unmarshaller; final class CachingJaxbBindingFactory implements JaxbBindingFactory { + private final ConcurrentMap, JAXBContext> cachedContexts = new ConcurrentHashMap<>(); - private final ConcurrentMap, JAXBContext> cachedContexts; - - CachingJaxbBindingFactory() { - cachedContexts = new ConcurrentHashMap<>(); - } + CachingJaxbBindingFactory() {} @Override public Marshaller createMarshaller(Class boundClass) throws JAXBException { @@ -47,7 +44,7 @@ public Unmarshaller createUnmarshaller(Class boundClass) throws JAXBException return getOrCreateContext(boundClass).createUnmarshaller(); } - // not private for testing + // Visible for testing. JAXBContext getOrCreateContext(Class boundClass) { return cachedContexts.computeIfAbsent( boundClass, diff --git a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbAdapter.java b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbAdapter.java index c6e183e06..7c1717840 100644 --- a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbAdapter.java +++ b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -47,7 +47,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; abstract class JaxbAdapter extends AbstractBodyAdapter { - final JaxbBindingFactory jaxbFactory; JaxbAdapter(JaxbBindingFactory jaxbFactory) { @@ -57,17 +56,17 @@ abstract class JaxbAdapter extends AbstractBodyAdapter { @Override public boolean supportsType(TypeRef type) { - if (type.type() instanceof Class) { - Class clazz = type.rawType(); - return clazz.isAnnotationPresent(XmlRootElement.class) - || clazz.isAnnotationPresent(XmlType.class) - || clazz.isAnnotationPresent(XmlEnum.class); + if (!(type.type() instanceof Class)) { + return false; } - return false; + + var clazz = type.rawType(); + return clazz.isAnnotationPresent(XmlRootElement.class) + || clazz.isAnnotationPresent(XmlType.class) + || clazz.isAnnotationPresent(XmlEnum.class); } static final class Encoder extends JaxbAdapter implements BodyAdapter.Encoder { - Encoder(JaxbBindingFactory factory) { super(factory); } @@ -77,9 +76,9 @@ public BodyPublisher toBody(Object object, @Nullable MediaType mediaType) { requireNonNull(object); requireSupport(object.getClass()); requireCompatibleOrNull(mediaType); - ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); + var outputBuffer = new ByteArrayOutputStream(); try { - Marshaller marshaller = jaxbFactory.createMarshaller(object.getClass()); + var marshaller = jaxbFactory.createMarshaller(object.getClass()); String encoding; if (mediaType != null && (encoding = mediaType.parameters().get("charset")) != null) { marshaller.setProperty(Marshaller.JAXB_ENCODING, encoding); @@ -93,7 +92,6 @@ public BodyPublisher toBody(Object object, @Nullable MediaType mediaType) { } static final class Decoder extends JaxbAdapter implements BodyAdapter.Decoder { - Decoder(JaxbBindingFactory factory) { super(factory); } @@ -103,9 +101,9 @@ public BodySubscriber toObject(TypeRef objectType, @Nullable MediaType requireNonNull(objectType); requireSupport(objectType); requireCompatibleOrNull(mediaType); - Class elementClass = objectType.exactRawType(); - Charset charset = charsetOrNull(mediaType); - Unmarshaller unmarshaller = createUnmarshallerUnchecked(elementClass); + var elementClass = objectType.exactRawType(); + var charset = charsetOrNull(mediaType); + var unmarshaller = createUnmarshallerUnchecked(elementClass); return BodySubscribers.mapping( BodySubscribers.ofByteArray(), bytes -> @@ -118,9 +116,9 @@ public BodySubscriber> toDeferredObject( requireNonNull(objectType); requireSupport(objectType); requireCompatibleOrNull(mediaType); - Class elementClass = objectType.exactRawType(); - Charset charset = charsetOrNull(mediaType); - Unmarshaller unmarshaller = createUnmarshallerUnchecked(elementClass); + var elementClass = objectType.exactRawType(); + var charset = charsetOrNull(mediaType); + var unmarshaller = createUnmarshallerUnchecked(elementClass); return BodySubscribers.mapping( BodySubscribers.ofInputStream(), in -> () -> unmarshalValue(elementClass, unmarshaller, in, charset)); @@ -133,7 +131,7 @@ private T unmarshalValue( @Nullable Charset charset) { try { // If the charset is known from the media type, use it for a Reader - // to avoid the overhead of having to infer it from the document + // to avoid the overhead of having to infer it from the document. return elementClass.cast( charset != null ? unmarshaller.unmarshal(new InputStreamReader(in, charset)) diff --git a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbAdapterFactory.java b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbAdapterFactory.java index 5dd089e5f..4137a18c9 100644 --- a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbAdapterFactory.java +++ b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbAdapterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,7 +27,6 @@ /** Creates {@link com.github.mizosoft.methanol.BodyAdapter} implementations for XML using JAXB. */ public class JaxbAdapterFactory { - private JaxbAdapterFactory() {} /** Returns a new {@code Encoder} using the default caching factory. */ diff --git a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbBindingFactory.java b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbBindingFactory.java index 603b8e709..e2022cdaa 100644 --- a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbBindingFactory.java +++ b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbBindingFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,9 +26,10 @@ import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; -/** Creates new {@link Marshaller} or {@link Unmarshaller} on demand for use by an adapter. */ +/** + * Creates new {@link Marshaller} or {@link Unmarshaller} objects on demand for use by an adapter. + */ public interface JaxbBindingFactory { - /** Returns a new {@code Marshaller} for encoding an object of the given class. */ Marshaller createMarshaller(Class boundClass) throws JAXBException; @@ -36,8 +37,8 @@ public interface JaxbBindingFactory { Unmarshaller createUnmarshaller(Class boundClass) throws JAXBException; /** - * Returns a default {@code JaxbBindingFactory} that creates and caches {@code JAXBContexts} for - * each requested type. + * Returns a new {@code JaxbBindingFactory} that creates and caches {@code JAXBContexts} for each + * requested type. */ static JaxbBindingFactory create() { return new CachingJaxbBindingFactory(); diff --git a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/UncheckedJaxbException.java b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/UncheckedJaxbException.java index ed873e14d..09fa970f5 100644 --- a/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/UncheckedJaxbException.java +++ b/methanol-jaxb/src/main/java/com/github/mizosoft/methanol/adapter/jaxb/UncheckedJaxbException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,7 +26,6 @@ /** Unchecked wrapper over a {@link JAXBException}. */ public class UncheckedJaxbException extends RuntimeException { - /** Creates a new {@code UncheckedJaxbException} with the given cause. */ public UncheckedJaxbException(JAXBException cause) { super(cause); diff --git a/methanol-jaxb/src/main/java/module-info.java b/methanol-jaxb/src/main/java/module-info.java index 538a4beae..a5183f12c 100644 --- a/methanol-jaxb/src/main/java/module-info.java +++ b/methanol-jaxb/src/main/java/module-info.java @@ -1,31 +1,11 @@ -/* - * Copyright (c) 2019, 2020 Moataz Abdelnasser - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ +import com.github.mizosoft.methanol.BodyAdapter; +import com.github.mizosoft.methanol.adapter.jaxb.JaxbAdapterFactory; /** - * Provides {@link com.github.mizosoft.methanol.BodyAdapter.Encoder} and {@link - * com.github.mizosoft.methanol.BodyAdapter.Decoder} implementations for XML using JAXB. Note that, - * for the sake of configurability, the adapters are not service-provided by default. You will need - * to explicitly declare service-providers that delegate to the instances created by {@link - * com.github.mizosoft.methanol.adapter.jaxb.JaxbAdapterFactory}. + * Provides {@link BodyAdapter.Encoder} and {@link BodyAdapter.Decoder} implementations for XML + * using JAXB. Note that, for the sake of configurability, the adapters are not service-provided by + * default. You will need to explicitly declare service-providers that delegate to the instances + * created by {@link JaxbAdapterFactory}. */ module methanol.adapter.jaxb { requires transitive methanol; diff --git a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/CachingJaxbBindingFactoryTest.java b/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/CachingJaxbBindingFactoryTest.java index 0d6cde958..018285d34 100644 --- a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/CachingJaxbBindingFactoryTest.java +++ b/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/CachingJaxbBindingFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,15 +24,9 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; class CachingJaxbBindingFactoryTest { - @BeforeAll - static void registerJaxbImplementation() { - JaxbUtils.registerImplementation(); - } - @Test void cachesContextsForSameType() { var factory = new CachingJaxbBindingFactory(); diff --git a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbDecoderTest.java b/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbDecoderTest.java index fac95da9f..2e29bd8c7 100644 --- a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbDecoderTest.java +++ b/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbDecoderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,15 +27,9 @@ import static java.nio.charset.StandardCharsets.UTF_16; import com.github.mizosoft.methanol.testing.TestException; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; class JaxbDecoderTest { - @BeforeAll - static void registerJaxbImplementation() { - JaxbUtils.registerImplementation(); - } - @Test void compatibleMediaTypes() { verifyThat(createDecoder()) diff --git a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbDeferredDecoderTest.java b/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbDeferredDecoderTest.java index dc2096dc6..e19b6a1bb 100644 --- a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbDeferredDecoderTest.java +++ b/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbDeferredDecoderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,15 +27,9 @@ import static java.nio.charset.StandardCharsets.UTF_16; import com.github.mizosoft.methanol.testing.TestException; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; class JaxbDeferredDecoderTest { - @BeforeAll - static void registerJaxbImplementation() { - JaxbUtils.registerImplementation(); - } - @Test void deserialize() { verifyThat(createDecoder()) diff --git a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbEncoderTest.java b/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbEncoderTest.java index 6b1a3f2eb..eb03d58cf 100644 --- a/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbEncoderTest.java +++ b/methanol-jaxb/src/test/java/com/github/mizosoft/methanol/adapter/jaxb/JaxbEncoderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Moataz Abdelnasser + * Copyright (c) 2024 Moataz Abdelnasser * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -30,15 +30,9 @@ import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; class JaxbEncoderTest { - @BeforeAll - static void registerJaxbImplementation() { - JaxbUtils.registerImplementation(); - } - @Test void compatibleMediaTypes() { verifyThat(createEncoder()) @@ -61,7 +55,8 @@ void incompatibleMediaTypes() { void serialize() { verifyThat(createEncoder()) .converting(new Point(1, 2)) - .succeedsWith(""); + .succeedsWith( + ""); } @Test @@ -70,8 +65,8 @@ void serializeWithUtf16() { .converting(new Point(1, 2)) .withMediaType("application/xml; charset=utf-16") .succeedsWith( - "" // MediaType lower-cases the charset - + "", UTF_16); + "", + UTF_16); } @Test @@ -79,47 +74,48 @@ void serializeList() { verifyThat(createEncoder()) .converting(new PointList(new Point(1, 2), new Point(3, 4))) .succeedsWith( - "" - + "" - + "" - + "" - + ""); + "" + + "" + + "" + + "" + + ""); } @Test - void serializeWithFormatedXml() { - var customFactory = new JaxbBindingFactory() { - private final JaxbBindingFactory delegate = JaxbBindingFactory.create(); - - /** Create a Marshaller with pretty printing. */ - @Override public Marshaller createMarshaller(Class boundClass) throws JAXBException { - var marshaller = delegate.createMarshaller(boundClass); - marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); - return marshaller; - } - - @Override public Unmarshaller createUnmarshaller(Class boundClass) { - return fail("unexpected unmarshalling"); - } - }; + void serializeWithFormattedXml() { + var customFactory = + new JaxbBindingFactory() { + private final JaxbBindingFactory delegate = JaxbBindingFactory.create(); + + /** Create a Marshaller with pretty printing. */ + @Override + public Marshaller createMarshaller(Class boundClass) throws JAXBException { + var marshaller = delegate.createMarshaller(boundClass); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + return marshaller; + } + + @Override + public Unmarshaller createUnmarshaller(Class boundClass) { + return fail("unexpected unmarshalling"); + } + }; verifyThat(createEncoder(customFactory)) .converting(new PointList(new Point(1, 2), new Point(3, 4))) .succeedsWithNormalizingLineEndings( - "\n" - + "\n" - + " \n" - + " \n" - + "\n"); + "\n" + + "\n" + + " \n" + + " \n" + + "\n"); } @Test void serializeWithUnsupportedType() { class NotAXmlRootElement {} - verifyThat(createEncoder()) - .converting(new NotAXmlRootElement()) - .isNotSupported(); + verifyThat(createEncoder()).converting(new NotAXmlRootElement()).isNotSupported(); } @Test diff --git a/settings.gradle.kts b/settings.gradle.kts index a516314b0..966e95ee0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,25 +1,3 @@ -/* - * Copyright (c) 2023 Moataz Abdelnasser - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - rootProject.name = "methanol-parent" include("methanol") @@ -29,6 +7,7 @@ include("methanol-jackson") include("methanol-jackson-flux") include("methanol-protobuf") include("methanol-jaxb") +include("methanol-jaxb-jakarta") include("methanol-brotli") include("methanol-blackbox") include("methanol-benchmarks")