Add support for custom parameter types with `Optional<T>`
Closes #4798
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 affd12a..c5eff7f 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 @@
@Override
public void init(InjectionManager injectionManager, BootstrapBag bootstrapBag) {
InstanceBinding<ParamConverters.AggregatedProvider> 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 3f39654..e4249b8 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 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 @@
if (value == null) {
return (T) Optional.empty();
} else {
- ParameterizedType parametrized = (ParameterizedType) genericType;
- Type type = parametrized.getActualTypeArguments()[0];
- T val = aggregated.getConverter((Class<T>) 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<ClassTypePair> 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 @@
/**
* 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 @@
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 85e647b..e6546de 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 testDateParamConverterIsChosenForDateString() {
initiateWebApplication();
final ParamConverter<Date> 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 ebbc92a..5306b21 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 {
@@ -51,6 +62,18 @@
}
@GET
+ @Path("/fromDate")
+ public Response fromDate(@QueryParam(PARAM_NAME) Optional<Date> data) throws ParseException {
+ return Response.ok(data.orElse(new Date(1609459200000L))).build();
+ }
+
+ @GET
+ @Path("/fromInstant")
+ public Response fromInstant(@QueryParam(PARAM_NAME) Optional<Instant> 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<Optional<Integer>> data) {
StringBuilder builder = new StringBuilder("");
@@ -61,9 +84,41 @@
}
}
+ @Provider
+ public static class InstantParamConverterProvider implements ParamConverterProvider {
+ @Override
+ public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
+ if (rawType.equals(Instant.class)) {
+ return new ParamConverter<T>() {
+ @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 @@
}
@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));
}