2
votes

I'm trying to implement a method in a dropwizard resource, that will service a call from a JS frontend (that uses DataTables).

The request has query parameters that look like this:

columns[0][data]=0&columns[0][name]=&columns[0][searchable]=false&columns[0][orderable]=false&columns[0][search][value]=&columns[0][search][regex]=false

columns[1][data]=iata&columns[1][name]=iata&columns[1][searchable]=true&columns[1][orderable]=true&columns[1][search][value]=&columns[1][search][regex]=false

The request comes from a JS frontend implemented with DataTables, and uses server-side processing. Info about how datatables sends the requests here:

https://datatables.net/manual/server-side

I'm having issues defining the data type for the above query parameters. With spring data, we can define it as:

List<Map<String, String>> columns

which can be wrapped in an object annotated with ModelAttribute and it will deserialize fine.

In my app I'm using an older version of dropwizard which depends on jersey 1.19. I've tried annotating it as a QueryParam, but the app fails at startup.

Method:

@Path("/mappings")
@GET
@Timed
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response getMappings(@QueryParam("columns") List<Map<String, String>> columns) {
  // processing here.
}

When I do this, I get:

ERROR [2016-11-07 14:16:13,061] com.sun.jersey.spi.inject.Errors: The following errors and warnings have been detected with resource and/or provider classes: SEVERE: Missing dependency for method public javax.ws.rs.core.Response com.ean.gds.proxy.ams.application.resource.gui.IataMappingGuiResource.getMappings(java.util.List) at parameter at index 0 WARN [2016-11-07 14:16:13,070] /: unavailable

My question is: do I have any option other than writing a custom deserializer for it ?

Note: If I grab the request with @Context, I can see that the decodedQueryParams are a MultivaluedMap, which maps String keys like "columns[0][data]" to Lists of String values, which always have a single element, that is the value.

Update: After some digging, I found the following JAX-RS specification (section 3.2) which explains why my approach isn't valid to begin with:

The following types are supported:

  1. Primitive Types

  2. Types that have a constructor that accepts a single String argument.

  3. Types that have a static method named valueOf with a single String argument.

  4. List, Set, or SortedSet where T satisfies 2 or 3 above.

Source: Handling Multiple Query Parameters in Jersey

So I've tried using just a List instead. This doesn't crash the app at startup, but when the request comes in, it deserializes into an empty list. So the question remains as to what approach is correct.

1
This is something you're going to have to parse manually. Or find a library that knows how to parse it. Jersey is not smart enough for thisPaul Samsotha
Does this client library not allow you to send the data in JSON format? If you need to stick to query parameters instead of sending it in the body, you can still parse the JSON a lot more easily then you would be able to do that current format. I know most JS datatable libraries do allow for JSON formatPaul Samsotha
@peeskillet Unfortunately it doesn't. I don't have any control over how the library sends this data in the request. I'm currently just going for a custom parser. Thanks !rares.urdea

1 Answers

3
votes

In fact, you're using such a very different structure from all the common ones we have mapped for Rest Web Services consummation. Also, because of this structural compliance problem, trying to use JSON to marshal/unmarshal the values won't suit, once we haven't object-based parameters being transported.

But, we have a couple of options to "work this situation around". Let's see:

  1. Going with the @QueryParam strategy is not possible because of two main reasons:

    • As you noticed, there are some limitations on its use regarding Collections other than Lists, Sets, etc;
    • This annotation maps one (or a list) of param(s) by its(their) name(s), so you need every single parameter (separated by &) to have the same name. It's easier when we think about a form that submits (via GET) a list of checkboxes values: once they all have the same name property, they'll be sent in "name=value1&name=value2" format.

    So, in order to get this requirement, you'd have to make something like:

    @GET
    public Response getMappings(@QueryParam("columns") List<String> columns) {
        return Response.status(200).entity(columns).build();
    }
    
    // URL to be called (with same param names): 
    // /mappings?columns=columns[1][name]=0&columns=columns[0][searchable]=false
    
    // Result: [columns[1][name]=0, columns[0][searchable]=false]
    

    You can also try creating a Custom Java Type for Param Annotations, like you see here. That would avoid encoding problems, but in my tests it didn't work for the brackets issue. :(

  2. You can use regex along with @Path annotation defining what is going to be accepted by a String parameter. Unfortunately, your URL would be composed by unvalid characteres (like the brackets []), which means your server is going to return a 500 error.

    One alternative for this is if you "replace" this chars for valid ones (like underscore character, e.g.):

    /mappings/columns_1_=0&columns_1__name_=
    

    This way, the solution can be applied with no worries:

    @GET
    @Path("/{columns: .*}")
    public Response getMappings(@PathParam("columns") String columns) {
        return Response.status(200).entity(columns).build();
    }
    
    // Result: columns_1_=0&columns_1__name_=
    
  3. A much better way to do this is through UriInfo object, as you may have tried. This is simpler because there's no need to change the URL and params. The object has a getQueryParameters() that returns a Map with the param values:

    @GET
    public Response getMappings(@Context UriInfo uriInfo) {
        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
    
        // In case you want to get the whole generated string
        String query = uriInfo.getRequestUri().getQuery();
    
        String output = "QueryParams: " + queryParams 
                + "<br> Keys: " + queryParams.keySet() 
                + "<br> Values: " + queryParams.values()
                + "<br> Query: " + query;
    
        return Response.status(200).entity(output).build();
    }
    
    // URL: /mappings?columns[1][name]=0&columns[0][searchable]=false
    
    /* Result:
     *  QueryParams: {columns[0][searchable]=[false], columns[1][name]=[0]}
     *  Keys: [columns[0][searchable], columns[1][name]]
     *  Values: [[false], [0]]
     *  Query: columns[1][name]=0&columns[0][searchable]=false
     */
    

    However, you must be aware that if you follow this approach (using a Map) you can't have duplicated keys, once the structure doesn't support it. That's why I include the getQuery() option where you get the whole string.

  4. A last possibility is creating a InjectableProvider, but I can't see many diffs to the getQuery() strategy (since you can split it and create your own map of values).