Tuesday, June 25, 2013

Enabling Session Preview and Dynamic Web Sites


Since the release date of the UI Update 2012 (Experience Manager or XPM) I have noticed some challenges during implementations while integrating XPM Session Preview with dynamic web sites or when some kind of URL Routing is used.
When I refer to dynamic web sites, I refer to web sites using MVC and any kind of URL Routing.

The Problem
  • Implementers are not aware that Tridion Content Delivery API (Java or .Net) and Tridion Content Delivery Web Service (since Tridion 2013 only) already understand when a session preview token is available and will return the data from the right data store (preview or regular broker) automatically.
  • Implementers consider that Page/Binary filters (Java) or the Session Preview Module (.Net) are mandatory in an XPM set up while they should be considered as optional and just useful in some scenarios. These filters and Modules are very useful when our site is static and there is no URL routing involved.

The Solution

The solution is always to identify what is your current scenario in order to select the correct setup and implementation, in this post I will describe some scenarios but of course they are not a final list.

·        Web Site with fixed URLs

The Page/Binary filters or Session Preview Module are ideal for this scenario, XPM will serialize temporary files by appending the session preview token to the file names forwarding requests to these files in case a Session Preview Toke is enabled.

Scenarios that fall in this category are:

o   ASP .Net Web Forms without Routing.
o   Classic JSP Web Application without routing.
o   Static HTML Web Site.

·        Web Site with different Context Locations

When I refer to different context locations I consider scenarios when in the Web Application we use a context location that does not match with the current URL, this scenario is not URL routing but URL forwarding. In this case the static files exist in the file system but they are stored in a different location so the web application needs to map the URL with the real file path and forward the request. There is a hot fix available for this scenarios, the hot fix will allow you to define 4 claims as defined below:
o   full_url: The full http request URL.
o   real_path: The path in the file system for the http request.
o   root_path: The context path.
o   forwarding: Indicates if the http request should be forwarded or not.
These Ambient Data Framework claims should be filled using some custom logic like a java filter or a .net http module.

        Scenarios that fall in this category are:
o   Java Web Application with a different context location defined in the Web Application Description.
o   Java Web Application that uses a Content Rendition Framework.

·        Dynamic Web Sites

In this category I will group all the web sites using MVC as an implementation pattern as well as any website using URL Routing.
For this kind of web sites I found that the Page/Binary filters or the Session Preview Module are not good options and we should avoid them, the solution for those scenarios can vary depending on the nature of the implementation, it could be as simple as just remove the filters or module from the web application to develop mechanisms to get information from the preview data store.
Scenarios that fall in this category are:
o   ASP .Net MVC Web Sites
o   Spring MVC Web Sites
o   ASP .Net Web Forms using Routing

The Example
 
For this example I am using ASP .Net MVC 4 and I will show how to accomplish session preview for a fully dynamic web site using a Virtual Path Provider and a extending the existing Razor View Engine.

  • Create a Virtual File

public class TridionVirtualFile : VirtualFile {
    private static readonly int publicationId = 7;

    public TridionVirtualFile(string virtualPath) : base(virtualPath) {
    }

    public static bool FileExist(string virtualPath) {
        if (Path.GetExtension(virtualPath).Equals(".cshtml")) {
            PageMetaFactory pageMetaFactory = new PageMetaFactory(publicationId);
            return pageMetaFactory.GetMetaByUrl(publicationId, virtualPath) != null;
        }
        return false;  
    }

    public bool FileExist(string virtualPath, out IPageMeta pageMeta) {
        PageMetaFactory pageMetaFactory = new PageMetaFactory(publicationId);
        pageMeta = pageMetaFactory.GetMetaByUrl(publicationId, virtualPath);
        return pageMeta != null;
    }

    public override Stream Open() {
        IPageMeta pageMeta;
        if (FileExist(VirtualPath, out pageMeta)) {
            PageContentAssembler assembler = new PageContentAssembler();
            string content =
assembler.GetContent(pageMeta.PublicationId, pageMeta.Id);
            byte[] bytes = Encoding.UTF8.GetBytes(content);
            return new MemoryStream(bytes);
        }
        return new MemoryStream();  
    }
}

  •  Create a Virtual Path Provider

public class TridionVirtualPathProvider : VirtualPathProvider {
    public override bool FileExists(string virtualPath) {
        if (TridionVirtualFile.FileExist(virtualPath)) {
            return true;
        }
        else {
            return Previous.FileExists(virtualPath);
        }
    }

    public override VirtualFile GetFile(string virtualPath) {
        if (TridionVirtualFile.FileExist(virtualPath)) {
            return new TridionVirtualFile(virtualPath);
        }
        else {
            return Previous.GetFile(virtualPath);
        }
    }

    public override CacheDependency GetCacheDependency(
string virtualPath,
IEnumerable virtualPathDependencies,
DateTime utcStart) {
        if (TridionVirtualFile.FileExist(virtualPath)) {
            return new CacheDependency(
                new string[] { HostingEnvironment.MapPath("~/Web.config") },
                new string[] { virtualPath }
            );
        }
        else {
            return Previous.GetCacheDependency(
virtualPath, virtualPathDependencies, utcStart);
        }
    }
}

  • Register the Virtual Path Provider
public static void RegisterVirtualPathProvider() {
HostingEnvironment.RegisterVirtualPathProvider(new TridionVirtualPathProvider());
}

  • Extend the Razor View Engine

public class TridionViewEngine : RazorViewEngine {

    private const int publicationId = [PublicationId];

    public override ViewEngineResult FindView(
ControllerContext controllerContext,
string viewName,
string masterName,
bool useCache) {

        ViewEngineResult result =
base.FindView(controllerContext, viewName, masterName, useCache);

        if (result.View != null) {
            string viewPath =
((RazorView)result.View).ViewPath.Replace("~", string.Empty);

            PageMetaFactory pageMetaFactory = new PageMetaFactory(publicationId);
            IPageMeta pageMeta =
pageMetaFactory.GetMetaByUrl(publicationId, viewPath);

            if (pageMeta != null) {
                if (HttpContext.Current.Cache[viewPath] == null) {
                    HttpContext.Current.Cache[viewPath] =
pageMeta.LastPublicationDate;
                }
                else {
                    DateTime lastPublishedDate =
                                      (DateTime)HttpContext.Current.Cache[viewPath];

                    if (lastPublishedDate < pageMeta.LastPublicationDate) {
                        HttpContext.Current.Cache.Remove(viewPath);

                        result =
base.FindView(controllerContext, viewName, masterName, useCache);

                        HttpContext.Current.Cache[viewPath] =
pageMeta.LastPublicationDate;
                    }
                }
            }
        }
        return result;
    }
}


  • Register our new View Engine

public static void RegisterViewEngines() {
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new TridionViewEngine());
}


  • Handle Images and Binaries
public class SessionPreviewBinaryModule : IHttpModule {
    private readonly int publicationId = 7;
    private readonly string SessionPreviewToken =
"taf:claim:contentdelivery:webservice:preview:sessionid";
    private readonly string defaultVariantId = "default";

    public void Init(HttpApplication context) {
        context.PreRequestHandlerExecute +=
new EventHandler(OnPreRequestHandlerExecute);
    }

    private void OnPreRequestHandlerExecute(object sender, EventArgs e) {
        HttpContext httpContext = HttpContext.Current;

        if (httpContext != null) {
            string url = httpContext.Request.Url.AbsolutePath;
            string relativeUrl = httpContext.Request.Url.PathAndQuery;

            if (Path.GetExtension(url).ToLower().Equals(".jpg") ||
                Path.GetExtension(url).ToLower().Equals(".gif") ||
                Path.GetExtension(url).ToLower().Equals(".png")) {

                ClaimStore claimStore = AmbientDataContext.CurrentClaimStore;
                Dictionary<Uri, object> claims =
(Dictionary<Uri, object>)claimStore.GetAll();
                Uri sessionTokenUri = new Uri(SessionPreviewToken);

                if (claims.ContainsKey(sessionTokenUri)) {
                    BinaryMetaFactory binaryMetaFactory = new BinaryMetaFactory();
                    BinaryMeta binaryMeta =
                        binaryMetaFactory.GetMetaByUrl(publicationId, relativeUrl);

                    if (binaryMeta != null) {
                        BinaryFactory binaryFactory = new BinaryFactory();
                        BinaryData binaryData = binaryFactory.GetBinary(publicationId,
                                                   binaryMeta.Id, defaultVariantId);

                        HttpResponse response = httpContext.Response;
                        response.Clear();
                        response.ContentType = binaryMeta.Type;
                        response.BinaryWrite(binaryData.Bytes);
                        response.Flush();
                    }
                }
            }
        }
    }

    public void Dispose() {
    }
}


  • Use our views
public ActionResult Index() {
    return View();
      }