t2-server 0.9.3 → 1.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.
@@ -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]