Skip to content

Commit

Permalink
Add ResourceSync resource list capability
Browse files Browse the repository at this point in the history
  • Loading branch information
jcoyne committed Jun 14, 2016
1 parent af886fa commit c21c866
Show file tree
Hide file tree
Showing 14 changed files with 335 additions and 0 deletions.
1 change: 1 addition & 0 deletions .rubocop.yml
@@ -1,6 +1,7 @@
inherit_from: .rubocop_todo.yml

AllCops:
TargetRubyVersion: 2.1
DisplayCopNames: true
Include:
- '**/Rakefile'
Expand Down
41 changes: 41 additions & 0 deletions app/controllers/sufia/resource_sync_controller.rb
@@ -0,0 +1,41 @@
class Sufia::ResourceSyncController < ApplicationController
def source_description
# Caching based on host, for multitenancy support
body = Rails.cache.fetch("source_description_#{request.host}", expires_in: 1.week) do
build_source_description
end
render body: body, content_type: 'application/xml'
end

def capability_list
# Caching based on host, for multitenancy support
body = Rails.cache.fetch("source_description_#{request.host}", expires_in: 1.week) do
build_capability_list
end
render body: body, content_type: 'application/xml'
end

def resource_list
# Caching based on host, for multitenancy support
body = Rails.cache.fetch("source_description_#{request.host}", expires_in: 1.week) do
build_resource_list
end
render body: body, content_type: 'application/xml'
end

private

def build_resource_list
Sufia::ResourceSync::ResourceListWriter.new(capability_list_url: sufia.capability_list_url,
resource_host: request.host).write
end

def build_capability_list
Sufia::ResourceSync::CapabilityListWriter.new(resource_list_url: sufia.resource_list_url,
description_url: sufia.source_description_url).write
end

def build_source_description
Sufia::ResourceSync::SourceDescriptionWriter.new(capability_list_url: sufia.capability_list_url).write
end
end
1 change: 1 addition & 0 deletions app/views/layouts/_head_tag_content.html.erb
Expand Up @@ -3,6 +3,7 @@

<!-- added for use on small devices like phones -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="resourcesync" href="<%= sufia.capability_list_url %>"/>

<!-- Twitter card metadata -->
<%= yield :twitter_meta %>
Expand Down
5 changes: 5 additions & 0 deletions config/routes.rb
Expand Up @@ -9,6 +9,11 @@
# e.g. https://scholarsphere.psu.edu/files/gm80hv36p
get '/files/:id', to: redirect('/concern/generic_works/%{id}')

# ResourceSync routes
get '/.well-known/resourcesync' => 'sufia/resource_sync#source_description', as: :source_description
get '/capabilitylist' => 'sufia/resource_sync#capability_list', as: :capability_list
get '/resourcelist' => 'sufia/resource_sync#resource_list', as: :resource_list

delete '/uploads/:id', to: 'sufia/uploads#destroy', as: :sufia_uploaded_file
post '/uploads', to: 'sufia/uploads#create'
# This is a hack that is required because the rails form the uploader is on
Expand Down
1 change: 1 addition & 0 deletions lib/sufia.rb
Expand Up @@ -30,6 +30,7 @@ module Sufia
autoload :Arkivo
autoload :Configuration
autoload :RedisEventStore
autoload :ResourceSync
autoload :Zotero
end

Expand Down
11 changes: 11 additions & 0 deletions lib/sufia/resource_sync.rb
@@ -0,0 +1,11 @@
module Sufia
module ResourceSync
extend ActiveSupport::Autoload

eager_autoload do
autoload :CapabilityListWriter
autoload :ResourceListWriter
autoload :SourceDescriptionWriter
end
end
end
31 changes: 31 additions & 0 deletions lib/sufia/resource_sync/capability_list_writer.rb
@@ -0,0 +1,31 @@
module Sufia
module ResourceSync
class CapabilityListWriter
attr_reader :resource_list_url, :description_url
def initialize(resource_list_url:, description_url:)
@resource_list_url = resource_list_url
@description_url = description_url
end

def write
builder.to_xml
end

private

def builder
Nokogiri::XML::Builder.new do |xml|
xml.urlset('xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
'xmlns:rs' => 'http://www.openarchives.org/rs/terms/') do
xml['rs'].ln(rel: "up", href: description_url)
xml['rs'].md(capability: "capabilitylist")
xml.url do
xml.loc resource_list_url
xml['rs'].md(capability: 'resourcelist')
end
end
end
end
end
end
end
77 changes: 77 additions & 0 deletions lib/sufia/resource_sync/resource_list_writer.rb
@@ -0,0 +1,77 @@
module Sufia
module ResourceSync
# TODO: the big assumption I'm making here is that the repository has fewer
# than 50,000 resources to list. The Sitemap protocol is limited at 50,000
# items, so if we require more than that, we must have multiple Resource
# lists and add a Resource List Index to point to all of them.
class ResourceListWriter
attr_reader :resource_host, :capability_list_url

def initialize(resource_host:, capability_list_url:)
@resource_host = resource_host
@capability_list_url = capability_list_url
end

def write
builder.to_xml
end

private

def builder(capability_list_url: 'http://example.com/dataset1/capabilitylist.xml')
Nokogiri::XML::Builder.new do |xml|
xml.urlset('xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
'xmlns:rs' => 'http://www.openarchives.org/rs/terms/') do
xml['rs'].ln(rel: "up", href: capability_list_url)
xml['rs'].md(capability: "resourcelist", at: Time.now.utc.iso8601)
build_collections(xml)
build_works(xml)
build_files(xml)
end
end
end

def build_collections(xml)
Collection.search_in_batches(public_access) do |doc_set|
build_resources(xml, doc_set)
end
end

def build_works(xml)
CurationConcerns::WorkRelation.new.search_in_batches(public_access) do |doc_set|
build_resources(xml, doc_set)
end
end

def build_files(xml)
FileSet.search_in_batches(public_access) do |doc_set|
build_resources(xml, doc_set)
end
end

def build_resources(xml, doc_set)
doc_set.each do |doc|
build_resource(xml, doc)
end
end

def build_resource(xml, doc)
xml.url do
key = doc.fetch('has_model_ssim', []).first.constantize.model_name.singular_route_key
xml.loc routes.send(key + "_url", doc['id'], host: resource_host)
xml.lastmod doc['system_modified_dtsi']
end
end

def routes
Rails.application.routes.url_helpers
end

delegate :collection_url, to: :routes

def public_access
{ Hydra.config.permissions.read.group => 'public' }
end
end
end
end
30 changes: 30 additions & 0 deletions lib/sufia/resource_sync/source_description_writer.rb
@@ -0,0 +1,30 @@
module Sufia
module ResourceSync
class SourceDescriptionWriter
attr_reader :capability_list_url
def initialize(capability_list_url: 'http://example.com/dataset1/capabilitylist.xml')
@capability_list_url = capability_list_url
end

def write
builder.to_xml
end

private

def builder
Nokogiri::XML::Builder.new do |xml|
xml.urlset('xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
'xmlns:rs' => 'http://www.openarchives.org/rs/terms/') do
xml['rs'].ln(rel: "up", href: capability_list_url)
xml['rs'].md(capability: "description")
xml.url do
xml.loc capability_list_url
xml['rs'].md(capability: 'capabilitylist')
end
end
end
end
end
end
end
52 changes: 52 additions & 0 deletions spec/controllers/sufia/resource_sync_controller_spec.rb
@@ -0,0 +1,52 @@
RSpec.describe Sufia::ResourceSyncController do
before do
Rails.cache.clear
end

describe "source_description" do
let(:writer) { double }
let(:document) { '<xml>' }
let(:capability_list) { Sufia::Engine.routes.url_helpers.capability_list_url(host: 'test.host') }

it "is successful" do
allow(Sufia::ResourceSync::SourceDescriptionWriter).to receive(:new).with(capability_list_url: capability_list).and_return(writer)
expect(writer).to receive(:write).and_return(document)
get :source_description
expect(response.content_type).to eq 'application/xml'
expect(response.body).to eq document
end
end

describe "capability_list" do
let(:writer) { double }
let(:document) { '<xml>' }
let(:capability_list) { Sufia::Engine.routes.url_helpers.capability_list_url(host: 'test.host') }

it "is successful" do
allow(Sufia::ResourceSync::CapabilityListWriter).to receive(:new).with(resource_list_url: "http://test.host/resourcelist",
description_url: "http://test.host/.well-known/resourcesync").and_return(writer)
expect(writer).to receive(:write).and_return(document)
get :capability_list
expect(response.content_type).to eq 'application/xml'
expect(response.body).to eq document
end
end

describe "resource_list" do
before do
Rails.cache.clear
end

let(:writer) { double }
let(:document) { '<xml>' }
let(:capability_list) { Sufia::Engine.routes.url_helpers.capability_list_url(host: 'test.host') }

it "is successful" do
allow(Sufia::ResourceSync::ResourceListWriter).to receive(:new).with(capability_list_url: capability_list, resource_host: "test.host").and_return(writer)
expect(writer).to receive(:write).and_return(document)
get :resource_list
expect(response.content_type).to eq 'application/xml'
expect(response.body).to eq document
end
end
end
26 changes: 26 additions & 0 deletions spec/lib/sufia/capability_list_writer_spec.rb
@@ -0,0 +1,26 @@
require 'spec_helper'

RSpec.describe Sufia::ResourceSync::CapabilityListWriter do
let(:sitemap) { 'http://www.sitemaps.org/schemas/sitemap/0.9' }
let(:rs) { 'http://www.openarchives.org/rs/terms/' }

let(:resource_list) { 'http://example.com/resourcelist.xml' }
let(:description) { 'http://example.com/resourcesync_description.xml' }

subject { described_class.new(resource_list_url: resource_list,
description_url: description).write }
let(:xml) { Nokogiri::XML.parse(subject) }

it "has url to the capability list" do
description_href = xml.xpath('/x:urlset/rs:ln[@rel="up"]/@href', 'x' => sitemap, 'rs' => rs).map(&:value)
expect(description_href).to eq [description]

capability = xml.xpath('/x:urlset/rs:md/@capability', 'x' => sitemap, 'rs' => rs).map(&:value)
expect(capability).to eq ["capabilitylist"]

url = xml.xpath('//x:url[1]/x:loc', 'x' => sitemap).text
expect(url).to eq resource_list
capability = xml.xpath('//x:url[1]/rs:md/@capability', 'x' => sitemap, 'rs' => rs).map(&:value)
expect(capability).to eq ["resourcelist"]
end
end
24 changes: 24 additions & 0 deletions spec/lib/sufia/resource_list_writer_spec.rb
@@ -0,0 +1,24 @@
require 'spec_helper'

RSpec.describe Sufia::ResourceSync::ResourceListWriter do
let(:sitemap) { 'http://www.sitemaps.org/schemas/sitemap/0.9' }
let!(:private_collection) { create(:private_collection) }
let!(:public_collection) { create(:public_collection) }
let!(:public_work) { create(:public_generic_work) }
let!(:private_work) { create(:work) }
let!(:file_set) { create(:file_set, :public) }
let(:capability_list) { 'http://example.com/capabilityList.xml' }

subject { described_class.new(resource_host: 'example.com', capability_list_url: capability_list).write }
let(:xml) { Nokogiri::XML.parse(subject) }

it "has two urls" do
first_url = xml.xpath('//x:url[1]/x:loc', 'x' => sitemap).text
second_url = xml.xpath('//x:url[2]/x:loc', 'x' => sitemap).text
third_url = xml.xpath('//x:url[3]/x:loc', 'x' => sitemap).text
expect(first_url).to eq "http://example.com/collections/#{public_collection.id}"
expect(second_url).to eq "http://example.com/concern/generic_works/#{public_work.id}"
expect(third_url).to eq "http://example.com/concern/file_sets/#{file_set.id}"
expect(xml.xpath('//x:url', 'x' => sitemap).count).to eq 3
end
end
21 changes: 21 additions & 0 deletions spec/lib/sufia/source_description_writer_spec.rb
@@ -0,0 +1,21 @@
require 'spec_helper'

RSpec.describe Sufia::ResourceSync::SourceDescriptionWriter do
let(:sitemap) { 'http://www.sitemaps.org/schemas/sitemap/0.9' }
let(:rs) { 'http://www.openarchives.org/rs/terms/' }

let(:capability_list) { 'http://example.com/capabilityList.xml' }

subject { described_class.new(capability_list_url: capability_list).write }
let(:xml) { Nokogiri::XML.parse(subject) }

it "has url to the capability list" do
capability = xml.xpath('/x:urlset/rs:md/@capability', 'x' => sitemap, 'rs' => rs).map(&:value)
expect(capability).to eq ["description"]

url = xml.xpath('//x:url[1]/x:loc', 'x' => sitemap).text
expect(url).to eq capability_list
capability = xml.xpath('//x:url[1]/rs:md/@capability', 'x' => sitemap, 'rs' => rs).map(&:value)
expect(capability).to eq ["capabilitylist"]
end
end
14 changes: 14 additions & 0 deletions spec/routing/route_spec.rb
@@ -1,6 +1,20 @@
describe 'Routes', type: :routing do
routes { Sufia::Engine.routes }

describe "ResourceSync" do
it 'routes the well-known uri' do
expect(get: '/.well-known/resourcesync').to route_to(controller: 'sufia/resource_sync', action: 'source_description')
end

it 'routes the capability list' do
expect(get: '/capabilitylist').to route_to(controller: 'sufia/resource_sync', action: 'capability_list')
end

it 'routes the resource list' do
expect(get: '/resourcelist').to route_to(controller: 'sufia/resource_sync', action: 'resource_list')
end
end

describe 'Homepage' do
it 'routes the root url to the homepage controller' do
expect(get: '/').to route_to(controller: 'sufia/homepage', action: 'index')
Expand Down

0 comments on commit c21c866

Please sign in to comment.