JorgeGRC
JorgeGRC

Reputation: 1052

Optional params in REST API request using Jersey 2.21

I'm playing around with Jersey 2.21 and I'd like to know if it's possible to have an "optional" param which can, or not, be present in the request made to the server.

I want to successfully access this two methods:

http://localhost:8080/my_domain/rest/api/myMethod/1
http://localhost:8080/my_domain/rest/api/myMethod

As you can see, I'm trying to make the integer (id) param an optional one.

I've declared myMethod as follows:

@GET
@Path("myMethod/{id}")
@Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
public String myMethod(@PathParam("id") Integer id, @Context HttpHeaders hh)

This works:

http://localhost:8080/my_domain/rest/api/myMethod/1

and this works too:

http://localhost:8080/my_domain/rest/api/myMethod/

but this won't work and I don't understand why. It throws a 404 Not Found error:

http://localhost:8080/my_domain/rest/api/myMethod

Can you point me in the right direction to work this out? I don't like the slash being mandatory on all my REST method calls and would like to suppress it if possible.

Upvotes: 11

Views: 9049

Answers (2)

ccleve
ccleve

Reputation: 15779

There is a way easier way to do this:

@GET
@Path("myMethod/{id}")
public String myMethod(@PathParam("id") Integer id) {
}

@GET
@Path("myMethod")
public String myMethod() {
  return myMethod(null);
}

No tricky regex required.

Upvotes: 14

Paul Samsotha
Paul Samsotha

Reputation: 208974

So after some dabbling around with some of the answers in Optional @PathParam in Jax-RS, the problem is that using this

@Path("/myMethod{id: (/\\d+)?}") 
public Response get(@PathParam("id") int id) {}

causes the / to be in the capture group. So when Jersey tries to parse /1 it will get an exception and send a 404. We could use a String, but then it gets ugly, as we need to get rid of the leading / and parse it ourselves.

@Path("/myMethod{id: (/\\d+)?}") 
public Response get(@PathParam("id") String id) {
    id = id.replace("/", "");
    int parsed = Integer.parseInt(id);
}

The other solution I came up with (the one that works for the OP), is to separate the / from the digits into two different path expressions, so that the leading / is not captured in the actual id and doesn't fail in parsing

@Path("/method{noop: (/)?}{id: ((?<=/)\\d+)?}")
public Response get(@PathParam("id") int id) {}

The {noop: (/)?} captures the optional /. And the {id: ((?<=/)\\d+)?} uses a positive lookbehind, saying that the numbers (\\d+) are allowed if and only if there is a / before it ((?<=/)). This is necessary as the / is optional. If we didn't use this assertion, then /myMethod123 would be allowed.

Here is a complete test case using Jersey Test Framework

public class OptionalParamTest extends JerseyTest {

    @Path("optional")
    public static class Resource {
        @GET
        @Path("/method{noop: (/)?}{id: ((?<=/)\\d+)?}")
        public String get(@PathParam("id") int id) {
            return String.valueOf(id);
        }
    }

    @Override
    public ResourceConfig configure() {
        return new ResourceConfig(Resource.class);
    }

    @Test
    public void should_return_id_1() {
        Response response = target("optional/method/1").request().get();
        System.out.println("status=" + response.getStatus());
        assertEquals("1", response.readEntity(String.class));
    }

    @Test
    public void should_return_id_0_with_no_id() {
        Response response = target("optional/method").request().get();
        assertEquals(200, response.getStatus());
        assertEquals("0", response.readEntity(String.class));
    }

    @Test
    public void should_return_404_with_numbers_and_no_slash() {
        Response response = target("optional/method12").request().get();
        assertEquals(404, response.getStatus());
    } 

    @Test
    public void should_return_404_with_numbers_and_letters() {
        Response response = target("optional/method/12b").request().get();
        assertEquals(404, response.getStatus());
    }

    @Test
    public void should_return_404_with_only_letters() {
        Response response = target("optional/method/ab").request().get();
        assertEquals(404, response.getStatus());
    } 
}

Here's the dependency for the test

<dependency>
    <groupId>org.glassfish.jersey.test-framework.providers</groupId>
    <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
    <version>${jersey2.version}</version>
    <scope>test</scope>
</dependency>

EDIT

For the tests, it would be better to use a boxed Integer instead of an int as the method parameter. With the former you would be able to do a null check, instead of receiving the default 0 for the primitive.

Upvotes: 11

Related Questions