using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Web; using System.Web.Caching; using System.Xml; using System.Xml.Xsl; using ScrewTurn.Wiki.PluginFramework; namespace ScrewTurn.Wiki.Plugins.PluginPack { /// /// Implements a formatter that display tickets from Unfuddle. /// public class UnfuddleTickets : IFormatterProviderV30 { private const string ConfigHelpHtmlValue = "Config consists of three lines:
<Url> - The base url to the Unfuddle API (i.e. http://account_name.unfuddle.com/api/v1/projects/project_ID)
<Username> - The username to the unfuddle account to use for authentication
<Password> - The password to the unfuddle account to use for authentication
"; private const string LoadErrorMessage = "Unable to load ticket report at this time."; private static readonly Regex UnfuddleRegex = new Regex(@"{unfuddle}", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly ComponentInformation Info = new ComponentInformation("Unfuddle Tickets", "ScrewTurn Software", "3.0.0.204", "http://www.screwturn.eu", "http://www.screwturn.eu/Version/PluginPack/UnfuddleTickets.txt"); private string _config; private IHostV30 _host; private string _baseUrl; private string _username; private string _password; #region IFormatterProviderV30 Members /// /// Specifies whether or not to execute Phase 1. /// public bool PerformPhase1 { get { return false; } } /// /// Specifies whether or not to execute Phase 2. /// public bool PerformPhase2 { get { return false; } } /// /// Specifies whether or not to execute Phase 3. /// public bool PerformPhase3 { get { return true; } } /// /// Gets the execution priority of the provider (0 lowest, 100 highest). /// public int ExecutionPriority { get { return 50; } } /// /// Gets the Information about the Provider. /// public ComponentInformation Information { get { return Info; } } /// /// Performs a Formatting phase. /// /// The raw content to Format. /// The Context information. /// The Phase. /// The Formatted content. public string Format(string raw, ContextInformation context, FormattingPhase phase) { var buffer = new StringBuilder(raw); var block = FindAndRemoveFirstOccurrence(buffer); if(block.Key != -1) { string unfuddleTickets = null; if(HttpContext.Current != null) unfuddleTickets = HttpContext.Current.Cache["UnfuddleTicketsStore"] as string; if(string.IsNullOrEmpty(unfuddleTickets)) unfuddleTickets = LoadUnfuddleTicketsFromWeb(); if(string.IsNullOrEmpty(unfuddleTickets)) unfuddleTickets = LoadErrorMessage; do { buffer.Insert(block.Key, unfuddleTickets); block = FindAndRemoveFirstOccurrence(buffer); } while(block.Key != -1); } return buffer.ToString(); } /// /// Prepares the title of an item for display (always during phase 3). /// /// The input title. /// The context information. /// The prepared title (no markup allowed). public string PrepareTitle(string title, ContextInformation context) { return title; } /// /// Initializes the Storage Provider. /// /// The Host of the Component. /// The Configuration data, if any. /// If the configuration string is not valid, the methoud should throw a . public void Init(IHostV30 host, string config) { _host = host; _config = config ?? string.Empty; var configEntries = _config.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); if(configEntries.Length != 3) throw new InvalidConfigurationException("Configuration missing required parameters."); _baseUrl = configEntries[0]; if(_baseUrl.EndsWith("/")) _baseUrl = _baseUrl.Substring(0, _baseUrl.Length - 1); _username = configEntries[1]; _password = configEntries[2]; } /// /// Method invoked on shutdown. /// /// This method might not be invoked in some cases. public void Shutdown() { // Nothing to do } /// /// Gets a brief summary of the configuration string format, in HTML. Returns null if no configuration is needed. /// public string ConfigHelpHtml { get { return ConfigHelpHtmlValue; } } #endregion #region Private Methods /// /// Finds and removes the first occurrence of the custom tag. /// /// The buffer. /// The index->content data. private static KeyValuePair FindAndRemoveFirstOccurrence(StringBuilder buffer) { Match match = UnfuddleRegex.Match(buffer.ToString()); if(match.Success) { buffer.Remove(match.Index, match.Length); return new KeyValuePair(match.Index, match.Value); } return new KeyValuePair(-1, null); } /// /// Builds an xml document from API calls to Unfuddle.com then runs them through an Xslt to format them. /// /// An html string that contains the tables to display the ticket information, or null private string LoadUnfuddleTicketsFromWeb() { var xml = BuildXmlFromApiCalls(); if(xml == null) return null; var settings = new XsltSettings { EnableScript = true, EnableDocumentFunction = true }; var xsl = new XslCompiledTransform(true); using(var reader = XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream("ScrewTurn.Wiki.Plugins.PluginPack.Resources.UnfuddleTickets.xsl"))) { xsl.Load(reader, settings, new XmlUrlResolver()); } string results; using(var sw = new StringWriter()) { using(var xnr = new XmlNodeReader(xml)) { xsl.Transform(xnr, null, sw); } results = sw.ToString(); } HttpContext.Current.Cache.Add("UnfuddleTicketsStore", results, null, DateTime.Now.AddMinutes(10), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); return results; } /// /// Builds 3 Xml Documents, the first two are lookups for Milestone, and People information, the second is the /// ticket information. /// /// private XmlDocument BuildXmlFromApiCalls() { var milestones = GetXml("/milestones", _username, _password); if(milestones == null) { LogWarning("Exception occurred while pulling unfuddled ticket information from the API."); return null; } var people = GetXml("/people", _username, _password); if(people == null) { LogWarning("Exception occurred while pulling unfuddled ticket information from the API."); return null; } var tickets = GetXml("/ticket_reports/dynamic?sort_by=priority&sort_direction=DESC&conditions_string=status-neq-closed&group_by=priority&fields_string=number,priority,summary,milestone,status,version", _username, _password); if(tickets == null) { LogWarning("Exception occurred while pulling unfuddled ticket information from the API."); return null; } var results = new XmlDocument(); results.AppendChild(results.CreateXmlDeclaration("1.0", "UTF-8", string.Empty)); var element = results.CreateElement("root"); results.AppendChild(element); element.AppendChild(results.ImportNode(milestones.ChildNodes[1], true)); element.AppendChild(results.ImportNode(people.ChildNodes[1], true)); element.AppendChild(results.ImportNode(tickets.ChildNodes[1], true)); return results; } /// /// Produces an API call, then returns the results as an Xml Document /// /// The Url to the specific API call /// An unfuddle account username /// The password to above unfuddle account /// private XmlDocument GetXml(string Url, string Username, string Password) { try { var results = new XmlDocument(); Url = string.Format("{0}{1}", _baseUrl, Url); var request = WebRequest.Create(Url); request.Credentials = new NetworkCredential(Username, Password); var response = request.GetResponse(); using(var reader = new StreamReader(response.GetResponseStream())) { var xmlString = reader.ReadToEnd(); try { results.LoadXml(xmlString); } catch { LogWarning("Received Unexpected Response from Unfuddle Server."); } } return results; } catch(Exception ex) { LogWarning(string.Format("Exception occurred: {0}", ex.Message)); return null; } } /// /// Logs a warning. /// /// The message. private void LogWarning(string message) { _host.LogEntry(message, LogEntryType.Warning, null, this); } #endregion } }