ruby-fedora 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+