active-fedora 2.3.8 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.rvmrc +1 -1
  2. data/Gemfile.lock +16 -10
  3. data/History.txt +4 -5
  4. data/README.textile +1 -1
  5. data/active-fedora.gemspec +2 -2
  6. data/lib/active_fedora.rb +36 -19
  7. data/lib/active_fedora/associations.rb +157 -0
  8. data/lib/active_fedora/associations/association_collection.rb +180 -0
  9. data/lib/active_fedora/associations/association_proxy.rb +177 -0
  10. data/lib/active_fedora/associations/belongs_to_association.rb +36 -0
  11. data/lib/active_fedora/associations/has_many_association.rb +52 -0
  12. data/lib/active_fedora/attribute_methods.rb +8 -0
  13. data/lib/active_fedora/base.rb +76 -80
  14. data/lib/active_fedora/datastream.rb +0 -1
  15. data/lib/active_fedora/delegating.rb +53 -0
  16. data/lib/active_fedora/model.rb +4 -2
  17. data/lib/active_fedora/nested_attributes.rb +153 -0
  18. data/lib/active_fedora/nokogiri_datastream.rb +17 -18
  19. data/lib/active_fedora/reflection.rb +140 -0
  20. data/lib/active_fedora/relationships_helper.rb +10 -5
  21. data/lib/active_fedora/semantic_node.rb +146 -57
  22. data/lib/active_fedora/solr_service.rb +0 -7
  23. data/lib/active_fedora/version.rb +1 -1
  24. data/lib/fedora/connection.rb +75 -111
  25. data/lib/fedora/repository.rb +14 -28
  26. data/lib/ruby-fedora.rb +1 -1
  27. data/spec/integration/associations_spec.rb +139 -0
  28. data/spec/integration/nested_attribute_spec.rb +40 -0
  29. data/spec/integration/repository_spec.rb +9 -14
  30. data/spec/integration/semantic_node_spec.rb +2 -0
  31. data/spec/spec_helper.rb +1 -3
  32. data/spec/unit/active_fedora_spec.rb +2 -1
  33. data/spec/unit/association_proxy_spec.rb +13 -0
  34. data/spec/unit/base_active_model_spec.rb +61 -0
  35. data/spec/unit/base_delegate_spec.rb +59 -0
  36. data/spec/unit/base_spec.rb +45 -58
  37. data/spec/unit/connection_spec.rb +21 -21
  38. data/spec/unit/datastream_spec.rb +0 -11
  39. data/spec/unit/has_many_collection_spec.rb +27 -0
  40. data/spec/unit/model_spec.rb +1 -1
  41. data/spec/unit/nokogiri_datastream_spec.rb +3 -29
  42. data/spec/unit/repository_spec.rb +2 -2
  43. data/spec/unit/semantic_node_spec.rb +2 -0
  44. data/spec/unit/solr_service_spec.rb +0 -7
  45. metadata +36 -15
  46. data/lib/active_fedora/active_fedora_configuration_exception.rb +0 -2
  47. data/lib/util/class_level_inheritable_attributes.rb +0 -23
@@ -65,13 +65,6 @@ module ActiveFedora
65
65
  return uri.gsub(/(:)/, '\\:')
66
66
  end
67
67
 
68
- # Escapes these characters
69
- # + - || ! ( ) { } [ ] ^ " ~ * ? : \
70
- # See: http://lucene.apache.org/java/2_4_0/queryparsersyntax.html#Escaping%20Special%20Characters
71
- def self.escape_characters_for_query(value)
72
- value.gsub(/([\#\+\-\|\{\}\^\"\~\*\:\>\<\?\(\)\[\]\+\!\\])/) {|v| "\\#{v}"}
73
- end
74
-
75
68
 
76
69
  end #SolrService
77
70
  class SolrNotInitialized < StandardError;end
@@ -1,3 +1,3 @@
1
1
  module ActiveFedora
2
- VERSION = "2.3.8"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -1,7 +1,6 @@
1
1
  require "base64"
2
2
  gem 'multipart-post'
3
3
  require 'net/http/post/multipart'
4
- require 'net/http/persistent'
5
4
  require 'cgi'
6
5
  require "mime/types"
7
6
  require 'net/http'
@@ -47,9 +46,6 @@ module Fedora
47
46
  # 409 Conflict
48
47
  class ResourceConflict < ClientError; end # :nodoc:
49
48
 
50
- # 415 Unsupported Media Type
51
- class UnsupportedMediaType < ClientError; end # :nodoc:
52
-
53
49
  # 5xx Server Error
54
50
  class ServerError < ConnectionError; end # :nodoc:
55
51
 
@@ -64,23 +60,6 @@ module Fedora
64
60
  # This class is used by ActiveResource::Base to interface with REST
65
61
  # services.
66
62
  class Connection
67
-
68
- CLASSES = [
69
- Net::HTTP::Delete,
70
- Net::HTTP::Get,
71
- Net::HTTP::Head,
72
- Net::HTTP::Post,
73
- Net::HTTP::Put
74
- ].freeze
75
-
76
- MIME_TYPES = {
77
- :binary => "application/octet-stream",
78
- :json => "application/json",
79
- :xml => "text/xml",
80
- :none => "text/plain"
81
- }.freeze
82
-
83
-
84
63
  attr_reader :site, :surrogate
85
64
  attr_accessor :format
86
65
 
@@ -99,102 +78,84 @@ module Fedora
99
78
  @surrogate=surrogate
100
79
  end
101
80
 
102
- ##
103
- # Perform an HTTP Delete, Head, Get, Post, or Put.
104
-
105
- CLASSES.each do |clazz|
106
- verb = clazz.to_s.split("::").last.downcase
107
-
108
- define_method verb do |*args|
109
- path = args[0]
110
- params = args[1] || {}
111
-
112
- response_for clazz, path, params
113
- end
114
- end
115
-
116
81
  # Set URI for remote service.
117
82
  def site=(site)
118
83
  @site = site.is_a?(URI) ? site : URI.parse(site)
119
84
  end
120
85
 
121
-
122
- private
123
-
124
- # Makes request to remote service.
125
- def response_for(clazz, path, params)
126
- logger.debug "#{clazz} #{path}"
127
- request = clazz.new path
128
- request.body = params[:body]
129
-
130
- handle_request request, params[:upload], params[:type], params[:headers] || {}
86
+ # Execute a GET request.
87
+ # Used to get (find) resources.
88
+ def get(path, headers = {})
89
+ format.decode(request(:get, path, build_request_headers(headers)).body)
131
90
  end
132
91
 
133
-
134
- def handle_request request, upload, type, headers
135
- handle_uploads request, upload, type
136
- handle_headers request, upload, type, headers
137
- result = http.request self.site, request
138
- handle_response(result)
92
+ # Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
93
+ # Used to delete resources.
94
+ def delete(path, headers = {})
95
+ request(:delete, path, build_request_headers(headers))
139
96
  end
140
97
 
141
- ##
142
- # Handle chunked uploads.
143
- #
144
- # +request+: A Net::HTTP request Object.
145
- # +upload+: A Hash with the following keys:
146
- # +:file+: The file to be HTTP chunked uploaded.
147
- # +:headers+: A Hash containing additional HTTP headers.
148
- # +:type+: A Symbol with the mime_type.
149
-
150
- def handle_uploads request, upload, type
151
- return unless upload
152
- io = nil
153
- if upload[:file].is_a?(File)
154
- io = File.open upload[:file].path
155
- else
156
- io = upload[:file]
157
- end
158
-
159
- request.body_stream = io
160
- end
161
98
 
162
99
 
163
- def handle_headers request, upload, type, headers
164
- request.basic_auth(self.site.user, self.site.password) if self.site.user
100
+ def raw_get(path, headers = {})
101
+ request(:get, path, build_request_headers(headers))
102
+ end
103
+ def post(path, body='', headers={})
104
+ do_method(:post, path, body, headers)
105
+ end
106
+ def put( path, body='', headers={})
107
+ do_method(:put, path, body, headers)
108
+ end
165
109
 
166
- request.add_field "Accept", mime_type(type)
167
- request.add_field "Content-Type", mime_type(type) if requires_content_type? request
110
+ private
111
+ def do_method(method, path, body = '', headers = {})
112
+ meth_map={:put=>Net::HTTP::Put::Multipart, :post=>Net::HTTP::Post::Multipart}
113
+ raise "undefined method: #{method}" unless meth_map.has_key? method
114
+ headers = build_request_headers(headers)
115
+ if body.respond_to?(:read)
116
+ if body.respond_to?(:original_filename?)
117
+ filename = File.basename(body.original_filename)
118
+ io = UploadIO.new(body, mime_type,filename)
119
+ elsif body.path
120
+ filename = File.basename(body.path)
121
+ else
122
+ filename="NOFILE"
123
+ end
124
+ mime_types = MIME::Types.of(filename)
125
+ mime_type ||= mime_types.empty? ? "application/octet-stream" : mime_types.first.content_type
126
+
127
+ io = nil
128
+ if body.is_a?(File)
129
+ io = UploadIO.new(body.path,mime_type)
130
+ else
131
+ io =UploadIO.new(body, mime_type, filename)
132
+ end
168
133
 
169
- headers.merge! chunked_headers upload
170
- headers.each do |header, value|
171
- request[header] = value
134
+ req = meth_map[method].new(path, {:file=>io}, headers)
135
+ multipart_request(req)
136
+ else
137
+ request(method, path, body.to_s, headers)
172
138
  end
173
139
  end
140
+ def multipart_request(req)
141
+ result = nil
142
+ result = http.start do |conn|
143
+ conn.read_timeout=60600 #these can take a while
144
+ conn.request(req)
145
+ end
146
+ handle_response(result)
174
147
 
175
- ##
176
- # Setting of chunked upload headers.
177
- #
178
- # +upload+: A Hash with the following keys:
179
- # +:file+: The file to be HTTP chunked uploaded.
180
-
181
- def chunked_headers upload
182
- return {} unless upload
183
-
184
- chunked_headers = {
185
- "Content-Type" => mime_type(:binary),
186
- "Transfer-Encoding" => "chunked"
187
- }.merge upload[:headers] || {}
188
148
  end
189
149
 
190
- def requires_content_type? request
191
- [Net::HTTP::Post, Net::HTTP::Put].include? request.class
150
+ # Makes request to remote service.
151
+ def request(method, path, *arguments)
152
+ result = http.send(method, path, *arguments)
153
+ handle_response(result)
192
154
  end
193
155
 
194
156
  # Handles response and error codes from remote service.
195
157
  def handle_response(response)
196
158
  message = "Error from Fedora: #{response.body}"
197
- logger.debug "Response: #{response.code}"
198
159
  case response.code.to_i
199
160
  when 301,302
200
161
  raise(Redirection.new(response))
@@ -212,8 +173,6 @@ module Fedora
212
173
  raise(MethodNotAllowed.new(response, message))
213
174
  when 409
214
175
  raise(ResourceConflict.new(response, message))
215
- when 415
216
- raise UnsupportedMediaType.new(response, message)
217
176
  when 422
218
177
  raise(ResourceInvalid.new(response, message))
219
178
  when 423...500
@@ -225,30 +184,35 @@ module Fedora
225
184
  end
226
185
  end
227
186
 
228
- def mime_type type
229
- if type.kind_of? String
230
- type
231
- else
232
- MIME_TYPES[type] || MIME_TYPES[:xml]
233
- end
234
- end
235
-
236
187
  # Creates new Net::HTTP instance for communication with
237
188
  # remote service and resources.
238
189
  def http
239
- return @http if @http
240
- @http = Net::HTTP::Persistent.new#(@site)
190
+ http = Net::HTTP.new(@site.host, @site.port)
241
191
  if(@site.is_a?(URI::HTTPS))
242
- @http.use_ssl = true
243
- @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
192
+ http.use_ssl = true
193
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
244
194
 
245
195
  if (defined?(SSL_CLIENT_CERT_FILE) && !SSL_CLIENT_CERT_FILE.nil? && defined?(SSL_CLIENT_KEY_FILE) && !SSL_CLIENT_KEY_FILE.nil? && defined?(SSL_CLIENT_KEY_PASS) && !SSL_CLIENT_KEY_PASS.nil?)
246
- @http.cert = OpenSSL::X509::Certificate.new( File.read(SSL_CLIENT_CERT_FILE) )
247
- @http.key = OpenSSL::PKey::RSA.new( File.read(SSL_CLIENT_KEY_FILE), SSL_CLIENT_KEY_PASS )
196
+ http.cert = OpenSSL::X509::Certificate.new( File.read(SSL_CLIENT_CERT_FILE) )
197
+ http.key = OpenSSL::PKey::RSA.new( File.read(SSL_CLIENT_KEY_FILE), SSL_CLIENT_KEY_PASS )
248
198
  end
249
199
  end
250
- @http
200
+ http
251
201
  end
252
202
 
203
+ def default_header
204
+ @default_header ||= { 'Content-Type' => format.mime_type }
205
+ end
206
+
207
+ # Builds headers for request to remote service.
208
+ def build_request_headers(headers)
209
+ headers.merge!({"From"=>surrogate}) if @surrogate
210
+ authorization_header.update(default_header).update(headers)
211
+ end
212
+
213
+ # Sets authorization header; authentication information is pulled from credentials provided with site URI.
214
+ def authorization_header
215
+ (@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
216
+ end
253
217
  end
254
218
  end
@@ -59,7 +59,7 @@ module Fedora
59
59
 
60
60
  # Fetch the raw content of either a fedora object or datastream
61
61
  def fetch_content(object_uri)
62
- response = connection.get("#{url_for(object_uri)}?format=xml")
62
+ response = connection.raw_get("#{url_for(object_uri)}?format=xml")
63
63
  StringResponse.new(response.body, response.content_type)
64
64
  end
65
65
 
@@ -95,7 +95,7 @@ module Fedora
95
95
  params[:sessionToken] = options[:sessionToken] if options[:sessionToken]
96
96
  includes = fields.inject("") { |s, f| s += "&#{f}=true"; s }
97
97
 
98
- convert_xml(XmlFormat.decode(connection.get("#{fedora_url.path}/objects?#{params.to_fedora_query}#{includes}").body))
98
+ convert_xml(connection.get("#{fedora_url.path}/objects?#{params.to_fedora_query}#{includes}"))
99
99
  end
100
100
 
101
101
  # Retrieve an object from fedora and load it as an instance of the given model/class
@@ -103,14 +103,10 @@ module Fedora
103
103
  # @param pid of the Fedora object to retrieve and deserialize
104
104
  # @param klazz the Model whose deserialize method the object's FOXML will be passed into
105
105
  def find_model(pid, klazz)
106
- obj = nil
107
- ms = 1000 * Benchmark.realtime do
108
- obj = self.find_objects("pid=#{pid}").first
109
- if obj.nil?
110
- raise ActiveFedora::ObjectNotFoundError, "The repository does not have an object with pid #{pid}. The repository URL is #{self.base_url}"
111
- end
106
+ obj = self.find_objects("pid=#{pid}").first
107
+ if obj.nil?
108
+ raise ActiveFedora::ObjectNotFoundError, "The repository does not have an object with pid #{pid}. The repository URL is #{self.base_url}"
112
109
  end
113
- logger.debug "loading #{pid} took #{ms} ms"
114
110
  doc = Nokogiri::XML::Document.parse(obj.object_xml)
115
111
  klazz.deserialize(doc)
116
112
  end
@@ -136,7 +132,7 @@ module Fedora
136
132
  case object
137
133
  when Fedora::FedoraObject
138
134
  pid = (object.pid ? object : 'new')
139
- response = connection.post("#{url_for(pid)}?" + object.attributes.to_fedora_query, :body=>object.blob)
135
+ response = connection.post("#{url_for(pid)}?" + object.attributes.to_fedora_query, object.blob)
140
136
  if response.code == '201'
141
137
  object.pid = extract_pid(response)
142
138
  object.new_object = false
@@ -146,14 +142,10 @@ module Fedora
146
142
  end
147
143
  when Fedora::Datastream
148
144
  raise ArgumentError, "Missing dsID attribute" if object.dsid.nil?
149
- headers = {}
150
- headers[:type] = object.attributes[:mimeType] if object.attributes[:mimeType]
151
- if object.blob.respond_to? :read
152
- headers[:upload] = {:file=>object.blob}
153
- else
154
- headers[:body]=object.blob
155
- end
156
- response = connection.post("#{url_for(object)}?" + object.attributes.to_fedora_query, headers)
145
+ extra_headers = {}
146
+ extra_headers['Content-Type'] = object.attributes[:mimeType] if object.attributes[:mimeType]
147
+ response = connection.post("#{url_for(object)}?" + object.attributes.to_fedora_query,
148
+ object.blob, extra_headers)
157
149
  if response.code == '201'
158
150
  object.new_object = false
159
151
  true
@@ -178,13 +170,7 @@ module Fedora
178
170
  response.code == '200' || '307'
179
171
  when Fedora::Datastream
180
172
  raise ArgumentError, "Missing dsID attribute" if object.dsid.nil?
181
- headers = {}
182
- if object.blob.respond_to? :read
183
- headers[:upload] = {:file=>object.blob}
184
- else
185
- headers[:body]=object.blob
186
- end
187
- response = connection.put("#{url_for(object)}?" + object.attributes.to_fedora_query, headers)
173
+ response = connection.put("#{url_for(object)}?" + object.attributes.to_fedora_query, object.blob)
188
174
  response.code == '200' || '201'
189
175
  return response.code
190
176
  else
@@ -239,7 +225,7 @@ module Fedora
239
225
  content_to_ingest = content_to_ingest.read
240
226
  end
241
227
 
242
- connection.post(url, :body=>content_to_ingest)
228
+ connection.post(url,content_to_ingest)
243
229
  end
244
230
 
245
231
  # Fetch the given object using custom method. This is used to fetch other aspects of a fedora object,
@@ -259,11 +245,11 @@ module Fedora
259
245
  end
260
246
 
261
247
  extra_params.delete(:format) if method == :export
262
- connection.get("#{url_for(object)}#{path}?#{extra_params.to_fedora_query}").body
248
+ connection.raw_get("#{url_for(object)}#{path}?#{extra_params.to_fedora_query}").body
263
249
  end
264
250
 
265
251
  def describe_repository
266
- result_body = connection.get("#{fedora_url.path}/describe?xml=true").body
252
+ result_body = connection.raw_get("#{fedora_url.path}/describe?xml=true").body
267
253
  XmlSimple.xml_in(result_body)
268
254
  end
269
255
 
@@ -17,4 +17,4 @@ require 'fedora/fedora_object'
17
17
  require 'fedora/formats'
18
18
  require 'fedora/generic_search'
19
19
  require 'fedora/repository'
20
- require 'util/class_level_inheritable_attributes'
20
+ #require 'util/class_level_inheritable_attributes'
@@ -0,0 +1,139 @@
1
+ require 'spec_helper'
2
+
3
+ class Library < ActiveFedora::Base
4
+ has_many :books, :property=>:has_constituent
5
+ end
6
+
7
+ class Book < ActiveFedora::Base
8
+ belongs_to :library, :property=>:has_constituent
9
+ end
10
+
11
+ describe ActiveFedora::Base do
12
+ describe "an unsaved instance" do
13
+ before do
14
+ @library = Library.new()
15
+ @book = Book.new
16
+ @book.save
17
+ @book2 = Book.new
18
+ @book2.save
19
+ end
20
+
21
+ it "should let you shift onto the association" do
22
+ @library.new_record?.should be_true
23
+ @library.books.size == 0
24
+ @library.books.to_ary.should == []
25
+ @library.book_ids.should ==[]
26
+ @library.books << @book
27
+ @library.books.map(&:pid).should == [@book.pid]
28
+ @library.book_ids.should ==[@book.pid]
29
+ end
30
+
31
+ it "should let you set an array of objects" do
32
+ @library.books = [@book, @book2]
33
+ @library.books.map(&:pid).should == [@book.pid, @book2.pid]
34
+ @library.save
35
+
36
+ @library.books = [@book]
37
+ @library.books.map(&:pid).should == [@book.pid]
38
+
39
+ end
40
+ it "should let you set an array of object ids" do
41
+ @library.book_ids = [@book.pid, @book2.pid]
42
+ @library.books.map(&:pid).should == [@book.pid, @book2.pid]
43
+ end
44
+
45
+ it "setter should wipe out previously saved relations" do
46
+ @library.book_ids = [@book.pid, @book2.pid]
47
+ @library.book_ids = [@book2.pid]
48
+ @library.books.map(&:pid).should == [@book2.pid]
49
+
50
+ end
51
+
52
+ after do
53
+ @book.delete
54
+ @book2.delete
55
+ end
56
+ end
57
+
58
+
59
+ describe "a saved instance" do
60
+ before do
61
+ @library = Library.new()
62
+ @library.save()
63
+ @book = Book.new
64
+ @book.save
65
+ end
66
+ it "should have many books once it has been saved" do
67
+ @library.save
68
+ @library.books << @book
69
+
70
+ @book.library.pid.should == @library.pid
71
+ @library.books.reload
72
+ @library.books.map(&:pid).should == [@book.pid]
73
+
74
+
75
+ @library2 = Library.find(@library.pid)
76
+ @library2.books.map(&:pid).should == [@book.pid]
77
+
78
+
79
+ end
80
+ after do
81
+ @library.delete
82
+ @book.delete
83
+ end
84
+ end
85
+
86
+ describe "setting belongs_to" do
87
+ before do
88
+ @library = Library.new()
89
+ @library.save()
90
+ @book = Book.new
91
+ end
92
+ it "should set the association" do
93
+ @book.library = @library
94
+ @book.library.pid.should == @library.pid
95
+ @book.save
96
+
97
+
98
+ Book.find(@book.pid).library.pid.should == @library.pid
99
+
100
+ end
101
+ it "should clear the association" do
102
+ @book.library = @library
103
+ @book.library = nil
104
+ @book.save
105
+
106
+ Book.find(@book.pid).library.should be_nil
107
+
108
+ end
109
+
110
+ it "should replace the association" do
111
+ @library2 = Library.new
112
+ @library2.save
113
+ @book.library = @library
114
+ @book.save
115
+ @book.library = @library2
116
+ @book.save
117
+ Book.find(@book.pid).library.pid.should == @library2.pid
118
+
119
+ end
120
+
121
+ it "should be able to be set by id" do
122
+ @book.library_id = @library.pid
123
+ @book.library_id.should == @library.pid
124
+ @book.library.pid.should == @library.pid
125
+ @book.save
126
+ Book.find(@book.pid).library_id.should == @library.pid
127
+ end
128
+
129
+ after do
130
+ @library.delete
131
+ @book.delete
132
+ @library2.delete if @library2
133
+ end
134
+ end
135
+
136
+
137
+
138
+
139
+ end