Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom parameter types with Optional<T> #4799

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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