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.
- 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.
|