t2-server 0.9.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2010-2012 The University of Manchester, UK.
1
+ # Copyright (c) 2010-2013 The University of Manchester, UK.
2
2
  #
3
3
  # All rights reserved.
4
4
  #
@@ -50,27 +50,8 @@ module T2Server
50
50
  end
51
51
 
52
52
  # SSL options
53
- opt.on("-E CERT_FILE:PASSWORD", "--cert=CERT_FILE:PASSWORD", "Use " +
54
- "the specified certificate file for client authentication. If the " +
55
- "optional password is not provided it will be asked for on the " +
56
- "command line. Must be in PEM format.") do |val|
57
- cert, cpass = val.chomp.split(":", 2)
58
- conn_params[:client_certificate] = cert
59
- conn_params[:client_password] = cpass if cpass
60
- end
61
- opt.on("--cacert=CERT_FILE", "Use the specified certificate file to " +
62
- "verify the peer. Must be in PEM format.") do |val|
63
- conn_params[:ca_file] = val.chomp
64
- end
65
- opt.on("--capath=CERTS_PATH", "Use the specified certificate " +
66
- "directory to verify the peer. Certificates must be in PEM " +
67
- "format") do |val|
68
- conn_params[:ca_path] = val.chomp
69
- end
70
- opt.on("-k", "--insecure", "Allow insecure connections: no peer " +
71
- "verification.") do
72
- conn_params[:verify_peer] = false
73
- end
53
+ ssl_auth_opts(opt, conn_params)
54
+ ssl_transport_opts(opt, conn_params)
74
55
 
75
56
  # common options
76
57
  opt.on_tail("-u", "--username=USERNAME", "The username to use for " +
@@ -100,7 +81,7 @@ module T2Server
100
81
 
101
82
  # separate the creds if they are supplied in the uri
102
83
  def parse_address(address, creds)
103
- if address == nil or address == ""
84
+ if address == nil || address == ""
104
85
  puts @opts
105
86
  exit 1
106
87
  end
@@ -112,5 +93,49 @@ module T2Server
112
93
  def opts
113
94
  @opts
114
95
  end
96
+
97
+ private
98
+
99
+ # The SSL authentication and peer verification options.
100
+ def ssl_auth_opts(opt, conn_params)
101
+ opt.on("-E CERT_FILE:PASSWORD", "--cert=CERT_FILE:PASSWORD", "Use " +
102
+ "the specified certificate file for client authentication. If the " +
103
+ "optional password is not provided it will be asked for on the " +
104
+ "command line. Must be in PEM format.") do |val|
105
+ cert, cpass = val.chomp.split(":", 2)
106
+ conn_params[:client_certificate] = cert
107
+ conn_params[:client_password] = cpass if cpass
108
+ end
109
+ opt.on("--cacert=CERT_FILE", "Use the specified certificate file to " +
110
+ "verify the peer. Must be in PEM format.") do |val|
111
+ conn_params[:ca_file] = val.chomp
112
+ end
113
+ opt.on("--capath=CERTS_PATH", "Use the specified certificate " +
114
+ "directory to verify the peer. Certificates must be in PEM " +
115
+ "format") do |val|
116
+ conn_params[:ca_path] = val.chomp
117
+ end
118
+ opt.on("-k", "--insecure", "Allow insecure connections: no peer " +
119
+ "verification.") do
120
+ conn_params[:verify_peer] = false
121
+ end
122
+ end
123
+
124
+ # The SSL transport options.
125
+ def ssl_transport_opts(opt, conn_params)
126
+ opt.on("-1", "--tlsv1", "Use TLS version 1 when negotiating with " +
127
+ "the remote Taverna Server server.") do
128
+ conn_params[:ssl_version] = :TLSv1
129
+ end
130
+ opt.on("-2", "--sslv2", "Use SSL version 2 when negotiating with " +
131
+ "the remote Taverna Server server.") do
132
+ conn_params[:ssl_version] = :SSLv23
133
+ end
134
+ opt.on("-3", "--sslv3", "Use SSL version 3 when negotiating with " +
135
+ "the remote Taverna Server server.") do
136
+ conn_params[:ssl_version] = :SSLv3
137
+ end
138
+ end
139
+
115
140
  end
116
141
  end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2010-2012 The University of Manchester, UK.
1
+ # Copyright (c) 2010-2013 The University of Manchester, UK.
2
2
  #
3
3
  # All rights reserved.
4
4
  #
@@ -38,6 +38,7 @@ require 't2-server/net/credentials'
38
38
  require 't2-server/net/connection'
39
39
  require 't2-server/net/parameters'
40
40
  require 't2-server/port'
41
+ require 't2-server/interaction'
41
42
  require 't2-server/server'
42
43
  require 't2-server/run'
43
44
  require 't2-server/admin'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2010-2012 The University of Manchester, UK.
1
+ # Copyright (c) 2010-2013 The University of Manchester, UK.
2
2
  #
3
3
  # All rights reserved.
4
4
  #
@@ -30,7 +30,11 @@
30
30
  #
31
31
  # Author: Robert Haines
32
32
 
33
+ # :stopdoc:
34
+ # This comment is needed to stop the above licence from being included in the
35
+ # documentation multiple times. Sigh.
33
36
  module T2Server
37
+ # :startdoc:
34
38
 
35
39
  # This call provides access to the administrative interface of a Taverna
36
40
  # Server instance.
@@ -56,7 +60,6 @@ module T2Server
56
60
  admin_description = xml_document(@server.read(@uri, "application/xml",
57
61
  @credentials))
58
62
  @resources = get_resources(admin_description)
59
- #@resources.each {|key, value| puts "#{key}: #{value}"}
60
63
 
61
64
  yield(self) if block_given?
62
65
  end
@@ -117,7 +120,7 @@ module T2Server
117
120
  # :startdoc:
118
121
 
119
122
  # :call-seq:
120
- # value -> String
123
+ # value -> string
121
124
  # value=
122
125
  #
123
126
  # Get or set the value held by this resource. This call always queries
@@ -129,7 +132,7 @@ module T2Server
129
132
  end
130
133
 
131
134
  # :call-seq:
132
- # writable? -> bool
135
+ # writable? -> true or false
133
136
  #
134
137
  # Is this resource writable?
135
138
  def writable?
@@ -83,12 +83,31 @@ module T2Server
83
83
  # not necessarily indicate a problem with the server.
84
84
  class UnexpectedServerResponse < T2ServerError
85
85
 
86
- # Create a new UnexpectedServerResponse with the specified unexpected
86
+ # The method that was called to produce this error.
87
+ attr_reader :method
88
+
89
+ # The path of the URI that returned this error.
90
+ attr_reader :path
91
+
92
+ # The HTTP error code of this error.
93
+ attr_reader :code
94
+
95
+ # The response body of this error. If the server did not supply one then
96
+ # this will be "<none>".
97
+ attr_reader :body
98
+
99
+ # Create a new UnexpectedServerResponse with details of which HTTP method
100
+ # was called, the path that it was called on and the specified unexpected
87
101
  # response. The response to be passed in is that which was returned by a
88
102
  # call to Net::HTTP#request.
89
- def initialize(response)
90
- body = response.body ? "\n#{response.body}" : ""
91
- super "Unexpected server response: #{response.code}\n#{body}"
103
+ def initialize(method, path, response)
104
+ @method = method
105
+ @path = path
106
+ @code = response.code
107
+ @body = response.body.empty? ? "<none>" : "#{response.body}"
108
+ message = "Unexpected server response:\n Method: #{@method}\n Path: "\
109
+ "#{@path}\n Code: #{@code}\n Body: #{@body}"
110
+ super message
92
111
  end
93
112
  end
94
113
 
@@ -0,0 +1,241 @@
1
+ # Copyright (c) 2010-2013 The University of Manchester, UK.
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # * Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # * Neither the names of The University of Manchester nor the names of its
16
+ # contributors may be used to endorse or promote products derived from this
17
+ # software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ # POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+ # Author: Robert Haines
32
+
33
+ require 'rubygems'
34
+ require 'atom'
35
+ require 'uri'
36
+
37
+ module T2Server
38
+
39
+ # The Interaction module provides access to Taverna workflow notifications
40
+ # as supplied by the Interaction Service. These are read from an Atom feed
41
+ # and returned as Notification objects. For more information about the
42
+ # Interaction Service, see
43
+ # http://dev.mygrid.org.uk/wiki/display/taverna/Interaction+service
44
+ module Interaction
45
+
46
+ # :stopdoc:
47
+ FEED_NS = "http://ns.taverna.org.uk/2012/interaction"
48
+
49
+ class Feed
50
+ def initialize(run)
51
+ @run = run
52
+ @cache = {:requests => {}, :replies => {}}
53
+ end
54
+
55
+ # Get all new notification requests since they were last checked.
56
+ #
57
+ # Here we really only want new unanswered notifications, but polling
58
+ # returns all requests new to *us*, even those that have been replied to
59
+ # elsewhere. Filter out answered requests here.
60
+ def new_requests
61
+ poll(:requests).select { |i| !i.has_reply? }
62
+ end
63
+
64
+ # Get all notifications, or all of a particular type.
65
+ def notifications(type = :all)
66
+ poll
67
+
68
+ case type
69
+ when :requests, :replies
70
+ @cache[type].values
71
+ else
72
+ @cache[:requests].values + @cache[:replies].values
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def entries(&block)
79
+ feed = Atom::Feed.load_feed(@run.read_notification_feed)
80
+ feed.each_entry(:paginate => true, &block)
81
+ end
82
+
83
+ # Poll for all notification types and update the caches.
84
+ #
85
+ # Returns any new notifications, [] otherwise. If you are only
86
+ # interested in knowing about new notifications of a specific type you
87
+ # can use the type parameter to specify this. Use :requests, :replies or
88
+ # :all (default).
89
+ def poll(type = :all)
90
+ updates = []
91
+ requests = @cache[:requests]
92
+ replies = @cache[:replies]
93
+
94
+ entries do |entry|
95
+ # It's worth noting what happens here.
96
+ #
97
+ # This connection to a run's notification feed may not be the only
98
+ # one, or it might be a reconnection. As a result we might see a
99
+ # reply before we see the original request; atom feeds are last in
100
+ # first out.
101
+ #
102
+ # So if we see a reply, we should check to see if we have the
103
+ # request before setting the request to "replied". And if we see a
104
+ # request we should check to see if we have a reply for it already;
105
+ # we may have seen the reply the previous time through the loop.
106
+ note = Notification.new(entry, @run)
107
+ if note.is_reply?
108
+ next if replies.has_key? note.reply_to
109
+ requests[note.reply_to].has_reply unless requests[note.reply_to].nil?
110
+ replies[note.reply_to] = note
111
+ updates << note if type == :replies || type == :all
112
+ else
113
+ next if requests.has_key? note.id
114
+ note.has_reply unless replies[note.id].nil?
115
+ requests[note.id] = note
116
+ updates << note if type == :requests || type == :all
117
+ end
118
+ end
119
+
120
+ updates
121
+ end
122
+
123
+ end
124
+ # :startdoc:
125
+
126
+ # This class represents a Taverna notification.
127
+ class Notification
128
+
129
+ # The identifier of this notification.
130
+ attr_reader :id
131
+
132
+ # If this notification is a reply then this is the identifier of the
133
+ # notification that it is a reply to.
134
+ attr_reader :reply_to
135
+
136
+ # The URI of the notification page to show.
137
+ attr_reader :uri
138
+
139
+ # The serial number of a notification. This identifies a notification
140
+ # within a workflow.
141
+ attr_reader :serial
142
+
143
+ # :stopdoc:
144
+ def initialize(entry, run)
145
+ @run = run
146
+ reply_to = entry[FEED_NS, "in-reply-to"]
147
+ if reply_to.empty?
148
+ @is_reply = false
149
+ @has_reply = false
150
+ @id = entry[FEED_NS, "id"][0]
151
+ @is_notification = entry[FEED_NS, "progress"].empty? ? false : true
152
+ @uri = get_link(entry.links)
153
+ @serial = "#{entry[FEED_NS, 'path'][0]}-#{entry[FEED_NS, 'count'][0]}"
154
+ else
155
+ @is_reply = true
156
+ @is_notification = false
157
+ @reply_to = reply_to[0]
158
+ end
159
+ end
160
+ # :startdoc:
161
+
162
+ # :call-seq:
163
+ # is_reply? -> true or false
164
+ #
165
+ # Is this notification a reply to another notification?
166
+ def is_reply?
167
+ @is_reply
168
+ end
169
+
170
+ # :call-seq:
171
+ # is_notification? -> true or false
172
+ #
173
+ # Is this notification a pure notification only? There is no user
174
+ # response to a pure notification, it is for information only.
175
+ def is_notification?
176
+ @is_notification
177
+ end
178
+
179
+ # :stopdoc:
180
+ def has_reply
181
+ @has_reply = true
182
+ end
183
+ # :startdoc:
184
+
185
+ # :call-seq:
186
+ # has_reply? -> true or false
187
+ #
188
+ # Does this notification have a reply? This only makes sense for
189
+ # notifications that are not replies or pure notifications.
190
+ def has_reply?
191
+ @has_reply
192
+ end
193
+
194
+ # :call-seq:
195
+ # input_data -> data
196
+ #
197
+ # Get the input data associated with this notification. Returns an empty
198
+ # string if this notification is a reply.
199
+ def input_data
200
+ return "" if is_reply?
201
+
202
+ data_name = "interaction#{@id}InputData.json"
203
+ @run.read_interaction_data(data_name)
204
+ rescue AttributeNotFoundError
205
+ # It does not matter if the file doesn't exist.
206
+ ""
207
+ end
208
+
209
+ # :call-seq:
210
+ # reply(status, data)
211
+ #
212
+ # Given a status and some data this method uploads the data and
213
+ # publishes an interaction reply on the run's notification feed.
214
+ def reply(status, data)
215
+ data_name = "interaction#{@id}OutputData.json"
216
+
217
+ notification = Atom::Entry.new do |entry|
218
+ entry.title = "A reply to #{@id}"
219
+ entry.id = "#{@id}reply"
220
+ entry.content = ""
221
+ entry[FEED_NS, "run-id"] << @run.id
222
+ entry[FEED_NS, "in-reply-to"] << @id
223
+ entry[FEED_NS, "result-status"] << status
224
+ end.to_xml
225
+
226
+ @run.write_interaction_data(data_name, data)
227
+ @run.write_notification(notification)
228
+ end
229
+
230
+ private
231
+
232
+ def get_link(links)
233
+ links.each do |l|
234
+ return URI.parse(l.to_s) if l.rel == "presentation"
235
+ end
236
+ end
237
+ end
238
+
239
+ end
240
+
241
+ end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2010-2012 The University of Manchester, UK.
1
+ # Copyright (c) 2010-2013 The University of Manchester, UK.
2
2
  #
3
3
  # All rights reserved.
4
4
  #
@@ -59,7 +59,7 @@ module T2Server
59
59
  end
60
60
 
61
61
  # if we're given params they must be of the right type
62
- if !params.nil? and !params.is_a? ConnectionParameters
62
+ if !params.nil? && !params.is_a?(ConnectionParameters)
63
63
  raise ArgumentError, "Parameters must be ConnectionParameters", caller
64
64
  end
65
65
 
@@ -93,23 +93,27 @@ module T2Server
93
93
  @uri = uri
94
94
  @params = params || DefaultConnectionParameters.new
95
95
 
96
- # set up persistent http connection
96
+ # Open a persistent HTTP connection.
97
97
  @http = Net::HTTP::Persistent.new("Taverna_Server_Ruby_Client")
98
+
99
+ # Set timeouts if specified.
100
+ @http.open_timeout = @params[:open_timeout] if @params[:open_timeout]
101
+ @http.read_timeout = @params[:read_timeout] if @params[:read_timeout]
98
102
  end
99
103
 
100
104
  # :call-seq:
101
- # GET(uri, type, range, credentials) -> String
105
+ # GET(uri, type, range, credentials) -> string
102
106
  #
103
107
  # HTTP GET a resource at _uri_ of _type_ from the server. If successful
104
108
  # the body of the response is returned. A portion of the data can be
105
109
  # retrieved by specifying a byte range, start..end, with the _range_
106
110
  # parameter.
107
- def GET(uri, type, range, credentials)
111
+ def GET(uri, type, range, credentials, &block)
108
112
  get = Net::HTTP::Get.new(uri.path)
109
113
  get["Accept"] = type
110
114
  get["Range"] = "bytes=#{range.min}-#{range.max}" unless range.nil?
111
115
 
112
- response = submit(get, uri, credentials)
116
+ response = submit(get, uri, credentials, &block)
113
117
 
114
118
  case response
115
119
  when Net::HTTPOK, Net::HTTPPartialContent
@@ -119,36 +123,35 @@ module T2Server
119
123
  when Net::HTTPMovedTemporarily
120
124
  new_conn = redirect(response["location"])
121
125
  raise ConnectionRedirectError.new(new_conn)
122
- when Net::HTTPNotFound
123
- raise AttributeNotFoundError.new(uri.path)
124
- when Net::HTTPForbidden
125
- raise AccessForbiddenError.new("attribute #{uri.path}")
126
- when Net::HTTPUnauthorized
127
- raise AuthorizationError.new(credentials)
128
126
  else
129
- raise UnexpectedServerResponse.new(response)
127
+ report_error("GET", uri.path, response, credentials)
130
128
  end
131
129
  end
132
130
 
133
131
  # :call-seq:
134
- # PUT(uri, value, type, credentials) -> bool
132
+ # PUT(uri, value, type, credentials) -> true or false
135
133
  # PUT(uri, value, type, credentials) -> URI
134
+ # PUT(uri, stream, type, credentials) -> URI
135
+ #
136
+ # Upload data via HTTP PUT. Data may be specified as a value or as a
137
+ # stream. The stream can be any object that has a read(length) method;
138
+ # instances of File or IO, for example.
136
139
  #
137
- # Perform a HTTP PUT of _value_ to a location on the server specified by
138
- # _uri_ . If successful _true_ , or a URI to the PUT resource, is returned
139
- # depending on whether the operation has set a parameter (true) or uploaded
140
- # data (URI).
141
- def PUT(uri, value, type, credentials)
140
+ # If successful _true_ or a URI to the uploaded resource is returned
141
+ # depending on whether the operation has altered a parameter (true) or
142
+ # uploaded new data (URI).
143
+ def PUT(uri, data, type, credentials)
142
144
  put = Net::HTTP::Put.new(uri.path)
143
145
  put.content_type = type
144
- put.body = value
146
+
147
+ set_upload_body(put, data)
145
148
 
146
149
  response = submit(put, uri, credentials)
147
150
 
148
151
  case response
149
- when Net::HTTPOK
150
- # We've set a parameter so we get 200 back from the server. Return
151
- # true to indicate success.
152
+ when Net::HTTPOK, Net::HTTPAccepted
153
+ # We've either set a parameter or started a run so we get 200 or 202
154
+ # back from the server, respectively. Return true to indicate success.
152
155
  true
153
156
  when Net::HTTPCreated
154
157
  # We've uploaded data so we get 201 back from the server. Return the
@@ -158,26 +161,27 @@ module T2Server
158
161
  # We've modified data so we get 204 back from the server. Return the
159
162
  # uri of the modified resource.
160
163
  uri
161
- when Net::HTTPNotFound
162
- raise AttributeNotFoundError.new(uri.path)
163
- when Net::HTTPForbidden
164
- raise AccessForbiddenError.new("attribute #{uri.path}")
165
- when Net::HTTPUnauthorized
166
- raise AuthorizationError.new(credentials)
164
+ when Net::HTTPServiceUnavailable
165
+ raise ServerAtCapacityError.new
167
166
  else
168
- raise UnexpectedServerResponse.new(response)
167
+ report_error("PUT", uri.path, response, credentials)
169
168
  end
170
169
  end
171
170
 
172
171
  # :call-seq:
173
- # POST(uri, value, type, credentials)
172
+ # POST(uri, value, type, credentials) -> URI
173
+ # POST(uri, stream, type, credentials) -> URI
174
+ #
175
+ # Upload data via HTTP POST. Data may be specified as a value or as a
176
+ # stream. The stream can be any object that has a read(length) method;
177
+ # instances of File or IO, for example.
174
178
  #
175
- # Perform an HTTP POST of _value_ to a location on the server specified by
176
- # _uri_ and return the URI of the created attribute.
177
- def POST(uri, value, type, credentials)
179
+ # If successful the URI of the uploaded resource is returned.
180
+ def POST(uri, data, type, credentials)
178
181
  post = Net::HTTP::Post.new(uri.path)
179
182
  post.content_type = type
180
- post.body = value
183
+
184
+ set_upload_body(post, data)
181
185
 
182
186
  response = submit(post, uri, credentials)
183
187
 
@@ -185,23 +189,15 @@ module T2Server
185
189
  when Net::HTTPCreated
186
190
  # return the URI of the newly created item
187
191
  URI.parse(response['location'])
188
- when Net::HTTPNotFound
189
- raise AttributeNotFoundError.new(uri.path)
190
- when Net::HTTPForbidden
191
- if response.body.chomp.include? "server load exceeded"
192
- raise ServerAtCapacityError.new
193
- else
194
- raise AccessForbiddenError.new("attribute #{uri.path}")
195
- end
196
- when Net::HTTPUnauthorized
197
- raise AuthorizationError.new(credentials)
192
+ when Net::HTTPServiceUnavailable
193
+ raise ServerAtCapacityError.new
198
194
  else
199
- raise UnexpectedServerResponse.new(response)
195
+ report_error("POST", uri.path, response, credentials)
200
196
  end
201
197
  end
202
198
 
203
199
  # :call-seq:
204
- # DELETE(uri, credentials) -> bool
200
+ # DELETE(uri, credentials) -> true or false
205
201
  #
206
202
  # Perform an HTTP DELETE on a _uri_ on the server. If successful true
207
203
  # is returned.
@@ -214,19 +210,13 @@ module T2Server
214
210
  when Net::HTTPNoContent
215
211
  # Success, carry on...
216
212
  true
217
- when Net::HTTPNotFound
218
- false
219
- when Net::HTTPForbidden
220
- raise AccessForbiddenError.new(uri)
221
- when Net::HTTPUnauthorized
222
- raise AuthorizationError.new(credentials)
223
213
  else
224
- raise UnexpectedServerResponse.new(response)
214
+ report_error("DELETE", uri.path, response, credentials)
225
215
  end
226
216
  end
227
217
 
228
218
  # :call-seq:
229
- # OPTIONS(uri, credentials) -> Hash
219
+ # OPTIONS(uri, credentials) -> hash
230
220
  #
231
221
  # Perform the HTTP OPTIONS command on the given _uri_ and return a hash
232
222
  # of the headers returned.
@@ -238,23 +228,59 @@ module T2Server
238
228
  case response
239
229
  when Net::HTTPOK
240
230
  response.to_hash
231
+ else
232
+ report_error("OPTIONS", uri.path, response, credentials)
233
+ end
234
+ end
235
+
236
+ private
237
+
238
+ # If one of the expected responses for a HTTP method is not received then
239
+ # handle the error condition here.
240
+ def report_error(method, path, response, credentials)
241
+ case response
242
+ when Net::HTTPNotFound
243
+ raise AttributeNotFoundError.new(path)
241
244
  when Net::HTTPForbidden
242
- raise AccessForbiddenError.new("resource #{uri.path}")
245
+ raise AccessForbiddenError.new("resource #{path}")
243
246
  when Net::HTTPUnauthorized
244
247
  raise AuthorizationError.new(credentials)
245
248
  else
246
- raise UnexpectedServerResponse.new(response)
249
+ raise UnexpectedServerResponse.new(method, path, response)
247
250
  end
248
251
  end
249
252
 
250
- private
253
+ # If we have a stream then we need to set body_stream and then either
254
+ # supply a content length or set the transfer encoding to "chunked". A
255
+ # file object can supply its size, a bare IO object cannot. If we have a
256
+ # simple value we can set body directly.
257
+ def set_upload_body(request, data)
258
+ if data.respond_to? :read
259
+ request.body_stream = data
260
+ if data.respond_to? :size
261
+ request.content_length = data.size
262
+ else
263
+ request["Transfer-encoding"] = "chunked"
264
+ end
265
+ else
266
+ request.body = data
267
+ end
268
+ end
251
269
 
252
- def submit(request, uri, credentials)
270
+ # If a block is passed in here then the response is returned in chunks
271
+ # (streamed). If no block is passed in the whole response is read into
272
+ # memory and returned.
273
+ def submit(request, uri, credentials, &block)
253
274
 
254
275
  credentials.authenticate(request) unless credentials.nil?
255
276
 
277
+ response = nil
256
278
  begin
257
- @http.request(uri, request)
279
+ @http.request(uri, request) do |r|
280
+ r.read_body(&block)
281
+ response = r
282
+ end
283
+ response
258
284
  rescue InternalHTTPError => e
259
285
  raise ConnectionError.new(e)
260
286
  end
@@ -276,6 +302,10 @@ module T2Server
276
302
  def initialize(uri, params = nil)
277
303
  super(uri, params)
278
304
 
305
+ if OpenSSL::SSL::SSLContext::METHODS.include? @params[:ssl_version]
306
+ @http.ssl_version = @params[:ssl_version]
307
+ end
308
+
279
309
  # Peer verification
280
310
  if @params[:verify_peer]
281
311
  if @params[:ca_file]