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:Once created, we can serialize the activity to the standardized JSON
format simply by calling the writeTo method on the activity: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: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.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: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.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: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: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.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.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: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: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.Non-blocking deserialization is also possible:You can also use a Future to wait for the result: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: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: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.If we call the writeTo method on the person object we can get an
idea of the JSON produced by this code: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: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: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.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:To enforce type-safety constraints, Abdera2 supports an alternative.
First, let's define an extension interface:Then, let's extend the PersonBuilder dynamically,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:The CollectionWriter interface provides a simplified interface for
streaming serialization of collections of Activity objects.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:We want to be able to set an instance of MyObject as the value of
an Activity Objects "foo" property, like so:When serialized into JSON, we want this is appear as a regular
String field: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: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: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: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: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:When serialized, this will look like:Creating a new static object type requires a few more steps but
is pretty straightforward: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:The output is identical to that produced by the dynamic API: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: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: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.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:Within the Abdera2 API, this would be: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:Abdera2 includes built in support for the Audience Targeting extension: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: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):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,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: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:Which outputs:We can step through the changes using the Difference object: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.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 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.When reading data from the binary object, calling the getInputStream()
method will automatically decompress and decode the Base64 data: