Agrest Server Framework


1. Overview

1.1. What is Agrest Server Framework

Agrest provides a server framework to quickly build Java RESTful applications that serve and consume data graphs in a way compatible with the Agrest Protocol.

The core API of the framework is not dependent on a specific REST engine (such as Jersey or Spring Web), and can be integrated with most of them. The same is true for data backends - a number of them can be plugged in, either provided by Agrest (Apache Cayenne ORM) or implemented by the user. The simplest form of a custom backend is a Java function performing some calculation to produce a value for a single property.

Agrest server is model-driven with its own metadata layer. It allows to take some existing data model (e.g. POJOs, ORM entities, etc.), and shape it in the form appropriate for the REST API you intend to build. Agrest entity model also allows defining per-entity, and per-request security policies at the object and attribute level.

Agrest integrates with Swagger to API serve documentation in the widely-adopted OpenAPI 3 format.

1.1.1. REST Engines

Out of the box Agrest supports the following Java REST engines:

It should be fairly straightforward to port Agrest to other engines like SpringWeb.

1.1.2. Data Sources

Out of the box Agrest supports the following data sources:

  • Apache Cayenne, which is a well-optimized ORM backend with support for all Agrest Protocol features. Currently, supported version of Cayenne is 4.2.

  • Custom functions (derived attribute calculation, REST services mapping, etc.)

We are working on alternative advanced query-based data backends, specifically JPA. As of this writing it is experimental, and we’d recommend Cayenne as the most tried and reliable.

1.2. Java Version

Java 11 or newer is required.

2. Getting Started

To load Agrest in your project, follow these simple steps:

  1. Declare Agrest dependency. Here is a Maven example. If you are using Gradle or Ant, you do what needs to be done there to include Agrest dependency.

    <!-- The main Agrest engine -->
    <dependency>
       <groupId>io.agrest</groupId>
       <artifactId>agrest-engine</artifactId>
       <version>{agrest.version}</version>
    </dependency>
    
    <!--
    You will likely also need "agrest-cayenne",
    as this is the most capable Agrest backend as of now
    -->
    <dependency>
       <groupId>io.agrest</groupId>
       <artifactId>agrest-cayenne</artifactId>
       <version>{agrest.version}</version>
    </dependency>
  2. Create AgRuntime, and load it in JAX-RS container. Assuming the container is Jersey, this may look like this:

    import javax.ws.rs.ApplicationPath;
    import org.glassfish.jersey.server.ResourceConfig;
    import io.agrest.runtime.AgBuilder;
    
    /**
     * A Jersey-specific JAX-RS Application class that loads Agrest.
     */
    @ApplicationPath("/")
    public class JaxRsApplication extends ResourceConfig {
    
    	public JaxRsApplication() {
    
                ServerRuntime cayenneRuntime = ...
                AgRuntime agRuntime = AgBuilder.build(cayenneRuntime);
                super.register(agRuntime);
    
                // continue with Application setup...
                ...
    	}
    }

Now you are ready to write Agrest endpoints.

3. Writing Resource Endpoints

Let’s create a resource class called DomainResource, annotated with JAX-RS @Path and @Produces annotations. One extra thing we need for Agrest to work is an instance of javax.ws.rs.core.Configuration, that can be injected with @Context annotation:

import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Context;

@Path("domain")
@Produces(MediaType.APPLICATION_JSON)
public class DomainResource {

    @Context
    private Configuration config;
}

3.1. Create Entity with "POST"

Now let’s implement a POST method in DomainResource class:

import io.agrest.Ag;
import io.agrest.SimpleResponse;

...

@POST
public SimpleResponse create(String data) {
    return Ag.create(Domain.class, config).sync(data);
}

Here we’ve built a very simple "create chain" using Agrest fluent API. It starts with a static "create" method on Agrest class, taking a type of entity to create (Domain) and previously injected Configuration. Finally it calls "sync" method to execute the request. "data" String is expected to be an "Update Document" (see [Update Document]), i.e. a single object or an array of objects. Now if you compile your app and deploy it in a web container (e.g. Tomcat), you may call this endpoint to create new Domain objects:

curl -i -X POST 'http://example.org/myapp/domain' \
>          -d '{"vhost":"mysite1.example.org","name":"My Site #1"}'
HTTP/1.1 201 Created
Content-Type: application/json
{"success":true}

In your container log you might see output from Cayenne, actually inserting a newly created object:

INFO CommonsJdbcEventLogger - INSERT INTO "domain" ("name", "vhost") VALUES (?, ?)
INFO CommonsJdbcEventLogger - [bind: 1->name:'My Site #1', 2->vhost:'mysite1.example.org']
INFO CommonsJdbcEventLogger - Generated PK: domain.id = 1
INFO CommonsJdbcEventLogger - === updated 1 row.

3.2. Read Collection of Entities with "GET"

You may create more Domain objects, executing POST request above. Now let’s write a GET method to search for domains:

import io.agrest.DataResponse;
import io.agrest.Ag;

...

@GET
public DataResponse<Domain> getAll(@Context UriInfo uriInfo) {
    return Ag.select(Domain.class, config).uri(uriInfo).get();
}

The above is a typical "select chain". Now GET can be invoked from the client like this:

curl -i -X GET 'http://example.org/myapp/domain'

HTTP/1.1 200 OK
Content-Type: application/json
{
    "data" : [
        { "id" : 1, "name" : "My Site #1", "vhost" : "mysite1.example.org" },
        { "id" : 2, "name" : "My Site #2", "vhost" : "mysite2.example.org" }
    ],
    "total" : 2
}

Since select chain above incorporates UriInfo, it will recognize Agrest control parameters passed from the client (see [Control Parameters]). Let’s try using "exp" filter and "include":

curl -i -X GET 'http://example.org/myapp/domain?exp=vhost="mysite1.example.org"&include=id'

HTTP/1.1 200 OK
Content-Type: application/json
{
    "data" : [
        { "id" : 1 }
    ],
    "total" : 1
}

3.3. Read a Single Entity with "GET"

A common request is to locate a single instance of an entity by ID. Here is how this can be done with Agrest:

import io.agrest.DataResponse;
import io.agrest.Ag;

...

@GET
@Path("{id}")
public DataResponse<Domain> getOne(@PathParam("id") int id, @Context UriInfo uriInfo) {
    return Ag.select(Domain.class, config).byId(id).uri(uriInfo).getOne();
}

Here we are binding "id" as a URL path parameter, but also notice that Agrest doesn’t mandate any specific place in the URL for ID. This is a decision made by the developer. Calling this endpoint, we’ll get an expected result:

curl -i -X GET 'http://example.org/myapp/domain/1'

HTTP/1.1 200 OK
Content-Type: application/json
{
    "data" : [
        { "id" : 1, "name" : "My Site #1", "vhost" : "mysite1.example.org" }
    ],
    "total" : 1
}

Even though we expect at most one object to be returned, the response is the same Collection Document as we’ve seen before.

3.4. Update Entity with "PUT"

To update the entity we created before Create Entity with "POST" we have to implement a PUT method in DomainResource class:

import io.agrest.Ag;
import io.agrest.SimpleResponse;

...

@PUT
@Path("{id}")
public SimpleResponse update(@PathParam("id") int id, String data) {
    return Ag.update(Domain.class, config).id(id).sync(data);
}

This simple "update chain" is very similar to the Create Entity with "POST" and the Read a Single Entity with "GET" chains. Try to send a request to this endpoint and get a result as expected

curl -i -X PUT 'http://example.org/myapp/domain/1' \
>          -d '{"vhost":"mysite2.example.org","name":"My Site #2"}'
HTTP/1.1 200 OK
Content-Type: application/json
{"success":true}

Apart of that, Agrest provides other ways to update entity with PUT. Please, refer to Idempotency of Updating Chains for more information.

4. Request Chains

4.1. Available Chains

As demonstrated by earlier examples, to process a given request you need to build an appropriate Agrest "chain". Each chain starts with a call to a static method of Agrest class, that determines chain type, parameters it can take, and the type of response it generates. Each chain type naturally maps to a single HTTP method. Although ultimately the mapping of chains to methods is not enforced by Agrest and is left to the application developer. The following chains are available:

// use with @GET
Ag.select(SomeEntity.class, config)...

// use with @DELETE
Ag.delete(SomeEntity.class, config)...

// use with @POST
Ag.create(SomeEntity.class, config)...

// use with @POST
Ag.createOrUpdate(SomeEntity.class, config)...

// use with @PUT
Ag.idempotentCreateOrUpdate(SomeEntity.class, config)...

// use with @PUT
Ag.idempotentFullSync(SomeEntity.class, config)...

// use with @GET for metadata endpoints
Ag.metadata(SomeEntity.class, config)...

4.2. Strategies for Object Matching

Many of the updating chains need to match objects coming as Update Documents (see Request: Update Document) against objects in the database. E.g. "createOrUpdate" needs to know whether a JSON object is new (and needs to be created) or it already exists (and needs to be updated). By default Agrest would attempt to match each JSON "id" attribute with a DB record primary key. This is a reasonable and useful strategy. Its main limitation though - it can’t be used for entities with ids generated on the server when combined with idempotent requests (see the next section on idempotency). To work around this limitation one may use a meaningful unique property that is known to the client at the object creation time. E.g. our Domain entity has a unique property "vhost".

To ensure the chain uses a property other than "id" for matching, a user may should set an explicit mapper on the chain:

Ag.idempotentCreateOrUpdate(Domain.class, config)
  .mapper(ByKeyObjectMapperFactory.byKey(Domain.VHOST))
  .sync(entityData);

ByKeyObjectMapperFactory mapper is provided by Agrest. If something other than mapping by property is needed, a custom ObjectMapperFactory can be coded by the user.

4.3. Idempotency of Updating Chains

It is easy to distinguish updating chains that are idempotent from those that are not (chain factory method starts with "idempotent" for the former). Both work the same way, except that "idempotent" ones perform an extra check on the input to ensure that it is repeatable, i.e. it will be safe to run it multiple times with the same effect as running it once. At the minimum this means that all the "new" objects have their ids set in the request. This is where ByKeyObjectMapperFactory discussed above comes in handy. Pretty much all idempotent chains need to use ByKeyObjectMapperFactory or an equivalent mapper to match by some unique property of the entity, that is known to the client at the object creation time.

5. Non-Persistent Properties and POJOs

Agrest maintains a model of all entities that can be exposed via REST. All persistent entities present in the underlying ORM (usually Cayenne) are automatically added to Agrest model. What if you want to expose additional non-persistent properties of peristent objects or build entire request chains that are not based on persistent entities? There are three annotations to help with it: @AgAttribute or @AgRelationship and @AgId.

The first example is a typical Cayenne persistent class that has some transient properties:

import io.agrest.annotation.AgAttribute;

// a typical Cayenne persistent class
public class Domain extends _Domain {

  @AgAttribute
  public String getLowercaseName() {
    return getName().toLowerCase();
  }
}

This one was simple. The second example is an entire POJO not known to Cayenne:

import io.agrest.annotation.AgAttribute;
import io.agrest.annotation.AgRelationship;

// a POJO not mapped in Cayenne
public class SomeClass {

  private int id;
  private String string;
  private SomeOtherClass related;

  @AgId
  public int getId() {
    return id;
  }

  @AgAttribute
  public String getString() {
    return string;
  }

  @AgRelationship
  public SomeOtherClass getRelated() {
    return related;
  }
}

Creating and annotating a POJO was easy. But Agrest still requires a backend that knows how to select and/or update those POJOs. Such custom "backends" can be configured per request chain using chain listener API. It is up to the caller what strategy the backend would utilize (maybe a REST call, or reading/writing from a NoSQL DB, etc.) :

// an object with methods annotated with one of the
// 'io.agrest.annotation.listener' annotations
SomeCustomBackend altBackend = new SomeCustomBackend();

Ag.select(SomeClass.class, config).listener(altBackend).uri(urlInfo).get();

6. Relationship Resources

7. Securing Endpoints

8. Customizing Request Processing

To customize request processing chain Agrest provides the stage mechanism. E.g. we have usual get-by-id request chain:

import io.agrest.DataResponse;
import io.agrest.Ag;

...

@Context
private Configuration config;

@GET
@Path("{id}")
public DataResponse<Domain> getOne(@PathParam("id") int id, @Context UriInfo uriInfo) {
    return Ag.select(Domain.class, config)
             .byId(id)
             .uri(uriInfo)
             .getOne();
}

8.1. stage

Just add stage method to the chain and put two parameters:

  • Name of stage after which your custom processing apply.

  • Lambda expression that implements the processing.

The implementation can use provided SelectContext for inspecting and modifying. Please, pay attention that the context may have different state for different stages.

...

@Context
private Configuration config;

@GET
@Path("{id}")
public DataResponse<Domain> getOne(@PathParam("id") int id, @Context UriInfo uriInfo) {
    return Ag.select(Domain.class, config)
             .byId(id)
             .uri(uriInfo)
             .stage(SelectStage.PARSE_REQUEST, (SelectContext<Domain> c) -> {
                // TODO: Add a customization with regards of the parse request stage
             })
             .getOne();
}

Agrest supports the following stage types:

SelectStage

START

PARSE_REQUEST

CREATE_ENTITY

APPLY_SERVER_PARAMS

ASSEMBLE_QUERY

FETCH_DATA

UpdateStage

START

PARSE_REQUEST

CREATE_ENTITY

APPLY_SERVER_PARAMS

UPDATE_DATA_STORE

FILL_RESPONSE

DeleteStage

START

DELETE_IN_DATA_STORE

Apart of the stage method Agrest provides additional two terminalStage and routingStage methods. These two could be customised in the same way.

Please, pay attention that these stage operations are composable. For each stage all custom processors will be invoked in the order they were registered.

8.2. terminalStage

Registers a consumer to be executed after the specified standard execution stage. The rest of the standard pipeline following the named stage will be skipped. This is useful for quick assembly of custom back-ends that reuse the initial stages of Agrest processing, but query the data store on their own. The consumer can inspect and modify provided context SelectContext or UpdateContext.

8.3. routingStage

Registers a processor to be executed after the specified standard execution stage. The processor can inspect and modify provided context SelectContext or UpdateContext. When finished, processor can either pass control to the next stage by returning ProcessorOutcome.CONTINUE, or terminate the pipeline by returning ProcessorOutcome.STOP.