Wednesday, January 22, 2014

Developing Translation Manager Plug Ins

In this post I want to talk about some of the cool but sadly no often used features available with SDL Tridion, Translation Manager plug in system. This plug in system allow us to customize the default Translation Jobs processing behavior. Before I start with the Translation Manager Plug In system I will provide some background about Translation Manager and how it is integrated with a Translation System like TMS or World Server.

The following picture will explain how the integration is done.


Just to provide a little more context. The translation is started in Tridion via Translation Manager and is sent to the Translation System via the Translation Manager Service, then after the translation is completed in the Translation system it is returned back to Tridion and the changes are applied by the Translation Manger Service and visualized by SDL Tridion and Translation Manager.

So the behavior above is the default one, but what happens if we want to customize it for instance we want to add or update items that are sent for translation or we want to execute some operation after the translation is completed (like notifications or workflow integrations). Here is when the Translation Manager Plug In system comes into the picture.

Developing a Translation Manager Plug In

Step 1

We should start identifying the API and the libraries that will help us to develop a Plug In.

Translation Manager API (Tridion.TranslationManager.DomainModel.dll)
Tridion Core Services (Tridion.ContentManager.CoreService.Client.dll) – I prefer using Core Services instead of TOM .Net API when I connect to Tridion from a Plug In, because I don’t need to start a new Session object and I don’t have to deal with session objects initialization and disposal.

Once you have identified the libraries that will help us to develop a plug in, you will need to reference them to your project in Visual Studio (Class Library project).

Step 2

The next step is to create the plug in class skeleton as you can see in the following code.

[TranslationManagerPlugIn]
public class MyTranslationPlugIn
{
    public MyTranslationPlugIn()
    {
        TranslationJobManager.TranslationJobCreated += TranslationJobManagerInitiated;
        TranslationJobManager.TranslationJobLoaded += TranslationJobManagerInitiated;
    }

    public void TranslationJobManagerInitiated(object sender, TranslationJobEventArgs e)
    {
            
    }
}

As you might noticed in the source code above we need to decorate the class with the TranslationManagerPlugIn attribute.

Step 3

Subscribe to a Translation Event that should be extended to meet your requirement. The events that can be used are actually Translation Jobs events. Below are the list and a some description of them.

Deleted: Executed when a Translation Job is deleted. A translation job can be deleted when it is completed or canceled from a Translation System.
Deleting: This event happens while the Translation Job is being deleted but the translation is not completed yet.
Saved: Executed when a Translation Job is saved. It is a good event to add / remove more items for Translation.
Saving: This event happens while the Translation Job is being saved but the transaction is not completed yet.
Resolved: Executed when all the items are resolved.
Resolving: This is the last stage where the resolved items collection can be modified and items can be added to the items sent for Translation. Items added at this stage are not visible in a Translation Job.
StateChanged: Executed when the Translation job changes its state. It can be used to execute logic when the Translation is completed.
StateChanging: This event happens when the Translation Job is changing its state but the transaction is not completed yet.

The following sample will add linked components to the Translation Job using the Save Event.

[TranslationManagerPlugIn]
public class AddLinkedComponents
{
    private readonly string UserId = "Administrator";

    public AddLinkedComponents()
    {
        TranslationJobManager.TranslationJobCreated += OnTranslationJobInitiated;
        TranslationJobManager.TranslationJobLoaded += OnTranslationJobInitiated;
    }

    public void OnTranslationJobInitiated(object sender, TranslationJobEventArgs e)
    {
        e.TranslationJob.Saving += OnSaving;
    }

    public void OnSaving(object sender, System.ComponentModel.CancelEventArgs e)
    {
        TranslationJob job = (TranslationJob)sender;
        if (job.State == TranslationJobState.Definition)
        {
            SessionAwareCoreServiceClient channel = new SessionAwareCoreServiceClient("netTcp_2012");
            try
            {
                channel.Impersonate(UserId);

                IEnumerable<XElement> links = null;
                XNamespace xLinkNS = XNamespace.Get("http://www.w3.org/1999/xlink");

                foreach (AddedItem addedItem in job.AddedItems)
                {
                    string addedItemId = addedItem.TcmUri.ToString();
                    RepositoryLocalObjectData item = (RepositoryLocalObjectData)channel.Read(addedItemId, new ReadOptions());
                    if (item is ComponentData) {
                        ComponentData component = (ComponentData)item;
                        if (!string.IsNullOrEmpty(component.Content)) {
                            XElement xContent = XElement.Parse(component.Content);
                            XNamespace xNS = XNamespace.Get(xContent.FirstAttribute.Value);

                            links = xContent.Descendants().Where(w => w.Attributes(xLinkNS + "href").Count() > 0);
                        }
                    }
                }

                if (links != null && links.Count() > 0)
                {
                    foreach (XElement link in links)
                    {
                        AddedItem newItem = new AddedItem(link.Attribute(xLinkNS + "href").Value, TranslationOptions.TranslateSubItems);
                        if (!job.AddedItems.Contains(newItem))
                        {
                            job.AddedItems.Add(newItem);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                channel.Abort();
                throw ex;
            }
        }
    }
}

Step 4

In this sample I am using Core Services to access Tridion Objects so you will need to configure the endpoints, the easiest is to configure the Core Services endpoints and bindings in the app.config files for your class library.

Step 5

Once you have finished developing your plug in then you need to register it. You can do in but updating the TranslationManager.xml file available at %TRIDION_HOME%/config

Here a sample:

  <PlugInAssemblies>
    <Assembly fullPath="D:\Tridion\Translation Manager\Plugins\Tridion.TranslationManager.PlugIn.dll"/>
  </PlugInAssemblies>

Wednesday, December 11, 2013

How to correctly dispose CoreService client objects

Today I was doing some research in the WCF internals and I realized that the stories around the “using” statement are not all true. If you are a C# developer you are familiar with the “using” statement and you know that it will save lines of code since it will explicitly call the Dispose method in the object getting instantiated in the “using” statement (the object must implement the IDisposable interface).

The sentence above is not true for WCF clients like the CoreServiceClient or SessionAwareCoreServiceClient classes because they are typed WCF clients.

Why I cannot use “using” to safely dispose CoreService clients?

The answer is pretty simple the Dispose method calls the Close method and it might throw an exception if a Transport related exception happens. So it is not correct to use “using” because you are not managing exceptions correctly and eventually the underline communication object will remain undisposed.

Sample:

using (channel = new CoreServiceClient("basicHttp_2013")) {
    ComponentData component = (ComponentData)channel.Read("tcm:5-2051", new ReadOptions());
} // Dispose will be called here and an exception might happen.

// This code might not be reached
Console.WriteLine("I have sucessfully retrieved my component");


I know I have some samples where I was using “using” and having a try catch finally block in order to handle exceptions and try to close the channel, it might help but there are still some gaps.
Sample:

using (channel = new CoreServiceClient("basicHttp_2013")) {
    try {
        ComponentData component = (ComponentData)channel.Read("tcm:5-2051", new ReadOptions());
    }
    catch (Exception ex) { // This code will manage any exception that might happen in the try block       
 throw ex;
    }
    finally {
        if (channel.State != CommunicationState.Closed) {
            channel.Close(); // This code might throw an exception if there is a Network issue
        }
    }
               
} // Dispose will be called here and an exception might happen if there is a Network issue.

// This code might not be reached
Console.WriteLine("I have sucessfully retrieved my component");


What is the right way to Dispose a CoreService client object?

Microsoft recommends a way to do it, basically they don’t use the “using” statement and they use a classic way to manage exceptions as back in the .Net Framework 1.1 J

Solution:

CoreServiceClient channel = new CoreServiceClient("basicHttp_2013");
try {
    ComponentData component = (ComponentData)channel.Read("tcm:5-2051", new ReadOptions());

    channel.Close(); //This line might throw a network exception
               
    Console.WriteLine("I have sucessfully retrieved my component");
}
catch (CommunicationException ex) {
    channel.Abort(); //The channel is aborted and the resources released.
}
catch (TimeoutException ex) {
    channel.Abort(); //The channel is aborted and the resources released.
}
catch (Exception ex) {
    channel.Abort(); // The channel is aborted and the resources released.
}


I know it looks tedious but we can put it in an Extension method or a Utility so that we don’t have to write all these catch sentences all the time.

Tuesday, December 10, 2013

Tridion 2013 SP1 - Synchronizing components

Continuing with my posts about Tridion 2013 SP1, I will talk about the new Synchronization API. This API that is available via the Core Services making this API easy to use and very light.

The new synchronization functionality will analyze the component content and the schema and it will infer which operations need to be completed in order to keep them in sync.

Sample:

private static CoreServiceClient channel { get; set; }

using (channel = new CoreServiceClient("basicHttp_2013"))
    try {
        SynchronizeComponent("tcm:5-2051", true);
    }
    finally {
        if (channel.State != CommunicationState.Closed) {
            channel.Close();
        }
    }
}

public static void SynchronizeComponent(string componentId, bool updateAfterSynchronize) {
    Console.WriteLine("Starting Sychronization...");

    SynchronizeOptions options = new SynchronizeOptions() {
        SynchronizeFlags = SynchronizeFlags.All
    };

    SynchronizationResult result;
    if (updateAfterSynchronize) {
        result = channel.SynchronizeWithSchemaAndUpdate(componentId, options);
    }
    else {
        IdentifiableObjectData item = channel.Read(componentId, new ReadOptions());
        result = channel.SynchronizeWithSchema(item, options);
    }

    foreach(SynchronizationActionData action in result.SynchronizationActions) {
        Console.WriteLine();

        Console.WriteLine("Field Name: {0}", action.FieldName);
        Console.WriteLine("Field Index: {0}", action.FieldIndex);
        Console.WriteLine("Action Taken: {0}", action.SynchronizationActionApplied);

        Console.WriteLine();
    }

    Console.WriteLine("Sychronization completed.");
    Console.ReadLine();
}

The first thing we need to consider while writing a synchronizing program would be the Synchronization Options, basically the Synchronization flags.

SynchronizeFlags.All
SynchronizeFlags.ApplyDefaultValuesForMissingMandatoryFields
SynchronizeFlags.ApplyDefaultValuesForMissingNonMandatoryFields
SynchronizeFlags.ApplyFilterXsltToXhtmlFields
SynchronizeFlags.Basic
SynchronizeFlags.ConvertFieldType
SynchronizeFlags.FixNamespace
SynchronizeFlags.RemoveAdditionalValues
SynchronizeFlags.RemoveUnknownFields
SynchronizeFlags.UnknownByClient

You must be careful while using those flags since some of them like  RemoveUnknownFields that will lead you to lose data if is not used carefully.

Other important feature in this new API is the fact that this will return feedback in the form of SynchronizationActionData objects. As you can see in the sample above it will give you information like the FileName, FileIndex and the action that was taken.

You may also notice that in the sample above I am using two different methods SycnrhonizeWithSchema and SynchronizeWithSchemaAndUpdate, the difference is clear, the first one won’t check in any changes and the second one will check in the changes leading you to lose data if not used carefully.

Tridion 2013 SP1 - API improvements

Continuing with my Tridion 2013 SP1 posts I will talk about some API improvements. Below a list containing API improvements.

StreamDownload endpoint can now download external items

Sample:
private static StreamDownloadClient downloadChannel { get; set; }

using (downloadChannel = new StreamDownloadClient("streamDownload_basicHttp_2013")) {
    try {
Stream stream = downloadChannel.DownloadExternalBinaryContent("http://www.sdl.com/Content/themes/common/images/sdl-logo.png");
MemoryStream ms = new MemoryStream();

int b;
while ((b = stream.ReadByte()) != -1) {
ms.WriteByte((byte)b);
}

using (FileStream fs = new FileStream("C:\\sdl-logo.png", FileMode.OpenOrCreate, FileAccess.ReadWrite))
using (BinaryWriter writer = new BinaryWriter(fs)) {
    try {
               writer.Write(ms.ToArray());
    } finally {
        writer.Close();
        fs.Close();
        ms.Close();
    }
}
    }
    finally {
        if (downloadChannel.State != CommunicationState.Closed) {
            downloadChannel.Close();
        }
    }
}

Schemas can be retrieved by Namespace

This is a change in both Core Services and TOM .NET APIs. In this post I will show a Core Service sample.

Sample:

private static CoreServiceClient channel { get; set; }

using (channel = new CoreServiceClient("basicHttp_2013"))
    try {
SchemaData schema = GetSchemaFromNamespace();
    }
    finally {
        if (channel.State != CommunicationState.Closed) {
            channel.Close();
        }
    }
}  

public static SchemaData GetSchemaFromNamespace() {
    LinkToSchemaData schema = channel.GetSchemasByNamespaceUri(PublicationId, "http://www.tridion.com/ContentManager/5.0/DefaultMultimediaSchema", null).FirstOrDefault();
    if (schema != null) {
        return (SchemaData)channel.Read(schema.IdRef, new ReadOptions());
    }
    return null;
}

This change is very important since now we can retrieve schemas by using namespaces (XML like functionality) without having the need to specify tcm uris or webdav urls.

Multimedia Components can be created without specifying a Multimedia Type

This is an small change but it will save you several lines of code J, if the file extension that you are using to create the multimedia component can be mapped to an existing multimedia type, then the API will do it for you.

Sample:

private static CoreServiceClient channel { get; set; }

using (channel = new CoreServiceClient("basicHttp_2013"))
    try {
SchemaData schema = GetSchemaFromNamespace();
CreateMultimediaComponent(schema);
    }
    finally {
        if (channel.State != CommunicationState.Closed) {
            channel.Close();
        }
    }
}  

public static void CreateMultimediaComponent(SchemaData schema) {
    ComponentData multimediaComponent = new ComponentData() {
        Id = TcmUri.UriNull.ToString(),
        Title = Guid.NewGuid().ToString(),
        Schema = new LinkToSchemaData() { IdRef = schema.Id },
        LocationInfo = new LocationInfo() { OrganizationalItem = new LinkToOrganizationalItemData() { IdRef = FolderId } },
        ComponentType = ComponentType.Multimedia,
        BinaryContent = new BinaryContentData() {
            Filename = "sdl-logo.png",
            UploadFromFile = "C:\\sdl-logo.png"
        }
    };

    channel.Create(multimediaComponent, new ReadOptions());
}

Decommissioning Publication Targets

This is a very important change, and it will allow us to mark items that were published to a publication target as unpublished to that publication target in one single operation. It is useful in case you have a publication target that is no longer used and can block other items to be deleted or moved.

Sample:

public static void DecommissioningPublicationTarget(string publicationTargetId) {
    channel.DecommissionPublicationTarget(publicationTargetId);
}