Documentation Center

Creating a PublishPostProcess Plugin in C#

This example shows how to create a C# .NET PublishPostProcess plugin that parses the exported XML content files, extracts the text from a given XML element and saves it into a "generated" field in the corresponding metadata file. The plugin will only take files into account that have a certain ishtype attribute value in the metadata file.

Before you begin

The following products need to be already installed on your machine:
  • Microsoft .NET FrameWork
  • Microsoft Visual Studio

Procedure

  1. Create a Visual Studio Project. In this example, the project is called MyCustomPluginLibrary, and its type is Class Library.
  2. Add a reference to a plugin SDK (Trisoft.InfoShare.Plugins.SDK.dll). For example, you can copy the file from \Applications\Common\Trisoft.InfoShare.Plugins.SDK.dll.
    1. Browse to the project, then right-click References, and select Add Reference.
    2. To select the reference you want to add, use the Browse button.
    3. Select Trisoft.InfoShare.Plugins.SDK.dll, then Add.
    4. To be able to view the code documentation of the Trisoft.InfoShare.Plugins.SDK.dll, also copy Trisoft.InfoShare.Plugins.SDK.xml.
  3. Add a reference to a plugin common helper (Trisoft.InfoShare.Plugins.Common.dll). For example, you can copy the file from \Applications\Plugins\Trisoft.InfoShare.Plugins.Common.dll.
    1. Browse to the project, then right-click References, and select Add Reference.
    2. To select the reference you want to add, use the Browse button.
    3. Select Trisoft.InfoShare.Plugins.Common.dll, then Add.
    4. To be able to view the code documentation of the Trisoft.InfoShare.Plugins.Common.dll, also copy Trisoft.InfoShare.Plugins.Common.xml.
  4. Add a reference to the System.ComponentModel.Composition assembly.
  5. Add a reference to the System.Xml assembly.
  6. Create a new MyCustomPlugin class.
    The name of the plugin corresponds to the value assigned to ExportAttribute, and it needs to be unique.
    
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Xml;
    using System.Xml.Linq;
    using System.Xml.XPath;
    using Trisoft.InfoShare.Plugins.SDK;
    using Trisoft.InfoShare.Plugins.SDK.Publish.PostProcess;
    
    namespace MyCustomPluginLibrary
    {
        /// <summary>
        /// Custom plugin that will be triggered as part of the publish process
        /// </summary>
        // This attribute is used to make the class discoverable by the plugin engine.
        [Export("MyCustomPlugin", typeof(IPublishPostProcessPlugin))]
        [PartCreationPolicy(CreationPolicy.NonShared)]
        public class MyCustomPlugin : IPublishPostProcessPlugin
        {
            #region Private constants
            /// <summary>
            /// The key in the Items collection that is set by a previous plugin to indicate which type of objects to parse
            /// </summary>
            private static readonly string Items_In_IshTypesToProcess = "IshTypesToProcess";
            /// <summary>
            /// The key in the Items collection that we will set for a next plugin to say in which field name it can find the text data
            /// </summary>
            private static readonly string Items_Out_MetadataFieldName = "ExtractedTextMetadataFieldName";
            /// <summary>
            /// The key in the Items collection that we will set for a next plugin to say in which field level it can find the text data
            /// </summary>
            private static readonly string Items_Out_MetadataFieldLevel = "ExtractedTextMetadataFieldLevel";
            /// <summary>
            /// Name of the xml element to process
            /// </summary>
            public static readonly string ParameterName_ElementNameToProcess = "ElementNameToProcess";
            /// <summary>
            /// Name of the field to add in the metadata file
            /// </summary>
            public static readonly string ParameterName_MetadataFieldName = "MetadataFieldName";
            /// <summary>
            /// Level of the field to add in the metadata file
            /// </summary>
            public static readonly string ParameterName_MetadataFieldLevel = "MetadataFieldLevel";
            /// <summary>
            /// Level of the field to add in the metadata file
            /// </summary>
            public static readonly string DefaultMetadataFieldLevel = "generated";
            /// <summary>
            /// The file extension of the metadata file
            /// </summary>
            public static readonly string MetadataFileExtension = ".met";
            /// <summary>
            /// Check every x files whether the publication output was not cancelled
            /// </summary>
            public static readonly long CheckForCancelledBatchSize = 1000;
            #endregion
    
            #region Private fields
            /// <summary>
            /// The configuration
            /// </summary>
            private IPluginConfiguration _configuration;
            /// <summary>
            /// The name of the xml element of which to extract the text
            /// </summary>
            private string _elementNameToProcess;
            /// <summary>
            /// The name to set in the metadata file for the extract text
            /// </summary>
            private string _metadataFieldName;
            /// <summary>
            /// The level to set in the metadata file for the extract text
            /// </summary>
            private string _metadataFieldLevel;
            #endregion
    
            #region Public properties
            /// <summary>
            /// This property will be set by the plugin engine
            /// </summary>
            public string Name { get; set; }
            #endregion
    
            #region Public Methods
            /// <summary>
            /// This method will be called by the engine for every plugin after the instance of the plugin is created. 
            /// </summary>
            /// <param name="configuration">Plugin configuration</param>
            public void Initialize(IPluginConfiguration configuration)
            {
                _configuration = configuration;
                // Read the value of the required parameter "ElementNameToProcess" from the publish plugin xml configuration
                if (!_configuration.Parameters.TryGetValue(ParameterName_ElementNameToProcess, out _elementNameToProcess))
                {
                    // If parameter was not provided, throw an exception
                    var exception = new InvalidOperationException("NodeXPath parameter was not provided");
                    _configuration.LogService.ErrorException($"Plugin '{this.Name}' failed", exception);
                    throw exception;
                }
                // Read the value of the optional parameter from the publish plugin xml configuration
                if (!_configuration.Parameters.TryGetValue(ParameterName_MetadataFieldName, out _metadataFieldName))
                {
                    _metadataFieldName = _elementNameToProcess;
                }
                // Read the value of the optional parameter from the publish plugin xml configuration
                if (!_configuration.Parameters.TryGetValue(ParameterName_MetadataFieldLevel, out _metadataFieldLevel))
                {
                    _metadataFieldLevel = DefaultMetadataFieldLevel;
                }
            }
    
            /// <summary>
            /// This method will be called by the engine as part of a main action
            /// </summary>
            /// <param name="context"></param>
            public void Run(IPublishPostProcessContext context)
            {
                _configuration.LogService.Debug("Started plugin {0}", Name);
    
                // Read the values of the ishtype set by a previous plugin we would like to process in this plugin
                List<string> ishTypesToProcess = new List<string>();
                object value;
                if (!context.Items.TryGetValue(Items_In_IshTypesToProcess, out value))
                {
                    ishTypesToProcess.Add("ISHIllustration");
                    ishTypesToProcess.Add("ISHLibrary");
                    ishTypesToProcess.Add("ISHModule");
                    ishTypesToProcess.Add("ISHMasterDoc");
                    ishTypesToProcess.Add("ISHTemplate");
                }
                else
                {
                    ishTypesToProcess = (List<string>)value;
                }
    
                var inputDir = context.RootWorkDirectory;
                var xmlFiles = inputDir.EnumerateFiles("*.xml", SearchOption.AllDirectories);
    
                int filesTotalCounter = xmlFiles.Count();
                int filesDoneCounter = 0;
                foreach (FileInfo xmlFile in xmlFiles)
                {
                    string metadataFileName = Path.ChangeExtension(xmlFile.FullName, MetadataFileExtension);
                    if (File.Exists(metadataFileName))
                    {
                        var xmlMetadata = XDocument.Load(metadataFileName);
                        var attr = ((IEnumerable)xmlMetadata.XPathEvaluate("/ishobject/@ishtype")).Cast<XAttribute>().FirstOrDefault();
                        if (attr != null)
                        {
                            string ishObjectType = attr.Value;
                            if (ishTypesToProcess.Contains(ishObjectType))
                            {
                                string text = ParseFile(xmlFile.FullName);
                                xmlMetadata.XPathSelectElement("/ishobject/ishfields").Add(
                                   new XElement("ishfield",
                                    new XAttribute("name", _metadataFieldName),
                                    new XAttribute("level", _metadataFieldLevel),
                                    text));
                                xmlMetadata.Save(metadataFileName);
                            }
                        }
                    }
    
                    filesDoneCounter++;
    
                    if (filesDoneCounter % CheckForCancelledBatchSize == 0)
                    {
                        if (context.IsCancelled())
                        {
                            // throw new PluginDetectedUserCancelException();
                        }
                    }
    
                    // Using 100 here, but should actually calculate this value so you would only get 10 lines in event monitor
                    if (filesDoneCounter % 100 == 0)
                    {
                        context.EventMonitor.AddEventDetail(context.ProgressId, EventLevel.Verbose,
                              "Processing files", $"Processed {filesDoneCounter}/{filesTotalCounter} files",
                              DetailStatus.Success);
                    }
                }
    
                if (filesDoneCounter % 100 != 0)
                {
                    // Make sure to log the last set of files
                    context.EventMonitor.AddEventDetail(context.ProgressId, EventLevel.Verbose,
                       "Processing files", $"Processed {filesDoneCounter}/{filesTotalCounter} files",
                       DetailStatus.Success);
                }
    
                // Add a message to the logging pipeline
                _configuration.LogService.Info(this.Name + " plugin executed successfully.");
    
                context.Items.Add(Items_Out_MetadataFieldName, _metadataFieldName);
                context.Items.Add(Items_Out_MetadataFieldLevel, _metadataFieldLevel);
            }
    
            /// <summary>
            /// This method will be called by the plugin engine after all plugins have executed 
            /// </summary>
            public void Dispose()
            {
                // We don't need a cleanup
            }
    
            #endregion
    
            #region Private methods
            /// <summary>
            /// Retrieves the value with the specified key from the Items collection of the context. 
            /// </summary>
            /// <typeparam name="T">Type to return</typeparam>
            /// <param name="context">Post process context</param>
            /// <param name="key">key</param>
            /// <returns>collection value</returns>
            private T GetItemValue<T>(IPublishPostProcessContext context, string key)
            {
                object value;
                if (!context.Items.TryGetValue(key, out value))
                {
                    throw new InvalidOperationException($"Items does not contain key '{key}'");
                }
                T valueConverted = default(T);
                try
                {
                    valueConverted = (T)value;
                }
                catch (InvalidCastException)
                {
                    throw new InvalidOperationException($"Items value with the key '{key}' has incorrect type.");
                }
                return valueConverted;
            }
    
            /// <summary>
            /// Parses an xml file and tries to find the text of the first xml element matching the  _elementNameToProcess
            /// </summary>
            /// <param name="xmlFilePath">File path of the xml file</param>
            /// <returns>The found text if the element name was found. Otherwise an empty string</returns>
            private string ParseFile(string xmlFilePath)
            {
                var xmlReaderSettings = new XmlReaderSettings();
                xmlReaderSettings.CloseInput = true;
                xmlReaderSettings.DtdProcessing = DtdProcessing.Ignore;
                using (var reader = XmlReader.Create(xmlFilePath, xmlReaderSettings))
                {
                    while (!reader.EOF)
                    {
                        switch (reader.NodeType)
                        {
                            case XmlNodeType.Element:
                                if (reader.LocalName == _elementNameToProcess)
                                {
                                    // Get all text from element, including text from child elements
                                    var text = new StringBuilder();
                                    XmlReader elementReader = reader.ReadSubtree();
                                    while (elementReader.Read())
                                    {
                                        if (elementReader.NodeType == XmlNodeType.Text)
                                        {
                                            text.Append(elementReader.Value);
                                        }
                                    }
                                    return text.ToString();
                                }
                                break;
                            default:
                                break;
                        }
                        reader.Read();
                    }
                }
                return String.Empty;
            }
            #endregion
        }
    }