rubydora 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +25 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +77 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/lib/rubydora/array_with_callback.rb +34 -0
- data/lib/rubydora/callbacks.rb +49 -0
- data/lib/rubydora/datastream.rb +152 -0
- data/lib/rubydora/digital_object.rb +190 -0
- data/lib/rubydora/ext/solr.rb +103 -0
- data/lib/rubydora/ext.rb +5 -0
- data/lib/rubydora/extension_parameters.rb +40 -0
- data/lib/rubydora/models_mixin.rb +28 -0
- data/lib/rubydora/relationships_mixin.rb +101 -0
- data/lib/rubydora/repository.rb +82 -0
- data/lib/rubydora/resource_index.rb +43 -0
- data/lib/rubydora/rest_api_client/v33.rb +36 -0
- data/lib/rubydora/rest_api_client.rb +267 -0
- data/lib/rubydora.rb +46 -0
- data/rubydora.gemspec +104 -0
- data/spec/datastream_spec.rb +100 -0
- data/spec/digital_object_spec.rb +260 -0
- data/spec/ext_solr_spec.rb +70 -0
- data/spec/integration_test_spec.rb +93 -0
- data/spec/repository_spec.rb +85 -0
- data/spec/resource_index_spec.rb +28 -0
- data/spec/rest_api_client_spec.rb +146 -0
- data/spec/spec_helper.rb +4 -0
- metadata +245 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
module Rubydora::Ext
|
2
|
+
# Mapping Fedora objects to Solr documents
|
3
|
+
module Solr
|
4
|
+
# load this module by mixing into appropriate modules
|
5
|
+
# @param [Hash] args
|
6
|
+
# @option args [Class] :digital_object
|
7
|
+
# @option args [Class] :datastream
|
8
|
+
def self.load args = { :digital_object => Rubydora::DigitalObject, :datastream => Rubydora::Datastream}
|
9
|
+
args[:digital_object].send(:include, Rubydora::Ext::Solr::DigitalObjectMixin) if args[:digital_object]
|
10
|
+
args[:datastream].send(:include, Rubydora::Ext::Solr::DatastreamMixin) if args[:datastream]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Datastreams mixin
|
14
|
+
module DatastreamMixin
|
15
|
+
# Initialize solr mapping logic
|
16
|
+
# @param [Class] base
|
17
|
+
def self.included(base)
|
18
|
+
base.instance_eval %Q{
|
19
|
+
class << self; attr_accessor :solr_mapping_logic end
|
20
|
+
}
|
21
|
+
|
22
|
+
base.class_eval %Q{
|
23
|
+
attr_writer :solr_mapping_logic
|
24
|
+
def solr_mapping_logic
|
25
|
+
@solr_mapping_logic ||= self.class.solr_mapping_logic.dup
|
26
|
+
end
|
27
|
+
}
|
28
|
+
|
29
|
+
base.solr_mapping_logic ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
# sets appropriate solr document parameters for this datastream
|
33
|
+
# @param [Hash] doc Solr document object (pass-by-reference)
|
34
|
+
def to_solr(doc = {})
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
# DigitalObject mixin
|
40
|
+
module DigitalObjectMixin
|
41
|
+
# Initialize solr mapping logic
|
42
|
+
# @param [Class] base
|
43
|
+
def self.included(base)
|
44
|
+
base.instance_eval %Q{
|
45
|
+
class << self; attr_accessor :solr_mapping_logic end
|
46
|
+
}
|
47
|
+
|
48
|
+
base.class_eval %Q{
|
49
|
+
attr_writer :solr_mapping_logic
|
50
|
+
def solr_mapping_logic
|
51
|
+
@solr_mapping_logic ||= self.class.solr_mapping_logic.dup
|
52
|
+
end
|
53
|
+
}
|
54
|
+
|
55
|
+
base.solr_mapping_logic ||= [:object_profile_to_solr,:datastreams_to_solr, :relations_to_solr]
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Set appropriate solr document attributes for this object
|
60
|
+
# @param [Hash] doc Solr document object (pass-by-reference)
|
61
|
+
def to_solr(doc = {})
|
62
|
+
self.solr_mapping_logic.each do |method_name|
|
63
|
+
send(method_name, doc)
|
64
|
+
end
|
65
|
+
|
66
|
+
doc.reject { |k,v| v.nil? or v.empty? }
|
67
|
+
end
|
68
|
+
|
69
|
+
# add solr document attributes from the object profile
|
70
|
+
# @param [Hash] doc Solr document object (pass-by-reference)
|
71
|
+
def object_profile_to_solr doc
|
72
|
+
doc['id'] = pid
|
73
|
+
doc['pid_s'] = pid
|
74
|
+
|
75
|
+
self.profile.each do |key, value|
|
76
|
+
doc["#{key}_s"] = value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# add solr document attributes from the object datastreams
|
81
|
+
# @param [Hash] doc Solr document object (pass-by-reference)
|
82
|
+
def datstreams_to_solr doc
|
83
|
+
datastreams.each do |dsid, ds|
|
84
|
+
doc['disseminates_s'] ||= []
|
85
|
+
doc['disseminates_s'] << [dsid]
|
86
|
+
ds.to_solr(doc)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# add solr document attributes by querying the resource index
|
91
|
+
# @param [Hash] doc Solr document object (pass-by-reference)
|
92
|
+
def relations_to_solr doc
|
93
|
+
self.repository.sparql("SELECT ? relation ?object FROM <#ri> WHERE {
|
94
|
+
#{uri} ?relation ?object
|
95
|
+
}").each do |row|
|
96
|
+
solr_field = "ri_#{row['relation'].split('#').last}_s"
|
97
|
+
doc[solr_field] ||= []
|
98
|
+
doc[solr_field] << row['object']
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/rubydora/ext.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Rubydora
|
2
|
+
# Copied in part from projectblacklight.org
|
3
|
+
module ExtensionParameters
|
4
|
+
# setup extension support
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ExtendableClassMethods
|
7
|
+
|
8
|
+
base.after_initialize do
|
9
|
+
apply_extensions
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# try to apply registered extensions
|
14
|
+
def apply_extensions
|
15
|
+
self.class.registered_extensions.each do |registration|
|
16
|
+
self.extend( registration[:module_obj] ) if registration[:condition_proc].nil? || registration[:condition_proc].call( self )
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Certain class-level modules needed for the document-specific
|
21
|
+
# extendability architecture
|
22
|
+
module ExtendableClassMethods
|
23
|
+
attr_writer :registered_extensions
|
24
|
+
|
25
|
+
# registered_extensions accessor
|
26
|
+
# @return [Array]
|
27
|
+
def registered_extensions
|
28
|
+
@registered_extensions ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
# register extensions
|
32
|
+
# @param [Module] module_obj
|
33
|
+
# @yield &condition
|
34
|
+
def use_extension( module_obj, &condition )
|
35
|
+
registered_extensions << {:module_obj => module_obj, :condition_proc => condition}
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Rubydora
|
2
|
+
##
|
3
|
+
# Provide access to registered content models
|
4
|
+
# FIXME: Ruby 1.9 provides instance_exec, which should make it
|
5
|
+
# possible to subsume this into Rubydora::RelationshipsMixin
|
6
|
+
module ModelsMixin
|
7
|
+
|
8
|
+
# Provides an accessor to the object content models
|
9
|
+
# @param [Hash] args
|
10
|
+
# @option args [Array] :values if nil, will query the resource index for related objects
|
11
|
+
# @return [ArrayWithCallback<Rubydora::DigitalObject>] an array that will call the #relationship_changed callback when values are modified
|
12
|
+
def models args = {}
|
13
|
+
@models = nil if args.delete(:refetch)
|
14
|
+
@models ||= relationship('info:fedora/fedora-system:def/model#hasModel', :values => args[:values] || profile['objModels'] || [])
|
15
|
+
end
|
16
|
+
|
17
|
+
# provides a setter that behaves as does #models
|
18
|
+
def models= arr
|
19
|
+
arr &&= [arr] unless arr.is_a? Array
|
20
|
+
old = models.dup || []
|
21
|
+
arr = @models = relationship('info:fedora/fedora-system:def/model#hasModel', :values => arr.flatten)
|
22
|
+
relationship_changed('info:fedora/fedora-system:def/model#hasModel', {:+ => arr - old, :- => old - arr }, @models)
|
23
|
+
|
24
|
+
@models
|
25
|
+
end
|
26
|
+
alias_method :model=, :models=
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Rubydora
|
2
|
+
#
|
3
|
+
# This model inject RELS-EXT-based helper methods
|
4
|
+
# for Fedora objects
|
5
|
+
#
|
6
|
+
module RelationshipsMixin
|
7
|
+
|
8
|
+
# FIXME: This should probably be defined on the DigitalObject
|
9
|
+
# Map Rubydora accessors to Fedora RELS-EXT predicates
|
10
|
+
RELS_EXT = {"annotations"=>"info:fedora/fedora-system:def/relations-external#hasAnnotation",
|
11
|
+
"has_metadata"=>"info:fedora/fedora-system:def/relations-external#hasMetadata",
|
12
|
+
"description_of"=>"info:fedora/fedora-system:def/relations-external#isDescription_of",
|
13
|
+
"part_of"=>"info:fedora/fedora-system:def/relations-external#isPart_of",
|
14
|
+
"descriptions"=>"info:fedora/fedora-system:def/relations-external#hasDescription",
|
15
|
+
"dependent_of"=>"info:fedora/fedora-system:def/relations-external#isDependent_of",
|
16
|
+
"constituents"=>"info:fedora/fedora-system:def/relations-external#hasConstituent",
|
17
|
+
"parts"=>"info:fedora/fedora-system:def/relations-external#hasPart",
|
18
|
+
"memberOfCollection"=>"info:fedora/fedora-system:def/relations-external#isMemberOfCollection",
|
19
|
+
"member_of"=>"info:fedora/fedora-system:def/relations-external#isMember_of",
|
20
|
+
"equivalents"=>"info:fedora/fedora-system:def/relations-external#hasEquivalent",
|
21
|
+
"derivations"=>"info:fedora/fedora-system:def/relations-external#hasDerivation",
|
22
|
+
"derivation_of"=>"info:fedora/fedora-system:def/relations-external#isDerivation_of",
|
23
|
+
"subsets"=>"info:fedora/fedora-system:def/relations-external#hasSubset",
|
24
|
+
"annotation_of"=>"info:fedora/fedora-system:def/relations-external#isAnnotation_of",
|
25
|
+
"metadata_for"=>"info:fedora/fedora-system:def/relations-external#isMetadataFor",
|
26
|
+
"dependents"=>"info:fedora/fedora-system:def/relations-external#hasDependent",
|
27
|
+
"subset_of"=>"info:fedora/fedora-system:def/relations-external#isSubset_of",
|
28
|
+
"constituent_of"=>"info:fedora/fedora-system:def/relations-external#isConstituent_of",
|
29
|
+
"collection_members"=>"info:fedora/fedora-system:def/relations-external#hasCollectionMember",
|
30
|
+
"members"=>"info:fedora/fedora-system:def/relations-external#hasMember"}
|
31
|
+
|
32
|
+
# generate accessor methods for each RELS_EXT property
|
33
|
+
def self.included(base)
|
34
|
+
|
35
|
+
# FIXME: ugly, but functional..
|
36
|
+
RELS_EXT.each do |name, property|
|
37
|
+
base.class_eval <<-RUBY
|
38
|
+
def #{name.to_s} args = {}
|
39
|
+
relationships[:#{name}] = nil if args.delete(:refetch)
|
40
|
+
relationships[:#{name}] ||= relationship('#{property}', args)
|
41
|
+
end
|
42
|
+
|
43
|
+
def #{name.to_s}= arr
|
44
|
+
arr &&= [arr] unless arr.is_a? Array
|
45
|
+
old = #{name.to_s}.dup || []
|
46
|
+
arr = relationships[:#{name}] = relationship('#{property}', :values => arr.flatten)
|
47
|
+
relationship_changed('#{property}', {:+ => arr - old, :- => old - arr }, arr)
|
48
|
+
|
49
|
+
arr
|
50
|
+
end
|
51
|
+
RUBY
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Provides an accessor to the `predicate` RELS-EXT relationship
|
57
|
+
# Using ArrayWithCallback, will commit any changes to Fedora
|
58
|
+
#
|
59
|
+
# @param [String] predicate
|
60
|
+
# @param [Hash] args
|
61
|
+
# @option args [Array] :values if nil, will query the resource index for related objects
|
62
|
+
# @return [ArrayWithCallback<Rubydora::DigitalObject>] an array that will call the #relationship_changed callback when values are modified
|
63
|
+
def relationship predicate, args = {}
|
64
|
+
arr = ArrayWithCallback.new(args[:values] || repository.find_by_sparql_relationship(fqpid, predicate))
|
65
|
+
arr.on_change << lambda { |arr, diff| relationship_changed(predicate, diff, arr) }
|
66
|
+
|
67
|
+
arr
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
##
|
72
|
+
# Given a predicate and a diff between before and after states
|
73
|
+
# commit the appropriate changes to Fedora
|
74
|
+
#
|
75
|
+
# @param [String] predicate
|
76
|
+
# @param [Hash] diff
|
77
|
+
# @option diff [Hash] :+ additions
|
78
|
+
# @option diff [Hash] :- deletions
|
79
|
+
# @param [Array] arr the current relationship state
|
80
|
+
def relationship_changed predicate, diff, arr = []
|
81
|
+
diff[:+] ||= []
|
82
|
+
diff[:-] ||= []
|
83
|
+
|
84
|
+
diff[:+].each do |o|
|
85
|
+
obj_uri = (( o.fqpid if o.respond_to? :fqpid ) || ( o.uri if o.respond_to? :uri ) || (o.to_s if o.respond_to? :to_s?) || o )
|
86
|
+
repository.add_relationship :subject => fqpid, :predicate => predicate, :object => obj_uri
|
87
|
+
end
|
88
|
+
|
89
|
+
diff[:-].each do |o|
|
90
|
+
obj_uri = (( o.fqpid if o.respond_to? :fqpid ) || ( o.uri if o.respond_to? :uri ) || (o.to_s if o.respond_to? :to_s?) || o )
|
91
|
+
repository.purge_relationship :subject => fqpid, :predicate => predicate, :object => obj_uri
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# accessor to all retrieved relationships
|
96
|
+
# @return [Hash]
|
97
|
+
def relationships
|
98
|
+
@relationships ||= {}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Rubydora
|
2
|
+
# Fedora Repository object that provides API access
|
3
|
+
class Repository
|
4
|
+
include ResourceIndex
|
5
|
+
include RestApiClient
|
6
|
+
|
7
|
+
# repository configuration (see #initialize)
|
8
|
+
attr_reader :config
|
9
|
+
|
10
|
+
# @param [Hash] options
|
11
|
+
# @option options [String] :url
|
12
|
+
# @option options [String] :user
|
13
|
+
# @option options [String] :password
|
14
|
+
def initialize options = {}
|
15
|
+
@config = options
|
16
|
+
load_api_abstraction
|
17
|
+
end
|
18
|
+
|
19
|
+
# {include:DigitalObject.find}
|
20
|
+
def find pid
|
21
|
+
DigitalObject.find(pid, self)
|
22
|
+
end
|
23
|
+
|
24
|
+
# repository profile (from API-A-LITE data)
|
25
|
+
# @return [Hash]
|
26
|
+
def profile
|
27
|
+
@profile ||= begin
|
28
|
+
profile_xml = client['describe?xml=true'].get
|
29
|
+
profile_xml.gsub! '<fedoraRepository', '<fedoraRepository xmlns="http://www.fedora.info/definitions/1/0/access/"' unless profile_xml =~ /xmlns=/
|
30
|
+
doc = Nokogiri::XML(profile_xml)
|
31
|
+
xmlns = { 'access' => "http://www.fedora.info/definitions/1/0/access/" }
|
32
|
+
h = doc.xpath('/access:fedoraRepository/*', xmlns).inject({}) do |sum, node|
|
33
|
+
sum[node.name] ||= []
|
34
|
+
case node.name
|
35
|
+
when "repositoryPID"
|
36
|
+
sum[node.name] << Hash[*node.xpath('access:*', xmlns).map { |x| [node.name, node.text]}.flatten]
|
37
|
+
else
|
38
|
+
sum[node.name] << node.text
|
39
|
+
end
|
40
|
+
sum
|
41
|
+
end
|
42
|
+
h.select { |key, value| value.length == 1 }.each do |key, value|
|
43
|
+
next if key == "objModels"
|
44
|
+
h[key] = value.first
|
45
|
+
end
|
46
|
+
|
47
|
+
h
|
48
|
+
rescue
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Float] repository version
|
54
|
+
def version
|
55
|
+
@version ||= profile['repositoryVersion'].to_f rescue nil
|
56
|
+
end
|
57
|
+
|
58
|
+
# Raise an error if unable to connect to the API endpoint
|
59
|
+
def ping
|
60
|
+
raise "Unable to establish connection to Fedora Repository" unless profile
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
# Load fallback API implementations for older versions of Fedora
|
67
|
+
def load_api_abstraction
|
68
|
+
return unless version
|
69
|
+
|
70
|
+
if version <= 3.0
|
71
|
+
end
|
72
|
+
|
73
|
+
if version < 3.4
|
74
|
+
require 'rubydora/rest_api_client/v33'
|
75
|
+
self.extend Rubydora::RestApiClient::V33
|
76
|
+
end
|
77
|
+
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Rubydora
|
2
|
+
# Fedora resource index helpers
|
3
|
+
module ResourceIndex
|
4
|
+
# Find new objects using a sparql query
|
5
|
+
# @param [String] query SPARQL query
|
6
|
+
# @param [Hash] options
|
7
|
+
# @option options [String] :binding the SPARQL binding name to create new objects from
|
8
|
+
# @return [Array<Rubydora::DigitalObject>]
|
9
|
+
def find_by_sparql query, options = { :binding => 'pid' }
|
10
|
+
self.sparql(query).map { |x| self.find(x[options[:binding]]) rescue nil }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Find new objects by their relationship to a subject
|
14
|
+
# @param [String] subject Subject URI
|
15
|
+
# @param [String] predicate Predicate URI
|
16
|
+
# @return [Array<Rubydora::DigitalObject>]
|
17
|
+
def find_by_sparql_relationship subject, predicate
|
18
|
+
find_by_sparql <<-RELSEXT
|
19
|
+
SELECT ?pid FROM <#ri> WHERE {
|
20
|
+
<#{subject}> <#{predicate}> ?pid
|
21
|
+
}
|
22
|
+
RELSEXT
|
23
|
+
end
|
24
|
+
|
25
|
+
# Run a raw SPARQL query and return a FasterCSV object
|
26
|
+
# @param [String] query SPARQL query
|
27
|
+
# @return [FasterCSV::Table]
|
28
|
+
def sparql query
|
29
|
+
FasterCSV.parse(self.risearch(query), :headers => true)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
# Run a raw query against the Fedora risearch resource index
|
34
|
+
# @param [String] query
|
35
|
+
# @param [Hash] options
|
36
|
+
def risearch query, options = {}
|
37
|
+
request_params = { :dt => 'on', :format => 'CSV', :lang => 'sparql', :limit => nil, :query => query, :type => 'tuples' }.merge(options)
|
38
|
+
|
39
|
+
self.client['risearch'].post request_params
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rubydora::RestApiClient
|
2
|
+
# Fall-back implementations for fcrepo < 3.4
|
3
|
+
module V33
|
4
|
+
# SOAP API endpoint
|
5
|
+
# @return [SOAP::WSDLDriverFactory]
|
6
|
+
def soap
|
7
|
+
return @soap if @soap
|
8
|
+
gem "soap4r"
|
9
|
+
require 'soap/wsdlDriver'
|
10
|
+
|
11
|
+
@soap = SOAP::WSDLDriverFactory.new("#{ config[:url] }/wsdl?api=API-M").create_rpc_driver
|
12
|
+
@soap.options['protocol.http.basic_auth'] << [config[:url], config[:user], config[:password]]
|
13
|
+
|
14
|
+
@soap
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
# {include:RestApiClient#relationships}
|
19
|
+
def relationships options = {}
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
# {include:RestApiClient#add_relationship}
|
24
|
+
def add_relationship options = {}
|
25
|
+
pid = options.delete(:pid) || options[:subject]
|
26
|
+
self.soap.addRelationship(:pid => pid, :relationship => options[:predicate], :object => options[:object], :isLiteral => false, :datatype => nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
# {include:RestApiClient#purge_relationship}
|
30
|
+
def purge_relationship options = {}
|
31
|
+
pid = options.delete(:pid) || options[:subject]
|
32
|
+
self.soap.purgeRelationship(:pid => pid, :relationship => options[:predicate], :object => options[:object], :isLiteral => false, :datatype => nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|