Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is it that @MatrixParam values seem to only come from the final segment?

I am exploring the use of MatrixURIs in a JAX-RS application. In the process I've noticed that the @MatrixParam annotated parameters seem to only get the matrix parameters from the final segment (which makes trying to use sub-resource locators a bit painful). I would like to know if this is a bug, or is part of the spec (and why?), or is simply an undefined crack.

I know that I likely could devise a way to use the @Context injected UriInfo object (which does change depending upon where in the locator chain one finds oneself), but this feels ugly.

Thumbing quickly through the JAX-RS 1.1 spec... the closest I could find to this not being supported was around: §3.2

Because injection occurs at object creation time, use of these annotations (with the exception of @Context) on resource class fields and bean properties is only supported for the default per-request resource class lifecycle.

But that's talking about Constructor/Field injection, not method parameters like in §3.3.2:

When a resource method is invoked, parameters annotated with @FormParam or one of the annotations listed in section 3.2 are mapped from the request according to the semantics of the annotation.

But of course the semantics of the annotation are seemingly vague.

Runtime Environment Details:

  • Jersey 1.13, 1.15
  • Tomcat 7.0.29
  • Java 1.6.0_31 (Apple)
  • MacOS X 10.7.5

Example resources:

public class Zero {

    public static final String[] IDS = { "1", "2", "3" };

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> getMe(@Context final UriInfo info) {
        final UriBuilder builder = info.getAbsolutePathBuilder().path("one");
        final UriBuilder two = builder.clone().matrixParam("id", "{one}", "{two}");

        final List<String> links = new ArrayList<String>();
        for (int i = 0; i < IDS.length; i++) {
            final String first = IDS[i];
            for (int j = i + 1; j < IDS.length; j++) {
                final String second = IDS[j];
                links.add(two.build(first, second).toASCIIString());
            }
        }

        final Map<String, Object> toReturn = new HashMap<String, Object>();
        toReturn.put("class", getClass().getSimpleName());
        toReturn.put("next", builder.build().toASCIIString());
        toReturn.put("skip", links);

        return toReturn;
    }

    /*************************/
    /**** <PROBLEM CHILD> ****/
    /*************************/
    @Path("one")
    public Object getNext(@MatrixParam("id") final Set<String> ids) {
        if (ids.isEmpty()) {
            return new One();
        }
        return new Two(ids.toArray(new String[ids.size()]));
    }
    /*************************/
    /**** </PROBLEM CHILD> ***/
    /*************************/

    public static class One {
        @GET
        @Produces(MediaType.APPLICATION_JSON)
        public Map<String, Object> getMe(@Context final UriInfo info) {
            final UriBuilder builder = info.getAbsolutePathBuilder().path("{id}");
            final List<String> links = new ArrayList<String>();
            for (final String id : IDS) {
                links.add(builder.build(id).toASCIIString());
            }
            final Map<String, Object> toReturn = new HashMap<String, Object>();
            toReturn.put("class", getClass().getSimpleName());
            toReturn.put("next", links);
            toReturn.put("last", getLastURI(info));
            return toReturn;
        }

        @Path("{id}")
        public Two getNext(@PathParam("id") final String id) {
            return new Two(id);
        }
    }

    public static class Two {

        private final String[] myids;
        private final Three three;

        public Two(final String... ids) {
            three = new Three(ids);
            myids = ids;
        }

        @GET
        @Produces(MediaType.APPLICATION_JSON)
        public Map<String, Object> getMe(@Context final UriInfo info) {
            final UriBuilder builder = info.getAbsolutePathBuilder().path("three");
            final Map<String, Object> toReturn = new HashMap<String, Object>();
            toReturn.put("class", getClass().getSimpleName());
            toReturn.put("ids", myids);
            toReturn.put("next", builder.build().toASCIIString());
            toReturn.put("last", getLastURI(info));
            return toReturn;
        }

        @Path("three")
        public Three getNext() {
            return three;
        }
    }

    public static class Three {
        private final String[] myids;

        public Three(final String... ids) {
            myids = ids;
        }

        @GET
        @Produces(MediaType.APPLICATION_JSON)
        public Map<String, Object> getMe(@Context final UriInfo info) {
            final Map<String, Object> toReturn = new HashMap<String, Object>();
            toReturn.put("class", getClass().getSimpleName());
            toReturn.put("ids", myids);
            toReturn.put("last", getLastURI(info));
            return toReturn;
        }
    }

    /**
     * Helper method since info.getMatchedURIs doesn't play nice with Matrix Params
     * @param info info object
     * @return parent URI
     */
    public static final String getLastURI(final UriInfo info) {
        final List<PathSegment> segments = info.getPathSegments(false);
        final UriBuilder builder = info.getBaseUriBuilder();
        for (int i = 0; i < segments.size() - 1; i++) {
            final PathSegment segment = segments.get(i);
            builder.path(segment.getPath());
            final MultivaluedMap<String, String> matrixParams = segment.getMatrixParameters();
            if (!matrixParams.isEmpty()) {
                for (final Map.Entry<String, List<String>> param : matrixParams.entrySet()) {
                    final String name = param.getKey();
                    final String[] values = param.getValue().toArray(new String[param.getValue().size()]);
                    builder.matrixParam(name, values);
                }
            }
        }
        return builder.build().toASCIIString();
    }
}

Example Outputs from above:

http://localhost:8080/context/zero/one/2

{
"last":"http://localhost:8080/context/zero/one",
"ids":["2"],
"next":"http://localhost:8080/context/zero/one/2/three",
"class":"Two"
}

http://localhost:8080/context/zero/one/2/three

{
"last":"http://localhost:8080/context/zero/one/2",
"ids":["2"],
"class":"Three"
}

http://localhost:8080/context/zero/one;id=1;id=2

{
"last":"http://localhost:8080/context/zero",
"ids":["2","1"],
"next":"http://localhost:8080/context/zero/one;id=1;id=2/three",
"class":"Two"
}

http://localhost:8080/context/zero/one;id=1;id=2/three

{
"last": "http://localhost:8080/context/zero/one;id=1;id=2",
"ids": ["three"],
"next": "http://localhost:8080/context/zero/one;id=1;id=2/three/three",
"class": "Two"
}
like image 541
Charlie Avatar asked Nov 04 '22 12:11

Charlie


1 Answers

I can't say for certain, but I suspect this is by design. Suppose you have a two level URI like this:

/foo/bar

On both levels one could have a matrix parm named filter. You could end up with a URL like so:

/foo;filter=13/bar;filter=April

In the Jersey resource for that path you'll have one paramter annotated with @MatrixParam("filter"). Which filter parameter should Jersey fill in? What if one or the other is not supplied?

Having just implemented some matrix parameters, I can see the value in the current implementation. Specifying @MatrixParam pulls only the last level. If you want matrix parameters earlier in the URI, specify @PathParameter and have the parameter type be a PathSegment. From the path segment you can pull out the matrix parameters.

Often times, the resource class will have a global @Path, and each method will have it's own @Path adding to the class level setting. In this case it seems to make more sense that @MatrixParameter would apply only to the last path segment.

So, putting this all together (for our example above) yields the following

@Path("{fooPath: foo}")
public class FooResource {

@GET
@Path("bar")
public String getFooBar(@PathParam("fooPath") PathSegment fooPath,
    @MatrixParam("filter") String filter) {
    MultivaluedMap<String, String> mParms = fooPath.getMatrixParameters();
    List<String> parms = mParms.get("filter");
    String fooFilter = parms.get(0);
    // ... the rest of your code
    }
} 
like image 73
Maladon Avatar answered Nov 10 '22 19:11

Maladon