PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/Atlassian.Jira/Remote/IssueService.cs

https://bitbucket.org/farmas/atlassian.net-sdk
C# | 685 lines | 565 code | 114 blank | 6 comment | 51 complexity | a7db2e1b29afcabe49fbee912bf115d5 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. using Atlassian.Jira.Linq;
  2. using Newtonsoft.Json;
  3. using Newtonsoft.Json.Linq;
  4. using RestSharp;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Collections.ObjectModel;
  8. using System.Diagnostics;
  9. using System.Linq;
  10. using System.Net;
  11. using System.Threading;
  12. using System.Threading.Tasks;
  13. namespace Atlassian.Jira.Remote
  14. {
  15. internal class IssueService : IIssueService
  16. {
  17. private const int DEFAULT_MAX_ISSUES_PER_REQUEST = 20;
  18. private const string ALL_FIELDS_QUERY_STRING = "*all";
  19. private readonly Jira _jira;
  20. private readonly JiraRestClientSettings _restSettings;
  21. private readonly string[] _excludedFields = new string[] { "comment", "attachment", "issuelinks", "subtasks", "watches", "worklog" };
  22. private JsonSerializerSettings _serializerSettings;
  23. public IssueService(Jira jira, JiraRestClientSettings restSettings)
  24. {
  25. _jira = jira;
  26. _restSettings = restSettings;
  27. }
  28. public JiraQueryable<Issue> Queryable
  29. {
  30. get
  31. {
  32. var translator = _jira.Services.Get<IJqlExpressionVisitor>();
  33. var provider = new JiraQueryProvider(translator, this);
  34. return new JiraQueryable<Issue>(provider);
  35. }
  36. }
  37. public bool ValidateQuery { get; set; } = true;
  38. public int MaxIssuesPerRequest { get; set; } = DEFAULT_MAX_ISSUES_PER_REQUEST;
  39. private async Task<JsonSerializerSettings> GetIssueSerializerSettingsAsync(CancellationToken token)
  40. {
  41. if (this._serializerSettings == null)
  42. {
  43. var fieldService = _jira.Services.Get<IIssueFieldService>();
  44. var customFields = await fieldService.GetCustomFieldsAsync(token).ConfigureAwait(false);
  45. var remoteFields = customFields.Select(f => f.RemoteField);
  46. var customFieldSerializers = new Dictionary<string, ICustomFieldValueSerializer>(this._restSettings.CustomFieldSerializers, StringComparer.InvariantCultureIgnoreCase);
  47. this._serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  48. this._serializerSettings.Converters.Add(new RemoteIssueJsonConverter(remoteFields, customFieldSerializers));
  49. }
  50. return this._serializerSettings;
  51. }
  52. public async Task<Issue> GetIssueAsync(string issueKey, CancellationToken token = default(CancellationToken))
  53. {
  54. var excludedFields = String.Join(",", _excludedFields.Select(field => $"-{field}"));
  55. var fields = $"{ALL_FIELDS_QUERY_STRING},{excludedFields}";
  56. var resource = $"rest/api/2/issue/{issueKey}?fields={fields}";
  57. var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
  58. var serializerSettings = await GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
  59. var issue = JsonConvert.DeserializeObject<RemoteIssueWrapper>(response.ToString(), serializerSettings);
  60. return new Issue(_jira, issue.RemoteIssue);
  61. }
  62. public Task<IPagedQueryResult<Issue>> GetIssuesFromJqlAsync(string jql, int? maxIssues = default(int?), int startAt = 0, CancellationToken token = default(CancellationToken))
  63. {
  64. var options = new IssueSearchOptions(jql)
  65. {
  66. MaxIssuesPerRequest = maxIssues,
  67. StartAt = startAt,
  68. ValidateQuery = this.ValidateQuery
  69. };
  70. return GetIssuesFromJqlAsync(options, token);
  71. }
  72. public async Task<IPagedQueryResult<Issue>> GetIssuesFromJqlAsync(IssueSearchOptions options, CancellationToken token = default(CancellationToken))
  73. {
  74. if (_jira.Debug)
  75. {
  76. Trace.WriteLine("[GetFromJqlAsync] JQL: " + options.Jql);
  77. }
  78. var fields = new List<string>();
  79. if (options.AdditionalFields == null || !options.AdditionalFields.Any())
  80. {
  81. fields.Add(ALL_FIELDS_QUERY_STRING);
  82. fields.AddRange(_excludedFields.Select(field => $"-{field}"));
  83. }
  84. else if (options.FetchBasicFields)
  85. {
  86. var excludedFields = _excludedFields.Where(excludedField => !options.AdditionalFields.Contains(excludedField, StringComparer.OrdinalIgnoreCase)).ToArray();
  87. fields.Add(ALL_FIELDS_QUERY_STRING);
  88. fields.AddRange(excludedFields.Select(field => $"-{field}"));
  89. }
  90. else
  91. {
  92. fields.AddRange(options.AdditionalFields.Select(field => field.Trim().ToLowerInvariant()));
  93. }
  94. var parameters = new
  95. {
  96. jql = options.Jql,
  97. startAt = options.StartAt,
  98. maxResults = options.MaxIssuesPerRequest ?? this.MaxIssuesPerRequest,
  99. validateQuery = options.ValidateQuery,
  100. fields = fields
  101. };
  102. var result = await _jira.RestClient.ExecuteRequestAsync(Method.POST, "rest/api/2/search", parameters, token).ConfigureAwait(false);
  103. var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
  104. var issues = result["issues"]
  105. .Cast<JObject>()
  106. .Select(issueJson =>
  107. {
  108. var remoteIssue = JsonConvert.DeserializeObject<RemoteIssueWrapper>(issueJson.ToString(), serializerSettings).RemoteIssue;
  109. return new Issue(_jira, remoteIssue);
  110. });
  111. return PagedQueryResult<Issue>.FromJson((JObject)result, issues);
  112. }
  113. public async Task UpdateIssueAsync(Issue issue, IssueUpdateOptions options, CancellationToken token = default(CancellationToken))
  114. {
  115. var resource = String.Format("rest/api/2/issue/{0}", issue.Key.Value);
  116. if (options.SuppressEmailNotification)
  117. {
  118. resource += "?notifyUsers=false";
  119. }
  120. var fieldProvider = issue as IRemoteIssueFieldProvider;
  121. var remoteFields = await fieldProvider.GetRemoteFieldValuesAsync(token).ConfigureAwait(false);
  122. var remoteIssue = await issue.ToRemoteAsync(token).ConfigureAwait(false);
  123. var fields = await this.BuildFieldsObjectFromIssueAsync(remoteIssue, remoteFields, token).ConfigureAwait(false);
  124. await _jira.RestClient.ExecuteRequestAsync(Method.PUT, resource, new { fields = fields }, token).ConfigureAwait(false);
  125. }
  126. public Task UpdateIssueAsync(Issue issue, CancellationToken token = default(CancellationToken))
  127. {
  128. var options = new IssueUpdateOptions();
  129. return UpdateIssueAsync(issue, options, token);
  130. }
  131. public async Task<string> CreateIssueAsync(Issue issue, CancellationToken token = default(CancellationToken))
  132. {
  133. var remoteIssue = await issue.ToRemoteAsync(token).ConfigureAwait(false);
  134. var remoteIssueWrapper = new RemoteIssueWrapper(remoteIssue, issue.ParentIssueKey);
  135. var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
  136. var requestBody = JsonConvert.SerializeObject(remoteIssueWrapper, serializerSettings);
  137. var result = await _jira.RestClient.ExecuteRequestAsync(Method.POST, "rest/api/2/issue", requestBody, token).ConfigureAwait(false);
  138. return (string)result["key"];
  139. }
  140. private async Task<JObject> BuildFieldsObjectFromIssueAsync(RemoteIssue remoteIssue, RemoteFieldValue[] remoteFields, CancellationToken token)
  141. {
  142. var issueWrapper = new RemoteIssueWrapper(remoteIssue);
  143. var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
  144. var issueJson = JsonConvert.SerializeObject(issueWrapper, serializerSettings);
  145. var fieldsJsonSerializerSettings = new JsonSerializerSettings()
  146. {
  147. DateParseHandling = DateParseHandling.None
  148. };
  149. var issueFields = JsonConvert.DeserializeObject<JObject>(issueJson, fieldsJsonSerializerSettings)["fields"] as JObject;
  150. var updateFields = new JObject();
  151. foreach (var field in remoteFields)
  152. {
  153. var issueFieldName = field.id;
  154. var issueFieldValue = issueFields[issueFieldName];
  155. if (issueFieldValue == null && issueFieldName.Equals("components", StringComparison.OrdinalIgnoreCase))
  156. {
  157. // JIRA does not accept 'null' as a valid value for the 'components' field.
  158. // So if the components field has been cleared it must be set to empty array instead.
  159. issueFieldValue = new JArray();
  160. }
  161. updateFields.Add(issueFieldName, issueFieldValue);
  162. }
  163. return updateFields;
  164. }
  165. public async Task ExecuteWorkflowActionAsync(Issue issue, string actionNameOrId, WorkflowTransitionUpdates updates, CancellationToken token = default(CancellationToken))
  166. {
  167. string actionId;
  168. if (int.TryParse(actionNameOrId, out int actionIdInt))
  169. {
  170. actionId = actionNameOrId;
  171. }
  172. else
  173. {
  174. var actions = await this.GetActionsAsync(issue.Key.Value, token).ConfigureAwait(false);
  175. var action = actions.FirstOrDefault(a => a.Name.Equals(actionNameOrId, StringComparison.OrdinalIgnoreCase));
  176. if (action == null)
  177. {
  178. throw new InvalidOperationException(String.Format("Workflow action with name '{0}' not found.", actionNameOrId));
  179. }
  180. actionId = action.Id;
  181. }
  182. updates = updates ?? new WorkflowTransitionUpdates();
  183. var resource = String.Format("rest/api/2/issue/{0}/transitions", issue.Key.Value);
  184. var fieldProvider = issue as IRemoteIssueFieldProvider;
  185. var remoteFields = await fieldProvider.GetRemoteFieldValuesAsync(token).ConfigureAwait(false);
  186. var remoteIssue = await issue.ToRemoteAsync(token).ConfigureAwait(false);
  187. var fields = await BuildFieldsObjectFromIssueAsync(remoteIssue, remoteFields, token).ConfigureAwait(false);
  188. var updatesObject = new JObject();
  189. if (!String.IsNullOrEmpty(updates.Comment))
  190. {
  191. updatesObject.Add("comment", new JArray(new JObject[]
  192. {
  193. new JObject(new JProperty("add",
  194. new JObject(new JProperty("body", updates.Comment))))
  195. }));
  196. }
  197. var requestBody = new
  198. {
  199. transition = new
  200. {
  201. id = actionId
  202. },
  203. update = updatesObject,
  204. fields = fields
  205. };
  206. await _jira.RestClient.ExecuteRequestAsync(Method.POST, resource, requestBody, token).ConfigureAwait(false);
  207. }
  208. public async Task<IssueTimeTrackingData> GetTimeTrackingDataAsync(string issueKey, CancellationToken token = default(CancellationToken))
  209. {
  210. if (String.IsNullOrEmpty(issueKey))
  211. {
  212. throw new InvalidOperationException("Unable to retrieve time tracking data, make sure the issue has been created.");
  213. }
  214. var resource = String.Format("rest/api/2/issue/{0}?fields=timetracking", issueKey);
  215. var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
  216. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  217. var timeTrackingJson = response["fields"]?["timetracking"];
  218. if (timeTrackingJson != null)
  219. {
  220. return JsonConvert.DeserializeObject<IssueTimeTrackingData>(timeTrackingJson.ToString(), serializerSettings);
  221. }
  222. else
  223. {
  224. return null;
  225. }
  226. }
  227. public async Task<IDictionary<string, IssueFieldEditMetadata>> GetFieldsEditMetadataAsync(string issueKey, CancellationToken token = default(CancellationToken))
  228. {
  229. var dict = new Dictionary<string, IssueFieldEditMetadata>();
  230. var resource = String.Format("rest/api/2/issue/{0}/editmeta", issueKey);
  231. var serializer = JsonSerializer.Create(_jira.RestClient.Settings.JsonSerializerSettings);
  232. var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
  233. JObject fields = result["fields"].Value<JObject>();
  234. foreach (var prop in fields.Properties())
  235. {
  236. var fieldName = (prop.Value["name"] ?? prop.Name).ToString();
  237. dict.Add(fieldName, new IssueFieldEditMetadata(prop.Value.ToObject<RemoteIssueFieldMetadata>(serializer)));
  238. }
  239. return dict;
  240. }
  241. public async Task<Comment> AddCommentAsync(string issueKey, Comment comment, CancellationToken token = default(CancellationToken))
  242. {
  243. if (String.IsNullOrEmpty(comment.Author))
  244. {
  245. throw new InvalidOperationException("Unable to add comment due to missing author field.");
  246. }
  247. var resource = String.Format("rest/api/2/issue/{0}/comment", issueKey);
  248. var remoteComment = await _jira.RestClient.ExecuteRequestAsync<RemoteComment>(Method.POST, resource, comment.ToRemote(), token).ConfigureAwait(false);
  249. return new Comment(remoteComment);
  250. }
  251. public async Task<Comment> UpdateCommentAsync(string issueKey, Comment comment, CancellationToken token = default(CancellationToken))
  252. {
  253. if (String.IsNullOrEmpty(comment.Id))
  254. {
  255. throw new InvalidOperationException("Unable to update comment due to missing id field.");
  256. }
  257. var resource = String.Format("rest/api/2/issue/{0}/comment/{1}", issueKey, comment.Id);
  258. var remoteComment = await _jira.RestClient.ExecuteRequestAsync<RemoteComment>(Method.PUT, resource, comment.ToRemote(), token).ConfigureAwait(false);
  259. return new Comment(remoteComment);
  260. }
  261. public async Task<IPagedQueryResult<Comment>> GetPagedCommentsAsync(string issueKey, int? maxComments = default(int?), int startAt = 0, CancellationToken token = default(CancellationToken))
  262. {
  263. var resource = $"rest/api/2/issue/{issueKey}/comment?startAt={startAt}";
  264. if (maxComments.HasValue)
  265. {
  266. resource += $"&maxResults={maxComments.Value}";
  267. }
  268. var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
  269. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  270. var comments = result["comments"]
  271. .Cast<JObject>()
  272. .Select(commentJson =>
  273. {
  274. var remoteComment = JsonConvert.DeserializeObject<RemoteComment>(commentJson.ToString(), serializerSettings);
  275. return new Comment(remoteComment);
  276. });
  277. return PagedQueryResult<Comment>.FromJson((JObject)result, comments);
  278. }
  279. public Task<IEnumerable<IssueTransition>> GetActionsAsync(string issueKey, CancellationToken token = default(CancellationToken))
  280. {
  281. return this.GetActionsAsync(issueKey, false, token);
  282. }
  283. public async Task<IEnumerable<IssueTransition>> GetActionsAsync(string issueKey, bool expandTransitionFields, CancellationToken token = default(CancellationToken))
  284. {
  285. var resource = $"rest/api/2/issue/{issueKey}/transitions";
  286. if (expandTransitionFields)
  287. {
  288. resource += "?expand=transitions.fields";
  289. }
  290. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  291. var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
  292. var remoteTransitions = JsonConvert.DeserializeObject<RemoteTransition[]>(result["transitions"].ToString(), serializerSettings);
  293. return remoteTransitions.Select(transition => new IssueTransition(transition));
  294. }
  295. public async Task<IEnumerable<Attachment>> GetAttachmentsAsync(string issueKey, CancellationToken token = default(CancellationToken))
  296. {
  297. var resource = String.Format("rest/api/2/issue/{0}?fields=attachment", issueKey);
  298. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  299. var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
  300. var attachmentsJson = result["fields"]["attachment"];
  301. var attachments = JsonConvert.DeserializeObject<RemoteAttachment[]>(attachmentsJson.ToString(), serializerSettings);
  302. return attachments.Select(remoteAttachment => new Attachment(_jira, remoteAttachment));
  303. }
  304. public async Task<string[]> GetLabelsAsync(string issueKey, CancellationToken token = default(CancellationToken))
  305. {
  306. var resource = String.Format("rest/api/2/issue/{0}?fields=labels", issueKey);
  307. var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
  308. var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource).ConfigureAwait(false);
  309. var issue = JsonConvert.DeserializeObject<RemoteIssueWrapper>(response.ToString(), serializerSettings);
  310. return issue.RemoteIssue.labels ?? new string[0];
  311. }
  312. public Task SetLabelsAsync(string issueKey, string[] labels, CancellationToken token = default(CancellationToken))
  313. {
  314. var resource = String.Format("rest/api/2/issue/{0}", issueKey);
  315. return _jira.RestClient.ExecuteRequestAsync(Method.PUT, resource, new
  316. {
  317. fields = new
  318. {
  319. labels = labels
  320. }
  321. }, token);
  322. }
  323. public async Task<IEnumerable<JiraUser>> GetWatchersAsync(string issueKey, CancellationToken token = default(CancellationToken))
  324. {
  325. if (string.IsNullOrEmpty(issueKey))
  326. {
  327. throw new InvalidOperationException("Unable to interact with the watchers resource, make sure the issue has been created.");
  328. }
  329. var resourceUrl = String.Format("rest/api/2/issue/{0}/watchers", issueKey);
  330. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  331. var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resourceUrl, null, token).ConfigureAwait(false);
  332. var watchersJson = result["watchers"];
  333. return watchersJson.Select(watcherJson => JsonConvert.DeserializeObject<JiraUser>(watcherJson.ToString(), serializerSettings));
  334. }
  335. public async Task<IEnumerable<IssueChangeLog>> GetChangeLogsAsync(string issueKey, CancellationToken token = default(CancellationToken))
  336. {
  337. var resourceUrl = String.Format("rest/api/2/issue/{0}?fields=created&expand=changelog", issueKey);
  338. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  339. var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resourceUrl, null, token).ConfigureAwait(false);
  340. var result = Enumerable.Empty<IssueChangeLog>();
  341. var changeLogs = response["changelog"];
  342. if (changeLogs != null)
  343. {
  344. var histories = changeLogs["histories"];
  345. if (histories != null)
  346. {
  347. result = histories.Select(history => JsonConvert.DeserializeObject<IssueChangeLog>(history.ToString(), serializerSettings));
  348. }
  349. }
  350. return result;
  351. }
  352. public Task DeleteWatcherAsync(string issueKey, string username, CancellationToken token = default(CancellationToken))
  353. {
  354. if (string.IsNullOrEmpty(issueKey))
  355. {
  356. throw new InvalidOperationException("Unable to interact with the watchers resource, make sure the issue has been created.");
  357. }
  358. var queryString = _jira.RestClient.Settings.EnableUserPrivacyMode ? "accountId" : "username";
  359. var resourceUrl = String.Format($"rest/api/2/issue/{issueKey}/watchers?{queryString}={System.Uri.EscapeUriString(username)}");
  360. return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resourceUrl, null, token);
  361. }
  362. public Task AddWatcherAsync(string issueKey, string username, CancellationToken token = default(CancellationToken))
  363. {
  364. if (string.IsNullOrEmpty(issueKey))
  365. {
  366. throw new InvalidOperationException("Unable to interact with the watchers resource, make sure the issue has been created.");
  367. }
  368. var requestBody = String.Format("\"{0}\"", username);
  369. var resourceUrl = String.Format("rest/api/2/issue/{0}/watchers", issueKey);
  370. return _jira.RestClient.ExecuteRequestAsync(Method.POST, resourceUrl, requestBody, token);
  371. }
  372. public Task<IPagedQueryResult<Issue>> GetSubTasksAsync(string issueKey, int? maxIssues = default(int?), int startAt = 0, CancellationToken token = default(CancellationToken))
  373. {
  374. var jql = String.Format("parent = {0}", issueKey);
  375. return GetIssuesFromJqlAsync(jql, maxIssues, startAt, token);
  376. }
  377. public Task AddAttachmentsAsync(string issueKey, UploadAttachmentInfo[] attachments, CancellationToken token = default(CancellationToken))
  378. {
  379. var resource = String.Format("rest/api/2/issue/{0}/attachments", issueKey);
  380. var request = new RestRequest();
  381. request.Method = Method.POST;
  382. request.Resource = resource;
  383. request.AddHeader("X-Atlassian-Token", "nocheck");
  384. request.AlwaysMultipartFormData = true;
  385. foreach (var attachment in attachments)
  386. {
  387. request.AddFile("file", attachment.Data, attachment.Name);
  388. }
  389. return _jira.RestClient.ExecuteRequestAsync(request, token);
  390. }
  391. public Task DeleteAttachmentAsync(string issueKey, string attachmentId, CancellationToken token = default(CancellationToken))
  392. {
  393. var resource = String.Format("rest/api/2/attachment/{0}", attachmentId);
  394. return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
  395. }
  396. public async Task<IDictionary<string, Issue>> GetIssuesAsync(IEnumerable<string> issueKeys, CancellationToken token = default(CancellationToken))
  397. {
  398. if (issueKeys.Any())
  399. {
  400. var distinctKeys = issueKeys.Distinct();
  401. var jql = String.Format("key in ({0})", String.Join(",", distinctKeys));
  402. var options = new IssueSearchOptions(jql)
  403. {
  404. MaxIssuesPerRequest = distinctKeys.Count(),
  405. ValidateQuery = false
  406. };
  407. var result = await this.GetIssuesFromJqlAsync(options, token).ConfigureAwait(false);
  408. return result.ToDictionary<Issue, string>(i => i.Key.Value);
  409. }
  410. else
  411. {
  412. return new Dictionary<string, Issue>();
  413. }
  414. }
  415. public Task<IDictionary<string, Issue>> GetIssuesAsync(params string[] issueKeys)
  416. {
  417. return this.GetIssuesAsync(issueKeys, default(CancellationToken));
  418. }
  419. public Task<IEnumerable<Comment>> GetCommentsAsync(string issueKey, CancellationToken token = default(CancellationToken))
  420. {
  421. var options = new CommentQueryOptions();
  422. options.Expand.Add("properties");
  423. return GetCommentsAsync(issueKey, options, token);
  424. }
  425. public async Task<IEnumerable<Comment>> GetCommentsAsync(string issueKey, CommentQueryOptions options, CancellationToken token = default(CancellationToken))
  426. {
  427. var resource = String.Format("rest/api/2/issue/{0}/comment", issueKey);
  428. if (options.Expand.Any())
  429. {
  430. resource += $"?expand={String.Join(",", options.Expand)}";
  431. }
  432. var issueJson = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
  433. var commentJson = issueJson["comments"];
  434. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  435. var remoteComments = JsonConvert.DeserializeObject<RemoteComment[]>(commentJson.ToString(), serializerSettings);
  436. return remoteComments.Select(c => new Comment(c));
  437. }
  438. public Task DeleteCommentAsync(string issueKey, string commentId, CancellationToken token = default(CancellationToken))
  439. {
  440. var resource = String.Format("rest/api/2/issue/{0}/comment/{1}", issueKey, commentId);
  441. return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
  442. }
  443. public async Task<Worklog> AddWorklogAsync(string issueKey, Worklog worklog, WorklogStrategy worklogStrategy = WorklogStrategy.AutoAdjustRemainingEstimate, string newEstimate = null, CancellationToken token = default(CancellationToken))
  444. {
  445. var remoteWorklog = worklog.ToRemote();
  446. string queryString = null;
  447. if (worklogStrategy == WorklogStrategy.RetainRemainingEstimate)
  448. {
  449. queryString = "adjustEstimate=leave";
  450. }
  451. else if (worklogStrategy == WorklogStrategy.NewRemainingEstimate)
  452. {
  453. queryString = "adjustEstimate=new&newEstimate=" + Uri.EscapeDataString(newEstimate);
  454. }
  455. var resource = String.Format("rest/api/2/issue/{0}/worklog?{1}", issueKey, queryString);
  456. var serverWorklog = await _jira.RestClient.ExecuteRequestAsync<RemoteWorklog>(Method.POST, resource, remoteWorklog, token).ConfigureAwait(false);
  457. return new Worklog(serverWorklog);
  458. }
  459. public Task DeleteWorklogAsync(string issueKey, string worklogId, WorklogStrategy worklogStrategy = WorklogStrategy.AutoAdjustRemainingEstimate, string newEstimate = null, CancellationToken token = default(CancellationToken))
  460. {
  461. string queryString = null;
  462. if (worklogStrategy == WorklogStrategy.RetainRemainingEstimate)
  463. {
  464. queryString = "adjustEstimate=leave";
  465. }
  466. else if (worklogStrategy == WorklogStrategy.NewRemainingEstimate)
  467. {
  468. queryString = "adjustEstimate=new&newEstimate=" + Uri.EscapeDataString(newEstimate);
  469. }
  470. var resource = String.Format("rest/api/2/issue/{0}/worklog/{1}?{2}", issueKey, worklogId, queryString);
  471. return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
  472. }
  473. public async Task<IEnumerable<Worklog>> GetWorklogsAsync(string issueKey, CancellationToken token = default(CancellationToken))
  474. {
  475. var resource = String.Format("rest/api/2/issue/{0}/worklog", issueKey);
  476. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  477. var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
  478. var worklogsJson = response["worklogs"];
  479. var remoteWorklogs = JsonConvert.DeserializeObject<RemoteWorklog[]>(worklogsJson.ToString(), serializerSettings);
  480. return remoteWorklogs.Select(w => new Worklog(w));
  481. }
  482. public async Task<Worklog> GetWorklogAsync(string issueKey, string worklogId, CancellationToken token = default(CancellationToken))
  483. {
  484. var resource = String.Format("rest/api/2/issue/{0}/worklog/{1}", issueKey, worklogId);
  485. var remoteWorklog = await _jira.RestClient.ExecuteRequestAsync<RemoteWorklog>(Method.GET, resource, null, token).ConfigureAwait(false);
  486. return new Worklog(remoteWorklog);
  487. }
  488. public Task DeleteIssueAsync(string issueKey, CancellationToken token = default(CancellationToken))
  489. {
  490. var resource = String.Format("rest/api/2/issue/{0}", issueKey);
  491. return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
  492. }
  493. public Task AssignIssueAsync(string issueKey, string assignee, CancellationToken token = default(CancellationToken))
  494. {
  495. var resource = $"/rest/api/2/issue/{issueKey}/assignee";
  496. object body = new { name = assignee };
  497. if (_jira.RestClient.Settings.EnableUserPrivacyMode)
  498. {
  499. body = new { accountId = assignee };
  500. }
  501. return _jira.RestClient.ExecuteRequestAsync(Method.PUT, resource, body, token);
  502. }
  503. public async Task<IEnumerable<string>> GetPropertyKeysAsync(string issueKey, CancellationToken token = default)
  504. {
  505. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  506. var serializer = JsonSerializer.Create(serializerSettings);
  507. var resource = $"rest/api/2/issue/{issueKey}/properties";
  508. var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
  509. var propertyRefsJson = response["keys"];
  510. var propertyRefs = propertyRefsJson.ToObject<IEnumerable<RemoteEntityPropertyReference>>(serializer);
  511. return propertyRefs.Select(x => x.key);
  512. }
  513. public async Task<ReadOnlyDictionary<string, JToken>> GetPropertiesAsync(string issueKey, IEnumerable<string> propertyKeys, CancellationToken token = default)
  514. {
  515. var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
  516. var serializer = JsonSerializer.Create(serializerSettings);
  517. var requestTasks = propertyKeys.Select((propertyKey) =>
  518. {
  519. // NOTE; There are no character limits on property keys
  520. var urlEncodedKey = WebUtility.UrlEncode(propertyKey);
  521. var resource = $"rest/api/2/issue/{issueKey}/properties/{urlEncodedKey}";
  522. return _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ContinueWith<JToken>(t =>
  523. {
  524. if (!t.IsFaulted)
  525. {
  526. return t.Result;
  527. }
  528. else if (t.Exception != null && t.Exception.InnerException is ResourceNotFoundException)
  529. {
  530. // WARN; Null result needs to be filtered out during processing!
  531. return null;
  532. }
  533. else
  534. {
  535. throw t.Exception;
  536. }
  537. });
  538. });
  539. var responses = await Task.WhenAll(requestTasks).ConfigureAwait(false);
  540. // NOTE; Response includes the key and value
  541. var transformedResponses = responses
  542. .Where(x => x != null)
  543. .Select(x => x.ToObject<RemoteEntityProperty>(serializer))
  544. .ToDictionary(x => x.key, x => x.value);
  545. return new ReadOnlyDictionary<string, JToken>(transformedResponses);
  546. }
  547. public Task SetPropertyAsync(string issueKey, string propertyKey, JToken obj, CancellationToken token = default)
  548. {
  549. if (propertyKey.Length <= 0 || propertyKey.Length >= 256)
  550. {
  551. throw new ArgumentOutOfRangeException(nameof(propertyKey), "PropertyKey length must be between 0 and 256 (both exclusive).");
  552. }
  553. var urlEncodedKey = WebUtility.UrlEncode(propertyKey);
  554. var resource = $"rest/api/2/issue/{issueKey}/properties/{urlEncodedKey}";
  555. return _jira.RestClient.ExecuteRequestAsync(Method.PUT, resource, obj, token);
  556. }
  557. public async Task DeletePropertyAsync(string issueKey, string propertyKey, CancellationToken token = default)
  558. {
  559. var urlEncodedKey = WebUtility.UrlEncode(propertyKey);
  560. var resource = $"rest/api/2/issue/{issueKey}/properties/{urlEncodedKey}";
  561. try
  562. {
  563. await _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token).ConfigureAwait(false);
  564. }
  565. catch (ResourceNotFoundException)
  566. {
  567. // No-op. The resource that we are trying to delete doesn't exist anyway.
  568. }
  569. }
  570. }
  571. }