t2-server 0.6.1 → 0.9.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/.rvmrc +1 -0
- data/CHANGES.rdoc +48 -0
- data/LICENCE.rdoc +2 -2
- data/README.rdoc +245 -10
- data/Rakefile +108 -0
- data/bin/t2-delete-runs +21 -34
- data/bin/t2-get-output +134 -0
- data/bin/t2-run-workflow +121 -109
- data/bin/t2-server-admin +128 -0
- data/bin/t2-server-info +25 -38
- data/lib/t2-server-cli.rb +116 -0
- data/lib/t2-server.rb +16 -27
- data/lib/t2-server/admin.rb +147 -0
- data/lib/t2-server/connection-parameters.rb +144 -0
- data/lib/t2-server/connection.rb +352 -0
- data/lib/t2-server/credentials.rb +84 -0
- data/lib/t2-server/exceptions.rb +42 -21
- data/lib/t2-server/port.rb +472 -0
- data/lib/t2-server/run.rb +822 -227
- data/lib/t2-server/server.rb +313 -317
- data/lib/t2-server/util.rb +71 -0
- data/lib/t2-server/xml/libxml.rb +87 -0
- data/lib/t2-server/xml/nokogiri.rb +85 -0
- data/lib/t2-server/xml/rexml.rb +85 -0
- data/lib/t2-server/xml/xml.rb +111 -0
- data/lib/t2server.rb +4 -1
- data/t2-server.gemspec +112 -0
- data/test/tc_admin.rb +63 -0
- data/test/{tc_paths.rb → tc_params.rb} +11 -25
- data/test/tc_perms.rb +132 -0
- data/test/tc_run.rb +200 -67
- data/test/tc_secure.rb +191 -0
- data/test/tc_server.rb +25 -23
- data/test/tc_util.rb +74 -0
- data/test/ts_t2server.rb +57 -12
- data/test/workflows/always_fail.t2flow +69 -0
- data/test/workflows/list_and_value.t2flow +12 -0
- data/test/workflows/list_with_errors.t2flow +107 -0
- data/test/workflows/secure/basic-http.t2flow +74 -0
- data/test/workflows/secure/basic-https.t2flow +74 -0
- data/test/workflows/secure/client-https.t2flow +162 -0
- data/test/workflows/secure/digest-http.t2flow +129 -0
- data/test/workflows/secure/digest-https.t2flow +107 -0
- data/test/workflows/secure/heater-pk.pem +20 -0
- data/test/workflows/secure/user-cert.p12 +0 -0
- data/test/workflows/secure/ws-http.t2flow +180 -0
- data/test/workflows/secure/ws-https.t2flow +180 -0
- data/test/workflows/strings.txt +10 -0
- data/test/workflows/xml_xpath.t2flow +136 -136
- data/version.yml +4 -0
- metadata +132 -34
- data/lib/t2-server/xml.rb +0 -86
@@ -0,0 +1,84 @@
|
|
1
|
+
# Copyright (c) 2010, 2011 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
|
+
module T2Server
|
34
|
+
|
35
|
+
# This class serves as a base class for concrete HTTP credential systems.
|
36
|
+
class HttpCredentials
|
37
|
+
# The username held by these credentials.
|
38
|
+
attr_reader :username
|
39
|
+
|
40
|
+
# Create a set of credentials with the supplied username and password.
|
41
|
+
def initialize(username, password)
|
42
|
+
@username = username
|
43
|
+
@password = password
|
44
|
+
end
|
45
|
+
|
46
|
+
# :call-seq:
|
47
|
+
# to_s
|
48
|
+
#
|
49
|
+
# Return the username held by these credentials.
|
50
|
+
def to_s
|
51
|
+
@username
|
52
|
+
end
|
53
|
+
|
54
|
+
# Used within #inspect, below to help override the built in version.
|
55
|
+
@@to_s = Kernel.instance_method(:to_s)
|
56
|
+
|
57
|
+
# :call-seq:
|
58
|
+
# inspect
|
59
|
+
#
|
60
|
+
# Override the Kernel#inspect method so that the password is not exposed
|
61
|
+
# when it is called.
|
62
|
+
def inspect
|
63
|
+
@@to_s.bind(self).call.sub!(/>\z/) {" Username:#{self}>"}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# A class representing HTTP Basic credentials.
|
68
|
+
class HttpBasic < HttpCredentials
|
69
|
+
|
70
|
+
# Create a set of credentials with the supplied username and password.
|
71
|
+
def initialize(username, password)
|
72
|
+
super(username, password)
|
73
|
+
end
|
74
|
+
|
75
|
+
# :call-seq:
|
76
|
+
# authenticate(request)
|
77
|
+
#
|
78
|
+
# Authenticate the supplied HTTP request with the credentials held within
|
79
|
+
# this class.
|
80
|
+
def authenticate(request)
|
81
|
+
request.basic_auth @username, @password
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/t2-server/exceptions.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2010
|
1
|
+
# Copyright (c) 2010-2012 The University of Manchester, UK.
|
2
2
|
#
|
3
3
|
# All rights reserved.
|
4
4
|
#
|
@@ -14,7 +14,7 @@
|
|
14
14
|
#
|
15
15
|
# * Neither the names of The University of Manchester nor the names of its
|
16
16
|
# contributors may be used to endorse or promote products derived from this
|
17
|
-
# software without specific prior written permission.
|
17
|
+
# software without specific prior written permission.
|
18
18
|
#
|
19
19
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
20
20
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
@@ -85,8 +85,8 @@ module T2Server
|
|
85
85
|
# response. The response to be passed in is that which was returned by a
|
86
86
|
# call to Net::HTTP#request.
|
87
87
|
def initialize(response)
|
88
|
-
body = "\n#{response.body}"
|
89
|
-
super "Unexpected server response: #{response.code}\n#{
|
88
|
+
body = response.body ? "\n#{response.body}" : ""
|
89
|
+
super "Unexpected server response: #{response.code}\n#{body}"
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
@@ -94,12 +94,12 @@ module T2Server
|
|
94
94
|
# expectation is that the run exists then it could have been destroyed by
|
95
95
|
# a timeout or another user.
|
96
96
|
class RunNotFoundError < T2ServerError
|
97
|
-
attr_reader :
|
97
|
+
attr_reader :identifier
|
98
98
|
|
99
|
-
# Create a new RunNotFoundError with the specified
|
100
|
-
def initialize(
|
101
|
-
@
|
102
|
-
super "Could not find run #{@
|
99
|
+
# Create a new RunNotFoundError with the specified identifier.
|
100
|
+
def initialize(id)
|
101
|
+
@identifier = id
|
102
|
+
super "Could not find run #{@identifier}"
|
103
103
|
end
|
104
104
|
end
|
105
105
|
|
@@ -118,13 +118,10 @@ module T2Server
|
|
118
118
|
|
119
119
|
# The server is at capacity and cannot accept anymore runs at this time.
|
120
120
|
class ServerAtCapacityError < T2ServerError
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
def initialize(limit)
|
126
|
-
@limit = limit
|
127
|
-
super "The server is already running its configured limit of concurrent workflows (#{@limit})"
|
121
|
+
# Create a new ServerAtCapacityError.
|
122
|
+
def initialize
|
123
|
+
super "The server is already running its configured limit of " +
|
124
|
+
"concurrent workflows."
|
128
125
|
end
|
129
126
|
end
|
130
127
|
|
@@ -137,7 +134,9 @@ module T2Server
|
|
137
134
|
# attribute.
|
138
135
|
def initialize(path)
|
139
136
|
@path = path
|
140
|
-
super "Access to #{@path} is forbidden. Either you do not have the
|
137
|
+
super "Access to #{@path} is forbidden. Either you do not have the " +
|
138
|
+
"required credentials or the server does not allow the requested " +
|
139
|
+
"operation"
|
141
140
|
end
|
142
141
|
end
|
143
142
|
|
@@ -146,9 +145,14 @@ module T2Server
|
|
146
145
|
attr_reader :username
|
147
146
|
|
148
147
|
# Create a new AuthorizationError with the rejected username
|
149
|
-
def initialize(
|
150
|
-
|
151
|
-
|
148
|
+
def initialize(credentials)
|
149
|
+
if credentials != nil
|
150
|
+
@username = credentials.username
|
151
|
+
else
|
152
|
+
@username = ""
|
153
|
+
end
|
154
|
+
super "The username '#{@username}' is not authorized to connect to " +
|
155
|
+
"this server"
|
152
156
|
end
|
153
157
|
end
|
154
158
|
|
@@ -160,7 +164,24 @@ module T2Server
|
|
160
164
|
# Create a new RunStateError specifying both the current state and that
|
161
165
|
# which is needed to run the operation.
|
162
166
|
def initialize(current, need)
|
163
|
-
super "The run is in the wrong state (#{current}); it should be
|
167
|
+
super "The run is in the wrong state (#{current}); it should be " +
|
168
|
+
"'#{need}' to perform that action"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Raised if the server wishes to redirect the connection. This typically
|
173
|
+
# happens if a client tries to connect to a https server vis a http uri.
|
174
|
+
class ConnectionRedirectError < T2ServerError
|
175
|
+
|
176
|
+
# The redirected connection
|
177
|
+
attr_reader :redirect
|
178
|
+
|
179
|
+
# Create a new ConnectionRedirectError with the new, redirected,
|
180
|
+
# connection supplied.
|
181
|
+
def initialize(connection)
|
182
|
+
@redirect = connection
|
183
|
+
|
184
|
+
super "The server returned an unhandled redirect to '#{@redirect}'."
|
164
185
|
end
|
165
186
|
end
|
166
187
|
end
|
@@ -0,0 +1,472 @@
|
|
1
|
+
# Copyright (c) 2010-2012 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
|
+
module T2Server
|
34
|
+
|
35
|
+
# Base class of InputPort and OutputPort
|
36
|
+
class Port
|
37
|
+
include XML::Methods
|
38
|
+
|
39
|
+
# The port's name
|
40
|
+
attr_reader :name
|
41
|
+
|
42
|
+
# The "depth" of the port. 0 = a singleton value.
|
43
|
+
attr_reader :depth
|
44
|
+
|
45
|
+
# :stopdoc:
|
46
|
+
# Create a new port.
|
47
|
+
def initialize(run, xml)
|
48
|
+
@run = run
|
49
|
+
|
50
|
+
parse_xml(xml)
|
51
|
+
end
|
52
|
+
# :startdoc:
|
53
|
+
|
54
|
+
private
|
55
|
+
def parse_xml(xml)
|
56
|
+
@name = xml_node_attribute(xml, 'name')
|
57
|
+
@depth = xml_node_attribute(xml, 'depth').to_i
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Represents an input to a workflow.
|
62
|
+
class InputPort < Port
|
63
|
+
|
64
|
+
# If set, the file which has been used to supply this port's data.
|
65
|
+
attr_reader :file
|
66
|
+
|
67
|
+
# If set, the value held by this port. Could be a list (of lists (etc)).
|
68
|
+
attr_reader :value
|
69
|
+
|
70
|
+
# :stopdoc:
|
71
|
+
# Create a new InputPort.
|
72
|
+
def initialize(run, xml)
|
73
|
+
super(run, xml)
|
74
|
+
|
75
|
+
@value = nil
|
76
|
+
@file = nil
|
77
|
+
@remote_file = false
|
78
|
+
end
|
79
|
+
# :startdoc:
|
80
|
+
|
81
|
+
# :call-seq:
|
82
|
+
# value = value
|
83
|
+
#
|
84
|
+
# Set the value of this input port. This has no effect if the run is
|
85
|
+
# already running or finished.
|
86
|
+
def value=(value)
|
87
|
+
return unless @run.initialized?
|
88
|
+
@file = nil
|
89
|
+
@remote_file = false
|
90
|
+
@value = value
|
91
|
+
end
|
92
|
+
|
93
|
+
# :call-seq:
|
94
|
+
# file? -> bool
|
95
|
+
#
|
96
|
+
# Is this port's data being supplied by a file? The file could be local or
|
97
|
+
# remote (already on the server) for this to return true.
|
98
|
+
def file?
|
99
|
+
!@file.nil?
|
100
|
+
end
|
101
|
+
|
102
|
+
# :call-seq:
|
103
|
+
# remote_file? -> bool
|
104
|
+
#
|
105
|
+
# Is this port's data being supplied by a remote (one that is already on
|
106
|
+
# the server) file?
|
107
|
+
def remote_file?
|
108
|
+
file? && @remote_file
|
109
|
+
end
|
110
|
+
|
111
|
+
# :call-seq:
|
112
|
+
# remote_file = filename
|
113
|
+
#
|
114
|
+
# Set the remote file to use for this port's data. The file must already be
|
115
|
+
# on the server. This has no effect if the run is already running or
|
116
|
+
# finished.
|
117
|
+
def remote_file=(filename)
|
118
|
+
return unless @run.initialized?
|
119
|
+
@value = nil
|
120
|
+
@file = filename
|
121
|
+
@remote_file = true
|
122
|
+
end
|
123
|
+
|
124
|
+
# :call-seq:
|
125
|
+
# file = filename
|
126
|
+
#
|
127
|
+
# Set the file to use for this port's data. The file will be uploaded to
|
128
|
+
# the server before the run starts. This has no effect if the run is
|
129
|
+
# already running or finished.
|
130
|
+
def file=(filename)
|
131
|
+
return unless @run.initialized?
|
132
|
+
@value = nil
|
133
|
+
@file = filename
|
134
|
+
@remote_file = false
|
135
|
+
end
|
136
|
+
|
137
|
+
# :call-seq:
|
138
|
+
# baclava? -> bool
|
139
|
+
#
|
140
|
+
# Has this port been set via a baclava document?
|
141
|
+
def baclava?
|
142
|
+
@run.baclava_input?
|
143
|
+
end
|
144
|
+
|
145
|
+
# :call-seq:
|
146
|
+
# set? -> bool
|
147
|
+
#
|
148
|
+
# Has this port been set?
|
149
|
+
def set?
|
150
|
+
!value.nil? or file? or baclava?
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Represents an output port of a workflow.
|
155
|
+
class OutputPort < Port
|
156
|
+
include XML::Methods
|
157
|
+
|
158
|
+
# :stopdoc:
|
159
|
+
# Create a new OutputPort.
|
160
|
+
def initialize(run, xml)
|
161
|
+
super(run, xml)
|
162
|
+
|
163
|
+
@error = false
|
164
|
+
@structure = parse_data(xml_first_child(xml))
|
165
|
+
|
166
|
+
# cached outputs
|
167
|
+
@values = nil
|
168
|
+
@refs = nil
|
169
|
+
@types = nil
|
170
|
+
@sizes = nil
|
171
|
+
@total_size = nil
|
172
|
+
end
|
173
|
+
# :startdoc:
|
174
|
+
|
175
|
+
# :call-seq:
|
176
|
+
# error? -> bool
|
177
|
+
#
|
178
|
+
# Is there an error associated with this output port?
|
179
|
+
def error?
|
180
|
+
@error
|
181
|
+
end
|
182
|
+
|
183
|
+
# :call-seq:
|
184
|
+
# [int] -> obj
|
185
|
+
#
|
186
|
+
# This call provides access to the underlying structure of the OutputPort.
|
187
|
+
# It can only be used for ports of depth >= 1. For singleton ports, use
|
188
|
+
# OutputPort#value instead.
|
189
|
+
#
|
190
|
+
# Example usage - To get part of a value from an output port with depth 3:
|
191
|
+
# port[1][0][1].value(10...100)
|
192
|
+
def [](i)
|
193
|
+
return @structure if depth == 0
|
194
|
+
@structure[i]
|
195
|
+
end
|
196
|
+
|
197
|
+
# :call-seq:
|
198
|
+
# value -> obj
|
199
|
+
# value(range) -> obj
|
200
|
+
# value -> Array
|
201
|
+
#
|
202
|
+
# For singleton outputs get the value (or part of it). For list outputs
|
203
|
+
# get all the values in an Array structure that mirrors the structure of
|
204
|
+
# the output port. To get part of a value from a list use
|
205
|
+
# 'port[].value(range)'.
|
206
|
+
def value(range = nil)
|
207
|
+
if depth == 0
|
208
|
+
if range.nil?
|
209
|
+
@structure.value
|
210
|
+
else
|
211
|
+
@structure.value(range)
|
212
|
+
end
|
213
|
+
else
|
214
|
+
@values = strip(:value) if @values.nil?
|
215
|
+
@values
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# :call-seq:
|
220
|
+
# reference -> String
|
221
|
+
# reference -> Array
|
222
|
+
#
|
223
|
+
# Get URI references to the data values of this output port as strings.
|
224
|
+
#
|
225
|
+
# For a singleton output a single uri is returned. For lists an array of
|
226
|
+
# uris is returned. For an individual reference from a list use
|
227
|
+
# 'port[].reference'.
|
228
|
+
def reference
|
229
|
+
@refs = strip(:reference) if @refs.nil?
|
230
|
+
@refs
|
231
|
+
end
|
232
|
+
|
233
|
+
# :call-seq:
|
234
|
+
# type -> String
|
235
|
+
# type -> Array
|
236
|
+
#
|
237
|
+
# Get the mime type of the data value in this output port.
|
238
|
+
#
|
239
|
+
# For a singleton output a single type is returned. For lists an array of
|
240
|
+
# types is returned. For an individual type from a list use 'port[].type'.
|
241
|
+
def type
|
242
|
+
@types = strip(:type) if @types.nil?
|
243
|
+
@types
|
244
|
+
end
|
245
|
+
|
246
|
+
# :call-seq:
|
247
|
+
# size -> int
|
248
|
+
# size -> Array
|
249
|
+
#
|
250
|
+
# Get the data size of the data value in this output port.
|
251
|
+
#
|
252
|
+
# For a singleton output a single size is returned. For lists an array of
|
253
|
+
# sizes is returned. For an individual size from a list use 'port[].size'.
|
254
|
+
def size
|
255
|
+
@sizes = strip(:size) if @sizes.nil?
|
256
|
+
@sizes
|
257
|
+
end
|
258
|
+
|
259
|
+
# :call-seq:
|
260
|
+
# error -> String
|
261
|
+
#
|
262
|
+
# Get the error message (if there is one) of this output port.
|
263
|
+
#
|
264
|
+
# This method is only for use on outputs of depth 0. For other depths use
|
265
|
+
# 'port[].error'
|
266
|
+
def error
|
267
|
+
return nil unless depth == 0
|
268
|
+
@structure.error
|
269
|
+
end
|
270
|
+
|
271
|
+
# :call-seq:
|
272
|
+
# total_size -> int
|
273
|
+
#
|
274
|
+
# Return the total data size of all the data in this output port.
|
275
|
+
def total_size
|
276
|
+
return @total_size if @total_size
|
277
|
+
if @structure.instance_of? Array
|
278
|
+
return 0 if @structure.empty?
|
279
|
+
@total_size = strip(:size).flatten.inject { |sum, i| sum + i }
|
280
|
+
else
|
281
|
+
@total_size = size
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# :stopdoc:
|
286
|
+
def download(ref, range = nil)
|
287
|
+
@run.download_output_data("#{path(ref)}", range)
|
288
|
+
end
|
289
|
+
# :startdoc:
|
290
|
+
|
291
|
+
private
|
292
|
+
|
293
|
+
# Parse the XML port description into a raw data value structure.
|
294
|
+
def parse_data(node)
|
295
|
+
case xml_node_name(node)
|
296
|
+
when 'list'
|
297
|
+
data = []
|
298
|
+
xml_children(node) do |child|
|
299
|
+
data << parse_data(child)
|
300
|
+
end
|
301
|
+
return data
|
302
|
+
when 'value'
|
303
|
+
return PortValue.new(self, xml_node_attribute(node, 'href'), false,
|
304
|
+
xml_node_attribute(node, 'contentType'),
|
305
|
+
xml_node_attribute(node, 'contentByteLength').to_i)
|
306
|
+
when 'error'
|
307
|
+
@error = true
|
308
|
+
return PortValue.new(self, xml_node_attribute(node, 'href'), true)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# Generate the path to the actual data for a data value.
|
313
|
+
def path(ref)
|
314
|
+
parts = ref.split('/')
|
315
|
+
@depth == 0 ? parts[-1] : "/" + parts[-(@depth + 1)..-1].join('/')
|
316
|
+
end
|
317
|
+
|
318
|
+
# Strip the requested attribute from the raw values structure.
|
319
|
+
def strip(attribute, struct = @structure)
|
320
|
+
if struct.instance_of? Array
|
321
|
+
data = []
|
322
|
+
struct.each { |item| data << strip(attribute, item) }
|
323
|
+
return data
|
324
|
+
else
|
325
|
+
struct.method(attribute).call
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# A class to represent an output port data value.
|
331
|
+
class PortValue
|
332
|
+
|
333
|
+
# The URI reference of this port value as a String.
|
334
|
+
attr_reader :reference
|
335
|
+
|
336
|
+
# The mime type of this port value as a String.
|
337
|
+
attr_reader :type
|
338
|
+
|
339
|
+
# The size (in bytes) of the port value.
|
340
|
+
attr_reader :size
|
341
|
+
|
342
|
+
# :stopdoc:
|
343
|
+
def initialize(port, ref, error, type = "", size = 0)
|
344
|
+
@port = port
|
345
|
+
@reference = ref
|
346
|
+
@type = type
|
347
|
+
@size = size
|
348
|
+
@value = nil
|
349
|
+
@vgot = nil
|
350
|
+
@error = nil
|
351
|
+
|
352
|
+
if error
|
353
|
+
@error = @port.download(@reference)
|
354
|
+
@type = "error"
|
355
|
+
end
|
356
|
+
end
|
357
|
+
# :startdoc:
|
358
|
+
|
359
|
+
# :call-seq:
|
360
|
+
# value -> obj
|
361
|
+
# value(range) -> obj
|
362
|
+
#
|
363
|
+
# Return the value of this port. It is downloaded from the server if it
|
364
|
+
# has not already been retrieved. If a range is specified then just that
|
365
|
+
# portion of the value is downloaded and returned. If no range is specified
|
366
|
+
# then the whole value is downloaded and returned.
|
367
|
+
#
|
368
|
+
# All downloaded data is cached and not downloaded a second time if the
|
369
|
+
# same or similar ranges are requested.
|
370
|
+
def value(range = 0...@size)
|
371
|
+
return nil if error?
|
372
|
+
return "" if @type == "application/x-empty"
|
373
|
+
return @value if range == :debug
|
374
|
+
|
375
|
+
# check that the range provided is sensible
|
376
|
+
range = 0..range.max if range.min < 0
|
377
|
+
range = range.min...@size if range.max >= @size
|
378
|
+
|
379
|
+
need = fill(@vgot, range)
|
380
|
+
case need.length
|
381
|
+
when 0
|
382
|
+
# we already have all the data we need, just return the right bit.
|
383
|
+
# @vgot cannot be nil here and must fully encompass range.
|
384
|
+
ret_range = (range.min - @vgot.min)..(range.max - @vgot.min)
|
385
|
+
@value[ret_range]
|
386
|
+
when 1
|
387
|
+
# we either have some data, at one end of range or either side of it,
|
388
|
+
# or none. @vgot can be nil here.
|
389
|
+
# In both cases we download what we need.
|
390
|
+
new_data = @port.download(@reference, need[0])
|
391
|
+
if @vgot.nil?
|
392
|
+
# this is the only data we have, return it all.
|
393
|
+
@vgot = range
|
394
|
+
@value = new_data
|
395
|
+
else
|
396
|
+
# add the new data to the correct end of the data we have, then
|
397
|
+
# return the range requested.
|
398
|
+
if range.max <= @vgot.max
|
399
|
+
@vgot = range.min..@vgot.max
|
400
|
+
@value = new_data + @value
|
401
|
+
@value[0..range.max]
|
402
|
+
else
|
403
|
+
@vgot = @vgot.min..range.max
|
404
|
+
@value = @value + new_data
|
405
|
+
@value[(range.min - @vgot.min)..@vgot.max]
|
406
|
+
end
|
407
|
+
end
|
408
|
+
when 2
|
409
|
+
# we definitely have some data and it is in the middle of the
|
410
|
+
# range requested. @vgot cannot be nil here.
|
411
|
+
@vgot = range
|
412
|
+
@value = @port.download(@reference, need[0]) + @value +
|
413
|
+
@port.download(@reference, need[1])
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# :call-seq:
|
418
|
+
# error? -> bool
|
419
|
+
#
|
420
|
+
# Does this port represent an error?
|
421
|
+
def error?
|
422
|
+
!@error.nil?
|
423
|
+
end
|
424
|
+
|
425
|
+
# :call-seq:
|
426
|
+
# error -> string
|
427
|
+
#
|
428
|
+
# Return the error message for this value, or _nil_ if it is not an error.
|
429
|
+
def error
|
430
|
+
@error
|
431
|
+
end
|
432
|
+
|
433
|
+
# Used within #inspect, below to help override the built in version.
|
434
|
+
@@to_s = Kernel.instance_method(:to_s)
|
435
|
+
|
436
|
+
# :call-seq:
|
437
|
+
# inspect -> string
|
438
|
+
#
|
439
|
+
# Return a printable representation of this port value for debugging
|
440
|
+
# purposes.
|
441
|
+
def inspect
|
442
|
+
@@to_s.bind(self).call.sub!(/>\z/) { " @value=#{value.inspect}, " +
|
443
|
+
"@type=#{type.inspect}, @size=#{size.inspect}>"
|
444
|
+
}
|
445
|
+
end
|
446
|
+
|
447
|
+
private
|
448
|
+
def fill(got, want)
|
449
|
+
return [want] if got.nil?
|
450
|
+
|
451
|
+
if got.member? want.min
|
452
|
+
if got.member? want.max
|
453
|
+
return []
|
454
|
+
else
|
455
|
+
return [(got.max + 1)..want.max]
|
456
|
+
end
|
457
|
+
else
|
458
|
+
if got.member? want.max
|
459
|
+
return [want.min..(got.min - 1)]
|
460
|
+
else
|
461
|
+
if want.max < got.min
|
462
|
+
return [want.min..(got.min - 1)]
|
463
|
+
elsif want.min > got.max
|
464
|
+
return [(got.max + 1)..want.max]
|
465
|
+
else
|
466
|
+
return [want.min..(got.min - 1), (got.max + 1)..want.max]
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|