/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Windows.Forms; using JetBrains.Omea.Base; using JetBrains.Omea.Jiffa.JiraSoap; using JetBrains.Omea.Jiffa.Res; using JetBrains.Omea.Nntp; using JetBrains.Omea.OpenAPI; namespace JetBrains.Omea.Jiffa { public class Submission { private readonly JiraProject _project; protected string _title = ""; protected string _body = ""; protected JiraComponent _component; protected JiraIssueType _issuetype; /// /// Issue priority, Null for server default. /// protected JiraPriority _priority; protected string _buildnumber = ""; protected JiraStatus _status; protected string _assignee = ""; protected RemoteIssue _issue = null; protected string _originaluri = ""; protected StringBuilder _errorlog = new StringBuilder(); protected Dictionary _attachments = new Dictionary(); /// /// While submit is in progress, holds the progress dialog. /// Otherwise, Null. /// protected ProgressDialog _wndSubmitProgress = null; public Submission(JiraProject project) { _project = project; // Init the values Component = null; Status = null; Assignee = ""; BuildNumber = ""; if(Project.Server.IssueTypes.Count > 0) IssueType = Project.Server.IssueTypes[0]; Priority = null; // Server-defined } public string Title { get { return _title; } set { if(value == null) throw new ArgumentNullException(); if(value.Length == 0) throw new ArgumentException(Stringtable.Error_TitleEmpty); _title = value; } } public string Body { get { return _body; } set { if(value == null) throw new ArgumentNullException(); if(value.Length == 0) throw new ArgumentException(Stringtable.Error_BodyEmpty); _body = value; } } /// /// The component to which the issue will be submitted. /// Null is also allowed. /// public JiraComponent Component { get { return _component; } set { if(value != null) { if(value.Project != Project) throw new ArgumentException(Stringtable.Error_ComponentOfWrongProject); } _component = value; } } public JiraIssueType IssueType { get { return _issuetype; } set { if(value == null) throw new ArgumentNullException("IssueType"); if(value.Server != Project.Server) throw new ArgumentNullException(Stringtable.Error_IssueTypeOfWrongServer); _issuetype = value; } } /// /// Gets or sets the issue priority. May be Null for the server to use the default value. /// public JiraPriority Priority { get { return _priority; } set { if((value != null) && (value.Server != Project.Server)) throw new ArgumentNullException(Stringtable.Error_PriorityOfWrongServer); _priority = value; } } public string BuildNumber { get { return _buildnumber; } set { if(value == null) throw new ArgumentNullException("BuildNumber"); _buildnumber = value; } } public JiraProject Project { get { return _project; } } public JiraStatus Status { get { return _status; } set { if(value != null) { if(value.Server != Project.Server) throw new ArgumentException(Stringtable.Error_ComponentOfWrongProject); } _status = value; } } /// /// The assignee for the issue. /// May be an empty string, must not be Null. /// public string Assignee { get { return _assignee; } set { if(value == null) throw new ArgumentNullException("Assignee"); _assignee = value; } } /// /// The URI that has originated the issue. /// public string OriginalUri { get { return _originaluri; } set { _originaluri = value; } } /// /// Gets the error log of the instance. /// Logs the non-fatal errors (warnings). /// public StringBuilder ErrorLog { get { return _errorlog; } } /// /// Gets the most recently submitted issue. /// public RemoteIssue Issue { get { return _issue; } } /// /// Gets the list of attachments associated with this issue. /// public Dictionary Attachments { get { return _attachments; } } /// /// Reads the property values from a news article resource. /// public void ReadArticle(IResource res) { if(res == null) throw new ArgumentNullException("res"); if(res.Type != "Article") throw new ArgumentException("A news article resource of type “Article” expected.", "res"); Title = res.GetPropText(Core.Props.Subject); OriginalUri = CopyArticleURLAction.GetArticleUri(res); ReadArticle_ExtractBuildNumber(res); ReadArticle_PickAttachments(res); ReadArticle_FormatBody(res); } /// /// Extracts the attachments, including the names and their content. /// private void ReadArticle_PickAttachments(IResource res) { IResourceList resAttachments = res.GetLinksTo(null, "NewsAttachment"); foreach(IResource resAttachment in resAttachments) { // Att name string sName = resAttachment.DisplayName; // Prevent duplicate names int nMaxTries = 0x20; int a; for(a = 0; (a < nMaxTries) && ((string.IsNullOrEmpty(sName)) || _attachments.ContainsKey(sName)); a++) sName = sName + Jiffa.GetRandomName(); if(a >= nMaxTries) throw new InvalidOperationException("Could not chose an unique name for an attachment."); // Att content byte[] data = null; using(Stream datastream = resAttachment.GetBlobProp("Content")) { if(datastream != null) { data = new byte[datastream.Length]; datastream.Read(data, 0, data.Length); } } data = data ?? new byte[] {}; // Store _attachments.Add(sName, data); } } protected void ReadArticle_ExtractBuildNumber(IResource res) { BuildNumber = ""; if(string.IsNullOrEmpty(JiffaSettings.BuildNumberMask)) return; string sSubject = res.GetPropText(Core.Props.Subject); if(string.IsNullOrEmpty(sSubject)) return; string sBuildNumber = null; Regex regex = new Regex(JiffaSettings.BuildNumberMask); foreach(Match match in regex.Matches(sSubject)) { bool first = true; foreach(Group group in match.Groups) { // Skip the first group that represents the whole match if(first) { first = false; continue; } if(!string.IsNullOrEmpty(group.Value)) sBuildNumber = group.Value; } } if(string.IsNullOrEmpty(sBuildNumber)) return; BuildNumber = sBuildNumber; } /// /// Prepares the body. /// protected void ReadArticle_FormatBody(IResource res) { string sFromName = res.GetPropText("RawFrom"); string sContent = res.GetPropText(Core.Props.LongBody); IResourceList resNewsgroups = res.GetLinksFrom("NewsGroup", "Newsgroups"); if(resNewsgroups.Count == 0) throw new InvalidOperationException("Could not find a newsgroup for the article."); string sNewsgroupName = resNewsgroups[0].GetPropText(Core.Props.Name); StringBuilder sb = new StringBuilder(); sb.AppendLine("This issue has been created from an NNTP article."); sb.AppendLine("* Newsgroup: " + sNewsgroupName); sb.AppendLine("* From: " + sFromName); sb.AppendLine("* Link: " + OriginalUri); if(!string.IsNullOrEmpty(BuildNumber)) sb.AppendLine("* Build Number: " + BuildNumber); int nAttachment = 0; foreach(KeyValuePair pair in _attachments) { sb.AppendFormat("* Attachment[{0}]: {1} ({2})", nAttachment++, pair.Key, Utils.SizeToString(pair.Value.Length)); sb.AppendLine(); } sb.AppendLine(); sb.AppendLine("----"); sb.AppendLine(); sb.Append(sContent); Body = sb.ToString(); } public void ShowUI(IWin32Window window) { IssueView view = new IssueView(this); view.Show(window); } /// /// Submits the current configuration to the server. /// /// The parent widnow for the progress window. Null for no visible progress. public void Submit(IWin32Window parent) { if(!Core.UserInterfaceAP.IsOwnerThread) throw new InvalidOperationException("The submission must be initiated from the UI thread."); if(_wndSubmitProgress != null) throw new InvalidOperationException("A submission is already in progress."); _issue = null; ErrorLog.Length = 0; // Create the progress _wndSubmitProgress = new ProgressDialog(); _wndSubmitProgress.Text = Jiffa.Name + " – Submitting Issue"; _wndSubmitProgress.Image = JiffaIconProvider.LoadIcon("JiraIssue.ico").ToBitmap(); if(parent != null) _wndSubmitProgress.Show(parent); // Go to the network thread Core.NetworkAP.QueueJob("Submitting JIRA Issue.", (MethodInvoker)Submit_Run); } /// /// Implements the submission on the network thread. /// protected void Submit_Run() { if(!Core.NetworkAP.IsOwnerThread) throw new InvalidOperationException("The submission impl must be run on the network thread."); try { SetStatus("Collecting initial parameters"); RemoteIssue issue = new RemoteIssue(); issue.project = Project.Key; issue.reporter = Project.Server.Username; issue.type = IssueType.JiraId.ToString(); issue.summary = Title; issue.description = Body; if(Priority != null) issue.priority = Priority.JiraId.ToString(); if(Component != null) { RemoteComponent rc = new RemoteComponent(); rc.id = Component.JiraId; issue.components = new RemoteComponent[] {rc}; } if(Status != null) issue.status = Status.JiraId; issue.assignee = Assignee; // Create the issue! SetStatus("Creating issue carcass"); _issue = Project.Server.Service.createIssue(Project.Server.GetSignInToken(), issue); // In case JIRA rejects some of the issue params at the creation time, set them one-by-one Submit_Run_UpdateIssue(); // Submit the attachments AddAttachmentsToIssue(); SetStatus("Done"); } catch(Exception ex) { ErrorLog.AppendFormat("FATAL ERROR. {0}", ex.Message); } finally { Core.UserInterfaceAP.QueueJob("JIRA Issue Submission Done.", (MethodInvoker)Submit_Done); } } /// /// Run on the UI thread when the submission is done. /// protected void Submit_Done() { if(!Core.UserInterfaceAP.IsOwnerThread) throw new InvalidOperationException("The submission must be terminated on the UI thread."); if(_wndSubmitProgress == null) throw new InvalidOperationException("A submission is not in progress."); _wndSubmitProgress.Visible = false; _wndSubmitProgress.Dispose(); _wndSubmitProgress = null; // Notify of the attempt if(Issue != null) FireIssueSubmitted(); else FireIssueSubmissionFailed(); } /// /// While submitting, updates the status of the process. /// Can be invoked from any thread. Will be ignored if there is no progress. /// /// protected void SetStatus(string status) { // Transition to the UI thread if(!Core.UserInterfaceAP.IsOwnerThread) { Core.UserInterfaceAP.QueueJob("Set JIRA Issue Submission Status.", (StringDelegate)SetStatus, status); return; } if(_wndSubmitProgress == null) return; _wndSubmitProgress.StatusText = status; } protected delegate void StringDelegate(string s); /// /// In case JIRA rejects some of the issue params at the creation time, set them one-by-one /// Uses the for project and JIRA server, and for the issue-key to update. /// protected void Submit_Run_UpdateIssue() { SetStatus("Preparing fields for the issue update"); List arValues = new List(); // Generic fields if(Component != null) arValues.Add(CreateFieldValue(JiraIssueType.JiraIssueKeys.components, Component.JiraId)); if(Priority != null) arValues.Add(CreateFieldValue(JiraIssueType.JiraIssueKeys.priority, Priority.JiraId)); //arValues.Add(CreateFieldValue(JiraIssueType.JiraIssueKeys.reporter, Project.Server.Username)); arValues.Add(CreateFieldValue(JiraIssueType.JiraIssueKeys.type, IssueType.JiraId)); arValues.Add(CreateFieldValue(JiraIssueType.JiraIssueKeys.summary, Title)); arValues.Add(CreateFieldValue(JiraIssueType.JiraIssueKeys.description, Body)); if(Status != null) arValues.Add(CreateFieldValue(JiraIssueType.JiraIssueKeys.status, Status.JiraId)); arValues.Add(CreateFieldValue(JiraIssueType.JiraIssueKeys.assignee, Assignee)); // Custom fields UpdateIssue_SetCustomFields(arValues); // Submit the values to the server SetStatus("Updating issue fields"); foreach(RemoteFieldValue value in arValues) { try { SetStatus("Updating issue fields — " + value.id); Project.Server.Service.updateIssue(Project.Server.GetSignInToken(), Issue.key, new RemoteFieldValue[] {value}); } catch(Exception ex) { ErrorLog.AppendFormat("Could not set the “{0}” field on the issue. {1}", value.id, ex.Message); ErrorLog.AppendLine(); } } // Dirty hack: try opening the issue if(Status.Name == "Open") { try { SetStatus("Updating issue fields — Opening issue"); Project.Server.Service.progressWorkflowAction(Project.Server.GetSignInToken(), Issue.key, "Open", new RemoteFieldValue[] {}); } catch(Exception ex) { ErrorLog.AppendFormat("Could not open the issue. {0}", ex.Message); ErrorLog.AppendLine(); } } } /// /// A part of the fgunction. /// Writes the custom field values into the array. /// /// protected void UpdateIssue_SetCustomFields(List arValues) { SetStatus("Getting the list of editable fields"); // Get the list of available fields RemoteField[] jiraFieldsForEdit = Project.Server.Service.getFieldsForEdit(Project.Server.GetSignInToken(), Issue.key); Dictionary mapFieldNameToId = new Dictionary(jiraFieldsForEdit.Length); foreach(RemoteField field in jiraFieldsForEdit) mapFieldNameToId[field.name] = field.id; // Process the fields UpdateIssue_SetCustomFields_Add(arValues, mapFieldNameToId, JiffaSettings.CustomFieldNames_BuildNumber, BuildNumber); UpdateIssue_SetCustomFields_Add(arValues, mapFieldNameToId, JiffaSettings.CustomFieldNames_OriginalUri, OriginalUri); } /// /// Adds one single custom field for the function. /// protected void UpdateIssue_SetCustomFields_Add(List arValues, Dictionary mapFieldNameToId, string sFieldName, string sFieldValue) { if(string.IsNullOrEmpty(sFieldName)) return; if(string.IsNullOrEmpty(sFieldValue)) return; // Lookup ID by the name string sFieldId; if(!mapFieldNameToId.TryGetValue(sFieldName, out sFieldId)) { ErrorLog.AppendFormat("Could not find a custom field named “{0}” on the “{1}” JIRA server.", sFieldName, Project.Server.Name); ErrorLog.AppendLine(); return; } // Add to the submit-list arValues.Add(CreateFieldValue(sFieldId, sFieldValue)); } /// /// Adds the to the . /// protected void AddAttachmentsToIssue() { if(Attachments.Count == 0) return; SetStatus("Preparing the list of attachments"); foreach(KeyValuePair pair in Attachments) { try { // Due to an error in WSDL, convert the data to s-bytes from bytes SetStatus(string.Format("Adding attachments — {0} — {1}", pair.Key, "Copying")); sbyte[] datatemp = new sbyte[pair.Value.Length]; for(int a = 0; a < pair.Value.Length; a++) datatemp[a] = unchecked((sbyte)pair.Value[a]); SetStatus(string.Format("Adding attachments — {0} — {1}", pair.Key, "Sending")); Project.Server.Service.addAttachmentsToIssue(Project.Server.GetSignInToken(), Issue.key, new string[] {pair.Key}, datatemp); } catch(Exception ex) { ErrorLog.AppendFormat("Could not add the “{0}” attachment to the issue. {1}", pair.Key, ex.Message); ErrorLog.AppendLine(); } } } /// /// A helper for creating the remote field value. /// public static RemoteFieldValue CreateFieldValue(string id, params string[] values) { RemoteFieldValue retval = new RemoteFieldValue(); retval.id = id; retval.values = values; return retval; } /// /// A helper for creating the remote field value. /// public static RemoteFieldValue CreateFieldValue(JiraIssueType.JiraIssueKeys id, params string[] values) { RemoteFieldValue retval = new RemoteFieldValue(); retval.id = id.ToString(); retval.values = values; return retval; } /// /// Fires . /// protected void FireIssueSubmitted() { try { if(IssueSubmitted != null) IssueSubmitted(this, EventArgs.Empty); } catch(Exception ex) { Core.ReportException(ex, ExceptionReportFlags.AttachLog); } } /// /// Fires . /// protected void FireIssueSubmissionFailed() { try { if(IssueSubmissionFailed != null) IssueSubmissionFailed(this, EventArgs.Empty); } catch(Exception ex) { Core.ReportException(ex, ExceptionReportFlags.AttachLog); } } /// /// Fires after the issue was submitted successfully. /// public event EventHandler IssueSubmitted; /// /// Fires after the issue was attempted to be submitted, but such an attempt failed. /// public event EventHandler IssueSubmissionFailed; } }