line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package JIRA::Client; |
2
|
|
|
|
|
|
|
# ABSTRACT: (DEPRECATED) Extended interface to JIRA's SOAP API |
3
|
|
|
|
|
|
|
$JIRA::Client::VERSION = '0.43'; |
4
|
2
|
|
|
2
|
|
54125
|
use strict; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
109
|
|
5
|
2
|
|
|
2
|
|
13
|
use warnings; |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
86
|
|
6
|
|
|
|
|
|
|
|
7
|
2
|
|
|
2
|
|
12
|
use Carp; |
|
2
|
|
|
|
|
10
|
|
|
2
|
|
|
|
|
228
|
|
8
|
2
|
|
|
2
|
|
1458
|
use Data::Util qw(:check); |
|
2
|
|
|
|
|
2091
|
|
|
2
|
|
|
|
|
430
|
|
9
|
2
|
|
|
2
|
|
1245
|
use SOAP::Lite; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
sub new { |
13
|
|
|
|
|
|
|
my ($class, @args) = @_; |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
my $args; |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
if (@args == 1) { |
18
|
|
|
|
|
|
|
$args = shift @args; |
19
|
|
|
|
|
|
|
is_hash_ref($args) or croak "$class::new sole argument must be a hash-ref.\n"; |
20
|
|
|
|
|
|
|
foreach my $arg (qw/baseurl user password/) { |
21
|
|
|
|
|
|
|
exists $args->{$arg} |
22
|
|
|
|
|
|
|
or croak "Missing $arg key to $class::new hash argument.\n"; |
23
|
|
|
|
|
|
|
} |
24
|
|
|
|
|
|
|
$args->{soapargs} = [] unless exists $args->{soapargs}; |
25
|
|
|
|
|
|
|
} elsif (@args >= 3) { |
26
|
|
|
|
|
|
|
my $baseurl = shift @args; |
27
|
|
|
|
|
|
|
my $user = shift @args; |
28
|
|
|
|
|
|
|
my $password = shift @args; |
29
|
|
|
|
|
|
|
$args = { |
30
|
|
|
|
|
|
|
baseurl => $baseurl, |
31
|
|
|
|
|
|
|
user => $user, |
32
|
|
|
|
|
|
|
password => $password, |
33
|
|
|
|
|
|
|
soapargs => \@args, |
34
|
|
|
|
|
|
|
}; |
35
|
|
|
|
|
|
|
} else { |
36
|
|
|
|
|
|
|
croak "Invalid number of arguments to $class::new.\n"; |
37
|
|
|
|
|
|
|
} |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
$args->{wsdl} = '/rpc/soap/jirasoapservice-v2?wsdl' unless exists $args->{wsdl}; |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
my $url = $args->{baseurl}; |
42
|
|
|
|
|
|
|
$url =~ s{/$}{}; # clean trailing slash |
43
|
|
|
|
|
|
|
$url .= $args->{wsdl}; |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
my $soap = SOAP::Lite->proxy($url, @{$args->{soapargs}}); |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
# Make all scalars be encoded as strings by default. |
48
|
|
|
|
|
|
|
$soap->typelookup({default => [0, sub {1}, 'as_string']}); |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
my $auth = $soap->login($args->{user}, $args->{password}); |
51
|
|
|
|
|
|
|
croak $auth->faultcode(), ', ', $auth->faultstring() |
52
|
|
|
|
|
|
|
if defined $auth->fault(); |
53
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
my $auth_result = $auth->result() |
55
|
|
|
|
|
|
|
or croak "Unknown error while connecting to JIRA. Please, check the URL.\n"; |
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
my $self = { |
58
|
|
|
|
|
|
|
soap => $soap, |
59
|
|
|
|
|
|
|
auth => $auth_result, |
60
|
|
|
|
|
|
|
iter => undef, |
61
|
|
|
|
|
|
|
cache => { |
62
|
|
|
|
|
|
|
components => {}, # project_key => {name => RemoteComponent} |
63
|
|
|
|
|
|
|
versions => {}, # project_key => {name => RemoteVersion} |
64
|
|
|
|
|
|
|
}, |
65
|
|
|
|
|
|
|
}; |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
return bless $self, $class; |
68
|
|
|
|
|
|
|
} |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
# This empty DESTROY is necessary because we're using AUTOLOAD. |
71
|
|
|
|
|
|
|
# http://www.perlmonks.org/?node_id=93045 |
72
|
|
|
|
|
|
|
sub DESTROY { } |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
# The issue "https://jira.atlassian.com/browse/JRA-12300" explains why |
75
|
|
|
|
|
|
|
# some fields in JIRA have nonintuitive names. Here we map them. |
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
my %JRA12300 = ( |
78
|
|
|
|
|
|
|
affectsVersions => 'versions', |
79
|
|
|
|
|
|
|
type => 'issuetype', |
80
|
|
|
|
|
|
|
); |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
my %JRA12300_backwards = reverse %JRA12300; |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
# These are some helper functions to convert names into ids. |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
sub _convert_type { |
87
|
|
|
|
|
|
|
my ($self, $type) = @_; |
88
|
|
|
|
|
|
|
if ($type =~ /\D/) { |
89
|
|
|
|
|
|
|
my $types = $self->get_issue_types(); |
90
|
|
|
|
|
|
|
return $types->{$type}{id} if exists $types->{$type}; |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
$types = $self->get_subtask_issue_types(); |
93
|
|
|
|
|
|
|
return $types->{$type}{id} if exists $types->{$type}; |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
croak "There is no issue type called '$type'.\n"; |
96
|
|
|
|
|
|
|
} |
97
|
|
|
|
|
|
|
return $type; |
98
|
|
|
|
|
|
|
} |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
sub _convert_priority { |
101
|
|
|
|
|
|
|
my ($self, $prio) = @_; |
102
|
|
|
|
|
|
|
if ($prio =~ /\D/) { |
103
|
|
|
|
|
|
|
my $prios = $self->get_priorities(); |
104
|
|
|
|
|
|
|
croak "There is no priority called '$prio'.\n" |
105
|
|
|
|
|
|
|
unless exists $prios->{$prio}; |
106
|
|
|
|
|
|
|
return $prios->{$prio}{id}; |
107
|
|
|
|
|
|
|
} |
108
|
|
|
|
|
|
|
return $prio; |
109
|
|
|
|
|
|
|
} |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
sub _convert_resolution { |
112
|
|
|
|
|
|
|
my ($self, $resolution) = @_; |
113
|
|
|
|
|
|
|
if ($resolution =~ /\D/) { |
114
|
|
|
|
|
|
|
my $resolutions = $self->get_resolutions(); |
115
|
|
|
|
|
|
|
croak "There is no resolution called '$resolution'.\n" |
116
|
|
|
|
|
|
|
unless exists $resolutions->{$resolution}; |
117
|
|
|
|
|
|
|
return $resolutions->{$resolution}{id}; |
118
|
|
|
|
|
|
|
} |
119
|
|
|
|
|
|
|
return $resolution; |
120
|
|
|
|
|
|
|
} |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
sub _convert_security_level { |
123
|
|
|
|
|
|
|
my ($self, $seclevel, $project) = @_; |
124
|
|
|
|
|
|
|
if ($seclevel =~ /\D/) { |
125
|
|
|
|
|
|
|
my $seclevels = $self->get_security_levels($project); |
126
|
|
|
|
|
|
|
croak "There is no security level called '$seclevel'.\n" |
127
|
|
|
|
|
|
|
unless exists $seclevels->{$seclevel}; |
128
|
|
|
|
|
|
|
return $seclevels->{$seclevel}{id}; |
129
|
|
|
|
|
|
|
} |
130
|
|
|
|
|
|
|
return $seclevel; |
131
|
|
|
|
|
|
|
} |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
# This routine receives an array with a list of $components specified |
134
|
|
|
|
|
|
|
# by RemoteComponent objects, names, and ids. It returns an array of |
135
|
|
|
|
|
|
|
# RemoteComponent objects. |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
sub _convert_components { |
138
|
|
|
|
|
|
|
my ($self, $components, $project) = @_; |
139
|
|
|
|
|
|
|
is_array_ref($components) or croak "The 'components' value must be an ARRAY ref.\n"; |
140
|
|
|
|
|
|
|
my @converted; |
141
|
|
|
|
|
|
|
my $pcomponents; # project components |
142
|
|
|
|
|
|
|
foreach my $component (@{$components}) { |
143
|
|
|
|
|
|
|
if (is_instance($component => 'RemoteComponent')) { |
144
|
|
|
|
|
|
|
push @converted, $component; |
145
|
|
|
|
|
|
|
} elsif (is_integer($component)) { |
146
|
|
|
|
|
|
|
push @converted, RemoteComponent->new($component); |
147
|
|
|
|
|
|
|
} else { |
148
|
|
|
|
|
|
|
# It's a component name. Let us convert it into its id. |
149
|
|
|
|
|
|
|
croak "Cannot convert component names because I don't know for which project.\n" |
150
|
|
|
|
|
|
|
unless $project; |
151
|
|
|
|
|
|
|
$pcomponents = $self->get_components($project) unless defined $pcomponents; |
152
|
|
|
|
|
|
|
croak "There is no component called '$component'.\n" |
153
|
|
|
|
|
|
|
unless exists $pcomponents->{$component}; |
154
|
|
|
|
|
|
|
push @converted, RemoteComponent->new($pcomponents->{$component}{id}); |
155
|
|
|
|
|
|
|
} |
156
|
|
|
|
|
|
|
} |
157
|
|
|
|
|
|
|
return \@converted; |
158
|
|
|
|
|
|
|
} |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
# This routine receives an array with a list of $versions specified by |
161
|
|
|
|
|
|
|
# RemoteVersion objects, names, and ids. It returns an array of |
162
|
|
|
|
|
|
|
# RemoteVersion objects. |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
sub _convert_versions { |
165
|
|
|
|
|
|
|
my ($self, $versions, $project) = @_; |
166
|
|
|
|
|
|
|
is_array_ref($versions) or croak "The '$versions' value must be an ARRAY ref.\n"; |
167
|
|
|
|
|
|
|
my @converted; |
168
|
|
|
|
|
|
|
my $pversions; # project versions |
169
|
|
|
|
|
|
|
foreach my $version (@{$versions}) { |
170
|
|
|
|
|
|
|
if (is_instance($version => 'RemoteVersion')) { |
171
|
|
|
|
|
|
|
push @converted, $version; |
172
|
|
|
|
|
|
|
} elsif (is_integer($version)) { |
173
|
|
|
|
|
|
|
push @converted, RemoteVersion->new($version); |
174
|
|
|
|
|
|
|
} else { |
175
|
|
|
|
|
|
|
# It is a version name. Let us convert it into its id. |
176
|
|
|
|
|
|
|
croak "Cannot convert version names because I don't know for which project.\n" |
177
|
|
|
|
|
|
|
unless $project; |
178
|
|
|
|
|
|
|
$pversions = $self->get_versions($project) unless defined $pversions; |
179
|
|
|
|
|
|
|
croak "There is no version called '$version'.\n" |
180
|
|
|
|
|
|
|
unless exists $pversions->{$version}; |
181
|
|
|
|
|
|
|
push @converted, RemoteVersion->new($pversions->{$version}{id}); |
182
|
|
|
|
|
|
|
} |
183
|
|
|
|
|
|
|
} |
184
|
|
|
|
|
|
|
return \@converted; |
185
|
|
|
|
|
|
|
} |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
# This routine returns a duedate as a SOAP::Data object with type |
188
|
|
|
|
|
|
|
# 'date'. It can generate this from a DateTime object or from a string |
189
|
|
|
|
|
|
|
# in the format YYYY-MM-DD. |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
sub _convert_duedate { |
192
|
|
|
|
|
|
|
my ($self, $duedate) = @_; |
193
|
|
|
|
|
|
|
if (is_instance($duedate => 'DateTime')) { |
194
|
|
|
|
|
|
|
return SOAP::Data->type(date => $duedate->strftime('%F')); |
195
|
|
|
|
|
|
|
} elsif (is_string($duedate)) { |
196
|
|
|
|
|
|
|
if (my ($year, $month, $day) = ($duedate =~ /^(\d{4})-(\d{2})-(\d{2})/)) { |
197
|
|
|
|
|
|
|
$month >= 1 and $month <= 12 |
198
|
|
|
|
|
|
|
or croak "Invalid duedate ($duedate).\n"; |
199
|
|
|
|
|
|
|
return SOAP::Data->type(date => $duedate); |
200
|
|
|
|
|
|
|
} |
201
|
|
|
|
|
|
|
} |
202
|
|
|
|
|
|
|
return $duedate; |
203
|
|
|
|
|
|
|
} |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
# This routine receives a hash mapping custom field's ids to |
206
|
|
|
|
|
|
|
# values. The ids can be specified by their real id or by their id's |
207
|
|
|
|
|
|
|
# numeric suffix (as the 1000 in 'customfield_1000'). Scalar values |
208
|
|
|
|
|
|
|
# are substituted by references to arrays containing the original |
209
|
|
|
|
|
|
|
# value. The routine returns a hash-ref to another hash with converted |
210
|
|
|
|
|
|
|
# keys and values. |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
sub _convert_custom_fields { |
213
|
|
|
|
|
|
|
my ($self, $custom_fields) = @_; |
214
|
|
|
|
|
|
|
is_hash_ref($custom_fields) or croak "The 'custom_fields' value must be a HASH ref.\n"; |
215
|
|
|
|
|
|
|
my %converted; |
216
|
|
|
|
|
|
|
while (my ($id, $values) = each %$custom_fields) { |
217
|
|
|
|
|
|
|
my $realid = $id; |
218
|
|
|
|
|
|
|
unless ($realid =~ /^customfield_\d+$/) { |
219
|
|
|
|
|
|
|
my $cfs = $self->get_custom_fields(); |
220
|
|
|
|
|
|
|
croak "Can't find custom field named '$id'.\n" |
221
|
|
|
|
|
|
|
unless exists $cfs->{$id}; |
222
|
|
|
|
|
|
|
$realid = $cfs->{$id}{id}; |
223
|
|
|
|
|
|
|
} |
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
# Custom field values must be specified as ARRAYs but we allow for some short-cuts. |
226
|
|
|
|
|
|
|
if (is_value($values)) { |
227
|
|
|
|
|
|
|
$converted{$realid} = [$values]; |
228
|
|
|
|
|
|
|
} elsif (is_array_ref($values)) { |
229
|
|
|
|
|
|
|
$converted{$realid} = $values; |
230
|
|
|
|
|
|
|
} elsif (is_hash_ref($values)) { |
231
|
|
|
|
|
|
|
# This is a short-cut for a Cascading select field, which |
232
|
|
|
|
|
|
|
# must be specified like this: http://tinyurl.com/2bmthoa |
233
|
|
|
|
|
|
|
# The short-cut requires a HASH where each cascading level |
234
|
|
|
|
|
|
|
# is indexed by its level number, starting at zero. |
235
|
|
|
|
|
|
|
foreach my $level (sort {$a <=> $b} keys %$values) { |
236
|
|
|
|
|
|
|
my $level_values = $values->{$level}; |
237
|
|
|
|
|
|
|
$level_values = [$level_values] unless ref $level_values; |
238
|
|
|
|
|
|
|
if ($level eq '0') { |
239
|
|
|
|
|
|
|
# The first level doesn't have a colon |
240
|
|
|
|
|
|
|
$converted{$realid} = $level_values |
241
|
|
|
|
|
|
|
} elsif ($level =~ /^\d+$/) { |
242
|
|
|
|
|
|
|
$converted{"$realid:$level"} = $level_values; |
243
|
|
|
|
|
|
|
} else { |
244
|
|
|
|
|
|
|
croak "Invalid cascading field values level spec ($level). It must be a natural number.\n"; |
245
|
|
|
|
|
|
|
} |
246
|
|
|
|
|
|
|
} |
247
|
|
|
|
|
|
|
} else { |
248
|
|
|
|
|
|
|
croak "Custom field '$id' got a '", ref($values), "' reference as a value.\nValues can only be specified as scalars, ARRAYs, or HASHes though.\n"; |
249
|
|
|
|
|
|
|
} |
250
|
|
|
|
|
|
|
} |
251
|
|
|
|
|
|
|
return \%converted; |
252
|
|
|
|
|
|
|
} |
253
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
my %_converters = ( |
255
|
|
|
|
|
|
|
affectsVersions => \&_convert_versions, |
256
|
|
|
|
|
|
|
components => \&_convert_components, |
257
|
|
|
|
|
|
|
custom_fields => \&_convert_custom_fields, |
258
|
|
|
|
|
|
|
duedate => \&_convert_duedate, |
259
|
|
|
|
|
|
|
fixVersions => \&_convert_versions, |
260
|
|
|
|
|
|
|
priority => \&_convert_priority, |
261
|
|
|
|
|
|
|
resolution => \&_convert_resolution, |
262
|
|
|
|
|
|
|
type => \&_convert_type, |
263
|
|
|
|
|
|
|
); |
264
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
# Accept both names for fields with duplicate names. |
266
|
|
|
|
|
|
|
foreach my $field (keys %JRA12300) { |
267
|
|
|
|
|
|
|
$_converters{$JRA12300{$field}} = $_converters{$field}; |
268
|
|
|
|
|
|
|
} |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
# This routine applies all the previous conversions to the $params |
271
|
|
|
|
|
|
|
# hash. It returns a reference another hash with converted keys and |
272
|
|
|
|
|
|
|
# values, which is the base for invoking the methods createIssue, |
273
|
|
|
|
|
|
|
# UpdateIssue, and progressWorkflowAction. |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
sub _convert_params { |
276
|
|
|
|
|
|
|
my ($self, $params, $project) = @_; |
277
|
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
my %converted; |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
# Convert fields' values |
281
|
|
|
|
|
|
|
while (my ($field, $value) = each %$params) { |
282
|
|
|
|
|
|
|
$converted{$field} = |
283
|
|
|
|
|
|
|
exists $_converters{$field} |
284
|
|
|
|
|
|
|
? $_converters{$field}->($self, $value, $project) |
285
|
|
|
|
|
|
|
: $value; |
286
|
|
|
|
|
|
|
} |
287
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
return \%converted; |
289
|
|
|
|
|
|
|
} |
290
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
# This routine gets a hash produced by _convert_params and flatens in |
292
|
|
|
|
|
|
|
# place its Component, Version, and custom_fields fields. It also |
293
|
|
|
|
|
|
|
# converts the hash's key according with the %JRA12300 table. It goes |
294
|
|
|
|
|
|
|
# a step further before invoking the methods UpdateIssue and |
295
|
|
|
|
|
|
|
# progressWorkflowAction. |
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
sub _flaten_components_and_versions { |
298
|
|
|
|
|
|
|
my ($params) = @_; |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
# Flaten Component and Version fields |
301
|
|
|
|
|
|
|
for my $field (grep {exists $params->{$_}} qw/components affectsVersions fixVersions/) { |
302
|
|
|
|
|
|
|
$params->{$field} = [map {$_->{id}} @{$params->{$field}}]; |
303
|
|
|
|
|
|
|
} |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
# Flaten the customFieldValues field |
306
|
|
|
|
|
|
|
if (my $custom_fields = delete $params->{custom_fields}) { |
307
|
|
|
|
|
|
|
while (my ($id, $values) = each %$custom_fields) { |
308
|
|
|
|
|
|
|
$params->{$id} = $values; |
309
|
|
|
|
|
|
|
} |
310
|
|
|
|
|
|
|
} |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
# Due to a bug in JIRA we have to substitute the names of some fields. |
313
|
|
|
|
|
|
|
foreach my $field (grep {exists $params->{$_}} keys %JRA12300) { |
314
|
|
|
|
|
|
|
$params->{$JRA12300{$field}} = delete $params->{$field}; |
315
|
|
|
|
|
|
|
} |
316
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
return; |
318
|
|
|
|
|
|
|
} |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
sub create_issue |
322
|
|
|
|
|
|
|
{ |
323
|
|
|
|
|
|
|
my ($self, $params, $seclevel) = @_; |
324
|
|
|
|
|
|
|
is_hash_ref($params) or croak "create_issue's requires a HASH-ref argument.\n"; |
325
|
|
|
|
|
|
|
for my $field (qw/project summary type/) { |
326
|
|
|
|
|
|
|
croak "create_issue's HASH ref must define a '$field'.\n" |
327
|
|
|
|
|
|
|
unless exists $params->{$field}; |
328
|
|
|
|
|
|
|
} |
329
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
$params = $self->_convert_params($params, $params->{project}); |
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
# Substitute customFieldValues array for custom_fields hash |
333
|
|
|
|
|
|
|
if (my $cfs = delete $params->{custom_fields}) { |
334
|
|
|
|
|
|
|
$params->{customFieldValues} = [map {RemoteCustomFieldValue->new($_, $cfs->{$_})} keys %$cfs]; |
335
|
|
|
|
|
|
|
} |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
if (my $parent = delete $params->{parent}) { |
338
|
|
|
|
|
|
|
if (defined $seclevel) { |
339
|
|
|
|
|
|
|
return $self->createIssueWithParentWithSecurityLevel($params, $parent, _convert_security_level($self, $seclevel, $params->{project})); |
340
|
|
|
|
|
|
|
} else { |
341
|
|
|
|
|
|
|
return $self->createIssueWithParent($params, $parent); |
342
|
|
|
|
|
|
|
} |
343
|
|
|
|
|
|
|
} else { |
344
|
|
|
|
|
|
|
if (defined $seclevel) { |
345
|
|
|
|
|
|
|
return $self->createIssueWithSecurityLevel($params, _convert_security_level($self, $seclevel, $params->{project})); |
346
|
|
|
|
|
|
|
} else { |
347
|
|
|
|
|
|
|
return $self->createIssue($params); |
348
|
|
|
|
|
|
|
} |
349
|
|
|
|
|
|
|
} |
350
|
|
|
|
|
|
|
} |
351
|
|
|
|
|
|
|
|
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
sub update_issue |
354
|
|
|
|
|
|
|
{ |
355
|
|
|
|
|
|
|
my ($self, $issue, $params) = @_; |
356
|
|
|
|
|
|
|
my $key; |
357
|
|
|
|
|
|
|
if (is_instance($issue => 'RemoteIssue')) { |
358
|
|
|
|
|
|
|
$key = $issue->{key}; |
359
|
|
|
|
|
|
|
} else { |
360
|
|
|
|
|
|
|
$key = $issue; |
361
|
|
|
|
|
|
|
$issue = $self->getIssue($key); |
362
|
|
|
|
|
|
|
} |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
is_hash_ref($params) or croak "update_issue second argument must be a HASH ref.\n"; |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
my ($project) = ($key =~ /^([^-]+)/); |
367
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
$params = $self->_convert_params($params, $project); |
369
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
_flaten_components_and_versions($params); |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
return $self->updateIssue($key, $params); |
373
|
|
|
|
|
|
|
} |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
sub get_issue_types { |
377
|
|
|
|
|
|
|
my ($self) = @_; |
378
|
|
|
|
|
|
|
$self->{cache}{issue_types} ||= {map {$_->{name} => $_} @{$self->getIssueTypes()}}; |
379
|
|
|
|
|
|
|
return $self->{cache}{issue_types}; |
380
|
|
|
|
|
|
|
} |
381
|
|
|
|
|
|
|
|
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
sub get_subtask_issue_types { |
384
|
|
|
|
|
|
|
my ($self) = @_; |
385
|
|
|
|
|
|
|
$self->{cache}{subtask_issue_types} ||= {map {$_->{name} => $_} @{$self->getSubTaskIssueTypes()}}; |
386
|
|
|
|
|
|
|
return $self->{cache}{subtask_issue_types}; |
387
|
|
|
|
|
|
|
} |
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
sub get_statuses { |
391
|
|
|
|
|
|
|
my ($self) = @_; |
392
|
|
|
|
|
|
|
$self->{cache}{statuses} ||= {map {$_->{name} => $_} @{$self->getStatuses()}}; |
393
|
|
|
|
|
|
|
return $self->{cache}{statuses}; |
394
|
|
|
|
|
|
|
} |
395
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
|
397
|
|
|
|
|
|
|
sub get_priorities { |
398
|
|
|
|
|
|
|
my ($self) = @_; |
399
|
|
|
|
|
|
|
$self->{cache}{priorities} ||= {map {$_->{name} => $_} @{$self->getPriorities()}}; |
400
|
|
|
|
|
|
|
return $self->{cache}{priorities}; |
401
|
|
|
|
|
|
|
} |
402
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
sub get_resolutions { |
405
|
|
|
|
|
|
|
my ($self) = @_; |
406
|
|
|
|
|
|
|
$self->{cache}{resolutions} ||= {map {$_->{name} => $_} @{$self->getResolutions()}}; |
407
|
|
|
|
|
|
|
return $self->{cache}{resolutions}; |
408
|
|
|
|
|
|
|
} |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
|
411
|
|
|
|
|
|
|
sub get_security_levels { |
412
|
|
|
|
|
|
|
my ($self, $project_key) = @_; |
413
|
|
|
|
|
|
|
$self->{cache}{seclevels}{$project_key} ||= {map {$_->{name} => $_} @{$self->getSecurityLevels($project_key)}}; |
414
|
|
|
|
|
|
|
return $self->{cache}{seclevels}{$project_key}; |
415
|
|
|
|
|
|
|
} |
416
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
|
418
|
|
|
|
|
|
|
sub get_custom_fields { |
419
|
|
|
|
|
|
|
my ($self) = @_; |
420
|
|
|
|
|
|
|
$self->{cache}{custom_fields} ||= {map {$_->{name} => $_} @{$self->getCustomFields()}}; |
421
|
|
|
|
|
|
|
return $self->{cache}{custom_fields}; |
422
|
|
|
|
|
|
|
} |
423
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
|
425
|
|
|
|
|
|
|
sub set_custom_fields { |
426
|
|
|
|
|
|
|
my ($self, $cfs) = @_; |
427
|
|
|
|
|
|
|
$self->{cache}{custom_fields} = $cfs; |
428
|
|
|
|
|
|
|
return; |
429
|
|
|
|
|
|
|
} |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
|
432
|
|
|
|
|
|
|
sub get_components { |
433
|
|
|
|
|
|
|
my ($self, $project_key) = @_; |
434
|
|
|
|
|
|
|
$self->{cache}{components}{$project_key} ||= {map {$_->{name} => $_} @{$self->getComponents($project_key)}}; |
435
|
|
|
|
|
|
|
return $self->{cache}{components}{$project_key}; |
436
|
|
|
|
|
|
|
} |
437
|
|
|
|
|
|
|
|
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
sub get_versions { |
440
|
|
|
|
|
|
|
my ($self, $project_key) = @_; |
441
|
|
|
|
|
|
|
$self->{cache}{versions}{$project_key} ||= {map {$_->{name} => $_} @{$self->getVersions($project_key)}}; |
442
|
|
|
|
|
|
|
return $self->{cache}{versions}{$project_key}; |
443
|
|
|
|
|
|
|
} |
444
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
sub get_favourite_filters { |
447
|
|
|
|
|
|
|
my ($self) = @_; |
448
|
|
|
|
|
|
|
$self->{cache}{filters} ||= {map {$_->{name} => $_} @{$self->getFavouriteFilters()}}; |
449
|
|
|
|
|
|
|
return $self->{cache}{filters}; |
450
|
|
|
|
|
|
|
} |
451
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
sub set_filter_iterator { |
454
|
|
|
|
|
|
|
my ($self, $filter, $cache_size) = @_; |
455
|
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
if ($filter =~ /\D/) { |
457
|
|
|
|
|
|
|
my $filters = $self->getSavedFilters(); |
458
|
|
|
|
|
|
|
foreach my $f (@$filters) { |
459
|
|
|
|
|
|
|
if ($f->{name} eq $filter) { |
460
|
|
|
|
|
|
|
$filter = $f->{id}; |
461
|
|
|
|
|
|
|
last; |
462
|
|
|
|
|
|
|
} |
463
|
|
|
|
|
|
|
} |
464
|
|
|
|
|
|
|
croak "Can't find filter '$filter'\n" if $filter =~ /\D/; |
465
|
|
|
|
|
|
|
} |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
if ($cache_size) { |
468
|
|
|
|
|
|
|
croak "set_filter_iterator's second arg must be a number ($cache_size).\n" |
469
|
|
|
|
|
|
|
if $cache_size =~ /\D/; |
470
|
|
|
|
|
|
|
} |
471
|
|
|
|
|
|
|
|
472
|
|
|
|
|
|
|
$self->{iter} = { |
473
|
|
|
|
|
|
|
id => $filter, |
474
|
|
|
|
|
|
|
offset => 0, # offset to be used in the next call to getIssuesFromFilterWithLimit |
475
|
|
|
|
|
|
|
issues => [], # issues returned by the last call to getIssuesFromFilterWithLimit |
476
|
|
|
|
|
|
|
size => $cache_size || 128, |
477
|
|
|
|
|
|
|
}; |
478
|
|
|
|
|
|
|
|
479
|
|
|
|
|
|
|
return; |
480
|
|
|
|
|
|
|
} |
481
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
|
483
|
|
|
|
|
|
|
sub next_issue { |
484
|
|
|
|
|
|
|
my ($self) = @_; |
485
|
|
|
|
|
|
|
defined $self->{iter} |
486
|
|
|
|
|
|
|
or croak "You must call setFilterIterator before calling nextIssue\n"; |
487
|
|
|
|
|
|
|
my $iter = $self->{iter}; |
488
|
|
|
|
|
|
|
if (@{$iter->{issues}} == 0) { |
489
|
|
|
|
|
|
|
if ($iter->{id}) { |
490
|
|
|
|
|
|
|
my $issues = eval {$self->getIssuesFromFilterWithLimit($iter->{id}, $iter->{offset}, $iter->{size})}; |
491
|
|
|
|
|
|
|
if ($@) { |
492
|
|
|
|
|
|
|
# The getIssuesFromFilterWithLimit appeared in JIRA |
493
|
|
|
|
|
|
|
# 3.13.4. Before that we had to use the unsafe |
494
|
|
|
|
|
|
|
# getIssuesFromFilter. Here we detect that we're talking |
495
|
|
|
|
|
|
|
# with an old JIRA and resort to the deprecated method |
496
|
|
|
|
|
|
|
# instead. |
497
|
|
|
|
|
|
|
croak $@ unless $@ =~ /No such operation/; |
498
|
|
|
|
|
|
|
$iter->{issues} = $self->getIssuesFromFilter($iter->{id}); |
499
|
|
|
|
|
|
|
$iter->{id} = undef; |
500
|
|
|
|
|
|
|
} |
501
|
|
|
|
|
|
|
elsif (@$issues) { |
502
|
|
|
|
|
|
|
$iter->{offset} += @$issues; |
503
|
|
|
|
|
|
|
$iter->{issues} = $issues; |
504
|
|
|
|
|
|
|
} |
505
|
|
|
|
|
|
|
else { |
506
|
|
|
|
|
|
|
$self->{iter} = undef; |
507
|
|
|
|
|
|
|
return; |
508
|
|
|
|
|
|
|
} |
509
|
|
|
|
|
|
|
} |
510
|
|
|
|
|
|
|
else { |
511
|
|
|
|
|
|
|
return; |
512
|
|
|
|
|
|
|
} |
513
|
|
|
|
|
|
|
} |
514
|
|
|
|
|
|
|
return shift @{$iter->{issues}}; |
515
|
|
|
|
|
|
|
} |
516
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
sub progress_workflow_action_safely { |
519
|
|
|
|
|
|
|
my ($self, $issue, $action, $params) = @_; |
520
|
|
|
|
|
|
|
my $key; |
521
|
|
|
|
|
|
|
if (is_instance($issue => 'RemoteIssue')) { |
522
|
|
|
|
|
|
|
$key = $issue->{key}; |
523
|
|
|
|
|
|
|
} else { |
524
|
|
|
|
|
|
|
$key = $issue; |
525
|
|
|
|
|
|
|
$issue = undef; |
526
|
|
|
|
|
|
|
} |
527
|
|
|
|
|
|
|
$params = {} unless defined $params; |
528
|
|
|
|
|
|
|
is_hash_ref($params) or croak "progress_workflow_action_safely's third arg must be a HASH-ref\n"; |
529
|
|
|
|
|
|
|
|
530
|
|
|
|
|
|
|
# Grok the action id if it's not a number |
531
|
|
|
|
|
|
|
if ($action =~ /\D/) { |
532
|
|
|
|
|
|
|
my @available_actions = @{$self->getAvailableActions($key)}; |
533
|
|
|
|
|
|
|
my @named_actions = grep {$action eq $_->{name}} @available_actions; |
534
|
|
|
|
|
|
|
if (@named_actions) { |
535
|
|
|
|
|
|
|
$action = $named_actions[0]->{id}; |
536
|
|
|
|
|
|
|
} else { |
537
|
|
|
|
|
|
|
croak "Unavailable action ($action).\n"; |
538
|
|
|
|
|
|
|
} |
539
|
|
|
|
|
|
|
} |
540
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
# Make sure $params contains all the fields that are present in |
542
|
|
|
|
|
|
|
# the action screen. |
543
|
|
|
|
|
|
|
my @fields = @{$self->getFieldsForAction($key, $action)}; |
544
|
|
|
|
|
|
|
foreach my $id (map {$_->{id}} @fields) { |
545
|
|
|
|
|
|
|
# Due to a bug in JIRA we have to substitute the names of some fields. |
546
|
|
|
|
|
|
|
$id = $JRA12300_backwards{$id} if $JRA12300_backwards{$id}; |
547
|
|
|
|
|
|
|
|
548
|
|
|
|
|
|
|
next if exists $params->{$id}; |
549
|
|
|
|
|
|
|
|
550
|
|
|
|
|
|
|
$issue = $self->getIssue($key) unless defined $issue; |
551
|
|
|
|
|
|
|
if (exists $issue->{$id}) { |
552
|
|
|
|
|
|
|
$params->{$id} = $issue->{$id} if defined $issue->{$id}; |
553
|
|
|
|
|
|
|
} |
554
|
|
|
|
|
|
|
# NOTE: It's not a problem if we can't find a missing |
555
|
|
|
|
|
|
|
# parameter in the issue. It will simply stay undefined. |
556
|
|
|
|
|
|
|
} |
557
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
my ($project) = ($key =~ /^([^-]+)/); |
559
|
|
|
|
|
|
|
|
560
|
|
|
|
|
|
|
$params = $self->_convert_params($params, $project); |
561
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
_flaten_components_and_versions($params); |
563
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
return $self->progressWorkflowAction($key, $action, $params); |
565
|
|
|
|
|
|
|
} |
566
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
sub get_issue_custom_field_values { |
569
|
|
|
|
|
|
|
my ($self, $issue, @cfs) = @_; |
570
|
|
|
|
|
|
|
my @values; |
571
|
|
|
|
|
|
|
my $cfs; |
572
|
|
|
|
|
|
|
CUSTOM_FIELD: |
573
|
|
|
|
|
|
|
foreach my $cf (@cfs) { |
574
|
|
|
|
|
|
|
unless ($cf =~ /^customfield_\d+$/) { |
575
|
|
|
|
|
|
|
$cfs = $self->get_custom_fields() unless defined $cfs; |
576
|
|
|
|
|
|
|
croak "Can't find custom field named '$cf'.\n" |
577
|
|
|
|
|
|
|
unless exists $cfs->{$cf}; |
578
|
|
|
|
|
|
|
$cf = $cfs->{$cf}{id}; |
579
|
|
|
|
|
|
|
} |
580
|
|
|
|
|
|
|
foreach my $rcfv (@{$issue->{customFieldValues}}) { |
581
|
|
|
|
|
|
|
if ($rcfv->{customfieldId} eq $cf) { |
582
|
|
|
|
|
|
|
push @values, $rcfv->{values}; |
583
|
|
|
|
|
|
|
next CUSTOM_FIELD; |
584
|
|
|
|
|
|
|
} |
585
|
|
|
|
|
|
|
} |
586
|
|
|
|
|
|
|
push @values, undef; # unset custom field |
587
|
|
|
|
|
|
|
} |
588
|
|
|
|
|
|
|
return wantarray ? @values : \@values; |
589
|
|
|
|
|
|
|
} |
590
|
|
|
|
|
|
|
|
591
|
|
|
|
|
|
|
|
592
|
|
|
|
|
|
|
sub attach_files_to_issue { |
593
|
|
|
|
|
|
|
my ($self, $issue, @files) = @_; |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
# First we process the @files specification. Filenames are pushed |
596
|
|
|
|
|
|
|
# in @filenames and @attachments will end up with IO objects from |
597
|
|
|
|
|
|
|
# which the file contents are going to be read later. |
598
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
my (@filenames, @attachments); |
600
|
|
|
|
|
|
|
|
601
|
|
|
|
|
|
|
for my $file (@files) { |
602
|
|
|
|
|
|
|
if (is_string($file)) { |
603
|
|
|
|
|
|
|
require File::Basename; |
604
|
|
|
|
|
|
|
push @filenames, File::Basename::basename($file); |
605
|
|
|
|
|
|
|
open my $fh, '<:raw', $file |
606
|
|
|
|
|
|
|
or croak "Can't open $file: $!\n"; |
607
|
|
|
|
|
|
|
push @attachments, $fh; |
608
|
|
|
|
|
|
|
close $fh; |
609
|
|
|
|
|
|
|
} elsif (is_hash_ref($file)) { |
610
|
|
|
|
|
|
|
while (my ($name, $contents) = each %$file) { |
611
|
|
|
|
|
|
|
push @filenames, $name; |
612
|
|
|
|
|
|
|
if (is_string($contents)) { |
613
|
|
|
|
|
|
|
open my $fh, '<:raw', $contents |
614
|
|
|
|
|
|
|
or croak "Can't open $contents: $!\n"; |
615
|
|
|
|
|
|
|
push @attachments, $fh; |
616
|
|
|
|
|
|
|
close $fh; |
617
|
|
|
|
|
|
|
} elsif (is_glob_ref($contents) |
618
|
|
|
|
|
|
|
|| is_instance($contents => 'IO::File') |
619
|
|
|
|
|
|
|
|| is_instance($contents => 'FileHandle')) { |
620
|
|
|
|
|
|
|
push @attachments, $contents; |
621
|
|
|
|
|
|
|
} else { |
622
|
|
|
|
|
|
|
croak "Invalid content specification for file $name.\n"; |
623
|
|
|
|
|
|
|
} |
624
|
|
|
|
|
|
|
} |
625
|
|
|
|
|
|
|
} else { |
626
|
|
|
|
|
|
|
croak "Files must be specified by STRINGs or HASHes, not by " . ref($file) . "s\n"; |
627
|
|
|
|
|
|
|
} |
628
|
|
|
|
|
|
|
} |
629
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
# Now we have to read all file contents and encode them to Base64. |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
require MIME::Base64; |
633
|
|
|
|
|
|
|
for my $i (0 .. $#attachments) { |
634
|
|
|
|
|
|
|
my $fh = $attachments[$i]; |
635
|
|
|
|
|
|
|
my $attachment = ''; |
636
|
|
|
|
|
|
|
my $chars_read; |
637
|
|
|
|
|
|
|
while ($chars_read = read $fh, my $buf, 57*72) { |
638
|
|
|
|
|
|
|
$attachment .= MIME::Base64::encode_base64($buf); |
639
|
|
|
|
|
|
|
} |
640
|
|
|
|
|
|
|
defined $chars_read |
641
|
|
|
|
|
|
|
or croak "Error reading '$filenames[$i]': $!\n"; |
642
|
|
|
|
|
|
|
length $attachment |
643
|
|
|
|
|
|
|
or croak "Can't attach empty file '$filenames[$i]'\n"; |
644
|
|
|
|
|
|
|
$attachments[$i] = $attachment; |
645
|
|
|
|
|
|
|
} |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
return $self->addBase64EncodedAttachmentsToIssue($issue, \@filenames, \@attachments); |
648
|
|
|
|
|
|
|
} |
649
|
|
|
|
|
|
|
|
650
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
sub attach_strings_to_issue { |
652
|
|
|
|
|
|
|
my ($self, $issue, $hash) = @_; |
653
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
require MIME::Base64; |
655
|
|
|
|
|
|
|
|
656
|
|
|
|
|
|
|
my (@filenames, @attachments); |
657
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
while (my ($filename, $contents) = each %$hash) { |
659
|
|
|
|
|
|
|
push @filenames, $filename; |
660
|
|
|
|
|
|
|
push @attachments, MIME::Base64::encode_base64($contents); |
661
|
|
|
|
|
|
|
} |
662
|
|
|
|
|
|
|
|
663
|
|
|
|
|
|
|
return $self->addBase64EncodedAttachmentsToIssue($issue, \@filenames, \@attachments); |
664
|
|
|
|
|
|
|
} |
665
|
|
|
|
|
|
|
|
666
|
|
|
|
|
|
|
|
667
|
|
|
|
|
|
|
sub filter_issues_unsorted { |
668
|
|
|
|
|
|
|
my ($self, $filter, $limit) = @_; |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
$filter =~ s/^\s*"?//; |
671
|
|
|
|
|
|
|
$filter =~ s/"?\s*$//; |
672
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
if ($filter =~ /^(?:[A-Z]+-\d+\s+)*[A-Z]+-\d+$/i) { |
674
|
|
|
|
|
|
|
# space separated key list |
675
|
|
|
|
|
|
|
|
676
|
|
|
|
|
|
|
# Let's construct a JQL query in the form "issuekey IN (...)" to |
677
|
|
|
|
|
|
|
# pass to getIssuesFromJqlSearch. |
678
|
|
|
|
|
|
|
|
679
|
|
|
|
|
|
|
my %keys = map {($_ => undef)} split / /, $filter; # discard duplicates |
680
|
|
|
|
|
|
|
my @keys = keys %keys; |
681
|
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
# If the list is too big we split it up and invoke |
683
|
|
|
|
|
|
|
# getIssuesFromJqlSearch several times. |
684
|
|
|
|
|
|
|
my @issues; |
685
|
|
|
|
|
|
|
while (my @subkeys = splice(@keys, 0, 128)) { |
686
|
|
|
|
|
|
|
my $jql = 'issuekey IN (' . join(',', @subkeys) . ')'; |
687
|
|
|
|
|
|
|
push @issues, @{$self->getIssuesFromJqlSearch($jql, 1000)}; |
688
|
|
|
|
|
|
|
} |
689
|
|
|
|
|
|
|
return @issues; |
690
|
|
|
|
|
|
|
} elsif ($filter =~ /^[\w-]+$/i) { |
691
|
|
|
|
|
|
|
# saved filter |
692
|
|
|
|
|
|
|
return @{$self->getIssuesFromFilterWithLimit($filter, 0, $limit || 1000)}; |
693
|
|
|
|
|
|
|
} else { |
694
|
|
|
|
|
|
|
# JQL filter |
695
|
|
|
|
|
|
|
return @{$self->getIssuesFromJqlSearch($filter, $limit || 1000)}; |
696
|
|
|
|
|
|
|
} |
697
|
|
|
|
|
|
|
} |
698
|
|
|
|
|
|
|
|
699
|
|
|
|
|
|
|
|
700
|
|
|
|
|
|
|
sub filter_issues { |
701
|
|
|
|
|
|
|
my ($self, $filter, $limit) = @_; |
702
|
|
|
|
|
|
|
|
703
|
|
|
|
|
|
|
# Order the issues by project key and then by numeric value using |
704
|
|
|
|
|
|
|
# a Schwartzian transform. |
705
|
|
|
|
|
|
|
return |
706
|
|
|
|
|
|
|
map {$_->[2]} |
707
|
|
|
|
|
|
|
sort {$a->[0] cmp $b->[0] or $a->[1] <=> $b->[1]} |
708
|
|
|
|
|
|
|
map {my ($p, $n) = ($_->{key} =~ /([A-Z]+)-(\d+)/); [$p, $n, $_]} |
709
|
|
|
|
|
|
|
filter_issues_unsorted($self, $filter, $limit); |
710
|
|
|
|
|
|
|
} |
711
|
|
|
|
|
|
|
|
712
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
## no critic (Modules::ProhibitMultiplePackages) |
714
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
package RemoteFieldValue; |
716
|
|
|
|
|
|
|
$RemoteFieldValue::VERSION = '0.43'; |
717
|
|
|
|
|
|
|
sub new { |
718
|
|
|
|
|
|
|
my ($class, $id, $values) = @_; |
719
|
|
|
|
|
|
|
|
720
|
|
|
|
|
|
|
# Due to a bug in JIRA we have to substitute the names of some fields. |
721
|
|
|
|
|
|
|
$id = $JRA12300{$id} if exists $JRA12300{$id}; |
722
|
|
|
|
|
|
|
|
723
|
|
|
|
|
|
|
$values = [$values] unless ref $values; |
724
|
|
|
|
|
|
|
return bless({id => $id, values => $values}, $class); |
725
|
|
|
|
|
|
|
} |
726
|
|
|
|
|
|
|
|
727
|
|
|
|
|
|
|
|
728
|
|
|
|
|
|
|
package RemoteCustomFieldValue; |
729
|
|
|
|
|
|
|
$RemoteCustomFieldValue::VERSION = '0.43'; |
730
|
|
|
|
|
|
|
sub new { |
731
|
|
|
|
|
|
|
my ($class, $id, $values) = @_; |
732
|
|
|
|
|
|
|
|
733
|
|
|
|
|
|
|
$values = [$values] unless ref $values; |
734
|
|
|
|
|
|
|
return bless({customfieldId => $id, key => undef, values => $values} => $class); |
735
|
|
|
|
|
|
|
} |
736
|
|
|
|
|
|
|
|
737
|
|
|
|
|
|
|
|
738
|
|
|
|
|
|
|
package RemoteComponent; |
739
|
|
|
|
|
|
|
$RemoteComponent::VERSION = '0.43'; |
740
|
|
|
|
|
|
|
sub new { |
741
|
|
|
|
|
|
|
my ($class, $id, $name) = @_; |
742
|
|
|
|
|
|
|
my $o = bless({id => $id}, $class); |
743
|
|
|
|
|
|
|
$o->{name} = $name if $name; |
744
|
|
|
|
|
|
|
return $o; |
745
|
|
|
|
|
|
|
} |
746
|
|
|
|
|
|
|
|
747
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
package RemoteVersion; |
749
|
|
|
|
|
|
|
$RemoteVersion::VERSION = '0.43'; |
750
|
|
|
|
|
|
|
sub new { |
751
|
|
|
|
|
|
|
my ($class, $id, $name) = @_; |
752
|
|
|
|
|
|
|
my $o = bless({id => $id}, $class); |
753
|
|
|
|
|
|
|
$o->{name} = $name if $name; |
754
|
|
|
|
|
|
|
return $o; |
755
|
|
|
|
|
|
|
} |
756
|
|
|
|
|
|
|
|
757
|
|
|
|
|
|
|
package JIRA::Client; |
758
|
|
|
|
|
|
|
|
759
|
|
|
|
|
|
|
# Almost all of the JIRA API parameters are strings. The %typeof hash |
760
|
|
|
|
|
|
|
# specifies the exceptions. It maps a method name to a hash mapping a |
761
|
|
|
|
|
|
|
# parameter position to its type. (The parameter position is |
762
|
|
|
|
|
|
|
# zero-based, after the authentication token. |
763
|
|
|
|
|
|
|
|
764
|
|
|
|
|
|
|
my %typeof = ( |
765
|
|
|
|
|
|
|
addActorsToProjectRole => {1 => \&_cast_remote_project_role}, |
766
|
|
|
|
|
|
|
addAttachmentsToIssue => \&_cast_attachments, |
767
|
|
|
|
|
|
|
addBase64EncodedAttachmentsToIssue => \&_cast_base64encodedattachments, |
768
|
|
|
|
|
|
|
addComment => {0 => \&_cast_issue_key, 1 => \&_cast_remote_comment}, |
769
|
|
|
|
|
|
|
addDefaultActorsToProjectRole => {1 => \&_cast_remote_project_role}, |
770
|
|
|
|
|
|
|
# addPermissionTo |
771
|
|
|
|
|
|
|
# addUserToGroup |
772
|
|
|
|
|
|
|
# addVersion |
773
|
|
|
|
|
|
|
addWorklogAndAutoAdjustRemainingEstimate => {0 => \&_cast_issue_key}, |
774
|
|
|
|
|
|
|
addWorklogAndRetainRemainingEstimate => {0 => \&_cast_issue_key}, |
775
|
|
|
|
|
|
|
addWorklogWithNewRemainingEstimate => {0 => \&_cast_issue_key}, |
776
|
|
|
|
|
|
|
archiveVersion => {2 => 'boolean'}, |
777
|
|
|
|
|
|
|
# createGroup |
778
|
|
|
|
|
|
|
# createIssue |
779
|
|
|
|
|
|
|
createIssueWithParent => {1 => \&_cast_issue_key}, |
780
|
|
|
|
|
|
|
createIssueWithParentWithSecurityLevel => {1 => \&_cast_issue_key, 2 => 'long'}, |
781
|
|
|
|
|
|
|
createIssueWithSecurityLevel => {1 => 'long'}, |
782
|
|
|
|
|
|
|
# createPermissionScheme |
783
|
|
|
|
|
|
|
# createProject |
784
|
|
|
|
|
|
|
# createProjectFromObject |
785
|
|
|
|
|
|
|
createProjectRole => {0 => \&_cast_remote_project_role}, |
786
|
|
|
|
|
|
|
# createUser |
787
|
|
|
|
|
|
|
# deleteGroup |
788
|
|
|
|
|
|
|
deleteIssue => {0 => \&_cast_issue_key}, |
789
|
|
|
|
|
|
|
# deletePermissionFrom |
790
|
|
|
|
|
|
|
# deletePermissionScheme |
791
|
|
|
|
|
|
|
# deleteProject |
792
|
|
|
|
|
|
|
deleteProjectAvatar => {0 => 'long'}, |
793
|
|
|
|
|
|
|
deleteProjectRole => {0 => \&_cast_remote_project_role, 1 => 'boolean'}, |
794
|
|
|
|
|
|
|
# deleteUser |
795
|
|
|
|
|
|
|
# deleteWorklogAndAutoAdjustRemainingEstimate |
796
|
|
|
|
|
|
|
# deleteWorklogAndRetainRemainingEstimate |
797
|
|
|
|
|
|
|
# deleteWorklogWithNewRemainingEstimate |
798
|
|
|
|
|
|
|
# editComment |
799
|
|
|
|
|
|
|
# getAllPermissions |
800
|
|
|
|
|
|
|
getAssociatedNotificationSchemes => {0 => \&_cast_remote_project_role}, |
801
|
|
|
|
|
|
|
getAssociatedPermissionSchemes => {0 => \&_cast_remote_project_role}, |
802
|
|
|
|
|
|
|
getAttachmentsFromIssue => {0 => \&_cast_issue_key}, |
803
|
|
|
|
|
|
|
getAvailableActions => {0 => \&_cast_issue_key}, |
804
|
|
|
|
|
|
|
getComment => {0 => 'long'}, |
805
|
|
|
|
|
|
|
getComments => {0 => \&_cast_issue_key}, |
806
|
|
|
|
|
|
|
# getComponents |
807
|
|
|
|
|
|
|
# getConfiguration |
808
|
|
|
|
|
|
|
# getCustomFields |
809
|
|
|
|
|
|
|
getDefaultRoleActors => {0 => \&_cast_remote_project_role}, |
810
|
|
|
|
|
|
|
# getFavouriteFilters |
811
|
|
|
|
|
|
|
getFieldsForAction => {0 => \&_cast_issue_key}, |
812
|
|
|
|
|
|
|
getFieldsForCreate => {1 => 'long'}, |
813
|
|
|
|
|
|
|
getFieldsForEdit => {0 => \&_cast_issue_key}, |
814
|
|
|
|
|
|
|
# getGroup |
815
|
|
|
|
|
|
|
getIssue => {0 => \&_cast_issue_key}, |
816
|
|
|
|
|
|
|
# getIssueById |
817
|
|
|
|
|
|
|
getIssueCountForFilter => {0 => \&_cast_filter_name_to_id}, |
818
|
|
|
|
|
|
|
getIssuesFromFilter => {0 => \&_cast_filter_name_to_id}, |
819
|
|
|
|
|
|
|
getIssuesFromFilterWithLimit => {0 => \&_cast_filter_name_to_id, 1 => 'int', 2 => 'int'}, |
820
|
|
|
|
|
|
|
getIssuesFromJqlSearch => {1 => 'int'}, |
821
|
|
|
|
|
|
|
# getIssuesFromTextSearch |
822
|
|
|
|
|
|
|
getIssuesFromTextSearchWithLimit => {1 => 'int', 2 => 'int'}, |
823
|
|
|
|
|
|
|
getIssuesFromTextSearchWithProject => {2 => 'int'}, |
824
|
|
|
|
|
|
|
# getIssueTypes |
825
|
|
|
|
|
|
|
# getIssueTypesForProject |
826
|
|
|
|
|
|
|
# getNotificationSchemes |
827
|
|
|
|
|
|
|
# getPermissionSchemes |
828
|
|
|
|
|
|
|
# getPriorities |
829
|
|
|
|
|
|
|
# getProjectAvatar |
830
|
|
|
|
|
|
|
getProjectAvatars => {1 => 'boolean'}, |
831
|
|
|
|
|
|
|
getProjectById => {0 => 'long'}, |
832
|
|
|
|
|
|
|
# getProjectByKey |
833
|
|
|
|
|
|
|
getProjectRole => {0 => 'long'}, |
834
|
|
|
|
|
|
|
getProjectRoleActors => {0 => \&_cast_remote_project_role}, |
835
|
|
|
|
|
|
|
# getProjectRoles |
836
|
|
|
|
|
|
|
# getProjectsNoSchemes |
837
|
|
|
|
|
|
|
getProjectWithSchemesById => {0 => 'long'}, |
838
|
|
|
|
|
|
|
getResolutionDateById => {0 => 'long'}, |
839
|
|
|
|
|
|
|
getResolutionDateByKey => {0 => \&_cast_issue_key}, |
840
|
|
|
|
|
|
|
# getResolutions |
841
|
|
|
|
|
|
|
# getSavedFilters |
842
|
|
|
|
|
|
|
getSecurityLevel => {0 => \&_cast_issue_key}, |
843
|
|
|
|
|
|
|
# getSecurityLevels |
844
|
|
|
|
|
|
|
# getSecuritySchemes |
845
|
|
|
|
|
|
|
# getServerInfo |
846
|
|
|
|
|
|
|
# getStatuses |
847
|
|
|
|
|
|
|
# getSubTaskIssueTypes |
848
|
|
|
|
|
|
|
# getSubTaskIssueTypesForProject |
849
|
|
|
|
|
|
|
# getUser |
850
|
|
|
|
|
|
|
# getVersions |
851
|
|
|
|
|
|
|
getWorklogs => {0 => \&_cast_issue_key}, |
852
|
|
|
|
|
|
|
hasPermissionToCreateWorklog => {0 => \&_cast_issue_key}, |
853
|
|
|
|
|
|
|
# hasPermissionToDeleteWorklog |
854
|
|
|
|
|
|
|
# hasPermissionToEditComment |
855
|
|
|
|
|
|
|
# hasPermissionToUpdateWorklog |
856
|
|
|
|
|
|
|
# isProjectRoleNameUnique |
857
|
|
|
|
|
|
|
# login ##NOT USED## |
858
|
|
|
|
|
|
|
# logout ##NOT USED## |
859
|
|
|
|
|
|
|
progressWorkflowAction => {0 => \&_cast_issue_key, 2 => \&_cast_remote_field_values}, |
860
|
|
|
|
|
|
|
# refreshCustomFields |
861
|
|
|
|
|
|
|
# releaseVersion |
862
|
|
|
|
|
|
|
removeActorsFromProjectRole => {1 => \&_cast_remote_project_role}, |
863
|
|
|
|
|
|
|
# removeAllRoleActorsByNameAndType |
864
|
|
|
|
|
|
|
# removeAllRoleActorsByProject |
865
|
|
|
|
|
|
|
removeDefaultActorsFromProjectRole => {1 => \&_cast_remote_project_role}, |
866
|
|
|
|
|
|
|
# removeUserFromGroup |
867
|
|
|
|
|
|
|
# setNewProjectAvatar |
868
|
|
|
|
|
|
|
setProjectAvatar => {1 => 'long'}, |
869
|
|
|
|
|
|
|
# setUserPassword |
870
|
|
|
|
|
|
|
# updateGroup |
871
|
|
|
|
|
|
|
updateIssue => {0 => \&_cast_issue_key, 1 => \&_cast_remote_field_values}, |
872
|
|
|
|
|
|
|
# updateProject |
873
|
|
|
|
|
|
|
updateProjectRole => {0 => \&_cast_remote_project_role}, |
874
|
|
|
|
|
|
|
# updateUser |
875
|
|
|
|
|
|
|
# updateWorklogAndAutoAdjustRemainingEstimate |
876
|
|
|
|
|
|
|
# updateWorklogAndRetainRemainingEstimate |
877
|
|
|
|
|
|
|
# updateWorklogWithNewRemainingEstimate |
878
|
|
|
|
|
|
|
); |
879
|
|
|
|
|
|
|
|
880
|
|
|
|
|
|
|
sub _cast_issue_key { |
881
|
|
|
|
|
|
|
my ($self, $issue) = @_; |
882
|
|
|
|
|
|
|
return ref $issue ? $issue->{key} : $issue; |
883
|
|
|
|
|
|
|
} |
884
|
|
|
|
|
|
|
|
885
|
|
|
|
|
|
|
sub _cast_remote_comment { |
886
|
|
|
|
|
|
|
my ($self, $arg) = @_; |
887
|
|
|
|
|
|
|
return ref $arg ? $arg : bless({body => $arg} => 'RemoteComment'); |
888
|
|
|
|
|
|
|
} |
889
|
|
|
|
|
|
|
|
890
|
|
|
|
|
|
|
sub _cast_filter_name_to_id { |
891
|
|
|
|
|
|
|
my ($self, $arg) = @_; |
892
|
|
|
|
|
|
|
is_string($arg) or croak "Filter arg must be a string.\n"; |
893
|
|
|
|
|
|
|
return $arg unless $arg =~ /\D/; |
894
|
|
|
|
|
|
|
my $filters = $self->get_favourite_filters(); |
895
|
|
|
|
|
|
|
exists $filters->{$arg} or croak "Unknown filter: $arg\n"; |
896
|
|
|
|
|
|
|
return $filters->{$arg}{id}; |
897
|
|
|
|
|
|
|
} |
898
|
|
|
|
|
|
|
|
899
|
|
|
|
|
|
|
sub _cast_remote_field_values { |
900
|
|
|
|
|
|
|
my ($self, $arg) = @_; |
901
|
|
|
|
|
|
|
return is_hash_ref($arg) ? [map {RemoteFieldValue->new($_, $arg->{$_})} keys %$arg] : $arg; |
902
|
|
|
|
|
|
|
} |
903
|
|
|
|
|
|
|
|
904
|
|
|
|
|
|
|
sub _cast_remote_project_role { |
905
|
|
|
|
|
|
|
my ($self, $arg) = @_; |
906
|
|
|
|
|
|
|
if (is_instance($arg => 'RemoteProjectRole') && exists $arg->{id} && is_string($arg->{id})) { |
907
|
|
|
|
|
|
|
$arg->{id} = SOAP::Data->type(long => $arg->{id}); |
908
|
|
|
|
|
|
|
} |
909
|
|
|
|
|
|
|
return $arg; |
910
|
|
|
|
|
|
|
} |
911
|
|
|
|
|
|
|
|
912
|
|
|
|
|
|
|
sub _cast_attachments { |
913
|
|
|
|
|
|
|
my ($self, $method, $args) = @_; |
914
|
|
|
|
|
|
|
# The addAttachmentsToIssue method is deprecated and requires too |
915
|
|
|
|
|
|
|
# much overhead to pass the file contents over the wire. Here we |
916
|
|
|
|
|
|
|
# convert the arguments to call the newer |
917
|
|
|
|
|
|
|
# addBase64EncodedAttachmentsToIssue method instead. |
918
|
|
|
|
|
|
|
require MIME::Base64; |
919
|
|
|
|
|
|
|
for my $content (@{$args->[2]}) { |
920
|
|
|
|
|
|
|
$content = MIME::Base64::encode_base64($content); |
921
|
|
|
|
|
|
|
} |
922
|
|
|
|
|
|
|
$$method = 'addBase64EncodedAttachmentsToIssue'; |
923
|
|
|
|
|
|
|
_cast_base64encodedattachments($self, $method, $args); |
924
|
|
|
|
|
|
|
return; |
925
|
|
|
|
|
|
|
} |
926
|
|
|
|
|
|
|
|
927
|
|
|
|
|
|
|
sub _cast_base64encodedattachments { |
928
|
|
|
|
|
|
|
my ($self, $method, $args) = @_; |
929
|
|
|
|
|
|
|
$args->[0] = _cast_issue_key($self, $args->[0]); |
930
|
|
|
|
|
|
|
# We have to set the names of the arrays and of its elements |
931
|
|
|
|
|
|
|
# because the default naming isn't properly understood by JIRA. |
932
|
|
|
|
|
|
|
for my $i (1 .. 2) { |
933
|
|
|
|
|
|
|
$args->[$i] = SOAP::Data->name( |
934
|
|
|
|
|
|
|
"array$i", |
935
|
|
|
|
|
|
|
[map {SOAP::Data->name("elem$i", $_)} @{$args->[$i]}], |
936
|
|
|
|
|
|
|
); |
937
|
|
|
|
|
|
|
} |
938
|
|
|
|
|
|
|
return; |
939
|
|
|
|
|
|
|
} |
940
|
|
|
|
|
|
|
|
941
|
|
|
|
|
|
|
# All methods follow the same call convention, which makes it easy to |
942
|
|
|
|
|
|
|
# implement them all with an AUTOLOAD. |
943
|
|
|
|
|
|
|
|
944
|
|
|
|
|
|
|
our $AUTOLOAD; |
945
|
|
|
|
|
|
|
sub AUTOLOAD { |
946
|
|
|
|
|
|
|
my ($self, @args) = @_; |
947
|
|
|
|
|
|
|
(my $method = $AUTOLOAD) =~ s/.*:://; |
948
|
|
|
|
|
|
|
|
949
|
|
|
|
|
|
|
# Perform any non-default type coersion |
950
|
|
|
|
|
|
|
if (my $typeof = $typeof{$method}) { |
951
|
|
|
|
|
|
|
if (is_hash_ref($typeof)) { |
952
|
|
|
|
|
|
|
while (my ($i, $type) = each %$typeof) { |
953
|
|
|
|
|
|
|
if (is_code_ref($type)) { |
954
|
|
|
|
|
|
|
$args[$i] = $type->($self, $args[$i]); |
955
|
|
|
|
|
|
|
} elsif (is_value($args[$i])) { |
956
|
|
|
|
|
|
|
$args[$i] = SOAP::Data->type($type => $args[$i]); |
957
|
|
|
|
|
|
|
} elsif (is_array_ref($args[$i])) { |
958
|
|
|
|
|
|
|
foreach (@{$args[$i]}) { |
959
|
|
|
|
|
|
|
$_ = SOAP::Data->type($type => $_); |
960
|
|
|
|
|
|
|
} |
961
|
|
|
|
|
|
|
} elsif (is_hash_ref($args[$i])) { |
962
|
|
|
|
|
|
|
foreach (values %{$args[$i]}) { |
963
|
|
|
|
|
|
|
$_ = SOAP::Data->type($type => $_); |
964
|
|
|
|
|
|
|
} |
965
|
|
|
|
|
|
|
} else { |
966
|
|
|
|
|
|
|
croak "Can't coerse argument $i of method $AUTOLOAD.\n"; |
967
|
|
|
|
|
|
|
} |
968
|
|
|
|
|
|
|
} |
969
|
|
|
|
|
|
|
} elsif (is_code_ref($typeof)) { |
970
|
|
|
|
|
|
|
$typeof->($self, \$method, \@args); |
971
|
|
|
|
|
|
|
} |
972
|
|
|
|
|
|
|
} |
973
|
|
|
|
|
|
|
|
974
|
|
|
|
|
|
|
my $call = $self->{soap}->call($method, $self->{auth}, @args); |
975
|
|
|
|
|
|
|
croak $call->faultcode(), ', ', $call->faultstring() |
976
|
|
|
|
|
|
|
if defined $call->fault(); |
977
|
|
|
|
|
|
|
return $call->result(); |
978
|
|
|
|
|
|
|
} |
979
|
|
|
|
|
|
|
|
980
|
|
|
|
|
|
|
|
981
|
|
|
|
|
|
|
1; # End of JIRA::Client |
982
|
|
|
|
|
|
|
|
983
|
|
|
|
|
|
|
__END__ |