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