ruby-fedora 0.1.0

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,43 @@
1
+ module Ambition
2
+ module Adapters
3
+ module ActiveFedora
4
+ class Sort < Base
5
+ # >> sort_by { |u| u.age }
6
+ # => #sort_by(:age)
7
+ def sort_by(method)
8
+ raise "Not implemented."
9
+ end
10
+
11
+ # >> sort_by { |u| -u.age }
12
+ # => #reverse_sort_by(:age)
13
+ def reverse_sort_by(method)
14
+ raise "Not implemented."
15
+ end
16
+
17
+ # >> sort_by { |u| u.profile.name }
18
+ # => #chained_sort_by(:profile, :name)
19
+ def chained_sort_by(receiver, method)
20
+ raise "Not implemented."
21
+ end
22
+
23
+ # >> sort_by { |u| -u.profile.name }
24
+ # => #chained_reverse_sort_by(:profile, :name)
25
+ def chained_reverse_sort_by(receiver, method)
26
+ raise "Not implemented."
27
+ end
28
+
29
+ # >> sort_by(&:name)
30
+ # => #to_proc(:name)
31
+ def to_proc(symbol)
32
+ raise "Not implemented."
33
+ end
34
+
35
+ # >> sort_by { rand }
36
+ # => #rand
37
+ def rand
38
+ raise "Not implemented."
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ class Fedora::BaseObject
2
+ attr_accessor :attributes, :xml_content, :uri, :url
3
+ attr_reader :errors, :uri
4
+ attr_writer :new_object
5
+
6
+ # == Parameters
7
+ # attrs<Hash>:: object attributes
8
+ #-
9
+ def initialize(attrs = nil)
10
+ @new_object = true
11
+ @attributes = attrs || {}
12
+ @errors = []
13
+ @xml_content = attributes.delete(:xml_content)
14
+ end
15
+
16
+ def new_object?
17
+ @new_object
18
+ end
19
+ end
@@ -0,0 +1,97 @@
1
+ require "base64"
2
+ require 'cgi'
3
+ require "mime/types"
4
+
5
+ # Add multipart/form-data support to net/http
6
+ #
7
+ # ==== Usage
8
+ # File.open(File.expand_path('script/test.png'), 'r') do |file|
9
+ # http = Net::HTTP.new('localhost', 3000)
10
+ # begin
11
+ # http.start do |http|
12
+ # request = Net::HTTP::Post.new('/your/url/here')
13
+ # request.set_multipart_data(:file => file, :title => 'test.png')
14
+ # response = http.request(request)
15
+ # puts response
16
+ # end
17
+ # rescue Net::HTTPServerException => e
18
+ # p e
19
+ # end
20
+ # end
21
+ module Fedora::Multipart
22
+ def set_multipart_data(param_hash={})
23
+ boundary_token = [Array.new(8) {rand(256)}].join
24
+ self.content_type = "multipart/form-data; boundary=#{boundary_token}"
25
+ boundary_marker = "--#{boundary_token}\r\n"
26
+ self.body = param_hash.map { |param_name, param_value|
27
+ boundary_marker + case param_value
28
+ when File then file_to_multipart(param_name, param_value)
29
+ when String then text_to_multipart(param_name, param_value)
30
+ else ""
31
+ end
32
+ }.join('') + "--#{boundary_token}--\r\n"
33
+ end
34
+
35
+ private
36
+ def file_to_multipart(key,file)
37
+ filename = File.basename(file.path)
38
+ mime_types = MIME::Types.of(filename)
39
+ mime_type = mime_types.empty? ? "application/octet-stream" : mime_types.first.content_type
40
+ part = %Q{Content-Disposition: form-data; name="#{key}"; filename="#{filename}"\r\n}
41
+ part += "Content-Transfer-Encoding: binary\r\n"
42
+ part += "Content-Type: #{mime_type}\r\n\r\n#{file.read}"
43
+ end
44
+
45
+ def text_to_multipart(key,value)
46
+ "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n#{value}\r\n"
47
+ end
48
+ end
49
+
50
+ class Net::HTTP::Post
51
+ include Fedora::Multipart
52
+ end
53
+
54
+ class Net::HTTP::Put
55
+ include Fedora::Multipart
56
+ end
57
+
58
+ # Add multipart/form-data support to active_resource
59
+ module ActiveResource
60
+ class Connection
61
+ alias_method :post_without_multipart, :post
62
+ alias_method :put_without_multipart, :put
63
+
64
+ def put(path, body = '', headers = {})
65
+ if body.is_a?(File)
66
+ multipart_request(Net::HTTP::Put.new(path, build_request_headers(headers)), body)
67
+ else
68
+ put_without_multipart(path, body, headers)
69
+ end
70
+ end
71
+
72
+ def post(path, body = '', headers = {})
73
+ if body.is_a?(File)
74
+ multipart_request(Net::HTTP::Post.new(path, build_request_headers(headers)), body)
75
+ else
76
+ post_without_multipart(path, body, headers)
77
+ end
78
+ end
79
+
80
+ def multipart_request(req, file)
81
+ logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
82
+ result = nil
83
+ time = Benchmark.realtime do
84
+ http.start do |conn|
85
+ req.set_multipart_data(:file => file)
86
+ result = conn.request(req)
87
+ end
88
+ end
89
+ logger.info "--> #{result.code} #{result.message} (#{result.body ? result.body : 0}b %.2fs)" % time if logger
90
+ handle_response(result)
91
+ end
92
+
93
+ def raw_get(path, headers = {})
94
+ request(:get, path, build_request_headers(headers))
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,21 @@
1
+ require 'fedora/base_object'
2
+
3
+ class Fedora::Datastream < Fedora::BaseObject
4
+ def initialize(attrs = nil)
5
+ super
6
+ # TODO: check for required attributes
7
+ end
8
+
9
+ def pid
10
+ attributes[:pid]
11
+ end
12
+
13
+ def dsid
14
+ attributes[:dsID]
15
+ end
16
+
17
+ # See http://www.fedora.info/definitions/identifiers/
18
+ def uri
19
+ "fedora:info/#{pid}/datastreams/#{dsid}"
20
+ end
21
+ end
@@ -0,0 +1,119 @@
1
+ require 'xmlsimple'
2
+ require 'rexml/document'
3
+ require 'fedora/base_object'
4
+
5
+ class Fedora::FedoraObject < Fedora::BaseObject
6
+ attr_accessor :target_repository
7
+
8
+ # = Parameters
9
+ # attrs<Hash>:: fedora object attributes (see below)
10
+ #
11
+ # == Attributes (attrs)
12
+ # namespace<Symbol>::
13
+ # pid<Symbol>::
14
+ # state<Symbol>::
15
+ # label<Symbol>::
16
+ # contentModel<Symbol>::
17
+ # objectXMLFormat<Symbol>::
18
+ # ownerID<Symbol>::
19
+ #-
20
+ def initialize(attrs = nil)
21
+ super
22
+ # TODO: check for required attributes
23
+ end
24
+
25
+ ####
26
+ # Attribute Accessors
27
+ ####
28
+
29
+ # TODO: Create appropriate attribute accessors for these values.
30
+ # Where applicable, make sure that attributes are written to and read from self.attributes
31
+ # [pid, label, create_date, modified_date, fedora_object_type, contentModel, state, ownerID, behavior_def, behavior_mech,]
32
+ # API-M Search Value Equivalents: [pid, label, cDate, mDate, fType, cModel, state, ownerId, bDef, bMech,]
33
+ # TODO: Mix In DC attribute finders/accessors
34
+ # TODO: Make sure that fedora_object_type and contentModel are juggled properly.
35
+ def retrieve_attr_from_fedora
36
+ self.attributes.merge!(objectProfile)
37
+ self.attributes.merge!({
38
+ :state => objectXML.root.elements["objectProperties/property[@NAME='info:fedora/fedora-system:def/model#state']"].attributes["value"]
39
+ })
40
+
41
+ end
42
+
43
+ def create_date
44
+ objectProfile[:create_date]
45
+ end
46
+
47
+ def modified_date
48
+ objectProfile[:modified_date]
49
+ end
50
+
51
+
52
+ def pid
53
+ self.attributes[:pid]
54
+ end
55
+
56
+ def pid=(new_pid)
57
+ self.attributes.merge!({:pid => new_pid})
58
+ end
59
+
60
+ def state
61
+ self.attributes[:state]
62
+ end
63
+
64
+ def state=(new_state)
65
+ if ["I", "A", "D"].include? new_state
66
+ self.attributes[:state] = new_state
67
+ else
68
+ raise 'The object state of "' + new_state + '" is invalid. The allowed values for state are: A (active), D (deleted), and I (inactive).'
69
+ end
70
+ end
71
+
72
+ def label
73
+ self.attributes[:label]
74
+ end
75
+
76
+ def label=(new_label)
77
+ self.attributes[:label] = new_label
78
+ end
79
+
80
+ def content_model
81
+ self.attributes[:contentModel]
82
+ end
83
+
84
+ def content_model=(new_content_model)
85
+ self.attributes[:contentModel] = new_content_model
86
+ end
87
+
88
+ # Get the object and read its @ownerId from the profile
89
+ def owner_id
90
+ self.attributes[:ownerID]
91
+ end
92
+
93
+ def owner_id=(new_owner_id)
94
+ self.attributes.merge!({:ownerID => new_owner_id})
95
+ end
96
+
97
+ def profile
98
+ # Use xmlsimple to slurp the attributes
99
+ objectProfile = XmlSimple.xml_in(@fedora.call_resource(:retrieve, :objects_profile, {:pid => @pid}))
100
+ # TODO: Find out if xmlsimple automatically expands camelCased element names...
101
+ profile_as_array = {
102
+ :owner_id => objectProfile[objOwnerId],
103
+ :content_model => objectProfile[objContentModel],
104
+ :label => objectProfile[objLabel],
105
+ :date_created => objectProfile[objCreateDate],
106
+ :date_modified => objectProfile[objLastModDate]
107
+ }
108
+ end
109
+
110
+ def objectXML
111
+ # Use REXML to slurp the attributes (can't use xmlsimple because the XML is too complex. Need XPath-like queries.
112
+ @objectXML ||= REXML::Document.new(@fedora.call_resource(:retrieve, :objects_objectXml, {:pid => @pid}))
113
+ end
114
+
115
+ # See http://www.fedora.info/definitions/identifiers
116
+ def uri
117
+ "fedora:info/#{pid}"
118
+ end
119
+ end
@@ -0,0 +1,30 @@
1
+ module Fedora
2
+ module XmlFormat
3
+ extend self
4
+
5
+ def extension
6
+ "xml"
7
+ end
8
+
9
+ def mime_type
10
+ "text/xml"
11
+ end
12
+
13
+ def encode(hash)
14
+ hash.to_xml
15
+ end
16
+
17
+ def decode(xml)
18
+ from_xml_data(Hash.from_xml(xml))
19
+ end
20
+
21
+ private
22
+ def from_xml_data(data)
23
+ if data.is_a?(Hash) && data.keys.size == 1
24
+ data.values.first
25
+ else
26
+ data
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,199 @@
1
+ require 'facets/hash/symbolize_keys'
2
+
3
+ require 'active_resource'
4
+ require 'fedora/connection'
5
+ require 'fedora/formats'
6
+ require 'fedora/fedora_object'
7
+ require 'fedora/datastream'
8
+
9
+ module Fedora
10
+ NAMESPACE = "fedora:info/"
11
+ ALL_FIELDS = [
12
+ :pid, :label, :fType, :cModel, :state, :ownerId, :cDate, :dcmDate,
13
+ :bMech, :title, :creator, :subject, :description, :contributor,
14
+ :date, :type, :format, :identifier, :source, :language, :relation, :coverage, :rights
15
+ ]
16
+
17
+ class Repository
18
+ attr_accessor :fedora_url
19
+
20
+ def initialize(fedora_url = "http://localhost:8080/fedora")
21
+ @fedora_url = fedora_url.is_a?(URI) ? fedora_url : URI.parse(fedora_url)
22
+ @connection = nil
23
+ end
24
+
25
+ # Fetch the raw content of either a fedora object or datastream
26
+ def fetch_conent(object_uri)
27
+ connection.raw_get("#{url_for(object_uri)}?format=xml").body
28
+ end
29
+
30
+ # Find fedora objects with http://www.fedora.info/wiki/index.php/API-A-Lite_findObjects
31
+ #
32
+ # == Parameters
33
+ # query<String>:: the query string to be sent to Fedora.
34
+ # options<Hash>:: see below
35
+ #
36
+ # == Options<Hash> keys
37
+ # limit<String|Number>:: set the maxResults parameter in fedora
38
+ # select<Symbol|Array>:: the fields to returned. To include all fields, pass :all as the value.
39
+ # The field "pid" is always included.
40
+ #
41
+ # == Examples
42
+ # find_objects("label=Image1"
43
+ # find_objects("pid~demo:*", "label=test")
44
+ # find_objects("label=Image1", :include => :all)
45
+ # find_objects("label=Image1", :include => [:label])
46
+ #-
47
+ def find_objects(*args)
48
+ raise ArgumentError, "Missing query string" unless args.length >= 1
49
+ options = args.last.is_a?(Hash) ? args.pop : {}
50
+
51
+ fields = options[:select]
52
+ fields = (fields.nil? || (fields == :all)) ? ALL_FIELDS : ([:pid] + ([fields].flatten! - [:pid]))
53
+
54
+ query = args.join(' ')
55
+ params = { :format => 'xml', :query => query }
56
+ params[:maxResults] = options[:limit] if options[:limit]
57
+ params[:sessionToken] = options[:sessionToken] if options[:sessionToken]
58
+ includes = fields.inject("") { |s, f| s += "&#{f}=true"; s }
59
+
60
+ convert_xml(connection.get("#{fedora_url.path}/objects?#{to_query(params)}#{includes}"))
61
+ end
62
+
63
+ # Create the given object if it's new (not obtained from a find method). Otherwise update the object.
64
+ #
65
+ # == Return
66
+ # boolean:: whether the operation is successful
67
+ #-
68
+ def save(object)
69
+ object.new_object? ? create(object) : update(object)
70
+ end
71
+
72
+ def create(object)
73
+ case object
74
+ when Fedora::FedoraObject
75
+ pid = (object.pid ? object : 'new')
76
+ response = connection.post("#{url_for(pid)}?" + object.attributes.to_query, object.xml_content)
77
+ if response.code == '201'
78
+ object.pid = extract_pid(response)
79
+ object.new_object = false
80
+ true
81
+ else
82
+ false
83
+ end
84
+ when Fedora::Datastream
85
+ raise ArgumentError, "Missing dsID attribute" if object.dsid.nil?
86
+ response = connection.post("#{url_for(object)}?" + object.attributes.to_query,
87
+ object.xml_content)
88
+ if response.code == '201'
89
+ object.new_object = false
90
+ true
91
+ else
92
+ false
93
+ end
94
+ else
95
+ raise ArgumentError, "Unknown object type"
96
+ end
97
+
98
+ end
99
+
100
+ # Update the given object
101
+ # == Return
102
+ # boolean:: whether the operation is successful
103
+ #-
104
+ def update(object)
105
+ raise ArgumentError, "Missing pid attribute" if object.nil? || object.pid.nil?
106
+ case object
107
+ when Fedora::FedoraObject
108
+ response = connection.put("#{url_for(object)}?" + object.attributes.to_query)
109
+ response.code == '307'
110
+ when Fedora::Datastream
111
+ raise ArgumentError, "Missing dsID attribute" if object.dsid.nil?
112
+ response = connection.put("#{url_for(object)}?" + object.attributes.to_query, object.xml_content)
113
+ response.code == '201'
114
+ else
115
+ raise ArgumentError, "Unknown object type"
116
+ end
117
+ end
118
+
119
+ # Delete the given pid
120
+ # == Parameters
121
+ # object<Object|String>:: The object to delete.
122
+ # This can be a uri String ("demo:1", "fedora:info/demo:1") or any object that responds uri method.
123
+ #
124
+ # == Return
125
+ # boolean:: whether the operation is successful
126
+ #-
127
+ def delete(object)
128
+ raise ArgumentError, "Object must not be nil" if object.nil?
129
+ response = connection.delete("#{url_for(object)}")
130
+ response.code == '200'
131
+ end
132
+
133
+ # Fetch the given object using custom method. This is used to fetch other aspects of a fedora object,
134
+ # such as profile, versions, etc...
135
+ # == Parameters
136
+ # object<String|Object>:: a fedora uri, pid, FedoraObject instance
137
+ # method<Symbol>:: the method to fetch such as :export, :history, :versions, etc
138
+ # extra_params<Hash>:: any other extra parameters to pass to fedora
139
+ #
140
+ # == Returns
141
+ # This method returns raw xml response from the server
142
+ #-
143
+ def fetch_custom(object, method, extra_params = { :format => 'xml' })
144
+ path = case method
145
+ when :profile then ""
146
+ else "/#{method}"
147
+ end
148
+
149
+ extra_params.delete(:format) if method == :export
150
+ connection.raw_get("#{url_for(object)}#{path}?#{to_query(extra_params)}").body
151
+ end
152
+
153
+ private
154
+ def convert_xml(response)
155
+ results = FedoraObjects.new
156
+ return results unless response && response['resultList']
157
+
158
+ results.session_token = response['listSession']['token'] if response['listSession']
159
+ objectFields = response['resultList']['objectFields']
160
+ case objectFields
161
+ when Array
162
+ objectFields.each { |attrs| results << FedoraObject.new(attrs.symbolize_keys!) }
163
+ when Hash
164
+ results << FedoraObject.new(objectFields.symbolize_keys!)
165
+ end
166
+ results
167
+ end
168
+
169
+ def url_for(object)
170
+ uri = object.respond_to?(:uri) ? object.uri : object.to_s
171
+ uri = (uri[0..NAMESPACE.length-1] == NAMESPACE ? uri[NAMESPACE.length..-1] : uri) # strip of fedora:info namespace
172
+ "#{fedora_url.path}/objects/#{uri}"
173
+ end
174
+
175
+ # Low level access to the remote fedora server
176
+ # The +refresh+ parameter toggles whether or not the connection is refreshed at every request
177
+ # or not (defaults to +false+).
178
+ def connection(refresh = false)
179
+ if refresh || @connection.nil?
180
+ @connection = ActiveResource::Connection.new(@fedora_url, Fedora::XmlFormat)
181
+ end
182
+ @connection
183
+ end
184
+
185
+ def extract_pid(response)
186
+ CGI.unescape(response['Location'].split('/').last)
187
+ end
188
+
189
+ # {:q => 'test', :num => 5}.to_query # => 'q=test&num=5'
190
+ def to_query(hash)
191
+ hash.collect { |key, value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" }.sort * '&'
192
+ end
193
+ end
194
+ end
195
+
196
+ class FedoraObjects < Array
197
+ attr_accessor :session_token
198
+ end
199
+