diff --git a/TechTalk.JiraRestClient/FieldAttribute.cs b/TechTalk.JiraRestClient/FieldAttribute.cs new file mode 100644 index 0000000..1935f1c --- /dev/null +++ b/TechTalk.JiraRestClient/FieldAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace TechTalk.JiraRestClient +{ + public class FieldAttribute : Attribute + { + public string FieldName { get; set; } + + public FieldAttribute(string fieldName) + { + this.FieldName = fieldName; + } + } +} diff --git a/TechTalk.JiraRestClient/IJiraClient.cs b/TechTalk.JiraRestClient/IJiraClient.cs index a023181..0dc895d 100644 --- a/TechTalk.JiraRestClient/IJiraClient.cs +++ b/TechTalk.JiraRestClient/IJiraClient.cs @@ -9,8 +9,11 @@ namespace TechTalk.JiraRestClient /// Returns all issues for the given project IEnumerable> GetIssues(String projectKey); /// Returns all issues of the specified type for the given project - IEnumerable> GetIssues(String projectKey, String issueType); + IEnumerable> GetIssues(String projectKey, String issueType); /// Returns all issues of the given type and the given project filtered by the given JQL query + + IEnumerable> GetIssuesByQuery(string[] projects, string issueType, string jqlQuery); + IEnumerable> GetIssuesByQuery(String projectKey, String issueType, String jqlQuery); /// Enumerates through all issues for the given project IEnumerable> EnumerateIssues(String projectKey); diff --git a/TechTalk.JiraRestClient/JiraClient.cs b/TechTalk.JiraRestClient/JiraClient.cs index b13346e..f3de56a 100644 --- a/TechTalk.JiraRestClient/JiraClient.cs +++ b/TechTalk.JiraRestClient/JiraClient.cs @@ -7,6 +7,8 @@ using System.Text; using RestSharp; using RestSharp.Deserializers; +using RestSharp.Extensions; +using static System.String; namespace TechTalk.JiraRestClient { @@ -29,8 +31,10 @@ public JiraClient(string baseUrl, string username, string password) private RestRequest CreateRequest(Method method, String path) { - var request = new RestRequest { Method = method, Resource = path, RequestFormat = DataFormat.Json }; - request.AddHeader("Authorization", "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(String.Format("{0}:{1}", username, password)))); + var request = new RestRequest { Method = method, Resource = path }; + var encodedLogin = Encoding.ASCII.GetBytes($"{username}:{password}"); + var base64String = Convert.ToBase64String(encodedLogin); + request.AddHeader("Authorization", $"Basic {base64String}"); return request; } @@ -64,6 +68,46 @@ public IEnumerable> GetIssuesByQuery(string projectKey, stri return EnumerateIssuesInternal(projectKey, issueType, jqlQuery); } + public IEnumerable> GetIssuesByQuery(string[] projectes, string issueType, string jqlQuery) + { + return EnumerateIssuesInternal(projectes, issueType, jqlQuery); + } + + private IEnumerable> EnumerateIssuesInternal(string[] projectes, string issueType, string jqlQuery) + { + { + var queryCount = 50; + var resultCount = 0; + while (true) + { + var projectsClause = string.Join("%2C+", projectes.Select(p => $"%22{Uri.EscapeUriString(p)}%22")); + var jql = $"project+in+({projectsClause})"; + if (!IsNullOrEmpty(issueType)) + jql += $"+AND+issueType={Uri.EscapeUriString(issueType)}"; + if (!IsNullOrEmpty(jqlQuery)) + jql += $"+AND+{Uri.EscapeUriString(jqlQuery)}"; + var path = $"search?jql={jql}&startAt={resultCount}&maxResults={queryCount}"; + var request = CreateRequest(Method.GET, path); + + var response = ExecuteRequest(request); + AssertStatus(response, HttpStatusCode.OK); + + var data = deserializer.Deserialize>(response); + var issues = data.issues?.ToArray() ?? Enumerable.Empty>().ToArray(); + + foreach (var item in issues) + { + yield return item; + } + + resultCount += issues.Length; + + if (resultCount < data.total) continue; + else /* all issues received */ break; + } + } + } + public IEnumerable> EnumerateIssues(String projectKey) { return EnumerateIssues(projectKey, null); @@ -88,12 +132,12 @@ private IEnumerable> EnumerateIssuesInternal(String projectK var resultCount = 0; while (true) { - var jql = String.Format("project={0}", Uri.EscapeUriString(projectKey)); - if (!String.IsNullOrEmpty(issueType)) - jql += String.Format("+AND+issueType={0}", Uri.EscapeUriString(issueType)); - if (!String.IsNullOrEmpty(jqlQuery)) - jql += String.Format("+AND+{0}", Uri.EscapeUriString(jqlQuery)); - var path = String.Format("search?jql={0}&startAt={1}&maxResults={2}", jql, resultCount, queryCount); + var jql = Format("project={0}", Uri.EscapeUriString(projectKey)); + if (!IsNullOrEmpty(issueType)) + jql += Format("+AND+issueType={0}", Uri.EscapeUriString(issueType)); + if (!IsNullOrEmpty(jqlQuery)) + jql += Format("+AND+{0}", Uri.EscapeUriString(jqlQuery)); + var path = Format("search?jql={0}&startAt={1}&maxResults={2}", jql, resultCount, queryCount); var request = CreateRequest(Method.GET, path); var response = ExecuteRequest(request); @@ -119,7 +163,7 @@ public Issue LoadIssue(String issueRef) { try { - var path = String.Format("issue/{0}", issueRef); + var path = Format("issue/{0}", issueRef); var request = CreateRequest(Method.GET, path); var response = ExecuteRequest(request); @@ -163,11 +207,22 @@ public Issue CreateIssue(String projectKey, String issueType, TIss if (issueFields.timetracking != null) issueData.Add("timetracking", new { originalEstimate = issueFields.timetracking.originalEstimate }); - var propertyList = typeof(TIssueFields).GetProperties().Where(p => p.Name.StartsWith("customfield_")); - foreach (var property in propertyList) + var propertyInfos = typeof(TIssueFields).GetProperties(); + var propertiesFromAttribute = propertyInfos + .Select(p => new { Property = p, FieldAttribute = p.GetAttribute() }) + .Where(a => a.FieldAttribute != null) + .Select(p => new NamedProperty(p.Property, p.FieldAttribute.FieldName)); + + var customFields = propertyInfos + .Where(p => p.Name.StartsWith("customfield_")) + .Select(p => new NamedProperty(p, p.Name)); + + var propertyList = customFields.Concat(propertiesFromAttribute); + + foreach (var namedProperty in propertyList) { - var value = property.GetValue(issueFields, null); - if (value != null) issueData.Add(property.Name, value); + var value = namedProperty.Property.GetValue(issueFields, null); + if (value != null) issueData.Add(namedProperty.FieldName, value); } request.AddBody(new { fields = issueData }); @@ -189,7 +244,7 @@ public Issue UpdateIssue(Issue issue) { try { - var path = String.Format("issue/{0}", issue.JiraIdentifier); + var path = Format("issue/{0}", issue.JiraIdentifier); var request = CreateRequest(Method.PUT, path); request.AddHeader("ContentType", "application/json"); @@ -228,7 +283,7 @@ public void DeleteIssue(IssueRef issue) { try { - var path = String.Format("issue/{0}?deleteSubtasks=true", issue.id); + var path = Format("issue/{0}?deleteSubtasks=true", issue.id); var request = CreateRequest(Method.DELETE, path); var response = ExecuteRequest(request); @@ -246,7 +301,7 @@ public IEnumerable GetTransitions(IssueRef issue) { try { - var path = String.Format("issue/{0}/transitions?expand=transitions.fields", issue.id); + var path = Format("issue/{0}/transitions?expand=transitions.fields", issue.id); var request = CreateRequest(Method.GET, path); var response = ExecuteRequest(request); @@ -266,7 +321,7 @@ public Issue TransitionIssue(IssueRef issue, Transition transition { try { - var path = String.Format("issue/{0}/transitions", issue.id); + var path = Format("issue/{0}/transitions", issue.id); var request = CreateRequest(Method.POST, path); request.AddHeader("ContentType", "application/json"); @@ -294,7 +349,7 @@ public IEnumerable GetWatchers(IssueRef issue) { try { - var path = String.Format("issue/{0}/watchers", issue.id); + var path = Format("issue/{0}/watchers", issue.id); var request = CreateRequest(Method.GET, path); var response = ExecuteRequest(request); @@ -314,7 +369,7 @@ public IEnumerable GetComments(IssueRef issue) { try { - var path = String.Format("issue/{0}/comment", issue.id); + var path = Format("issue/{0}/comment", issue.id); var request = CreateRequest(Method.GET, path); var response = ExecuteRequest(request); @@ -334,7 +389,7 @@ public Comment CreateComment(IssueRef issue, String comment) { try { - var path = String.Format("issue/{0}/comment", issue.id); + var path = Format("issue/{0}/comment", issue.id); var request = CreateRequest(Method.POST, path); request.AddHeader("ContentType", "application/json"); request.AddBody(new Comment { body = comment }); @@ -355,7 +410,7 @@ public void DeleteComment(IssueRef issue, Comment comment) { try { - var path = String.Format("issue/{0}/comment/{1}", issue.id, comment.id); + var path = Format("issue/{0}/comment/{1}", issue.id, comment.id); var request = CreateRequest(Method.DELETE, path); var response = ExecuteRequest(request); @@ -378,7 +433,7 @@ public Attachment CreateAttachment(IssueRef issue, Stream fileStream, String fil { try { - var path = String.Format("issue/{0}/attachments", issue.id); + var path = Format("issue/{0}/attachments", issue.id); var request = CreateRequest(Method.POST, path); request.AddHeader("X-Atlassian-Token", "nocheck"); request.AddHeader("ContentType", "multipart/form-data"); @@ -400,7 +455,7 @@ public void DeleteAttachment(Attachment attachment) { try { - var path = String.Format("attachment/{0}", attachment.id); + var path = Format("attachment/{0}", attachment.id); var request = CreateRequest(Method.DELETE, path); var response = ExecuteRequest(request); @@ -470,7 +525,7 @@ public void DeleteIssueLink(IssueLink link) { try { - var path = String.Format("issueLink/{0}", link.id); + var path = Format("issueLink/{0}", link.id); var request = CreateRequest(Method.DELETE, path); var response = ExecuteRequest(request); @@ -488,7 +543,7 @@ public IEnumerable GetRemoteLinks(IssueRef issue) { try { - var path = string.Format("issue/{0}/remotelink", issue.id); + var path = Format("issue/{0}/remotelink", issue.id); var request = CreateRequest(Method.GET, path); request.AddHeader("ContentType", "application/json"); @@ -509,7 +564,7 @@ public RemoteLink CreateRemoteLink(IssueRef issue, RemoteLink remoteLink) { try { - var path = string.Format("issue/{0}/remotelink", issue.id); + var path = Format("issue/{0}/remotelink", issue.id); var request = CreateRequest(Method.POST, path); request.AddHeader("ContentType", "application/json"); request.AddBody(new @@ -545,7 +600,7 @@ public RemoteLink UpdateRemoteLink(IssueRef issue, RemoteLink remoteLink) { try { - var path = string.Format("issue/{0}/remotelink/{1}", issue.id, remoteLink.id); + var path = Format("issue/{0}/remotelink/{1}", issue.id, remoteLink.id); var request = CreateRequest(Method.PUT, path); request.AddHeader("ContentType", "application/json"); @@ -571,7 +626,7 @@ public void DeleteRemoteLink(IssueRef issue, RemoteLink remoteLink) { try { - var path = string.Format("issue/{0}/remotelink/{1}", issue.id, remoteLink.id); + var path = Format("issue/{0}/remotelink/{1}", issue.id, remoteLink.id); var request = CreateRequest(Method.DELETE, path); request.AddHeader("ContentType", "application/json"); diff --git a/TechTalk.JiraRestClient/NamedProperty.cs b/TechTalk.JiraRestClient/NamedProperty.cs new file mode 100644 index 0000000..dd65cdd --- /dev/null +++ b/TechTalk.JiraRestClient/NamedProperty.cs @@ -0,0 +1,17 @@ +namespace TechTalk.JiraRestClient +{ + using System.Reflection; + + internal class NamedProperty + { + public PropertyInfo Property { get; set; } + + public string FieldName { get; set; } + + public NamedProperty(PropertyInfo property, string fieldName) + { + this.Property = property; + this.FieldName = fieldName; + } + } +} \ No newline at end of file diff --git a/TechTalk.JiraRestClient/TechTalk.JiraRestClient.csproj b/TechTalk.JiraRestClient/TechTalk.JiraRestClient.csproj index cf1f1d6..783958c 100644 --- a/TechTalk.JiraRestClient/TechTalk.JiraRestClient.csproj +++ b/TechTalk.JiraRestClient/TechTalk.JiraRestClient.csproj @@ -41,7 +41,9 @@ + + diff --git a/TechTalk.JiraRestClient/TechTalk.JiraRestClient_NoNuget.csproj b/TechTalk.JiraRestClient/TechTalk.JiraRestClient_NoNuget.csproj new file mode 100644 index 0000000..03d3631 --- /dev/null +++ b/TechTalk.JiraRestClient/TechTalk.JiraRestClient_NoNuget.csproj @@ -0,0 +1,75 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {210529FA-454E-4C32-A2C8-353ECBD4DA05} + Library + Properties + TechTalk.JiraRestClient + TechTalk.JiraRestClient + v4.6.2 + + + 512 + ..\ + true + + + + + + AnyCPU + bin\Debug\ + DEBUG;TRACE + false + + + AnyCPU + bin\Release\ + TRACE + false + + + + ..\..\packages\RestSharp.104.1\lib\net4-client\RestSharp.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + \ No newline at end of file diff --git a/TechTalk.JiraRestClient/packages.config b/TechTalk.JiraRestClient/packages.config index a64d06c..3ef434c 100644 --- a/TechTalk.JiraRestClient/packages.config +++ b/TechTalk.JiraRestClient/packages.config @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file