1

I have a need to filter response entity properties based on parameters provided in the request.

As an example:

GET http://example.org/api/entity/1

would respond with

{ "id": 1234, "foo": "FOO", "bar": "BAR" }

If the request contained a parameter, include=id,foo, I want to only include the properties identified by the parameter:

{ "id": 1234, "foo": "FOO" }

In general, this type of dynamic filtering appears to be possible using Jackson filters, as documented in a few places. What I can't quite figure out is a way to provide the FilterProvider with a reference to the HttpServletRequest (or anything scoped like it).

ObjectMapper mapper = ...
mapper.setFilters(new FilterProvider() {
    public BeanPropertyFilter findFilter(Object filterId) {
        // FIXME: filter out all except those in HttpServletRequest#getParameter("include")
        return SimpleBeanPropertyFilter.filterOutAllExcept(Collections.<String>emptySet());
    }
});

There is a potential solution, and example, which might work if I add a servlet or Jersey filter to configure the ObjectWriterInjector. I'm hoping there's a clearer, more direct approach.

This is all happening in a Dropwizard application, if that happens to make any difference.

Community
  • 1
  • 1
ptomli
  • 11,730
  • 4
  • 40
  • 68
  • Not sure which dropwizard version you're using, but if it's a 0.8.x version, you can see Jersey's [Entity Data Filtering](https://jersey.java.net/documentation/latest/entity-filtering.html). It does what I think you're trying to achieve. You can set the query param name for the filtering. See [example 19.3](https://jersey.java.net/documentation/latest/entity-filtering.html#d0e13997) for configuration. There are a bunch of other things you can do also with this feature. [example usage](https://jersey.java.net/documentation/latest/entity-filtering.html#ef.selectable.annotations) – Paul Samsotha Jul 10 '15 at 11:05
  • While DW 0.8.x is based on Jersey 2.x, which does indeed include EDF, the way DW wires itself up gets in the way of actually using it. The Jersey EDF integration with Jackson is via `FilteringJacksonJaxbJsonProvider` in `jersey-media-json-jackson`. DW doesn't use this for its own `JacksonMessageBodyProvider`, rather subclassing `JacksonJaxbJsonProvider` directly. The Jersey EDF does seem exactly what I'm looking for, so I'll probably persist trying to bang the square peg into the round hole. – ptomli Jul 10 '15 at 12:32
  • There's a issue filed against Dropwizard regarding this https://github.com/dropwizard/dropwizard/issues/1005 – ptomli Jul 14 '15 at 09:47

3 Answers3

0

Let me start with a disclaimer, never worked with DropWizard so I might be off the bat here. But you can do something like this:

@GET
@Path("test")
public String get(@QueryParam("include") List<String> includeList) throws JsonProcessingException {
    MyClass myClass = new MyClass("aaa");

    ObjectMapper mapper = ...;

    FilterProvider filterProvider = new SimpleFilterProvider()
            .addFilter("myFilter", SimpleBeanPropertyFilter.filterOutAllExcept(new HashSet<>(includeList)));

    mapper.setFilters(filterProvider);

    return mapper.writeValueAsString(myClass);
}

Where the request will be:

GET http://example.org/api/entity/1

You can of course keep the request as in your question and add:

@GET
@Path("test")
public String get2(@QueryParam("include") String includeList) throws JsonProcessingException {
    MyClass myClass = new MyClass("testing123");

    String[] arr = includeList.split(",");
    ObjectMapper mapper = ...;

    FilterProvider filterProvider = new SimpleFilterProvider()

            .addFilter("myFilter", SimpleBeanPropertyFilter.filterOutAllExcept(new HashSet<>(Arrays.asList(arr))));
    mapper.setFilters(filterProvider);

    return mapper.writeValueAsString(myClass);
}

The problem with this solution is your return String and not the object itself which makes testing harder.

Uri Shalit
  • 2,198
  • 2
  • 19
  • 29
0

Ok, hold on to your hat: this gets bit involved. :)

Jackson 2.3 added general-purpose extensions for JAX-RS provider, which allow deep customization of ObjectReader and ObjectWriter that are used for reading and writing JSON (and other formats). Addition was via this issue:

https://github.com/FasterXML/jackson-jaxrs-providers/issues/33

but the gist of it is that you can register ObjectReaderInjector and/or ObjectWriterInjector, during processing of a request: typically this is done from doFilter() method of javax.servlet.Filter, before DropWizard invokes Jackson to read request, write response. Injectors, if any registered, are then called right before read/write, to let your code make any changes it wants to. Instances of ObjectReader and ObjectWriter are used instead of ObjectMapper since they are fully thread-safe; you can not safely reconfigure ObjectMapper, but reader/writer instances are safe to reconfigure using various "withXxx()" calls (just remember that they create new instances and do NOT modify instance methods are called on!).

An example of this, via above issue:

https://github.com/FasterXML/jackson-jaxrs-providers/commit/ddf256f6aaaf1d669c1c4323c5359cc20cf40654

shows how it all ties together. Code is not as simple as one would hope, but it is general-purpose and very powerful way to customize all aspects of reading requests, writing responses.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • Using the `ObjectWriterInjector` was the one way I could see of doing this, but I was really looking for a simpler way. – ptomli Jul 14 '15 at 09:44
  • Alas, JAX-RS does not make it easy to do such configuration. But if you think a declarative way -- adding a new Jackson JAX-RS provider annotation to indicate filter provider to use, by specifying class to instantiate -- feel free to file an RFE at https://github.com/FasterXML/jackson-jaxrs-providers/issues. Adding support should be relatively simple actually. – StaxMan Jul 14 '15 at 23:59
  • It gets a bit more nasty when you get into it. The `jersey-media-json-jackson` artifact includes a `ServiceLocator` auto-discoverable `Feature`, which then totally overrides the Dropwizard config. Additionally, the filtering implementation appears to be somewhat buried in `FilteringJacksonJaxbJsonProvider`, which is declared final. And finally, Dropwizard doesn't allow any control of the `JacksonJsonProvider` it uses, simply instantiating it's own variant. Cue reimplementing 4 classes :( – ptomli Jul 20 '15 at 13:26
  • Hmmh. I wouldn't think you need to change JAX-RS json provider (you just ensure proper version is included via build definition, like maven pom.xml). `jersey-media-json-jackson` sounds like something to completely avoid (as far as I know). Test cases I built for injecting functionality (part of jackson jaxrs provider project) do run on jersey via dropwizard. It may also be that older versions had more issues; I am using Jackson 2.5 with DropWizard 0.9-rc2 – StaxMan Jul 20 '15 at 22:08
  • I ended up looking at `jersey-media-json-jackson` as that seemed to be the artifact providing Jackson/Jersey EDF integration. I'm using DW 0.8.1 right now, but if integrating EDF is easier/possible in 0.9 then I'll look at making that transition. Do you have an example? I'm missing it in the issue 33 link above, though my grip on the Jersey 2 architecture is tenuous at best – ptomli Jul 21 '15 at 09:20
  • @ptomli I am not sure what EDF means in this context? But the working sample of filtering is in `json/src/test/java/com/fasterxml/jackson/jaxrs/json/dw/TestWriteModifications.java` of that commit. It needs to configure `ObjectWriterModifier` to use, which is done from a standard Servlet filter, registered via Jetty hook that DropWizard exposes. – StaxMan Jul 21 '15 at 18:22
  • EDF here is Jersey Entity Data Filtering https://jersey.java.net/documentation/latest/entity-filtering.html. §19.5 of that reference discusses the dynamic filtering, which I'm trying to achieve. The integration rabbit hole starts here https://github.com/jersey/jersey/blob/master/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/FilteringJacksonJaxbJsonProvider.java. Are we talking at cross purposes? – ptomli Jul 22 '15 at 10:16
  • Ah. I think that's the mixup: I am talking about basic Servlet filters, although I presume there may be other ways to be able to inject things into context -- at any rate, functionality does not need access to the Entity in question, but only needs to be called before Jersey invokes `MessageBodyReader` and/or `MessageBodyWriter`. – StaxMan Jul 22 '15 at 18:40
0

You can get object mapper from environment parameter inside run method of MyApplication class:

public class MyApplication extends Application<MyApplicationConfiguration> {
    @Override
    public void run(MyApplicationConfiguration configuration, Environment environment) throws Exception {
        environment.getObjectMapper().setFilterProvider(new FilterProvider() {
            public BeanPropertyFilter findFilter(Object filterId) {
                //filter code here
            }
        });
    }
}
Harun
  • 667
  • 7
  • 13