Saturday, April 12, 2008

JavaRESTAdaptor

I know ... There was no "This Week in Wonder" this week. Mostly because I had "This Week in Real Job". But here's a new framework in the Wonder family to tide you over till the Next Week in Wonder:

JavaRESTAdaptor
Note: My first post called this ERRESTAdaptor, but it's actually JavaRESTAdaptor, because all adaptors have to be named JavaXxxAdaptor. Whoops.

Overview
JavaRESTAdaptor is an EOF adaptor implementation on top of a RESTful web service, similar to ActiveResource in Rails. Currently JavaRESTAdaptor is read-only, but should be sufficient for consuming RESTful services.

Usage
The usage of JavaRESTAdaptor is best explained in an example. This example is adapted from an ActiveResource tutorial, but it provides several examples of common techniques. The service we're going to communicate with is a Beast installation. Beast is a simple Rails forum application. We'll use http://beast.caboo.se as our example service provider.

Creating the Model
Note that the instructions here assume that you're using the build of WOLips that builds tonight (to be able to pick "REST" from the adaptor list).


  1. Create a Wonder project.

  2. Create an EOModel (of any name) and choose "None" from the list of adaptors in the wizard.

  3. In the default database config (assuming you're using a recent WOLips), select the prototype "EORESTPrototypes" and select the Adaptor "REST".

  4. In the URL, enter "http://beast.caboo.se" and leave the username and password blank (currently JavaRESTAdaptor does not support HTTP authentication).


Creating Entites
Beast provides four entities that we want to interact with: Forum, Topic, Post, User.

  1. Create an entity named Forum.

  2. The "table name" of Forum will tell JavaRESTAdaptor the URL extensions and XML tags that can be used to access the entity. In the case of Forum, the XML tag name comes in two variants -- a singular and a plural form. For the Forum entity, set the table name to "forum,forums".

    Table name for JavaRESTAdaptor comes in several forms:
    • If you do not set a table name, JavaRESTAdaptor will simply use the entity name as the singular form, and use ERXLocalizer to generate a plural form.

    • If you only set a single value (i.e. "forum"), the adaptor will use that as the singular form and use ERXLocalizer to generate a plural form.

    • If you specify two values (i.e. "forum,forums"), the adaptor will use the first value as the singular form and the second value as the plural form.

    • Additionally, as you will see later, you can specify a set of possibly URL prefixes to use to access the entity. When you do not specify a URL prefix (as in our Forum example), the adaptor will assume that it can go to /[plural form].xml and /[plural form]/[id].xml to retrieve objects of this entity type.

  3. We need to determine the attributes of Forum, so open http://beast.caboo.se/forums.xml in your browser and view the source. ActiveRecord (or more importantly Ruby) can one-up us here, because they automatically determine the attributes of records based on queries to the service. We ultimately want to generate Java code, so we need to create an EOModel with attributes.

  4. Forum has the attributes: description, description-html, id, name, position, posts-count, and topics-count. Create attributes in your entity with the names converted to camelCase (description-html becomes descriptionHtml, etc). For each attribute, select a representative prototype, and set the external name to be the original name from the XML. For example, descriptionHtml is a "varcharLarge" prototype (we could pick one with a smaller restriction if we want to limit the values) and its external name is "description-html". postsCount is an "intNumber" and its external name is "posts-count". Do this for the rest of the attributes of Forum.

  5. In Beast, forums contain topics, so let's create the Topic entity. Topics present an interesting issue with modeling, because Beast does not allow you to select topics from the top level, rather you can only select topics through a Forum. For example, there is no /topics/1.xml there is only /forums/1/topics/1.xml.

    This oddity does cause some problems for EOF, because EOF expects to be able to fetch objects with only a primary key. JavaRESTAdaptor provides the ability to model fetching these entities, but you should be careful of places where EOF may cause faults for individual objects. You may find it safer to model relationships to entities of this type in code with fetch specs, or if you're using the Wonder eogen templates, traverse these kind of relationships with forum.topics(null, true), which will force a refetch of the entities.

  6. Request http://beast.caboo.se/forums/1/topics.xml to see an example of what topics look like. Follow the same procedure as for Forum, creating attributes from the XML. For dates, use the prototype "dateTime". For foreign keys, use the attribute "id" and mark them as non-class attributes.

  7. Now to address the URL issue for Topics. Because there is no /topics.xml, we need to tell JavaRESTAdaptor how to find topic objects. Edit the Topic entity and set the table name to "/forums/[forumID]/topics,topic,topics". This says that to fetch a topic, you must use the URL "/forums/[forumID]/topics" where forumID is a variable that corresponds to the Topic attribute of the same name. Any time a fetch is performed on a Topic, a forumID must appear in the qualifier or an exception will be thrown from JavaRESTAdaptor. When traversing a to-many relationship from a Forum to a Topic, this happens automatically inside of EOF. However, if you are trying to fetch a particular topic, you must provide its forum in your qualifier to prevent an error.

  8. Now create the Post entity. Posts are similar to Topics in that you cannot access them from the top level of the service, but they can be accessed in several different ways. It turns out that you can get posts for a user, for a forum, or for a topic (which requires a forum). JavaRESTAdaptor allows you to model these methods as alternative URLs in the table name by separating the URLs with a pipe. For instance, the table name for Post is "/users/[userID]/posts|/forums/[forumID]/posts|/forums/[forumID]/topics/[topicID]/posts,post,posts". I admit this is ugly, and ideally these definitions would be defined on the relationships instead of the entity, but unfortunately at the adaptor level, the EORelationship that was used to fetch the objects is long gone. What ERREST does in this case is that it looks at the qualifier provided and finds the URL that matches the most variables from your qualifier keys and uses that as the fetching URL. For instance, if your qualifier only has a userID in it, the /users/[userID]/posts URL will be used. However, if you qualifier has a forumID and a topicID in it, the /forums/[forumID]/topics/[topicID]/posts will be used. An exception will be thrown if a suitable URL cannot be found given the provided qualifier.

  9. Lastly, create your User entity. Nothing fancy here, and top-level fetches are allowed, so take a look at http://beast.caboo.se/users.xml to create your attributes, and use the table name "user,users" on this entity.


Relationships
You have created all of the entities for reading information from Beast. You can now create the relationships between these entities. For entities that can be fetched from the top level, or when modeling relationships that will provide enough context for the fetch, relationships can be modeled just like a normal eomodel.

For instance, from post.user() is a completely normal to-one relationship because User can be fetched from the top level, and post will provide the "id" necessary to execute the fetch. Similarly, forum.topics() is a normal to-many relationship, because while Topic does not have a topic level fetch URL, traversing the relationship from Forum will provide the "forumID" necessary to complete the fetch.

The "problem child" relationships are, for instance, topic.posts(). Traversing the posts relationship on Topic will only provide a topicID. Unfortunately Posts requires a topicID AND a forumID. It turns out that this can be modeled by putting both topicID and forumID in the joins of the to-many relationship definition. The downside of this is that it will not be symmetric with the other side of the relationship, and will not be updated automatically when write ability is added to JavaRESTAdaptor. The other problem is that, while this technique works on to-many relationships, it does not work on to-one relationships. EOF does not allow a to-one relationship to be defined with a join on anything expect a primary key attribute.

In this situation, you will need to construct the relationship using a fetch spec, and not using normal relationship definitions in the model. Your fetch spec will work just like a normal fetch spec, but you must provide all variables necessary to complete a fetch. As an example, if you want to fetch a particular topic, you must fetch it like:


Topic singleTopic = Topic.fetchRequiredTopic(editingContext, Topic.FORUM.eq(aForum).and(ERXQ.equals("id", 633)));


or


aForum.topics(ERXQ.equals("id", 633), true)


Note that JavaRESTAdaptor allows fetching on non-class attributes.

Fetching Notes
When fetching against the remote service, only topic level EOKeyValueQualifiers, EOAndQualifiers, and EOOrQualifiers with a single entry will be processed for constructing the remote URL. Complex qualifiers can be constructed, but they will be evaluated in-memory against the most restrictive URL that could be constructed given your qualifier attributes.

Examples

EOEditingContext editingContext = ERXEC.newEditingContext();

NSArray forums = Forum.fetchAllForums(editingContext);
System.out.println("Application.Application: Fetching all forums");
for (Forum forum : forums) {
System.out.println("Application.Application: " + forum.name() + ", " + forum.postsCount() + ", " + forum.topicsCount());
}

System.out.println("Application.Application: Fetching forum w/ PK");
Forum singleForum = (Forum) EOUtilities.objectWithPrimaryKeyValue(editingContext, Forum.ENTITY_NAME, "3");
System.out.println("Application.Application: " + singleForum.name());

System.out.println("Application.Application: Fetching topics for " + singleForum.name());
NSArray topics = singleForum.topics();
for (Topic topic : topics) {
System.out.println("Application.Application: " + topic.title() + " created " + topic.createdAt());
}

System.out.println("Application.Application: Fetching posts for forum");
NSArray forumPosts = singleForum.posts();
for (Post post : forumPosts) {
System.out.println("Application.Application: " + post.createdAt());
}

System.out.println("Application.Application: Refetching single topic w/ PK");
Topic singleTopic = Topic.fetchRequiredTopic(editingContext, Topic.FORUM.eq(singleForum).and(ERXQ.equals("id", 633)));
System.out.println("Application.Application: " + singleTopic.title());

System.out.println("Application.Application: Fetching topic user");
User user = singleTopic.user();
System.out.println("Application.Application: " + user.displayName());

System.out.println("Application.Application: Fetching posts for topic (composite pk, which is kind of interesting)");
NSArray topicPosts = singleTopic.posts();
for (Post post : topicPosts) {
System.out.println("Application.Application: " + post.createdAt());
}

Post randomPost = topicPosts.lastObject();
System.out.println("Application.Application: Fetching the topic for a post (this will break if topic is not already fetched)");
System.out.println("Application.Application: " + randomPost.topic().title());

System.out.println("Application.Application: Fetch author of post");
User postedByUser = randomPost.user();
System.out.println("Application.Application: " + postedByUser.displayName());

System.out.println("Application.Application: Fetching posts by author");
NSArray userPosts = postedByUser.posts();
for (Post post : userPosts) {
System.out.println("Application.Application: " + post.createdAt());
}


Check out JavaRESTAdaptorExample project to see the model described here fully completed.

5 comments:

  1. I'm trying to follow the Rest tutorial but I get stuck on the first part. After creating the model I see the Rest Adapter for not the Rest prototypes.

    Does anyone know what I'm doing wrong?

    ReplyDelete
  2. EORESTPrototypes is just like any other prototype in ERPrototypes, so it should appear in the list. I would make sure you're using an fully up-to-date Wonder and that you're linked against the latest ERPrototypes. You can also look in your ERPrototypes model to make sure it has the EORESTPrototypes entity in it.

    ReplyDelete
  3. Who knows where to download XRumer 5.0 Palladium?
    Help, please. All recommend this program to effectively advertise on the Internet, this is the best program!

    ReplyDelete
  4. I have been visiting various blogs for my term papers writing research. I have found your blog to be quite useful. Keep updating your blog with valuable information... Regards

    ReplyDelete