diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverterConfigurator.java b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverterConfigurator.java index affd12a6f0..c5eff7fb55 100644 --- a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverterConfigurator.java +++ b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverterConfigurator.java @@ -32,7 +32,7 @@ public class ParamConverterConfigurator implements BootstrapConfigurator { @Override public void init(InjectionManager injectionManager, BootstrapBag bootstrapBag) { InstanceBinding aggregatedConverters = - Bindings.service(new ParamConverters.AggregatedProvider()) + Bindings.service(new ParamConverters.AggregatedProvider(injectionManager)) .to(ParamConverterProvider.class); injectionManager.register(aggregatedConverters); } diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java index 3f396549fb..e4249b8139 100644 --- a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java +++ b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java @@ -21,11 +21,11 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.security.AccessController; import java.text.ParseException; import java.util.Date; +import java.util.List; import java.util.Optional; import javax.inject.Inject; @@ -37,6 +37,7 @@ import org.glassfish.jersey.internal.LocalizationMessages; import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.internal.util.collection.ClassTypePair; import org.glassfish.jersey.message.internal.HttpDateFormat; /** @@ -257,11 +258,11 @@ public String toString(final T value) throws IllegalArgumentException { public static class OptionalProvider implements ParamConverterProvider { // Delegates to this provider when the type of Optional is extracted. - private final AggregatedProvider aggregated; + private final InjectionManager manager; @Inject - public OptionalProvider(AggregatedProvider aggregated) { - this.aggregated = aggregated; + public OptionalProvider(InjectionManager manager) { + this.manager = manager; } @Override @@ -273,18 +274,20 @@ public T fromString(String value) { if (value == null) { return (T) Optional.empty(); } else { - ParameterizedType parametrized = (ParameterizedType) genericType; - Type type = parametrized.getActualTypeArguments()[0]; - T val = aggregated.getConverter((Class) type, type, annotations).fromString(value.toString()); - if (val != null) { - return (T) Optional.of(val); - } else { - /* - * In this case we don't send Optional.empty() because 'value' is not null. - * But we return null because the provider didn't find how to parse it. - */ - return null; + final List ctps = ReflectionHelper.getTypeArgumentAndClass(genericType); + final ClassTypePair ctp = (ctps.size() == 1) ? ctps.get(0) : null; + + for (ParamConverterProvider provider : Providers.getProviders(manager, ParamConverterProvider.class)) { + final ParamConverter converter = provider.getConverter(ctp.rawClass(), ctp.type(), annotations); + if (converter != null) { + return (T) Optional.of(value).map(s -> converter.fromString(value)); + } } + /* + * In this case we don't send Optional.empty() because 'value' is not null. + * But we return null because the provider didn't find how to parse it. + */ + return null; } } @@ -313,8 +316,8 @@ public static class AggregatedProvider implements ParamConverterProvider { /** * Create new aggregated {@link ParamConverterProvider param converter provider}. */ - public AggregatedProvider() { - providers = new ParamConverterProvider[] { + public AggregatedProvider(InjectionManager manager) { + this.providers = new ParamConverterProvider[] { // ordering is important (e.g. Date provider must be executed before String Constructor // as Date has a deprecated String constructor new DateProvider(), @@ -323,7 +326,7 @@ public AggregatedProvider() { new CharacterProvider(), new TypeFromString(), new StringConstructor(), - new OptionalProvider(this) + new OptionalProvider(manager) }; } diff --git a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java index 85e647b14a..e6546de6ca 100644 --- a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java +++ b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java @@ -285,7 +285,7 @@ public void testLazyConverter() throws Exception { public void testDateParamConverterIsChosenForDateString() { initiateWebApplication(); final ParamConverter converter = - new ParamConverters.AggregatedProvider().getConverter(Date.class, Date.class, null); + new ParamConverters.AggregatedProvider(null).getConverter(Date.class, Date.class, null); assertEquals("Unexpected date converter provider class", ParamConverters.DateProvider.class, converter.getClass().getEnclosingClass()); diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/OptionalParamConverterTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/OptionalParamConverterTest.java index ebbc92a195..5306b217a5 100644 --- a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/OptionalParamConverterTest.java +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/OptionalParamConverterTest.java @@ -16,6 +16,11 @@ package org.glassfish.jersey.tests.e2e.server; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.text.ParseException; +import java.time.Instant; +import java.util.Date; import java.util.List; import java.util.Optional; @@ -24,12 +29,18 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Application; import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import javax.ws.rs.ext.Provider; +import org.glassfish.jersey.internal.LocalizationMessages; +import org.glassfish.jersey.internal.inject.ExtractorException; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; public class OptionalParamConverterTest extends JerseyTest { @@ -50,6 +61,18 @@ public Response fromInteger(@QueryParam(PARAM_NAME) Optional data) { return Response.ok(data.orElse(0)).build(); } + @GET + @Path("/fromDate") + public Response fromDate(@QueryParam(PARAM_NAME) Optional data) throws ParseException { + return Response.ok(data.orElse(new Date(1609459200000L))).build(); + } + + @GET + @Path("/fromInstant") + public Response fromInstant(@QueryParam(PARAM_NAME) Optional data) { + return Response.ok(data.orElse(Instant.parse("2021-01-01T00:00:00Z")).toString()).build(); + } + @GET @Path("/fromList") public Response fromList(@QueryParam(PARAM_NAME) List> data) { @@ -61,9 +84,41 @@ public Response fromList(@QueryParam(PARAM_NAME) List> data) { } } + @Provider + public static class InstantParamConverterProvider implements ParamConverterProvider { + @Override + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + if (rawType.equals(Instant.class)) { + return new ParamConverter() { + @Override + public T fromString(String value) { + if (value == null) { + throw new IllegalArgumentException(LocalizationMessages.METHOD_PARAMETER_CANNOT_BE_NULL("value")); + } + try { + return rawType.cast(Instant.parse(value)); + } catch (Exception e) { + throw new ExtractorException(e); + } + } + + @Override + public String toString(T value) { + if (value == null) { + throw new IllegalArgumentException(); + } + return value.toString(); + } + }; + } else { + return null; + } + } + } + @Override protected Application configure() { - return new ResourceConfig(OptionalResource.class); + return new ResourceConfig(OptionalResource.class, InstantParamConverterProvider.class); } @Test @@ -77,22 +132,81 @@ public void fromOptionalStr() { } @Test - public void fromOptionalInt() { - Response empty = target("/OptionalResource/fromInteger").request().get(); + public void fromOptionalInteger() { + Response missing = target("/OptionalResource/fromInteger").request().get(); + Response empty = target("/OptionalResource/fromInteger").queryParam(PARAM_NAME, "").request().get(); Response notEmpty = target("/OptionalResource/fromInteger").queryParam(PARAM_NAME, 1).request().get(); + Response invalid = target("/OptionalResource/fromInteger").queryParam(PARAM_NAME, "invalid").request().get(); + assertEquals(200, missing.getStatus()); + assertEquals(Integer.valueOf(0), missing.readEntity(Integer.class)); assertEquals(200, empty.getStatus()); assertEquals(Integer.valueOf(0), empty.readEntity(Integer.class)); assertEquals(200, notEmpty.getStatus()); assertEquals(Integer.valueOf(1), notEmpty.readEntity(Integer.class)); + assertEquals(404, invalid.getStatus()); + assertFalse(invalid.hasEntity()); + } + + @Test + public void fromOptionalDate() { + Response missing = target("/OptionalResource/fromDate").request().get(); + Response empty = target("/OptionalResource/fromDate").queryParam(PARAM_NAME, "").request().get(); + Response notEmpty = target("/OptionalResource/fromDate").queryParam(PARAM_NAME, "Sat, 01 May 2021 12:00:00 GMT") + .request().get(); + Response invalid = target("/OptionalResource/fromDate").queryParam(PARAM_NAME, "invalid").request().get(); + assertEquals(200, missing.getStatus()); + assertEquals(new Date(1609459200000L), missing.readEntity(Date.class)); + assertEquals(404, empty.getStatus()); + assertFalse(empty.hasEntity()); + assertEquals(200, notEmpty.getStatus()); + assertEquals(new Date(1619870400000L), notEmpty.readEntity(Date.class)); + assertEquals(404, invalid.getStatus()); + assertFalse(invalid.hasEntity()); + } + + @Test + public void fromOptionalInstant() { + Response missing = target("/OptionalResource/fromInstant").request().get(); + Response empty = target("/OptionalResource/fromInstant").queryParam(PARAM_NAME, "").request().get(); + Response notEmpty = target("/OptionalResource/fromInstant").queryParam(PARAM_NAME, "2021-05-01T12:00:00Z") + .request().get(); + Response invalid = target("/OptionalResource/fromInstant").queryParam(PARAM_NAME, "invalid").request().get(); + assertEquals(200, missing.getStatus()); + assertEquals("2021-01-01T00:00:00Z", missing.readEntity(String.class)); + assertEquals(404, empty.getStatus()); + assertFalse(empty.hasEntity()); + assertEquals(200, notEmpty.getStatus()); + assertEquals("2021-05-01T12:00:00Z", notEmpty.readEntity(String.class)); + assertEquals(404, invalid.getStatus()); + assertFalse(invalid.hasEntity()); } @Test public void fromOptionalList() { - Response empty = target("/OptionalResource/fromList").request().get(); - Response notEmpty = target("/OptionalResource/fromList").queryParam(PARAM_NAME, 1) + Response missing = target("/OptionalResource/fromList").request().get(); + Response empty = target("/OptionalResource/fromList") + .queryParam(PARAM_NAME, "").request().get(); + Response partiallyEmpty = target("/OptionalResource/fromList") + .queryParam(PARAM_NAME, 1) + .queryParam(PARAM_NAME, "").request().get(); + Response invalid = target("/OptionalResource/fromList") + .queryParam(PARAM_NAME, "invalid").request().get(); + Response partiallyInvalid = target("/OptionalResource/fromList") + .queryParam(PARAM_NAME, 1) + .queryParam(PARAM_NAME, "invalid").request().get(); + Response notEmpty = target("/OptionalResource/fromList") + .queryParam(PARAM_NAME, 1) .queryParam(PARAM_NAME, 2).request().get(); + assertEquals(200, missing.getStatus()); + assertEquals("", missing.readEntity(String.class)); assertEquals(200, empty.getStatus()); - assertEquals("", empty.readEntity(String.class)); + assertEquals("0", empty.readEntity(String.class)); + assertEquals(200, partiallyEmpty.getStatus()); + assertEquals("10", partiallyEmpty.readEntity(String.class)); + assertEquals(404, invalid.getStatus()); + assertFalse(invalid.hasEntity()); + assertEquals(404, partiallyInvalid.getStatus()); + assertFalse(partiallyInvalid.hasEntity()); assertEquals(200, notEmpty.getStatus()); assertEquals("12", notEmpty.readEntity(String.class)); }