Build a Custom Publisher with qTrace Integration API 2.0

In last week’s post, I introduced the qTrace Integration API 2.0 and discussed the 2 separate APIs it provides: Publisher and Service Adapter. This week, let’s build a custom publisher using the Publisher API. The example is based on the FTP Publisher that was built with the qTrace Integration API 1.0.

Setup

Start Visual Studio .NET and create a new library project, name it Ftp.

Add references to the following assemblies (the first is shipped with qTrace, the rest are .NET assemblies):

  • qTrace.Publishing.Contracts
  • PresentationCore
  • PresentationFramework
  • WindowBase
  • System.Xaml
  • System.ComponentModel.Composition

Add the following post-build event to the project.

Create a custom publisher

Create a class named Ftp and implements the IPublisher interface. Make sure you include the ExportAttribute for the class.

namespace Ftp
{
    using System;
    using System.ComponentModel.Composition;
    using qTrace.Publishing.Contracts;

    [Export(typeof(IPublisher))]
    public class Ftp : IPublisher
    {
        public IPublishingContext Context { set; private get; }

        public PublisherInfo GetMetadata()
        {
            throw new NotImplementedException();
        }

        public VerificationResult Verify(PublisherSettings settings)
        {
            throw new NotImplementedException();
        }

        public PublishingResult Publish(PublisherSettings settings, PublishingRecord record, AttachmentFunc createAttachments)
        {
            throw new NotImplementedException();
        }
    }
}

Context

Recall from the last article, Context provides access to common functionality of qTrace such as showing progress indicator, interact with Dropbox etc. Because FTP operations usually take some time, so we will want to show the progress indicator. Also note that this property, as any other method of the publisher, must be thread-safe. I’ll simply use a monitor here.

private IPublishingContext _context;
private readonly object _contextLock = new object();

public IPublishingContext Context
{
    get { lock (_contextLock) return _context; }
    set { lock (_contextLock) _context = value; }
}

GetMetadata()

Now, let’s implement GetMetadata(). This method returns information that qTrace needs to know about the publisher in order to properly display it in the Settings screen. Because qTrace Integration API 2.0 supports custom setting fields, now we can enhance our FTP publisher to support Transfer Mode and SSL.

public PublisherInfo GetMetadata()
{
    return new PublisherInfo {
            SmallIconUri = "/Ftp;component/ftp.png",
            DisplayName = "FTP Publisher",
            HelpUrl = "https://www.qasymphony.com//building-a-custom-defect-submitter-for-qtrace.html",
            CustomSettingFields = new List {
                    new SettingField {
                            DisplayName = "Transfer Mode",
                            Name = "Mode",
                            FieldType = SettingFieldType.Dropdown,
                            AcceptedValues = new List {
                                new SettingAcceptedValue {Id = "Active", Value = "Active"},
                                new SettingAcceptedValue {Id = "Passive", Value = "Passive"}
                            }
                    },
                    new SettingField {
                            DisplayName = "Enable SSL",
                            Name = "Ssl",
                            FieldType = SettingFieldType.Checkbox
                    }
            }
    };
}

Some notes about the above implementation:

  • SmallIconUri specifies the path to an icon which will be bound to Source property of WFP Image element. In this example, I have an image with Built-Action set to Resource and located at the root of the Ftp project.
  • HelpUrl is an optional property indicating the URL where user will navigate to when clicking the Setup Help link in the Connection screen.
  • Our publisher adds two custom fields while retaining the standard Url, User Name and Password fields. The API allows developers to customize there as well. In addition, it also allows developers to indicate which fields are required and must be entered by users before the connection can be saved.

Build the project and run qTrace. Open the Settings screen. Click New Connection and you’ll see the FTP Publisher in the list of publishers. Select it will show the screen with 2 custom setting fields. Since we haven’t implemented Verify(), we can’t do anything with this screen yet.

Verify()

Let’s implement Verify(). This method attempts to connect to the server using the provided credentials and connection settings.

public VerificationResult Verify(PublisherSettings settings)
{
    Context.ShowProgressIndicator(message: "Connecting to FTP...");
    try {
        var ftpRequest = CreateRequest(settings.Url, WebRequestMethods.Ftp.ListDirectory, settings);
        using (ftpRequest.GetResponse()) {}
        return new VerificationResult();
    }
    finally {
        Context.HideProgressIndicator();
    }
}

private static FtpWebRequest CreateRequest(
    string uri, string method, PublisherSettings settings)
{
    var ftpRequest = (FtpWebRequest) WebRequest.Create(uri);
    ftpRequest.Credentials = new NetworkCredential(settings.UserName, settings.Password);
    ftpRequest.UsePassive = settings.CustomSettingsFieldsValues["Mode"] == "Passive";
    ftpRequest.EnableSsl = bool.Parse(settings.CustomSettingsFieldsValues["Ssl"]);
    ftpRequest.Timeout = settings.TimeoutInMillis;
    ftpRequest.Method = method;
    return ftpRequest;
}

Note that we use Context to display and hide the progress indicator. We also need not to perform the action in a background thread (to make sure the progress indicator, which is executing on the UI thread, keeps showing progress) because qTrace invokes this method from a threadpool thread.

Custom setting fields are accessed via PublisherSettings.CustomSettingsFieldsValues. All values are serialized as string, so we must make necessary conversion before using. qTrace uses returned value from this method to determine what to display to users. If it returns a VerificationResult, qTrace will show a success message (which can be overriden by setting VerificationResult.Message). Otherwise, if it returns null or there is an exception, qTrace will show an error message.

Build and run qTrace again. This time, input valid credentials and connection settings, you should have something like this:

Publish()

Now, if users capture a defect and click on the Submit button, there will be an exception. We need to implement the Publish() method

public PublishingResult Publish(PublisherSettings settings, PublishingRecord record, AttachmentFunc createAttachments)
{
    Context.ShowProgressIndicator(message: "Publishing to FTP...");
    try {
        var tasks = from attachment in createAttachments()
                    select Task.Factory.StartNew(() => Upload(settings, attachment));
        Task.WaitAll(tasks.ToArray());
        return new PublishingResult();
    }
    catch (AggregateException e) {
        throw new PublishingException(e.InnerExceptions[0].Message, e.InnerExceptions[0]);
    }
    finally {
        Context.HideProgressIndicator();
    }
}

private void Upload(PublisherSettings settings, string filePath)
{
    var uri = settings.Url +
                (settings.Url.EndsWith("/") ? string.Empty : "/") +
                Path.GetFileName(filePath);
    var ftpRequest = CreateRequest(uri, WebRequestMethods.Ftp.UploadFile, settings);
    using (Stream writer = ftpRequest.GetRequestStream()) {
        var fileContent = File.ReadAllBytes(filePath);
        writer.Write(fileContent, 0, fileContent.Length);
    }
}

First, we invoke the createAttachments delegate to generate qTrace output file(s). Then for each output file, we start a task to upload the file. We could do all the uploads in the calling thread (which is background thread as seen in Verify()), but concurrent uploads should be more efficient than serially performing network-bound operation with mostly idle CPU(s).

Besides, to make sure we cascade the appropriate status message to users, we wait for the task(s) to complete and throw an exception if there’s any error in any task. If everything’s fine, we return a PublishingResult to tell qTrace the operation was successful. We can override qTrace’s success and error message by setting the Message property of the returned PublishingResult or thrown exception. If we return null, qTrace will not show any message, assuming that the publisher already takes care of informing users about their action.

Build and run qTrace again, capture a defect and click Submit, we should see a success message from qTrace.

That’s everything. Below is the full code example for our FTP Publisher.

namespace Ftp
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Threading.Tasks;
    using qTrace.Publishing.Contracts;

    [Export(typeof(IPublisher))]
    public class Ftp : IPublisher
    {
        private IPublishingContext _context;
        private readonly object _contextLock = new object();

        public IPublishingContext Context
        {
            get { lock (_contextLock) return _context; }
            set { lock (_contextLock) _context = value; }
        }

        public PublisherInfo GetMetadata()
        {
            return new PublisherInfo {
                    SmallIconUri = "/Ftp;component/ftp.png",
                    DisplayName = "FTP Publisher",
                    HelpUrl = "https://www.qasymphony.com//building-a-custom-defect-submitter-for-qtrace.html",
                    CustomSettingFields = new List {
                            new SettingField {
                                    DisplayName = "Transfer Mode",
                                    Name = "Mode",
                                    FieldType = SettingFieldType.Dropdown,
                                    AcceptedValues = new List {
                                        new SettingAcceptedValue {Id = "Active", Value = "Active"},
                                        new SettingAcceptedValue {Id = "Passive", Value = "Passive"}
                                    }
                            },
                            new SettingField {
                                    DisplayName = "Enable SSL",
                                    Name = "Ssl",
                                    FieldType = SettingFieldType.Checkbox
                            }
                    }
            };
        }

        public VerificationResult Verify(PublisherSettings settings)
        {
            Context.ShowProgressIndicator(message: "Connecting to FTP...");
            try {
                var ftpRequest = CreateRequest(settings.Url, WebRequestMethods.Ftp.ListDirectory, settings);
                using (ftpRequest.GetResponse()) {}
                return new VerificationResult();
            }
            finally {
                Context.HideProgressIndicator();
            }
        }

        public PublishingResult Publish(PublisherSettings settings, PublishingRecord record, AttachmentFunc createAttachments)
        {
            Context.ShowProgressIndicator(message: "Publishing to FTP...");
            try {
                var tasks = from attachment in createAttachments()
                            select Task.Factory.StartNew(() => Upload(settings, attachment));
                Task.WaitAll(tasks.ToArray());
                return new PublishingResult();
            }
            catch (AggregateException e) {
                throw new PublishingException(e.InnerExceptions[0].Message, e.InnerExceptions[0]);
            }
            finally {
                Context.HideProgressIndicator();
            }
        }

        private void Upload(PublisherSettings settings, string filePath)
        {
            var uri = settings.Url +
                      (settings.Url.EndsWith("/") ? string.Empty : "/") +
                      Path.GetFileName(filePath);
            var ftpRequest = CreateRequest(uri, WebRequestMethods.Ftp.UploadFile, settings);
            using (Stream writer = ftpRequest.GetRequestStream()) {
                var fileContent = File.ReadAllBytes(filePath);
                writer.Write(fileContent, 0, fileContent.Length);
            }
        }

        private static FtpWebRequest CreateRequest(
            string uri, string method, PublisherSettings settings)
        {
            var ftpRequest = (FtpWebRequest) WebRequest.Create(uri);
            ftpRequest.Credentials = new NetworkCredential(settings.UserName, settings.Password);
            ftpRequest.UsePassive = settings.CustomSettingsFieldsValues["Mode"] == "Passive";
            ftpRequest.EnableSsl = bool.Parse(settings.CustomSettingsFieldsValues["Ssl"]);
            ftpRequest.Timeout = settings.TimeoutInMillis;
            ftpRequest.Method = method;
            return ftpRequest;
        }
    }
}

You can access to the full VS.NET project and qTrace.Publishers.Contracts.dll from GitHub. Again, note that this is a preview release of the API, by the time qTrace 2.6 is released, it might have had some changes. Please leave a comment or email info@qasymphony.com if you have any question/suggestion.

1 comment on Build a Custom Publisher with qTrace Integration API 2.0

  1. Do you mind if I quote a couple of your articles as long as I provide credit and sources back to your website? My website is in the exact same area of interest as yours and my visitors would really benefit from some of the information you present here. Please let me know if this alright with you. Thanks! North Platte Roofing Pro, 3765 E. Cavalry Hills Dr., North Platte, NE, 69101, US, 308-221-8734

Leave a Reply

Your email address will not be published. Required fields are marked *

More Great Content

Get Started with QASymphony