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.
@@ -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
@@ -0,0 +1,5 @@
1
+ module Rubydora
2
+ # Extensions namespace
3
+ module Ext
4
+ end
5
+ end
@@ -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