Store Open Annotation RDF in Fedora4 to support the Linked Data for Libraries use cases.
Add this line to your Rails application's Gemfile
gem 'triannon'
Then install the gems:
$ bundle install
Then run the triannon generator:
$ rails g triannon:install
Edit the config/triannon.yml
file:
ldp:
Properties of LDP server
url:
the baseurl of LDP serveruber_container:
name of an LDP Basic Container holding all anno_containers
anno_containers:
names of LDP Basic Containers holding annossolr_url:
Points to the baseurl of Solr instance configured for Triannontriannon_base_url:
Used as the base url for all annotations hosted by your Triannon server. Identifiers from the LDP server will be appended to this base-url. Generally something like "https://your-triannon-rails-box/annotations", as "/annotations" is added to the path by the Triannon gemanno_containers:
foo:
bar:
auth:
users: []
workgroups:
- org:wg-A
- org:wg-B
Authorization applies only to POST and DELETE requests. In this example, the foo
container requires no authorization (all operations are allowed). On the other hand, the bar
container requires authorization. There are no authorized users
, but two workgroups
are authorized to modify annos in the container ('org:wg-A' and 'org:wg-B').
authorized_clients:
clientA: secretA
# expiry values are in seconds
client_token_expiry: 120
access_token_expiry: 3600
When authorization is required on a container, there must be at least one authorized client. The client credentials are used to validate an authorized client that will present requests on behalf of an authorized user or workgroup (see below for details on the authorization workflow). The client credentials are not specific to any container.
$ rake triannon:create_root_containers
This will generate the uber_container and the anno_containers (aka 'root containers') under the uber_container.
NOTE: you MUST create the root containers before creating any annotations. All annotation MUST be created as a child of a root container.
by using Rack::Cache for RestClient:
gem 'rest-client'
gem 'rack-cache'
gem 'rest-client-components'
bundle install
create config/initializers/rest_client.rb
require 'restclient/components'
require 'rack/cache'
RestClient.enable Rack::Cache,
metastore: "file:#{Rails.root}/tmp/rack-cache/meta",
entitystore: "file:#{Rails.root}/tmp/rack-cache/body",
default_ttl: 86400, # when to recheck, in seconds (daily = 60 x 60 x 24)
verbose: false
GET requests do not require authorization, even for containers that are configured with authorization for POST and DELETE requests.
as a IIIF Annotation List (see http://iiif.io/api/presentation/2.0/#other-content-resources)
GET
: http://(host)/annotations/search?targetUri=some.url.org
Search Parameters:
targetUri
- matches URI for target, with or without http or https scheme prefix
bodyUri
- matches URI for target, with or without http or https scheme prefix
bodyExact
- matches body characters exactly
bodyKeyword
- matches terms in body characters
motivatedBy
- matches fragment part of motivation predicate URI, e.g. commenting, tagging, painting
anno_root
- matches the root container of the result annos
use HTTP Accept
header with mime type to indicate desired format
Accept
: application/ld+json
Accept
: application/x-turtle
as a IIIF Annotation List (see http://iiif.io/api/presentation/2.0/#other-content-resources)
GET
: http://(host)/annotations/(root container)/search?targetUri=some.url.org
Search Parameters as above.
Accept
header with mime type to indicate desired format
Accept
: application/ld+json
Accept
: application/x-turtle
GET
: http://(host)/annotations/(root container)/(anno_id)
NOTE: you may need to URL encode the anno_id (e.g. "6f%2F0e%2F79%2F92%2F6f0e7992-83f5-4f31-8bb7-94a23465fdfb" instead of "6f/0e/79/92/6f0e7992-83f5-4f31-8bb7-94a23465fdfb"), particularly from a web browser.
Accept
header with mime type to indicate desired format
Accept
: application/ld+json; profile="http://www.w3.org/ns/oa-context-20130208.json"
Accept
: application/ld+json; profile="http://iiif.io/api/presentation/2/context.json"
Accept
: application/json
Link
: http://www.w3.org/ns/oa.json; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
You can request IIIF or OA context for jsonld.
The correct way:
Accept
header with mime type and context url:
Accept
: application/ld+json; profile="http://www.w3.org/ns/oa-context-20130208.json"
Accept
: application/ld+json; profile="http://iiif.io/api/presentation/2/context.json"
You can also use this method (with the correct HTTP Accept header):
GET
: http://(host)/annotations/(root)/(anno_id)?jsonld_context=iiif
GET
: http://(host)/annotations/(root)/(anno_id)?jsonld_context=oa
Note that OA (Open Annotation) is the default context if none is specified.
When a container is configured with authorization, it applies to POST and DELETE requests. For these requests, a valid access token is required in the request header, i.e. 'Authorization': 'Bearer {token_here}' (see details below on how to obtain an access token).
Note that annos must be created in an existing root container.
POST
: http://(host)/annotations/(root container)
{ "commit" => "Create Annotation", "annotation" => { "data" => oa_jsonld } }
Content-Type
header should be the mime type matching the bodyAccept
header
Accept
: application/ld+json; profile="http://www.w3.org/ns/oa-context-20130208.json"
Accept
: application/ld+json; profile="http://iiif.io/api/presentation/2/context.json"
Accept
: application/json
Link
: http://www.w3.org/ns/oa.json; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
DELETE
: http://(host)/annotations/(root container)/(anno_id)
NOTE: URL encode the anno_id (e.g. "6f%2F0e%2F79%2F92%2F6f0e7992-83f5-4f31-8bb7-94a23465fdfb" instead of "6f/0e/79/92/6f0e7992-83f5-4f31-8bb7-94a23465fdfb")
The triannon authorization is modeled on IIIF proposals, see
The authorization workflow accepts json and returns json (not json-ld). It involves three phases and triannon manages authorization using cookies, which must be retained across requests.
Let's assume we have the authorization configuration noted above.
curl
# client and user credentials (consistent with configs)
client='{"clientId":"clientA","clientSecret":"secretA"}'
user='{"userId":"userA","workgroups":"org:wg-A"}'
# 1. POST client credentials to '/auth/client_identity'
# to get client authorization code (save cookies)
code=$(curl -s -X POST -H "Content-Type: application/json" \
-c cookies.txt -d $client http://localhost:3000/auth/client_identity)
echo $code
#>> {"authorizationCode":"VjZQODJmbGtXU1VUMzdpcDhPemt4bjA2N3A4ZU1xQy9laTl3Uk9sUUd5YlZDMmtuZ05COFFtUVpHSGZzMGxLeC0ta2hDemRXdUdNQy9sMVJVeFJwdzN2dz09--8405018ec9fc7d751726417101963f18ee175c71"}
client_code=$(echo $code | sed -e 's/{"authorizationCode":"\(.*\)"}/\1/')
# 2. POST login credentials to '/auth/login' (save modified cookies)
curl -H "Content-Type: application/json" -X POST \
-c cookies.txt -b cookies.txt -d $user \
http://localhost:3000/auth/login?code=$client_code
# 3. GET '/auth/access_token' (save cookies)
curl -H "Content-Type: application/json" \
-c cookies.txt -b cookies.txt \
http://localhost:3000/auth/access_token?code=$client_code
#>> {"accessToken":"d09pSG5jVkhFMlVLendBNTdLd1lFZzBjZk5TZE1ONktNcDFiQzhibUV4eklsNURiRFNTbGg5YVFReElLR21HMVhmRzdYSU4vZUxjKzA5OGRjYjFMejJHTmo1UHF1cU00T0ZaNTNWMWVuR2M9LS1FT2RNUkJRbHlaTXU2ZTNvVnAwbGZRPT0=--c0a26b65e91137c82a5a42bcb9fd32f29bdfd0f3","tokenType":"Bearer","expiresIn":1438821498}
rest-client
require 'rest-client'
triannon_auth = RestClient::Resource.new(
'http://localhost:3000/auth',
cookies: {},
headers: { accept: :json, content_type: :json }
)
# 1. Obtain a client authorization code (short-lived token)
client = { clientId: 'clientA', clientSecret: 'secretA' }
response = triannon_auth["/client_identity"].post client.to_json
triannon_auth.options[:cookies] = response.cookies # save the cookie data
auth = JSON.parse(response.body)
client_code = auth['authorizationCode']
client_param = "?code=#{client_code}"
# 2. The client POSTs user credentials
user = { userId: 'userA', workgroups: 'org:wg-A' }
response = triannon_auth["/login#{client_param}"].post user.to_json
triannon_auth.options[:cookies] = response.cookies # save the cookie data
# 3. The client, on behalf of user, obtains a long-lived access token.
response = triannon_auth["/access_token#{client_param}"].get # no content type
triannon_auth.options[:cookies] = response.cookies # save the cookie data
access = JSON.parse(response.body)
access_token = "Bearer #{access['accessToken']}"
triannon_auth.headers[:Authorization] = access_token
See also the authenticate
method in:
There is a bundled rake task for running triannon in a test rails application, but there is some one-time set up.
$ rake engine_cart:generate # (first run only)
$ rake jetty:download
$ rake jetty:unzip
$ rake jetty:environment
$ rake triannon:jetty_config
triannon:jetty_config task does the following:
$ rake jetty:start
Note that jetty can be very sloooooooow (a couple of minutes) to start up with Fedora and Solr.
Go to
Go to
If all is not well for Fedora or Solr, try:
$ rake triannon:jetty_reset
This stops jetty, cleans out the jetty directory, recreates it anew from the download, configures jetty for Triannon, and starts jetty. It may take a long time (a couple of minutes) for jetty to restart.
Then check the Solr and Fedora urls again.
After you ensure that Fedora is running, you need to create the root anno containers using the configuration of test rails app created by engine_cart:
$ cd spec/internal
$ rake triannon:create_root_containers
$ cd ../..
NOTE: You probably won't need to change this file - it will work with the jetty setup provided.
$ vi spec/internal/config/triannon.yml
$ rake jetty:start # if it isn't still running
$ rake triannon:server
$ <cntl + C> # to stop Rails application
$ rake jetty:stop # to stop Fedora and Solr
The app will be running at localhost:3000, with root containers "foo" and "blah" available (and empty).
Run tests:
$ rake spec