See: The 20-Minute Server Implementation Overview (pdf format)

Server Implementation Guide

The Abdera Server module provides a framework for constructing Atom Publishing Protocol server implementations.

The framework consists of several key components that must be implemented by developers and wired together. These components include:

How an Abdera server works

The AbderaServlet is the entry point to the Abdera server framework. When a request is dispatched to the servlet, it creates a RequestContext object and forwards it on to an array of Filter objects and a Provider. The Filter's operate in a manner identical to that of Servlet Filters but use the Abdera API's. Once the request has been passed through the filters, the Provider analyzes the request, determines what action is being requested, and forwards the request on to the appropriate method on the appropriate CollectionAdapter object. The Provider uses a Workspace Manager object to select a CollectionAdapter to process the request. The Provider also uses the Workspace Manager to collect metadata about the available Atompub Workspaces and Collections and to construct an Atompub Service Document. The Target Resolver object is used by the RequestContext, Provider, WorkspaceManager and CollectionAdapter to determine the identity of the resource being operated on (think of it as a special purpose URI parser). The Target Builder object is used to construct URIs used by the server for a variety of purposes. Once the Collection Adapter processes the request, it creates a Response Context object that is handed back to the Provider, passed back up through the array of Filters and back out to the AbderaServlet, which writes the ResponseContext out to the Servlet's HttpServletResponse object, completing the request.

How to implement an Abdera server (the short version)

The process of implementing an Abdera server is fairly straightforward. First, the developer needs to implement one or more Collection Adapters. These objects provide the business logic of the server, mapping the Atom Publishing Protocol methods into code that manipulates some backend persistence layer. The Collection Adapter is where the overwhelming majority of development will occur.

Once the Collection Adapter is implemented, the developer needs to either select, customize or implement a Provider. Several Provider implementations ship with Abdera by default (the DefaultProvider, the BasicProvider, etc), each with their own distinct capabilities. These can either be used directly or customized by subclassing the Provider. Alternatively, the developer can choose to implement their own Provider entirely by subclassing the AbstractProvider class.

Within the Provider, several components are wired together.

The following example Provider illustrates each of these steps.

1  import org.apache.abdera.protocol.server.CollectionAdapter;
2  import org.apache.abdera.protocol.server.Filter;
3  import org.apache.abdera.protocol.server.FilterChain;
4  import org.apache.abdera.protocol.server.RequestContext;
5  import org.apache.abdera.protocol.server.ResponseContext;
6  import org.apache.abdera.protocol.server.TargetType;
7  import org.apache.abdera.protocol.server.context.RequestContextWrapper;
8  import org.apache.abdera.protocol.server.impl.AbstractWorkspaceProvider;
9  import org.apache.abdera.protocol.server.impl.RegexTargetResolver;
10 import org.apache.abdera.protocol.server.impl.SimpleWorkspaceInfo;
11 import org.apache.abdera.protocol.server.impl.TemplateTargetBuilder;
12 
13 public class CustomProvider 
14  extends AbstractWorkspaceProvider {
15 
16   private final SimpleAdapter adapter;
17  
18   public CustomProvider() {
19    
20     this.adapter = new SimpleAdapter();
21     
22     setTargetResolver(      
23       new RegexTargetResolver()
24         .setPattern("/atom(\\?[^#]*)?", TargetType.TYPE_SERVICE)
25         .setPattern("/atom/([^/#?]+);categories", TargetType.TYPE_CATEGORIES, "collection")
26         .setPattern("/atom/([^/#?;]+)(\\?[^#]*)?", TargetType.TYPE_COLLECTION, "collection")
27         .setPattern("/atom/([^/#?]+)/([^/#?]+)(\\?[^#]*)?", TargetType.TYPE_ENTRY, "collection","entry")
28         .setPattern("/search", OpenSearchFilter.TYPE_OPENSEARCH_DESCRIPTION)
29     );
30     
31     setTargetBuilder(
32       new TemplateTargetBuilder()
33         .setTemplate(TargetType.TYPE_SERVICE, "{target_base}/atom")
34         .setTemplate(TargetType.TYPE_COLLECTION, "{target_base}/atom/{collection}{-opt|?|q,c,s,p,l,i,o}{-join|&|q,c,s,p,l,i,o}")
35         .setTemplate(TargetType.TYPE_CATEGORIES, "{target_base}/atom/{collection};categories")
36         .setTemplate(TargetType.TYPE_ENTRY, "{target_base}/atom/{collection}/{entry}")
37         .setTemplate(OpenSearchFilter.TYPE_OPENSEARCH_DESCRIPTION, "{target_base}/search")
38     );
39     
40     SimpleWorkspaceInfo workspace = new SimpleWorkspaceInfo();
41     workspace.setTitle("A Simple Workspace");
42     workspace.addCollection(adapter);
43     addWorkspace(workspace);
44     
45     addFilter(new SimpleFilter());
46     addFilter(
47       new OpenSearchFilter()
48         .setShortName("My OpenSearch")
49         .setDescription("My Description")
50         .setTags("test","example","opensearch")
51         .setContact("john.doe@example.org")
52         .setTemplate("http://localhost:8080/atom/feed?q={searchTerms}&c={count?}&s={startIndex?}&p={startPage?}&l={language?}&i={indexEncoding?}&o={outputEncoding?}")
53         .mapTargetParameter("q", "searchTerms")
54         .mapTargetParameter("c", "count")
55         .mapTargetParameter("s", "startIndex")
56         .mapTargetParameter("p", "startPage")
57         .mapTargetParameter("l", "language")
58         .mapTargetParameter("i", "inputEncoding")
59         .mapTargetParameter("o", "outputEncoding")
60       );
61   }
62 
63   public CollectionAdapter getCollectionAdapter(
64     RequestContext request) {
65       return adapter;
66   }
67 
68   public class SimpleFilter 
69     implements Filter {
70       public ResponseContext filter(
71         RequestContext request, 
72         FilterChain chain) {
73           RequestContextWrapper rcw = new RequestContextWrapper(request);
74           rcw.setAttribute("offset", 10);
75           rcw.setAttribute("count", 10);
76           return chain.next(rcw);
77       }    
78   }
79 
80 }

In this example, the CustomProvider implements AbstractWorkspaceProvider, an abstract class that extends AbstractProvider and implements the Workspace Manager interface. This means that the Provider is acting as it's own Workspace Manager.

At Line 16, we create an instance of a Collection Adapter. In this example, only one Collection Adapter instance is used. An implementation can use any number of Collection Adapters. It is possible to use a single Collection Adapter per Atompub Collection or to have multiple Collection Adapters for a single Atompub Collection.

Lines 22-29 configure the Target Resolver. This is the component that is used to analyze the request URI to determine which resource is being targeted by the request. In this example, regular expressions are used to not only identify the resource, but to parse the request uri and extract key pieces of information from the request URI that will be used later by the Provider and the Collection Adapter.

Lines 31-38 configure the Target Builder. This is the component that is used to construct URIs. Developers familiar with Ruby on Rails and similar frameworks have similar mechanisms that allow developers to define URI patterns that can be expanded into complete URIs by passing in a few simple variable values. This example uses URI Templates to define the URI Patterns.

In this example, the Target Resolver and Target Builder are two separate components. However, it is possible for one object to be both the Target Resolver and the Target Builder. Two examples of a combined implementation ship as part of Abdera: the StructuredTargetResolver and the RouteManager. StructuredTargetResolver is a simple implementation used by the DefaultProvider implementation that assumes a very simple hierarchical collection/entry URI structure. The RouteManager uses "routes" similar to those used by Ruby on Rails, e.g. ":collection/:feed".

Lines 40-43 establish the metadata used by the Provider to create the Atompub Service Document. There are four distinct metadata interfaces: WorkspaceInfo, CollectionInfo, CategoriesInfo and CategoryInfo. In this example, the SimpleAdapter Collection Adapter implementation also implements the CollectionInfo interface. The CategoriesInfo and CategoryInfo objects are not used. The SimpleAdapter is added as a CollectionInfo to a single SimpleWorkspaceInfo object which is added to the Provider. The resulting Service Document will contain exactly one atom:workspace element with exactly one atom:collection element. The atom:title elements from each will be pulled from their respective *Info classes, as will the other necessary data such as the collection href, the list of acceptable media types, and so on.

Lines 45-60 add and configure two Filters to the provider. These are invoked by the AbderaServlet, in the order they are added, prior to invoking the Provider. The invocation model is identical to that used by Servlet filters, which each filter forwarding the request on to the next filter in the chain. The design ensures that both the request and the response are passed through the chain of filters. By default, only a single array of Filters is configured on the provider, however, it is possible for a custom provider implementation to customize the chain of filters per request.

The rest of the example should be fairly self-explanatory.

Once the Collection Adapter and Provider are implemented, the only remaining step is to deploy the servlet and to configure it to use the selected Provider. There are several ways this can be done. One, you could subclass the AbderaServlet and hard-code it to use your Provider. Two, you could use the Abdera Spring module and use the Spring framework to wire the Provider and servlet together. Or three, you could simply deploy the servlet and specify an initialization parameter.

  server = new Server(port);
  Context context = new Context(server, "/", Context.SESSIONS);
  ServletHolder servletHolder = new ServletHolder(new AbderaServlet());
  servletHolder.setInitParameter(
    "org.apache.abdera.protocol.server.Provider", 
    "org.apache.abdera.examples.appserver.CustomProvider");
  context.addServlet(servletHolder, "/*");
    server.start();

Assuming you have configured the Target Resolver, Target Builder, and Collection Adapter properly, the Atompub server should now be up and running.

Target Resolvers and Target Builders

Target Resolvers

The Target Resolver takes information from the request context and uses it to select a Target. The Target object provides an abstraction API to the request URI and other request parameters so that an Abdera server implementation can be decoupled from the specific URI's used.

The Target interface is simple:

public interface Target 
  extends Iterable<String> {

  TargetType getType();
  
  String getIdentity();
  
  String getParameter(String name);
  
  String[] getParameterNames();
  
}

The getType() method returns the TargetType, a special identifier that classifies the target into one of several distinct kinds of objects: e.g. Atompub Collections, Service Documents, Categories Documents, Media resources, etc.

The getIdentity() method returns a single string value that uniquely identifies the target resource. Typically, this is going to be the request URI, but specific Target implementations can return any value.

The getParameter and getParameterNames methods return information about specific Target parameters. These values may come from the query string arguments, from segments of the request URI path, from the HTTP request headers, etc.

A Target Resolver is an implementation of the org.apache.abdera.protocol.Resolver class that creates Target objects for the request

public class RegexTargetResolver 
  implements Resolver<Target> {
    ...
    public Target resolve(Request request) {
      ...
    }
    ...
}

Target Types

The TargetType class is a special class that closely resembles a type safe enum. The key difference between TargetType and a traditional enum, however, is that TargetTypes are extensible. By default, there are five TargetTypes defined: TargetType.TYPE_SERVICE, TargetType.TYPE_CATEGORIES, TargetType.TYPE_COLLECTION, TargetType.TYPE_ENTRY and TargetType.TYPE_MEDIA. Each represent a distinct kinds of Atom Publishing Protocol artifacts.

To create a new TargetType, developers simply call

TargetType.get("Foo",true)

. The second parameter tells the method to create the target type if it does not already exist. The new type is added to the collection of types.

Target Builders

A Target Builder object implements the TargetBuilder interface.

public interface TargetBuilder {

  String urlFor(RequestContext context, Object key, Object param);
  
}

The urlFor method uses information from the request context and the param to expand the URI pattern identified by the key into a URI suitable for use by the server application. The TargetBuilder is configured by the Provider and is accessible to the CollectionAdapter and Filters via the RequestContext interface.

The Regex Target Resolver

The RegexTargetResolver is a Resolver<Target> implementation that uses regular expressions to analyze and parse the Request URI and to select the Target for a given request.

  new RegexTargetResolver()
    .setPattern("/atom(\\?[^#]*)?", TargetType.TYPE_SERVICE)
    .setPattern("/atom/([^/#?]+);categories", TargetType.TYPE_CATEGORIES, "collection")
    .setPattern("/atom/([^/#?;]+)(\\?[^#]*)?", TargetType.TYPE_COLLECTION, "collection")
    .setPattern("/atom/([^/#?]+)/([^/#?]+)(\\?[^#]*)?", TargetType.TYPE_ENTRY, "collection","entry")
    .setPattern("/search", OpenSearchFilter.TYPE_OPENSEARCH_DESCRIPTION)

Each regex is associated with exactly one TargetType and zero or more group labels (e.g. "collection" and "entry" in the example). Group labels are applied to each of the capturing groups defined in the regex. For instance, in the TargetType.TYPE_ENTRY regex above, the label "collection" is mapped to group 1; the label "entry" is mapped to group 2.

When the RequestContext object is created, a Target object will be resolved. Assuming a match is found, the RegexTargetResolver will return a RegexTarget object. The parameters of the RegexTarget will include both the capturing groups and the querystring parameters from the request.

  Target target = requestContext.getTarget();
  System.out.println( target.getParameter("collection") );
  System.out.println( target.getParameter("entry") );
  System.out.println( target.getParameter("q") );

The Structured Target Resolver

The StructuredTargetResolver is a non-configurable TargetResolver and TargetBuilder implementation. It assumes a static and hierarchical feed/entry URL structure that cannot be modified. When used, the Atompub service document is always located at /(context_path)/(servlet_path)/, where context_path is the path of the context path of the web application and servlet_path is the URL mapping for the AbderaServlet. Collections are always located at /(context_path)/(servlet_path)/(collection), where collection is the name of the collection. Categories documents are always located at /(context_path)/(servlet_path)/(collection);categories. Entry documents are always located at /(context_path)/(servlet_path)/(collection)/(entry) where entry is the name of the entry.

A subclass of StructuredTargetResolver could modify these mappings but there would likely be very little reason to do so.

Routes and the RouteManager

Routes are a highly-experimental simple analog to the Ruby on Rails concept of a route (a URI pattern used for request dispatching and URL creation). The RouteManager is a Target Resolver and Target Builder implementation that uses Routes.

  RouteManager rm = new RouteManager()
    .addRoute("feed", ":collection", TargetType.TYPE_COLLECTION)
    .addRoute("entry", ":collection/:entry:", TargetType.TYPE_SERVICE); 

Currently, Routes are experimental and the RouteManager implementation is still being evolved.

Workspace Managers

Providers use one or more Workspace Manager objects to collect information about the Atompub workspaces and collections available to the provider. The Workspace Manager serves a dual purpose: 1. it acts as a repository for metadata about the collections and 2. it provides Collection Adapter instances to the provider for dispatching and handling requests.

public interface WorkspaceManager {
    
  CollectionAdapter getCollectionAdapter(RequestContext request);
  
  Collection<WorkspaceInfo> getWorkspaces(RequestContext request);
  
}

As is the case with the BasicProvider, a Provider can choose to implement the WorkspaceManager interface itself, thus acting as it's own Workspace Manager. Providers that wish to emulate this approach can extend the AbstractWorkspaceProvider class.

It is possible for a Provider to be implemented such that a different Workspace Manager is used depending on the request. However, typically, Providers will use only a single Workspace Manager.

Workspace Metadata

The WorkspaceInfo, CollectionInfo, CategoriesInfo and CategoryInfo interfaces provide access to metadata that is used by the Provider to construct the Atompub Service Document.

  SimpleWorkspaceInfo workspace = 
    new SimpleWorkspaceInfo(
      "A Simple Workspace");
  CollectionInfo collection = 
    new SimpleCollectionInfo(
      "A Simple Collection",
      "/atom/feed",
      "application/atom+xml;type=entry"
    );
  CategoriesInfo categories =
    new SimpleCategoriesInfo();
  CategoryInfo category = 
    new SimpleCategoryInfo("foo");
  categories.addCategoryInfo(category);
  collection.addCategoriesInfo(categories);
  workspace.addCollection(collection;
  provider.addWorkspace(workspace);

This will produce the follow Service document,

<service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
  <workspace>
    <atom:title>A Simple Workspace</atom:title>
    <collection href="/atom/feed">
      <atom:title>A Simple Collection</atom:title>
      <accept>application/atom+xml;type=entry</accept>
      <categories fixed="false">
        <atom:category term="foo" />
      </categories>
    </collection>
  </workspace>
</service>

Filters

Filters are special objects through which requests and responses are passed before and after the AbderaServlet invokes the Provider.

public interface Filter { 
  ResponseContext filter(RequestContext request, FilterChain chain);  
}

The invocation model for Filters is identical to that used by Servlet Filters, with each filter instance being given a reference to the filter chain and responsible for calling the next filter in the chain. This model allows filters to augment or intercept both the request and the response.

  public class SimpleFilter 
    implements Filter {
      public ResponseContext filter(
        RequestContext request, 
        FilterChain chain) {
          RequestContextWrapper rcw = new RequestContextWrapper(request);
          rcw.setAttribute("offset", 10);
          rcw.setAttribute("count", 10);
          return chain.next(rcw);
      }    
  }

Collection Adapters

Collection Adapters are the components that implement the business logic of an Atompub server.

public interface CollectionAdapter {

  ResponseContext postEntry(RequestContext request);
  
  ResponseContext deleteEntry(RequestContext request);
  
  ResponseContext getEntry(RequestContext request);
  
  ResponseContext putEntry(RequestContext request);

  ResponseContext getFeed(RequestContext request);

  ResponseContext getCategories(RequestContext request);

  ResponseContext extensionRequest(RequestContext request);
  
}

Each of the methods of the CollectionAdapter interface, with the exception of extensionRequest, correspond to specific Atom Publishing Protocol actions.

Note that there is no method for retrieving an Atom Service Document. It is the Providers responsibility to serve the Service Document.

The extensionRequest method is called when the client sends a request that does not correspond to any of the other methods.

The implementation of most Collection Adapters will vary broadly across implementations, however, a number of abstract helper classes have been developed to make it easier.

Please see the Collection Adapter Implementation Guide for more details on implementing a Collection Adapter.

Entity Tags

Entity Tags are a request and response headers that can be used in order to implement a cache system. We are following the same example than rfc5023 in order to see how to use them.

When the client creates a new entry the server returns an ETag header in the response:

Document<Entry> entryDoc = (Document<Entry>) request.getDocument(parser).clone();
Entry entry = entryDoc.getRoot();
...
BaseResponseContext response = new BaseResponseContext(201);
response.setEntityTag(EntityTag.generate(entry.getId().toString(), entry.getEdited().toString()));

The client can use the returned Entity Tag to construct a conditional GET using the If-None-Match header. In this case the RequestContext object already contains a method that converts this header to an Entity Tag object.

EntityTag eTag = EntityTag.generate(entry.getId().toString(), entry.getEdited().toString());
if (EntityTag.matchesAny(eTag, request.getIfNoneMatch()) {
  return new EmptyResponseContext(304);
}

If the Entry has not been modified, the response will be a status code of 304 ("Not Modified").

The client can also use the returned Entity Tag to construct a conditional PUT using the If-Match header, so the server can check if it has received a more recent copy than the client's.

EntityTag eTag = EntityTag.generate(entry.getId().toString(), entry.getEdited().toString());
if (!EntityTag.matchesAny(eTag, request.getIfMatch()) {
  return new EmptyResponseContext(412);
}

If the entry has been modified by other client, the response will be a status code of 412 ("Precondition Failed").

Request Processors

Request processors are the classes in charge of call the specific method into an adapter. The provider selects the right request processor depending on the TargetType that it's trying to resolve.

By default, Abdera includes request processors in order to resolve every atomPub stuff but they can be extended with more processors or overrided by other ones:

Provider provider = new DefaultProvider();

Map<TargetType, RequestProcessor> custom = new HashMap<TargetType, RequestProcessor>() {{
  put(TargetType.TYPE_ENTRY, new MyCustomRequestProcessor());
}};

provider.addRequestProcessors(custom);