Support for Entity Filtering in Jersey introduces a convenient facility for reducing the amount of data exchanged over the wire between client and server without a need to create specialized data view components. The main idea behind this feature is to give you means that will let you selectively filter out any non-relevant data from the model before sending the data to the other party.
Entity Data Filtering is not a new feature but so far the support was limited to MOXy JSON provider. Since Jersey 2.16, we support also Jackson (2.x) JSON processor and in this article we’re going to take a look into it in more detail.
What do I need?
Assuming that you’re already using Jackson 2.x support in your application via jersey-media-json-jackson module all you have to do is to update the version to at least 2.16 (-SNAPSHOT since Jersey 2.16 is not released at the time of writing this article). A new module, jersey-entity-filtering, would be transitively added to your class-path. With this setup you have all the dependencies needed to leverage the support of Entity Filtering.
Registering/Enabling Entity Filtering feature is done by registering
- JacksonFeature
- one of the entity filtering features (described below)
in your application, For example,
@ApplicationPath("/")
public class MyApplication extends ResourceConfig {
public MyApplication() {
// ...
register(EntityFilteringFeature.class);
// register(SecurityEntityFilteringFeature.class);
// register(SelectableEntityFilteringFeature.class);
register(JacksonFeature.class);
}
}
How can I filter data with it?
Certainly, the most important question. Entity Filtering can be used in your application in three ways, via
- Entity Filtering Annotations,
- Security (javax.annotation.security) Annotations, or
- Query Parameters (Dynamic Filtering).
Each of these ways comes with a fully working example already available on GitHub. The examples are, by default, configured to work with MOXy JSON provider but enabling Jackson instead of MOXy is very simple.
There are exactly the following lines
// Configure MOXy Json provider. Comment this line to use Jackson. Uncomment to use MOXy.
register(new MoxyJsonConfig().setFormattedOutput(true).resolver());
// Configure Jackson Json provider. Comment this line to use MOXy. Uncomment to use Jackson.
// register(JacksonFeature.class);
in an Jersey application class present in every example. Comment the first highlighted line and uncomment the second highlighted line in the class. That’s all, now you’re using Jackson to work with JSON in your application.
Entity Filtering Annotations
Full example available on GitHub: entity-filtering
In it’s nature the Entity Filtering feature is designed in a very similar way the Name Binding in JAX-RS is. Simply create an annotation that could be attached to domain classes and resource methods (resource classes), bind these two concepts together and let the framework act upon it. Nothing too difficult. In fact, the simplest scenario consists of
- registering Jersey’s EntityFilteringFeature (and media provider) in your application
- creating custom filtering annotation, and
- applying this annotation to model and resource methods.
The first step is described in previous sections. The more interesting second step is to create a custom annotation and make Jersey to recognize it as entity filtering annotation. This is done by using meta-annotation @EntityFiltering.
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EntityFiltering
public @interface ProjectDetailedView {
// ...
}
So far so good. And now, the third step. Use this new annotation to annotate the model …
public class Project {
private Long id;
private String name;
private String description;
@ProjectDetailedView
private List<Task> tasks;
@ProjectDetailedView
private List<User> users;
// getters and setters
}
… and a resource class.
@Path("projects")
@Produces("application/json")
public class ProjectsResource {
@GET
public List<Project> getProjects() {
return getDetailedProjects();
}
@GET
@Path("detailed")
@ProjectDetailedView
public List<Project> getDetailedProjects() {
return EntityStore.getProjects();
}
}
And … done! So what do we have here?
We defined two views on our Project class – basic and detailed. “Basic” (or default) view is implicit and contains all non-annotated fields (id, name, description). Detailed view is explicit and is defined by placing @ProjectDetailedView over some of the fields (classes/getters/setters/property-accessors). This view contains all of the basic view fields (since there are no other constraints defined) as well as annotated fields (tasks, users).
To bind (or assign) views together with resource methods we also use entity filtering annotations. If a resource method should return a detailed view on our model (returned instance) we simply annotate the method with an entity filtering annotation (see ProjectsResource#getDetailedProjects()). If we don’t annotate a resource method with any entity filtering annotation then basic (or default) view is assumed and only the fields tied with this view are sent over the wire.
To summarize this, even though the same instance of Project class is used to create response for both basic and detailed view the actual data sent over the wire differ for each of these two views.
Basic View on a Project (JSON representation):
{
"description" : "Jersey is the open source (under dual CDDL+GPL license) JAX-RS 2.0 (JSR 339) production quality Reference Implementation for building RESTful Web services.",
"id" : 1,
"name" : "Jersey"
}
Detailed View on a Project (JSON representation):
{
"description" : "Jersey is the open source (under dual CDDL+GPL license) JAX-RS 2.0 (JSR 339) production quality Reference Implementation for building RESTful Web services.",
"id" : 1,
"name" : "Jersey",
"tasks" : [ {
"description" : "Entity Data Filtering",
"id" : 1,
"name" : "ENT_FLT"
}, {
"description" : "OAuth 1 + 2",
"id" : 2,
"name" : "OAUTH"
} ],
"users" : [ {
"email" : "very@secret.com",
"id" : 1,
"name" : "Jersey Robot"
} ]
}
More information about this approach is available in Jersey User Guide:
Security Annotations
Full example available on GitHub: entity-filtering-security
I have already written an article on Filtering JAX-RS Entities with Standard Security Annotations so if you’re interested in filtering data based on user roles, please, read that blog post.
Query Parameters (Dynamic Filtering)
Full example available on GitHub: entity-filtering-selectable
Filtering the content sent over the wire dynamically based on query parameters is another commonly required use case. We need to register SelectableEntityFilteringFeature in the application and optionally define the name of the query parameter (via SelectableEntityFilteringFeature.QUERY_PARAM_NAME property) that would be used to list desired fields for further processing (marshalling).
And that’s all from the code point of view. Easy, isn’t it.
So lets rather take a look at some examples. Assume our domain model contains following classes:
public class Person {
private String givenName;
private String familyName;
private String honorificSuffix;
private String honorificPrefix;
// same name as Address.region
private String region;
private List<Address> addresses;
private Map<String, PhoneNumber> phoneNumbers;
// ...
}
public class Address {
private String streetAddress;
private String region;
private PhoneNumber phoneNumber;
// ...
}
public class PhoneNumber {
private String areaCode;
private String number;
// ...
}
The resource class is very simple as well:
@Path("people")
@Produces("application/json")
public class PersonResource {
@GET
@Path("{id}")
public Person getPerson() {
// ...
return person;
}
}
As you can see no entity filtering annotations are used this time and even though the same instance of Person class is used to create response the given values of select
query parameter are used to select the fields that would be transferred over the wire.
For people/1234?select=familyName,givenName
the response looks like:
{
"familyName": "Dowd",
"givenName": "Andrew"
}
And for people/1234?select=familyName,givenName,addresses.phoneNumber.number
the response would be:
{
"addresses":[
{
"phoneNumber":{
"number": "867-5309"
}
}
],
"familyName": "Dowd",
"givenName": "Andrew"
}
More information about this approach is available in Jersey User Guide:
Hope you’ll enjoy the Entity Data Filtering feature with Jackson JSON provider and if you have any comments or suggestions, please comment in the discussion below or send me a note at michal@dejavu.sk.