active-fedora 2.3.8 → 3.0.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.
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