11

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.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
JorgeGRC
  • 1,032
  • 2
  • 18
  • 37
  • @peeskillet please read carefully the question, as this is not a duplicate. The other question regards using the slash at the end of the request but in my question I ask for getting rid of it. – JorgeGRC Sep 24 '15 at 16:20
  • Sorry. I don't think you can get rid of it. I've explored the possibility a few times and couldn't find an answer. Maybe there is one who knows. I would just make two methods. Doesn't really make sense to me two have just one method. One method for the collection, and one for the single resource. – Paul Samsotha Sep 24 '15 at 16:24
  • And actually it does answer your question as it is making the slash optional using a regex. Did you even try?. I guess I jumped to quick again. The problem I was trying to figure out was bit different – Paul Samsotha Sep 24 '15 at 16:27
  • @peeskillet Yes, indeed I tried the regex but with no luck, the slash was still mandatory. Did it work for you? Maybe I did something wrong. – JorgeGRC Sep 25 '15 at 07:27
  • I have never used it in real code (for the reason mentioned above), but answering questions, I've tested it many times, and it should work. Did you use something like `myMethod{id: (/\\d+)?}`? The `?` behind the parenthesis says that everything include in the parenthesis is optional. So `/` followed by digits is allowed but not required. – Paul Samsotha Sep 25 '15 at 07:31
  • `@Path("/myMethod{id : (/id)?}")` throws 404 when I try to access `http://localhost:8080/my_domain/rest/api/myMethod/1` but it lets me access `http://localhost:8080/my_domain/rest/api/myMethod` and `http://localhost:8080/my_domain/rest/api/myMethod/` . This is how it is pictured on the `application.wadl` : `` – JorgeGRC Sep 25 '15 at 07:58
  • Do you know what `(/id)?` means in regex? You are saying that only `/id` is allowed. So you can only access `/mymthod/id`. You need to actually use a regex that will match what you want. See my previous comment. `\\d+` means any number of digits – Paul Samsotha Sep 25 '15 at 08:04
  • @peeskillet yes, I did try with your regex suggestion but didn't work when I passed a param in the request `(http://localhost:8080/my_domain/rest/api/myMethod/1)` but worked when there was no param and/or no slash `(http://localhost:8080/my_domain/rest/api/myMethod)`, `(http://localhost:8080/my_domain/rest/api/myMethod/)` – JorgeGRC Sep 25 '15 at 08:15
  • I see the problem. It won't work with integer parameters. Here's what happens. When we use `(/\\d+)`, the `/` is part of the `{id}` param. So when we use `@PathParam("id") int id`, it tries to parse `/1` to an int. When an exception is thrown during parsing, it leads to a 404. Only options going this way is to use String param, and remove the leading `/` and parse it yourself. A headache compared to simply having two resource methods. That is definitely the way I recommend :-) – Paul Samsotha Sep 25 '15 at 08:31
  • Hm, that could be... can you check if this regex is correct for my purpouse? I can't access the method although I changed the param to `String`: `@GET @Path("myMethod{id : (/\\d+)?}") @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") public String myMethod(@PathParam("id") String id, @Context HttpHeaders hh)` – JorgeGRC Sep 25 '15 at 08:41
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/90591/discussion-between-peeskillet-and-jorgegrc). – Paul Samsotha Sep 25 '15 at 08:43

2 Answers2

14

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.

ccleve
  • 15,239
  • 27
  • 91
  • 157
11

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.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • 3
    Great and elaborated answer. Thanks a lot for your time :) – JorgeGRC Sep 25 '15 at 09:20
  • I'm facing now a problem related to this solution and the regex expression... When making a `POST` request with form params to the same path used in the `GET` (two different web services, one is triggered by a `POST` request and the other by a `GET` request, but they share the same path: `/routes`) it throws a `405 Method not Allowed`. While debugging on my browser I found that this `405` response comes with the param `Allow: GET, OPTIONS`. Have you got any idea of what can be causing this behaviour? If I remove the regex expression from the `GET`, it correctly runs the `POST` service. – JorgeGRC Sep 30 '15 at 08:39
  • You should probably post another question with complete details – Paul Samsotha Sep 30 '15 at 08:44