1. Prerequisites
-
Java 1.8 or newer
-
A Java web app project that will serve your REST requests.
-
A JAX-RS 2.0 container, such as Jersey 2.x.
-
Cayenne 4.2 or newer. Mapping your database and starting Cayenne ServerRuntime is outside the scope of this document. Please refer to the corresponding Cayenne docs
2. Getting Started
To load Agrest in your project, follow these simple steps:
-
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>
-
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 a 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 sent 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 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();
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
.