Abdera2 - Activity StreamsTBDAn 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.NameTypeverborg.apache.abdera2.activities.model.Verburl",org.apache.abdera2.common.iri.IRIfileUrlorg.apache.abdera2.common.iri.IRIgadgetorg.apache.abdera2.common.iri.IRIupdatedorg.joda.time.DateTimepublishedorg.joda.time.DateTimelangorg.apache.abdera2.common.lang.Lang@languageorg.apache.abdera2.common.lang.Lang@baseorg.apache.abdera2.common.iri.IRI$reforg.apache.abdera2.common.iri.IRIiconorg.apache.abdera2.activities.model.MediaLinkimageorg.apache.abdera2.activities.model.MediaLinktotalItemsIntegerdurationIntegerheightIntegerlocationorg.apache.abdera2.activities.model.objects.PlaceObjectreactionsorg.apache.abdera2.activities.model.objects.TaskObjectmoodorg.apache.abdera2.activities.model.objects.Moodaddressorg.apache.abdera2.activities.model.objects.Addressstreamorg.apache.abdera2.activities.model.MediaLinkfullImageorg.apache.abdera2.activities.model.MediaLinkendTimeorg.joda.time.DateTimestartTimeorg.joda.time.DateTimemimeTypejavax.activation.MimeTyperatingDoublepositionorg.apache.abdera2.common.geo.IsoPositionetagorg.apache.abdera2.common.http.EntityTagattendingorg.apache.abdera2.activities.model.Collectionfollowersorg.apache.abdera2.activities.model.Collectionfollowingorg.apache.abdera2.activities.model.Collectionfriendsorg.apache.abdera2.activities.model.Collectionfriend-requestsorg.apache.abdera2.activities.model.Collectionlikesorg.apache.abdera2.activities.model.CollectionnotAttendingorg.apache.abdera2.activities.model.CollectionmaybeAttendingorg.apache.abdera2.activities.model.Collectionmembersorg.apache.abdera2.activities.model.Collectionrepliesorg.apache.abdera2.activities.model.Collectionreviewsorg.apache.abdera2.activities.model.Collectionsavesorg.apache.abdera2.activities.model.Collectionsharesorg.apache.abdera2.activities.model.CollectionNote 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:addcancelcheckindeletefavoritefollowgiveignoreinvitejoinleavelikemake-friendpostplayreceiveremoveremove-friendrequest-friendrsvp-maybersvp-norsvp-yessavesharestop-followingtagunfavoriteunlikeunsaveupdatecommentpurchaseconsumehostreadapproverejectarchiveinstallcloseopenresolveEach 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:attendingfollowersfollowingfriendsfriend-requestslikesnotAttendingmaybeAttendingmembersrepliesreviewssavessharesFor 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();
PairThe 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);
]]>