diff --git a/bom/pom.xml b/bom/pom.xml index 23dbb1655e..0cd67c4b8c 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -296,6 +296,11 @@ jersey-media-json-processing ${project.version} + + org.glassfish.jersey.media + jersey-media-json-gson + ${project.version} + org.glassfish.jersey.media jersey-media-json-binding diff --git a/media/json-gson/pom.xml b/media/json-gson/pom.xml new file mode 100644 index 0000000000..d1b45ea967 --- /dev/null +++ b/media/json-gson/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + + org.glassfish.jersey.media + project + 2.37-SNAPSHOT + + + jersey-media-json-gson + jar + jersey-media-json-gson + + + Jersey GSON support module. + + + + + + com.sun.istack + istack-commons-maven-plugin + true + + + org.codehaus.mojo + build-helper-maven-plugin + true + + + org.apache.felix + maven-bundle-plugin + true + true + + + org.glassfish.jersey.gson.* + ${javax.annotation.osgi.version},* + + true + + + + + + + + org.glassfish.jersey.core + jersey-common + ${project.version} + + + com.google.code.gson + gson + + + junit + junit + test + + + \ No newline at end of file diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/JsonGsonFeature.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/JsonGsonFeature.java new file mode 100644 index 0000000000..9e5cc55658 --- /dev/null +++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/JsonGsonFeature.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.gson; + +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; + +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.gson.internal.JsonGsonAutoDiscoverable; +import org.glassfish.jersey.gson.internal.JsonGsonProvider; +import org.glassfish.jersey.internal.InternalProperties; +import org.glassfish.jersey.internal.util.PropertiesHelper; +/** + * Feature used to register Gson providers. + *

+ * The Feature is automatically enabled when {@link JsonGsonAutoDiscoverable} is on classpath. + * Default GSON configuration obtained by calling {@code GsonBuilder.create()} is used. + *

+ * Custom configuration, if required, can be achieved by implementing custom {@link javax.ws.rs.ext.ContextResolver} and + * registering it as a provider into JAX-RS runtime: + *

+ * @Provider
+ * @class GsonContextResolver implements ContextResolver<Gson> {
+ *      @Override
+ *      public Gson getContext(Class type) {
+ *          GsonBuilder builder = new GsonBuilder();
+ *          // add custom configuration
+ *          return builder.create();
+ *      }
+ * }
+ * 
+ * + */ +public class JsonGsonFeature implements Feature { + + private static final String JSON_FEATURE = JsonGsonFeature.class.getSimpleName(); + + @Override + public boolean configure(final FeatureContext context) { + final Configuration config = context.getConfiguration(); + + final String jsonFeature = CommonProperties.getValue( + config.getProperties(), + config.getRuntimeType(), + InternalProperties.JSON_FEATURE, JSON_FEATURE, String.class); + + // Other JSON providers registered. + if (!JSON_FEATURE.equalsIgnoreCase(jsonFeature)) { + return false; + } + + // Disable other JSON providers. + context.property(PropertiesHelper.getPropertyNameForRuntime( + InternalProperties.JSON_FEATURE, config.getRuntimeType()), JSON_FEATURE); + + context.register(JsonGsonProvider.class); + + return true; + } +} diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonAutoDiscoverable.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonAutoDiscoverable.java new file mode 100644 index 0000000000..0456439a8a --- /dev/null +++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonAutoDiscoverable.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.gson.internal; + +import javax.annotation.Priority; +import javax.ws.rs.core.FeatureContext; + +import org.glassfish.jersey.gson.JsonGsonFeature; +import org.glassfish.jersey.internal.spi.AutoDiscoverable; +import org.glassfish.jersey.internal.spi.ForcedAutoDiscoverable; + +/** + * {@link ForcedAutoDiscoverable} registering {@link JsonGsonFeature} if the feature is not already registered. + *

+ * + * @see JsonGsonFeature + */ +@Priority(AutoDiscoverable.DEFAULT_PRIORITY - 210) +public class JsonGsonAutoDiscoverable implements ForcedAutoDiscoverable { + + @Override + public void configure(FeatureContext context) { + if (!context.getConfiguration().isRegistered(JsonGsonFeature.class)) { + context.register(JsonGsonFeature.class); + } + } +} diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java new file mode 100644 index 0000000000..3799af1ebc --- /dev/null +++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.gson.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.Consumes; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NoContentException; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; +import javax.ws.rs.ext.Providers; + +import org.glassfish.jersey.gson.LocalizationMessages; +import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider; +import org.glassfish.jersey.message.internal.EntityInputStream; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Entity provider (reader and writer) for Gson. + * + */ +@Provider +@Produces({"application/json", "text/json", "*/*"}) +@Consumes({"application/json", "text/json", "*/*"}) +public class JsonGsonProvider extends AbstractMessageReaderWriterProvider { + + private static final String JSON = "json"; + private static final String PLUS_JSON = "+json"; + + private Providers providers; + + public JsonGsonProvider(@Context Providers providers) { + this.providers = providers; + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return supportsMediaType(mediaType); + } + + @Override + public Object readFrom(Class type, Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream) throws IOException, WebApplicationException { + EntityInputStream entityInputStream = new EntityInputStream(entityStream); + entityStream = entityInputStream; + if (entityInputStream.isEmpty()) { + throw new NoContentException(LocalizationMessages.ERROR_GSON_EMPTYSTREAM()); + } + Gson gson = getGson(type); + try { + return gson.fromJson(new InputStreamReader(entityInputStream, + AbstractMessageReaderWriterProvider.getCharset(mediaType)), genericType); + } catch (Exception e) { + throw new ProcessingException(LocalizationMessages.ERROR_GSON_DESERIALIZATION(), e); + } + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return supportsMediaType(mediaType); + } + + @Override + public void writeTo(Object o, Class type, Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) throws IOException, WebApplicationException { + Gson gson = getGson(type); + try { + entityStream.write(gson.toJson(o).getBytes(AbstractMessageReaderWriterProvider.getCharset(mediaType))); + entityStream.flush(); + } catch (Exception e) { + throw new ProcessingException(LocalizationMessages.ERROR_GSON_SERIALIZATION(), e); + } + } + + private Gson getGson(Class type) { + final ContextResolver contextResolver = providers.getContextResolver(Gson.class, MediaType.APPLICATION_JSON_TYPE); + if (contextResolver != null) { + return contextResolver.getContext(type); + } else { + return GsonSingleton.INSTANCE.getInstance(); + } + } + + /** + * @return true for all media types of the pattern */json and + * */*+json. + */ + private static boolean supportsMediaType(final MediaType mediaType) { + return mediaType.getSubtype().equals(JSON) || mediaType.getSubtype().endsWith(PLUS_JSON); + } + + private enum GsonSingleton { + INSTANCE; + + // Thread-safe + private Gson gsonInstance; + + Gson getInstance() { + return gsonInstance; + } + + GsonSingleton() { + this.gsonInstance = new GsonBuilder().create(); + } + } +} diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/package-info.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/package-info.java new file mode 100644 index 0000000000..12e6b90587 --- /dev/null +++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey classes supporting JSON marshalling and unmarshalling using GSON. + */ +package org.glassfish.jersey.gson; diff --git a/media/json-gson/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.ForcedAutoDiscoverable b/media/json-gson/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.ForcedAutoDiscoverable new file mode 100644 index 0000000000..7f4e0ee8b1 --- /dev/null +++ b/media/json-gson/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.ForcedAutoDiscoverable @@ -0,0 +1 @@ +org.glassfish.jersey.gson.internal.JsonGsonAutoDiscoverable diff --git a/media/json-gson/src/main/resources/org/glassfish/jersey/gson/localization.properties b/media/json-gson/src/main/resources/org/glassfish/jersey/gson/localization.properties new file mode 100644 index 0000000000..b2ea26adc0 --- /dev/null +++ b/media/json-gson/src/main/resources/org/glassfish/jersey/gson/localization.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +error.gson.serialization=Error writing GSON serialized object. +error.gson.deserialization=Error deserializing object from entity stream. +error.gson.emptystream=GSON cannot parse empty input stream. diff --git a/media/json-gson/src/test/java/org/glassfish/jersey/gson/internal/JsonGsonProviderTest.java b/media/json-gson/src/test/java/org/glassfish/jersey/gson/internal/JsonGsonProviderTest.java new file mode 100644 index 0000000000..c7884d6299 --- /dev/null +++ b/media/json-gson/src/test/java/org/glassfish/jersey/gson/internal/JsonGsonProviderTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.gson.internal; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.NoContentException; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Providers; + +import org.junit.Test; + +public class JsonGsonProviderTest { + + @Test(expected = NoContentException.class) + public void shouldThrowNoContentException() throws IOException { + Providers providers = new EmptyProviders(); + MessageBodyReader mbr = (MessageBodyReader) new JsonGsonProvider(providers); + mbr.readFrom(Foo.class, Foo.class, new Annotation[0], APPLICATION_JSON_TYPE, + new MultivaluedHashMap<>(), new ByteArrayInputStream(new byte[0])); + } + + private static final class Foo { + // no members + } + + private static final class EmptyProviders implements Providers { + + @Override + public final MessageBodyReader getMessageBodyReader(final Class type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return null; + } + + @Override + public final MessageBodyWriter getMessageBodyWriter(final Class type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return null; + } + + @Override + public final ExceptionMapper getExceptionMapper(final Class type) { + return null; + } + + @Override + public final ContextResolver getContextResolver(final Class contextType, final MediaType mediaType) { + return null; + } + + } + +} diff --git a/media/pom.xml b/media/pom.xml index 20fc3b49b0..4f0d77f237 100644 --- a/media/pom.xml +++ b/media/pom.xml @@ -38,6 +38,7 @@ jaxb json-binding + json-gson json-jackson json-jackson1 json-jettison diff --git a/pom.xml b/pom.xml index 2466f40215..4047aa127c 100644 --- a/pom.xml +++ b/pom.xml @@ -2061,6 +2061,12 @@ yasson ${yasson.version} + + + com.google.code.gson + gson + ${gson.version} + io.opentracing @@ -2205,5 +2211,6 @@ 3.0.1 4.0.4 1.0.11 + 2.9.0 diff --git a/tests/e2e-server/pom.xml b/tests/e2e-server/pom.xml index 6b8f3aa732..2650a3d27e 100644 --- a/tests/e2e-server/pom.xml +++ b/tests/e2e-server/pom.xml @@ -100,6 +100,11 @@ jersey-media-json-processing test + + org.glassfish.jersey.media + jersey-media-json-gson + test + org.glassfish.jersey.ext jersey-bean-validation diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonCustomTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonCustomTest.java new file mode 100644 index 0000000000..3d9f0850f6 --- /dev/null +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonCustomTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.server; + +import static org.junit.Assert.assertEquals; + +import java.util.Date; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +import org.glassfish.jersey.gson.JsonGsonFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class GsonCustomTest extends JerseyTest { + + private static final Date date = new Date(0); + + @Path("/test") + public static class Resource { + + @GET + @Consumes("application/json") + public Date get() { + return date; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(Resource.class).register(JsonGsonFeature.class).register(GsonContextResolver.class); + } + + @Test + public void get() { + Response response = target("/test").request().get(); + assertEquals(200, response.getStatus()); + String obj = response.readEntity(String.class); + assertEquals("\"1970\"", obj); + } + + @Provider + public static class GsonContextResolver implements ContextResolver { + @Override + public Gson getContext(Class type) { + GsonBuilder builder = new GsonBuilder(); + builder.setDateFormat("yyyy"); + return builder.create(); + } + } +} diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonDefaultTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonDefaultTest.java new file mode 100644 index 0000000000..724a32864b --- /dev/null +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonDefaultTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.server; + +import static org.junit.Assert.assertEquals; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.gson.JsonGsonFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +public class GsonDefaultTest extends JerseyTest { + + @Path("/test") + public static class Resource { + + @POST + @Consumes("application/json") + @Produces("application/json") + public Obj post(Obj entity) { + entity.setValue("bar"); + return entity; + } + + @GET + @Consumes("application/json") + public Obj get() { + Obj entity = new Obj(); + entity.setValue("get"); + return entity; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(Resource.class) + .register(JsonGsonFeature.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(JsonGsonFeature.class); + } + + @Test + public void get() { + Response response = target("/test").request().get(); + assertEquals(200, response.getStatus()); + Obj obj = response.readEntity(Obj.class); + assertEquals("get", obj.getValue()); + } + + @Test + public void post() { + Obj obj = new Obj(); + obj.setValue("foo"); + Response response = target("/test").request().post(Entity.json(obj)); + assertEquals(200, response.getStatus()); + obj = response.readEntity(Obj.class); + assertEquals("bar", obj.getValue()); + } + + public static class Obj { + private String value; + + public String getValue() { + return value; + } + public void setValue(String value) { + this.value = value; + } + } +}