Tuesday, May 29, 2012

Inspecting SDL Tridion Core Services SOAP messages

While debugging and troubleshooting tasks it is really important to have access to the soap messages being transmitted between the core services clients and the content manager server. Also, this kind of functionality can be used in order to modify the message before/after it is received and giving it a different meaning; in this post I am just writing those messages to the console stream but the same base line can be used to intercept and change messages like updating the soap header to include a different X509 certificate when signing messages.

In a well defined SOA design it is common to have message brokers that dispatch messages based in well defined policies. Message brokers can use message interceptors to change soap messages before they leave the bounds of the application based on business rules.

Adding a new Messages Inspector

Implement the interface IClientMessageInspector defined in the System.ServiceModel.Dispatcher namespace. This interface defines two methods that must be implemented AfterReceiveReply (intercepts the soap result message) and BeforeSendRequest(Intercepts the soap request message).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;

namespace Tridion.Extensions.CoreServices {
    public class CustomMessageInspector : IClientMessageInspector {
        public void AfterReceiveReply(ref Message reply, object correlationState) {
            Console.WriteLine(reply);
        }

        public object BeforeSendRequest(ref Message request, IClientChannel channel) {
            Console.WriteLine(request);
            return null;
        }
    }
}

Adding a new Endpoint Behavior

Implement the interface IEndpointBehavior by registering the CustomMessageInspector as part of the client behavior. Note that you can add multiple MessageInspectors that will be executed in the order they were added.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;

namespace Tridion.Extensions.CoreServices {
    public class CustomEndpointBehavior : IEndpointBehavior {
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) {
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) {
            clientRuntime.MessageInspectors.Add(new CustomMessageInspector());
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) {
        }

        public void Validate(ServiceEndpoint endpoint) {
        }
    }
}

Adding some client code for testing

CoreServiceClient channel = new CoreServiceClient("basicHttp_2011");
channel.ChannelFactory.Endpoint.Behaviors.Add(new CustomEndpointBehavior());

OrganizationalItemItemsFilterData filter = new OrganizationalItemItemsFilterData();
filter.ItemTypes = new ItemType[] { ItemType.Component, ItemType.Folder };
filter.BaseColumns = ListBaseColumns.IdAndTitle;
           
XElement result = channel.GetListXml("tcm:5-4-2", filter);

This code will output the following input/output soap messages.

Input Messaage
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://www.sdltridion.com/ContentManager/CoreService/2011/ICoreService/GetListXml</Action>
  </s:Header>
  <s:Body>
    <GetListXml xmlns="http://www.sdltridion.com/ContentManager/CoreService/2011">
      <id>tcm:5-4-2</id>
      <filter xmlns:d4p1="http://www.sdltridion.com/ContentManager/R6" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" i:type="d4p1:OrganizationalItemItemsFilterData">
        <d4p1:BaseColumns>IdAndTitle</d4p1:BaseColumns>
        <d4p1:IncludeAllowedActionsColumns>false</d4p1:IncludeAllowedActionsColumns>
        <d4p1:ItemTypes>
          <d4p1:ItemType>Component</d4p1:ItemType>
          <d4p1:ItemType>Folder</d4p1:ItemType>
        </d4p1:ItemTypes>
      </filter>
    </GetListXml>
  </s:Body>
</s:Envelope>

Output Message
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <ActivityId CorrelationId="21305a1e-ab4c-453e-99a0-44d53163d617" xmlns="http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics">244b2dd5-4477-4d09-88a9-d909f867d3b3</ActivityId>
  </s:Header>
  <s:Body>
    <GetListXmlResponse xmlns="http://www.sdltridion.com/ContentManager/CoreService/2011">
      <GetListXmlResult>
        <tcm:ListItems Managed="10682" ID="tcm:5-4-2" xmlns:tcm="http://www.tridion.com/ContentManager/5.0">
          <tcm:Item ID="tcm:5-38-2" Title="FootNotes" />
          <tcm:Item ID="tcm:5-35-2" Title="Level1" />
          <tcm:Item ID="tcm:5-40-2" Title="LightBoxes" />
          <tcm:Item ID="tcm:5-43-2" Title="Multimedia Library" />
          <tcm:Item ID="tcm:5-46-2" Title="Notes" />
          <tcm:Item ID="tcm:5-41-2" Title="Overlays" />
          <tcm:Item ID="tcm:5-24-2" Title="TestContent" />
          <tcm:Item ID="tcm:5-42-2" Title="TooTips" />
          <tcm:Item ID="tcm:5-643" Title="MC1" />
          <tcm:Item ID="tcm:5-642" Title="New Component" />
        </tcm:ListItems>
      </GetListXmlResult>
    </GetListXmlResponse>
  </s:Body>
</s:Envelope>

Consuming SDL Tridion Core Services from Adobe Flex - Part 1

With the SDL Tridion 2011 arrival several integration opportunities came into the picture of content consumption and integration. In this post I will cover the basics about how to consume content from SDL Tridion by using non COM, .Net or java related technologies like adobe flex.

The techniques covered in this post can be used for a wide range of technologies which can support standard SOAP 1.1 web services.

Establishing connectivity between Core Services and Flex. 

In order to establish connectivity in adobe flex we will use the mx.rpc.soap.WebService class. Not like .Net or Java, flex does not provide an abstraction layer like a stub which hides the low level details, in this case it is necessary to have a good understanding of the Core Services WSDL.

Not like WCF where we have the concepts of endpoint, binding, addresses, methods and contracts, in adobe flex we have to use standard WSDL concepts like ports, operations, messages and schemas.

The following WSDL fragment will show the list of Core Services ports that can be used in flex.

<wsdl:service name="CoreService2011">
  <wsdl:port name="basicHttp" binding="i0:basicHttp">
    <soap:address location="http://localhost/webservices/CoreService2011.svc/basicHttp" />
  </wsdl:port>
  <wsdl:port name="streamDownload_basicHttp" binding="i0:streamDownload_basicHttp">
    <soap:address location="http://localhost/webservices/CoreService2011.svc/streamDownload_basicHttp" />
  </wsdl:port>
  <wsdl:port name="streamUpload_basicHttp" binding="i0:streamUpload_basicHttp">
    <soap:address location="http://localhost/webservices/CoreService2011.svc/streamUpload_basicHttp" />
  </wsdl:port>
</wsdl:service>

streamDownload_basicHttp and streamUpload_basicHttp are not 100% soap 1.1 compatible since they use MTOM/XOP optimizations as message format and Flex does not support MTOM/XOP yet. I found a workaround for it by adding a new binding/endpoint combination that uses base64Array(Text) as message format which is the format used for interoperability.

For this post I have developed 3 actionscript classes than can be used to connect by using different kind of ports. CoreServicesClientBase is the base class used by BasicHttpClient and StreamDownloadClient

CoreServicesClientBase.cs

package com.tridion.cs {
    import mx.rpc.events.*;
    import mx.rpc.soap.*;
     
    public class CoreServicesClientBase {
        public const WSDL_URL:String = "http://localhost/webservices/CoreService2011.svc?singleWsdl";
        public const CORESERVICE_NS:String = "http://www.sdltridion.com/ContentManager/CoreService/2011";
        public const TRIDION_NS:String = "http://www.sdltridion.com/ContentManager/R6";
        public const XSDI_NS:String = "http://www.w3.org/2001/XMLSchema-instance";
        public const TRIDION_5_NS:String = "http://www.tridion.com/ContentManager/5.0";
       
        protected var port:String;
        protected var channel:WebService;
       
        public function CoreServicesClientBase(loadListener:Function) {
            channel = new WebService();
            channel.useProxy = false;
            channel.port = port;
            channel.addEventListener(LoadEvent.LOAD, loadListener);
            channel.loadWSDL(WSDL_URL);
        }
    }
}

BasicHttpClient.cs

package com.tridion.cs {
    import mx.rpc.events.*;
    import mx.rpc.soap.*;
    import mx.rpc.wsdl.*;
    import mx.rpc.xml.*;
    import mx.utils.ObjectProxy;
   
    public class BasicHttpClient extends CoreServicesClientBase {
        public function BasicHttpClient(loadListener:Function) {
            port = "basicHttp";
            super(loadListener);
        }
    }
}


StreamDownloadClient.cs

package com.tridion.cs {
    import mx.rpc.events.*;
    import mx.rpc.soap.*;
    import mx.rpc.wsdl.*;
    import mx.rpc.xml.*;
    import mx.utils.ObjectProxy;
   
    public class StreamDownloadClient extends CoreServicesClientBase {
        public function StreamDownloadClient(loadListener:Function) {
            port = "streamDownload_base64_basicHttp";
            super(loadListener);
        }
    }
}


*Note: As mentioned above, I added a new binding/endpoint "streamDownload_base64_bassicHttp" to the Core Services configuration file in order to disable MTOM/XOP but keeping it in the out of the box functionality.


The last step in this post will be the actual code to connect to the Core Services as shown in the code fragment below.

protected var channel:BasicHttpClient;
protected var downloadChannel:StreamDownloadClient;

protected function Init():void {
    InitBasicHttpChannel();
    InitDownloadHttpChannel();
}

protected function InitBasicHttpChannel():void {
    var OnChannelLoaded:Function = function(event:LoadEvent):void {
        event.currentTarget.removeEventListener(LoadEvent.LOAD, OnChannelLoaded);
       
        trace("Connection is established using BasicHttp");
    };
    channel = new BasicHttpClient(OnChannelLoaded);   
}

protected function InitDownloadHttpChannel():void {
    var OnChannelLoaded:Function = function(event:LoadEvent):void {
        event.currentTarget.removeEventListener(ResultEvent.RESULT, OnChannelLoaded);
       
        trace("Connection is established using StreamDownload_BasicHttp");
    };
    downloadChannel = new StreamDownloadClient(OnChannelLoaded);
}