Table of Contents
jakarta.annotation.security
) annotationsSupport 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.
Jersey entity filtering feature is supported via Jersey extension modules listed in Section 21.8, “Modules with support for Entity Data Filtering”.
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.1.9</version> </dependency>
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 Feature
s which you can register into server/client
runtime in prior to use Entity Filtering in an application:
Filtering based on entity-filtering annotations (or i.e. external configuration file) created using @EntityFiltering meta-annotation.
SecurityEntityFilteringFeature
Filtering based on security (jakarta.annotation.security
) and entity-filtering
annotations.
SelectableEntityFilteringFeature
Filtering based on dynamic and configurable query parameters.
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):
EntityFilteringFeature.ENTITY_FILTERING_SCOPE - "jersey.config.entityFiltering.scope
"
Defines one or more annotations that should be used as entity-filtering scope when reading/writing an entity.
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.
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(); } }
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.
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" }
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()})); } }
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).
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.
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:
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:
Implementations of this SPI are invoked to process entity class and its members. Custom implementations can extend from AbstractEntityProcessor.
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(); }
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:
To be able to obtain an instance of a filtering object model your provider understands and can act on. The implementations can extend AbstractObjectProvider.
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); } } } }
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.
To see a complete working examples of entity-filtering feature refer to the: