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.
- data/CHANGES.rdoc +79 -0
- data/LICENCE.rdoc +1 -1
- data/README.rdoc +94 -26
- data/Rakefile +6 -5
- data/bin/t2-delete-runs +25 -23
- data/bin/t2-get-output +11 -13
- data/bin/t2-run-workflow +91 -29
- data/bin/t2-server-info +29 -12
- data/extras/t2-server-stress +184 -0
- data/lib/t2-server-cli.rb +48 -23
- data/lib/t2-server.rb +2 -1
- data/lib/t2-server/admin.rb +7 -4
- data/lib/t2-server/exceptions.rb +23 -4
- data/lib/t2-server/interaction.rb +241 -0
- data/lib/t2-server/net/connection.rb +90 -60
- data/lib/t2-server/net/credentials.rb +25 -9
- data/lib/t2-server/net/parameters.rb +21 -6
- data/lib/t2-server/port.rb +229 -140
- data/lib/t2-server/run-cache.rb +99 -0
- data/lib/t2-server/run.rb +349 -332
- data/lib/t2-server/server.rb +115 -164
- data/lib/t2-server/util.rb +11 -9
- data/lib/t2-server/xml/libxml.rb +3 -2
- data/lib/t2-server/xml/nokogiri.rb +4 -3
- data/lib/t2-server/xml/rexml.rb +3 -2
- data/lib/t2-server/xml/xml.rb +47 -36
- data/lib/{t2server.rb → t2-server/xml/xpath_cache.rb} +29 -7
- data/t2-server.gemspec +16 -5
- data/test/tc_misc.rb +61 -0
- data/test/tc_perms.rb +17 -1
- data/test/tc_run.rb +164 -34
- data/test/tc_secure.rb +11 -2
- data/test/tc_server.rb +23 -2
- data/test/ts_t2server.rb +10 -8
- data/test/workflows/missing_outputs.t2flow +440 -0
- data/version.yml +3 -3
- metadata +42 -4
data/lib/t2-server/server.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2010-
|
1
|
+
# Copyright (c) 2010-2013 The University of Manchester, UK.
|
2
2
|
#
|
3
3
|
# All rights reserved.
|
4
4
|
#
|
@@ -32,6 +32,7 @@
|
|
32
32
|
|
33
33
|
require 'base64'
|
34
34
|
require 'uri'
|
35
|
+
require 't2-server/run-cache'
|
35
36
|
|
36
37
|
module T2Server
|
37
38
|
|
@@ -45,21 +46,22 @@ module T2Server
|
|
45
46
|
# endpoints.
|
46
47
|
REST_ENDPOINT = "rest/"
|
47
48
|
|
48
|
-
|
49
|
+
XPATHS = {
|
49
50
|
# Server top-level XPath queries
|
50
|
-
:server =>
|
51
|
-
:policy =>
|
52
|
-
:run =>
|
53
|
-
:runs =>
|
51
|
+
:server => "//nsr:serverDescription",
|
52
|
+
:policy => "//nsr:policy",
|
53
|
+
:run => "//nsr:run",
|
54
|
+
:runs => "//nsr:runs",
|
54
55
|
|
55
56
|
# Server policy XPath queries
|
56
|
-
:runlimit
|
57
|
-
:
|
58
|
-
:
|
59
|
-
:
|
60
|
-
:notify =>
|
61
|
-
XML::Methods.xpath_compile("//nsr:enabledNotificationFabrics")
|
57
|
+
:runlimit => "//nsr:runLimit",
|
58
|
+
:permworkflows => "//nsr:permittedWorkflows",
|
59
|
+
:permlisteners => "//nsr:permittedListenerTypes",
|
60
|
+
:notifications => "//nsr:enabledNotificationFabrics"
|
62
61
|
}
|
62
|
+
|
63
|
+
@@xpaths = XML::XPathCache.instance
|
64
|
+
@@xpaths.register_xpaths XPATHS
|
63
65
|
# :startdoc:
|
64
66
|
|
65
67
|
# :call-seq:
|
@@ -72,16 +74,9 @@ module T2Server
|
|
72
74
|
#
|
73
75
|
# It will _yield_ itself if a block is given.
|
74
76
|
def initialize(uri, params = nil)
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
# strip username and password from the URI if present
|
81
|
-
if uri.user != nil
|
82
|
-
uri = URI::HTTP.new(uri.scheme, nil, uri.host, uri.port, nil,
|
83
|
-
uri.path, nil, nil, nil);
|
84
|
-
end
|
77
|
+
# Convert strings to URIs and strip any credentials that have been given
|
78
|
+
# in the URI. We do not want to store credentials in this class.
|
79
|
+
uri, creds = Util.strip_uri_credentials(uri)
|
85
80
|
|
86
81
|
# setup connection
|
87
82
|
@connection = ConnectionFactory.connect(uri, params)
|
@@ -93,20 +88,12 @@ module T2Server
|
|
93
88
|
@version_components = nil
|
94
89
|
@links = nil
|
95
90
|
|
96
|
-
#
|
97
|
-
@
|
91
|
+
# Initialize the run object cache.
|
92
|
+
@run_cache = RunCache.new(self)
|
98
93
|
|
99
94
|
yield(self) if block_given?
|
100
95
|
end
|
101
96
|
|
102
|
-
# :stopdoc:
|
103
|
-
def Server.connect(uri, username="", password="")
|
104
|
-
warn "[DEPRECATION] 'Server#connect' is deprecated and will be " +
|
105
|
-
"removed in 1.0."
|
106
|
-
new(uri)
|
107
|
-
end
|
108
|
-
# :startdoc:
|
109
|
-
|
110
97
|
# :call-seq:
|
111
98
|
# administrator(credentials = nil) -> Administrator
|
112
99
|
# administrator(credentials = nil) {|admin| ...}
|
@@ -126,47 +113,61 @@ module T2Server
|
|
126
113
|
#
|
127
114
|
# Create a run on this server using the specified _workflow_.
|
128
115
|
# This method will _yield_ the newly created Run if a block is given.
|
116
|
+
#
|
117
|
+
# The _workflow_ parameter may be the workflow itself, a file name or a
|
118
|
+
# File or IO object.
|
129
119
|
def create_run(workflow, credentials = nil)
|
130
|
-
|
131
|
-
run = Run.create(self, "", credentials,
|
120
|
+
uri = initialize_run(workflow, credentials)
|
121
|
+
run = Run.create(self, "", credentials, uri)
|
132
122
|
|
133
|
-
#
|
134
|
-
|
135
|
-
@runs[user] = {} unless @runs[user]
|
136
|
-
@runs[user][id] = run
|
123
|
+
# Add the newly created run object to the user's run cache
|
124
|
+
@run_cache.add_run(run, credentials)
|
137
125
|
|
138
126
|
yield(run) if block_given?
|
139
127
|
run
|
140
128
|
end
|
141
129
|
|
142
|
-
# :
|
143
|
-
#
|
130
|
+
# :stopdoc:
|
131
|
+
# Create a run on this server using the specified _workflow_ and return
|
132
|
+
# the URI to it.
|
144
133
|
#
|
145
|
-
#
|
146
|
-
#
|
134
|
+
# We need to catch AccessForbiddenError here to be compatible with Server
|
135
|
+
# versions pre 2.4.2. When we no longer support them we can remove the
|
136
|
+
# rescue clause of this method.
|
147
137
|
def initialize_run(workflow, credentials = nil)
|
148
|
-
#
|
149
|
-
|
150
|
-
|
138
|
+
# If workflow is a String, it might be a filename! If so, stream it.
|
139
|
+
if (workflow.instance_of? String) && (File.file? workflow)
|
140
|
+
return File.open(workflow, "r") do |file|
|
141
|
+
create(links[:runs], file, "application/vnd.taverna.t2flow+xml",
|
142
|
+
credentials)
|
143
|
+
end
|
144
|
+
end
|
151
145
|
|
152
|
-
|
153
|
-
|
146
|
+
# If we get here then workflow could either be a String containing a
|
147
|
+
# workflow or a File or IO object.
|
148
|
+
create(links[:runs], workflow, "application/vnd.taverna.t2flow+xml",
|
149
|
+
credentials)
|
150
|
+
rescue AccessForbiddenError => afe
|
151
|
+
(major, minor, patch) = version_components
|
152
|
+
if minor == 4 && patch >= 2
|
153
|
+
# Need to re-raise as it's a real error for later versions.
|
154
|
+
raise afe
|
155
|
+
else
|
156
|
+
raise ServerAtCapacityError.new
|
157
|
+
end
|
154
158
|
end
|
159
|
+
# :startdoc:
|
155
160
|
|
156
161
|
# :call-seq:
|
157
|
-
# version ->
|
162
|
+
# version -> string
|
158
163
|
#
|
159
164
|
# The version string of the remote Taverna Server.
|
160
165
|
def version
|
161
|
-
|
162
|
-
@version = _get_version
|
163
|
-
end
|
164
|
-
|
165
|
-
@version
|
166
|
+
@version ||= _get_version
|
166
167
|
end
|
167
168
|
|
168
169
|
# :call-seq:
|
169
|
-
# version_components ->
|
170
|
+
# version_components -> array
|
170
171
|
#
|
171
172
|
# An array of the major, minor and patch version components of the remote
|
172
173
|
# Taverna Server.
|
@@ -188,7 +189,7 @@ module T2Server
|
|
188
189
|
end
|
189
190
|
|
190
191
|
# :call-seq:
|
191
|
-
# run_limit(credentials = nil) ->
|
192
|
+
# run_limit(credentials = nil) -> fixnum
|
192
193
|
#
|
193
194
|
# The maximum number of runs that this server will allow at any one time.
|
194
195
|
# Runs in any state (+Initialized+, +Running+ and +Finished+) are counted
|
@@ -213,26 +214,6 @@ module T2Server
|
|
213
214
|
get_runs(credentials)[identifier]
|
214
215
|
end
|
215
216
|
|
216
|
-
# :stopdoc:
|
217
|
-
def delete_run(run, credentials = nil)
|
218
|
-
warn "[DEPRECATION] 'Server#delete_run' is deprecated and will be " +
|
219
|
-
"removed in 1.0. Please use 'Run#delete' to delete a run."
|
220
|
-
|
221
|
-
# get the identifier from the run if that is what is passed in
|
222
|
-
if run.instance_of? Run
|
223
|
-
run = run.identifier
|
224
|
-
end
|
225
|
-
|
226
|
-
run_uri = Util.append_to_uri_path(links[:runs], run)
|
227
|
-
if delete(run_uri, credentials)
|
228
|
-
# delete cached run object - this must be done per user
|
229
|
-
user = credentials.nil? ? :all : credentials.username
|
230
|
-
@runs[user].delete(run) if @runs[user]
|
231
|
-
true
|
232
|
-
end
|
233
|
-
end
|
234
|
-
# :startdoc:
|
235
|
-
|
236
217
|
# :call-seq:
|
237
218
|
# delete_all_runs(credentials = nil)
|
238
219
|
#
|
@@ -240,56 +221,31 @@ module T2Server
|
|
240
221
|
# only those runs that the provided credentials have permission to delete
|
241
222
|
# will be deleted.
|
242
223
|
def delete_all_runs(credentials = nil)
|
243
|
-
#
|
224
|
+
# Refresh run list, delete everything, clear the user's run cache.
|
244
225
|
runs(credentials).each {|run| run.delete}
|
226
|
+
@run_cache.clear!(credentials)
|
245
227
|
end
|
246
228
|
|
247
229
|
# :stopdoc:
|
248
|
-
def set_run_input(run, input, value, credentials = nil)
|
249
|
-
warn "[DEPRECATION] 'Server#set_run_input' is deprecated and will be " +
|
250
|
-
"removed in 1.0. Input ports are set directly instead. The most " +
|
251
|
-
"direct replacement for this method is: " +
|
252
|
-
"'Run#input_port(input).value = value'"
|
253
|
-
|
254
|
-
# get the run from the identifier if that is what is passed in
|
255
|
-
if not run.instance_of? Run
|
256
|
-
run = run(run, credentials)
|
257
|
-
end
|
258
|
-
|
259
|
-
run.input_port(input).value = value
|
260
|
-
end
|
261
|
-
|
262
|
-
def set_run_input_file(run, input, filename, credentials = nil)
|
263
|
-
warn "[DEPRECATION] 'Server#set_run_input_file' is deprecated and " +
|
264
|
-
"will be removed in 1.0. Input ports are set directly instead. The " +
|
265
|
-
"most direct replacement for this method is: " +
|
266
|
-
"'Run#input_port(input).remote_file = filename'"
|
267
|
-
|
268
|
-
# get the run from the identifier if that is what is passed in
|
269
|
-
if not run.instance_of? Run
|
270
|
-
run = run(run, credentials)
|
271
|
-
end
|
272
|
-
|
273
|
-
run.input_port(input).remote_file = filename
|
274
|
-
end
|
275
|
-
|
276
230
|
def mkdir(uri, dir, credentials = nil)
|
277
231
|
@connection.POST(uri, XML::Fragments::MKDIR % dir, "application/xml",
|
278
232
|
credentials)
|
279
233
|
end
|
280
234
|
|
281
|
-
def make_run_dir(run, root, dir, credentials = nil)
|
282
|
-
warn "[DEPRECATION] 'Server#make_run_dir' is deprecated and will be " +
|
283
|
-
"removed in 1.0. Please use 'Run#mkdir' instead."
|
284
|
-
|
285
|
-
create_dir(run, root, dir, credentials)
|
286
|
-
end
|
287
|
-
|
288
235
|
def upload_file(filename, uri, remote_name, credentials = nil)
|
289
|
-
|
236
|
+
# Different Server versions support different upload methods
|
237
|
+
(major, minor, patch) = version_components
|
238
|
+
|
290
239
|
remote_name = filename.split('/')[-1] if remote_name == ""
|
291
240
|
|
292
|
-
|
241
|
+
if minor == 4 && patch >= 1
|
242
|
+
File.open(filename, "rb") do |file|
|
243
|
+
upload_data(file, remote_name, uri, credentials)
|
244
|
+
end
|
245
|
+
else
|
246
|
+
contents = IO.read(filename)
|
247
|
+
upload_data(contents, remote_name, uri, credentials)
|
248
|
+
end
|
293
249
|
end
|
294
250
|
|
295
251
|
def upload_data(data, remote_name, uri, credentials = nil)
|
@@ -307,14 +263,6 @@ module T2Server
|
|
307
263
|
end
|
308
264
|
end
|
309
265
|
|
310
|
-
def upload_run_file(run, filename, location, rename, credentials = nil)
|
311
|
-
warn "[DEPRECATION] 'Server#upload_run_file' is deprecated and will " +
|
312
|
-
"be removed in 1.0. Please use 'Run#upload_file' or " +
|
313
|
-
"'Run#input_port(input).file = filename' instead."
|
314
|
-
|
315
|
-
upload_file(run, filename, location, rename, credentials)
|
316
|
-
end
|
317
|
-
|
318
266
|
def is_resource_writable?(uri, credentials = nil)
|
319
267
|
headers = @connection.OPTIONS(uri, credentials)
|
320
268
|
headers["allow"][0].split(",").include? "PUT"
|
@@ -324,7 +272,7 @@ module T2Server
|
|
324
272
|
@connection.POST(uri, value, type, credentials)
|
325
273
|
end
|
326
274
|
|
327
|
-
def read(uri, type, *rest)
|
275
|
+
def read(uri, type, *rest, &block)
|
328
276
|
credentials = nil
|
329
277
|
range = nil
|
330
278
|
|
@@ -340,7 +288,7 @@ module T2Server
|
|
340
288
|
end
|
341
289
|
|
342
290
|
begin
|
343
|
-
@connection.GET(uri, type, range, credentials)
|
291
|
+
@connection.GET(uri, type, range, credentials, &block)
|
344
292
|
rescue ConnectionRedirectError => cre
|
345
293
|
# We've been redirected so save the new connection object with the new
|
346
294
|
# server URI and try again with the new URI.
|
@@ -350,21 +298,51 @@ module T2Server
|
|
350
298
|
end
|
351
299
|
end
|
352
300
|
|
301
|
+
# An internal helper to write streamed data directly to another stream.
|
302
|
+
# The number of bytes written to the stream is returned. The stream passed
|
303
|
+
# in may be anything that provides a +write+ method; instances of IO and
|
304
|
+
# File, for example.
|
305
|
+
def read_to_stream(stream, uri, type, *rest)
|
306
|
+
raise ArgumentError,
|
307
|
+
"Stream passed in must provide a write method" unless
|
308
|
+
stream.respond_to? :write
|
309
|
+
|
310
|
+
bytes = 0
|
311
|
+
|
312
|
+
read(uri, type, *rest) do |chunk|
|
313
|
+
bytes += stream.write(chunk)
|
314
|
+
end
|
315
|
+
|
316
|
+
bytes
|
317
|
+
end
|
318
|
+
|
319
|
+
# An internal helper to write streamed data straight to a file.
|
320
|
+
def read_to_file(filename, uri, type, *rest)
|
321
|
+
File.open(filename, "wb") do |file|
|
322
|
+
read_to_stream(file, uri, type, *rest)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
353
326
|
def update(uri, value, type, credentials = nil)
|
354
327
|
@connection.PUT(uri, value, type, credentials)
|
355
328
|
end
|
356
329
|
|
357
330
|
def delete(uri, credentials = nil)
|
358
331
|
@connection.DELETE(uri, credentials)
|
332
|
+
rescue AttributeNotFoundError => ane
|
333
|
+
# Ignore this. Delete is idempotent so deleting something that has
|
334
|
+
# already been deleted, or is for some other reason not there, should
|
335
|
+
# happen silently. Return true here because when deleting it's enough to
|
336
|
+
# know that it's no longer there rather than whether it was deleted
|
337
|
+
# *this time* or not.
|
338
|
+
true
|
359
339
|
end
|
360
340
|
# :startdoc:
|
361
341
|
|
362
342
|
private
|
363
343
|
|
364
344
|
def links
|
365
|
-
@links
|
366
|
-
|
367
|
-
@links
|
345
|
+
@links ||= _get_server_links
|
368
346
|
end
|
369
347
|
|
370
348
|
def _get_server_description
|
@@ -378,15 +356,14 @@ module T2Server
|
|
378
356
|
|
379
357
|
def _get_version
|
380
358
|
doc = _get_server_description
|
381
|
-
version = xpath_attr(doc,
|
359
|
+
version = xpath_attr(doc, @@xpaths[:server], "serverVersion")
|
382
360
|
if version == nil
|
383
361
|
raise RuntimeError.new("Taverna Servers prior to version 2.3 " +
|
384
362
|
"are no longer supported.")
|
385
363
|
else
|
386
|
-
# Remove
|
387
|
-
|
388
|
-
|
389
|
-
end
|
364
|
+
# Remove extra version tags if present.
|
365
|
+
version.gsub!("-SNAPSHOT", "")
|
366
|
+
version.gsub!(/alpha[0-9]*/, "")
|
390
367
|
|
391
368
|
# Add .0 if we only have a major and minor component.
|
392
369
|
if version.split(".").length == 2
|
@@ -399,23 +376,11 @@ module T2Server
|
|
399
376
|
|
400
377
|
def _get_server_links
|
401
378
|
doc = _get_server_description
|
402
|
-
links =
|
403
|
-
links[:runs] = URI.parse(xpath_attr(doc, XPaths[:runs], "href"))
|
379
|
+
links = get_uris_from_doc(doc, [:runs, :policy])
|
404
380
|
|
405
|
-
links[:policy] = URI.parse(xpath_attr(doc, XPaths[:policy], "href"))
|
406
381
|
doc = xml_document(read(links[:policy], "application/xml"))
|
407
|
-
|
408
|
-
|
409
|
-
URI.parse(xpath_attr(doc, XPaths[:permlstt], "href"))
|
410
|
-
links[:notifications] =
|
411
|
-
URI.parse(xpath_attr(doc, XPaths[:notify], "href"))
|
412
|
-
|
413
|
-
links[:runlimit] =
|
414
|
-
URI.parse(xpath_attr(doc, XPaths[:runlimit], "href"))
|
415
|
-
links[:permworkflows] =
|
416
|
-
URI.parse(xpath_attr(doc, XPaths[:permwkf], "href"))
|
417
|
-
|
418
|
-
links
|
382
|
+
links.merge get_uris_from_doc(doc,
|
383
|
+
[:permlisteners, :notifications, :runlimit, :permworkflows])
|
419
384
|
end
|
420
385
|
|
421
386
|
def get_runs(credentials = nil)
|
@@ -425,29 +390,15 @@ module T2Server
|
|
425
390
|
|
426
391
|
# get list of run identifiers
|
427
392
|
run_list = {}
|
428
|
-
xpath_find(doc,
|
393
|
+
xpath_find(doc, @@xpaths[:run]).each do |run|
|
429
394
|
uri = URI.parse(xml_node_attribute(run, "href"))
|
430
395
|
id = xml_node_content(run)
|
431
396
|
run_list[id] = uri
|
432
397
|
end
|
433
398
|
|
434
|
-
#
|
435
|
-
|
436
|
-
@runs[user] = {} unless @runs[user]
|
437
|
-
|
438
|
-
# add new runs to the user cache
|
439
|
-
run_list.each_key do |id|
|
440
|
-
if !@runs[user].has_key? id
|
441
|
-
@runs[user][id] = Run.create(self, "", credentials, run_list[id])
|
442
|
-
end
|
443
|
-
end
|
444
|
-
|
445
|
-
# clear out the expired runs
|
446
|
-
if @runs[user].length > run_list.length
|
447
|
-
@runs[user].delete_if {|key, val| !run_list.member? key}
|
448
|
-
end
|
449
|
-
|
450
|
-
@runs[user]
|
399
|
+
# Refresh the user's cache and return the runs in it.
|
400
|
+
@run_cache.refresh_all!(run_list, credentials)
|
451
401
|
end
|
402
|
+
|
452
403
|
end
|
453
404
|
end
|
data/lib/t2-server/util.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2010-
|
1
|
+
# Copyright (c) 2010-2013 The University of Manchester, UK.
|
2
2
|
#
|
3
3
|
# All rights reserved.
|
4
4
|
#
|
@@ -30,23 +30,25 @@
|
|
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 module contains various utility methods that the library uses
|
36
40
|
# internally.
|
37
41
|
module Util
|
38
42
|
|
39
43
|
# :call-seq:
|
40
|
-
# Util.strip_uri_credentials(uri) -> URI,
|
44
|
+
# Util.strip_uri_credentials(uri) -> URI, HttpBasic
|
41
45
|
#
|
42
46
|
# Strip user credentials from an address in URI or String format and return
|
43
|
-
# a tuple of the URI minus the credentials and a T2Server::
|
47
|
+
# a tuple of the URI minus the credentials and a T2Server::HttpBasic
|
44
48
|
# object.
|
45
49
|
def self.strip_uri_credentials(uri)
|
46
50
|
# we want to use URIs here but strings can be passed in
|
47
|
-
unless uri.is_a? URI
|
48
|
-
uri = URI.parse(Util.strip_path_slashes(uri))
|
49
|
-
end
|
51
|
+
uri = URI.parse(Util.strip_path_slashes(uri)) unless uri.is_a? URI
|
50
52
|
|
51
53
|
creds = nil
|
52
54
|
|
@@ -62,7 +64,7 @@ module T2Server
|
|
62
64
|
end
|
63
65
|
|
64
66
|
# :call-seq:
|
65
|
-
# Util.strip_path_slashes(path) ->
|
67
|
+
# Util.strip_path_slashes(path) -> string
|
66
68
|
#
|
67
69
|
# Returns a new String with one leading and one trailing slash
|
68
70
|
# removed from the ends of _path_ (if present).
|
@@ -95,8 +97,8 @@ module T2Server
|
|
95
97
|
new_uri
|
96
98
|
end
|
97
99
|
|
98
|
-
# :
|
99
|
-
# Util.get_path_leaf_from_uri(uri) ->
|
100
|
+
# :call-seq:
|
101
|
+
# Util.get_path_leaf_from_uri(uri) -> string
|
100
102
|
#
|
101
103
|
# Get the final component from the path of a URI. This method returns the
|
102
104
|
# empty string (not _nil_ ) if the URI does not have a path.
|