Abdera2 - Activity Streams TBD
An Activity represents an event that has occurred and consists of four primary components: Identifies the entity that performed the action. Identifies the action that was taken. Identifies the object that was acted upon. Identifies the object to which the action was directed. For instance, given the sentence, "Joe posted a link to Sally's Profile", "Joe" is the Actor, "posted" is the verb, "a link" is the object, and "Sally's Profile" is the target. To represent this activity using the JSON Activity Streams Format using Abdera2, we would use the following code:
Activity activity = makeActivity() .actor(makePerson("Joe")) .verb(POST) .object(makeBookmark("http://example.org")) .target(makeService().displayName("Sally's Wall")) .get();
Once created, we can serialize the activity to the standardized JSON format simply by calling the writeTo method on the activity:
IO io = IO.make().prettyPrint().get(); activity.writeTo(io, System.out);
For now, ignore the code that creates the IO object, we'll get back to the IO object in a bit. By calling writeTo in this way, however, Abdera2 will print out a nicely formatted JSON object:
{ "objectType": "activity", "actor": { "objectType": "person", "displayName": "Joe" }, "verb": "post", "object": { "objectType": "bookmark", "targetUrl": "http://example.org" }, "target": { "objectType": "service", "displayName": "Sally\u0027s Wall" } }
So now we have an Activity, but we do not quite yet have an Activity *Stream*. For that, we need to create a stream that contains the activity. If you're familiar with the Atom Syndication Format and RSS, Activities and Streams are the logical equivalent to Entries and Feeds.
stream = Collection.makeCollection() .item(activity) .get(); ]]>
Once the activity has been added to the stream, we can use the writeTo method (and the IO object we created) to output the results:
{ "objectType": "collection", "totalItems": 1, "items": [ { "objectType": "activity", "actor": { "objectType": "person", "displayName": "Joe" }, "verb": "post", "object": { "objectType": "bookmark", "targetUrl": "http://example.org" }, "target": { "objectType": "service", "displayName": "Sally\u0027s Wall" } } ] }
We now have a simple Activity Stream containing exactly one Activity. Note that the code example above uses a variety of objects and functions that have been statically imported to improve code readability. The imports used are shown below. In particular, note the static import of the various factory methods (e.g. makeCollection, makeActivity, etc). We'll touch more on the Factory API a bit later.
import static org.apache.abdera2.activities.model.Collection.makeCollection; import static org.apache.abdera2.activities.model.Activity.makeActivity; import static org.apache.abdera2.activities.model.objects.PersonObject.makePerson; import static org.apache.abdera2.activities.model.objects.ServiceObject.makeService; import static org.apache.abdera2.activities.model.objects.BookmarkObject.makeBookmark; import static org.apache.abdera2.activities.model.Verb.POST; import org.apache.abdera2.activities.model.Activity; import org.apache.abdera2.activities.model.Collection; import org.apache.abdera2.activities.model.IO;
Reading the Activity Stream is as equally straightforward. Let's assume that our Activity Stream has been handed to us in the form of a Reader, parsing the stream is as simple as:
stream = io.readCollection(reader); ]]>
Note that we're just reusing the IO object from the previous example. The readCollection method gives us back a Collection object that we can then iterate over to extract the Activity:
for (Activity a : stream.getItems()) { System.out.println(a.getActor().getDisplayName()); System.out.println(a.getVerb()); System.out.println(a.getObject().getObjectType()); System.out.println(a.getTarget().getDisplayName()); }
The IO object is the primary interface through which Activity Streams are Serialized and Deserialized. It handles all the details of converting between JSON and the Java Objects. Every IO object is created using a simple factory pattern that is leveraged extensively throughout the entire Activity Streams implementation. Once created, IO objects are immutable and threadsafe. There are two basic ways of creating an IO object. The first is to call the static get() method on the IO class, which returns an IO object that uses default configuration parameters. One or more TypeAdapters can be passed in as arguments to the get() method, but we'll address TypeAdapters later.
IO io = IO.get(); // create default, immutable IO object
The second way of creating IO is to use the static IO.make() method to create an IO Factory used to configure the IO will non-default options. You've already seen one example of using make() in the previous examples.
IO io = IO.make() .prettyPrint() .autoClose() .get();
Using the IO Factory is most useful when dealing with custom object serializations, which will be covered in more detail later.
Using the IO object to serialize Activity objects is as simple as calling an appropriate write() method. There are a variety of options depending on the specific needs of your application:
Serialize to a String: String s = io.write(stream);
Serialize to a Writer Writer writer = ... io.write(stream, writer);
Serialize to an OutputStream OutputStream out = ... io.write(stream, out);
One of the more advanced features of the IO object is the ability to perform nonblocking serialization and deserialization. This is done by integrating with the mechanisms provided by the java.util.concurrent.* package:
Nonblocking Serialization OutputStream out = ... ExecutorService exec = MoreExecutors2.getExitingExecutor(); io.write(stream, out, exec);
Using the IO object to deserialize Activity objects is just a slightly more complicated in that, because of the typeless nature of JSON Documents, you have to be reasonably sure in advance what exactly it is you're parsing (e.g. individual Activity or a Stream). Calling the io.read() method, IO will attempt to make a best guess based on heuristic analysis of the objects content to determine what kind of object is being parsed but it doesn't always get it right. Accordingly, if you know that you're parsing a Stream, you should use the appropriate readCollection methods. If you know you're parsing an individual Activity object, then use the readActivity method.
Reading from a Stringstream = io.readCollection(s) ]]>
Reading from a Readerstream = io.readCollection(reader) ]]>
Reading from an InputStreamstream = io.readCollection(in) ]]>
Non-blocking deserialization is also possible:
>() { public void onComplete(Collection stream) { // do something with the stream } }); ]]>
You can also use a Future to wait for the result:
> future = io.readCollection( s, MoreExecutors2.getExitingExecutor()); Collection stream = future.get(); ]]>
All Activity objects are created using a simple factory pattern. Created instances are all Immutable and Threadsafe. Let's look back at the very first example and break it down:
Activity activity = makeActivity() .actor(makePerson("Joe")) .verb(POST) .object(makeBookmark("http://example.org")) .target(makeService().displayName("Sally's Wall")) .get();
The first thing you should notice is that the factory uses what is known as a "Fluent" API. Another name for this is "method chaining". This pattern is utilized extensively throughout Abdera2. If you've never used a Fluent API before, it can take some getting used to, but with some practice it becomes very natural to use. The makeActivity() method is statically imported from the org.apache.abdera2.activities.model.Activity object:
import static org.apache.abdera2.activities.model.Activity.makeActivity;
It returns an ActivityBuilder object that is used to construct our activity. The methods of this object reflect all the various properties of the Activity. The call to "actor()" sets the value of the Activities "actor" property, etc. Notice how calls to other statically imported factory methods are mixed in. Each of these returns builders for their own respective types of objects. The makePerson method, for instance, returns a PersonBuilder, whilch makeBookmark returns a BookmarkBuilder. The final call to get() triggers the ActivityBuilder to build the immutable Activity object using the specified properties. Let's take a look at another example.
Creating a Person Object PersonObject person = makePerson() .displayName("John Doe") .email("john.doe@example.org") .id("acct:john.doe@example.org") .name(makeName() .givenName("John") .familyName("Doe")) .set("foo","bar") .get();
If we call the writeTo method on the person object we can get an idea of the JSON produced by this code:
{ "objectType": "person", "displayName": "John Doe", "id": "acct:john.doe@example.org", "name": { "objectType": "name", "givenName": "John", "familyName": "Doe" }, "foo": "bar", "emails": [ "john.doe@example.org" ] }
The Activity Streams implementation supports a broad range of specific object types like PersonObject that will be discussed shortly. These are designed to be composed together with Activities as the values of the actor, object and target properties.
All Activity Objects, once created, are immutable. To modify the properties of an object, we need to use it as a template to create a new object entirely. Suppose, for example, that we wish to add a property to the person object example given previously:
template() .aboutMe("This is John Doe") .get(); ]]>
The template() method is available on all objects and returns a builder object appropriate for that type. By default, the builder will have all the properties of the original object set. If you wish to change the value of an existing property, you must create a template that filters out the value to be modified:
template(withoutFields("emails")) .email("john.doe@example2.org") .get(); ]]>
There are occasions, albeit rare, that you'll need to treat one kind of object as if it were another type. This is most common when IO ends up generating the wrong kind of object during parse. Every object supports an as() method that creates a new instance of the desired type with a copy of the source objects properties.
ServiceObject service = person.as(ServiceObject.class);
The ability to convert objects like this leads to some rather interesting advanced capabilities that are beyond the scope of this getting started guide. Advanced topics will be covered separately.
Activity Stream objects are arbitrarily extensible. That is, new properties can be added to any object type at any time. The basic builder for each object supports a generic set property whose arguments take a string and any arbitrary object as the value. This is useful, but it's not typesafe. For instance, suppose our application requires that a Person have an extension property named "friendCount" whose value must be a integer. Using set, there's no way for us to enforce that constraint:
PersonObject person = makePerson() .displayName("John Doe") .set("friendCount","1") .get();
To enforce type-safety constraints, Abdera2 supports an alternative. First, let's define an extension interface:
public static interface MyExt extends Extra.ExtensionBuilder { MyExt friendCount(int i); }
Then, let's extend the PersonBuilder dynamically,
unwrap() .get(); ]]>
Note that "friendCount" is now set in a manner that is completely type-safe, and we maintain our Fluent API pattern. The objects themselves can also be extended in similar fashion:
public static interface MyExt2 extends Extra.ExtensionObject { int getFriendCount(); } int fc = person.extend(MyExt2.class).getFriendCount();
The CollectionWriter interface provides a simplified interface for streaming serialization of collections of Activity objects.
CollectionWriter cw = io.getCollectionWriter(System.out, "UTF-8"); cw.writeObject( makeActivity() .verb(POST) .actor(makePerson("Joe")) .object(makeBookmark("http://example.org")));
Out of the box, the IO object is capable of working with a broad range of simple and complex object types, including most of the types typically associated with Atom and Activity Streams implementations (Joda-Time DateTime objects, Entity Tags, URI Templates, Collection objects, Maps, etc). However, there are occasions when an application has to use a custom class object. For instance, suppose we have the following class:
public static class MyObject { private final String val; public MyObject(String v) { this.val = v; } public String toString() { return val; } public String getVal() { return val; } }
We want to be able to set an instance of MyObject as the value of an Activity Objects "foo" property, like so:
PersonObject person = makePerson("Joe") .set("foo", new MyObject("bar"));
When serialized into JSON, we want this is appear as a regular String field:
{ "objectType":"person", "displayName":"Joe", "foo":"bar" }
When parsing that JSON, however, we want the "foo" property to be interpreted as a MyObject instance so that calling object.getProperty("foo") returns MyObject. To achieve that goal, we first need to create a custom type adapter:
{ protected MyObject deserialize(String v) { return new MyObject(v); } } ]]>
The org.apache.abdera2.activities.io.gson.SimpleAdapter class is an abstract base class that handles most of the difficult work for us. By default, it uses the custom objects toString() method to serialize the object into a JSON string. If you need a more complex serialization, you will need to overload the serialize method. Note the use of the @AdaptedType annotation, this is required for custom type adapters. It tells the IO class which type of object this custom adapter is for. Once created, we need to register the type adapter with the IO object and tell it to associate the "foo" property with MyObject instances:
IO io = IO.make() .adapter(new MyObjectAdapter()) .property("foo", MyObject.class) .get();
Note that we're creating a new instance of the IO object. Since IO objects are immutable, custom type adapters and property assignments MUST be set up during the construction of the IO object. Once the IO instance is created, we can proceed as usual:
PersonObject person = makePerson("Joe") .set("foo", new MyObject("bar")); String str = io.write(base); // now parse it to check.. person = io.readObject(new StringReader(str)); MyObject obj = person.getProperty("foo"); System.out.println(obj.getVal());
Note that you need to take care when mapping property names to specific kinds of objects because IO will attempt to treat all instances of that property name as the given object type. This will cause problems if you use the same property name with different types of values within a single document. By default, IO uses the following property mappings -- meaning that whenever fields with these names are encountered in a document, they will automatically be interpreted as the given type. You can override the default interpretation by registering your own property mapping during IO construction. Name Type verborg.apache.abdera2.activities.model.Verb url",org.apache.abdera2.common.iri.IRI fileUrlorg.apache.abdera2.common.iri.IRI gadgetorg.apache.abdera2.common.iri.IRI updatedorg.joda.time.DateTime publishedorg.joda.time.DateTime langorg.apache.abdera2.common.lang.Lang @languageorg.apache.abdera2.common.lang.Lang @baseorg.apache.abdera2.common.iri.IRI $reforg.apache.abdera2.common.iri.IRI iconorg.apache.abdera2.activities.model.MediaLink imageorg.apache.abdera2.activities.model.MediaLink totalItemsInteger durationInteger heightInteger locationorg.apache.abdera2.activities.model.objects.PlaceObject reactionsorg.apache.abdera2.activities.model.objects.TaskObject moodorg.apache.abdera2.activities.model.objects.Mood addressorg.apache.abdera2.activities.model.objects.Address streamorg.apache.abdera2.activities.model.MediaLink fullImageorg.apache.abdera2.activities.model.MediaLink endTimeorg.joda.time.DateTime startTimeorg.joda.time.DateTime mimeTypejavax.activation.MimeType ratingDouble positionorg.apache.abdera2.common.geo.IsoPosition etagorg.apache.abdera2.common.http.EntityTag attendingorg.apache.abdera2.activities.model.Collection followersorg.apache.abdera2.activities.model.Collection followingorg.apache.abdera2.activities.model.Collection friendsorg.apache.abdera2.activities.model.Collection friend-requestsorg.apache.abdera2.activities.model.Collection likesorg.apache.abdera2.activities.model.Collection notAttendingorg.apache.abdera2.activities.model.Collection maybeAttendingorg.apache.abdera2.activities.model.Collection membersorg.apache.abdera2.activities.model.Collection repliesorg.apache.abdera2.activities.model.Collection reviewsorg.apache.abdera2.activities.model.Collection savesorg.apache.abdera2.activities.model.Collection sharesorg.apache.abdera2.activities.model.Collection Note that property mapping applies even if the properties value is an array, for instance, given our custom type mapping using the MyObject class, "foo":["bar","baz"] would be interpreted as collection of MyObject values:
list = person.getProperty("foo"); for (MyObject obj : list) {...} ]]>
The Activity Streams model is extensible, allowing developers to describe any type of activity with any type of object. As part of the standard, Activity Streams defines a handful of common basic object types and provides the mechanism for creating more. Abdera2 supports all of the core standard object types and introduces a number of its own. Refer to the API Documentation for details on each of Abdera's provided object types. To support creation of new object types, Abdera2 gives you the choice of either using the dynamic org.apache.abdera2.activities.model.ASObject API or the ability to extend the core objects to create a static extension. Creating a new object type using the dynamic API is simple:
ASObject myObject = ASObject.makeObject() .objectType("foo") .displayName("My Foo Object") .set("bar","baz") .get();
When serialized, this will look like:
{ "objectType":"foo", "displayName":"My Foo Object", "bar":"baz" }
Creating a new static object type requires a few more steps but is pretty straightforward:
Builder makeFoo() { return new Builder("foo"); } @Name("foo") public static final class Builder extends ASObject.Builder { public Builder() { super(FooObject,Builder.class); } public Builder(String objectType) { super(objectType,FooObject,Builder.class); } public Builder(Map map) { super(map,FooObject,Builder.class); } public Builder bar(String val) { set("bar","val"); return this; } } public FooObject(Map map) { super(map,Builder.class,FooObject.class); } public >FooObject(Map map, Class _class, Class_obj) { super(map,_class,_obj); } public String getBar() { return getProperty("bar"); } } ]]>
Here, we're creating two objects: FooObject and FooObject.Builder. FooObject.Builder extends from the core ASObject.Builder and provides all the necessary methods for constructing immutable instances of the FooObject class. FooObject extends from ASObject, inheriting all of the core Activity object fields and introducing a single extension field called "bar". Once defined, we can use our custom object type:
FooObject foo = FooObject.makeFoo() .displayName("My Foo Object") .bar("baz") .get();
The output is identical to that produced by the dynamic API:
{ "objectType":"foo", "displayName":"My Foo Object", "bar":"baz" }
The final step is to register your custom object type with IO, in order to have IO automatically generate instances of your custom object type when encountered within a document:
IO io = IO.make() .object(FooObject.Builder.class); .get(); FooObject foo = io.readObject(...);
Another key extension point within Activity Streams are the use of custom verbs. Within the standard, a collection of common verbs are defined and supported by Abdera, along with a handful of additional extension verbs. These include: add cancel checkin delete favorite follow give ignore invite join leave like make-friend post play receive remove remove-friend request-friend rsvp-maybe rsvp-no rsvp-yes save share stop-following tag unfavorite unlike unsave update comment purchase consume host read approve reject archive install close open resolve Each of these common verbs correlate to a constant on the org.apache.abdera2.activities.model.Verb object. Whenever possible, applications should always use an existing verb. However, there are cases when a new verb must be created. There are a couple points to keep in mind when creating a new verb: 1) They are always a single token value, whitespace is not allowed and 2) They are always case-insensitive, "Post" is equivalent to "post". To create a new verb within Abdera2, simple call the Verb.get() method, passing in the name of the verb:
Activity activity = Activity.makeActivity() .actor(PersonObject.makePerson("Me"))) .verb(Verb.get("foo")) .object(NoteObject.makeNote("A Note")) .get();
The "Responses for Activity Streams" specification defines an extensions to Activity Streams that support threaded conversations. Support for the extension has been built into Abdera2. Any Activity Streams object may have an "inReplyTo" property, whose value is an array of one or more objects for which the containing object is considered a response. The conceptual model is generally identical to that defined by the Atom Threading Extensions.
NoteObject note = NoteObject .makeNote() .id("urn:foo") .content("This is a note") .get(); NoteObject comment = NoteObject .makeNote() .id("urn:bar") .content("This is a comment") .inReplyTo(note) .get();
A common application use case is the ability to show the number of responses that are known for a given type of object. For that, the responses specification defines a number of common property names that are mapped to Activity Collection object values. These include: attending followers following friends friend-requests likes notAttending maybeAttending members replies reviews saves shares For example, if I have a note object that has received two comments, has been shared by one person, and liked by five people, within the JSON serialization it would look something like:
{ "objectType":"note", "content":"This is a note", "replies":{ "totalItems":2, "items":[ {"objectType":"note", "content":"This is a comment"}, {"objectType":"note", "content":"This is another comment"} ] }, "shares":{ "totalItems":1, "items":[ {"objectType":"person", "displayName":"Joe"} ] }, "likes":{ "totalItems":5 } }
Within the Abdera2 API, this would be:
replies = note.getProperty("replies"); Collection shares = note.getProperty("shares"); Collection likes = note.getProperty("likes"); System.out.println( String.format( "%d Comments, %d Shares, %d Likes", replies.getTotalItems(), shares.getTotalItems(), likes.getTotalItems()); ]]>
The "Audience Targeting for JSON Activities" specification defines a set of extension properties used to identify the target audience of the activity. Each of these properties ("to","cc","bto" and "bcc") are defined as arrays of objects. For instance:
{ "to":[{"objectType":"person","displayName":"Joe"}, {"objectType":"person","displayName":"Sally"}], "bto":[{"alias":"@network"}] }
Abdera2 includes built in support for the Audience Targeting extension:
Activity activity = Activity.makeActivity() .actor(PersonObject.makePerson("James")) .verb(Verb.POST) .object(NoteObject.makeNote().content("Test").get()) .target(ServiceObject.makeService().id("urn:my:wall").get()) .to(PersonObject.makePerson("Joe")) .to(PersonObject.makePerson("Sally")) .bto(Objects.NETWORK) .get();
Once created, you can use an extensive array of static methods from the org.apache.abdera2.activities.extra.Extra class to determine the audience of an activity, for instance:
import static org.apache.abdera2.activities.model.objects.PersonObject.*; import static org.apache.abdera2.activities.extra.Extra.*; isToMeOr(makePerson("Joe").get()).select(activity); // true isTo(makePerson("Joe").get()).select(activity); // true isCc(makePerson("Jane").get()).select(activity); // false isBccMe().select(activity); // false isBtoNetwork().select(activity); // true isTo(makePerson("Joe").id("urn:foo").get()).select(activity); // false
The audience testing methods are integrated with the Abdera2 Selector framework making it possible to filter Activity Streams based on the audience. For instance, suppose you want to grab only the activities from a stream that are targeted directly to Joe (using the "to" property):
stream = io.readCollection(...); Iterable items = stream.getItems( isTo(makePerson("Joe").get()); for (Activity activity : items) {...} ]]>
One important case is the ability to compare two instances of an object to determine what has changed from one to the other. Abdera2 supports a coarse-grained mechanism for comparing the differences between objects. For example,
template( ASBase.withoutFields("foo","bar")) .set("bar","xyz") .set("baz",123) .get(); Difference diff = person1.diff(person2); System.out.println(diff); ]]>
Here, we create one Person object with displayName = "Joe" and two extension properties named "foo" and "bar". We then use that object as a template to create a second one. Doing so, we remove the existing "foo" and "bar" fields and add a different "bar" field and a new "baz" field. Serialized as JSON, these two objects look something like:
{ "objectType":"person", "displayName":"Joe", "foo":"bar", "bar":"baz" } { "objectType":"person", "displayName":"Joe", "bar":"xyz", "baz":123 }
The call to person1.diff(person2) results in the creation of a Difference object that contains a summary of the differences between the two objects:
Difference diff = person1.diff(person2); System.out.println(diff);
Which outputs:
Changes: [[bar,[baz,xyz]]] Added: [[baz,123]] Removed: [[foo,bar]]
We can step through the changes using the Difference object:
> change : diff.changed()) { String field = change.first(); Pair values = change.second(); Object origValue = values.first(); Object newValue = values.second(); System.out.println( String.format( "Field '%s' changed from '%s' to '%s'", field, origValue, newValue)); } ]]>
The Activity Streams Base Schema specification defines a number of standard common extensions to the base Activity Streams format, including the ability to associate geographical location information with any Activity Streams object.
To adding location data to an Activity Streams object, set the "location" property:
The JSON produced looks like:
The "position" and "address" properties on the Place Object are optional, as are the extension "radius" and "elevation" fields. This design is intended to make the format as flexible as possible for a broad range of geolocation scenarios.
Activity Streams objects can be "tagged" with objects associated with the object. For instance, in the following example, the Person object is associated with another Person object and two hypothetical "hashtag" object types:
The JSON generated:
The Activity Streams format may have any arbitrary number of attachments associated with an object:
The "binary" object type is an extension object type introduced by Abdera2 that allows Base64-encoded binary data to be included within an Activity Stream. The binary data can be optionally compressed using GZip or Deflate and may include a hash code.
Example JSON for a Binary object:
When reading data from the binary object, calling the getInputStream() method will automatically decompress and decode the Base64 data:
-1) System.out.write(data,0,r); ]]>