A Custom Resolver in practice

Back in February I posted an article about Custom Resolvers. Yesterday I rolled my first Custom Resolver into a production environment, so I figured it was time to share my findings.

Background

To set the scene, it probably helps to explain the business requirements first. We have a large implementation with over 300 publications. Many of these share content, some of which needs to be secured, and links to binaries that also need to be secured. We have a third party security solution, which is implemented as a proxy on top of our published site. The proxy looks for a security.xml in the folder of any request, and then prompts for login etc depending what is contained in the XML file. This works very well for pages, but the pages often link to binaries (which were all contained in the “/images” directory for each publication). In order to secure binaries with different sets of restrictions we needed to bind the binaries in different Structure Groups. To simplify the concept, we decided to publish a variant of each binary linked from a page to the same Structure Group as the page. This has the desired effect of securing all binaries that are linked from secured pages with the same restrictions.  When a binary is linked from multiple secured pages, multiple variants of the binary are published.

We implemented this first in SDL Tridion 2011 GA by creating a “Binary Publisher” dynamic component template with the following code:

using System;
using System.Collections.Generic;
using System.Text;
using Tridion.ContentManager;
using Tridion.ContentManager.Publishing;
using Tridion.ContentManager.Publishing.Resolving;
using Tridion.ContentManager.ContentManagement;
using Tridion.ContentManager.CommunicationManagement;
using Tridion.ContentManager.ContentManagement.Fields;
using Tridion.ContentManager.Templating;
using Tridion.ContentManager.Templating.Assembly;
using Tridion.Logging;
using Tridion.Collections;
using System.IO;

namespace UrbanCherry.Net.SDLTridion.Templates
{
    class InContextBinaryPublisher : ITemplate
    {
        /// <summary>
        /// Used to publish binaries as Dynamic Component Presentations with a variant 
        /// created in each structure group of pages thatlink to the binary.
        /// 
        /// This alows the Yale Security Proxy to apply the same security to the binaries as 
        /// the pages.
        /// 
        /// If the binary is used on multiple pages, the file will be deployed to potentially
        /// multiple structure groups
        /// </summary>
        /// <param name="engine"></param>
        /// <param name="package"></param>
        public  void Transform(Engine engine, Package package)
        {
            Item componentItem = package.GetByType(ContentType.Component);
            Component component = (Component)engine.GetObject(componentItem);
            PublicationTarget target;

            //Only run this on Multimedia Components
            if (component.ComponentType == ComponentType.Multimedia)
            {
                //Load the binary into a stream so we can publish it with a safe filename
                MemoryStream binaryStream = new MemoryStream(component.BinaryContent.GetByteArray());

                //Get the current filename and mime type
                String filename = component.BinaryContent.Filename;
                String mimetype = component.BinaryContent.MultimediaType.MimeType;

                if (filename.Contains("\\")){
                   filename = filename.Substring(filename.LastIndexOf("\\") + 1);
                    
                }

                //Get a copy of the current resolve instruction 
                ResolveInstruction resolveInstruction = engine.PublishingContext.PublishInstruction.ResolveInstruction;

                //Make sure the 'IncludeComponentLinks' parameter is set to true, so we can find which pages link to it
                resolveInstruction.IncludeComponentLinks = true;

                //Get a target to use for the context (a dummy one if needed)
                if (engine.RenderMode == RenderMode.Publish)
                {
                    target = engine.PublishingContext.PublicationTarget;
                }
                else{
                     target = (PublicationTarget)engine.GetObject("tcm:0-2-65537");
                }

                //Get a PublishContext based on the current Publication and Target
                PublishContext pubContext = new PublishContext((Publication)component.ContextRepository, target);

                //Get a list of all the items that would be resolved if the item was published
                Set<ResolvedItem> resolvedItems = new Set<ResolvedItem>();
                resolvedItems = (Set<ResolvedItem>)ResolveEngine.ResolveItem(component, resolveInstruction, pubContext);

                //Loop through the resolved items to find PAges which link to the item
                foreach (ResolvedItem resolvedItem in resolvedItems)
                {
                    IdentifiableObject item = resolvedItem.Item;
                    if (item.Id.ItemType == ItemType.Page)
                    {
                        Page page = (Page)item;
                        StructureGroup sg = (StructureGroup)page.OrganizationalItem;

                        //TODO: Would be nice to only publish if it has changed and not already in the PubQ

                        //Add the binary to the package as a variant for the Page's Structure Group
                        engine.PublishingContext.RenderedItem.AddBinary(binaryStream, component.Id.ItemId + "_" + filename, sg, sg.Id.ItemId.ToString(), component, mimetype);               
                    }
                }
            }
        }
    }
}

We then had a Modular “Resolve Binary Links” TBB added to the end of our Page Templates which looked at all the binary links in the output and modified them  to link to a specific variant (the variant ID and filename is created based on the Structure Group URI). This used the following code:

class processDynamicLinks : ITemplate
    {
        TemplatingLogger log = TemplatingLogger.GetLogger(typeof(processDynamicLinks));
        public void Transform(Engine engine, Package package)
        {

            try
            {
                log.Debug("Looking for component links");
                Item output = package.GetByName("Output");
                Item pageItem = package.GetByType(ContentType.Page);
                Page page = (Page)engine.GetObject(pageItem);
                String sgID = page.OrganizationalItem.Id.ItemId.ToString();

                XmlDocument domOutput = output.GetAsXmlDocument();
                XmlNamespaceManager nsmgr = new XmlNamespaceManager(new NameTable());
                nsmgr.AddNamespace("tridion", "http://www.tridion.com/ContentManager/5.0");
                if (domOutput != null)
                {
                    foreach (XmlNode nodeCompLink in domOutput.SelectNodes("//a[@tridion:href]", nsmgr))
                    {
                        String uriCompLink = nodeCompLink.Attributes["href", "http://www.tridion.com/ContentManager/5.0"].Value;
                        try
                        {
                            Component linkedComponent = (Component)engine.GetObject(uriCompLink);
                            if (linkedComponent.ComponentType == ComponentType.Multimedia)
                            {
                                //Add a attribute to make this a binary link
                                XmlAttribute binaryLinkAttribute = domOutput.CreateAttribute("tridion", "type", "http://www.tridion.com/ContentManager/5.0");
                                binaryLinkAttribute.Value = "Binary";
                                nodeCompLink.Attributes.Append(binaryLinkAttribute);
                                if (engine.RenderMode == RenderMode.Publish)
                                {
                                    String title = linkedComponent.Schema.Title.ToLower();

                                    if ((title == "document") || (title == "audio") || (title == "compressed files") || (title == "video") || (title == "image"))
                                    {
                                        Session privilegedSession = new Session("domain\\mtsuser");
                                        Component privilegedLinkedComponent = (Component)privilegedSession.GetObject(linkedComponent.Id);
                                        PublishEngine.Publish(new IdentifiableObject[] { privilegedLinkedComponent }, engine.PublishingContext.PublishInstruction, new List<PublicationTarget>() { engine.PublishingContext.PublicationTarget });  
                                        
                                        //Create a "variantID" attribute on the node for creating a specific link to the item in the current SG    
                                        XmlAttribute variantLinkAttribute = domOutput.CreateAttribute("tridion", "variantid", "http://www.tridion.com/ContentManager/5.0");
                                        variantLinkAttribute.Value = sgID;
                                        nodeCompLink.Attributes.Append(variantLinkAttribute);
                                    }
                                    else
                                    {
                                        engine.PublishingContext.RenderedItem.AddBinary(linkedComponent);
                                    }
                                }
                            }
                        }
                        catch (Exception e)
                        {
                            log.Error("There is an error: " + e.Message);
                        }
                    }
                    output.SetAsString(domOutput.OuterXml);
                }
            }
            catch (Exception e)
            {
                log.Error(e.Message);
            }
        }
    }
}

One of the biggest down sides of this approach was that every binary was placed in the Publishing Queue as a separate Publish Transaction (with low priority marked as published by the System User). This meant that often there was a delay between the page and the binaries being published, and the Publishing Queue would often get filled with thousands of Multimedia Components which impacted both caching performance and the experience that editors had while working with the system.

Furthermore we had implemented an Event System which was triggered when publishing a Multimedia Component which prevented links form being resolved. This was necessary to prevent the pages which used the binaries from being re-published (which would have created an infinite loop of publishing triggers).

Solution

As I mentioned in the previous article, a new security measure was put in place with SDL Tridion 2011 SP1 which prevents calling PublishEngine.Publish() in our templates. Instead of overriding this new behavior using the allowWriteOperationsInTemplates configuration setting, we decided to implement a Custom Resolver.

using System;
using System.Text;
using Tridion.ContentManager.Publishing;
using Tridion.ContentManager.Publishing.Resolving;
using Tridion.ContentManager;
using Tridion.Logging;
using Tridion.ContentManager.Templating;
using Tridion.ContentManager.ContentManagement;
using Tridion.ContentManager.CommunicationManagement;
using Tridion.Collections;
using Tridion.Localization;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using UrbanCherry.Net.SDLTridion.CustomResolvers.TridionCoreService;
using System.ServiceModel;


namespace UrbanCherry.Net.SDLTridion.CustomResolvers
{
    class DynamicBinaryLinkResolver : IResolver
    {
        private static TemplatingLogger log;
        private Session session;

        public void Resolve(IdentifiableObject item, ResolveInstruction instruction, PublishContext context, ISet<ResolvedItem> resolvedItems)
        {                
            log = TemplatingLogger.GetLogger(typeof(DynamicBinaryLinkResolver));
            log.Debug("Loaded Dynamic Binary Link Resolver for " + item.Title);
            Int32 originalNumberOfResolvedItems = resolvedItems.Count;
            session = context.Session;
            String pathCT = "/webdav/3%20YSM%20Design/Building%20Blocks/System/Templates/Component%20Templates/Document%20Publisher.tctcmp";
            ComponentTemplate renderTemplate = (ComponentTemplate)session.GetObject(pathCT);

            //Copy resolved items to an array for looping
            ResolvedItem[] originalResolveItemList = new ResolvedItem[resolvedItems.Count];
            resolvedItems.CopyTo(originalResolveItemList, 0);

            if (item is Page)
            {
                ProcessPageComponents((Page)item, renderTemplate, resolvedItems, context);
            }
            else if (item is Component)
            {
                Component component = (Component)item;
                if (component.ComponentType == ComponentType.Multimedia)
                {
                    foreach (ResolvedItem resolvedItem in originalResolveItemList)
                    {
                        if (resolvedItem.Item.Id != item.Id)
                        {
                            resolvedItems.Remove(resolvedItem);
                        }
                    }
                }
            }
            else
            {

                foreach (ResolvedItem resolveditem in originalResolveItemList)
                {
                    if (resolveditem.Item is Page)
                    {
                        ProcessPageComponents((Page)resolveditem.Item, renderTemplate, resolvedItems, context);
                    }
                }
            }
        }

        private static void ProcessPageComponents(Page page, ComponentTemplate renderTemplate, ISet<ResolvedItem> resolvedItems, PublishContext context)
        {
            log.Debug("Processing Components for the Page: " + page.Title);
            UsedItemsFilter itemTypeFilter = new UsedItemsFilter(context.Session);
            itemTypeFilter.ItemTypes = new[] { ItemType.Component };

            foreach (Tridion.ContentManager.CommunicationManagement.ComponentPresentation cp in page.ComponentPresentations)
            {
                XmlElement usingItemsXml = cp.Component.GetListUsedItems(itemTypeFilter);
                foreach (XmlElement element in usingItemsXml.SelectNodes("/*/*"))
                {
                    String uriUsedItem = element.GetAttribute("ID");
                    Component usedItem = (Component)context.Session.GetObject(uriUsedItem);
                    try
                    {
                        if (ComponentType.Multimedia.Equals(usedItem.ComponentType))
                        {
                            String title = usedItem.Schema.Title.ToLower();
                            if ((title == "document") || (title == "audio") || (title == "compressed files") || (title == "video") || (title == "image"))
                            {
                                ResolvedItem newResolvedItem = new ResolvedItem(usedItem, renderTemplate);
                                resolvedItems.Add(newResolvedItem);
                            }
                        }
                    }
                    catch(Exception e)
                    {
                        log.Error(e.Message);
                        throw new Exception("Something went wrong whilst resolving the Components on Page: " + page.Id);
                    }
                }
            }
        }
    }
}

The resolver was then strongly named, and then added to the GAC. The final step was to add the resolver to the node of the Tridion.ContentManager.config file. It was necessary to add the resolver for Publications, Structure Groups, Component and Pages as follows:

<resolving>
	<mappings>
		<clear/>
		<add itemType="Tridion.ContentManager.CommunicationManagement.Page">
			<resolvers>
				<add type="Tridion.ContentManager.Publishing.Resolving.PageResolver" assembly="Tridion.ContentManager.Publishing, Version=6.1.0.996, Culture=neutral, PublicKeyToken=360aac4d3354074b"/>
				<add type="UrbanCherry.Net.SDLTridion.CustomResolvers.DynamicBinaryLinkResolver" assembly="UrbanCherry.Net.SDLTridion.CustomResolvers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e7729a00ff9574fb"/>
			</resolvers>
		</add>
		<add itemType="Tridion.ContentManager.CommunicationManagement.PageTemplate">
			<resolvers>
				<add type="Tridion.ContentManager.Publishing.Resolving.PageTemplateResolver" assembly="Tridion.ContentManager.Publishing, Version=6.1.0.996, Culture=neutral, PublicKeyToken=360aac4d3354074b"/>
			</resolvers>
		</add>
		<add itemType="Tridion.ContentManager.ContentManagement.Component">
			<resolvers>
				<add type="Tridion.ContentManager.Publishing.Resolving.ComponentResolver" assembly="Tridion.ContentManager.Publishing, Version=6.1.0.996, Culture=neutral, PublicKeyToken=360aac4d3354074b"/>
				<add type="UrbanCherry.Net.SDLTridion.CustomResolvers.DynamicBinaryLinkResolver" assembly="UrbanCherry.Net.SDLTridion.CustomResolvers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e7729a00ff9574fb"/>
			</resolvers>
		</add>
		<add itemType="Tridion.ContentManager.CommunicationManagement.ComponentTemplate">
			<resolvers>
				<add type="Tridion.ContentManager.Publishing.Resolving.ComponentTemplateResolver" assembly="Tridion.ContentManager.Publishing, Version=6.1.0.996, Culture=neutral, PublicKeyToken=360aac4d3354074b"/>
			</resolvers>
		</add>
		<add itemType="Tridion.ContentManager.CommunicationManagement.Publication">
			<resolvers>
				<add type="Tridion.ContentManager.Publishing.Resolving.PublicationResolver" assembly="Tridion.ContentManager.Publishing, Version=6.1.0.996, Culture=neutral, PublicKeyToken=360aac4d3354074b"/>
				<add type="UrbanCherry.Net.SDLTridion.CustomResolvers.DynamicBinaryLinkResolver" assembly="UrbanCherry.Net.SDLTridion.CustomResolvers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e7729a00ff9574fb"/>
			</resolvers>
		</add>
		<add itemType="Tridion.ContentManager.CommunicationManagement.StructureGroup">
			<resolvers>
				<add type="Tridion.ContentManager.Publishing.Resolving.StructureGroupResolver" assembly="Tridion.ContentManager.Publishing, Version=6.1.0.996, Culture=neutral, PublicKeyToken=360aac4d3354074b"/>
				<add type="UrbanCherry.Net.SDLTridion.CustomResolvers.DynamicBinaryLinkResolver" assembly="UrbanCherry.Net.SDLTridion.CustomResolvers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e7729a00ff9574fb"/>
			</resolvers>
		</add>
		<add itemType="Tridion.ContentManager.ContentManagement.Category">
			<resolvers>
				<add type="Tridion.ContentManager.Publishing.Resolving.CategoryResolver" assembly="Tridion.ContentManager.Publishing, Version=6.1.0.996, Culture=neutral, PublicKeyToken=360aac4d3354074b"/>
			</resolvers>
		</add>
	</mappings>
</resolving>

We then removed the PublishEngine.Publish code from the templates, removed the event system and restarted all the SDL Tridion Services and the COM+ packages.

Our binaries are now added to the Publish Transactions (rather than becoming separate transactions).

Lessons learned

The biggest challenges that occurred were related to getting items in the GAC, how to use the Session object, and understanding that the resolvers need to be configured for several item types.

For the Session issue I had made the mistake of storing the Session object in a static variable. The code worked perfectly on my DEV machine, but as soon as I put it in a multi-threaded publishing environment, the whole thing fell apart.

To place an assembly in the GAC, I have always used GacUtil.exe, it had not occurred to me that this utility would not be in my production environment. After some insistence from Dominic Cronin, I finally broke down and actually made an installer for my Resolver. This was much easier than I expected, and I plan to always use installers from now on.

Finally I had only implemented the Custom Resolver for Pages, and had not realized that it would not be called when I published Structure Groups or Publications etc. Don’t forget those, I think I probably need to implement it for Page Templates and Component Templates as well, possibly all item types.

This entry was posted in Development and templating, Security, Tridion 2011 and tagged , , , , , , , by Chris Summers. Bookmark the permalink.

About Chris Summers

Chris has spent his career creating and developing technology for website operation and management. With a background in engineering and design, for the past 12 years, Chris has focused on implementing SDL Tridion products, working with companies and their technical staff to ensure an in-depth understanding of the software and complete successful, on-going implementations. Chris has worked with more than 60 of the largest and most expansive SDL Tridion implementations in the world, from launching custom integrations, offering technical training and mentoring consultants through to certification. When he’s not talking or thinking about websites, Chris is an avid chef, an amateur carpenter and a flying trapeze enthusiast. A fan of travel and adventure, he’s a citizen of the world who currently makes his home in Boston, USA.

2 thoughts on “A Custom Resolver in practice

  1. Thanks Chris!
    I’m currently working on a custom resolver to optimize bulk (re-)publishing. Great tips, thanks again !

    Mario

  2. Pingback: Setting up Custom Resolver in SDL Tridion / Web 8 | Vikas Kumar | SDL Tridion Blogs

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>