FRENDS

Endpoint from scratch HowTo - Creating a new endpoint

Scenario: Get and put files via HTTP

You want to fetch a file with HTTP from a specific URL or POST files to a web page.

This example has been designed to work on an environment which has IIS 7 set up. A few tests have been written for the HttpEndPoint. The example solution location can be found here.

The unit tests for the end point can be found in the Frends.Cobalt.Extensions.Tests project's HttpEndPointTests.cs file and the integration tests are located in IntegrationTests/HttpEndPointWorkflowIntegrationTest.cs file. The integration tests test the EndPoint with the Cobalt workflow. The requirements for the test environment can be found from the beginning of the test files.

Basics

All endpoints have to be derived from the abstract EndPointBase class. Which is defined as following:
    public interface IEndPointDetails
    {
        /// <summary>
        /// The name of the file (or file mask) that this endpoint is configured to use. Meant for reporting purposes.
        /// </summary>
        string FileName { get; }

        /// <summary>
        /// The name of the directory this endpoint is configured to use. Meant for reporting purposes.
        /// </summary>
        string Dir { get; }

        /// <summary>
        /// The possible server address this endpoint is configured to use. Meant for reporting purposes.
        /// </summary>
        string Address { get; }

        /// <summary>
        /// The string representation of the type of transfer for the endpoint. Meant for reporting purposes.
        /// </summary>
        string TransferType { get; }
    }

	///<summary>
    /// The common interface for all Cobalt endpoints.
    /// This interface defines the methods needed for both source and destination endpoints
    ///</summary>
    public abstract class EndPointBase : IEndPointDetails, IDisposable
    {

        /// <summary>
        /// Returns the name or description of EndPoint
        /// </summary>
        /// <returns></returns>
        public virtual string GetEndPointName();
        ///<summary>
        /// Deletes the given remote file.        
        ///</summary>
        ///<param name="remoteFile">The name of the remote file to remove. 
        /// If this is a relative path ("foo/file.txt") or just a file name ("file.txt"), it will be based on the current directory.
        /// If you give an absolute path (that has a root, i.e. '/' or e.g. 'C:\'), it will be used as is              
        /// </param>
        public abstract void Delete(string remoteFile);

        ///<summary>
        /// Renames the remote file.
        /// <remarks>This method is called when: 
        /// <list type="bullet">
        /// <item>Starting the transfer of a file: renames the source file with a unique name in order to lock the file during transfer</item>
        /// <item>Executing the source file operation: Nothing renames the source temp file back to the original file name, Rename renames the source temp file to the given name</item>
        /// <item>Rolling back the transfer (due to an error): The temporary source file is renamed back to the original file name to release it.</item>
        /// <item>Ending the transfer: renames the temporary destination file (again, done to lock the file during transfer) to the final file name</item>
        /// <item></item>
        /// </list>
        /// </remarks>
        ///</summary>
        ///<param name="remoteFile">
        /// The name of the remote file to rename.
        /// If this is a relative path ("foo/file.txt") or just a file name ("file.txt"), it will be based on the current directory.
        /// If you give an absolute path (that has a root, i.e. '/' or e.g. 'C:\'), it will be used as is. 
        /// </param>
        ///<param name="toFile">
        /// The new name of the file.
        /// As with <see cref="remoteFile"/>, this can be either a relative or absolute path.
        /// </param>
        ///<returns>The full path to the renamed file (so it can be used in when executing the SourceOperation, Get etc.) </returns>
        public abstract string Rename(string remoteFile, string toFile);

        /// <summary>
        /// Moves the file to another directory
        /// <remarks>
        /// Used when executing the source file operation: Move moves the file to the given directory
        /// </remarks>
        /// </summary>
        /// <param name="remoteFile">Source file name</param>
        /// <param name="toPath">Full path, with filename, where to move source file after transfer</param>
        public abstract string Move(string remoteFile, string toPath);

        /// <summary>
        /// Opens the endpoint connection. 
        /// This method must be called before using the endpoint, and 
        /// after this method has been called, the user should be able to call other methods of the endpoint
        /// This can mean different things depending on the endpoint type, e.g.
        /// opening a connection, logging in and setting the default directory
        /// </summary>
        public abstract void Open();

        /// <summary>
        /// Closes the endpoint connection.
        /// When this method has been called, all internal connections should be disconnected and resources released.        
        /// </summary>
        public abstract void Close();

        /// <summary>
        /// Gets a file from the endpoint.
        /// Called for each file returned by <see cref="ListFiles"/>
        /// </summary>
        /// <param name="remoteFile">
        /// The name of the file to fetch from the source.
        /// Relative path ("foo/file.txt") or just a file name ("file.txt") will be based on the current directory.
        /// Absolute path (that has a root, i.e. '/' or e.g. 'C:\') will be used as is. 
        /// </param>
        ///  <param name="localFilePath">The full path to the local temporary file to store the file to</param>
        public abstract void Get(string remoteFile, string localFilePath);

        /// <summary>
        /// Creates and uploads a file to the endpoint         
        /// </summary>
        /// <param name="sourceFile">
        /// The full path to the local temporary file to upload (the result of possible transforms etc.)
        /// </param>
        /// <param name="remoteFile">
        /// The name of the file to create on the remote endpoint.
        /// Relative path ("foo/file.txt") or just a file name ("file.txt") will be based on the current directory.
        /// Absolute path (that has a root, i.e. '/' or e.g. 'C:\') will be used as is. 
        /// </param>
        public abstract void Put(string sourceFile, string remoteFile);

        /// <summary>
        /// Appends the content of the local source file to the destination file
        /// </summary>
        /// <param name="sourceFile">The full path to the local temporary file</param>
        /// <param name="remoteFile">The name of the file to append to on the remote endpoint.
        /// Relative path ("foo/file.txt") or just a file name ("file.txt") will be based on the current directory.
        /// Absolute path (that has a root, i.e. '/' or e.g. 'C:\') will be used as is. 
        /// </param>
        public abstract void Append(string sourceFile, string remoteFile);

        /// <summary>
        /// Gets the list of files from the source endpoint that match the <see cref="FileName"/> mask.
        /// The method skips all entries for the routine 
        /// </summary>
        /// <returns>The list of file details</returns>
        public abstract IList<FileItem> ListFiles();

        /// <summary>
        /// Checks if the given file exists on the remote endpoint
        /// </summary>
        /// <param name="remoteFilePath">Full path to the file to check</param>
        /// <returns>True if the file exists</returns>
        public abstract bool FileExists(string remoteFilePath);
    }

For this HTTP example, the most important methods to implement are:

If an endpoint can not support some of the methods defined in the base, it should throw an exception if those methods are called. In our HTTP example these unsupported methods are: These are not supported by HTTP alone and would have to have specific pages or web services to handle their functionality. The Put functionality also needs help to function and we have provided a simple example ASP .NET-page which can receive a file through a POST-request. This page can be found in the Frends.Cobalt.Extensions project.

HTTP example walkthrough

WebClientWrapper

We wrap the .NET class WebClient in our own wrapper WebClientWrapper which implements the interface IWebClientWrapper to make it mockable.
[assembly: InternalsVisibleTo("Frends.Cobalt.Extensions.Tests")]
namespace Frends.Cobalt.Extensions
{
    public class WebClientWrapper : IWebClientWrapper
    {
        private WebClient _client;


        public WebClientWrapper(WebClient client)
        {
            _client = client;
        }

        public void Dispose()
        {
            if(_client != null)
            {
                _client.Dispose();
                _client = null;
            }
            
        }

        public ICredentials Credentials
        {
            get { return _client.Credentials; }
            set { _client.Credentials = value; }
        }

        public byte[] DownloadData(Uri uri)
        {
            return _client.DownloadData(uri);
        }

        public byte[] UploadFile(Uri uri, string method, string file)
        {
            return _client.UploadFile(uri, method, file);
        }
    }

    public interface IWebClientWrapper
    {
        void Dispose();
        ICredentials Credentials { get; set; }
        byte[] DownloadData(Uri uri);
        byte[] UploadFile(Uri uri, string method, string file);
    }

Dispose method

Since the EndPointBase interface implements IDisposable we have to write a Dispose() method which frees the resources reserved by the class. This means we should dispose of all disposable classes we have initialized. In our case this means the WebClient.
        public override void Dispose()
        {
            Dispose(true);
        }

        private void Dispose(bool disposing)
        {
            if (!disposing)
                return;

            if (_webClient != null)
            {
                _webClient.Dispose();
                _webClient = null;
            }
        }

The properties

In addition to the properties inherited from EndPointBase class, HTTP endpoint has some other data stored in private fields as well. We get all of these from the TransferEndPointConfig, which is initialized from the config-xml, except for the WebClient.
        private readonly string _username;
        private readonly string _password;
        private readonly int _port;

        private IWebClientWrapper _webClient;

The constructors

There are two constructors, one to enable testing with a mocked WebClientWrapper and another one to implement the EndPointBase, which initializes a new WebClient. TransferEndPointConfig contains the basic connection information for the enpoint. If there were some extra parameters that we needed, we would parse them from the parameters string in the constructor, which is defined in the xml-configuration.
        internal HttpEndPoint(TransferEndPointConfig endPointConfig, IWebClientWrapper webClient) : base(endPointConfig)
        {
            _webClient = webClient;

            TransferType = "Http";
            _port = endPointConfig.ServerPort <= 0 ? 80 : endPointConfig.ServerPort;
            _username = endPointConfig.ServerUsername ?? "";
            _password = endPointConfig.ServerPassword ?? "";

            //If password or username defined, add them to the request
            SetPossibleCredentials();
        }

        public HttpEndPoint(TransferEndPointConfig endPointConfig) : this(endPointConfig, new WebClientWrapper(new WebClient())) { }

GetEndPointName -method

This returns a basic description of the endpoint, in our case we return the URL for the endpoint. It could contain more information, such as the file size if we knew it.

        public string GetEndPointName()
        {
            return GetUrl(FileName).ToString();
        }

Delete -method

This is the delete method used for deleting files at the endpoint, the workflow uses this with Source operation: Delete and Destination File Exists operation: Overwrite. The delete operation is not possible with the HTTP-endpoint, so an exception is thrown if it is called.
        public void Delete(string remoteFile)
        {
            //deleting is not possible
            throw new NotSupportedException("Deletion is not possible with HTTP Endpoint");
        }

Rename -method

The Rename -method is used while transferring files for locking purposes. Since renaming is not always possible with endpoints, it can be disabled in the configuration Paramaters section with the RenameSourceFileBeforeTransfer and RenameDestinationFileDuringTransfer -settings. Rename is not supported by HTTP, so an exception is thrown if it is called.
        public string Rename(string remoteFile, string toFile)
        {
            //rename is not possible
            throw new NotSupportedException("Renaming is not possible with HTTP Endpoint");
        }

Move -method

The Move -method is used with the Source Operation: Move to move the file that was transferred to another directory. This is not supported by the HTTP -endpoint so an exception is thrown when called.
        public string Move(string remoteFile, string toPath)
        {
            //Move is not possible
            throw new NotSupportedException("Moving files is not possible with HTTP Endpoint");
        }

Open/Close -methods

The open method is called when the workflow is started and close is called when the workflow has finished its transfer. They should contain the code for opening and closing the connection if needed. Our HTTP -endpoint does not need them because each transfer is handled in its own connection, so we do nothing in these methods.
        public void Open()
        {
            //Transfers are done in their own connections
        }

        public void Close()
        {
            //Transfers are done in their own connections
        }

GetUrl -utility method

This method does not belong to the interface and is used to build the url for the webpage. This is used to create the URI for the WebClient and with the GetEndPointName-method.
        internal Uri GetUrl(string remoteFile)
        {
            UriBuilder builder = new UriBuilder();
            builder.Host = Address;
            builder.Port = _port;
            builder.Scheme = "http";
            builder.Path = Path.Combine(Dir, remoteFile);

            return builder.Uri;
        }

Get -method

The Get -method contains the code used for getting the files from the source endpoint. In our HTTP -endpoint we use the .NET WebClient class to download the data from the URI.
        public void Get(string remoteFile, string localFilePath)
        {
            Uri uri = GetUrl(remoteFile);

            var fileBytes = _webClient.DownloadData(uri);

            File.WriteAllBytes(localFilePath, fileBytes);

        }

SetPossibleCredentials -utility method

This method does not belong to the interface and is used to setup the credentials to our WebClient if either username or password is provided.
        private void SetPossibleCredentials()
        {
            if (!String.IsNullOrEmpty(_username) || !String.IsNullOrEmpty(_password))
                _webClient.Credentials = new NetworkCredential(_username, _password);
        }

Put -method

The Put method contains the code used for sending the files to destination endpoint. The HTTP -endpoint simply uses the .NET WebClient class to POST the file the URI. The response is discarded, but it could be used to detect errors.
        public void Put(string sourceFile, string remoteFile)
        {
            Uri uri = GetUrl(remoteFile);

            var response = _webClient.UploadFile(uri, "POST", sourceFile); //TODO: Handle response, errors such as 404 are thrown out by the _webClient as exceptions
        }

Append -method

Append method is called if the Destination Exists operation: Append is selected and the destination file exists(FileExists returns true). The method should contain the code to append the content of the sourcefile to the destination file. The sourcefile is the path to the temporary local file and remoteFile is the path to the file on the remote machine. The HTTP -endpoint does not support append operations so it throws an exception if called.
        public void Append(string sourceFile, string remoteFile)
        {
            throw new NotSupportedException("Append is not supported by http");
        }

ListFiles -method

The ListFiles -method is used in the beginning of the workflow to retrieve the list of files to transfer from the source endpoint. It should handle matching the filenames to the Filemask and only return an IList of FileItems which will be transferred. The HTTP -endpoint as a source endpoint supports only the retrieval of a single file, so it returns the filename that was provided in the config.
        public IList<FileItem> ListFiles()
        {
            //HttpEndpoint can only transfer single files so we return the original FileName, 
            //which normally is used for reporting only. 
            //This example only supports getting a single file because implementing a directory
            //listing parser would be too tedious for the scope of this example.
            //TODO: Check if the FileName is a filemask and throw an exception if this is the case
            return new List<FileItem>() { new HttpFileItem(FileName) };
        }

FileExists -method

FileExists is called when transferring files to the destination endpoint before each transfer. If it returns true the workflow acts accordingly to the DestinationFileExists Operation setting in the config. The HTTP -endpoint always returns false because the destination page should always exist because it handles receiving the file data.
        public bool FileExists(string remoteFilePath)
        {
            //Destination filename is the page which is receiving the data with the POST
            //This method is used when checking if the remote file exists so we won't overwrite it
            //but remoteFilePath is a page which should always exist, and we cannot overwrite it
            //therefore we always return false so the Cobalt workflow will function correctly
            return false; 
        }

HttpFileItem -utility class

We create a new HttpFileItem class that inherits the FileItem class. We don't use the base class because most of the data defined is unavailable for us with the HTTP -endpoint. This is why we instantiate the FileItem object with some default values.
    public class HttpFileItem : FileItem
    {
        public HttpFileItem(string fileName)
        {
            //Using static values because we cannot get the file information without getting the whole file.            
            this.Modified = DateTime.MaxValue;
            this.Name = fileName;
            this.Size = -1;
        }
    }
}

Using the new module in FRENDS Cobalt

Step 1: Compile and deploy the assembly

In order to use the code with Cobalt you first need to compile it to an assembly and deploy the assembly somewhere FRENDS Iron can find the code. This means either the \Program Files\Frends Technology\FRENDS Iron -directory or the GAC

Step 2: Configure Cobalt

Once this is done, you need to configure Cobalt to use the new custom endpoint. To do this, just create a new Cobalt connection point as described in the manual and:

Sample configuration which gets the file from http://localhost:80/test/test.txt and POSTs it to http://localhost:80/test/HttpFileReceiver.aspx.

You could also put the whole address+query to Directory field (http://localhost:80/test/test.txt) and leave FileName empty (server address and port will be ignored).

Connection point with parameters

Step 3: Create and execute the file transfer routine

As in the manual create and execute file transfer routine.