dor-workflow-client 3.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +1 -0
- data/.rubocop.yml +11 -0
- data/.rubocop_todo.yml +21 -0
- data/.travis.yml +10 -0
- data/.yardopts +1 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/Rakefile +16 -0
- data/bin/console +14 -0
- data/dor-workflow-client.gemspec +36 -0
- data/lib/dor/models/response/process.rb +55 -0
- data/lib/dor/models/response/update.rb +20 -0
- data/lib/dor/models/response/workflow.rb +54 -0
- data/lib/dor/workflow/client/connection_factory.rb +75 -0
- data/lib/dor/workflow/client/lifecycle_routes.rb +71 -0
- data/lib/dor/workflow/client/queues.rb +208 -0
- data/lib/dor/workflow/client/requestor.rb +48 -0
- data/lib/dor/workflow/client/version.rb +9 -0
- data/lib/dor/workflow/client/version_routes.rb +33 -0
- data/lib/dor/workflow/client/workflow_routes.rb +192 -0
- data/lib/dor/workflow/client.rb +77 -0
- data/lib/dor/workflow_exception.rb +6 -0
- data/spec/models/response/workflow_spec.rb +142 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/workflow/client/connection_factory_spec.rb +25 -0
- data/spec/workflow/client/lifecycle_routes_spec.rb +27 -0
- data/spec/workflow/client/requestor_spec.rb +33 -0
- data/spec/workflow/client/workflow_routes_spec.rb +53 -0
- data/spec/workflow/client_spec.rb +633 -0
- metadata +309 -0
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dor
|
4
|
+
module Workflow
|
5
|
+
class Client
|
6
|
+
# Makes requests relating to the workflow queues
|
7
|
+
class Queues
|
8
|
+
def initialize(requestor:)
|
9
|
+
@requestor = requestor
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns all the distinct laneIds for a given workflow step
|
13
|
+
#
|
14
|
+
# @param [String] repo The repository the object resides in. The service recoginzes "dor" and "sdr" at the moment
|
15
|
+
# @param [String] workflow name
|
16
|
+
# @param [String] process name
|
17
|
+
# @return [Array<String>] all of the distinct laneIds. Array will be empty if no lane ids were found
|
18
|
+
def lane_ids(repo, workflow, process)
|
19
|
+
uri = "workflow_queue/lane_ids?step=#{repo}:#{workflow}:#{process}"
|
20
|
+
doc = Nokogiri::XML(requestor.request(uri))
|
21
|
+
nodes = doc.xpath('/lanes/lane')
|
22
|
+
nodes.map { |n| n['id'] }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Gets all of the workflow steps that have a status of 'queued' that have a last-updated timestamp older than the number of hours passed in
|
26
|
+
# This will enable re-queueing of jobs that have been lost by the job manager
|
27
|
+
# @param [String] repository name of the repository you want to query, like 'dor' or 'sdr'
|
28
|
+
# @param [Hash] opts optional values for query
|
29
|
+
# @option opts [Integer] :hours_ago steps older than this value will be returned by the query. If not passed in, the service defaults to 0 hours,
|
30
|
+
# meaning you will get all queued workflows
|
31
|
+
# @option opts [Integer] :limit sets the maximum number of workflow steps that can be returned. Defaults to no limit
|
32
|
+
# @return [Array[Hash]] each Hash represents a workflow step. It will have the following keys:
|
33
|
+
# :workflow, :step, :druid, :lane_id
|
34
|
+
def stale_queued_workflows(repository, opts = {})
|
35
|
+
uri_string = build_queued_uri(repository, opts)
|
36
|
+
parse_queued_workflows_response requestor.request(uri_string)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a count of workflow steps that have a status of 'queued' that have a last-updated timestamp older than the number of hours passed in
|
40
|
+
# @param [String] repository name of the repository you want to query, like 'dor' or 'sdr'
|
41
|
+
# @param [Hash] opts optional values for query
|
42
|
+
# @option opts [Integer] :hours_ago steps older than this value will be returned by the query. If not passed in, the service defaults to 0 hours,
|
43
|
+
# meaning you will get all queued workflows
|
44
|
+
# @return [Integer] number of stale, queued steps if the :count_only option was set to true
|
45
|
+
def count_stale_queued_workflows(repository, opts = {})
|
46
|
+
uri_string = build_queued_uri(repository, opts) + '&count-only=true'
|
47
|
+
doc = Nokogiri::XML(requestor.request(uri_string))
|
48
|
+
doc.at_xpath('/objects/@count').value.to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a list of druids from the workflow service that meet the criteria
|
52
|
+
# of the passed in completed and waiting params
|
53
|
+
#
|
54
|
+
# @param [Array<String>, String] completed An array or single String of the completed steps, should use the qualified format: `repository:workflow:step-name`
|
55
|
+
# @param [String] waiting name of the waiting step
|
56
|
+
# @param [String] repository default repository to use if it isn't passed in the qualified-step-name
|
57
|
+
# @param [String] workflow default workflow to use if it isn't passed in the qualified-step-name
|
58
|
+
# @param [String] lane_id issue a query for a specific lane_id for the waiting step
|
59
|
+
# @param [Hash] options
|
60
|
+
# @param options [String] :default_repository repository to query for if not using the qualified format
|
61
|
+
# @param options [String] :default_workflow workflow to query for if not using the qualified format
|
62
|
+
# @option options [Integer] :limit maximum number of druids to return (nil for no limit)
|
63
|
+
# @return [Array<String>] Array of druids
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# objects_for_workstep(...)
|
67
|
+
# => [
|
68
|
+
# "druid:py156ps0477",
|
69
|
+
# "druid:tt628cb6479",
|
70
|
+
# "druid:ct021wp7863"
|
71
|
+
# ]
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# objects_for_workstep(..., "lane1")
|
75
|
+
# => {
|
76
|
+
# "druid:py156ps0477",
|
77
|
+
# "druid:tt628cb6479",
|
78
|
+
# }
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# objects_for_workstep(..., "lane1", limit: 1)
|
82
|
+
# => {
|
83
|
+
# "druid:py156ps0477",
|
84
|
+
# }
|
85
|
+
#
|
86
|
+
def objects_for_workstep(completed, waiting, lane_id = 'default', options = {})
|
87
|
+
waiting_param = qualify_step(options[:default_repository], options[:default_workflow], waiting)
|
88
|
+
uri_string = "workflow_queue?waiting=#{waiting_param}"
|
89
|
+
if completed
|
90
|
+
Array(completed).each do |step|
|
91
|
+
completed_param = qualify_step(options[:default_repository], options[:default_workflow], step)
|
92
|
+
uri_string += "&completed=#{completed_param}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
uri_string += "&limit=#{options[:limit].to_i}" if options[:limit]&.to_i&.positive?
|
97
|
+
uri_string += "&lane-id=#{lane_id}"
|
98
|
+
|
99
|
+
resp = requestor.request uri_string
|
100
|
+
#
|
101
|
+
# response looks like:
|
102
|
+
# <objects count="2">
|
103
|
+
# <object id="druid:ab123de4567"/>
|
104
|
+
# <object id="druid:ab123de9012"/>
|
105
|
+
# </objects>
|
106
|
+
#
|
107
|
+
# convert into:
|
108
|
+
# ['druid:ab123de4567', 'druid:ab123de9012']
|
109
|
+
#
|
110
|
+
result = Nokogiri::XML(resp).xpath('//object[@id]')
|
111
|
+
result.map { |n| n[:id] }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get a list of druids that have errored out in a particular workflow and step
|
115
|
+
#
|
116
|
+
# @param [String] workflow name
|
117
|
+
# @param [String] step name
|
118
|
+
# @param [String] repository -- optional, default=dor
|
119
|
+
#
|
120
|
+
# @return [Hash] hash of results, with key has a druid, and value as the error message
|
121
|
+
# @example
|
122
|
+
# client.errored_objects_for_workstep('accessionWF','content-metadata')
|
123
|
+
# => {"druid:qd556jq0580"=>"druid:qd556jq0580 - Item error; caused by
|
124
|
+
# #<Rubydora::FedoraInvalidRequest: Error modifying datastream contentMetadata for druid:qd556jq0580. See logger for details>"}
|
125
|
+
def errored_objects_for_workstep(workflow, step, repository = 'dor')
|
126
|
+
resp = requestor.request "workflow_queue?repository=#{repository}&workflow=#{workflow}&error=#{step}"
|
127
|
+
result = {}
|
128
|
+
Nokogiri::XML(resp).xpath('//object').collect do |node|
|
129
|
+
result.merge!(node['id'] => node['errorMessage'])
|
130
|
+
end
|
131
|
+
result
|
132
|
+
end
|
133
|
+
|
134
|
+
# Used by preservation robots stats reporter
|
135
|
+
def count_objects_in_step(workflow, step, type, repo)
|
136
|
+
resp = requestor.request "workflow_queue?repository=#{repo}&workflow=#{workflow}&#{type}=#{step}"
|
137
|
+
extract_object_count(resp)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns the number of objects that have a status of 'error' in a particular workflow and step
|
141
|
+
#
|
142
|
+
# @param [String] workflow name
|
143
|
+
# @param [String] step name
|
144
|
+
# @param [String] repository -- optional, default=dor
|
145
|
+
#
|
146
|
+
# @return [Integer] Number of objects with this repository:workflow:step that have a status of 'error'
|
147
|
+
def count_errored_for_workstep(workflow, step, repository = 'dor')
|
148
|
+
count_objects_in_step(workflow, step, 'error', repository)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Returns the number of objects that have a status of 'queued' in a particular workflow and step
|
152
|
+
#
|
153
|
+
# @param [String] workflow name
|
154
|
+
# @param [String] step name
|
155
|
+
# @param [String] repository -- optional, default=dor
|
156
|
+
#
|
157
|
+
# @return [Integer] Number of objects with this repository:workflow:step that have a status of 'queued'
|
158
|
+
def count_queued_for_workstep(workflow, step, repository = 'dor')
|
159
|
+
count_objects_in_step(workflow, step, 'queued', repository)
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
attr_reader :requestor
|
165
|
+
|
166
|
+
def build_queued_uri(repository, opts = {})
|
167
|
+
uri_string = "workflow_queue/all_queued?repository=#{repository}"
|
168
|
+
uri_string += "&hours-ago=#{opts[:hours_ago]}" if opts[:hours_ago]
|
169
|
+
uri_string += "&limit=#{opts[:limit]}" if opts[:limit]
|
170
|
+
uri_string
|
171
|
+
end
|
172
|
+
|
173
|
+
# Converts repo-workflow-step into repo:workflow:step
|
174
|
+
# @param [String] default_repository
|
175
|
+
# @param [String] default_workflow
|
176
|
+
# @param [String] step if contains colon :, then the value for workflow and/or workflow/repository. For example: 'jp2-create', 'assemblyWF:jp2-create' or 'dor:assemblyWF:jp2-create'
|
177
|
+
# @return [String] repo:workflow:step
|
178
|
+
# @example
|
179
|
+
# dor:assemblyWF:jp2-create
|
180
|
+
def qualify_step(default_repository, default_workflow, step)
|
181
|
+
current = step.split(/:/, 3)
|
182
|
+
current.unshift(default_workflow) if current.length < 3
|
183
|
+
current.unshift(default_repository) if current.length < 3
|
184
|
+
current.join(':')
|
185
|
+
end
|
186
|
+
|
187
|
+
def parse_queued_workflows_response(xml)
|
188
|
+
doc = Nokogiri::XML(xml)
|
189
|
+
doc.xpath('/workflows/workflow').collect do |wf_node|
|
190
|
+
{
|
191
|
+
workflow: wf_node['name'],
|
192
|
+
step: wf_node['process'],
|
193
|
+
druid: wf_node['druid'],
|
194
|
+
lane_id: wf_node['laneId']
|
195
|
+
}
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def extract_object_count(resp)
|
200
|
+
node = Nokogiri::XML(resp).at_xpath('/objects')
|
201
|
+
raise Dor::WorkflowException, 'Unable to determine count from response' if node.nil?
|
202
|
+
|
203
|
+
node['count'].to_i
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dor
|
4
|
+
module Workflow
|
5
|
+
class Client
|
6
|
+
# Makes requests to the workflow service and retries them if necessary.
|
7
|
+
class Requestor
|
8
|
+
def initialize(connection:)
|
9
|
+
@connection = connection
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :connection
|
13
|
+
|
14
|
+
# calls workflow_resource[uri_string]."#{meth}" with variable number of optional arguments
|
15
|
+
# The point of this is to wrap ALL remote calls with consistent error handling and logging
|
16
|
+
# @param [String] uri_string resource to request
|
17
|
+
# @param [String] meth REST method to use on resource (get, put, post, delete, etc.)
|
18
|
+
# @param [String] payload body for (e.g. put) request
|
19
|
+
# @param [Hash] opts addtional headers options
|
20
|
+
# @return [Object] response from method
|
21
|
+
def request(uri_string, meth = 'get', payload = '', opts = {})
|
22
|
+
response = send_workflow_resource_request(uri_string, meth, payload, opts)
|
23
|
+
response.body
|
24
|
+
rescue Faraday::Error => e
|
25
|
+
msg = "Failed to retrieve resource: #{meth} #{base_url}/#{uri_string}"
|
26
|
+
msg += " (HTTP status #{e.response[:status]})" if e.respond_to?(:response) && e.response
|
27
|
+
raise Dor::WorkflowException, msg
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
##
|
33
|
+
# Get the configured URL for the connection
|
34
|
+
def base_url
|
35
|
+
connection.url_prefix
|
36
|
+
end
|
37
|
+
|
38
|
+
def send_workflow_resource_request(uri_string, meth = 'get', payload = '', opts = {})
|
39
|
+
connection.public_send(meth, uri_string) do |req|
|
40
|
+
req.body = payload unless meth == 'delete'
|
41
|
+
req.params.update opts[:params] if opts[:params]
|
42
|
+
req.headers.update opts.except(:params)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dor
|
4
|
+
module Workflow
|
5
|
+
class Client
|
6
|
+
# Makes requests relating to versions
|
7
|
+
class VersionRoutes
|
8
|
+
def initialize(requestor:)
|
9
|
+
@requestor = requestor
|
10
|
+
end
|
11
|
+
|
12
|
+
# Calls the versionClose endpoint of the workflow service:
|
13
|
+
#
|
14
|
+
# - completes the versioningWF:submit-version and versioningWF:start-accession steps
|
15
|
+
# - initiates accesssionWF
|
16
|
+
#
|
17
|
+
# @param [String] repo The repository the object resides in. The service recoginzes "dor" and "sdr" at the moment
|
18
|
+
# @param [String] druid The id of the object to delete the workflow from
|
19
|
+
# @param [Boolean] create_accession_wf Option to create accessionWF when closing a version. Defaults to true
|
20
|
+
def close_version(repo, druid, create_accession_wf = true)
|
21
|
+
uri = "#{repo}/objects/#{druid}/versionClose"
|
22
|
+
uri += '?create-accession=false' unless create_accession_wf
|
23
|
+
requestor.request(uri, 'post', '')
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :requestor
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dor
|
4
|
+
module Workflow
|
5
|
+
class Client
|
6
|
+
# Makes requests relating to a workflow
|
7
|
+
class WorkflowRoutes
|
8
|
+
def initialize(requestor:)
|
9
|
+
@requestor = requestor
|
10
|
+
end
|
11
|
+
|
12
|
+
# Creates a workflow for a given object in the repository. If this particular workflow for this objects exists,
|
13
|
+
# it will replace the old workflow with wf_xml passed to this method. You have the option of creating a datastream or not.
|
14
|
+
# Returns true on success. Caller must handle any exceptions
|
15
|
+
#
|
16
|
+
# @param [String] repo The repository the object resides in. The service recoginzes "dor" and "sdr" at the moment
|
17
|
+
# @param [String] druid The id of the object
|
18
|
+
# @param [String] workflow_name The name of the workflow you want to create
|
19
|
+
# @param [String] wf_xml The xml that represents the workflow
|
20
|
+
# @param [Hash] opts optional params
|
21
|
+
# @option opts [Boolean] :create_ds if true, a workflow datastream will be created in Fedora. Set to false if you do not want a datastream to be created
|
22
|
+
# If you do not pass in an <b>opts</b> Hash, then :create_ds is set to true by default
|
23
|
+
# @option opts [String] :lane_id adds laneId attribute to all process elements in the wf_xml workflow xml. Defaults to a value of 'default'
|
24
|
+
# @return [Boolean] always true
|
25
|
+
#
|
26
|
+
def create_workflow(repo, druid, workflow_name, wf_xml, opts = { create_ds: true })
|
27
|
+
lane_id = opts.fetch(:lane_id, 'default')
|
28
|
+
xml = add_lane_id_to_workflow_xml(lane_id, wf_xml)
|
29
|
+
_status = requestor.request "#{repo}/objects/#{druid}/workflows/#{workflow_name}", 'put', xml,
|
30
|
+
content_type: 'application/xml',
|
31
|
+
params: { 'create-ds' => opts[:create_ds] }
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
# Updates the status of one step in a workflow.
|
36
|
+
# Returns true on success. Caller must handle any exceptions
|
37
|
+
#
|
38
|
+
# @param [String] repo The repository the object resides in. The service recoginzes "dor" and "sdr" at the moment
|
39
|
+
# @param [String] druid The id of the object
|
40
|
+
# @param [String] workflow The name of the workflow
|
41
|
+
# @param [String] process The name of the process step
|
42
|
+
# @param [String] status The status that you want to set -- using one of the values in VALID_STATUS
|
43
|
+
# @param [Hash] opts optional values for the workflow step
|
44
|
+
# @option opts [Float] :elapsed The number of seconds it took to complete this step. Can have a decimal. Is set to 0 if not passed in.
|
45
|
+
# @option opts [String] :lifecycle Bookeeping label for this particular workflow step. Examples are: 'registered', 'shelved'
|
46
|
+
# @option opts [String] :note Any kind of string annotation that you want to attach to the workflow
|
47
|
+
# @option opts [String] :lane_id Id of processing lane used by the job manager. Can convey priority or name of an applicaiton specific processing lane (e.g. 'high', 'critical', 'hydrus')
|
48
|
+
# @option opts [String] :current_status Setting this string tells the workflow service to compare the current status to this value. If the current value does not match this value, the update is not performed
|
49
|
+
# @return [Boolean] always true
|
50
|
+
# Http Call
|
51
|
+
# ==
|
52
|
+
# The method does an HTTP PUT to the URL defined in `Dor::WF_URI`. As an example:
|
53
|
+
#
|
54
|
+
# PUT "/dor/objects/pid:123/workflows/GoogleScannedWF/convert"
|
55
|
+
# <process name=\"convert\" status=\"completed\" />"
|
56
|
+
def update_workflow_status(repo, druid, workflow, process, status, opts = {})
|
57
|
+
raise ArgumentError, "Unknown status value #{status}" unless VALID_STATUS.include?(status.downcase)
|
58
|
+
|
59
|
+
opts = { elapsed: 0, lifecycle: nil, note: nil }.merge!(opts)
|
60
|
+
opts[:elapsed] = opts[:elapsed].to_s
|
61
|
+
current_status = opts.delete(:current_status)
|
62
|
+
xml = create_process_xml({ name: process, status: status.downcase }.merge!(opts))
|
63
|
+
uri = "#{repo}/objects/#{druid}/workflows/#{workflow}/#{process}"
|
64
|
+
uri += "?current-status=#{current_status.downcase}" if current_status
|
65
|
+
response = requestor.request(uri, 'put', xml, content_type: 'application/xml')
|
66
|
+
|
67
|
+
Workflow::Response::Update.new(json: response)
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Retrieves the process status of the given workflow for the given object identifier
|
72
|
+
# @param [String] repo The repository the object resides in. Currently recoginzes "dor" and "sdr".
|
73
|
+
# @param [String] druid The id of the object
|
74
|
+
# @param [String] workflow The name of the workflow
|
75
|
+
# @param [String] process The name of the process step
|
76
|
+
# @return [String] status for repo-workflow-process-druid
|
77
|
+
def workflow_status(repo, druid, workflow, process)
|
78
|
+
workflow_md = workflow_xml(repo, druid, workflow)
|
79
|
+
doc = Nokogiri::XML(workflow_md)
|
80
|
+
raise Dor::WorkflowException, "Unable to parse response:\n#{workflow_md}" if doc.root.nil?
|
81
|
+
|
82
|
+
processes = doc.root.xpath("//process[@name='#{process}']")
|
83
|
+
process = processes.max { |a, b| a.attr('version').to_i <=> b.attr('version').to_i }
|
84
|
+
process&.attr('status')
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Retrieves the raw XML for the given workflow
|
89
|
+
# @param [String] repo The repository the object resides in. Currently recoginzes "dor" and "sdr".
|
90
|
+
# @param [String] druid The id of the object
|
91
|
+
# @param [String] workflow The name of the workflow
|
92
|
+
# @return [String] XML of the workflow
|
93
|
+
def workflow_xml(repo, druid, workflow)
|
94
|
+
raise ArgumentError, 'missing workflow' unless workflow
|
95
|
+
|
96
|
+
requestor.request "#{repo}/objects/#{druid}/workflows/#{workflow}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Updates the status of one step in a workflow to error.
|
100
|
+
# Returns true on success. Caller must handle any exceptions
|
101
|
+
#
|
102
|
+
# @param [String] repo The repository the object resides in. The service recoginzes "dor" and "sdr" at the moment
|
103
|
+
# @param [String] druid The id of the object
|
104
|
+
# @param [String] workflow The name of the workflow
|
105
|
+
# @param [String] error_msg The error message. Ideally, this is a brief message describing the error
|
106
|
+
# @param [Hash] opts optional values for the workflow step
|
107
|
+
# @option opts [String] :error_text A slot to hold more information about the error, like a full stacktrace
|
108
|
+
# @return [Boolean] always true
|
109
|
+
#
|
110
|
+
# Http Call
|
111
|
+
# ==
|
112
|
+
# The method does an HTTP PUT to the URL defined in `Dor::WF_URI`.
|
113
|
+
#
|
114
|
+
# PUT "/dor/objects/pid:123/workflows/GoogleScannedWF/convert"
|
115
|
+
# <process name=\"convert\" status=\"error\" />"
|
116
|
+
def update_workflow_error_status(repo, druid, workflow, process, error_msg, opts = {})
|
117
|
+
opts = { error_text: nil }.merge!(opts)
|
118
|
+
xml = create_process_xml({ name: process, status: 'error', errorMessage: error_msg }.merge!(opts))
|
119
|
+
requestor.request "#{repo}/objects/#{druid}/workflows/#{workflow}/#{process}", 'put', xml, content_type: 'application/xml'
|
120
|
+
true
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Retrieves the raw XML for all the workflows for the the given object
|
125
|
+
# @param [String] druid The id of the object
|
126
|
+
# @return [String] XML of the workflow
|
127
|
+
def all_workflows_xml(druid)
|
128
|
+
requestor.request "objects/#{druid}/workflows"
|
129
|
+
end
|
130
|
+
|
131
|
+
# Get workflow names into an array for given PID
|
132
|
+
# This method only works when this gem is used in a project that is configured to connect to DOR
|
133
|
+
#
|
134
|
+
# @param [String] pid of druid
|
135
|
+
# @param [String] repo repository for the object
|
136
|
+
# @return [Array<String>] list of worklows
|
137
|
+
# @example
|
138
|
+
# client.workflows('druid:sr100hp0609')
|
139
|
+
# => ["accessionWF", "assemblyWF", "disseminationWF"]
|
140
|
+
def workflows(pid, repo = 'dor')
|
141
|
+
xml_doc = Nokogiri::XML(workflow_xml(repo, pid, ''))
|
142
|
+
xml_doc.xpath('//workflow').collect { |workflow| workflow['id'] }
|
143
|
+
end
|
144
|
+
|
145
|
+
# @param [String] repo repository of the object
|
146
|
+
# @param [String] pid id of object
|
147
|
+
# @param [String] workflow_name The name of the workflow
|
148
|
+
# @return [Workflow::Response::Workflow]
|
149
|
+
def workflow(repo: 'dor', pid:, workflow_name:)
|
150
|
+
xml = workflow_xml(repo, pid, workflow_name)
|
151
|
+
Workflow::Response::Workflow.new(xml: xml)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Deletes a workflow from a particular repository and druid
|
155
|
+
# @param [String] repo The repository the object resides in. The service recoginzes "dor" and "sdr" at the moment
|
156
|
+
# @param [String] druid The id of the object to delete the workflow from
|
157
|
+
# @param [String] workflow The name of the workflow to be deleted
|
158
|
+
# @return [Boolean] always true
|
159
|
+
def delete_workflow(repo, druid, workflow)
|
160
|
+
requestor.request "#{repo}/objects/#{druid}/workflows/#{workflow}", 'delete'
|
161
|
+
true
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
attr_reader :requestor
|
167
|
+
|
168
|
+
# Adds laneId attributes to each process of workflow xml
|
169
|
+
#
|
170
|
+
# @param [String] lane_id to add to each process element
|
171
|
+
# @param [String] wf_xml the workflow xml
|
172
|
+
# @return [String] wf_xml with lane_id attributes
|
173
|
+
def add_lane_id_to_workflow_xml(lane_id, wf_xml)
|
174
|
+
doc = Nokogiri::XML(wf_xml)
|
175
|
+
doc.xpath('/workflow/process').each { |proc| proc['laneId'] = lane_id }
|
176
|
+
doc.to_xml
|
177
|
+
end
|
178
|
+
|
179
|
+
# @param [Hash] params
|
180
|
+
# @return [String]
|
181
|
+
def create_process_xml(params)
|
182
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
183
|
+
attrs = params.reject { |_k, v| v.nil? }
|
184
|
+
attrs = Hash[attrs.map { |k, v| [k.to_s.camelize(:lower), v] }] # camelize all the keys in the attrs hash
|
185
|
+
xml.process(attrs)
|
186
|
+
end
|
187
|
+
builder.to_xml
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'dor/workflow_exception'
|
7
|
+
require 'dor/models/response/workflow'
|
8
|
+
require 'dor/models/response/update'
|
9
|
+
|
10
|
+
require 'dor/workflow/client/connection_factory'
|
11
|
+
require 'dor/workflow/client/lifecycle_routes'
|
12
|
+
require 'dor/workflow/client/queues'
|
13
|
+
require 'dor/workflow/client/requestor'
|
14
|
+
require 'dor/workflow/client/version_routes'
|
15
|
+
require 'dor/workflow/client/workflow_routes'
|
16
|
+
|
17
|
+
module Dor
|
18
|
+
module Workflow
|
19
|
+
# TODO: VALID_STATUS should be just another attribute w/ default
|
20
|
+
#
|
21
|
+
# Create and update workflows
|
22
|
+
class Client
|
23
|
+
# From Workflow Service's admin/Process.java
|
24
|
+
VALID_STATUS = %w[waiting completed error queued skipped hold].freeze
|
25
|
+
|
26
|
+
attr_accessor :requestor
|
27
|
+
|
28
|
+
# Configure the workflow service
|
29
|
+
# @param [String] :url points to the workflow service
|
30
|
+
# @param [Logger] :logger defaults writing to workflow_service.log with weekly rotation
|
31
|
+
# @param [Integer] :timeout number of seconds for HTTP timeout
|
32
|
+
# @param [Faraday::Connection] :connection the REST client resource
|
33
|
+
def initialize(url: nil, logger: default_logger, timeout: nil, connection: nil)
|
34
|
+
raise ArgumentError, 'You must provide either a connection or a url' if !url && !connection
|
35
|
+
|
36
|
+
@requestor = Requestor.new(connection: connection || ConnectionFactory.build_connection(url, timeout: timeout, logger: logger))
|
37
|
+
end
|
38
|
+
|
39
|
+
delegate :create_workflow, :update_workflow_status, :workflow_status, :workflow_xml,
|
40
|
+
:update_workflow_error_status, :all_workflows_xml, :workflows,
|
41
|
+
:workflow, :delete_workflow, to: :workflow_routes
|
42
|
+
|
43
|
+
delegate :lifecycle, :active_lifecycle, :milestones, to: :lifecycle_routes
|
44
|
+
|
45
|
+
delegate :lane_ids, :stale_queued_workflows, :count_stale_queued_workflows,
|
46
|
+
:objects_for_workstep, :errored_objects_for_workstep, :count_objects_in_step,
|
47
|
+
:count_errored_for_workstep, :count_queued_for_workstep,
|
48
|
+
to: :queues
|
49
|
+
|
50
|
+
delegate :close_version, to: :version_routes
|
51
|
+
|
52
|
+
def queues
|
53
|
+
@queues ||= Queues.new(requestor: requestor)
|
54
|
+
end
|
55
|
+
|
56
|
+
def workflow_routes
|
57
|
+
@workflow_routes ||= WorkflowRoutes.new(requestor: requestor)
|
58
|
+
end
|
59
|
+
|
60
|
+
def lifecycle_routes
|
61
|
+
@lifecycle_routes ||= LifecycleRoutes.new(requestor: requestor)
|
62
|
+
end
|
63
|
+
|
64
|
+
def version_routes
|
65
|
+
@version_routes ||= VersionRoutes.new(requestor: requestor)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Among other things, a distinct method helps tests mock default logger
|
71
|
+
# @return [Logger] default logger object
|
72
|
+
def default_logger
|
73
|
+
Logger.new('workflow_service.log', 'weekly')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|