Agrest Server Framework


1. Introduction

Agrest provides a Java server framework for rapid development of RESTful data applications that are compatible with the Agrest Protocol. The framework core is fully independent of specific data sources and REST engines. Integrations with Apache Cayenne ORM and JAX-RS (Jakarta and the older JavaEE) are included out of the box, and a variety of custom backends can be integrated by the user.

Agrest apps can be run on Spring Boot, Bootique or any other environment. The framework plays well with other parts of the application - Agrest REST endpoints can coexist with "regular" REST endpoints without a conflict.

Agrest is model-driven, with customizable "schema" that describes request and response entities. Agrest integrates with Swagger to generate REST documentation in the widely-adopted OpenAPI 3 format.

1.1. Java Version

Java 11 or newer is required.

2. Dependencies

Before we can start writing Agrest apps, we need to include the relevant modules as dependencies. The first step is usually to import the common BOM (a so-called "bill of materials"), that will define the version of all the other modules. It is placed in the <dependencyManagement/> section of the pom.xml:

<dependency>
    <groupId>io.agrest</groupId>
    <artifactId>agrest-bom</artifactId>
    <!-- Replace with a specific 5.x version of Agrest -->
    <version>${agrest.version}</version>
    <scope>import</scope>
    <type>pom</type>
</dependency>

With this out of the way, the rest of the document will show the use of individual Agrest dependencies without having to worry about their version.

While all the build examples are Maven-based, Agrest apps most certainly can be assembled with Gradle or any other Java build tool you might prefer.

3. Runtime

AgRuntime object is the core of Agrest. It provides the strategies to process various types of requests, stores the app data "schema", and serves as an integration point for various data backends. When working in the environments like Jersey / JAX-RS, you may not be interacting with AgRuntime directly, but it is still the object doing all the heavy lifting under the hood. And you will still need to assemble the runtime before you can use Agrest.

This is how you create a bare-bone default runtime:

AgRuntime agRuntime = AgRuntime.build();

Usually you would add at least one backend provider though (such as Apache Cayenne), and often - your own extensions. Internally AgRuntime uses a lightweight dependency-injection container to assemble its services, and can be extended using "modules":

AgRuntime agRuntime = AgRuntime
        .builder()
        .module(b -> b (1)
                .bindMap(JsonValueConverter.class)
                .put(MoneyConverter.class.getName(), MoneyConverter.class))
        .build();
1 Registering a module with support for a custom value type of Money, providing a converter for it to be read from JSON. b is a special Binder object that allows to define or override services in the DI environment.
To see what kinds of services and extensions are available and how to use the Binder API, you may check the source code of io.agrest.runtime.AgCoreModule on GitHub. This is the main module of Agrest, and defines the default behavior for the framework.

3.1. Auto-Loadable Modules

More complex Agrest extensions may be packaged in their own .jar files. Agrest has a mechanism to automatically detect such jars on the classpath, load and merge them into the runtime. To turn your code into an auto-loadable module, add a file called META-INF/services/io.agrest.AgModuleProvider to the classpath of your project (usually under src/main/resources), with a single line that is a fully-qualified class name of the AgModuleProvider implementor. E.g. the same extension as above can be rewritten to be auto-loadable:

META-INF/services/io.agrest.AgModuleProvider
com.foo.MyModuleProvider
public class MyModuleProvider implements AgModuleProvider {

    @Override
    public Module module() {
        return new MyModule();
    }

    @Override
    public Class<? extends Module> moduleType() {
        return MyModule.class;
    }
}
public class MyModule implements Module {

    @Override
    public void configure(Binder binder) {
        binder
                .bindMap(JsonValueConverter.class)
                .put(MoneyConverter.class.getName(), MoneyConverter.class);
    }
}
You may not be writing auto-loadable modules that often for "normal" applications, as it is much easier to create inline modules. But this is a useful mechanism for creating reusable Agrest extensions.

4. Schema

Agrest has a metadata management service called AgSchema with access to models of app data objects, each represented by an AgEntity instance. Each entity contains properties. Those can be one of id, attribute or relationship. Entity and property models define names, data types and data retrieval and access control strategies. Here is how you can obtain an AgSchema singleton, entities and properties in runtime:

AgSchema schema = runtime.service(AgSchema.class); (1)
AgEntity<Book> bookEntity = schema.getEntity(Book.class); (2)
AgAttribute titleAttribute = bookEntity.getAttribute("title"); (3)
1 AgSchema object is a "service" within the Agrest runtime
2 AgEntity is a REST API model corresponding to a specific Java class
3 AgAttribute is one of the 3 property types in the entity model. Among other things it contains a "reader", i.e. resolution strategy.
As a developer, you’d rarely use AgSchema directly. But you still need to define how the schema looks like for the benefit of Agrest runtime. More on this below.
Even though schemas are usually associated with specific Java classes (such as Book or Author), they are not necessarily identical to the structure of those classes, as they represent the client view of the data. Some object properties may be excluded from the REST API, others may be "virtual" and have no match in the object itself.

Let’s looks at the various ways to define the schema in the app. Those are Ag annotations, external metadata and entity overlays. These three approaches are complimentary and can be combined with each other.

4.1. Schema Annotations

@AgId, @AgAttribute and @AgRelationship annotations are used to define which object properties should be included in the default Agrest model. They are applied to "getter" methods of Java classes:

public class Book {

    private long id;
    private String title;
    private String publishStatus;
    private Author author;

    @AgId (1)
    public long getId() {
        return id;
    }

    @AgAttribute(writable = false) (2)
    public String getTitle() {
        return title;
    }

    @AgRelationship (3)
    public Author getAuthor() {
        return author;
    }

    public Book setId(long id) {
        this.id = id;
        return this;
    }

    public Book setTitle(String title) {
        this.title = title;
        return this;
    }

    public Book setAuthor(Author author) {
        this.author = author;
        return this;
    }

    public Book setPublishStatus(String publishStatus) {
        this.publishStatus = publishStatus;
        return this;
    }
}
1 Tags the property as an id
2 Tags the property as an attribute (i.e. the property that is a "value"). Read-only policy is defined.
3 Tags the property as a relationship (i.e. the property pointing to another entity)

4.2. External Metadata

With some Agrest backends, an AgSchema can be auto-generated from another preexisting backend-provided schema. E.g. Apache Cayenne ORM backend automatically compiles AgSchema that directly corresponds to the Cayenne ORM model. Application can then customize it via annotations (e.g. @AgAttribute(readable=false,writable=false) allows to hide a certain property from the REST API), or via entity overlays discussed next, but the bulk of the schema is created without any explicit effort on the part of the developer.

If you are writing your own backend integration, and there is a preexisting schema you’d like Agrest to use, you can implement your own "entity compiler":

class MyEntityCompiler implements AgEntityCompiler {

    @Override
    public <T> AgEntity<T> compile(Class<T> aClass, AgSchema agSchema) {

        return isHandledByMyBackend(aClass)
                ? doCompile(aClass, agSchema) (1)
                : null; (2)
    }
}
1 If the class should be handled by our custom backend, generate an AgEntity in some backend-specific way
2 If the class is not recognized, return null to give some other compiler a chance to provide the schema

Here is how to register the compiler with Agrest runtime:

AgRuntime runtime = AgRuntime
        .builder()
        .module(b -> b
                .bindList(AgEntityCompiler.class)
                .insertBefore(MyEntityCompiler.class, AnnotationsAgEntityCompiler.class))
        .build();

4.3. Entity Overlays

Entity overlays is a manual way to tweak the entities in the schema. As the name implies, overlays are used to change the structure of the existing entities, that were compiled from annotations or via some other mechanism. Overlays is a somewhat verbose, but rather versatile mechanism frequently used in applications. Here is an example of an overlay:

AgEntityOverlay<Author> overlay = AgEntity
        .overlay(Author.class)
        .attribute( (1)
                "age",
                Integer.TYPE,
                a -> Period.between(a.getDateOfBirth(), LocalDate.now()).getYears())
        .writablePropFilter(p -> p.property("dateOfBirth", false)); (2)
1 Adds a new calculated attribute called "age"
2 Adds a policy forbidding changing "dateOfBirth" attribute

Overlays can be applied globally, affecting the entire runtime. Here they redefine the schema that was created from annotations or from external metadata:

AgRuntime runtime = AgRuntime
        .builder()
        .entityOverlay(overlay)
        .build();

Overlays can also be applied per-request:

AgJaxrs.select(Book.class, config).entityOverlay(overlay).get();

This way dynamically-generated overlays can shape the entities based on some request data, such as user permissions. And indeed, per-request Agrest access control methods are internally implemented as entity overlays.

Another thing to notice here, is that overlay doesn’t need to be of the same type as the root entity of the request. In the example above, the request is for Book, while overlay is for Author. So it will be applied in case the request contains include=author control parameter.

5. Jersey Integration

Jersey is often used to run Agrest applications. It supports Java API for RESTful services (JAX-RS). Agrest works with either the legacy Jersey 2 (agrest-jaxrs2 module) or the Jakarta-based Jersey 3 (agrest-jaxrs3 module). Their main difference is JAX-RS package names (javax. vs jakarta.). We will provide Jersey 3 examples below, but they should equally apply to Jersey 2, and for the most part work with other JAX-RS servers.

Jersey doesn’t run standalone until at least version 3.1, and requires some kind of Java "server", such as Jetty, Spring Boot, Bootique, etc. Examples of server integrations are provided in the links below:

For the rest of this document we will focus on Agrest usage in the Jersey environment, ignoring the underlying server.

SpringBoot: SpringBoot 2.x supports Jersey 2, while SpringBoot 3.x will support Jersey 3
Bootique Note: Bootique 2.x supports Jersey 2, while Bootique 3.x supports both Jersey 2 and 3

5.1. Dependencies

You may or may not need to explicitly import Jersey dependencies, depending on how you are planning to run it (see above). But you will need the following Agrest import:

<dependency>
    <groupId>io.agrest</groupId>
    <artifactId>agrest-jaxrs3</artifactId>
</dependency>

5.2. Jersey Bootstrap

Jersey provides a convenience superclass called ResourceConfig that you can subclass and bootstrap your application in the constructor:

public class AgrestJerseyApp extends ResourceConfig { (1)

    public AgrestJerseyApp() {

        (2)
        AgRuntime runtime = AgRuntime
                .builder()
                // add backend configurations and runtime extensions
                // .module(...)
                .build();

        (3)
        AgJaxrsFeature agJaxRs = AgJaxrsFeature.build(runtime);
        super.register(agJaxRs);

        (4)
        super.register(AuthorApi.class);
        super.register(BookApi.class);
    }
}
1 The app class extending Jersey ResourceConfig
2 Create Agrest runtime, as described in the Runtime section
3 Register Agrest runtime with Jersey / JAX-RS environment
4 Register your app API endpoints with Jersey (we will see these endpoint classes shortly)

5.3. Custom Extensions

TODO: JAX-RS extensions via AgFeatureProvider

6. Handling Requests

Agrest runtime provides handlers for the various types of HTTP requests (GET, POST, PUT, DELETE). We will discuss each one individually in the following chapters. Here we’ll look at general principles of request handling.

Below is a typical JAX-RS endpoint implemented with Agrest:

@Path("author")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AuthorApi {

    @Context
    private Configuration config; (1)

    @GET
    public DataResponse<Author> get(@Context UriInfo uri) { (2)
        return AgJaxrs
                .select(Author.class, config) (3)
                .clientParams(uri.getQueryParameters())
                .get();
    }
}
1 JAX-RS Configuration is injected using JAX-RS @Context annotation
2 One way to collect protocol parameters for the request is via JAX-RS UriInfo injection
3 AgJaxrs is a helper that locates the AgRuntime within the Configuration and starts an Agrest processor
Agrest tries to stay maximally non-invasive within the app. You can mix Agrest endpoints with non-Agrest endpoints, and even within the endpoint you can execute any custom code.

6.1. Handler Flavors

Requests are handled by starting a request-appropriate builder via AgJaxrs, which is then configured with client request parameters, client body, custom entity schemas, access rules, etc., and then executed. Below are the main types of handlers:

Select handler. Returns DataResponse<T> object:

@GET
public DataResponse<Author> get(@Context UriInfo uri) {
    return AgJaxrs
            .select(Author.class, config)
            .clientParams(uri.getQueryParameters())
            .get();
}

"Simple" update handler. Creates and/or updates data, returning SimpleResponse. create handler is shown in this example:

@POST
public SimpleResponse create(String entityData) {
    return AgJaxrs
            .create(Author.class, config)
            .sync(entityData);
}
Updating handlers have a variety of flavors for idempotent and non-idempotent requests. We’ll discuss them in the chapters on POST and PUT requests.

"Selecting" update handler. Creates and/or updates data, returning DataResponse<T> with the objects that where modified. It is identical to the "simple" handler, except you’d call syncAndSelect at the end instead of sync:

@POST
public DataResponse<Author> createWithData(@Context UriInfo uri, String entityData) {
    return AgJaxrs
            .create(Author.class, config)
            .clientParams(uri.getQueryParameters())
            .syncAndSelect(entityData);
}

Unrelate handler. Breaks a relationship between entities:

@PUT
@Path("{id}")
public SimpleResponse unrelate(@PathParam("id") long id) {
    return AgJaxrs
            .unrelate(Author.class, config)
            .sourceId(id)
            .allRelated("books")
            .sync();
}

Delete handler:

@DELETE
@Path("{id}")
public SimpleResponse delete(@PathParam("id") long id) {
    return AgJaxrs
            .delete(Author.class, config)
            .byId(id)
            .sync();
}

6.2. Handlers with Custom Stages

TODO…​

7. GET Requests

With just a few lines of code, Agrest can process GET requests conforming to Agrest protocol. Here is how you may program a request handler:

@GET
public DataResponse<Author> get(@Context UriInfo uri) {
    return AgJaxrs.select(Author.class, config) (1)
            .clientParams(uri.getQueryParameters()) (2)
            .get(); (3)
}
1 Start "select" operation builder
2 Pass all request parameters from JAX-RS UriInfo object. The framework will process those that are defined in the Agrest protocol (exp, include, etc.) and ignore the rest
3 Execute the select operation

7.1. GET a Single Object

Often a REST API would only return a single object matching a given id. Here is how this can be implemented in Agrest:

@GET
@Path("{id}")
public DataResponse<Author> getById(
        @PathParam("id") long id,  (1)
        @Context UriInfo uri) {
    return AgJaxrs.select(Author.class, config)
            .byId(id) (2)
            .clientParams(uri.getQueryParameters())
            .getOne(); (3)
}
1 "id" may be modeled as a part of the URL path and injected via JAX-RS @PathParam
2 Here is how the "id" is passed to the Agrest operation builder
3 Instead of get(), call getOne() to execute the operation. It still returns the same DataResponse, but will handle cases when there is no matching object found (404 response status code instead of 200) and also when more than one object is found (this will result in 500 status code).

7.2. GET Object Children

Another common pattern in a REST API design is hierarchical resource URLs used to retrieve "children" of a given "parent" object. E.g. the books of a single known author:

@GET
@Path("{author_id}/books") (1)
public DataResponse<Book> booksForAuthor(
        @PathParam("author_id") long authorId, (2)
        @Context UriInfo uri) {

    return AgJaxrs.select(Book.class, config)
            .parent(Author.class, authorId, "books") (3)
            .clientParams(uri.getQueryParameters())
            .get();
}
1 Define a URL path with author ID and relationship name
2 Capture author ID as a @PathParam
3 Set the "parent" object of the request
"Parent" API is just syntactic sugar to support hierarchical resource URLs like /author/576/books. Similar functionality can also be implemented as /book?exp=author=576 or /author/576?include=books.

7.3. Explicit Protocol Parameters

Not every endpoint has to support every single protocol parameter. Depending on the application needs (and backend capabilities), you may explicitly define which parameters are supported:

@GET
public DataResponse<Author> getIncludeAndSort(
        @QueryParam(ControlParams.INCLUDE) List<String> includes,  (1)
        @QueryParam(ControlParams.SORT) String sort,
        @QueryParam(ControlParams.DIRECTION) String direction) {

    AgRequest request = AgJaxrs.request(config) (2)
            .addIncludes(includes)
            .addSort(sort, direction)
            .build();

    return AgJaxrs.select(Author.class, config)
            .request(request) (3)
            .get();
}
1 Instead of UriInfo, capture individual query parameters using @QueryParam annotation
2 Build a AgRequest object with all the passed parameters
3 Pass the AgRequest to the operation builder

7.4. Server-Side Protocol Parameters

The AgRequest object has another useful property. Unlike UriInfo that captures everything that was passed in the URL by the client, it is manually constructed on the server. So you can amend the request object in your application code to limit what the client can do or to define the default data appearance:

@GET
public DataResponse<Author> getModernAuthors(
        @QueryParam(ControlParams.INCLUDE) List<String> includes,
        @QueryParam(ControlParams.EXP) String urlExp) {

    AgRequest request = AgJaxrs.request(config)
            .addIncludes(includes)
            .andExp(Exp.keyValue("dateOfBirth", ">=", LocalDate.of(1970, 1, 1))) (1)
            .andExp(urlExp) (2)
            .addSort("name", "asc") (3)
            .build();

    return AgJaxrs.select(Author.class, config)
            .request(request)
            .get();
}
1 Pass an expression limiting the dataset. The client will not be able to override this filter, and can only narrow the dataset further by using the URL exp parameter (see the next item)
2 Combine the server-side expression with a client expression that came from the URL
3 Define data ordering (and do not allow the client to change it)

7.5. Customizing Entities

As already mentioned in the chapter on Entity Overlays, you can redefine the shape of entities, their retrieval strategies and access rules via per-request overlays:

@GET
public DataResponse<Author> getWithOverlay(@Context UriInfo uri) {

    SelectBuilder<Author> builder = AgJaxrs
            .select(Author.class, config)
            .clientParams(uri.getQueryParameters());

    if (!isAdminRole()) {  (1)

        AgEntityOverlay<Book> bookModelChanges = AgEntity
                .overlay(Book.class)
                .readablePropFilter(b -> b.property("copiesSold", false));

        builder.entityOverlay(bookModelChanges); (2)
    }

    return builder.get();
}
1 Hide an attribute using an overlay if the request user is not an administrator
2 Apply the overlay to the request
While the request is for the Author entity, overlays can be for any entity at all (including the Author). In our case the overlay is for the Book, meaning that it will be applied if the user runs a request like /author?include=books

7.6. Property Visibility

There’s a more direct API to implement per-request property visibility rules. It can also be applied to any entity in the request graph, not just the root entity:

@GET
public DataResponse<Author> getWithPropFilter(@Context UriInfo uri) {

    SelectBuilder<Author> builder = AgJaxrs
            .select(Author.class, config)
            .clientParams(uri.getQueryParameters());

    if (!isAdminRole()) {
        builder.propFilter(Book.class, pfb -> pfb.property("copiesSold", false)); (1)
    }

    return builder.get();
}
1 Apply "property filter" for non-admin users to the Book entity, hiding "copiesSold" property from the response objects

Internally property filter is implemented as an overlay, similar to the previous example, but from the user perspective the filter API is less verbose.

Don’t forget that you can also define readability rules for individual properties globally using @AgAttribute annotation (see Schema Annotations)

7.7. Object Visibility

The previous examples shows how you can hide certain properties based on per-request custom logic. Similarly, you can exclude certain objects from the response with custom "read filters":

@GET
public DataResponse<Author> getWithReadFilter(@Context UriInfo uri) {

    SelectBuilder<Author> builder = AgJaxrs
            .select(Author.class, config)
            .clientParams(uri.getQueryParameters());

    if (isModernAuthorsOnly()) {
        LocalDate threshold = LocalDate.of(1970, 1, 1);
        builder.filter(Author.class, a -> a.getDateOfBirth().isAfter(threshold) ); (1)
    }

    return builder.get();
}
1 If only modern authors are requested, apply a "read filter" to exclude older authors. The filter lambda must return "true" for the objects that pass the check and should be included in the response.
If custom object filtering rules can be written as an Exp on the root entity, consider doing that instead of using a read filter. It would move the filtering logic to the data store and out of your code, potentially optimizing the processing. You can refer to an example above from the Server-Side Protocol Parameters chapter.

8. POST Requests

Agrest works with REST resources that represent data entities. For such resources, POST requests are used to create new entities in a non-idempotent way. In practice, it means that created entity id is not known to the client upfront, and will be generated on the server. The simplest POST handler may look like this:

@POST
public SimpleResponse createAndForget(String data) { (1)
    return AgJaxrs.create(Author.class, config) (2)
            .sync(data); (3)
}
1 Author entity data is accepted as a String. The String must contain JSON and must conform to the Agrest Protocol Update Document format, i.e. be either a single object or a collection of objects. Object properties should match the properties of the created entity. Here is an example of a single object payload: {"name":"Ernest Hemingway","dateOfBirth":"1899-07-21"}
2 Start "create" operation builder
3 Call "sync" that will execute the "create" operation and return a "simple" response

Such "create-and-forget" style of POST is rarely used, as the client usually needs to know the server-generated id of the created entity or entities. Instead, you could use a "selecting" flavor that returns back the created object(s), combining create and select in one call:

@POST
public DataResponse<Author> createAndSelect(String data) {
    return AgJaxrs.create(Author.class, config)
            .syncAndSelect(data); (1)
}
1 Instead of "sync", call "syncAndSelect"

Just like GET, such requests can take Agrest protocol parameters to shape, filter and order the response collection. "Read" object and property filters can also be applied to them, just like with GET.

8.1. Representation of Create Data

We’ve already seen how to pass object data to Agrest as a JSON String representing either a single object, or a collection of objects per Agrest Protocol Update Document format. Alternatively, each JSON object can be represented as an EntityUpdate<T>. E.g.:

@POST
public DataResponse<Author> createWithEntityUpdate(
        EntityUpdate<Author> authorUpdate) { (1)

    return AgJaxrs.create(Author.class, config)
            .syncAndSelect(authorUpdate);
}
1 Instead of a String, pass EntityUpdate<Author>

Similarly, a collection of updates can be passed:

@POST
public DataResponse<Author> createWithEntityUpdates(
        List<EntityUpdate<Author>> authorUpdates) { (1)

    return AgJaxrs.create(Author.class, config)
            .syncAndSelect(authorUpdates);
}
1 Instead of a String, pass List<EntityUpdate<Author>>

There’s no functional difference between using Strings or EntityUpdates. In both cases Agrest performs the same operation. But EntityUpdate has other advantages:

  • It makes the expected payload of your REST method more explicit (e.g. for the purpose of generating OpenAPI documentation)

  • It allows to distinguish between endpoints that can process just a single object vs. those that process collections

  • It allows to tweak the client request in your server-side code

String and EntityUpdate data formats are used in the same way across all handlers that create or modify entities, and both POST and PUT requests.

8.2. Customizing Entities for POST

8.3. Property Set Authorization

8.4. Object Create Authorization

9. PUT Requests

10. DELETE Requests

11. Cayenne Integration

12. Swagger Integration