Showing posts with label xml. Show all posts
Showing posts with label xml. Show all posts

Tuesday, July 17, 2012

Validating content on Save - Part 1 of many

Recently I had to do a project where we are validating quite some content options whenever the editors decide their work is done. This got me thinking about sharing some of these experiences. This post is the first of a few that I have lined up on this subject, and where I try to cover the available options for content validation from the simple (and cheap) ones to the very complex and over-engineered.

For (many) years, the solution to the eternal "how do I validate content" question with Tridion has been a combination of making fields mandatory and using the Event System (OnComponentSavePre anyone?).
Recently I had to take a different approach to this, since Tridion 2011 brings all this amazing extensibility framework (codenamed Anguilla) that allows to do whatever we want with our content, right? While this is true, the correct answer comes in many shades of gray, and the biggest obstacle on creating a great solution for validating content for your editors is the learning curve.

Let’s think about how we can do this, by taking a very simple example, then growing it out to bigger things. In the first scenario we're covering in this series of posts, we will focus on how to validate that a field named "Title" starts with a capital letter, and we'll do this using 3 different approaches:
  • Schema restrictions
  • Event System
  • CME Command Extension
Using A Schema Restriction As described in LiveContent, you can apply XSD restrictions to simple Tridion Web Schema fields, so we could change the field definition to be something like this:
<xsd:element maxoccurs="1" minoccurs="1" name="Title">
    <xsd:annotation>
        <xsd:appinfo>
            <tcm:extensionxml xmlns:tcm="http://www.tridion.com/ContentManager/5.0"></tcm:extensionxml>
        </xsd:appinfo>
    </xsd:annotation>
    <xsd:simpletype>
        <xsd:restriction base="xsd:string">
            <xsd:pattern value="[A-Z][A-Za-z0-9_ ]*"></xsd:pattern>
        </xsd:restriction>
    </xsd:simpletype>
</xsd:element> 

This will now force our field to start with an upper case letter, and Tridion will enforce the rule.

 If I now try to save my content, Tridion will let me know I don’t understand what I’m doing:

And since I won’t understand the error message either, this becomes more complex than what we can handle with simple schema restrictions…
There are other simple scenarios, particularly with number fields, that Tridion handles quite gracefully (by not allowing the editor to type a number higher than specified in "xsd:maxInclusive" for instance), or limiting the maximum number of characters you can type in a field. However, fairly quickly we fall into a domain where business logic is required to validate fields (common examples are Start and End Dates for events, where you depend on 2 fields to provide the correct validation).

So this was the "cheap" content validation: schema restrictions.

Let’s go now to the 2nd option, using the Event System.

Taking the same example from above (first character must be upper case) we could take one of 2 approaches: Let the user know that the first letter must be upper case, or automatically changing it. Which approach you take depends on your organization’s culture. In some companies it is OK to have the system take decisions for you, while other organizations need to give the editorial team full control about any content that gets created. Let's build an event system to deal with the second approach (letting the editors know about the errors of their ways):



Step 1: create an Event Class Library with Visual Studio.


Add some references from [Tridion-Home]\bin\client


Then let’s write some code to validate if your first character is upper case, and notify the editor if that’s not the case.
using System;
using System.Collections.Generic;
using Tridion.ContentManager.ContentManagement;
using Tridion.ContentManager.ContentManagement.Fields;
using Tridion.ContentManager.Extensibility;
using Tridion.ContentManager.Extensibility.Events;

namespace ValidateTitleFieldUpperCaseFirstLetter
{
    [TcmExtension("ValidateTitleFieldUpperCaseFirstLetter")]
    public class Validate : TcmExtension, IDisposable
    {
        private readonly List<EventSubscription> _subscriptions = new List<Eventsubscription>();

        public Validate()
        {
            Subscribe();
        }

        private void Subscribe()
        {
            EventSubscription subscription =
                EventSystem.Subscribe<Component, SaveEventArgs>(ValidateFirstLetterIsUpperCase, EventPhases.Initiated);
            _subscriptions.Add(subscription);
        }

        private void ValidateFirstLetterIsUpperCase(Component component, SaveEventArgs args, EventPhases phases)
        {
            if (component.Schema.Title != "Article") return;
            if (component.ComponentType != ComponentType.Normal) return;
            ItemFields fields = new ItemFields(component.Content, component.Schema);
            SingleLineTextField titleField = (SingleLineTextField)fields["Title"];
            if(titleField.Values.Count == 0)
            {
                // Tridion should take care of this, but just in case someone changed the field to optional
                throw new Exception("Title field is mandatory.");
            }
            string fieldContent = titleField.Value;
            if(char.IsLower(fieldContent[0]))
            {
                throw new Exception("The first letter of the Title field must be a Capital letter.");
            }
        }

        public void Dispose()
        {
            foreach (EventSubscription subscription in _subscriptions)
            {
                subscription.Unsubscribe();
            }
        }
    }
}

Now let’s build our code and tell Tridion to execute it (add a line similar to this to Tridion.ContentManager.Config under "<extensions>"

<add assemblyFileName="D:\Tridion 2011\PathToYourDll\ValidateTitleFieldUpperCaseFirstLetter.dll" />

Restart Tridion (COM+ and Service Host) and let’s see what it does.


So, it’s a nice improvement from the previous message. Cost of development? Well, it took me about 20 minutes to write it and deploy it (fair enough, it’s far from ready, with things that you shouldn’t see in production like "if (component.Schema.Title != "Article") return;"), but you get the picture.

You could also just be slightly smart about it and outline this requirement in the field’s description:
But this is too specific to this use case, and it’s a simple one after all.


Next (within a few days, I promise) we will start looking at how we could do this with Anguilla.

Sunday, June 05, 2011

Importing Content into Tridion

Something I get asked almost every time a new project starts - or a new consultant or partner starts working with Tridion - is how to import content into Tridion.

It kinda baffles me, since I always thought it is pretty easy to import content into Tridion, but apparently that's not the case. Here's a few things to consider, I'm pretty sure most of this applies to _any_ content management system, and is not really related to Tridion.

Well formed content
Though it seems obvious, I see many content migrations coming from systems that do not enforce a strict XML schema as Tridion does, and therefore a simple one-to-one migration will fail miserably. "Easy" workarounds on this one:
- Use XmlWriter when creating the content representation for Tridion, and the ItemFields collections to create your content in. If it fails validation, it will probably fail before you try to save it.
- When dealing with Rich Text fields, use Tidy.NET to ensure your content is valid Xhtml.

Consider if a content migration is really what you want
One of the main reasons to change WCM is that your current content format does not match the business requirements. Guess what happens if you migrate your content "as-is" into Tridion? Yup, the content format still does not match your business requirements. So why are you even contemplating migration? Sure, you can get some of the content in, but you really should think about what you're trying to achieve before spending weeks writing a content migration tool that will prove to be worthless in a very short time frame. Do not underestimate the power of manual content migration in some cases.

How easy is it to get the source content?
This obviously depends on a lot of aspects of your current/old WCM, not all of them are as easy, and all of them are different.

In other words, really think about what it is you're trying to achieve before embarking on a migration project that insists on changing mid-way through the migration.

Since you read this far, here's a couple of bonus code samples :)

Converting html to xhtml using Tidy.NET:
private const String XhtmlNamespace = "http://www.w3.org/1999/xhtml";
public static String ConvertHtmlToXhtml(String source)
{
    MemoryStream input = new MemoryStream(Encoding.UTF8.GetBytes(source));
    MemoryStream output = new MemoryStream();

    TidyMessageCollection tmc = new TidyMessageCollection();
    Tidy tidy = new Tidy();


    tidy.Options.DocType = DocType.Omit;
    tidy.Options.DropFontTags = true;
    tidy.Options.LogicalEmphasis = true;
    tidy.Options.Xhtml = true;
    tidy.Options.XmlOut = true;
    tidy.Options.MakeClean = true;
    tidy.Options.TidyMark = false;
    tidy.Options.NumEntities = true;

    tidy.Parse(input, output, tmc);

    XmlDocument x = new XmlDocument();
    XmlDocument xhtml = new XmlDocument();
    xhtml.LoadXml("<body />");
    XmlNode xhtmlBody = xhtml.SelectSingleNode("/body");

    x.LoadXml(Encoding.UTF8.GetString(output.ToArray()));
    XmlAttribute ns = x.CreateAttribute("xmlns");
    ns.Value = XhtmlNamespace;
    XmlNode body = x.SelectSingleNode("/html/body");
    if (body != null)
        foreach (XmlNode node in body.ChildNodes)
        {
            if (node.NodeType == XmlNodeType.Element)
                if (node.Attributes != null) 
                    node.Attributes.Append(ns);

            if (xhtmlBody != null) 
                xhtmlBody.AppendChild(xhtml.ImportNode
                    (node, true));
        }
    return xhtmlBody != null ? xhtmlBody.InnerXml : null;
}


Getting a new or existing component (for update vs creation, CoreService with a custom client library)
static Component GetNewOrExistingComponent
    (string componentName, Folder folder)
{
    Component returnObject = null;
    componentName = SecurityElement.Escape(componentName);
    XmlNamespaceManager nm = new XmlNamespaceManager
        (new NameTable());
    nm.AddNamespace(Constants.TcmPrefix,
        Constants.TcmNamespace);
    CoreServiceSession session =
        new CoreServiceSession(CoreServiceEndpoint);
    OrganizationalItemItemsFilter filter = 
        new OrganizationalItemItemsFilter(session)
            {ItemTypes = new[] {ItemType.Component}};

    string xpath = String.Format
        ("tcm:Item[@Title='{0}']", componentName);
    XmlElement listItems = folder.GetListItems(filter);
    if (listItems != null)
        if (listItems.SelectNodes(xpath, nm).Count > 0)
        {
            string componentId = listItems.SelectSingleNode
                (xpath, nm).Attributes["ID"].Value;
            returnObject = session.GetObject
                (new TcmUri(componentId)) as Component;
        }
        else
        {
            returnObject = new Component(session, folder.Id);
        }
    return returnObject;
}

Note: This sample uses a custom client Library I wrote on top of the CoreService, and this library is not available yet - and I'm not sure I will make it available at all due to how long it took me to write it, and the fact that it is a work-in-progress. Releasing it means supporting it, and I unfortunately don't have the time to support my plants at home, let alone a still-half-buggy library that someone may try to use in production systems. Anyway, the code just looks like TOM.NET, so you shouldn't have any trouble reverse-engineering what this code does.

Saturday, April 10, 2010

Outputting a keyword hierarchy in XML

With Tridion 2009 we saw the introduction of hierarchical keywords - aka Taxonomy, aka Intelligent Navigation - and with it a whole new world of possibilities for content classification.

However, in some cases, you really don't want all the intelligence around it and would much rather have just an "old style" xml hierarchy. If you tried this yourself, you probably figured out by now that the keyword hierarchy doesn't look so... hierarchical when viewed through the API. Not at all.

It looks rather flat.

I had to write a template recently with some information about every keyword in the hierarchy (including its metadata) for a site navigation, and keeping the keyword "parent/child'' relationships.

Here's how can you do it.

Category Navigation = engine.GetObject(NAVIGATION_CATEGORY) as Category;
using (MemoryStream ms = new MemoryStream())
{
    XmlTextWriter w = new XmlTextWriter(ms, new System.Text.UTF8Encoding(false));
    w.Indentation = 4;
    w.Formatting = Formatting.Indented;

    w.WriteStartDocument();
    w.WriteStartElement("Navigation");
    log.Debug("/Navigation created");

    Filter filter = new Filter();
    filter.Conditions.Add("Recursive", false);
    foreach (XmlNode RootChildren in Navigation.GetListKeywords(filter).SelectNodes("//*[@IsRoot='true']"))
    {
        Keyword RootKeyword = engine.GetObject(RootChildren.Attributes["ID"].Value) as Keyword;
        log.Debug("Generating XML for keyword " + RootKeyword.Title);
        w.WriteStartElement("Item");
        w.WriteAttributeString("ID", RootKeyword.Id);
        w.WriteAttributeString("Title", RootKeyword.Title);
        if (RootKeyword.Metadata != null)
        {
            WriteKeywordMeta(RootKeyword, w);
        }
        WriteChildrenXml(RootKeyword.Id.ToString(), w);
        w.WriteEndElement();
    }
    w.WriteEndElement();
    w.WriteEndDocument();
    w.Flush();
    w.Close();

    package.PushItem(Package.OutputName, package.CreateStringItem(ContentType.Xml, Encoding.UTF8.GetString(ms.ToArray())));
}

And the recursive "WriteChildrenXml" method would look more or less like this:

private void WriteChildrenXml(String RootKeywordId, XmlWriter writer)
{
    Keyword ParentKeyword = _engine.GetObject(RootKeywordId) as Keyword;
    Filter filter = new Filter();
    filter.Conditions.Add("Recursive", false);

    foreach (Keyword ChildKeyword in ParentKeyword.GetChildKeywords(filter))
    {
        writer.WriteStartElement("Item");
        writer.WriteAttributeString("ID", ChildKeyword.Id);
        writer.WriteAttributeString("Title", ChildKeyword.Title);
        if (ChildKeyword.Metadata != null)
        {
            WriteKeywordMeta(ChildKeyword, writer);
        }
        WriteChildrenXml(ChildKeyword.Id.ToString(), writer);
        writer.WriteEndElement();
    }
}
Nuno