diff --git a/NOTICE.md b/NOTICE.md index 471ce8d412f..61bbace699e 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -70,11 +70,16 @@ Javassist Version 3.30.2-GA * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. -Jackson JAX-RS Providers Version 2.19.1 +Jackson JAX-RS Providers Version 2.20.1 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2024 FasterXML, LLC. All rights reserved unless otherwise indicated. +Jackson Jakarta RS Providers Version 3.0.4 +* License: Apache License, 2.0 +* Project: https://github.com/FasterXML/jackson-jakarta-rs-providers +* Copyright: (c) 2009-2025 FasterXML, LLC. All rights reserved unless otherwise indicated. + jQuery v3.7.1 * License: jquery.org/license * Project: jquery.org diff --git a/bom/pom.xml b/bom/pom.xml index 095579f3534..53f94cda726 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -312,6 +312,11 @@ jersey-media-json-jackson ${project.version} + + org.glassfish.jersey.media + jersey-media-json-jackson3 + ${project.version} + org.glassfish.jersey.media jersey-media-json-jettison diff --git a/bundles/apidocs/pom.xml b/bundles/apidocs/pom.xml index 2b41cd87269..0f77d11e568 100644 --- a/bundles/apidocs/pom.xml +++ b/bundles/apidocs/pom.xml @@ -1,7 +1,7 @@ + diff --git a/etc/config/copyright-exclude b/etc/config/copyright-exclude index 9c4e32df484..7db39d9967f 100644 --- a/etc/config/copyright-exclude +++ b/etc/config/copyright-exclude @@ -85,6 +85,7 @@ build.readme /core-server/src/main/java/org/glassfish/jersey/server/internal/monitoring/core/UniformTimeValuesSnapshot.java /core-server/src/main/java/jersey/repackaged /media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson +/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson /META-INF/NOTICE.md NOTICE.md /ext/cdi/jersey-cdi1x/src/main/java/org/glassfish/jersey/ext/cdi1x/internal/ProcessAllAnnotatedTypes.java diff --git a/examples/NOTICE.md b/examples/NOTICE.md index c6361cca1a9..4886c1506c5 100644 --- a/examples/NOTICE.md +++ b/examples/NOTICE.md @@ -71,11 +71,16 @@ Javassist Version 3.30.2-GA * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. -Jackson JAX-RS Providers Version 2.19.1 +Jackson JAX-RS Providers Version 2.20.1 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. +Jackson Jakarta RS Providers Version 3.0.4 +* License: Apache License, 2.0 +* Project: https://github.com/FasterXML/jackson-jakarta-rs-providers +* Copyright: (c) 2009-2025 FasterXML, LLC. All rights reserved unless otherwise indicated. + jQuery v3.7.1 * License: jquery.org/license * Project: jquery.org diff --git a/examples/json-jackson3/README.MD b/examples/json-jackson3/README.MD new file mode 100644 index 00000000000..8ad8506b6ca --- /dev/null +++ b/examples/json-jackson3/README.MD @@ -0,0 +1,91 @@ +[//]: # " Copyright (c) 2015, 2020, 2026 Oracle and/or its affiliates. All rights reserved. " +[//]: # " " +[//]: # " This program and the accompanying materials are made available under the " +[//]: # " terms of the Eclipse Distribution License v. 1.0, which is available at " +[//]: # " http://www.eclipse.org/org/documents/edl-v10.php. " +[//]: # " " +[//]: # " SPDX-License-Identifier: BSD-3-Clause " + +Jackson JAX-RS JSON Provider Example +==================================== + +This example demonstrates how to produce/consume JSON representations +from Java objects. This applies not only to JAXB beans, as shown in the +`json-from-jaxb` example but also to ordinary, un-annotated, POJOs. + +This example hosts three simple read-only resources: One provides an +example of using a Jackson JSON provider (registered by the feature +`JacksonFeature` in the `MyApplication` class) instead of using JAXB +(Object->JAXB->JSON) which has some limitations (e.g. empty arrays +in JAXB beans). For this resource the JSON representation is produced by +the Jackson JAX-RS provider, while the XML representation is generated +by JAXB as usual. The second web resource is based on a simple +un-annotated POJO and provides only JSON-based representations: JSON and +JSON with padding (JSONP). The third resource depicts how Jackson and +JAXB annotations could be mixed together within a single POJO. + +Contents +-------- + +The "empty array" web resource is implemented by the `org.glassfish.jersey.examples.jackson3.EmptyArrayResource` class. + +The "non JAXB" web resource is implemented by the `org.glassfish.jersey.examples.jackson3.NonJaxbBeanResource` class. + +Both resources use the default Jackson mapper configuration to serialize JSON +data out and depicts the corner cases, where the Jersey internal, StAX +based, JSON processing can not be utilized. + +The `org.glassfish.jersey.examples.jackson3.CombinedAnnotationResource` +class is used to show how JAXB and Jackson annotations could be combined +together in one Java bean for JSON serialization. See the + +`org.glassfish.jersey.examples.jackson3.MyJsonMapperProvider` class +where Jackson specific options are used to set up the desired JSON +serialization configuration. + +The mapping of the URI path space is presented in the following table: + +URI path | Resource class | HTTP method +---------------------------- | ---------------------------- | ------------- +**_/emptyArrayResource_** | EmptyArrayResource | GET +**_/nonJaxbResource_** | NonJaxbBeanResource | GET +**_/combinedAnnotations_** | CombinedAnnotationResource | GET + +To use Jackson specific configuration options, one can implement a +`ContextResolver` provider. For an example of such an +implementation, see the `MyJsonMapperProvider` class. + +Running the Example +------------------- + +Run the example as follows: + +> mvn clean compile exec:java + +This deploys the example using[Grizzly](https://projects.eclipse.org/projects/ee4j.grizzly) + +A [WADL description](http://wadl.java.net/#spec) may be accessed at the URL: + +- + +The three resources are available at + +- +- +- + +To easily obtain the different output types available, +[cURL](http://curl.haxx.se/) can be used as follows: + +Obtain the JSON output of EmptyArrayResource (use +`-HAccept:application/xml` for XML output): + +> curl -HAccept:application/json http://localhost:8080/jackson/emptyArrayResource + +Obtain the JSON output of NonJaxbBeanResource (use `-HAccept:application/javascript` for JSONP output): + +> curl -HAccept:application/json http://localhost:8080/jackson/nonJaxbResource + +Obtain the JSON output of CombinedAnnotationResource: + +> curl -HAccept:application/json http://localhost:8080/jackson/combinedAnnotations diff --git a/examples/json-jackson3/pom.xml b/examples/json-jackson3/pom.xml new file mode 100644 index 00000000000..94e40acbc98 --- /dev/null +++ b/examples/json-jackson3/pom.xml @@ -0,0 +1,91 @@ + + + + + 4.0.0 + + + org.glassfish.jersey.examples + project + ${revision} + + + json-jackson3 + jar + jersey-examples-json-jackson + + Jersey JSON with Jackson example. + + + + org.glassfish.jersey.containers + jersey-container-grizzly2-http + + + org.glassfish.jersey.inject + jersey-hk2 + + + org.glassfish.jersey.media + jersey-media-json-jackson3 + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-bundle + pom + test + + + org.glassfish.jersey.test-framework + jersey-test-framework-util + test + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + com.sun.xml.bind + jaxb-osgi + + + + + + + org.codehaus.mojo + exec-maven-plugin + + org.glassfish.jersey.examples.jackson3.App + + + + + + + + pre-release + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + + diff --git a/examples/json-jackson3/src/main/assembly/src.xml b/examples/json-jackson3/src/main/assembly/src.xml new file mode 100644 index 00000000000..66182aa0537 --- /dev/null +++ b/examples/json-jackson3/src/main/assembly/src.xml @@ -0,0 +1,28 @@ + + + + project + + zip + + + + . + + true + + **/target/** + + + + diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/App.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/App.java new file mode 100644 index 00000000000..cb6313606e5 --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/App.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2010, 2019, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import java.io.IOException; +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + + +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; +import org.glassfish.jersey.server.ResourceConfig; + +import org.glassfish.grizzly.http.server.HttpServer; + +/** + * Utility class which creates {@link MyApplication} instances and provides support + * for running this sample from command line. + * + * @author Jakub Podlesak + */ +public class App { + + private static final URI BASE_URI = URI.create("http://localhost:8080/jackson/"); + + public static void main(String[] args) { + try { + System.out.println("JSON with Jackson Jersey Example App"); + + final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(BASE_URI, createApp(), false); + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + server.shutdownNow(); + } + })); + server.start(); + + System.out.println(String.format("Application started.%nStop the application using CTRL+C")); + + Thread.currentThread().join(); + } catch (IOException | InterruptedException ex) { + Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex); + } + + } + + public static ResourceConfig createApp() { + return new MyApplication(); + } +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/CombinedAnnotationBean.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/CombinedAnnotationBean.java new file mode 100644 index 00000000000..70c34cb58f3 --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/CombinedAnnotationBean.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2010, 2022, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import jakarta.xml.bind.annotation.XmlRootElement; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * TODO javadoc. + * + * @author Jakub Podlesak + */ +// Jackson works with javax API, do not change it to jakarta API. +@XmlRootElement(name = "account") +public class CombinedAnnotationBean { + + @JsonProperty("value") + int x; + + public CombinedAnnotationBean(int x) { + this.x = x; + } + + public CombinedAnnotationBean() { + this(15); + } +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/CombinedAnnotationResource.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/CombinedAnnotationResource.java new file mode 100644 index 00000000000..85d27b98d15 --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/CombinedAnnotationResource.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2010, 2020, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * TODO javadoc. + * + * @author Jakub Podlesak + */ +@Path("combinedAnnotations") +public class CombinedAnnotationResource { + + @Produces(MediaType.APPLICATION_JSON) + @GET + public CombinedAnnotationBean getAccount() { + return new CombinedAnnotationBean(12); + } +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/DummyBean.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/DummyBean.java new file mode 100644 index 00000000000..05ad0a46912 --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/DummyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014, 2018, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +/** + * Testing bean for ExceptionMappingTestResource. + * + */ +public class DummyBean { + private int key; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int getKey() { + return key; + } + + public void setKey(int key) { + this.key = key; + } + +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/EmptyArrayBean.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/EmptyArrayBean.java new file mode 100644 index 00000000000..8a8796842dd --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/EmptyArrayBean.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010, 2020, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import jakarta.xml.bind.annotation.XmlRootElement; + +/** + * TODO javadoc. + * + * @author Jakub Podlesak + */ +@XmlRootElement +public class EmptyArrayBean { + + public String[] emtpyArray = new String[0]; +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/EmptyArrayResource.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/EmptyArrayResource.java new file mode 100644 index 00000000000..e6af02e005a --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/EmptyArrayResource.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010, 2020, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * TODO javadoc. + * + * @author Jakub Podlesak + */ +@Path("/emptyArrayResource") +public class EmptyArrayResource { + + // the resource JSON representation will be serialized by Jackson JAX-RS provider, + // while the XML will still be generated by JAXB + @GET + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public EmptyArrayBean getIt() { + return new EmptyArrayBean(); + } +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/ExceptionMappingTestResource.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/ExceptionMappingTestResource.java new file mode 100644 index 00000000000..ed0d8f78809 --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/ExceptionMappingTestResource.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014, 2020, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; + +/** + * Testing bean that accepts JSON for the PUT method. + * + * @author Adam Lindenthal + */ +@Path("parseExceptionTest") +public class ExceptionMappingTestResource { + + @Consumes(MediaType.APPLICATION_JSON) + @PUT + public DummyBean getAccount(DummyBean bean) { + return bean; + } +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/MyApplication.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/MyApplication.java new file mode 100644 index 00000000000..0b5d8ad5d13 --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/MyApplication.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010, 2020, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import org.glassfish.jersey.jackson3.JacksonFeature; +import org.glassfish.jersey.server.ResourceConfig; + +/** + * {@link jakarta.ws.rs.core.Application} descendant. + * + * Used to set resource and providers classes. + * + * @author Jakub Podlesak + */ +public class MyApplication extends ResourceConfig { + public MyApplication() { + super( + EmptyArrayResource.class, + NonJaxbBeanResource.class, + CombinedAnnotationResource.class, + // register Jackson JsonMapper resolver + MyJsonMapperProvider.class, + ExceptionMappingTestResource.class, + JacksonFeature.class + ); + } +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/MyJsonMapperProvider.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/MyJsonMapperProvider.java new file mode 100644 index 00000000000..8e3cfa5f1f5 --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/MyJsonMapperProvider.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010, 2022, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import tools.jackson.databind.json.JsonMapper; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Provider; + +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; +import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; + +/** + * TODO javadoc. + * + * @author Jakub Podlesak + */ +@Provider +public class MyJsonMapperProvider implements ContextResolver { + + final JsonMapper defaultJsonMapper; + final JsonMapper combinedJsonMapper; + + public MyJsonMapperProvider() { + defaultJsonMapper = createDefaultMapper(); + combinedJsonMapper = createCombinedMapper(); + } + + @Override + public JsonMapper getContext(final Class type) { + + if (type == CombinedAnnotationBean.class) { + return combinedJsonMapper; + } else { + return defaultJsonMapper; + } + } + + private static JsonMapper createCombinedMapper() { + return JsonMapper.builder() + .enable(SerializationFeature.WRAP_ROOT_VALUE) + .enable(DeserializationFeature.UNWRAP_ROOT_VALUE) + .annotationIntrospector(createJaxbJacksonAnnotationIntrospector()) + .build(); + } + + private static JsonMapper createDefaultMapper() { + return JsonMapper.builder() + .enable(SerializationFeature.INDENT_OUTPUT) + .build(); + } + + private static AnnotationIntrospector createJaxbJacksonAnnotationIntrospector() { + + final AnnotationIntrospector jaxbIntrospector = new JakartaXmlBindAnnotationIntrospector(); + final AnnotationIntrospector jacksonIntrospector = new JacksonAnnotationIntrospector(); + + return AnnotationIntrospector.pair(jacksonIntrospector, jaxbIntrospector); + } +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/NonJaxbBean.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/NonJaxbBean.java new file mode 100644 index 00000000000..1b3e2ff7cd9 --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/NonJaxbBean.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010, 2019, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +/** + * TODO javadoc. + * + * @author Jakub Podlesak + */ +public class NonJaxbBean { + + private String name = "non-JAXB-bean"; + private String description = "I am not a JAXB bean, just an unannotated POJO"; + private int[] array = {1, 1, 2, 3, 5, 8, 13, 21}; + + public int[] getArray() { + return array; + } + + public void setArray(int[] array) { + this.array = array; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/NonJaxbBeanResource.java b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/NonJaxbBeanResource.java new file mode 100644 index 00000000000..5e9463a6eda --- /dev/null +++ b/examples/json-jackson3/src/main/java/org/glassfish/jersey/examples/jackson3/NonJaxbBeanResource.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010, 2020, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.glassfish.jersey.server.JSONP; + +/** + * @author Jakub Podlesak + */ +@Path("/nonJaxbResource") +public class NonJaxbBeanResource { + + @GET + @JSONP + @Produces({"application/javascript", MediaType.APPLICATION_JSON}) + public NonJaxbBean getSimpleBeanJSONP() { + return new NonJaxbBean(); + } +} diff --git a/examples/json-jackson3/src/test/java/org/glassfish/jersey/examples/jackson3/JacksonTest.java b/examples/json-jackson3/src/test/java/org/glassfish/jersey/examples/jackson3/JacksonTest.java new file mode 100644 index 00000000000..074a6ea5517 --- /dev/null +++ b/examples/json-jackson3/src/test/java/org/glassfish/jersey/examples/jackson3/JacksonTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2010, 2022, 2026 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.jackson3; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson3.JacksonFeature; +import org.glassfish.jersey.message.internal.MediaTypes; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Jakub Podlesak + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class JacksonTest extends JerseyTest { + + @Override + protected ResourceConfig configure() { + enable(TestProperties.LOG_TRAFFIC); + enable(TestProperties.DUMP_ENTITY); + + return App.createApp(); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(new JacksonFeature()).register(MyJsonMapperProvider.class); + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + public void testEmptyArrayPresent() { + WebTarget target = target(); + String responseMsg = target.path("emptyArrayResource").request(MediaType.APPLICATION_JSON).get(String.class); + assertTrue(responseMsg.replaceAll("[ \t]*", "").contains("[]")); + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + public void testJSONPPresent() { + WebTarget target = target(); + String responseMsg = target.path("nonJaxbResource").request("application/javascript").get(String.class); + assertTrue(responseMsg.startsWith("callback(")); + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + public void testJSONDoesNotReflectJSONPWrapper() { + WebTarget target = target(); + String responseMsg = target.path("nonJaxbResource").request("application/json").get(String.class); + assertTrue(!responseMsg.contains("jsonSource")); + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + public void testCombinedAnnotationResource() { + WebTarget target = target(); + String responseMsg = target.path("combinedAnnotations").request("application/json").get(String.class); + assertTrue(responseMsg.contains("account") && responseMsg.contains("value")); + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + public void testEmptyArrayBean() { + WebTarget target = target(); + EmptyArrayBean responseMsg = target.path("emptyArrayResource").request(MediaType.APPLICATION_JSON) + .get(EmptyArrayBean.class); + assertNotNull(responseMsg); + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + public void testCombinedAnnotationBean() { + WebTarget target = target(); + CombinedAnnotationBean responseMsg = target.path("combinedAnnotations").request("application/json") + .get(CombinedAnnotationBean.class); + assertNotNull(responseMsg); + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + @Disabled + // TODO un-ignore once a JSON reader for "application/javascript" is supported + public void testJSONPBean() { + WebTarget target = target(); + NonJaxbBean responseMsg = target.path("nonJaxbResource").request("application/javascript").get(NonJaxbBean.class); + assertNotNull(responseMsg); + } + + /** + * Test if a WADL document is available at the relative path + * "application.wadl". + *

+ */ + @Test + @Execution(ExecutionMode.CONCURRENT) + public void testApplicationWadl() { + WebTarget target = target(); + String serviceWadl = target.path("application.wadl").request(MediaTypes.WADL_TYPE).get(String.class); + + assertTrue(serviceWadl.length() > 0); + } + + /** + * Test, that in case of malformed JSON, the jackson exception mappers will be used and the response will be + * 400 - bad request instead of 500 - server error + */ + @Test + @Execution(ExecutionMode.CONCURRENT) + public void testExceptionMapping() { + enable(TestProperties.LOG_TRAFFIC); + // create a request with invalid json string to cause an exception in Jackson + Response response = target().path("parseExceptionTest").request("application/json") + .put(Entity.entity("Malformed json string.", MediaType.valueOf("application/json"))); + + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } +} diff --git a/examples/pom.xml b/examples/pom.xml index d7c05cb4c64..b189551564d 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -1,7 +1,7 @@ + + + 4.0.0 + + + org.glassfish.jersey.media + project + ${revision} + + + jersey-media-json-jackson3 + jar + jersey-media-json-jackson3 + + + Jersey JSON Jackson (3.x) entity providers support module. + + + + + EPL 2.0 + http://www.eclipse.org/legal/epl-2.0 + repo + Except for Guava, JSR-166 files, Dropwizard Monitoring inspired classes, ASM and Jackson Jakarta RS Providers. + See also https://github.com/eclipse-ee4j/jersey/blob/master/NOTICE.md + + + The GNU General Public License (GPL), Version 2, With Classpath Exception + https://www.gnu.org/software/classpath/license.html + repo + Except for Jackson JAX-RS Providers. + See also https://github.com/jersey/jersey/blob/master/NOTICE.md + + + Apache License, 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + Jackson Jakarta RS Providers @ org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs + + + + + + + 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.jackson.* + + ${jakarta.annotation.osgi.version}, + ${jakarta.rest.osgi.version}, + * + + + true + + + + + + + + org.glassfish.jersey.core + jersey-common + ${project.version} + + + jakarta.activation + jakarta.activation-api + + + + + org.glassfish.jersey.ext + jersey-entity-filtering + ${project.version} + + + + + + com.fasterxml.jackson.core + jackson-annotations + + + tools.jackson.core + jackson-databind + + + tools.jackson.module + jackson-module-jakarta-xmlbind-annotations + + + jakarta.xml.bind + jakarta.xml.bind-api + + + jakarta.activation + jakarta.activation-api + + + com.fasterxml.jackson.core + jackson-annotations + + + + + tools.jackson.module + jackson-module-no-ctor-deser + test + + + tools.jackson.module + jackson-module-mrbean + test + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + org.junit.jupiter + junit-jupiter + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + ${project.version} + test + + + diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/JacksonFeature.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/JacksonFeature.java new file mode 100644 index 00000000000..c3679cdd40b --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/JacksonFeature.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2012, 2024, 2026 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.jackson3; + +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Feature; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; + +import tools.jackson.core.StreamReadConstraints; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.internal.InternalProperties; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.jackson3.internal.DefaultJacksonXmlBindJsonProvider; +import org.glassfish.jersey.jackson3.internal.FilteringJacksonXmlBindJsonProvider; +import org.glassfish.jersey.jackson3.internal.JacksonFilteringFeature; +import org.glassfish.jersey.jackson3.internal.JakartaRSFeatureBag; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base.DatabindExceptionMapper; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base.StreamReadExceptionMapper; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.JakartaRSFeature; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.JacksonXmlBindJsonProvider; +import org.glassfish.jersey.message.MessageProperties; +import org.glassfish.jersey.message.filtering.EntityFilteringFeature; + + +/** + * Feature used to register Jackson JSON providers. + * + * @author Stepan Kopriva + * @author Michal Gajdos + */ +public class JacksonFeature extends JakartaRSFeatureBag implements Feature { + + /** + * Define whether to use Jackson's exception mappers ore not + * Using them can provide useful information to the user, but it can expose unnecessary information, too. + */ + private final boolean registerExceptionMappers; + + /** + * Overridable Jackon's {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN} value. + */ + private int maxStringLength = StreamReadConstraints.DEFAULT_MAX_STRING_LEN; + + /** + * Default constructor enables registering Jackson's exception mappers + */ + public JacksonFeature() { + this(true); + } + + private JacksonFeature(boolean registerExceptionMappers) { + this.registerExceptionMappers = registerExceptionMappers; + } + + /** + * Create JacksonFeature with working Jackson's exception mappers + * @return JacksonFeature with working Jackson's exception mappers + */ + public static JacksonFeature withExceptionMappers() { + return new JacksonFeature(); + } + + /** + * Create JacksonFeature without registered Jackson's exception mappers + * @return JacksonFeature without registered Jackson's exception mappers + */ + public static JacksonFeature withoutExceptionMappers() { + return new JacksonFeature(false); + } + + /** + *

+ * Sets the {@link MessageProperties#JSON_MAX_STRING_LENGTH} property to a provided value. The property value already + * {@link Configuration configured} takes priority. + *

+ *

+ * Both uses of {@link #maxStringLength(int)} and {@link MessageProperties#JSON_MAX_STRING_LENGTH} override + * StreamReadConstraints defined on Jackson's {@code ObjectMapper's JsonFactory} provided via + * {@link jakarta.ws.rs.ext.ContextResolver ContextResolver<ObjectMapper>}. + *

+ * @param maxStringLength the integer value to override the default Jackson's + * {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN}. + * @return JacksonFeature that has the Jackson's {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN} set. + */ + public JacksonFeature maxStringLength(int maxStringLength) { + this.maxStringLength = maxStringLength; + return this; + } + + /** + * Register {@link JakartaRSFeature} with the Jackson providers. + * @param feature the {@link JakartaRSFeature} to be enabled or disabled. + * @param state {@code true} for enabling the feature, {@code false} for disabling. + * @return JacksonFeature with {@link JakartaRSFeature} registered to be set on a created Jackson provider. + */ + public JacksonFeature jakartaRSFeature(JakartaRSFeature feature, boolean state) { + return super.jakartaRSFeature(feature, state); + } + + private static final String JSON_FEATURE = JacksonFeature.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); + + // Register Jackson. + if (!config.isRegistered(JacksonXmlBindJsonProvider.class)) { + + if (registerExceptionMappers) { + // add the default Jackson exception mappers + context.register(DatabindExceptionMapper.class); + context.register(StreamReadExceptionMapper.class); + } + + if (EntityFilteringFeature.enabled(config)) { + context.register(JacksonFilteringFeature.class); + context.register(FilteringJacksonXmlBindJsonProvider.class, MessageBodyReader.class, MessageBodyWriter.class); + } else { + context.register(DefaultJacksonXmlBindJsonProvider.class, MessageBodyReader.class, MessageBodyWriter.class); + } + } + + if (config.getProperty(MessageProperties.JSON_MAX_STRING_LENGTH) == null) { + context.property(MessageProperties.JSON_MAX_STRING_LENGTH, maxStringLength); + } + + if (hasJakartaRSFeature()) { + context.property(JakartaRSFeatureBag.JAKARTA_RS_FEATURE, this); + } + + return true; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/JakartaRSFeatureJsonMapper.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/JakartaRSFeatureJsonMapper.java new file mode 100644 index 00000000000..a91450ac0c8 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/JakartaRSFeatureJsonMapper.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 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.jackson3; + +import tools.jackson.databind.json.JsonMapper; +import org.glassfish.jersey.jackson3.internal.AbstractJsonMapper; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.JakartaRSFeature; + + +/** + * The Jackson {@link JsonMapper} supporting {@link JakartaRSFeature}s. + */ +public class JakartaRSFeatureJsonMapper extends AbstractJsonMapper { + + public JakartaRSFeatureJsonMapper() { + super(); + } + + /** + * Method for changing state of an on/off {@link JakartaRSFeature} + * features. + */ + public JsonMapper configure(JakartaRSFeature f, boolean state) { + jakartaRSFeatureBag.jakartaRSFeature(f, state); + return this; + } + + /** + * Method for enabling specified {@link JakartaRSFeature}s + * for parser instances this object mapper creates. + */ + public JsonMapper enable(JakartaRSFeature... features) { + if (features != null) { + for (JakartaRSFeature f : features) { + jakartaRSFeatureBag.jakartaRSFeature(f, true); + } + } + return this; + } + + /** + * Method for disabling specified {@link JakartaRSFeature}s + * for parser instances this object mapper creates. + */ + public JsonMapper disable(JakartaRSFeature... features) { + if (features != null) { + for (JakartaRSFeature f : features) { + jakartaRSFeatureBag.jakartaRSFeature(f, false); + } + } + return this; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/AbstractJsonMapper.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/AbstractJsonMapper.java new file mode 100644 index 00000000000..2f14b08e405 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/AbstractJsonMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024, 2026 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.jackson3.internal; + +import tools.jackson.databind.json.JsonMapper; + +/** + * Internal JsonMapper with {@link JakartaRSFeatureBag}. + */ +public abstract class AbstractJsonMapper extends JsonMapper { + protected AbstractJsonMapper() { + + } + protected JakartaRSFeatureBag jakartaRSFeatureBag = new JakartaRSFeatureBag(); +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/DefaultJacksonXmlBindJsonProvider.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/DefaultJacksonXmlBindJsonProvider.java new file mode 100644 index 00000000000..986beaa5a68 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/DefaultJacksonXmlBindJsonProvider.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020, 2025, 2026 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.jackson3.internal; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.Providers; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.jackson3.LocalizationMessages; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.JacksonXmlBindJsonProvider; +import org.glassfish.jersey.message.MessageProperties; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; + +import java.util.logging.Logger; + +/** + * Entity Data provider based on Jackson JSON provider. + */ +@Singleton +public class DefaultJacksonXmlBindJsonProvider extends JacksonXmlBindJsonProvider { + + private static final Logger LOGGER = Logger.getLogger(DefaultJacksonXmlBindJsonProvider.class.getName()); + + private final Configuration commonConfig; + + @Inject + public DefaultJacksonXmlBindJsonProvider(@Context Providers providers, @Context Configuration config) { + super(new JacksonMapperConfigurator(null, + AnnotationIntrospector.pair(new JacksonAnnotationIntrospector(), JaxbHolder.get()), + config)); + + this.commonConfig = config; + _providers = providers; + try { + Object jakartaRSFeatureBagObject = config.getProperty(JakartaRSFeatureBag.JAKARTA_RS_FEATURE); + if ((jakartaRSFeatureBagObject instanceof JakartaRSFeatureBag jakartaRSFeatureBag)) { + jakartaRSFeatureBag.configureJakartaRSFeatures(this); + } + updateDefaultStreamReadConstraints(); + } catch (RuntimeException e) { + // ignore - not configured + LOGGER.fine(LocalizationMessages.ERROR_CONFIGURING(e.getMessage())); + } + } + + @Override + protected JsonMapper _locateMapperViaProvider(Class type, MediaType mediaType) { + JsonMapper mapper = super._locateMapperViaProvider(type, mediaType); + if (mapper instanceof AbstractJsonMapper abstractJsonMapper) { + abstractJsonMapper.jakartaRSFeatureBag.configureJakartaRSFeatures(this); + } + return mapper; + } + + // Is this still necessary with Jackson 3? Seems like an attempt to handle misc Jackson 2.x versions + private void updateDefaultStreamReadConstraints() { + // Priorities 1. property, 2.JacksonFeature#maxStringLength, 3.defaultValue + final Object maxStringLengthObject = commonConfig.getProperty(MessageProperties.JSON_MAX_STRING_LENGTH); + final Integer maxStringLength = PropertiesHelper.convertValue(maxStringLengthObject, Integer.class); + + if (maxStringLength != null && maxStringLength != StreamReadConstraints.DEFAULT_MAX_STRING_LEN) { + StreamReadConstraints.Builder builder = StreamReadConstraints.builder() + .maxStringLength(maxStringLength); + StreamReadConstraints.overrideDefaultStreamReadConstraints(builder.build()); + } + } + + // Simple encapsulation to defer loading of JAXB introspector + private static class JaxbHolder { + public static AnnotationIntrospector get() { + return new JakartaXmlBindAnnotationIntrospector(); + } + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/FilteringJacksonXmlBindJsonProvider.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/FilteringJacksonXmlBindJsonProvider.java new file mode 100644 index 00000000000..3fe61a4be09 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/FilteringJacksonXmlBindJsonProvider.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2015, 2023, 2026 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.jackson3.internal; + +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import jakarta.ws.rs.ext.Providers; + +import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.EndpointConfigBase; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.ObjectWriterInjector; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.ObjectWriterModifier; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.JsonEndpointConfig; +import org.glassfish.jersey.message.filtering.spi.ObjectProvider; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.introspect.Annotated; +import tools.jackson.databind.introspect.AnnotatedClass; +import tools.jackson.databind.introspect.AnnotatedField; +import tools.jackson.databind.introspect.AnnotatedMethod; +import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.ser.FilterProvider; +import tools.jackson.databind.ser.PropertyFilter; + +/** + * Entity Data Filtering provider based on Jackson JSON provider. + * + * @author Michal Gajdos + */ +@Singleton +public final class FilteringJacksonXmlBindJsonProvider extends DefaultJacksonXmlBindJsonProvider { + + private final Provider> provider; + + @Inject + public FilteringJacksonXmlBindJsonProvider(@Context Provider> provider, + @Context Providers providers, + @Context Configuration config) { + super(providers, config); + this.provider = provider; + } + + + @Override + protected JsonEndpointConfig _configForWriting(final JsonMapper mapper, final Annotation[] annotations, + final Class defaultView) { + final AnnotationIntrospector customIntrospector = mapper.serializationConfig().getAnnotationIntrospector(); + // Set the custom (user) introspector to be the primary one. + AnnotationIntrospector forFilteringMapper = AnnotationIntrospector.pair(customIntrospector, + new JacksonAnnotationIntrospector() { + @Override + public Object findFilterId(MapperConfig config, final Annotated a) { + final Object filterId = super.findFilterId(config, a); + + if (filterId != null) { + return filterId; + } + + if (a instanceof AnnotatedMethod) { + final Method method = ((AnnotatedMethod) a).getAnnotated(); + + // Interested only in getters - trying to obtain "field" name from them. + if (ReflectionHelper.isGetter(method)) { + return ReflectionHelper.getPropertyName(method); + } + } + if (a instanceof AnnotatedField || a instanceof AnnotatedClass) { + return a.getName(); + } + + return null; + } + }); + final JsonMapper filteringMapper = mapper.rebuild().annotationIntrospector(forFilteringMapper).build(); + return super._configForWriting(filteringMapper, annotations, defaultView); + } + + @Override + public void writeTo(final Object value, + final Class type, + final Type genericType, + final Annotation[] annotations, + final MediaType mediaType, + final MultivaluedMap httpHeaders, + final OutputStream entityStream) { + final FilterProvider filterProvider = provider.get().getFilteringObject(genericType, true, annotations); + if (filterProvider != null) { + ObjectWriterInjector.set(new FilteringObjectWriterModifier(filterProvider, ObjectWriterInjector.getAndClear())); + } + + super.writeTo(value, type, genericType, annotations, mediaType, httpHeaders, entityStream); + } + + private static final class FilteringObjectWriterModifier extends ObjectWriterModifier { + + private final ObjectWriterModifier original; + private final FilterProvider filterProvider; + + private FilteringObjectWriterModifier(final FilterProvider filterProvider, final ObjectWriterModifier original) { + this.original = original; + this.filterProvider = filterProvider; + } + + @Override + public ObjectWriter modify(final EndpointConfigBase endpoint, + final MultivaluedMap responseHeaders, + final Object valueToWrite, + final ObjectWriter w) throws JacksonException { + final ObjectWriter writer = original == null ? w : original.modify(endpoint, responseHeaders, valueToWrite, w); + final FilterProvider customFilterProvider = writer.getConfig().getFilterProvider(); +//public abstract PropertyFilter findPropertyFilter(SerializationContext ctxt, Object filterId, Object valueToFilter); + // Try the custom (user) filter provider first. + return customFilterProvider == null + ? writer.with(filterProvider) + : writer.with(new FilterProvider() { + @Override + public FilterProvider snapshot() { + return this; + } + + @Override + public PropertyFilter findPropertyFilter(SerializationContext ctxt, + final Object filterId, final Object valueToFilter) { + final PropertyFilter filter = customFilterProvider.findPropertyFilter(ctxt, filterId, valueToFilter); + if (filter != null) { + return filter; + } + + return filterProvider.findPropertyFilter(ctxt, filterId, valueToFilter); + } + }); + } + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonAutoDiscoverable.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonAutoDiscoverable.java new file mode 100644 index 00000000000..0cb3ede03a7 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonAutoDiscoverable.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014, 2020, 2026 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.jackson3.internal; + +import jakarta.ws.rs.core.FeatureContext; + +import jakarta.annotation.Priority; + +import org.glassfish.jersey.internal.spi.AutoDiscoverable; +import org.glassfish.jersey.jackson3.JacksonFeature; + +/** + * {@link AutoDiscoverable} registering {@link JacksonFeature} if the feature is not already registered. + * + * @author Michal Gajdos + */ +@Priority(AutoDiscoverable.DEFAULT_PRIORITY) +public class JacksonAutoDiscoverable implements AutoDiscoverable { + + @Override + public void configure(final FeatureContext context) { + if (!context.getConfiguration().isRegistered(JacksonFeature.class)) { + context.register(JacksonFeature.class); + } + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonFilteringFeature.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonFilteringFeature.java new file mode 100644 index 00000000000..5f30a3b24e0 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonFilteringFeature.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2015, 2024, 2026 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.jackson3.internal; + +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Feature; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.core.GenericType; + +import jakarta.inject.Singleton; + +import org.glassfish.jersey.innate.inject.InternalBinder; +import org.glassfish.jersey.message.filtering.spi.ObjectGraphTransformer; +import org.glassfish.jersey.message.filtering.spi.ObjectProvider; + +import tools.jackson.databind.ser.FilterProvider; + +/** + * {@link Feature} adding support for Entity Data Filtering into Jackson media module. + * + * @author Michal Gajdos + */ +public final class JacksonFilteringFeature implements Feature { + + @Override + public boolean configure(final FeatureContext context) { + final Configuration config = context.getConfiguration(); + + if (!config.isRegistered(Binder.class)) { + context.register(new Binder()); + return true; + } + return false; + } + + private static final class Binder extends InternalBinder { + + @Override + protected void configure() { + bindAsContract(JacksonObjectProvider.class) + // FilteringObjectProvider. + .to(new GenericType>() {}) + // FilteringGraphTransformer. + .to(new GenericType>() {}) + // Scope. + .in(Singleton.class); + } + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonMapperConfigurator.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonMapperConfigurator.java new file mode 100644 index 00000000000..1f9e9e7d006 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonMapperConfigurator.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2022, 2026 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.jackson3.internal; + +import jakarta.ws.rs.core.Configuration; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.jackson3.LocalizationMessages; +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.json.JsonMapper; + +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.JsonMapperConfigurator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import java.util.logging.Logger; + +public class JacksonMapperConfigurator extends JsonMapperConfigurator { + + //do not register JaxbAnnotationModule because it breaks default annotations processing + private static final String[] EXCLUDE_MODULE_NAMES = {"JaxbAnnotationModule", "JakartaXmlBindAnnotationModule"}; + private static final Logger LOGGER = Logger.getLogger(JacksonMapperConfigurator.class.getName()); + + private final Configuration commonConfig; + + public JacksonMapperConfigurator(JsonMapper mapper, AnnotationIntrospector aiOverride, Configuration commonConfig) { + super(mapper, aiOverride); + this.commonConfig = commonConfig; + } + + @Override + protected MapperBuilder _builderWithConfiguration(MapperBuilder mapperBuilder) { + MapperBuilder configuredBuilder = super._builderWithConfiguration(mapperBuilder); + final List modules = filterModules(commonConfig, JsonMapper.Builder::findModules); + return configuredBuilder.removeAllModules().addModules(modules); + } + + private List filterModules(Configuration commonConfig, Supplier> moduleSupplier) { + final String disabledModules = + CommonProperties.getValue(commonConfig.getProperties(), + commonConfig.getRuntimeType(), + CommonProperties.JSON_JACKSON_DISABLED_MODULES, String.class); + final String enabledModules = + CommonProperties.getValue(commonConfig.getProperties(), + commonConfig.getRuntimeType(), + CommonProperties.JSON_JACKSON_ENABLED_MODULES, String.class); + + final List modules = new ArrayList<>(); + try { + modules.addAll(moduleSupplier.get()); + } catch (Throwable e) { + LOGGER.warning(LocalizationMessages.ERROR_MODULES_NOT_LOADED(e.getMessage())); + return modules; + } + + for (String exludeModuleName : EXCLUDE_MODULE_NAMES) { + modules.removeIf(mod -> mod.getModuleName().contains(exludeModuleName)); + } + + if (enabledModules != null && !enabledModules.isEmpty()) { + final List enabledModulesList = Arrays.asList(enabledModules.split(",")); + modules.removeIf(mod -> !enabledModulesList.contains(mod.getModuleName())); + } else if (disabledModules != null && !disabledModules.isEmpty()) { + final List disabledModulesList = Arrays.asList(disabledModules.split(",")); + modules.removeIf(mod -> disabledModulesList.contains(mod.getModuleName())); + } + + return modules; + } + +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonObjectProvider.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonObjectProvider.java new file mode 100644 index 00000000000..55292b3d5a5 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JacksonObjectProvider.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2015, 2024, 2026 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.jackson3.internal; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +import org.glassfish.jersey.message.filtering.spi.AbstractObjectProvider; +import org.glassfish.jersey.message.filtering.spi.EntityGraphProvider; +import org.glassfish.jersey.message.filtering.spi.EntityInspector; +import org.glassfish.jersey.message.filtering.spi.ObjectGraph; +import org.glassfish.jersey.message.filtering.spi.ScopeProvider; + +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import tools.jackson.databind.ser.FilterProvider; +import tools.jackson.databind.ser.PropertyFilter; +import tools.jackson.databind.ser.PropertyWriter; +import tools.jackson.databind.ser.std.SimpleBeanPropertyFilter; + +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Context; + +/** + * @author Michal Gajdos + */ +final class JacksonObjectProvider extends AbstractObjectProvider { + + @Inject + public JacksonObjectProvider(@Context ScopeProvider scopeProvider, + @Context EntityInspector entityInspector, + @Context EntityGraphProvider graphProvider) { + super(scopeProvider, entityInspector, graphProvider); + } + + @Override + public FilterProvider transform(final ObjectGraph graph) { + // Root entity. + final FilteringPropertyFilter root = new FilteringPropertyFilter(graph.getEntityClass(), + graph.getFields(), + createSubfilters(graph.getEntityClass(), graph.getSubgraphs())); + + return new FilteringFilterProvider(root); + } + + private Map createSubfilters(final Class entityClass, + final Map entitySubgraphs) { + final Map subfilters = new HashMap<>(); + + for (final Map.Entry entry : entitySubgraphs.entrySet()) { + final String fieldName = entry.getKey(); + final ObjectGraph graph = entry.getValue(); + + // Subgraph Fields. + final Map subgraphs = graph.getSubgraphs(fieldName); + + Map subSubfilters = new HashMap<>(); + if (!subgraphs.isEmpty()) { + final Class subEntityClass = graph.getEntityClass(); + final Set processed = Collections.singleton(subgraphIdentifier(entityClass, fieldName, subEntityClass)); + + subSubfilters = createSubfilters(fieldName, subEntityClass, subgraphs, processed); + } + + final FilteringPropertyFilter filter = new FilteringPropertyFilter(graph.getEntityClass(), + graph.getFields(fieldName), subSubfilters); + + subfilters.put(fieldName, filter); + } + + return subfilters; + } + + private Map createSubfilters(final String parent, final Class entityClass, + final Map entitySubgraphs, + final Set processed) { + final Map subfilters = new HashMap<>(); + + for (final Map.Entry entry : entitySubgraphs.entrySet()) { + final String fieldName = entry.getKey(); + final ObjectGraph graph = entry.getValue(); + + final String path = parent + "." + fieldName; + + // Subgraph Fields. + final Map subgraphs = graph.getSubgraphs(path); + + final Class subEntityClass = graph.getEntityClass(); + final String processedSubgraph = subgraphIdentifier(entityClass, fieldName, subEntityClass); + + Map subSubfilters = new HashMap<>(); + if (!subgraphs.isEmpty() && !processed.contains(processedSubgraph)) { + // duplicate processed set so that elements in different subtrees aren't skipped (JERSEY-2892) + final Set subProcessed = immutableSetOf(processed, processedSubgraph); + + subSubfilters = createSubfilters(path, subEntityClass, subgraphs, subProcessed); + } + + subfilters.put(fieldName, new FilteringPropertyFilter(graph.getEntityClass(), graph.getFields(path), subSubfilters)); + } + + return subfilters; + } + + private static class FilteringFilterProvider extends FilterProvider { + + private final FilteringPropertyFilter root; + private final Stack stack = new Stack<>(); + + public FilteringFilterProvider(final FilteringPropertyFilter root) { + this.root = root; + } + + public FilteringFilterProvider(final FilteringFilterProvider src) { + this.root = src.root; + this.stack.addAll(src.stack); + } + + @Override + public FilteringFilterProvider snapshot() { + return new FilteringFilterProvider(this); + } + + @Override + public PropertyFilter findPropertyFilter(SerializationContext ctzt, final Object filterId, final Object valueToFilter) { + if (filterId instanceof String) { + final String id = (String) filterId; + + // FilterId should represent a class only in case of root entity is marshalled. + if (id.equals(root.getEntityClass().getName())) { + stack.clear(); + return stack.push(root); + } + + while (!stack.isEmpty()) { + final FilteringPropertyFilter peek = stack.peek(); + final FilteringPropertyFilter subfilter = peek.findSubfilter(id); + + if (subfilter != null) { + stack.push(subfilter); + + // Need special handling for maps here - map keys can be filtered as well so we just say that every key is + // allowed. + if (valueToFilter instanceof Map) { + final Map map = (Map) valueToFilter; + return new FilteringPropertyFilter(Map.class, map.keySet(), + Collections.emptyMap()); + } + return subfilter; + } else { + stack.pop(); + } + } + } + return SimpleBeanPropertyFilter.filterOutAllExcept(); + } + } + + private static final class FilteringPropertyFilter implements PropertyFilter { + + private final Class entityClass; + + private final Set fields; + private final Map subfilters; + + private FilteringPropertyFilter(final Class entityClass, + final Set fields, final Map subfilters) { + this.entityClass = entityClass; + + this.fields = fields; + this.subfilters = subfilters; + } + + private boolean include(final String fieldName) { + return fields.contains(fieldName) || subfilters.containsKey(fieldName); + } + + @Override + public FilteringPropertyFilter snapshot() { + return this; + } + + @Override + public void serializeAsProperty(final Object pojo, + final JsonGenerator jgen, + final SerializationContext ctxt, + final PropertyWriter writer) throws Exception { + if (include(writer.getName())) { + writer.serializeAsProperty(pojo, jgen, ctxt); + } + } + + @Override + public void serializeAsElement(final Object elementValue, + final JsonGenerator jgen, + final SerializationContext ctxt, + final PropertyWriter writer) throws Exception { + if (include(writer.getName())) { + writer.serializeAsElement(elementValue, jgen, ctxt); + } + } + + @Override + public void depositSchemaProperty(final PropertyWriter writer, + final JsonObjectFormatVisitor objectVisitor, + final SerializationContext ctxt) { + if (include(writer.getName())) { + writer.depositSchemaProperty(objectVisitor, ctxt); + } + } + + public FilteringPropertyFilter findSubfilter(final String fieldName) { + return subfilters.get(fieldName); + } + + public Class getEntityClass() { + return entityClass; + } + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JakartaRSFeatureBag.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JakartaRSFeatureBag.java new file mode 100644 index 00000000000..772ba5b89e8 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/JakartaRSFeatureBag.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 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.jackson3.internal; + +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base.ProviderBase; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.JakartaRSFeature; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Internal holder class for {@link JakartaRSFeature} settings and their values. + */ +public class JakartaRSFeatureBag { + protected static final String JAKARTA_RS_FEATURE = "jersey.config.jackson.jakarta.rs.feature"; + + private static class JakartaRSFeatureState { + /* package */ final JakartaRSFeature feature; + /* package */ final boolean state; + public JakartaRSFeatureState(JakartaRSFeature feature, boolean state) { + this.feature = feature; + this.state = state; + } + } + + private Optional> jakartaRSFeature = Optional.empty(); + + public T jakartaRSFeature(JakartaRSFeature feature, boolean state) { + if (!jakartaRSFeature.isPresent()) { + jakartaRSFeature = Optional.of(new ArrayList<>()); + } + jakartaRSFeature.ifPresent(list -> list.add(new JakartaRSFeatureState(feature, state))); + return (T) this; + } + + protected boolean hasJakartaRSFeature() { + return jakartaRSFeature.isPresent(); + } + + /* package */ void configureJakartaRSFeatures(ProviderBase providerBase) { + jakartaRSFeature.ifPresent(list -> list.stream().forEach(state -> providerBase.configure(state.feature, state.state))); + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/annotation/JacksonFeatures.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/annotation/JacksonFeatures.java new file mode 100644 index 00000000000..2078e539df4 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/annotation/JacksonFeatures.java @@ -0,0 +1,39 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.SerializationFeature; + +/** + * Annotation that can be used enable and/or disable various + * features for ObjectReaders and ObjectWriters. + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +@com.fasterxml.jackson.annotation.JacksonAnnotation +public @interface JacksonFeatures +{ + /** + * Deserialization features to enable. + */ + public DeserializationFeature[] deserializationEnable() default { }; + + /** + * Deserialization features to disable. + */ + public DeserializationFeature[] deserializationDisable() default { }; + + /** + * Serialization features to enable. + */ + public SerializationFeature[] serializationEnable() default { }; + + /** + * Serialization features to disable. + */ + public SerializationFeature[] serializationDisable() default { }; +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/annotation/package-info.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/annotation/package-info.java new file mode 100644 index 00000000000..2608684c2ae --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/annotation/package-info.java @@ -0,0 +1,12 @@ +/** + * Package that contains annotations applicable to all content types. + * Currently defined are: + *
    + *
  • {@link org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.annotation.JacksonFeatures} allows + * enabling and/or disabling {@link tools.jackson.databind.DeserializationFeature}s + * and {@link tools.jackson.databind.SerializationFeature}s for individual + * endpoints. + *
  • + *
+ */ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.annotation; diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/DatabindExceptionMapper.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/DatabindExceptionMapper.java new file mode 100644 index 00000000000..c480611aeaf --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/DatabindExceptionMapper.java @@ -0,0 +1,17 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base; + +import tools.jackson.databind.DatabindException; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; + +/** + * Implementation if {@link ExceptionMapper} to send down a "400 Bad Request" + * response in the event that unmappable JSON is received. + */ +public class DatabindExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(DatabindException exception) { + return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build(); + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/ProviderBase.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/ProviderBase.java new file mode 100644 index 00000000000..5842753bf3a --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/ProviderBase.java @@ -0,0 +1,972 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.NoContentException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; + +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.AnnotationBundleKey; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.EndpointConfigBase; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.JakartaRSFeature; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.MapperConfiguratorBase; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.ObjectReaderInjector; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.ObjectReaderModifier; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.ObjectWriterInjector; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.ObjectWriterModifier; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base.util.ClassKey; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonEncoding; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.StreamReadFeature; +import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.Versioned; +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.MappingIterator; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.util.LookupCache; +import tools.jackson.databind.type.TypeFactory; +import tools.jackson.databind.util.SimpleLookupCache; + +public abstract class ProviderBase< + THIS extends ProviderBase, + MAPPER extends ObjectMapper, + EP_CONFIG extends EndpointConfigBase, + MAPPER_CONFIG extends MapperConfiguratorBase +> + implements + MessageBodyReader, + MessageBodyWriter, + Versioned +{ + /** + * This header is useful on Windows, trying to deal with potential XSS attacks. + */ + public final static String HEADER_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options"; + + private final static String NO_CONTENT_MESSAGE = "No content (empty input stream)"; + + /** + * Looks like we need to worry about accidental + * data binding for types we shouldn't be handling. This is + * probably not a very good way to do it, but let's start by + * blacklisting things we are not to handle. + *

+ * (why ClassKey? since plain old Class has no hashCode() defined, + * lookups are painfully slow) + */ + protected final static HashSet DEFAULT_UNTOUCHABLES = new HashSet(); + static { + // First, I/O things (direct matches) + DEFAULT_UNTOUCHABLES.add(new ClassKey(java.io.InputStream.class)); + DEFAULT_UNTOUCHABLES.add(new ClassKey(java.io.Reader.class)); + DEFAULT_UNTOUCHABLES.add(new ClassKey(java.io.OutputStream.class)); + DEFAULT_UNTOUCHABLES.add(new ClassKey(java.io.Writer.class)); + + // then some primitive types + DEFAULT_UNTOUCHABLES.add(new ClassKey(char[].class)); + + // 27-Apr-2012, tatu: Ugh. As per issue #12 (in old tracker) + // better revert this back, to make them untouchable again. + DEFAULT_UNTOUCHABLES.add(new ClassKey(String.class)); + DEFAULT_UNTOUCHABLES.add(new ClassKey(byte[].class)); + } + + /** + * These are classes that we never use for reading + * (never try to deserialize instances of these types). + */ + public final static Class[] DEFAULT_UNREADABLES = new Class[] { + InputStream.class, Reader.class + }; + + /** + * These are classes that we never use for writing + * (never try to serialize instances of these types). + */ + public final static Class[] DEFAULT_UNWRITABLES = new Class[] { + InputStream.class, // as per [Issue#19] + OutputStream.class, Writer.class, + StreamingOutput.class, Response.class + }; + + protected final static int JAKARTA_RS_FEATURE_DEFAULTS = JakartaRSFeature.collectDefaults(); + + /* + /********************************************************************** + /* General configuration + /********************************************************************** + */ + + /** + * Helper object used for encapsulating configuration aspects + * of {@link ObjectMapper} + */ + protected final MAPPER_CONFIG _mapperConfig; + + /** + * Map that contains overrides to default list of untouchable + * types: true meaning that entry is untouchable, + * false that is is not. + */ + protected HashMap _cfgCustomUntouchables; + + /** + * Feature flags set. + */ + protected int _jakartaRSFeatures; + + /** + * View to use for reading if none defined for the end point. + */ + protected Class _defaultReadView; + + /** + * View to use for writing if none defined for the end point. + */ + protected Class _defaultWriteView; + + /* + /********************************************************************** + /* Excluded types + /********************************************************************** + */ + + public final static HashSet _untouchables = DEFAULT_UNTOUCHABLES; + + public final static Class[] _unreadableClasses = DEFAULT_UNREADABLES; + + public final static Class[] _unwritableClasses = DEFAULT_UNWRITABLES; + + /* + /********************************************************************** + /* Bit of caching + /********************************************************************** + */ + + /** + * Cache for resolved endpoint configurations when reading JSON data + */ + protected final LookupCache _readers; + + /** + * Cache for resolved endpoint configurations when writing JSON data + */ + protected final LookupCache _writers; + + /* + /********************************************************************** + /* Life-cycle + /********************************************************************** + */ + + protected ProviderBase(MAPPER_CONFIG mconfig) { + this(mconfig, + new SimpleLookupCache<>(16, 120), + new SimpleLookupCache<>(16, 120)); + } + + /** + * Constructor that is only added to resolve problems + * with combination of RESTeasy and CDI. + * Should NOT be used by any code explicitly; only exists + * for proxy support. + */ + @Deprecated // just to denote it should NOT be directly called; will NOT be removed + protected ProviderBase() { + this(null); + } + + /** + * @since 2.17 + */ + protected ProviderBase(MAPPER_CONFIG mconfig, + LookupCache readerCache, + LookupCache writerCache + ) { + _mapperConfig = mconfig; + _jakartaRSFeatures = JAKARTA_RS_FEATURE_DEFAULTS; + _readers = readerCache; + _writers = writerCache; + } + + /* + /********************************************************************** + /* Configuring + /********************************************************************** + */ + + /** + * Method for marking specified type as "untouchable", meaning that provider + * will not try to read or write values of this type (or its subtypes). + * + * @param type Type to consider untouchable; can be any kind of class, + * including abstract class or interface. No instance of this type + * (including subtypes, i.e. types assignable to this type) will + * be read or written by provider + */ + public void addUntouchable(Class type) + { + if (_cfgCustomUntouchables == null) { + _cfgCustomUntouchables = new HashMap(); + } + _cfgCustomUntouchables.put(new ClassKey(type), Boolean.TRUE); + } + + /** + * Method for removing definition of specified type as untouchable: + * usually only + */ + public void removeUntouchable(Class type) + { + if (_cfgCustomUntouchables == null) { + _cfgCustomUntouchables = new HashMap(); + } + _cfgCustomUntouchables.put(new ClassKey(type), Boolean.FALSE); + } + + /** + * Method for overriding {@link AnnotationIntrospector} to use instead of + * default {@code JacksonAnnotationIntrospector}: often used to add + * JAXB-backed introspector. + * + * @param aiOverride AnnotationIntrospector to configure mapper to use + * ({@code null} to leave it as default one) + */ + public void setAnnotationsToUse(AnnotationIntrospector aiOverride) { + _mapperConfig.setAnnotationIntrospector(aiOverride); + } + + /** + * Method that can be used to directly define {@link ObjectMapper} to use + * for serialization and deserialization; if null, will use the standard + * provider discovery from context instead. Default setting is null. + */ + public void setMapper(MAPPER m) { + _mapperConfig.setMapper(m); + } + + /** + * Method for specifying JSON View to use for reading content + * when end point does not have explicit View annotations. + */ + public THIS setDefaultReadView(Class view) { + _defaultReadView = view; + return _this(); + } + + /** + * Method for specifying JSON View to use for reading content + * when end point does not have explicit View annotations. + */ + public THIS setDefaultWriteView(Class view) { + _defaultWriteView = view; + return _this(); + } + + /** + * Method for specifying JSON View to use for reading and writing content + * when end point does not have explicit View annotations. + * Functionally equivalent to: + * + * setDefaultReadView(view); + * setDefaultWriteView(view); + * + */ + public THIS setDefaultView(Class view) { + _defaultReadView = _defaultWriteView = view; + return _this(); + } + + // // // JakartaRSFeature config + + public THIS configure(JakartaRSFeature feature, boolean state) { + return state ? enable(feature) : disable(feature); + } + + public THIS enable(JakartaRSFeature feature) { + _jakartaRSFeatures |= feature.getMask(); + return _this(); + } + + public THIS enable(JakartaRSFeature first, JakartaRSFeature... f2) { + _jakartaRSFeatures |= first.getMask(); + for (JakartaRSFeature f : f2) { + _jakartaRSFeatures |= f.getMask(); + } + return _this(); + } + + public THIS disable(JakartaRSFeature feature) { + _jakartaRSFeatures &= ~feature.getMask(); + return _this(); + } + + public THIS disable(JakartaRSFeature first, JakartaRSFeature... f2) { + _jakartaRSFeatures &= ~first.getMask(); + for (JakartaRSFeature f : f2) { + _jakartaRSFeatures &= ~f.getMask(); + } + return _this(); + } + + public boolean isEnabled(JakartaRSFeature f) { + return (_jakartaRSFeatures & f.getMask()) != 0; + } + + // // // DeserializationFeature + + public THIS configure(DeserializationFeature f, boolean state) { + _mapperConfig.configure(f, state); + return _this(); + } + + public THIS enable(DeserializationFeature f) { + _mapperConfig.configure(f, true); + return _this(); + } + + public THIS disable(DeserializationFeature f) { + _mapperConfig.configure(f, false); + return _this(); + } + + // // // SerializationFeature + + public THIS configure(SerializationFeature f, boolean state) { + _mapperConfig.configure(f, state); + return _this(); + } + + public THIS enable(SerializationFeature f) { + _mapperConfig.configure(f, true); + return _this(); + } + + public THIS disable(SerializationFeature f) { + _mapperConfig.configure(f, false); + return _this(); + } + + /* + /********************************************************************** + /* Abstract methods sub-classes need to implement + /********************************************************************** + */ + + /** + * Helper method used to check whether given media type + * is supported by this provider for read operations + * (when binding input data such as POST body). + *

+ * Default implementation simply calls {@link #hasMatchingMediaType}. + */ + protected boolean hasMatchingMediaTypeForReading(MediaType mediaType) { + return hasMatchingMediaType(mediaType); + } + + /** + * Helper method used to check whether given media type + * is supported by this provider for writing operations, + * such as when converting response object to response + * body of request (like GET or POST). + *

+ * Default implementation simply calls {@link #hasMatchingMediaType}. + */ + protected boolean hasMatchingMediaTypeForWriting(MediaType mediaType) { + return hasMatchingMediaType(mediaType); + } + + /** + * Helper method used to check whether given media type + * is supported by this provider. + */ + protected abstract boolean hasMatchingMediaType(MediaType mediaType); + + /** + * Helper method that is called if no mapper has been explicitly configured. + */ + protected abstract MAPPER _locateMapperViaProvider(Class type, MediaType mediaType); + + protected EP_CONFIG _configForReading(MAPPER mapper, + Annotation[] annotations, Class defaultView) + { +// ObjectReader r = _readerInjector.getAndClear(); + ObjectReader r; + if (defaultView != null) { + r = mapper.readerWithView(defaultView); + } else { + r = mapper.reader(); + } + // 25-Jan-2020, tatu: Important: JAX-RS expects that the InputStream + // is NOT closed by parser so... + r = r.without(StreamReadFeature.AUTO_CLOSE_SOURCE); + if (JakartaRSFeature.READ_FULL_STREAM.enabledIn(_jakartaRSFeatures)) { + r = r.withFeatures(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + } + return _configForReading(r, annotations); + } + + protected EP_CONFIG _configForWriting(MAPPER mapper, + Annotation[] annotations, Class defaultView) + { +// ObjectWriter w = _writerInjector.getAndClear(); + ObjectWriter w; + if (defaultView != null) { + w = mapper.writerWithView(defaultView); + } else { + w = mapper.writer(); + } + // 25-Jan-2020, tatu: Important: JAX-RS expects that the OutputStream + // is NOT closed by generator so... + w = w.without(StreamWriteFeature.AUTO_CLOSE_TARGET); + return _configForWriting(w, annotations); + } + + protected abstract EP_CONFIG _configForReading(ObjectReader reader, + Annotation[] annotations); + + protected abstract EP_CONFIG _configForWriting(ObjectWriter writer, + Annotation[] annotations); + + /* + /********************************************************************** + /* Partial MessageBodyWriter impl + /********************************************************************** + */ + + /** + * Method that Jakarta-RS container calls to try to figure out + * serialized length of given value. Since computation of + * this length is about as expensive as serialization itself, + * implementation will return -1 to denote "not known", so + * that container will determine length from actual serialized + * output (if needed). + */ + @Override + public long getSize(Object value, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + // In general figuring output size requires actual writing; usually not + // worth it to write everything twice. + return -1; + } + + /** + * Method that Jakarta-RS container calls to try to check whether + * given value (of specified type) can be serialized by + * this provider. + * Implementation will first check that expected media type is + * expected one (by call to {@link #hasMatchingMediaType}); then verify + * that type is not one of "untouchable" types. + *

+ * NOTE: in 2.x also checked (if configured) for "canSerialize()", but + * with 3.x that is not done since there that detection never worked + * in a useful manner. + */ + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + if (!hasMatchingMediaType(mediaType)) { + return false; + } + Boolean customUntouchable = _findCustomUntouchable(type); + if (customUntouchable != null) { + // negation: Boolean.TRUE means untouchable -> can not write + return !customUntouchable.booleanValue(); + } + // Ok: looks like we must weed out some core types here; ones that + // make no sense to try to bind from JSON: + if (_isIgnorableForWriting(new ClassKey(type))) { + return false; + } + // but some are interface/abstract classes, so + for (Class cls : _unwritableClasses) { + if (cls.isAssignableFrom(type)) { + return false; + } + } + // 29-Jan-2018, tatu: Mapper does not really know, a priori, much about what might not + // be something it can serialize. Jackson 2.x did have `canSerialize()` method which didn't + // do much good; 3.x does not. + /* + if (_cfgCheckCanSerialize) { + if (!locateMapper(type, mediaType).canSerialize(type)) { + return false; + } + } + */ + return true; + } + + /** + * Method that Jakarta-RS container calls to serialize given value. + */ + @Override + public void writeTo(Object value, Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws JacksonException + { + EP_CONFIG endpoint = _endpointForWriting(value, type, genericType, annotations, + mediaType, httpHeaders); + + // Any headers we should write? + _modifyHeaders(value, type, genericType, annotations, httpHeaders, endpoint); + + ObjectWriter writer = endpoint.getWriter(); + + // Where can we find desired encoding? Within HTTP headers? + JsonEncoding enc = findEncoding(mediaType, httpHeaders); + JavaType rootType = null; + + if ((genericType != null) && (value != null)) { + // 10-Jan-2011, tatu: as per [JACKSON-456], it's not safe to just force root + // type since it prevents polymorphic type serialization. Since we really + // just need this for generics, let's only use generic type if it's truly generic. + + if (!(genericType instanceof Class)) { // generic types are other impls of 'java.lang.reflect.Type' + // This is still not exactly right; should root type be further + // specialized with 'value.getClass()'? Let's see how well this works before + // trying to come up with more complete solution. + + // 18-Mar-2015, tatu: As per [#60], there is now a problem with non-polymorphic lists, + // since forcing of type will then force use of content serializer, which is + // generally not the intent. Fix may require addition of functionality in databind + + TypeFactory typeFactory = writer.typeFactory(); + JavaType baseType = typeFactory.constructType(genericType); + rootType = typeFactory.constructSpecializedType(baseType, type); + // 26-Feb-2011, tatu: To help with [JACKSON-518], we better recognize cases where + // type degenerates back into "Object.class" (as is the case with plain TypeVariable, + // for example), and not use that. + if (rootType.getRawClass() == Object.class) { + rootType = null; + } + } + } + + // Most of the configuration now handled through EndpointConfig, ObjectWriter + // but we may need to force root type: + if (rootType != null) { + writer = writer.forType(rootType); + } + // Some end points decorate value: for JSON we may want JSONP wrapping for example + value = endpoint.modifyBeforeWrite(value); + + // [jaxrs-providers#32]: allow modification by filter-injectible thing + ObjectWriterModifier mod = ObjectWriterInjector.getAndClear(); + if (mod != null) { + writer = mod.modify(endpoint, httpHeaders, value, writer); + } + + try (JsonGenerator g = _createGenerator(writer, entityStream, enc)) { + writer.writeValue(g, value); + } + } + + /** + * Helper method to use for determining desired output encoding. + * For now, will always just use UTF-8... + */ + protected JsonEncoding findEncoding(MediaType mediaType, MultivaluedMap httpHeaders) + { + return JsonEncoding.UTF8; + } + + /** + * Overridable method used for adding optional response headers before + * serializing response object. + */ + protected void _modifyHeaders(Object value, Class type, Type genericType, Annotation[] annotations, + MultivaluedMap httpHeaders, + EP_CONFIG endpoint) + throws JacksonException + { + // Add "nosniff" header? + if (isEnabled(JakartaRSFeature.ADD_NO_SNIFF_HEADER)) { + httpHeaders.add(HEADER_CONTENT_TYPE_OPTIONS, "nosniff"); + } + } + + /** + * Overridable helper method called to create a {@link JsonGenerator} for writing + * contents into given raw {@link OutputStream}. + */ + protected JsonGenerator _createGenerator(ObjectWriter writer, OutputStream rawStream, JsonEncoding enc) + throws JacksonException + { + // Note: disabling of AUTO_CLOSE_TARGET should have happened earlier + return writer.createGenerator(rawStream, enc); + } + + protected EP_CONFIG _endpointForWriting(Object value, Class type, Type genericType, + Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders) + { + // 29-Jun-2016, tatu: allow skipping caching + if (!isEnabled(JakartaRSFeature.CACHE_ENDPOINT_WRITERS)) { + return _configForWriting(locateMapper(type, mediaType), annotations, _defaultWriteView); + } + + AnnotationBundleKey key = new AnnotationBundleKey(annotations, type); + EP_CONFIG endpoint = _writers.get(key); + // not yet resolved (or not cached any more)? Resolve! + if (endpoint == null) { + MAPPER mapper = locateMapper(type, mediaType); + endpoint = _configForWriting(mapper, annotations, _defaultWriteView); + // and cache for future reuse + _writers.put(key.immutableKey(), endpoint); + } + return endpoint; + } + + /* + /********************************************************************** + /* MessageBodyReader impl + /********************************************************************** + */ + + /** + * Method that Jakarta-RS container calls to try to check whether + * values of given type (and media type) can be deserialized by + * this provider. + * Implementation will first check that expected media type is + * a JSON type (via call to {@link #hasMatchingMediaType}); + * then verify + * that type is not one of "untouchable" types (types we will never + * automatically handle). + *

+ * NOTE: in 2.x also checked (if configured) for "canDeserialize()", but + * with 3.x that is not done since there that detection never worked + * in a useful manner. + */ + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + if (!hasMatchingMediaType(mediaType)) { + return false; + } + Boolean customUntouchable = _findCustomUntouchable(type); + if (customUntouchable != null) { + // negation: Boolean.TRUE means untouchable -> can not write + return !customUntouchable.booleanValue(); + } + // Ok: looks like we must weed out some core types here; ones that + // make no sense to try to bind from JSON: + if (_isIgnorableForReading(new ClassKey(type))) { + return false; + } + // and there are some other abstract/interface types to exclude too: + for (Class cls : _unreadableClasses) { + if (cls.isAssignableFrom(type)) { + return false; + } + } + // 29-Jan-2018, tatu: Mapper does not really know, a priori, much about what might not + // be something it can serialize. Jackson 2.x did have `canSerialize()` method which didn't + // do much good; 3.x does not. + /* + // Finally: if we really want to verify that we can serialize, we'll check: + if (_cfgCheckCanDeserialize) { + if (_isSpecialReadable(type)) { + return true; + } + ObjectMapper mapper = locateMapper(type, mediaType); + if (!mapper.canDeserialize(mapper.constructType(type))) { + return false; + } + } + */ + return true; + } + + /** + * Method that Jakarta-RS container calls to deserialize given value. + */ + @Override + public Object readFrom(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap httpHeaders, + InputStream entityStream) + throws JacksonException, + NoContentException + { + EP_CONFIG endpoint = _endpointForReading(type, genericType, annotations, + mediaType, httpHeaders); + + ObjectReader reader = endpoint.getReader(); + JsonParser p = _createParser(reader, entityStream); + + // If null is returned, considered to be empty stream + if (p == null || p.nextToken() == null) { + if (JakartaRSFeature.ALLOW_EMPTY_INPUT.enabledIn(_jakartaRSFeatures)) { + return null; + } + throw _createNoContentException(); + } + Class rawType = type; + if (rawType == JsonParser.class) { + return p; + } + final TypeFactory tf = reader.typeFactory(); + final JavaType resolvedType = tf.constructType(genericType); + + // 09-Jul-2015, tatu: As per [jaxrs-providers#69], handle MappingIterator too + boolean multiValued = (rawType == MappingIterator.class); + + if (multiValued) { + JavaType[] contents = tf.findTypeParameters(resolvedType, MappingIterator.class); + JavaType valueType = (contents == null || contents.length == 0) + ? tf.constructType(Object.class) : contents[0]; + reader = reader.forType(valueType); + } else { + reader = reader.forType(resolvedType); + } + + // Allow modification by filter-injectable thing + ObjectReaderModifier mod = ObjectReaderInjector.getAndClear(); + if (mod != null) { + reader = mod.modify(endpoint, httpHeaders, resolvedType, reader, p); + } + + if (multiValued) { + return reader.readValues(p); + } + return reader.readValue(p); + } + + /** + * Overridable helper method called to create a {@link JsonParser} for reading + * contents of given raw {@link InputStream}. + * May return null to indicate that Stream is empty; that is, contains no + * content. + */ + protected JsonParser _createParser(ObjectReader reader, InputStream rawStream) + throws JacksonException + { + // Note: disabling of AUTO_CLOSE_SOURCE should have happened earlier + // so can just construct and return parser as-is + return reader.createParser(rawStream); + } + + /** + * Overridable helper method that will basically fetch representation of the + * endpoint that can be used to get {@link ObjectReader} to use for deserializing + * content + */ + protected EP_CONFIG _endpointForReading(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap httpHeaders) + { + // Allow skipping caching + if (!isEnabled(JakartaRSFeature.CACHE_ENDPOINT_READERS)) { + return _configForReading(locateMapper(type, mediaType), annotations, _defaultReadView); + } + + AnnotationBundleKey key = new AnnotationBundleKey(annotations, type); + EP_CONFIG endpoint = _readers.get(key); + // not yet resolved (or not cached any more)? Resolve! + if (endpoint == null) { + MAPPER mapper = locateMapper(type, mediaType); + endpoint = _configForReading(mapper, annotations, _defaultReadView); + // and cache for future reuse + _readers.put(key.immutableKey(), endpoint); + } + return endpoint; + } + + /* + /********************************************************************** + /* Overridable helper methods + /********************************************************************** + */ + + /** + * Method called to locate {@link ObjectMapper} to use for serialization + * and deserialization. Exact logic depends on setting of + * {@link JakartaRSFeature#DYNAMIC_OBJECT_MAPPER_LOOKUP}. + * + *

+ * If {@link JakartaRSFeature#DYNAMIC_OBJECT_MAPPER_LOOKUP} is disabled (default + * setting unless changed), behavior is as follows: + *

    + *
  1. If an instance has been explicitly defined by + * {@link #setMapper} (or non-null instance passed in constructor), that + * will be used. + *
  2. + *
  3. If not, will try to locate it using standard Jakarta-RS + * ContextResolver mechanism, if it has been properly configured + * to access it (by Jakarta-RS runtime). + *
  4. + *
  5. Finally, if no mapper is found, will return a default unconfigured + * {@link ObjectMapper} instance (one constructed with default constructor + * and not modified in any way) + *
  6. + *
+ *

+ * If {@link JakartaRSFeature#DYNAMIC_OBJECT_MAPPER_LOOKUP} is enabled, steps + * 1 and 2 are reversed, such that Jakarta-RS ContextResolver + * is first used, and only if none is defined will configured mapper be used. + * + * @param type Class of object being serialized or deserialized; + * not checked at this point, since it is assumed that unprocessable + * classes have been already weeded out, + * but will be passed to ContextResolver as is. + * @param mediaType Declared media type for the instance to process: + * not used by this method, + * but will be passed to ContextResolver as is. + */ + public MAPPER locateMapper(Class type, MediaType mediaType) + { + // 29-Jun-2016, tatu: As per [jaxrs-providers#86] may want to do provider lookup first + if (isEnabled(JakartaRSFeature.DYNAMIC_OBJECT_MAPPER_LOOKUP)) { + MAPPER m = _locateMapperViaProvider(type, mediaType); + if (m == null) { + m = (MAPPER) _mapperConfig.getConfiguredMapper(); + if (m == null) { + m = _mapperConfig.getDefaultMapper(); + } + } + return m; + } + + // Otherwise start with (pre-)configured Mapper and only check provider + // if not found + + MAPPER m = _mapperConfig.getConfiguredMapper(); + if (m == null) { + // If not, maybe we can get one configured via context? + m = _locateMapperViaProvider(type, mediaType); + if (m == null) { + // If not, let's get the fallback default instance + m = _mapperConfig.getDefaultMapper(); + } + } + return m; + } + + /** + * Overridable helper method used to allow handling of somewhat special + * types for reading + */ + protected boolean _isSpecialReadable(Class type) { + return JsonParser.class == type; + } + + /** + * Overridable helper method called to check whether given type is a known + * "ignorable type" (in context of reading), values of which are not bound + * from content. + */ + protected boolean _isIgnorableForReading(ClassKey typeKey) + { + return _untouchables.contains(typeKey); + } + + /** + * Overridable helper method called to check whether given type is a known + * "ignorable type" (in context of reading), values of which + * can not be written out. + */ + protected boolean _isIgnorableForWriting(ClassKey typeKey) + { + return _untouchables.contains(typeKey); + } + + protected NoContentException _createNoContentException() { + return new NoContentException(NO_CONTENT_MESSAGE); + } + + /* + /********************************************************************** + /* Private/sub-class helper methods + /********************************************************************** + */ + + protected static boolean _containedIn(Class mainType, HashSet set) + { + if (set != null) { + ClassKey key = new ClassKey(mainType); + // First: type itself? + if (set.contains(key)) return true; + // Then supertypes (note: will not contain Object.class) + for (Class cls : findSuperTypes(mainType, null)) { + key.reset(cls); + if (set.contains(key)) return true; + } + } + return false; + } + + protected Boolean _findCustomUntouchable(Class mainType) + { + if (_cfgCustomUntouchables != null) { + ClassKey key = new ClassKey(mainType); + // First: type itself? + Boolean b = _cfgCustomUntouchables.get(key); + if (b != null) { + return b; + } + // Then supertypes (note: will not contain Object.class) + for (Class cls : findSuperTypes(mainType, null)) { + key.reset(cls); + b = _cfgCustomUntouchables.get(key); + if (b != null) { + return b; + } + } + } + return null; + } + + protected static List> findSuperTypes(Class cls, Class endBefore) + { + return findSuperTypes(cls, endBefore, new ArrayList>(8)); + } + + protected static List> findSuperTypes(Class cls, Class endBefore, List> result) + { + _addSuperTypes(cls, endBefore, result, false); + return result; + } + + protected static void _addSuperTypes(Class cls, Class endBefore, Collection> result, boolean addClassItself) + { + if (cls == endBefore || cls == null || cls == Object.class) { + return; + } + if (addClassItself) { + if (result.contains(cls)) { // already added, no need to check supers + return; + } + result.add(cls); + } + for (Class intCls : cls.getInterfaces()) { + _addSuperTypes(intCls, endBefore, result, true); + } + _addSuperTypes(cls.getSuperclass(), endBefore, result, true); + } + + @SuppressWarnings("unchecked") + private final THIS _this() { + return (THIS) this; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/StreamReadExceptionMapper.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/StreamReadExceptionMapper.java new file mode 100644 index 00000000000..2980e1c0e76 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/StreamReadExceptionMapper.java @@ -0,0 +1,18 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base; + +import tools.jackson.core.exc.StreamReadException; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; + +/** + * Implementation of {@link ExceptionMapper} to send down a "400 Bad Request" + * in the event unparsable JSON is received. + */ +public class StreamReadExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(StreamReadException exception) { + return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build(); + } +} + diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/util/ClassKey.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/util/ClassKey.java new file mode 100644 index 00000000000..a16d0f50173 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/util/ClassKey.java @@ -0,0 +1,88 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base.util; + +/** + * Efficient key class, used instead of using Class. + * The reason for having a separate key class instead of + * directly using {@link Class} as key is mostly + * to allow for redefining hashCode method -- + * for some strange reason, {@link Class} does not + * redefine {@link Object#hashCode} and thus uses identity + * hash, which is pretty slow. This makes key access using + * {@link Class} unnecessarily slow. + *

+ * Note: since class is not strictly immutable, caller must + * know what it is doing, if changing field values. + */ +public final class ClassKey + implements Comparable +{ + private String _className; + + private Class _class; + + /** + * Let's cache hash code straight away, since we are + * almost certain to need it. + */ + private int _hashCode; + + public ClassKey() + { + _class = null; + _className = null; + _hashCode = 0; + } + + public ClassKey(Class clz) + { + _class = clz; + _className = clz.getName(); + _hashCode = _className.hashCode(); + } + + public void reset(Class clz) + { + _class = clz; + _className = clz.getName(); + _hashCode = _className.hashCode(); + } + + /* + /********************************************************** + /* Comparable + /********************************************************** + */ + + // Just need to sort by name, ok to collide (unless used in TreeMap/Set!) + @Override + public int compareTo(ClassKey other) { + return _className.compareTo(other._className); + } + + /* + /********************************************************** + /* Standard methods + /********************************************************** + */ + + @Override + public boolean equals(Object o) + { + if (o == this) return true; + if (o == null) return false; + if (o.getClass() != getClass()) return false; + ClassKey other = (ClassKey) o; + + // Is it possible to have different Class object for same name + class loader combo? + // Let's assume answer is no: if this is wrong, will need to uncomment following functionality + /* + return (other._className.equals(_className)) + && (other._class.getClassLoader() == _class.getClassLoader()); + */ + return other._class == _class; + } + + @Override public int hashCode() { return _hashCode; } + + @Override public String toString() { return _className; } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/util/EndpointAsBeanProperty.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/util/EndpointAsBeanProperty.java new file mode 100644 index 00000000000..8f8580bd07a --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/util/EndpointAsBeanProperty.java @@ -0,0 +1,77 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base.util; + +import java.lang.annotation.Annotation; +import java.util.Arrays; + +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.PropertyMetadata; +import tools.jackson.databind.PropertyName; +import tools.jackson.databind.introspect.AnnotationMap; + +/** + * {@link BeanProperty} implementation used for passing annotations + * from Jakarta-RS endpoint into Jackson. This tries to imitate behavior + * one would get if actual resource method was used as POJO property; + * ideally this would be how implementation works but due Jakarta-RS API + * limitations, we are only given annotations associated, and that + * has to do. + *

+ * NOTE: not yet used by Jakarta-RS provider, directly, as of Jackson 2.13 + */ +public class EndpointAsBeanProperty + extends BeanProperty.Std +{ + private static final long serialVersionUID = 1L; + + public final static PropertyName ENDPOINT_NAME = new PropertyName("Jakarta-RS/endpoint"); + + private final static AnnotationMap NO_ANNOTATIONS = new AnnotationMap(); + + protected transient Annotation[] _rawAnnotations; + + public AnnotationMap _annotations; + + public EndpointAsBeanProperty(PropertyName name, JavaType type, Annotation[] annotations) + { + // TODO: find and pass wrapper; isRequired marker? + super(name, type, /*PropertyName wrapperName*/ null, + null, PropertyMetadata.STD_OPTIONAL); + _rawAnnotations = annotations; + _annotations = null; + } + + protected EndpointAsBeanProperty(EndpointAsBeanProperty base, JavaType newType) + { + super(base, newType); + _rawAnnotations = base._rawAnnotations; + _annotations = base._annotations; + } + + @Override + public Std withType(JavaType type) { + if (_type == type) { + return this; + } + return new Std(_name, type, _wrapperName, _member, _metadata); + } + + @Override + public A getAnnotation(Class acls) { + return annotations().get(acls); + } + + protected AnnotationMap annotations() { + AnnotationMap am = _annotations; + if (am == null) { + Annotation[] raw = _rawAnnotations; + if (raw == null || raw.length == 0) { + am = NO_ANNOTATIONS; + } else { + am = AnnotationMap.of(Arrays.asList(raw)); + } + _annotations = am; + } + return am; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/util/package-info.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/util/package-info.java new file mode 100644 index 00000000000..282cbe3ab91 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/base/util/package-info.java @@ -0,0 +1,4 @@ +/** + * Miscellaneous helper classes used by providers. + */ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base.util; diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/AnnotationBundleKey.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/AnnotationBundleKey.java new file mode 100644 index 00000000000..6c98bd4e01d --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/AnnotationBundleKey.java @@ -0,0 +1,158 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg; + +import java.lang.annotation.Annotation; + +/** + * Helper class used to allow efficient caching of information, + * given a sequence of Annotations. + * This is mostly used for reusing introspected information on + * Jakarta-RS end points. + */ +public final class AnnotationBundleKey +{ + private final static Annotation[] NO_ANNOTATIONS = new Annotation[0]; + + private final Annotation[] _annotations; + + /** + * We also seem to need the type as part of the key (as per [Issue#11]); + * hopefully that and annotations are enough (if not, may need to reconsider + * the way caching is done, and possibly only cache derivation of annotations, + * not mapper or reader/writer). + */ + private final Class _type; + + private final boolean _annotationsCopied; + + private final int _hashCode; + + /* + /********************************************************************** + /* Construction + /********************************************************************** + */ + + public AnnotationBundleKey(Annotation[] annotations, Class type) + { + _type = type; + // getting hash of name is faster than Class.hashCode() just because latter uses system identity hash: + final int typeHash = type.getName().hashCode(); + if (annotations == null || annotations.length == 0) { + annotations = NO_ANNOTATIONS; + _annotationsCopied = true; + _hashCode = typeHash; + } else { + _annotationsCopied = false; + _hashCode = calcHash(annotations) ^ typeHash; + } + _annotations = annotations; + } + + private AnnotationBundleKey(Annotation[] annotations, Class type, int hashCode) + { + _annotations = annotations; + _annotationsCopied = true; + _type = type; + _hashCode = hashCode; + } + + private final static int calcHash(Annotation[] annotations) + { + /* hmmh. Can't just base on Annotation type; chances are that Annotation + * instances use identity hash, which has to do. + */ + final int len = annotations.length; + int hash = len; + for (int i = 0; i < len; ++i) { + hash = (hash * 31) + annotations[i].hashCode(); + } + return hash; + } + + /** + * Method called to create a safe immutable copy of the key; used when + * adding entry with this key -- lookups are ok without calling the method. + */ + public AnnotationBundleKey immutableKey() { + if (_annotationsCopied) { + return this; + } + int len = _annotations.length; + Annotation[] newAnnotations = new Annotation[len]; + System.arraycopy(_annotations, 0, newAnnotations, 0, len); + return new AnnotationBundleKey(newAnnotations, _type, _hashCode); + } + + /* + /********************************************************************** + /* Overridden methods + /********************************************************************** + */ + + @Override + public int hashCode() { + return _hashCode; + } + + @Override + public String toString() { + return "[Annotations: "+_annotations.length+", type: " + +_type.getName()+", hash 0x"+Integer.toHexString(_hashCode) + +", copied: "+_annotationsCopied+"]"; + } + + @Override + public boolean equals(Object o) + { + if (o == this) return true; + if (o == null) return false; + if (o.getClass() != getClass()) return false; + AnnotationBundleKey other = (AnnotationBundleKey) o; + if ((other._hashCode != _hashCode) || (other._type != _type)) { + return false; + } + return _equals(other._annotations); + } + + private final boolean _equals(Annotation[] otherAnn) + { + final int len = _annotations.length; + if (otherAnn.length != len) { + return false; + } + + // 05-May-2019, tatu: If we wanted to true equality of contents we should + // do order-insensitive check; however, our use case is not unifying all + // possible permutations but rather trying to ensure that caching of same + // method signature is likely to match. So false negatives are acceptable + // over having to do order-insensitive comparison. + + switch (len) { + default: + for (int i = 0; i < len; ++i) { + if (!_annotations[i].equals(otherAnn[i])) { + return false; + } + } + return true; + + case 3: + if (!_annotations[2].equals(otherAnn[2])) { + return false; + } + // fall through + case 2: + if (!_annotations[1].equals(otherAnn[1])) { + return false; + } + // fall through + case 1: + if (!_annotations[0].equals(otherAnn[0])) { + return false; + } + // fall through + case 0: + } + return true; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/EndpointConfigBase.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/EndpointConfigBase.java new file mode 100644 index 00000000000..1932b29993e --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/EndpointConfigBase.java @@ -0,0 +1,182 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg; + +import java.lang.annotation.Annotation; + +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.annotation.JacksonFeatures; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.annotation.JsonView; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.cfg.MapperConfig; + +/** + * Container class for figuring out annotation-based configuration + * for Jakarta-RS end points. + */ +public abstract class EndpointConfigBase> +{ + // // General configuration + + protected final MapperConfig _config; + + protected Class _activeView; + + protected String _rootName; + + // // Deserialization-only config + + protected DeserializationFeature[] _deserEnable; + protected DeserializationFeature[] _deserDisable; + + protected ObjectReader _reader; + + // // Serialization-only config + + protected SerializationFeature[] _serEnable; + protected SerializationFeature[] _serDisable; + + protected ObjectWriter _writer; + + /* + /********************************************************************** + /* Construction + /********************************************************************** + */ + + protected EndpointConfigBase(MapperConfig config) { + _config = config; + } + + @SuppressWarnings("unchecked") + protected THIS add(Annotation[] annotations, boolean forWriting) + { + if (annotations != null) { + for (Annotation annotation : annotations) { + addAnnotation(annotation.annotationType(), annotation, forWriting); + } + } + return (THIS) this; + } + + protected void addAnnotation(Class type, + Annotation annotation, boolean forWriting) + { + if (type == JsonView.class) { + // Can only use one view; but if multiple defined, use first (no exception) + Class[] views = ((JsonView) annotation).value(); + _activeView = (views.length > 0) ? views[0] : null; + } else if (type == JacksonFeatures.class) { + JacksonFeatures feats = (JacksonFeatures) annotation; + if (forWriting) { + _serEnable = nullIfEmpty(feats.serializationEnable()); + _serDisable = nullIfEmpty(feats.serializationDisable()); + } else { + _deserEnable = nullIfEmpty(feats.deserializationEnable()); + _deserDisable = nullIfEmpty(feats.deserializationDisable()); + } + } else if (type == JsonRootName.class) { + _rootName = ((JsonRootName) annotation).value(); + } else if (type == JacksonAnnotationsInside.class) { + // skip; processed below (in parent), so encountering here is of no use + } else { + // For all unrecognized types, check meta-annotation(s) to see if they are bundles + JacksonAnnotationsInside inside = type.getAnnotation(JacksonAnnotationsInside.class); + if (inside != null) { + add(type.getAnnotations(), forWriting); + } + } + } + + @SuppressWarnings("unchecked") + protected THIS initReader(ObjectReader reader) + { + if (_activeView != null) { + reader = reader.withView(_activeView); + } + if (_rootName != null) { + reader = reader.withRootName(_rootName); + } + // Then deser features + if (_deserEnable != null) { + reader = reader.withFeatures(_deserEnable); + } + if (_deserDisable != null) { + reader = reader.withoutFeatures(_deserDisable); + } + _reader = reader; + return (THIS) this; + } + + @SuppressWarnings("unchecked") + protected THIS initWriter(ObjectWriter writer) + { + if (_activeView != null) { + writer = writer.withView(_activeView); + } + if (_rootName != null) { + writer = writer.withRootName(_rootName); + } + // Then features + if (_serEnable != null) { + writer = writer.withFeatures(_serEnable); + } + if (_serDisable != null) { + writer = writer.withoutFeatures(_serDisable); + } + _writer = writer; + return (THIS) this; + } + + /* + /********************************************************************** + /* Accessors + /********************************************************************** + */ + + public String getRootName() { + return _rootName; + } + + public Class getActiveView() { + return _activeView; + } + + public final ObjectReader getReader() { + if (_reader == null) { // sanity check, should never happen + throw new IllegalStateException(); + } + return _reader; + } + + public final ObjectWriter getWriter() { + if (_writer == null) { // sanity check, should never happen + throw new IllegalStateException(); + } + return _writer; + } + + /* + /********************************************************************** + /* Value modifications + /********************************************************************** + */ + + public abstract Object modifyBeforeWrite(Object value); + + /* + /********************************************************************** + /* Helper methods + /********************************************************************** + */ + + protected static T[] nullIfEmpty(T[] arg) { + if (arg == null || arg.length == 0) { + return null; + } + return arg; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/JakartaRSFeature.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/JakartaRSFeature.java new file mode 100644 index 00000000000..1257c794c08 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/JakartaRSFeature.java @@ -0,0 +1,128 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg; + +import tools.jackson.databind.cfg.ConfigFeature; + +/** + * Enumeration that defines simple on/off features that can be + * used on all Jackson Jakarta-RS providers, regardless of + * underlying data format. + */ +public enum JakartaRSFeature implements ConfigFeature +{ + /* + /********************************************************************** + /* Input handling + /********************************************************************** + */ + + /** + * Feature to define whether empty input is considered legal or not. + * If set to true, empty content is allowed and will be read as Java 'null': if false, + * an {@link java.io.IOException} will be thrown. + *

+ * NOTE: in case of Jakarta-RS 2.0, specific exception will be {@code jakarta.ws.rs.core.NoContentException}, + */ + ALLOW_EMPTY_INPUT(true), + + /** + * For HTTP keep-alive or multipart content to work correctly, Jackson must read the entire HTTP input + * stream up until reading EOF (-1). + * Issue #108 + * If set to true, always consume all input content. This has a side-effect of failing on trailing content. + *

+ * Feature is enabled by default. + * Note that this means that behavior in earlier versions + * (2.14 and before) differs from 2.15 and later. + * + * @since 2.15 + */ + READ_FULL_STREAM(true), + + /* + /********************************************************************** + /* HTTP headers + /********************************************************************** + */ + + /** + * Feature that can be enabled to make provider automatically + * add "nosniff" (see + * this entry + * for details + *

+ * Feature is disabled by default. + */ + ADD_NO_SNIFF_HEADER(false), + + /* + /********************************************************************** + /* Caching, related + /********************************************************************** + */ + + /** + * Feature that may be enabled to force dynamic lookup of ObjectMapper + * via Jakarta-RS Provider interface, regardless of whether MapperConfigurator + * has explicitly configured mapper or not; if disabled, static configuration will + * take precedence. + * Note that if this feature is enabled, it typically makes sense to also disable + * {@link JakartaRSFeature#CACHE_ENDPOINT_READERS} and {@link JakartaRSFeature#CACHE_ENDPOINT_WRITERS} + * since caching would prevent lookups. + *

+ * Feature is disabled by default. + */ + DYNAMIC_OBJECT_MAPPER_LOOKUP(false), + + /** + * Feature that determines whether provider will cache endpoint + * definitions for reading or not (including caching of actual ObjectReader to use). + * Feature may be disabled if reconfiguration or alternate instance of ObjectMapper is needed. + *

+ * Note that disabling of the feature may add significant amount of overhead for processing. + *

+ * Feature is enabled by default. + */ + CACHE_ENDPOINT_READERS(true), + + /** + * Feature that determines whether provider will cache endpoint + * definitions for writing or not (including caching of actual ObjectWriter to use). + * Feature may be disabled if reconfiguration or alternate instance of ObjectMapper is needed. + *

+ * Note that disabling of the feature may add significant amount of overhead for processing. + *

+ * Feature is enabled by default. + */ + CACHE_ENDPOINT_WRITERS(true), + + /* + /********************************************************************** + /* Other + /********************************************************************** + */ + + ; + + private final boolean _defaultState; + + private JakartaRSFeature(boolean defaultState) { + _defaultState = defaultState; + } + + public static int collectDefaults() { + int flags = 0; + for (JakartaRSFeature f : values()) { + if (f.enabledByDefault()) { flags |= f.getMask(); } + } + return flags; + } + + @Override + public boolean enabledByDefault() { return _defaultState; } + + @Override + public int getMask() { return (1 << ordinal()); } + + @Override + public boolean enabledIn(int flags) { return (flags & getMask()) != 0; } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/MapperConfiguratorBase.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/MapperConfiguratorBase.java new file mode 100644 index 00000000000..3048116f4d5 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/MapperConfiguratorBase.java @@ -0,0 +1,222 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg; + +import java.util.EnumMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.cfg.MapperBuilder; + +/** + * Helper class used to encapsulate details of configuring an + * {@link ObjectMapper} instance to be used for data binding, as + * well as accessing it. + */ +public abstract class MapperConfiguratorBase, + MAPPER extends ObjectMapper +> +{ + private final ReentrantLock _lock = new ReentrantLock(); + + /* + /********************************************************************** + /* Configuration, simple features + /********************************************************************** + */ + + /** + * {@link DeserializationFeature}s to explicitly enable or disable + */ + protected EnumMap _mapperFeatures; + + /** + * {@link DeserializationFeature}s to explicitly enable or disable + */ + protected EnumMap _deserFeatures; + + /** + * {@link SerializationFeature}s to explicitly enable or disable + */ + protected EnumMap _serFeatures; + + /* + /********************************************************************** + /* Configuration, other + /********************************************************************** + */ + + /** + * {@code AnnotationIntrospector} to use as an override over default + * {@code JacksonAnnotationIntrospector}, if any. + * + * @since 3.0 + */ + protected AnnotationIntrospector _instropectorOverride; + + /* + /********************************************************************** + /* Lazily constructed Mapper instance(s) + /********************************************************************** + */ + + /** + * Mapper provider was constructed with if any, or that was constructed + * due to a call to explicitly configure mapper. + * If defined (explicitly or implicitly) it will be used, instead + * of using provider-based lookup. + */ + protected MAPPER _mapper; + + /** + * If no mapper was specified when constructed, and no configuration + * calls are made, a default mapper is constructed. The difference + * between default mapper and regular one is that default mapper + * is only used if no mapper is found via provider lookup. + */ + protected MAPPER _defaultMapper; + + /* + /********************************************************************** + /* Life-cycle + /********************************************************************** + */ + + public MapperConfiguratorBase(MAPPER mapper, + AnnotationIntrospector instropectorOverride) + { + _mapper = mapper; + _instropectorOverride = instropectorOverride; + } + + public MAPPER getDefaultMapper() { + if (_defaultMapper == null) { + _lock.lock(); + try { + if (_defaultMapper == null) { + _defaultMapper = _mapperWithConfiguration(mapperBuilder()); + } + } finally { + _lock.unlock(); + } } + return _defaultMapper; + } + + /** + * Helper method that will ensure that there is a configurable non-default + * mapper (constructing an instance if one didn't yet exit), and return + * that mapper. + */ + protected MAPPER mapper() + { + if (_mapper == null) { + _lock.lock(); + try { + if (_mapper == null) { + _mapper = _mapperWithConfiguration(mapperBuilder()); + } + } finally { + _lock.unlock(); + } + } + return _mapper; + } + + /* + /********************************************************************** + /* Abstract methods to implement + /********************************************************************** + */ + + /** + * @since 3.0 + */ + protected abstract MapperBuilder mapperBuilder(); + + /* + /********************************************************************** + /* Configuration methods + /********************************************************************** + */ + + /** + * Method that locates, configures and returns {@link ObjectMapper} to use + */ + public MAPPER getConfiguredMapper() { + // important: should NOT call mapper(); needs to return null + // if no instance has been passed or constructed + return _mapper; + } + + public final void setMapper(MAPPER m) { + _mapper = m; + } + + public final void setAnnotationIntrospector(AnnotationIntrospector aiOverride) { + _instropectorOverride = aiOverride; + } + + public final void configure(DeserializationFeature f, boolean state) { + if (_deserFeatures == null) { + _deserFeatures = new EnumMap<>(DeserializationFeature.class); + } + _deserFeatures.put(f, state); + } + + public final void configure(SerializationFeature f, boolean state) { + if (_serFeatures == null) { + _serFeatures = new EnumMap<>(SerializationFeature.class); + } + _serFeatures.put(f, state); + } + + /* + /********************************************************************** + /* Helper methods for sub-classes + /********************************************************************** + */ + + /** + * Helper method that will configure given builder using configured overrides. + */ + @SuppressWarnings("unchecked") + protected MAPPER _mapperWithConfiguration(MapperBuilder mapperBuilder) + { + return (MAPPER) _builderWithConfiguration(mapperBuilder) + .build(); + } + + /** + * Overridable helper method that applies all configuration on given builder. + */ + protected MapperBuilder _builderWithConfiguration(MapperBuilder mapperBuilder) + { + // First, AnnotationIntrospector settings + if (_instropectorOverride != null) { + mapperBuilder = mapperBuilder.annotationIntrospector(_instropectorOverride); + } + + // Features? + if (_mapperFeatures != null) { + for (Map.Entry entry : _mapperFeatures.entrySet()) { + mapperBuilder = mapperBuilder.configure(entry.getKey(), entry.getValue()); + } + } + if (_serFeatures != null) { + for (Map.Entry entry : _serFeatures.entrySet()) { + mapperBuilder = mapperBuilder.configure(entry.getKey(), entry.getValue()); + } + } + if (_deserFeatures != null) { + for (Map.Entry entry : _deserFeatures.entrySet()) { + mapperBuilder = mapperBuilder.configure(entry.getKey(), entry.getValue()); + } + } + + // anything else? + return mapperBuilder; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectReaderInjector.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectReaderInjector.java new file mode 100644 index 00000000000..0354c925f81 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectReaderInjector.java @@ -0,0 +1,43 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg; + +import java.util.concurrent.atomic.AtomicBoolean; + +import tools.jackson.databind.ObjectReader; + +/** + * This class allows registering a + * modifier ({@link ObjectReaderModifier}) that can be used to + * reconfigure {@link ObjectReader} + * that Jakarta-RS Resource will use for reading input into Java Objects. + * Usually this class is accessed from a Servlet or Jakarta-RS filter + * before execution reaches resource. + */ +public class ObjectReaderInjector +{ + protected static final ThreadLocal _threadLocal = new ThreadLocal(); + + /** + * Simple marker used to optimize out {@link ThreadLocal} access in cases + * where this feature is not being used + */ + protected static final AtomicBoolean _hasBeenSet = new AtomicBoolean(false); + + private ObjectReaderInjector() { } + + public static void set(ObjectReaderModifier mod) { + _hasBeenSet.set(true); + _threadLocal.set(mod); + } + + public static ObjectReaderModifier get() { + return _hasBeenSet.get() ? _threadLocal.get() : null; + } + + public static ObjectReaderModifier getAndClear() { + ObjectReaderModifier mod = get(); + if (mod != null) { + _threadLocal.remove(); + } + return mod; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectReaderModifier.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectReaderModifier.java new file mode 100644 index 00000000000..44d16abd983 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectReaderModifier.java @@ -0,0 +1,26 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectReader; + +import jakarta.ws.rs.core.MultivaluedMap; + +public abstract class ObjectReaderModifier +{ + /** + * Method called to let modifier make any changes it wants to to objects + * used for reading request objects for specified endpoint. + * + * @param endpoint End point for which reader is used + * @param httpHeaders HTTP headers sent with request (read-only) + * @param resultType Type that input is to be bound to + * @param r ObjectReader as constructed for endpoint, type to handle + * @param p Parser to use for reading content + */ + public abstract ObjectReader modify(EndpointConfigBase endpoint, + MultivaluedMap httpHeaders, + JavaType resultType, ObjectReader r, JsonParser p) + throws JacksonException; +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectWriterInjector.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectWriterInjector.java new file mode 100644 index 00000000000..2965d7b6208 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectWriterInjector.java @@ -0,0 +1,40 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg; + +import java.util.concurrent.atomic.AtomicBoolean; + +import tools.jackson.databind.ObjectWriter; + +/** + * This class allows "overriding" of {@link ObjectWriter} + * that Jakarta-RS Resource will use; usually this is done from a Servlet + * or Jakarta-RS filter before execution reaches resource. + */ +public class ObjectWriterInjector +{ + protected static final ThreadLocal _threadLocal = new ThreadLocal(); + + /** + * Simple marker used to optimize out {@link ThreadLocal} access in cases + * where this feature is not being used + */ + protected static final AtomicBoolean _hasBeenSet = new AtomicBoolean(false); + + private ObjectWriterInjector() { } + + public static void set(ObjectWriterModifier mod) { + _hasBeenSet.set(true); + _threadLocal.set(mod); + } + + public static ObjectWriterModifier get() { + return _hasBeenSet.get() ? _threadLocal.get() : null; + } + + public static ObjectWriterModifier getAndClear() { + ObjectWriterModifier mod = get(); + if (mod != null) { + _threadLocal.remove(); + } + return mod; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectWriterModifier.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectWriterModifier.java new file mode 100644 index 00000000000..5cd29f9a435 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/cfg/ObjectWriterModifier.java @@ -0,0 +1,20 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectWriter; + +import jakarta.ws.rs.core.MultivaluedMap; + +public abstract class ObjectWriterModifier +{ + /** + * Method called to let modifier make any changes it wants to to objects + * used for writing response for specified endpoint. + * + * @param responseHeaders HTTP headers being returned with response (mutable) + */ + public abstract ObjectWriter modify(EndpointConfigBase endpoint, + MultivaluedMap responseHeaders, + Object valueToWrite, ObjectWriter w) + throws JacksonException; +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JacksonJsonProvider.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JacksonJsonProvider.java new file mode 100644 index 00000000000..5a06039e4e5 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JacksonJsonProvider.java @@ -0,0 +1,234 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json; + +import java.lang.annotation.Annotation; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.ext.Providers; + +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.base.ProviderBase; + +import tools.jackson.core.Version; +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.json.JsonMapper; + +/** + * Basic implementation of Jakarta-RS abstractions ({@link MessageBodyReader}, + * {@link MessageBodyWriter}) needed for binding + * JSON ("application/json") content to and from Java Objects ("POJO"s). + *

+ * Actual data binding functionality is implemented by {@link ObjectMapper}: + * mapper to use can be configured in multiple ways: + *

    + *
  • By explicitly passing mapper to use in constructor + *
  • By explictly setting mapper to use by {@link #setMapper} + *
  • By defining Jakarta-RS Provider that returns {@link ObjectMapper}s. + *
  • By doing none of above, in which case a default mapper instance is + * constructed (and configured if configuration methods are called) + *
+ * The last method ("do nothing specific") is often good enough; explicit passing + * of Mapper is simple and explicit; and Provider-based method may make sense + * with Dependency Injection frameworks, or if Mapper has to be configured differently + * for different media types. + *

+ * Note that the default mapper instance will be automatically created if + * one of explicit configuration methods (like {@link #configure}) + * is called: if so, Provider-based introspection is NOT used, but the + * resulting Mapper is used as configured. + *

+ * There is also ({@link JacksonXmlBindJsonProvider}) which + * is configured by default to use both Jackson and Jakarta XmlBin annotations + * for configuration (base class when used as-is defaults to using just Jackson annotations) + * + * @author Tatu Saloranta + */ +@Provider +@Consumes(MediaType.WILDCARD) // NOTE: required to support "non-standard" JSON variants +@Produces({MediaType.APPLICATION_JSON, "text/json", MediaType.WILDCARD}) +public class JacksonJsonProvider + extends ProviderBase +{ + public final static String MIME_JAVASCRIPT = "application/javascript"; + + public final static String MIME_JAVASCRIPT_MS = "application/x-javascript"; + + /* + /********************************************************************** + /* General configuration + /********************************************************************** + */ + + /** + * JSONP function name to use for automatic JSONP wrapping, if any; + * if null, no JSONP wrapping is done. + * Note that this is the default value that can be overridden on + * per-endpoint basis. + */ + protected String _jsonpFunctionName; + + /* + /********************************************************************** + /* Context configuration + /********************************************************************** + */ + + /** + * Injectable context object used to locate configured + * instance of {@link JsonMapper} to use for actual + * serialization. + */ + @Context + protected Providers _providers; + + /* + /********************************************************************** + /* Construction + /********************************************************************** + */ + + /** + * Default constructor, usually used when provider is automatically + * configured to be used with JAX-RS implementation. + */ + public JacksonJsonProvider() { + this(null, null); + } + + public JacksonJsonProvider(JsonMapper mapper) { + this(mapper, null); + } + + /** + * Constructor to use when a custom mapper (usually components + * like serializer/deserializer factories that have been configured) + * is to be used. + * + * @param aiOverride AnnotationIntrospector to override default with, if any + */ + public JacksonJsonProvider(JsonMapper mapper, + AnnotationIntrospector aiOverride) { + super(new JsonMapperConfigurator(mapper, aiOverride)); + } + + /** + * Constructor for use with a custom mapperConfigurator (usually implementing + * some methods from MapperConfiguratorBase) + * + * @param mapperConfigurator custom mapper configurator to use + * + * @since 3.1 + */ + public JacksonJsonProvider(JsonMapperConfigurator mapperConfigurator) { + super(mapperConfigurator); + } + + /** + * Method that will return version information stored in and read from jar + * that contains this class. + */ + @Override + public Version version() { + return PackageVersion.VERSION; + } + + /* + /********************************************************************** + /* JSON-specific configuration + /********************************************************************** + */ + + public void setJSONPFunctionName(String fname) { + _jsonpFunctionName = fname; + } + + /* + /********************************************************************** + /* Abstract method impls + /********************************************************************** + */ + + /** + * Helper method used to check whether given media type + * is supported by this provider. + * Current implementation essentially checks to see whether + * {@link MediaType#getSubtype} returns "json" or something + * ending with "+json". + * Or "text/x-json" (since 2.3) + */ + @Override + protected boolean hasMatchingMediaType(MediaType mediaType) + { + /* As suggested by Stephen D, there are 2 ways to check: either + * being as inclusive as possible (if subtype is "json"), or + * exclusive (major type "application", minor type "json"). + * Let's start with inclusive one, hard to know which major + * types we should cover aside from "application". + */ + if (mediaType != null) { + // Ok: there are also "xxx+json" subtypes, which count as well + String subtype = mediaType.getSubtype(); + + // [Issue#14]: also allow 'application/javascript' + return "json".equalsIgnoreCase(subtype) || subtype.endsWith("+json") + || "javascript".equals(subtype) + // apparently Microsoft once again has interesting alternative types? + || "x-javascript".equals(subtype) + || "x-json".equals(subtype) // [Issue#40] + ; + } + // Not sure if this can happen; but it seems reasonable + // that we can at least produce JSON without media type? + return true; + } + + @Override + protected JsonMapper _locateMapperViaProvider(Class type, MediaType mediaType) + { + JsonMapper m = _mapperConfig.getConfiguredMapper(); + if (m == null) { + if (_providers != null) { + ContextResolver resolver = _providers.getContextResolver(JsonMapper.class, mediaType); + /* Above should work as is, but due to this bug + * [https://jersey.dev.java.net/issues/show_bug.cgi?id=288] + * in Jersey, it doesn't. But this works until resolution of + * the issue: + */ + if (resolver == null) { + resolver = _providers.getContextResolver(JsonMapper.class, null); + } + if (resolver != null) { + return resolver.getContext(type); + } + } + if (m == null) { + // If not, let's get the fallback default instance + m = _mapperConfig.getDefaultMapper(); + } + } + return m; + } + + @Override + protected JsonEndpointConfig _configForReading(ObjectReader reader, + Annotation[] annotations) { + return JsonEndpointConfig.forReading(reader, annotations); + } + + @Override + protected JsonEndpointConfig _configForWriting(ObjectWriter writer, + Annotation[] annotations) { + return JsonEndpointConfig.forWriting(writer, annotations, + _jsonpFunctionName); + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JacksonXmlBindJsonProvider.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JacksonXmlBindJsonProvider.java new file mode 100644 index 00000000000..3232631b7bb --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JacksonXmlBindJsonProvider.java @@ -0,0 +1,67 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.Provider; + +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; + +/** + * JSON content type provider automatically configured to use both Jackson + * and Jakarta XmlBind annotations (in that order of priority). Otherwise functionally + * same as {@link JacksonJsonProvider}. + *

+ * Typical usage pattern is to just instantiate instance of this + * provider for Jakarta-RS and use as is: this will use both Jackson and + * Jakarta XmlBind annotations (with Jackson annotations having priority). + *

+ * Note: class annotations are duplicated from super class, since it + * is not clear whether Jakarta-RS implementations are required to + * check settings of super-classes. It is important to keep annotations + * in sync if changed. + */ +@Provider +@Consumes(MediaType.WILDCARD) // NOTE: required to support "non-standard" JSON variants +@Produces({MediaType.APPLICATION_JSON, "text/json", MediaType.WILDCARD}) +public class JacksonXmlBindJsonProvider extends JacksonJsonProvider +{ + /** + * Default constructor, usually used when provider is automatically + * configured to be used with Jakarta-RS implementation. + */ + public JacksonXmlBindJsonProvider() { + this(null, JaxbHolder.get()); + } + + /** + * Constructor to use when a custom mapper (usually components + * like serializer/deserializer factories that have been configured) + * is to be used. + */ + public JacksonXmlBindJsonProvider(JsonMapper mapper, + AnnotationIntrospector aiOverride) + { + super(mapper, aiOverride); + } + + /** + * Constructor for use with a custom mapperConfigurator (usually implementing + * some methods from MapperConfiguratorBase) + * @since 3.1 + */ + public JacksonXmlBindJsonProvider(JsonMapperConfigurator mapperConfigurator) { + super(mapperConfigurator); + } + + // Silly class to encapsulate reference to JAXB introspector class so that + // loading of parent class does not require it; only happens if and when + // introspector needed + private static class JaxbHolder { + public static AnnotationIntrospector get() { + return new JakartaXmlBindAnnotationIntrospector(); + } + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JsonEndpointConfig.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JsonEndpointConfig.java new file mode 100644 index 00000000000..fc1c4869eb3 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JsonEndpointConfig.java @@ -0,0 +1,104 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json; + +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.EndpointConfigBase; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.annotation.JSONP; + +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.util.JSONPObject; +import tools.jackson.databind.util.JSONWrappedObject; + +import java.lang.annotation.Annotation; + +/** + * Container class for figuring out annotation-based configuration + * for Jakarta-RS end points. + */ +public class JsonEndpointConfig + extends EndpointConfigBase +{ + // // Serialization-only config + + protected JSONP.Def _jsonp; + + /* + /********************************************************** + /* Construction + /********************************************************** + */ + + protected JsonEndpointConfig(MapperConfig config) { + super(config); + } + + public static JsonEndpointConfig forReading(ObjectReader reader, + Annotation[] annotations) + { + return new JsonEndpointConfig(reader.getConfig()) + .add(annotations, false) + .initReader(reader); + } + + public static JsonEndpointConfig forWriting(ObjectWriter writer, + Annotation[] annotations, + String defaultJsonpMethod) + { + JsonEndpointConfig config = new JsonEndpointConfig(writer.getConfig()); + if (defaultJsonpMethod != null) { + config._jsonp = new JSONP.Def(defaultJsonpMethod); + } + return config + .add(annotations, true) + .initWriter(writer) + ; + } + + /* + /********************************************************** + /* Abstract method impls, overrides + /********************************************************** + */ + + @Override + protected void addAnnotation(Class type, + Annotation annotation, boolean forWriting) + { + if (type == JSONP.class) { + if (forWriting) { + _jsonp = new JSONP.Def((JSONP) annotation); + } + } else { + super.addAnnotation(type, annotation, forWriting); + } + } + + @Override + public Object modifyBeforeWrite(Object value) { + return applyJSONP(value); + } + + /* + /********************************************************** + /* Accessors + /********************************************************** + */ + + /** + * Method that will add JSONP wrapper object, if and as + * configured by collected annotations. + */ + public Object applyJSONP(Object value) + { + if (_jsonp != null) { + // full prefix+suffix? + if (_jsonp.prefix != null || _jsonp.suffix != null) { + return new JSONWrappedObject(_jsonp.prefix, _jsonp.suffix, value); + } + if (_jsonp.method != null) { + return new JSONPObject(_jsonp.method, value); + } + } + return value; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JsonMapperConfigurator.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JsonMapperConfigurator.java new file mode 100644 index 00000000000..43f7011b0fe --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/JsonMapperConfigurator.java @@ -0,0 +1,35 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json; + +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.MapperConfiguratorBase; + +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.json.JsonMapper; + +/** + * Helper class used to encapsulate details of configuring an + * {@link ObjectMapper} instance to be used for data binding, as + * well as accessing it. + */ +public class JsonMapperConfigurator + extends MapperConfiguratorBase +{ + public JsonMapperConfigurator(JsonMapper mapper, + AnnotationIntrospector aiOverride) + { + super(mapper, aiOverride); + } + + /* + /********************************************************************** + /* Abstract method impls + /********************************************************************** + */ + + @Override + protected MapperBuilder mapperBuilder() { + return JsonMapper.builder(); + } +} + diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/PackageVersion.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/PackageVersion.java new file mode 100644 index 00000000000..6d95425700d --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/PackageVersion.java @@ -0,0 +1,20 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json; + +import tools.jackson.core.Version; +import tools.jackson.core.Versioned; +import tools.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public static final Version VERSION = VersionUtil.parseVersion( + "3.0.4", "tools.jackson.jakarta.rs", "jackson-jakarta-rs-json-provider"); + + @Override + public Version version() { + return VERSION; + } +} diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/annotation/JSONP.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/annotation/JSONP.java new file mode 100644 index 00000000000..5bd49d5a84c --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/annotation/JSONP.java @@ -0,0 +1,95 @@ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Note: applicable to annotations to allow bundling (if support added + * to Jakarta-RS bundle itself), as well as methods to indicate that return + * type is to be wrapped. + * Other types are not allowed, since there is no current usage for those; + * input can't be wrapped (so no need for parameters); fields are not + * exposed through Jakarta-RS; and we do not allow 'default wrapping' for + * types. + *

+ * Note on properties: if either {@link #prefix()} or {@link #suffix()} + * is non-empty, they are used as literal prefix and suffix to use. + * Otherwise {@link #value()} is used as the function name, followed + * by opening parenthesis, value, and closing parenthesis. + *

+ * Example usage: + *

+ *  class Wrapper {
+ *     @JSONP("myFunc") public int value = 3;
+ *  }
+ *
+ * would serialize as: + *
+ *  myFunc({"value":3})
+ *
+ *  whereas
+ *
+ *
+ *  class Wrapper {
+ *     @JSONP(prefix="call(", suffix=")+3") public int value = 1;
+ *  }
+ *
+ * would serialize as: + *
+ *  call({"value":1})+3
+ *
+ */
+@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@com.fasterxml.jackson.annotation.JacksonAnnotation
+public @interface JSONP
+{
+    /**
+     * Method used for JSONP, unless {@link #prefix()} or
+     * {@link #suffix()} return non-empty Strings.
+     */
+    public String value() default "";
+
+    /**
+     * Prefix String used for JSONP if not empty: will be included
+     * verbatim before JSON value.
+     */
+    public String prefix() default "";
+
+    /**
+     * Suffix String used for JSONP if not empty: will be included
+     * verbatim after JSON value.
+     */
+    public String suffix() default "";
+
+    /**
+     * Helper class for encapsulating information from {@link JSONP}
+     * annotation instance.
+     */
+    public static class Def {
+        public final String method;
+        public final String prefix;
+        public final String suffix;
+
+        public Def(String m) {
+            method = m;
+            prefix = null;
+            suffix = null;
+        }
+
+        public Def(JSONP json) {
+            method = emptyAsNull(json.value());
+            prefix = emptyAsNull(json.prefix());
+            suffix = emptyAsNull(json.suffix());
+        }
+
+        private final static String emptyAsNull(String str) {
+            if (str == null || str.length() == 0) {
+                return null;
+            }
+            return str;
+        }
+    }
+}
diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/annotation/package-info.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/annotation/package-info.java
new file mode 100644
index 00000000000..ba18c3c7375
--- /dev/null
+++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/annotation/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * Package that contains annotations specific to JSON dataformat.
+ *

+ * NOTE: starting with version 2.2, general-purpose annotations + * will be moved to a shared package, and this package will only + * contains JSON-specific annotations. + */ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.annotation; diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/package-info.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/package-info.java new file mode 100644 index 00000000000..69643cbe021 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/internal/jackson/jakarta/rs/json/package-info.java @@ -0,0 +1,21 @@ +/** + * Jackson-based Jakarta-RS provider that can automatically + * serialize and deserialize resources for + * JSON content type (MediaType). + *

+ * Also continues supporting functionality, such as + * exception mappers that can simplify handling of + * error conditions. + *

+ * There are two default provider classes: + *

    + *
  • {@link org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.JacksonJsonProvider} is the basic + * provider configured to use Jackson annotations + *
  • + *
  • {@link org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.JacksonXmlBindJsonProvider} is extension + * of the basic provider, configured to additionally use JAXB annotations, + * in addition to (or in addition of, if so configured) Jackson annotations. + *
  • + *
+ */ +package org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json; diff --git a/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/package-info.java b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/package-info.java new file mode 100644 index 00000000000..ace9e239fc8 --- /dev/null +++ b/media/json-jackson3/src/main/java/org/glassfish/jersey/jackson3/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2013, 2018, 2026 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 media type processing based on Jackson 3. + */ +package org.glassfish.jersey.jackson3; diff --git a/media/json-jackson3/src/main/resources/META-INF/NOTICE.markdown b/media/json-jackson3/src/main/resources/META-INF/NOTICE.markdown new file mode 100644 index 00000000000..7b1f957d2b9 --- /dev/null +++ b/media/json-jackson3/src/main/resources/META-INF/NOTICE.markdown @@ -0,0 +1,37 @@ +# Notice for Jersey Json Jackson3 module +This content is produced and maintained by the Eclipse Jersey project. + +* https://projects.eclipse.org/projects/ee4j.jersey + +## Trademarks +Eclipse Jersey is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +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 + +## Source Code +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jersey + +## Third-party Content + +Jackson JAX-RS Providers version 3.0.4 +* License: Apache License, 2.0 +* Project: https://github.com/FasterXML/jackson-jakarta-rs-providers +* Copyright: (c) 2009-2025 FasterXML, LLC. All rights reserved unless otherwise indicated. diff --git a/media/json-jackson3/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable b/media/json-jackson3/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable new file mode 100644 index 00000000000..cf5b4a4f4b5 --- /dev/null +++ b/media/json-jackson3/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable @@ -0,0 +1 @@ +org.glassfish.jersey.jackson3.internal.JacksonAutoDiscoverable diff --git a/media/json-jackson3/src/main/resources/org/glassfish/jersey/jackson3/localization.properties b/media/json-jackson3/src/main/resources/org/glassfish/jersey/jackson3/localization.properties new file mode 100644 index 00000000000..a5d50c6f8f8 --- /dev/null +++ b/media/json-jackson3/src/main/resources/org/glassfish/jersey/jackson3/localization.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023, 2025, 2026 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.configuring=Error configuring the DefaultJacksonJaxbJsonProvider: {0}. +error.modules.not.loaded=Jackson modules could not be loaded: {0} \ No newline at end of file diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJacksonXmlBindJsonProviderTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJacksonXmlBindJsonProviderTest.java new file mode 100644 index 00000000000..9309a9786fb --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJacksonXmlBindJsonProviderTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020, 2022, 2026 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.jackson3.internal; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson3.JacksonFeature; +import org.glassfish.jersey.jackson3.internal.model.NoCtorPojo; +import org.glassfish.jersey.jackson3.internal.model.ServiceTest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.core.Application; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class DefaultJacksonXmlBindJsonProviderTest extends JerseyTest { + + @Override + protected final Application configure() { + return new ResourceConfig(ServiceTest.class) + .property(CommonProperties.JSON_JACKSON_DISABLED_MODULES, "NoCtorDeserModule"); + } + + // Exclude NoCtorDeserModule because of dependencies + @Override + protected void configureClient(ClientConfig config) { + config.property(CommonProperties.JSON_JACKSON_DISABLED_MODULES, "NoCtorDeserModule"); + } + + @Test + public final void testSimpleGet() { + final ServiceTest.EntityTest response = target("entity/simple").request().get(ServiceTest.EntityTest.class); + assertEquals("Hello", response.getName()); + assertEquals("World", response.getValue().orElse("")); + } + + @Test + public final void testSimplePost() { + Entity request = Entity.json(new ServiceTest.EntityTest("Hello", "World")); + try (Response response = target("entity/exchange").request(MediaType.APPLICATION_JSON).post(request)) { + assertEquals(200, response.getStatus()); + ServiceTest.EntityTest responseEntity = response.readEntity(ServiceTest.EntityTest.class); + assertEquals("Hello", responseEntity.getName()); + assertEquals("Universe", responseEntity.getValue().orElse("")); + } + } +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForAnotherEnabledModuleTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForAnotherEnabledModuleTest.java new file mode 100644 index 00000000000..3dbd16eb191 --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForAnotherEnabledModuleTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2026 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.jackson3.internal; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Providers; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson3.JacksonFeature; +import org.glassfish.jersey.jackson3.internal.model.NoCtorPojo; +import org.glassfish.jersey.jackson3.internal.model.NoCtorServiceTest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import tools.jackson.core.JacksonException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +// Test to verify that enabling one module will remove others. Should not able to deserialize NoCtorResponse +public class DefaultJsonJacksonProviderForAnotherEnabledModuleTest extends JerseyTest { + + @Override + protected final Application configure() { + return new ResourceConfig(NoCtorServiceTest.class) + .property(CommonProperties.JSON_JACKSON_ENABLED_MODULES, "MrBeanModule"); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(JacksonFeature.class) + .register(TestJacksonXmlBindJsonProvider.class) + .property(CommonProperties.JSON_JACKSON_ENABLED_MODULES, "MrBeanModule"); + } + + @Test + public final void testAnotherEnabledModule() { + try (Response response = target("entity/simple").request().get()) { + assertEquals(200, response.getStatus()); + assertThrows(JacksonException.class, () -> response.readEntity(NoCtorPojo.class)); + } + } + + @Test + public final void testExchangeAnotherEnabledModule() { + Entity request = Entity.json(new NoCtorPojo("Hello", "World")); + try (Response response = target("entity/exchange").request(MediaType.APPLICATION_JSON).post(request)) { + assertEquals(400, response.getStatus()); + assertTrue(response.readEntity(String.class).startsWith("Cannot construct instance of")); + } + } + + + static class TestJacksonXmlBindJsonProvider extends DefaultJacksonXmlBindJsonProvider { + + private final Configuration configuration; + + @Inject + public TestJacksonXmlBindJsonProvider(@Context Providers providers, @Context Configuration configuration) { + super(providers, configuration); + this.configuration = configuration; + } + + @PostConstruct + public void checkModulesCount() { + final String enabledModules = + CommonProperties.getValue(configuration.getProperties(), + configuration.getRuntimeType(), + CommonProperties.JSON_JACKSON_ENABLED_MODULES, String.class); + assertEquals("MrBeanModule", enabledModules); + } + + } + +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForDisabledModulesTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForDisabledModulesTest.java new file mode 100644 index 00000000000..b99b55a7e84 --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForDisabledModulesTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2026 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.jackson3.internal; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Providers; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson3.JacksonFeature; +import org.glassfish.jersey.jackson3.internal.model.NoCtorPojo; +import org.glassfish.jersey.jackson3.internal.model.NoCtorServiceTest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DatabindException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +// Test to verify that disabled modules are registered. Should not able to deserialize NoCtorResponse +public class DefaultJsonJacksonProviderForDisabledModulesTest extends JerseyTest { + + @Override + protected final Application configure() { + return new ResourceConfig(NoCtorServiceTest.class) + .property(CommonProperties.JSON_JACKSON_DISABLED_MODULES, "NoCtorDeserModule"); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(JacksonFeature.class) + .register(TestJacksonXmlBindJsonProvider.class) + .property(CommonProperties.JSON_JACKSON_DISABLED_MODULES, "NoCtorDeserModule"); + } + + @Test + public final void testDisabledModule() { + try (Response response = target("entity/simple").request().get()) { + assertEquals(200, response.getStatus()); + assertThrows(DatabindException.class, () -> response.readEntity(NoCtorPojo.class)); + } + } + + @Test + public final void testExchangeDisabledModule() { + Entity request = Entity.json(new NoCtorPojo("Hello", "World")); + try (Response response = target("entity/exchange").request(MediaType.APPLICATION_JSON).post(request)) { + assertEquals(400, response.getStatus()); + assertTrue(response.readEntity(String.class).startsWith("Cannot construct instance of")); + } + } + + + private static class TestJacksonXmlBindJsonProvider extends DefaultJacksonXmlBindJsonProvider { + + private final Configuration configuration; + + @Inject + public TestJacksonXmlBindJsonProvider(@Context Providers providers, @Context Configuration configuration) { + super(providers, configuration); + this.configuration = configuration; + } + + @PostConstruct + public void checkModulesCount() { + final String disabledModules = + CommonProperties.getValue(configuration.getProperties(), + configuration.getRuntimeType(), + CommonProperties.JSON_JACKSON_DISABLED_MODULES, String.class); + assertEquals("NoCtorDeserModule", disabledModules); + } + + } + + +} \ No newline at end of file diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForEnabledModulesTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForEnabledModulesTest.java new file mode 100644 index 00000000000..90e9ed6cbd4 --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForEnabledModulesTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026 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.jackson3.internal; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Providers; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson3.JacksonFeature; +import org.glassfish.jersey.jackson3.internal.model.NoCtorPojo; +import org.glassfish.jersey.jackson3.internal.model.NoCtorServiceTest; +import org.glassfish.jersey.message.MessageProperties; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; +import tools.jackson.core.StreamReadConstraints; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +// Test to verify that enabled modules are registered. Should be able to deserialize NoCtorResponse +public class DefaultJsonJacksonProviderForEnabledModulesTest extends JerseyTest { + + @Override + protected final Application configure() { + return new ResourceConfig(NoCtorServiceTest.class) + .property(CommonProperties.JSON_JACKSON_ENABLED_MODULES, "NoCtorDeserModule"); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(JacksonFeature.class) + .register(TestJacksonXmlBindJsonProvider.class) + .property(CommonProperties.JSON_JACKSON_ENABLED_MODULES, "NoCtorDeserModule"); + } + + + @Test + public final void testEnabledModule() { + try (Response response = target("entity/simple").request().get()) { + assertEquals(200, response.getStatus()); + NoCtorPojo responseEntity = response.readEntity(NoCtorPojo.class); + assertEquals("Hello", responseEntity.name); + assertEquals("World", responseEntity.value); + } + } + + @Test + public final void testExchangeEnabledModule() { + Entity request = Entity.json(new NoCtorPojo("Hello", "World")); + try (Response response = target("entity/exchange").request(MediaType.APPLICATION_JSON).post(request)) { + assertEquals(200, response.getStatus()); + NoCtorPojo responseEntity = response.readEntity(NoCtorPojo.class); + assertEquals("Howdy", responseEntity.name); + assertEquals("World", responseEntity.value); + } + } + + static class TestJacksonXmlBindJsonProvider extends DefaultJacksonXmlBindJsonProvider { + + private final Configuration configuration; + + @Inject + public TestJacksonXmlBindJsonProvider(@Context Providers providers, @Context Configuration configuration) { + super(providers, configuration); + this.configuration = configuration; + } + + @PostConstruct + public void checkModulesCount() { + final String enabledModules = + CommonProperties.getValue(configuration.getProperties(), + configuration.getRuntimeType(), + CommonProperties.JSON_JACKSON_ENABLED_MODULES, String.class); + assertEquals("NoCtorDeserModule", enabledModules); + } + + } + +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForNoModulePropertiesTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForNoModulePropertiesTest.java new file mode 100644 index 00000000000..f77f04ea656 --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/DefaultJsonJacksonProviderForNoModulePropertiesTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 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.jackson3.internal; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson3.JacksonFeature; +import org.glassfish.jersey.jackson3.internal.model.NoCtorPojo; +import org.glassfish.jersey.jackson3.internal.model.NoCtorServiceTest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// Test will succeed since NoCtorDeserModule is imported with test scope +public class DefaultJsonJacksonProviderForNoModulePropertiesTest extends JerseyTest { + + @Override + protected final Application configure() { + return new ResourceConfig(NoCtorServiceTest.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(JacksonFeature.class); + } + + @Test + public final void testModuleNotSpecified() { + try (Response response = target("entity/simple").request().get()) { + assertEquals(200, response.getStatus()); + NoCtorPojo responseEntity = response.readEntity(NoCtorPojo.class); + assertEquals("Hello", responseEntity.name); + assertEquals("World", responseEntity.value); + } + } + + @Test + public final void testExchangeModuleNotSpecified() { + Entity request = Entity.json(new NoCtorPojo("Hello", "World")); + try (Response response = target("entity/exchange").request(MediaType.APPLICATION_JSON).post(request)) { + assertEquals(200, response.getStatus()); + NoCtorPojo responseEntity = response.readEntity(NoCtorPojo.class); + assertEquals("Howdy", responseEntity.name); + assertEquals("World", responseEntity.value); + } + } + +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/JacksonJaxb2JsonProviderTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/JacksonJaxb2JsonProviderTest.java new file mode 100644 index 00000000000..6de83bcaeba --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/JacksonJaxb2JsonProviderTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022, 2026 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.jackson3.internal; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson3.internal.model.Jaxb2ServiceTest; +import org.glassfish.jersey.jackson3.internal.model.ServiceTest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class JacksonJaxb2JsonProviderTest extends JerseyTest { + + @Override + protected final Application configure() { + return new ResourceConfig(Jaxb2ServiceTest.class) + .property(CommonProperties.JSON_JACKSON_DISABLED_MODULES, "NoCtorDeserModule"); + } + + // Exclude NoCtorDeserModule because of dependencies + @Override + protected void configureClient(ClientConfig config) { + config.property(CommonProperties.JSON_JACKSON_DISABLED_MODULES, "NoCtorDeserModule"); + } + + @Test + public final void testJavaOptional() { + final String response = target("entity/simple").request().get(String.class); + assertEquals("{\"name\":\"Hello\",\"value\":\"World\"}", response); + } + + @Test + public final void testSimpleGet() { + final Jaxb2ServiceTest.EntityTest response = target("entity/simple").request().get(Jaxb2ServiceTest.EntityTest.class); + assertEquals("Hello", response.getName()); + assertEquals("World", response.getValue().orElse("")); + } + + @Test + public final void testSimplePost() { + Entity request = Entity.json(new Jaxb2ServiceTest.EntityTest("Hello", "World")); + try (Response response = target("entity/exchange").request(MediaType.APPLICATION_JSON).post(request)) { + assertEquals(200, response.getStatus()); + Jaxb2ServiceTest.EntityTest responseEntity = response.readEntity(Jaxb2ServiceTest.EntityTest.class); + assertEquals("Hello", responseEntity.getName()); + assertEquals("Universe", responseEntity.getValue().orElse("")); + } + } + +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/JakartaRSFeatureTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/JakartaRSFeatureTest.java new file mode 100644 index 00000000000..58a3ea71207 --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/JakartaRSFeatureTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024, 2026 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.jackson3.internal; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Providers; +import org.glassfish.jersey.jackson3.JacksonFeature; +import org.glassfish.jersey.jackson3.JakartaRSFeatureJsonMapper; +import org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.cfg.JakartaRSFeature; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class JakartaRSFeatureTest { + @Test + public void testJakartaRsFeatureOnJacksonFeature() { + Client client = ClientBuilder.newClient() + .register(new JacksonFeature().jakartaRSFeature(JakartaRSFeature.READ_FULL_STREAM, false)) + .register(JakartaRsFeatureFilter.class); + + try (Response r = client.target("http://xxx.yyy").request().get()) { + var s = r.readEntity(String.class); + MatcherAssert.assertThat(r.getStatus(), Matchers.is(200)); + } + } + + @Test + public void testJakartaRsFeatureOnContextResolver() { + Client client = ClientBuilder.newClient() + .register(JacksonFeature.class) + .register(JakartaRsFeatureContextResolver.class) + .register(JakartaRsFeatureFilter.class); + + try (Response r = client.target("http://xxx.yyy").request().get()) { + MatcherAssert.assertThat(r.getStatus(), Matchers.is(200)); + } + } + + + public static class JakartaRsFeatureFilter implements ClientRequestFilter { + private final DefaultJacksonXmlBindJsonProvider jacksonProvider; + @Inject + public JakartaRsFeatureFilter(Providers allProviders) { + jacksonProvider = (DefaultJacksonXmlBindJsonProvider) + allProviders.getMessageBodyReader(Object.class, Object.class, null, MediaType.APPLICATION_JSON_TYPE); + try { + jacksonProvider.readFrom(Object.class, Object.class, null, MediaType.APPLICATION_JSON_TYPE, null, + new ByteArrayInputStream("{}".getBytes())); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + Response.Status status = jacksonProvider.isEnabled(JakartaRSFeature.READ_FULL_STREAM) + ? Response.Status.FORBIDDEN + : Response.Status.OK; + requestContext.abortWith(Response.status(status).build()); + } + } + + public static class JakartaRsFeatureContextResolver implements ContextResolver { + + @Override + public JsonMapper getContext(Class type) { + JakartaRSFeatureJsonMapper jsonMapper = new JakartaRSFeatureJsonMapper(); + jsonMapper.disable(JakartaRSFeature.READ_FULL_STREAM); + return jsonMapper; + } + } +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/StreamReadConstrainsTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/StreamReadConstrainsTest.java new file mode 100644 index 00000000000..c166321d66a --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/StreamReadConstrainsTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2023, 2024, 2026 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.jackson3.internal; + +import com.fasterxml.jackson.annotation.JsonGetter; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import tools.jackson.core.json.JsonFactory; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.Version; +import tools.jackson.core.exc.StreamConstraintsException; +import tools.jackson.core.json.PackageVersion; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.StringNode; + +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.ExceptionMapper; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson3.JacksonFeature; +import org.glassfish.jersey.message.MessageProperties; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +public class StreamReadConstrainsTest extends JerseyTest { + + private static final String ERROR_MSG_PART = "maximum allowed ("; + + @Override + protected final Application configure() { + return new ResourceConfig(TestLengthResource.class, + MyStreamReadConstraints.class, + MyStreamReadConstraintsExceptionMapper.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(JacksonFeature.class); + } + + @Test + void testNumberLength() { + try (Response response = target("len/entity").request() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(new MyEntity(3), MediaType.APPLICATION_JSON_TYPE))) { + Assertions.assertEquals(200, response.getStatus()); + JsonNode entity = response.readEntity(JsonNode.class); + Assertions.assertEquals("1234", entity.get("value").asText()); + } + + try (Response response = target("len/entity").request() + .post(Entity.entity(new MyEntity(8), MediaType.APPLICATION_JSON_TYPE))) { + Assertions.assertEquals(200, response.getStatus()); + String errorMsg = response.readEntity(String.class); + Assertions.assertTrue(errorMsg.contains(ERROR_MSG_PART + 4)); + } + } + + @Test + void testStringLengthUsingProperty() { + testConstraintOnClient( + client() + .property(MessageProperties.JSON_MAX_STRING_LENGTH, 4) + .target(getBaseUri()) + .path("len/strlen"), + 4 + ); + } + + @Test + void testStringLengthPriorityProperty() { + testConstraintOnClient( + ClientBuilder.newClient() + .register(JacksonFeature.withExceptionMappers().maxStringLength(30)) + .property(MessageProperties.JSON_MAX_STRING_LENGTH, "3" /* check string value */) + .target(getBaseUri()).path("len/strlen"), + 3); + } + + @Test + void testStringLengthUsingFeature() { + testConstraintOnClient( + ClientBuilder.newClient() + .register(JacksonFeature.withExceptionMappers().maxStringLength(3)) + .target(getBaseUri()) + .path("len/strlen"), + 3 + ); + } + + void testConstraintOnClient(WebTarget target, int expectedLength) { + try (Response response = target.request().post(Entity.entity(expectedLength + 1, MediaType.APPLICATION_JSON_TYPE))) { + Assertions.assertEquals(200, response.getStatus()); + + JsonNode errorMsg = response.readEntity(JsonNode.class); + Assertions.fail("StreamConstraintsException has not been thrown"); + } catch (StreamConstraintsException ex) { + if (!StreamConstraintsException.class.isInstance(ex)) { + throw ex; + } + String errorMsg = ex.getMessage(); + Assertions.assertTrue(errorMsg.contains(ERROR_MSG_PART + String.valueOf(expectedLength))); + } + StreamReadConstraints.Builder builder = StreamReadConstraints.builder() + .maxStringLength(StreamReadConstraints.DEFAULT_MAX_STRING_LEN); + StreamReadConstraints.overrideDefaultStreamReadConstraints(builder.build()); + } + + @Test + void testMatchingVersion() { + final Version coreVersion = PackageVersion.VERSION; + final Version jerseyVersion = org.glassfish.jersey.jackson3.internal.jackson.jakarta.rs.json.PackageVersion.VERSION; + + StringBuilder message = new StringBuilder(); + message.append("Dependency Jackson Version is ") + .append(coreVersion.getMajorVersion()) + .append(".") + .append(coreVersion.getMinorVersion()); + message.append("\n Repackaged Jackson Version is ") + .append(jerseyVersion.getMajorVersion()) + .append(".") + .append(jerseyVersion.getMinorVersion()); + + Assertions.assertEquals(coreVersion.getMajorVersion(), jerseyVersion.getMajorVersion(), message.toString()); + Assertions.assertEquals(coreVersion.getMinorVersion(), jerseyVersion.getMinorVersion(), message.toString()); + Assertions.assertEquals(coreVersion.getMajorVersion(), 3, + "update " + DefaultJacksonXmlBindJsonProvider.class.getName() + + " updateFactoryConstraints method to support version " + coreVersion.getMajorVersion()); + } + + @Test + void testStreamReadConstraintsMethods() { + String message = "There are additional methods in Jackson's StreamReaderConstraints.Builder." + + " Please update the code in " + DefaultJacksonXmlBindJsonProvider.class.getName() + + " updateFactoryConstraints method"; + Method[] method = StreamReadConstraints.Builder.class.getDeclaredMethods(); + // 2.17 : five setMax... + build() methods + // 2.18 : six setMax... + build() methods + Assertions.assertEquals(7, method.length, message); + } + + @Path("len") + public static class TestLengthResource { + @POST + @Path("number") + @Produces(MediaType.APPLICATION_JSON) + public MyEntity number(int len) { + return new MyEntity(len); + } + + @POST + @Path("strlen") + @Produces(MediaType.APPLICATION_JSON) + public JsonNode string(int len) { + return new StringNode(String.valueOf(new MyEntity(len).getValue())); + } + + + @POST + @Path("entity") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public MyEntity number(MyEntity entity) { + return new MyEntity(4); + } + } + + static class MyEntity { + + private int value; + + // For Jackson + MyEntity() { + } + + MyEntity(int length) { + int val = 0; + for (int i = 1, j = 1; i != length + 1; i++, j++) { + if (j == 10) { + j = 0; + } + val = 10 * val + j; + } + this.value = val; + } + + @JsonGetter("value") + public int getValue() { + return value; + } + } + + static class MyStreamReadConstraintsExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(StreamConstraintsException exception) { + return Response.ok().entity(exception.getMessage()).build(); + } + } + + static class MyStreamReadConstraints implements ContextResolver { + + @Override + public JsonMapper getContext(Class type) { + JsonFactory jsonFactory = JsonFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder().maxNumberLength(4).build()) + .build(); + return JsonMapper.builder(jsonFactory).findAndAddModules().build(); + } + } +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/Jaxb2ServiceTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/Jaxb2ServiceTest.java new file mode 100644 index 00000000000..7a91855d8cd --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/Jaxb2ServiceTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020, 2022, 2026 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.jackson3.internal.model; + +import com.fasterxml.jackson.annotation.JsonGetter; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import jakarta.xml.bind.annotation.XmlElement; +import java.util.Optional; + +@Path("/entity/") +public final class Jaxb2ServiceTest { + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/simple") + public final EntityTest simple() { + return new EntityTest("Hello", "World"); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("/exchange") + public EntityTest exchange(EntityTest entity) { + return new EntityTest(entity.getName(), "Universe"); + } + + public static final class EntityTest { + + private String name; + + private String value; + + public EntityTest() { + // For deserialization without NoCtorDeserModule + } + + public EntityTest(final String name, final String value) { + this.name = name; + this.value = value; + } + + @XmlElement(name = "jaxb") + @JsonGetter("name") + public final String getName() { + return name; + } + + @JsonGetter("value") + public final Optional getValue() { + return Optional.ofNullable(value); + } + } +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/NoCtorPojo.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/NoCtorPojo.java new file mode 100644 index 00000000000..14f383a02bd --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/NoCtorPojo.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 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.jackson3.internal.model; + +// Simple POJO that will fail deserialization without NoCtorDeserModule +public class NoCtorPojo { + + public String name; + public String value; + + public NoCtorPojo(String name, String value) { + this.name = name; + this.value = value; + } + +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/NoCtorServiceTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/NoCtorServiceTest.java new file mode 100644 index 00000000000..b5cf4fb3ae3 --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/NoCtorServiceTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026 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.jackson3.internal.model; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import jakarta.ws.rs.core.MediaType; + +@Path("/entity/") +public final class NoCtorServiceTest { + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/simple") + public NoCtorPojo simple() { + return new NoCtorPojo("Hello", "World"); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Path("/exchange") + public NoCtorPojo exchange(NoCtorPojo request) { + return new NoCtorPojo("Howdy", request.value); + } + +} diff --git a/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/ServiceTest.java b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/ServiceTest.java new file mode 100644 index 00000000000..5958d27eecf --- /dev/null +++ b/media/json-jackson3/src/test/java/org/glassfish/jersey/jackson3/internal/model/ServiceTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2020, 2022, 2026 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.jackson3.internal.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.xml.bind.annotation.XmlElement; + +import java.util.Optional; + +@Path("/entity/") +public final class ServiceTest { + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/simple") + public EntityTest simple() { + return new EntityTest("Hello", "World"); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("/exchange") + public EntityTest exchange(EntityTest entity) { + return new EntityTest(entity.getName(), "Universe"); + } + + public static final class EntityTest { + + private String name; + private String value; + + public EntityTest() { + // For deserialization without NoCtorDeserModule + } + + public EntityTest(final String name, final String value) { + this.name = name; + this.value = value; + } + + @XmlElement(name = "jaxb") + @JsonGetter("name") + public final String getName() { + return name; + } + + @JsonGetter("value") + public final Optional getValue() { + return Optional.ofNullable(value); + } + } + + + + +} diff --git a/media/pom.xml b/media/pom.xml index 3d988b757b0..1b0daa60a4a 100644 --- a/media/pom.xml +++ b/media/pom.xml @@ -1,7 +1,7 @@ 3.3.2.Final diff --git a/tests/integration/jackson-14/pom.xml b/tests/integration/jackson-14/pom.xml index 8be015fd342..6f0f7ed7f3a 100644 --- a/tests/integration/jackson-14/pom.xml +++ b/tests/integration/jackson-14/pom.xml @@ -1,7 +1,7 @@ + + + 4.0.0 + + + org.glassfish.jersey.tests.performance.testcases + project + ${revision} + + + json-jackson3 + jar + + jersey-tests-performance-json-jackson3-test + + + UTF-8 + + + + + + jakarta.ws.rs + jakarta.ws.rs-api + provided + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + test + + + + org.glassfish.jersey.media + jersey-media-json-jackson3 + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + **/* + + + **/* + + + + + + diff --git a/tests/performance/test-cases/mbw-json-jackson3/src/main/java/org/glassfish/jersey/tests/performance/mbw/json/JakartaRsApplication.java b/tests/performance/test-cases/mbw-json-jackson3/src/main/java/org/glassfish/jersey/tests/performance/mbw/json/JakartaRsApplication.java new file mode 100644 index 00000000000..01a543bd44b --- /dev/null +++ b/tests/performance/test-cases/mbw-json-jackson3/src/main/java/org/glassfish/jersey/tests/performance/mbw/json/JakartaRsApplication.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2015, 2020, 2026 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.performance.mbw.json; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.ws.rs.core.Application; + +import org.glassfish.jersey.jackson3.JacksonFeature; + +/** + * Test case JAX-RS application. + * + * @author Jakub Podlesak + */ +public class JakartaRsApplication extends Application { + + static final Set> APP_CLASSES = new HashSet>() { + { + add(JsonEntityResource.class); + add(JacksonFeature.class); + } + }; + + @Override + public Set> getClasses() { + return APP_CLASSES; + } +} diff --git a/tests/performance/test-cases/mbw-json-jackson3/src/main/java/org/glassfish/jersey/tests/performance/mbw/json/JsonEntityResource.java b/tests/performance/test-cases/mbw-json-jackson3/src/main/java/org/glassfish/jersey/tests/performance/mbw/json/JsonEntityResource.java new file mode 100644 index 00000000000..b6523d3c812 --- /dev/null +++ b/tests/performance/test-cases/mbw-json-jackson3/src/main/java/org/glassfish/jersey/tests/performance/mbw/json/JsonEntityResource.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, 2020, 2026 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.performance.mbw.json; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; + +/** + * Test resource. + * + * @author Jakub Podlesak + */ +@Path("/") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class JsonEntityResource { + + @POST + public Person echo(final Person person) { + return person; + } + + @PUT + public void put(final Person person) { + } + + @GET + public Person get() { + return new Person("Wolfgang", 21, "Salzburg"); + } +} diff --git a/tests/performance/test-cases/mbw-json-jackson3/src/main/java/org/glassfish/jersey/tests/performance/mbw/json/Person.java b/tests/performance/test-cases/mbw-json-jackson3/src/main/java/org/glassfish/jersey/tests/performance/mbw/json/Person.java new file mode 100644 index 00000000000..391fdd77ea5 --- /dev/null +++ b/tests/performance/test-cases/mbw-json-jackson3/src/main/java/org/glassfish/jersey/tests/performance/mbw/json/Person.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, 2019, 2026 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.performance.mbw.json; + +import java.util.Objects; + +/** + * Test data bean. + * + * @author Jakub Podlesak + */ +public class Person { + + public String name; + public int age; + public String address; + + public Person(String name, int age, String address) { + this.name = name; + this.age = age; + this.address = address; + } + + public Person() { + } + + @Override + public int hashCode() { + int hash = 7; + hash = 19 * hash + Objects.hashCode(this.name); + hash = 19 * hash + this.age; + hash = 19 * hash + Objects.hashCode(this.address); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Person other = (Person) obj; + if (!Objects.equals(this.name, other.name)) { + return false; + } + if (this.age != other.age) { + return false; + } + return (Objects.equals(this.address, other.address)); + } +} diff --git a/tests/performance/test-cases/mbw-json-jackson3/src/main/webapp/WEB-INF/web.xml b/tests/performance/test-cases/mbw-json-jackson3/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..dcf21a6564b --- /dev/null +++ b/tests/performance/test-cases/mbw-json-jackson3/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,38 @@ + + + + + + + JakartaRsApplication + org.glassfish.jersey.servlet.ServletContainer + + jakarta.ws.rs.Application + org.glassfish.jersey.tests.performance.mbw.json.JakartaRsApplication + + 1 + + + JakartaRsApplication + /* + + diff --git a/tests/performance/test-cases/mbw-json-jackson3/src/test/java/org/glassfish/jersey/tests/performance/mbw/json/JsonEntityTest.java b/tests/performance/test-cases/mbw-json-jackson3/src/test/java/org/glassfish/jersey/tests/performance/mbw/json/JsonEntityTest.java new file mode 100644 index 00000000000..98451c80459 --- /dev/null +++ b/tests/performance/test-cases/mbw-json-jackson3/src/test/java/org/glassfish/jersey/tests/performance/mbw/json/JsonEntityTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015, 2022, 2026 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.performance.mbw.json; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson3.JacksonFeature; + +import org.glassfish.jersey.test.JerseyTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +/** + * Test for json resource. + * + * @author Jakub Podlesak + */ +public class JsonEntityTest extends JerseyTest { + + @Override + protected Application configure() { + return new JakartaRsApplication(); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(JacksonFeature.class); + } + + @Test + public void testGet() { + final Person getResponse = target().request().get(Person.class); + assertEquals("Wolfgang", getResponse.name); + assertEquals(21, getResponse.age); + assertEquals("Salzburg", getResponse.address); + } + + @Test + public void testPost() { + final Person[] testData = new Person[] {new Person("Joseph", 23, "Nazareth"), new Person("Mary", 18, "Nazareth") }; + for (Person original : testData) { + final Person postResponse = target().request().post(Entity.json(original), Person.class); + assertEquals(original, postResponse); + } + } + + @Test + public void testPut() { + final Response putResponse = target().request().put(Entity.json(new Person("Jules", 12, "Paris"))); + assertEquals(204, putResponse.getStatus()); + } +} diff --git a/tests/performance/test-cases/mbw-json-moxy/src/main/webapp/WEB-INF/web.xml b/tests/performance/test-cases/mbw-json-moxy/src/main/webapp/WEB-INF/web.xml index 28637cdbe34..75c4ab36be2 100644 --- a/tests/performance/test-cases/mbw-json-moxy/src/main/webapp/WEB-INF/web.xml +++ b/tests/performance/test-cases/mbw-json-moxy/src/main/webapp/WEB-INF/web.xml @@ -1,7 +1,7 @@