PageRenderTime 54ms CodeModel.GetById 20ms app.highlight 27ms RepoModel.GetById 1ms app.codeStats 1ms

/Atlassian.Jira/Remote/IssueService.cs

https://bitbucket.org/dotnetmatt/atlassian
C# | 657 lines | 519 code | 112 blank | 26 comment | 51 complexity | 09aeb6fef31ac55554a85d7be6572daa MD5 | raw file
  1using System;
  2using System.Collections.Generic;
  3using System.Diagnostics;
  4using System.Linq;
  5using System.Threading;
  6using System.Threading.Tasks;
  7using Atlassian.Jira.Linq;
  8using Newtonsoft.Json;
  9using Newtonsoft.Json.Linq;
 10using RestSharp;
 11using System.Text.RegularExpressions;
 12
 13namespace Atlassian.Jira.Remote
 14{
 15    internal class IssueService : IIssueService
 16    {
 17        private const int DEFAULT_MAX_ISSUES_PER_REQUEST = 20;
 18
 19        private readonly Jira _jira;
 20        private readonly JiraRestClientSettings _restSettings;
 21        private readonly string[] _excludedFields = new string[] { "*all", "-comment", "-attachment", "-issuelinks", "-subtasks", "-watches", "-worklog" };
 22
 23        private JsonSerializerSettings _serializerSettings;
 24
 25        private class JqlOptions
 26        {
 27            public JqlOptions(string jql, CancellationToken token)
 28            {
 29                this.Jql = jql;
 30                this.Token = token;
 31            }
 32
 33            public string Jql { get; private set; }
 34            public CancellationToken Token { get; private set; }
 35            public int MaxIssuesPerRequest { get; set; } = DEFAULT_MAX_ISSUES_PER_REQUEST;
 36            public int StartAt { get; set; } = 0;
 37            public bool ValidateQuery { get; set; } = true;
 38        }
 39
 40        public IssueService(Jira jira, JiraRestClientSettings restSettings)
 41        {
 42            _jira = jira;
 43            _restSettings = restSettings;
 44        }
 45
 46        public JiraQueryable<Issue> Queryable
 47        {
 48            get
 49            {
 50                var translator = _jira.Services.Get<IJqlExpressionVisitor>();
 51                var provider = new JiraQueryProvider(translator, this);
 52                return new JiraQueryable<Issue>(provider);
 53            }
 54        }
 55
 56        public bool ValidateQuery { get; set; } = true;
 57
 58        public int MaxIssuesPerRequest { get; set; } = DEFAULT_MAX_ISSUES_PER_REQUEST;
 59
 60        private async Task<JsonSerializerSettings> GetIssueSerializerSettingsAsync(CancellationToken token)
 61        {
 62            if (this._serializerSettings == null)
 63            {
 64                var fieldService = _jira.Services.Get<IIssueFieldService>();
 65                var customFields = await fieldService.GetCustomFieldsAsync(token).ConfigureAwait(false);
 66                var remoteFields = customFields.Select(f => f.RemoteField);
 67
 68                var serializers = new Dictionary<string, ICustomFieldValueSerializer>(this._restSettings.CustomFieldSerializers, StringComparer.InvariantCultureIgnoreCase);
 69
 70                this._serializerSettings = new JsonSerializerSettings();
 71                this._serializerSettings.NullValueHandling = NullValueHandling.Ignore;
 72                this._serializerSettings.Converters.Add(new RemoteIssueJsonConverter(remoteFields, serializers));
 73            }
 74
 75            return this._serializerSettings;
 76        }
 77
 78        public async Task<Issue> GetIssueAsync(string issueKey, CancellationToken token = default(CancellationToken))
 79        {
 80            var excludedFields = String.Join(",", _excludedFields);
 81            var resource = $"rest/api/2/issue/{issueKey}?fields={excludedFields}";
 82            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
 83            var serializerSettings = await GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
 84            var issue = JsonConvert.DeserializeObject<RemoteIssueWrapper>(response.ToString(), serializerSettings);
 85
 86            return new Issue(_jira, issue.RemoteIssue);
 87        }
 88
 89        public Task<IPagedQueryResult<Issue>> GetIssuesFromJqlAsync(string jql, int? maxIssues = default(int?), int startAt = 0, CancellationToken token = default(CancellationToken))
 90        {
 91            return GetIssuesFromJqlAsync(new JqlOptions(jql, token)
 92            {
 93                MaxIssuesPerRequest = maxIssues ?? this.MaxIssuesPerRequest,
 94                StartAt = startAt,
 95                ValidateQuery = this.ValidateQuery
 96            });
 97        }
 98
 99        private async Task<IPagedQueryResult<Issue>> GetIssuesFromJqlAsync(JqlOptions options)
100        {
101            if (_jira.Debug)
102            {
103                Trace.WriteLine("[GetFromJqlAsync] JQL: " + options.Jql);
104            }
105
106            var parameters = new
107            {
108                jql = options.Jql,
109                startAt = options.StartAt,
110                maxResults = options.MaxIssuesPerRequest,
111                validateQuery = options.ValidateQuery,
112                fields = _excludedFields
113            };
114
115            var result = await _jira.RestClient.ExecuteRequestAsync(Method.POST, "rest/api/2/search", parameters, options.Token).ConfigureAwait(false);
116            var serializerSettings = await this.GetIssueSerializerSettingsAsync(options.Token).ConfigureAwait(false);
117            var issues = result["issues"]
118                .Cast<JObject>()
119                .Select(issueJson =>
120                {
121                    var remoteIssue = JsonConvert.DeserializeObject<RemoteIssueWrapper>(issueJson.ToString(), serializerSettings).RemoteIssue;
122                    return new Issue(_jira, remoteIssue);
123                });
124
125            return PagedQueryResult<Issue>.FromJson((JObject)result, issues);
126        }
127
128        public async Task UpdateIssueAsync(Issue issue, IssueUpdateOptions options, CancellationToken token = default(CancellationToken))
129        {
130            var resource = String.Format("rest/api/2/issue/{0}", issue.Key.Value);
131            if (options.SuppressEmailNotification)
132            {
133                resource += "?notifyUsers=false";
134            }
135            var fieldProvider = issue as IRemoteIssueFieldProvider;
136            var remoteFields = await fieldProvider.GetRemoteFieldValuesAsync(token).ConfigureAwait(false);
137            var remoteIssue = await issue.ToRemoteAsync(token).ConfigureAwait(false);
138            var fields = await this.BuildFieldsObjectFromIssueAsync(remoteIssue, remoteFields, token).ConfigureAwait(false);
139
140            await _jira.RestClient.ExecuteRequestAsync(Method.PUT, resource, new { fields = fields }, token).ConfigureAwait(false);
141        }
142
143        public Task UpdateIssueAsync(Issue issue, CancellationToken token = default(CancellationToken))
144        {
145            var options = new IssueUpdateOptions();
146            return UpdateIssueAsync(issue, options, token);
147        }
148
149        public async Task<string> CreateIssueAsync(Issue issue, CancellationToken token = default(CancellationToken))
150        {
151            var remoteIssue = await issue.ToRemoteAsync(token).ConfigureAwait(false);
152            var remoteIssueWrapper = new RemoteIssueWrapper(remoteIssue, issue.ParentIssueKey);
153            var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
154            var requestBody = JsonConvert.SerializeObject(remoteIssueWrapper, serializerSettings);
155
156            var result = await _jira.RestClient.ExecuteRequestAsync(Method.POST, "rest/api/2/issue", requestBody, token).ConfigureAwait(false);
157            return (string)result["key"];
158        }
159
160        private async Task<JObject> BuildFieldsObjectFromIssueAsync(RemoteIssue remoteIssue, RemoteFieldValue[] remoteFields, CancellationToken token)
161        {
162            var issueWrapper = new RemoteIssueWrapper(remoteIssue);
163            var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
164            var issueJson = JsonConvert.SerializeObject(issueWrapper, serializerSettings);
165
166            var fieldsJsonSerializerSettings = new JsonSerializerSettings()
167            {
168                DateParseHandling = DateParseHandling.None
169            };
170
171            var issueFields = JsonConvert.DeserializeObject<JObject>(issueJson, fieldsJsonSerializerSettings)["fields"] as JObject;
172            var updateFields = new JObject();
173
174            foreach (var field in remoteFields)
175            {
176                var issueFieldName = field.id;
177                var issueFieldValue = issueFields[issueFieldName];
178
179                if (issueFieldValue == null && issueFieldName.Equals("components", StringComparison.OrdinalIgnoreCase))
180                {
181                    // JIRA does not accept 'null' as a valid value for the 'components' field.
182                    //   So if the components field has been cleared it must be set to empty array instead.
183                    issueFieldValue = new JArray();
184                }
185
186                updateFields.Add(issueFieldName, issueFieldValue);
187            }
188
189            return updateFields;
190        }
191
192        public async Task ExecuteWorkflowActionAsync(Issue issue, string actionName, WorkflowTransitionUpdates updates, CancellationToken token = default(CancellationToken))
193        {
194            var actions = await this.GetActionsAsync(issue.Key.Value, token).ConfigureAwait(false);
195            var action = actions.FirstOrDefault(a => a.Name.Equals(actionName, StringComparison.OrdinalIgnoreCase));
196
197            if (action == null)
198            {
199                throw new InvalidOperationException(String.Format("Workflow action with name '{0}' not found.", actionName));
200            }
201
202            updates = updates ?? new WorkflowTransitionUpdates();
203
204            var resource = String.Format("rest/api/2/issue/{0}/transitions", issue.Key.Value);
205            var fieldProvider = issue as IRemoteIssueFieldProvider;
206            var remoteFields = await fieldProvider.GetRemoteFieldValuesAsync(token).ConfigureAwait(false);
207            var remoteIssue = await issue.ToRemoteAsync(token).ConfigureAwait(false);
208            var fields = await BuildFieldsObjectFromIssueAsync(remoteIssue, remoteFields, token).ConfigureAwait(false);
209            var updatesObject = new JObject();
210
211            if (!String.IsNullOrEmpty(updates.Comment))
212            {
213                updatesObject.Add("comment", new JArray(new JObject[]
214                {
215                    new JObject(new JProperty("add",
216                        new JObject(new JProperty("body", updates.Comment))))
217                }));
218            }
219
220            var requestBody = new
221            {
222                transition = new
223                {
224                    id = action.Id
225                },
226                update = updatesObject,
227                fields = fields
228            };
229
230            await _jira.RestClient.ExecuteRequestAsync(Method.POST, resource, requestBody, token).ConfigureAwait(false);
231        }
232
233        public async Task<IssueTimeTrackingData> GetTimeTrackingDataAsync(string issueKey, CancellationToken token = default(CancellationToken))
234        {
235            if (String.IsNullOrEmpty(issueKey))
236            {
237                throw new InvalidOperationException("Unable to retrieve time tracking data, make sure the issue has been created.");
238            }
239
240            var resource = String.Format("rest/api/2/issue/{0}?fields=timetracking", issueKey);
241            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
242
243            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
244            var timeTrackingJson = response["fields"]?["timetracking"];
245
246            if (timeTrackingJson != null)
247            {
248                return JsonConvert.DeserializeObject<IssueTimeTrackingData>(timeTrackingJson.ToString(), serializerSettings);
249            }
250            else
251            {
252                return null;
253            }
254        }
255
256        public async Task<IDictionary<string, IssueFieldEditMetadata>> GetFieldsEditMetadataAsync(string issueKey, CancellationToken token = default(CancellationToken))
257        {
258            var dict = new Dictionary<string, IssueFieldEditMetadata>();
259            var resource = String.Format("rest/api/2/issue/{0}/editmeta", issueKey);
260            var serializer = JsonSerializer.Create(_jira.RestClient.Settings.JsonSerializerSettings);
261            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
262            JObject fields = result["fields"].Value<JObject>();
263
264            foreach (var prop in fields.Properties())
265            {
266                var fieldName = (prop.Value["name"] ?? prop.Name).ToString();
267                dict.Add(fieldName, prop.Value.ToObject<IssueFieldEditMetadata>(serializer));
268            }
269
270            return dict;
271        }
272
273        public async Task<Comment> AddCommentAsync(string issueKey, Comment comment, CancellationToken token = default(CancellationToken))
274        {
275            if (String.IsNullOrEmpty(comment.Author))
276            {
277                throw new InvalidOperationException("Unable to add comment due to missing author field.");
278            }
279
280            var resource = String.Format("rest/api/2/issue/{0}/comment", issueKey);
281            var remoteComment = await _jira.RestClient.ExecuteRequestAsync<RemoteComment>(Method.POST, resource, comment.toRemote(), token).ConfigureAwait(false);
282            return new Comment(remoteComment);
283        }
284
285        public async Task<IPagedQueryResult<Comment>> GetPagedCommentsAsync(string issueKey, int? maxComments = default(int?), int startAt = 0, CancellationToken token = default(CancellationToken))
286        {
287            var resource = $"rest/api/2/issue/{issueKey}/comment?startAt={startAt}";
288
289            if (maxComments.HasValue)
290            {
291                resource += $"&maxResults={maxComments.Value}";
292            }
293
294            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource).ConfigureAwait(false);
295            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
296            var comments = result["comments"]
297                .Cast<JObject>()
298                .Select(commentJson =>
299                {
300                    var remoteComment = JsonConvert.DeserializeObject<RemoteComment>(commentJson.ToString(), serializerSettings);
301                    return new Comment(remoteComment);
302                });
303
304            return PagedQueryResult<Comment>.FromJson((JObject)result, comments);
305        }
306
307        public async Task<IEnumerable<JiraNamedEntity>> GetActionsAsync(string issueKey, CancellationToken token = default(CancellationToken))
308        {
309            var resource = String.Format("rest/api/2/issue/{0}/transitions", issueKey);
310            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
311            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
312            var remoteTransitions = JsonConvert.DeserializeObject<RemoteNamedObject[]>(result["transitions"].ToString(), serializerSettings);
313
314            return remoteTransitions.Select(transition => new JiraNamedEntity(transition));
315        }
316
317        public async Task<IEnumerable<Attachment>> GetAttachmentsAsync(string issueKey, CancellationToken token = default(CancellationToken))
318        {
319            var resource = String.Format("rest/api/2/issue/{0}?fields=attachment", issueKey);
320            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
321            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
322            var attachmentsJson = result["fields"]["attachment"];
323            var attachments = JsonConvert.DeserializeObject<RemoteAttachment[]>(attachmentsJson.ToString(), serializerSettings);
324
325            return attachments.Select(remoteAttachment => new Attachment(_jira, new WebClientWrapper(_jira), remoteAttachment));
326        }
327
328        public async Task<string[]> GetLabelsAsync(string issueKey, CancellationToken token = default(CancellationToken))
329        {
330            var resource = String.Format("rest/api/2/issue/{0}?fields=labels", issueKey);
331            var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
332            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource).ConfigureAwait(false);
333            var issue = JsonConvert.DeserializeObject<RemoteIssueWrapper>(response.ToString(), serializerSettings);
334            return issue.RemoteIssue.labels ?? new string[0];
335        }
336
337        public Task SetLabelsAsync(string issueKey, string[] labels, CancellationToken token = default(CancellationToken))
338        {
339            var resource = String.Format("rest/api/2/issue/{0}", issueKey);
340            return _jira.RestClient.ExecuteRequestAsync(Method.PUT, resource, new
341            {
342                fields = new
343                {
344                    labels = labels
345                }
346
347            }, token);
348        }
349
350        public async Task<IEnumerable<JiraUser>> GetWatchersAsync(string issueKey, CancellationToken token = default(CancellationToken))
351        {
352            if (string.IsNullOrEmpty(issueKey))
353            {
354                throw new InvalidOperationException("Unable to interact with the watchers resource, make sure the issue has been created.");
355            }
356
357            var resourceUrl = String.Format("rest/api/2/issue/{0}/watchers", issueKey);
358            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
359            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resourceUrl, null, token).ConfigureAwait(false);
360            var watchersJson = result["watchers"];
361            return watchersJson.Select(watcherJson => JsonConvert.DeserializeObject<JiraUser>(watcherJson.ToString(), serializerSettings));
362        }
363
364        public async Task<IEnumerable<IssueChangeLog>> GetChangeLogsAsync(string issueKey, CancellationToken token = default(CancellationToken))
365        {
366            var resourceUrl = String.Format("rest/api/2/issue/{0}?fields=created&expand=changelog", issueKey);
367            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
368            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resourceUrl, null, token).ConfigureAwait(false);
369            var result = Enumerable.Empty<IssueChangeLog>();
370            var changeLogs = response["changelog"];
371            if (changeLogs != null)
372            {
373                var histories = changeLogs["histories"];
374                if (histories != null)
375                {
376                    result = histories.Select(history => JsonConvert.DeserializeObject<IssueChangeLog>(history.ToString(), serializerSettings));
377                }
378            }
379
380            return result;
381        }
382
383        public Task DeleteWatcherAsync(string issueKey, string username, CancellationToken token = default(CancellationToken))
384        {
385            if (string.IsNullOrEmpty(issueKey))
386            {
387                throw new InvalidOperationException("Unable to interact with the watchers resource, make sure the issue has been created.");
388            }
389
390            var resourceUrl = String.Format("rest/api/2/issue/{0}/watchers?username={1}", issueKey, System.Uri.EscapeDataString(username));
391            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resourceUrl, null, token);
392        }
393
394        public Task AddWatcherAsync(string issueKey, string username, CancellationToken token = default(CancellationToken))
395        {
396            if (string.IsNullOrEmpty(issueKey))
397            {
398                throw new InvalidOperationException("Unable to interact with the watchers resource, make sure the issue has been created.");
399            }
400
401            var requestBody = String.Format("\"{0}\"", username);
402            var resourceUrl = String.Format("rest/api/2/issue/{0}/watchers", issueKey);
403            return _jira.RestClient.ExecuteRequestAsync(Method.POST, resourceUrl, requestBody, token);
404        }
405
406        public Task<IPagedQueryResult<Issue>> GetSubTasksAsync(string issueKey, int? maxIssues = default(int?), int startAt = 0, CancellationToken token = default(CancellationToken))
407        {
408            var jql = String.Format("parent = {0}", issueKey);
409            return GetIssuesFromJqlAsync(jql, maxIssues, startAt, token);
410        }
411
412        public Task AddAttachmentsAsync(string issueKey, UploadAttachmentInfo[] attachments, CancellationToken token = default(CancellationToken))
413        {
414            var resource = String.Format("rest/api/2/issue/{0}/attachments", issueKey);
415            var request = new RestRequest();
416            request.Method = Method.POST;
417            request.Resource = resource;
418            request.AddHeader("X-Atlassian-Token", "nocheck");
419            request.AlwaysMultipartFormData = true;
420
421            foreach (var attachment in attachments)
422            {
423                request.AddFile("file", attachment.Data, attachment.Name);
424            }
425
426            return _jira.RestClient.ExecuteRequestAsync(request, token);
427        }
428
429        public Task DeleteAttachmentAsync(string issueKey, string attachmentId, CancellationToken token = default(CancellationToken))
430        {
431            var resource = String.Format("rest/api/2/attachment/{0}", attachmentId);
432
433            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
434        }
435
436        public async Task<IDictionary<string, Issue>> GetIssuesAsync(IEnumerable<string> issueKeys, CancellationToken token = default(CancellationToken))
437        {
438            if (issueKeys.Any())
439            {
440                var distinctKeys = issueKeys.Distinct();
441                var jql = String.Format("key in ({0})", String.Join(",", distinctKeys));
442                var result = await this.GetIssuesFromJqlAsync(new JqlOptions(jql, token)
443                {
444                    MaxIssuesPerRequest = distinctKeys.Count(),
445                    ValidateQuery = false
446                }).ConfigureAwait(false);
447                return result.ToDictionary<Issue, string>(i => i.Key.Value);
448            }
449            else
450            {
451                return new Dictionary<string, Issue>();
452            }
453        }
454
455        public Task<IDictionary<string, Issue>> GetIssuesAsync(params string[] issueKeys)
456        {
457            return this.GetIssuesAsync(issueKeys, default(CancellationToken));
458        }
459
460        public async Task<IEnumerable<Comment>> GetCommentsAsync(string issueKey, CancellationToken token = default(CancellationToken))
461        {
462            var resource = String.Format("rest/api/2/issue/{0}/comment?expand=properties", issueKey);
463            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
464            var issueJson = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
465            var commentJson = issueJson["comments"];
466
467            var regex = new Regex(@"((Mon|Tues|Wednes|Thurs|Fri|Satur|Sun|Yester|To)day)\s(([0-9]{1,2}):([0-9]{2}))\s(AM|PM)");
468            var conmmentsWithConvertedDates = regex.Replace(commentJson.ToString(), delegate (Match m)
469            {
470                var date = new DateTime();
471                var time = new DateTime();
472                var day = m.Value.Split(' ')[0];
473
474                if (m.Value.StartsWith("Yesterday", StringComparison.InvariantCultureIgnoreCase) || m.Value.StartsWith("Today", StringComparison.InvariantCultureIgnoreCase))
475                {
476                    date = this.ParseDayOfWeekDate(day);
477                    time = this.ParseDayOfWeekTime(m, day);
478                }
479                else
480                {
481                    var dayOfWeek = this.DayOfWeekFromString(day);
482
483                    date = this.ParseDayOfWeekDate(dayOfWeek);
484                    time = this.ParseDayOfWeekTime(m, dayOfWeek);
485                }
486
487                var finalDateTime = new DateTime(date.Year, date.Month, date.Day, time.Hour, time.Minute, time.Second, time.Millisecond);
488
489                var result = finalDateTime.ToString("O");
490
491                return result;
492            });
493
494            // Convert back to Json, and make sure we still have a valid Json document
495            var jToken = JToken.Parse(conmmentsWithConvertedDates);
496
497            var remoteComments = JsonConvert.DeserializeObject<RemoteComment[]>(jToken.ToString(), serializerSettings);
498
499            return remoteComments.Select(c => new Comment(c));
500        }
501
502        /// <summary>
503        /// Converts an input string to the corresponding DayOfWeek enum.
504        /// </summary>
505        /// <param name="input">The input string.</param>
506        private DayOfWeek DayOfWeekFromString(string input)
507        {
508            return (DayOfWeek)Enum.Parse(typeof(DayOfWeek), input.Trim(), true);
509        }
510
511        /// <summary>
512        /// Finds the last date corresponding to the specified DayOfWeek.
513        /// For example if input is Monday, the function will return the date of the last Monday.
514        /// </summary>
515        /// <param name="dayOfWeek">The Day Of Week.</param>
516        private DateTime ParseDayOfWeekDate(DayOfWeek dayOfWeek)
517        {
518            var dtNow = DateTime.Now;
519
520            while (dtNow.DayOfWeek != dayOfWeek)
521            {
522                dtNow = dtNow.AddDays(-1);
523            }
524
525            return dtNow.Date;
526        }
527
528        /// <summary>
529        /// Finds Yesterday and Today dates.
530        /// </summary>
531        /// <param name="yesterdayOrToday">Values must be Yesterday or Today.</param>
532        private DateTime ParseDayOfWeekDate(string yesterdayOrToday)
533        {
534            if (string.IsNullOrWhiteSpace(yesterdayOrToday) || yesterdayOrToday != "Yesterday" || yesterdayOrToday != "Today")
535            {
536                throw new ArgumentOutOfRangeException($"The parameter yesterdayOrToday's value must be either Yesterday or Today. Current value: '{yesterdayOrToday}'.");
537            }
538
539            var dtNow = DateTime.Now;
540
541            if (yesterdayOrToday == "Yesterday")
542            {
543                dtNow = dtNow.AddDays(-1);
544            }
545
546            return dtNow.Date;
547        }
548
549        /// <summary>
550        /// Parses the time part of the Created or Updated date field.
551        /// </summary>
552        /// <param name="match">The regex match.</param>
553        /// <param name="dayOfWeek">The DayOfWeek.</param>
554        private DateTime ParseDayOfWeekTime(Match match, DayOfWeek dayOfWeek)
555        {
556            var time = new DateTime();
557
558            var timeParsed = DateTime.TryParse(match.Value.Replace(dayOfWeek.ToString(), string.Empty).Trim(), out time);
559            if (!timeParsed)
560            {
561                throw new ApplicationException($"Unable to parse time from input: '{match.Value}'");
562            }
563
564            return time;
565        }
566
567        /// <summary>
568        /// Parses the time part of the Created or Updated date field, when the day is specified as Yesterday or Today.
569        /// </summary>
570        /// <param name="match">The regex match.</param>
571        /// <param name="yesterdayOrToday">Values must be Yesterday or Today.</param>
572        private DateTime ParseDayOfWeekTime(Match match, string yesterdayOrToday)
573        {
574            if (string.IsNullOrWhiteSpace(yesterdayOrToday) || yesterdayOrToday != "Yesterday" || yesterdayOrToday != "Today")
575            {
576                throw new ArgumentOutOfRangeException($"The parameter yesterdayOrToday's value must be either Yesterday or Today. Current value: '{yesterdayOrToday}'.");
577            }
578
579            var time = new DateTime();
580
581            var timeParsed = DateTime.TryParse(match.Value.Replace(yesterdayOrToday, string.Empty).Trim(), out time);
582            if (!timeParsed)
583            {
584                throw new ApplicationException($"Unable to parse time from input: '{match.Value}'");
585            }
586
587            return time;
588        }
589
590        public Task DeleteCommentAsync(string issueKey, string commentId, CancellationToken token = default(CancellationToken))
591        {
592            var resource = String.Format("rest/api/2/issue/{0}/comment/{1}", issueKey, commentId);
593
594            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
595        }
596
597        public async Task<Worklog> AddWorklogAsync(string issueKey, Worklog worklog, WorklogStrategy worklogStrategy = WorklogStrategy.AutoAdjustRemainingEstimate, string newEstimate = null, CancellationToken token = default(CancellationToken))
598        {
599            var remoteWorklog = worklog.ToRemote();
600            string queryString = null;
601
602            if (worklogStrategy == WorklogStrategy.RetainRemainingEstimate)
603            {
604                queryString = "adjustEstimate=leave";
605            }
606            else if (worklogStrategy == WorklogStrategy.NewRemainingEstimate)
607            {
608                queryString = "adjustEstimate=new&newEstimate=" + Uri.EscapeDataString(newEstimate);
609            }
610
611            var resource = String.Format("rest/api/2/issue/{0}/worklog?{1}", issueKey, queryString);
612            var serverWorklog = await _jira.RestClient.ExecuteRequestAsync<RemoteWorklog>(Method.POST, resource, remoteWorklog, token).ConfigureAwait(false);
613            return new Worklog(serverWorklog);
614        }
615
616        public Task DeleteWorklogAsync(string issueKey, string worklogId, WorklogStrategy worklogStrategy = WorklogStrategy.AutoAdjustRemainingEstimate, string newEstimate = null, CancellationToken token = default(CancellationToken))
617        {
618            string queryString = null;
619
620            if (worklogStrategy == WorklogStrategy.RetainRemainingEstimate)
621            {
622                queryString = "adjustEstimate=leave";
623            }
624            else if (worklogStrategy == WorklogStrategy.NewRemainingEstimate)
625            {
626                queryString = "adjustEstimate=new&newEstimate=" + Uri.EscapeDataString(newEstimate);
627            }
628
629            var resource = String.Format("rest/api/2/issue/{0}/worklog/{1}?{2}", issueKey, worklogId, queryString);
630            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
631        }
632
633        public async Task<IEnumerable<Worklog>> GetWorklogsAsync(string issueKey, CancellationToken token = default(CancellationToken))
634        {
635            var resource = String.Format("rest/api/2/issue/{0}/worklog", issueKey);
636            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
637            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
638            var worklogsJson = response["worklogs"];
639            var remoteWorklogs = JsonConvert.DeserializeObject<RemoteWorklog[]>(worklogsJson.ToString(), serializerSettings);
640
641            return remoteWorklogs.Select(w => new Worklog(w));
642        }
643
644        public async Task<Worklog> GetWorklogAsync(string issueKey, string worklogId, CancellationToken token = default(CancellationToken))
645        {
646            var resource = String.Format("rest/api/2/issue/{0}/worklog/{1}", issueKey, worklogId);
647            var remoteWorklog = await _jira.RestClient.ExecuteRequestAsync<RemoteWorklog>(Method.GET, resource, null, token).ConfigureAwait(false);
648            return new Worklog(remoteWorklog);
649        }
650
651        public Task DeleteIssueAsync(string issueKey, CancellationToken token = default(CancellationToken))
652        {
653            var resource = String.Format("rest/api/2/issue/{0}", issueKey);
654            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
655        }
656    }
657}