Links: Table of Contents | Single HTML

Chapter 21. Entity Data Filtering

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 APIs that will let you to selectively filter out any non-relevant data from the marshalled object model before sending the data to the other party based on the context of the particular message exchange. This way, only the necessary or relevant portion of the data is transferred over the network with each client request or server response, without a need to create special facade models for transferring these limited subsets of the model data.

Entity filtering feature allows you to define your own entity-filtering rules for your entity classes based on the current context (e.g. matched resource method) and keep these rules in one place (directly in your domain model). With Jersey entity filtering facility it is also possible to assign security access rules to entity classes properties and property accessors.

We will first explain the main concepts and then we will explore the entity filtering feature topics from a perspective of basic use-cases,

as well as some more complex ones.

Note

Jersey entity filtering feature is supported via Jersey extension modules listed in Section 21.8, “Modules with support for Entity Data Filtering”.

21.1. Enabling and configuring Entity Filtering in your application

Entity Filtering support in Jersey is provided as an extension module and needs to be mentioned explicitly in your pom.xml file (in case of using Maven):

<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-entity-filtering</artifactId>
    <version>3.0.16</version>
</dependency>

Note

If you're not using Maven make sure to have also all the transitive dependencies (see jersey-entity-filtering) on the classpath.

The entity-filtering extension module provides three Features which you can register into server/client runtime in prior to use Entity Filtering in an application:

If you want to use both entity-filtering annotations and security annotations for entity data filtering it is enough to register SecurityEntityFilteringFeature as this feature registers also EntityFilteringFeature.

Entity-filtering currently recognizes one property that can be passed into the Configuration instance (client/server):

Note

Processing of entity-filtering annotations to create an entity-filtering scope is defined by following: "Request/Resource entity annotations" > "Configuration" > "Resource method/class annotations" (on server).

You can configure entity-filtering on server (basic + security examples) as follows:

Example 21.1. Registering and configuring entity-filtering feature on server.

new ResourceConfig()
    // Set entity-filtering scope via configuration.
    .property(EntityFilteringFeature.ENTITY_FILTERING_SCOPE, new Annotation[] {ProjectDetailedView.Factory.get()})
    // Register the EntityFilteringFeature.
    .register(EntityFilteringFeature.class)
    // Further configuration of ResourceConfig.
    .register( ... );


Example 21.2. Registering and configuring entity-filtering feature with security annotations on server.

new ResourceConfig()
    // Set entity-filtering scope via configuration.
    .property(EntityFilteringFeature.ENTITY_FILTERING_SCOPE, new Annotation[] {SecurityAnnotations.rolesAllowed("manager")})
    // Register the SecurityEntityFilteringFeature.
    .register(SecurityEntityFilteringFeature.class)
    // Further configuration of ResourceConfig.
    .register( ... );


Example 21.3. Registering and configuring entity-filtering feature based on dynamic and configurable query parameters.

new ResourceConfig()
    // Set query parameter name for dynamic filtering
    .property(SelectableEntityFilteringFeature.QUERY_PARAM_NAME, "select")
    // Register the SelectableEntityFilteringFeature.
    .register(SelectableEntityFilteringFeature.class)
    // Further configuration of ResourceConfig.
    .register( ... );


Use similar steps to register entity-filtering on client:

Example 21.4. Registering and configuring entity-filtering feature on client.

final ClientConfig config = new ClientConfig()
    // Set entity-filtering scope via configuration.
    .property(EntityFilteringFeature.ENTITY_FILTERING_SCOPE, new Annotation[] {ProjectDetailedView.Factory.get()})
    // Register the EntityFilteringFeature.
    .register(EntityFilteringFeature.class)
    // Further configuration of ClientConfig.
    .register( ... );

// Create new client.
final Client client = ClientClientBuilder.newClient(config);

// Use the client.


21.2. Components used to describe Entity Filtering concepts

In the next section the entity-filtering features will be illustrated on a project-tracking application that contains three classes in its domain model and few resources (only Project resource will be shown in this chapter). The full source code for the example application can be found in Jersey Entity Filtering example.

Suppose there are three domain model classes (or entities) in our model: Project, User and Task (getters/setter are omitted for brevity).

Example 21.5. Project

public class Project {

    private Long id;

    private String name;

    private String description;

    private List<Task> tasks;

    private List<User> users;

    // getters and setters
}


Example 21.6. User

public class User {

    private Long id;

    private String name;

    private String email;

    private List<Project> projects;

    private List<Task> tasks;

    // getters and setters
}


Example 21.7. Task

public class Task {

    private Long id;

    private String name;

    private String description;

    private Project project;

    private User user;

    // getters and setters
}


To retrieve the entities from server to client, we have created also a couple of JAX-RS resources from whose the ProjectsResource is shown as example.

Example 21.8. ProjectsResource

@Path("projects")
@Produces("application/json")
public class ProjectsResource {

    @GET
    @Path("{id}")
    public Project getProject(@PathParam("id") final Long id) {
        return getDetailedProject(id);
    }

    @GET
    public List<Project> getProjects() {
        return getDetailedProjects();
    }
}


21.3. Using custom annotations to filter entities

Entity filtering via annotations is based on an @EntityFiltering meta-annotation. This meta-annotation is used to identify entity-filtering annotations that can be then attached to

  • domain model classes (supported on both, server and client sides), and

  • resource methods / resource classes (only on server side)

An example of entity-filtering annotation applicable to a class, field or method can be seen in Example 21.9, “ProjectDetailedView” below.

Example 21.9. ProjectDetailedView

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EntityFiltering
public @interface ProjectDetailedView {

    /**
     * Factory class for creating instances of {@code ProjectDetailedView} annotation.
     */
    public static class Factory
                        extends AnnotationLiteral<ProjectDetailedView>
                        implements ProjectDetailedView {

        private Factory() {
        }

        public static ProjectDetailedView get() {
            return new Factory();
        }
    }
}


Since creating annotation instances directly in Java code is not trivial, it is a good practice to provide an inner annotation Factory class in each custom filtering annotation, through which new instances of the annotation can be directly created. The annotation factory class can be created by extending the HK2 AnnotationLiteral class and implementing the annotation interface itself. It should also provide a static factory method that will create and return a new instance of the Factory class when invoked. Such annotation instances can be then passed to the client and server run-times to define or override entity-filtering scopes.

By placing an entity-filtering annotation on an entity (class, fields, getters or setters) we define a so-called entity-filtering scope for the entity. The purpose of entity-filtering scope is to identify parts of the domain model that should be processed when the model is to be sent over the wire in a particular entity-filtering scope. We distinguish between:

  • global entity-filtering scope (defined by placing filtering annotation on a class itself), and

  • local entity-filtering scope (defined by placing filtering annotation on a field, getter or setter)

Unannotated members of a domain model class are automatically added to all existing global entity-filtering scopes. If there is no explicit global entity-filtering scope defined on a class a default scope is created for this class to group these members.

Creating entity-filtering scopes using custom entity-filtering annotations in domain model classes is illustrated in the following examples.

Example 21.10. Annotated Project

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
}


Example 21.11. Annotated User

public class User {

    private Long id;

    private String name;

    private String email;

    @UserDetailedView
    private List<Project> projects;

    @UserDetailedView
    private List<Task> tasks;

    // getters and setters
}


Example 21.12. Annotated Task

public class Task {

    private Long id;

    private String name;

    private String description;

    @TaskDetailedView
    private Project project;

    @TaskDetailedView
    private User user;

    // getters and setters
}


As you can see in the examples above, we have defined 3 separate scopes using @ProjectDetailedView, @UserDetailedView and @TaskDetailedView annotations and we have applied these scopes selectively to certain fields in the domain model classes.

Once the entity-filtering scopes are applied to the parts of a domain model, the entity filtering facility (when enabled) will check the active scopes when the model is being sent over the wire, and filter out all parts from the model for which there is no active scope set in the given context. Therefore, we need a way how to control the scopes active in any given context in order to process the model data in a certain way (e.g. expose the detailed view). We need to tell the server/client runtime which entity-filtering scopes we want to apply. There are 2 ways how to do this for client-side and 3 ways for server-side:

  • Outbound client request or server response programmatically created with entity-filtering annotations that identify the scopes to be applied (available on both, client and server)

  • Property identifying the applied scopes passed through Configuration (available on both, client and server)

  • Entity-filtering annotations identifying the applied scopes attached to a resource method or class (server-side only)

When the multiple approaches are combined, the priorities of calculating the applied scopes are as follows: Entity annotations in request or response > Property passed through Configuration > Annotations applied to a resource method or class.

In a graph of domain model objects, the entity-filtering scopes are applied to the root node as well as transitively to all the child nodes. Fields and child nodes that do not match at least a single active scope are filtered out. When the scope matching is performed, annotations applied to the domain model classes and fields are used to compute the scope for each particular component of the model. If there are no annotations on the class or its fields, the default scope is assumed. During the filtering, first, the annotations on root model class and its fields are considered. For all composite fields that have not been filtered out, the annotations on the referenced child class and its fields are considered next, and so on.

21.3.1. Server-side Entity Filtering

To pass entity-filtering annotations via Response returned from a resource method you can leverage the Response.ResponseBuilder#entity(Object, Annotation[]) method. The next example illustrates this approach. You will also see why every custom entity-filtering annotation should contain a factory for creating instances of the annotation.

Example 21.13. ProjectsResource - Response entity-filtering annotations

@Path("projects")
@Produces("application/json")
public class ProjectsResource {

    @GET
    public Response getProjects(@QueryParam("detailed") final boolean isDetailed) {
        return Response
                .ok()
                .entity(new GenericEntity<List<Project>>(EntityStore.getProjects()) {},
                        isDetailed ? new Annotation[]{ProjectDetailedView.Factory.get()} : new Annotation[0])
                .build();
    }
}


Annotating a resource method / class is typically easier although it is less flexible and may require more resource methods to be created to cover all the alternative use case scenarios. For example:

Example 21.14. ProjectsResource - Entity-filtering annotations on methods

@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();
    }
}


To see how entity-filtering scopes can be applied using a Configuration property, see the Example 21.1, “Registering and configuring entity-filtering feature on server.” example.

When a Project model from the example above is requested in a scope represented by @ProjectDetailedView entity-filtering annotation, the Project model data sent over the wire would contain:

  • Project - id, name, description, tasks, users

  • Task - id, name, description

  • User - id, name, email

Or, to illustrate this in JSON format:

{
   "description" : "Jersey is the open source (under dual EPL+GPL license) JAX-RS 3.0 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"
   } ]
}

For the default entity-filtering scope the filtered model would look like:

  • Project - id, name, description

Or in JSON format:

{
   "description" : "Jersey is the open source (under dual EPL+GPL license) JAX-RS 3.0 production quality Reference Implementation for building RESTful Web services.",
   "id" : 1,
   "name" : "Jersey"
}

21.3.2. Client-side Entity Filtering

As mentioned above you can define applied entity-filtering scopes using a property set either in the client run-time Configuration (see Example 21.4, “Registering and configuring entity-filtering feature on client.”) or by passing the entity-filtering annotations during a creation of an individual request to be sent to the server.

Example 21.15. Client - Request entity-filtering annotations

ClientBuilder.newClient(config)
    .target(uri)
    .request()
    .post(Entity.entity(project, new Annotation[] {ProjectDetailedView.Factory.get()}));


You can use the mentioned method with client injected into a resource as well.

Example 21.16. Client - Request entity-filtering annotations

@Path("clients")
@Produces("application/json")
public class ClientsResource {

    @Uri("projects")
    private WebTarget target;

    @GET
    public List<Project> getProjects() {
        return target.request()
            .post(Entity.entity(project, new Annotation[] {ProjectDetailedView.Factory.get()}));
    }
}


21.4. Role-based Entity Filtering using (jakarta.annotation.security) annotations

Filtering the content sent to the client (or server) based on the authorized security roles is a commonly required use case. By registering SecurityEntityFilteringFeature you can leverage the Jersey Entity Filtering facility in connection with standard jakarta.annotation.security annotations exactly the same way as you would with custom entity-filtering annotations described in previous chapters. Supported security annotations are:

Although the mechanics of the Entity Data Filtering feature used for the security annotation-based filtering is the same as with the entity-filtering annotations, the processing of security annotations differs in a few important aspects:

  • Custom SecurityContext should be set by a container request filter in order to use @RolesAllowed for role-based filtering of domain model data (server-side)

  • There is no need to provide entity-filtering (or security) annotations on resource methods in order to define entity-filtering scopes for @RolesAllowed that is applied to the domain model components, as all the available roles for the current user are automatically determined using the information from the provided SecurityContext (server-side only).

Note

Instances of security annotations (to be used for programmatically defined scopes either on client or server) can be created using one of the methods in the SecurityAnnotations factory class that is part of the Jersey Entity Filtering API.

21.5. Entity Filtering based on dynamic and configurable query parameters

Filtering the content sent to the client (or server) dynamically based on query parameters is another commonly required use case. By registering SelectableEntityFilteringFeature you can leverage the Jersey Entity Filtering facility in connection with query parameters exactly the same way as you would with custom entity-filtering annotations described in previous chapters.

Example 21.17. Sever - Query Parameter driven entity-filtering

@XmlRootElement
public class Address {

    private String streetAddress;

    private String region;

    private PhoneNumber phoneNumber;
}


Query parameters are supported in comma delimited "dot notation" style similar to BeanInfo objects and Spring path expressions. As an example, the following URL: http://jersey.example.com/addresses/51234?select=region,streetAddress may render only the address's region and street address properties as in the following example:

Example 21.18. 

{
   "region" : "CA",
   "streetAddress" : "1234 Fake St."
}

21.6. Defining custom handling for entity-filtering annotations

To create a custom entity-filtering annotation with special handling, i.e. an field aggregator annotation used to annotate classes like the one in Example 21.19, “Entity-filtering annotation with custom meaning” it is, in most cases, sufficient to implement and register the following SPI contracts:

  • EntityProcessor

    Implementations of this SPI are invoked to process entity class and its members. Custom implementations can extend from AbstractEntityProcessor.

  • ScopeResolver

    Implementations of this SPI are invoked to retrieve entity-filtering scopes from an array of provided annotations.

Example 21.19. Entity-filtering annotation with custom meaning

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@EntityFiltering
public @interface FilteringAggregator {

    /**
     * Entity-filtering scope to add given fields to.
     */
    Annotation filteringScope();

    /**
     * Fields to be a part of the entity-filtering scope.
     */
    String[] fields();
}


21.7. Supporting Entity Data Filtering in custom entity providers or frameworks

To support Entity Data Filtering in custom entity providers (e.g. as in Example 21.20, “Entity Data Filtering support in MOXy JSON binding provider”), it is sufficient in most of the cases to implement and register the following SPI contracts:

  • ObjectProvider

    To be able to obtain an instance of a filtering object model your provider understands and can act on. The implementations can extend AbstractObjectProvider.

  • ObjectGraphTransformer

    To transform a read-only generic representation of a domain object model graph to be processed into an entity-filtering object model your provider understands and can act on. The implementations can extend AbstractObjectProvider.

Example 21.20. Entity Data Filtering support in MOXy JSON binding provider

@Singleton
public class FilteringMoxyJsonProvider extends ConfigurableMoxyJsonProvider {

    @Inject
    private Provider<ObjectProvider<ObjectGraph>> provider;

    @Override
    protected void preWriteTo(final Object object, final Class<?> type, final Type genericType, final Annotation[] annotations,
                              final MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders,
                              final Marshaller marshaller) throws JAXBException {
        super.preWriteTo(object, type, genericType, annotations, mediaType, httpHeaders, marshaller);

        // Entity Filtering.
        if (marshaller.getProperty(MarshallerProperties.OBJECT_GRAPH) == null) {
            final Object objectGraph = provider.get().getFilteringObject(genericType, true, annotations);

            if (objectGraph != null) {
                marshaller.setProperty(MarshallerProperties.OBJECT_GRAPH, objectGraph);
            }
        }
    }

    @Override
    protected void preReadFrom(final Class<Object> type, final Type genericType, final Annotation[] annotations,
                               final MediaType mediaType, final MultivaluedMap<String, String> httpHeaders,
                               final Unmarshaller unmarshaller) throws JAXBException {
        super.preReadFrom(type, genericType, annotations, mediaType, httpHeaders, unmarshaller);

        // Entity Filtering.
        if (unmarshaller.getProperty(MarshallerProperties.OBJECT_GRAPH) == null) {
            final Object objectGraph = provider.get().getFilteringObject(genericType, false, annotations);

            if (objectGraph != null) {
                unmarshaller.setProperty(MarshallerProperties.OBJECT_GRAPH, objectGraph);
            }
        }
    }
}


21.8. Modules with support for Entity Data Filtering

List of modules from Jersey workspace that support Entity Filtering:

In order to use Entity Filtering in mentioned modules you need to explicitly register either EntityFilteringFeature, SecurityEntityFilteringFeature or SelectableEntityFilteringFeature to activate Entity Filtering for particular module.