From 6d4b3fe68be95c519413978ff7dbc642d9949ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Rou=C3=A9l?= Date: Mon, 28 Jul 2025 19:46:48 +0200 Subject: [PATCH] Add support for custom GSON type adapter factories It is not currently possible to add custom Gson type adapters in a modular way at runtime. Adding them requires manually instantiating GsonSupport, which undermines the modular approach of extending Helidon with media type implementations. To resolve this, I have added the ability to add custom TypeAdapterFactory instances as service providers. These providers are loaded during the initialization of GsonSupport by the GsonMediaSupportProvider using Java's ServiceLoader API. This allows for a more flexible and modular way to extend Gson's functionality within Helidon. --- .../helidon/http/media/gson/GsonSupport.java | 8 +++ .../http/media/gson/GsonSupportTest.java | 61 +++++++++++++++++ ...GsonSupportTestBookTypeAdapterFactory.java | 66 +++++++++++++++++++ .../media/gson/src/test/java/my/pkg/Book.java | 4 ++ .../java/my/pkg/BookTypeAdapterFactory.java | 49 ++++++++++++++ .../com.google.gson.TypeAdapterFactory | 1 + 6 files changed, 189 insertions(+) create mode 100644 http/media/gson/src/test/java/io/helidon/http/media/gson/GsonSupportTest.java create mode 100644 http/media/gson/src/test/java/io/helidon/http/media/gson/GsonSupportTestBookTypeAdapterFactory.java create mode 100644 http/media/gson/src/test/java/my/pkg/Book.java create mode 100644 http/media/gson/src/test/java/my/pkg/BookTypeAdapterFactory.java create mode 100644 http/media/gson/src/test/resources/META-INF/services/com.google.gson.TypeAdapterFactory diff --git a/http/media/gson/src/main/java/io/helidon/http/media/gson/GsonSupport.java b/http/media/gson/src/main/java/io/helidon/http/media/gson/GsonSupport.java index 0d0dac5f5e1..dd0258e9498 100644 --- a/http/media/gson/src/main/java/io/helidon/http/media/gson/GsonSupport.java +++ b/http/media/gson/src/main/java/io/helidon/http/media/gson/GsonSupport.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Objects; +import java.util.ServiceLoader; import java.util.function.Consumer; import io.helidon.builder.api.Prototype; @@ -34,6 +35,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapterFactory; import static io.helidon.http.HeaderValues.CONTENT_TYPE_JSON; @@ -82,6 +84,12 @@ public static MediaSupport create(Config config, String name) { Objects.requireNonNull(config, "Config must not be null"); Objects.requireNonNull(name, "Name must not be null"); + GsonBuilder gsonBuilder = new GsonBuilder(); + // Enable the registering of custom type adapters by using service providers for TypeAdapterFactory. + for (var factory : ServiceLoader.load(TypeAdapterFactory.class)) { + gsonBuilder.registerTypeAdapterFactory(factory); + } + Gson gson = gsonBuilder.create(); return builder() .name(name) .config(config) diff --git a/http/media/gson/src/test/java/io/helidon/http/media/gson/GsonSupportTest.java b/http/media/gson/src/test/java/io/helidon/http/media/gson/GsonSupportTest.java new file mode 100644 index 00000000000..9efb9c9b017 --- /dev/null +++ b/http/media/gson/src/test/java/io/helidon/http/media/gson/GsonSupportTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.http.media.gson; + +import io.helidon.common.GenericType; +import io.helidon.common.config.Config; +import io.helidon.http.WritableHeaders; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class GsonSupportTest { + + record Book(String title, int pages) { + } + + @Test + void test() { + var support = GsonSupport.create(Config.empty(), "gson"); + var headers = WritableHeaders.create(); + var type = GenericType.create(Book.class); + var outputStream = new ByteArrayOutputStream(); + var instance = new Book("some-title", 123); + + support.writer(type, headers) + .supplier() + .get() + .write(type, instance, outputStream, headers); + + assertThat(GsonSupportTestBookTypeAdapterFactory.writeCount.get(), is(1)); + + Book sanity = support.reader(type, headers) + .supplier() + .get() + .read(type, new ByteArrayInputStream(outputStream.toByteArray()), headers); + + assertThat(GsonSupportTestBookTypeAdapterFactory.readCount.get(), is(1)); + + assertThat(sanity.title(), is("some-title")); + assertThat(sanity.pages(), is(123)); + assertThat(sanity, is(instance)); + } +} diff --git a/http/media/gson/src/test/java/io/helidon/http/media/gson/GsonSupportTestBookTypeAdapterFactory.java b/http/media/gson/src/test/java/io/helidon/http/media/gson/GsonSupportTestBookTypeAdapterFactory.java new file mode 100644 index 00000000000..4c5d3b0f45f --- /dev/null +++ b/http/media/gson/src/test/java/io/helidon/http/media/gson/GsonSupportTestBookTypeAdapterFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.http.media.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +public class GsonSupportTestBookTypeAdapterFactory implements TypeAdapterFactory { + + static final AtomicInteger readCount = new AtomicInteger(0); + static final AtomicInteger writeCount = new AtomicInteger(0); + + private static final TypeAdapter instance = new TypeAdapter() { + @Override + public void write(JsonWriter writer, GsonSupportTest.Book book) throws IOException { + writer.beginObject(); + writer.name("title"); + writer.value(book.title()); + writer.name("pages"); + writer.value(book.pages()); + writer.endObject(); + writeCount.incrementAndGet(); + } + + @Override + public GsonSupportTest.Book read(JsonReader reader) throws IOException { + reader.beginObject(); + reader.nextName(); + var title = reader.nextString(); + reader.nextName(); + var pages = reader.nextInt(); + reader.endObject(); + readCount.incrementAndGet(); + return new GsonSupportTest.Book(title, pages); + } + }; + + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (typeToken.getRawType().isAssignableFrom(GsonSupportTest.Book.class)) { + return instance; + } + return null; + } +} diff --git a/http/media/gson/src/test/java/my/pkg/Book.java b/http/media/gson/src/test/java/my/pkg/Book.java new file mode 100644 index 00000000000..284a054f345 --- /dev/null +++ b/http/media/gson/src/test/java/my/pkg/Book.java @@ -0,0 +1,4 @@ +package my.pkg; + +public record Book(String title, int pages) { +} \ No newline at end of file diff --git a/http/media/gson/src/test/java/my/pkg/BookTypeAdapterFactory.java b/http/media/gson/src/test/java/my/pkg/BookTypeAdapterFactory.java new file mode 100644 index 00000000000..0aa97179c46 --- /dev/null +++ b/http/media/gson/src/test/java/my/pkg/BookTypeAdapterFactory.java @@ -0,0 +1,49 @@ +package my.pkg; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +public class BookTypeAdapterFactory implements TypeAdapterFactory { + + private static final TypeAdapter instance = new TypeAdapter<>() { + @Override + public void write(JsonWriter writer, Book book) throws IOException { + writer.beginObject(); + writer.name("title"); + writer.value(book.title()); + writer.name("pages"); + writer.value(book.pages()); + writer.endObject(); + } + + @Override + public Book read(JsonReader reader) throws IOException { + reader.beginObject(); + String title = null; + int pages = 0; + while (reader.hasNext()) { + switch (reader.nextName()) { + case "title" -> title = reader.nextString(); + case "pages" -> pages = reader.nextInt(); + default -> reader.skipValue(); + } + } + reader.endObject(); + return new Book(title, pages); + } + }; + + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (typeToken.getRawType().isAssignableFrom(Book.class)) { + return (TypeAdapter) instance; + } + return null; + } +} \ No newline at end of file diff --git a/http/media/gson/src/test/resources/META-INF/services/com.google.gson.TypeAdapterFactory b/http/media/gson/src/test/resources/META-INF/services/com.google.gson.TypeAdapterFactory new file mode 100644 index 00000000000..2bca4bb4ff5 --- /dev/null +++ b/http/media/gson/src/test/resources/META-INF/services/com.google.gson.TypeAdapterFactory @@ -0,0 +1 @@ +io.helidon.http.media.gson.GsonSupportTestBookTypeAdapterFactory