Skip to content

Commit

Permalink
Add support for custom parameter types with Optional<T>
Browse files Browse the repository at this point in the history
Closes #4798
  • Loading branch information
joschi authored and jansupol committed Jul 8, 2021
1 parent 8c9acd6 commit 532dd1e
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class ParamConverterConfigurator implements BootstrapConfigurator {
@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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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
Expand All @@ -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<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;
}
}

Expand Down Expand Up @@ -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(),
Expand All @@ -323,7 +326,7 @@ public AggregatedProvider() {
new CharacterProvider(),
new TypeFromString(),
new StringConstructor(),
new OptionalProvider(this)
new OptionalProvider(manager)
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ public void testLazyConverter() throws Exception {
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {

Expand All @@ -50,6 +61,18 @@ public Response fromInteger(@QueryParam(PARAM_NAME) Optional<Integer> data) {
return Response.ok(data.orElse(0)).build();
}

@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) {
Expand All @@ -61,9 +84,41 @@ public Response fromList(@QueryParam(PARAM_NAME) List<Optional<Integer>> data) {
}
}

@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
Expand All @@ -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));
}
Expand Down

0 comments on commit 532dd1e

Please sign in to comment.