Skip to main content

Executing an ITemplate Inside a JAR TBB

In this post I'll talk a bit about the next functionality of my Java Mediator for SDL Tridion templating -- uploading a JAR containing ITemplate classes into the Content Manager and executing a particular template.

JAR Template Building Blocks

In a very similar fashion to a .net assembly TBB, I implemented the functionality of having a new Template type for storing JARs inside a TBB, in a binary fashion. These TBBs have a special type, binary, that allows uploading a JAR.
A JAR TBB contains Java classes, some of them representing implementations of the ITemplate interface. The actual interface is the TOM.Java proxy tridion.contentmanager.templating.assembly.ITemplate.

I created a new Template type in configuration file Tridion.ContentManager.config, under node <configuration> / <templateTypeRegistry> / <templateTypes>:

<add id="10" name="JAR TBB" mimeType="application/java-archive" hasBinaryContent="true" contentHandler="Mitza.Mediator.GAC.JarContentHandler, Mediator.GAC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7324749889d34429">
    <webDavFileExtensions>
        <add itemType="TemplateBuildingBlock" fileExtension="jar"/>
    </webDavFileExtensions>
</add>

Note the contentHandler has to be specified. Without it, errors are thrown upon save. The content handler implementation is empty.

Execute an ITemplate inside a JAR TBB

For executing templates inside a JAR TBB, I chose the same approach the out-of-the-box SDL Tridion does: a TBB that calls a special tag triggering the execution of a named ITemplate implementation class and the TcmUri of the JAR TBB the ITemplate is defined in. In my case, I chose a JSP/JSTL TBB to trigger the template execution:

<%@taglib prefix="t" uri="http://www.mitza.net/java/mediator/tag"%>
<t:executeTemplate jar="tcm:20-848-2048" template="mitza.template.FormatDate"/>
The executeTemplate JSP custom tag uses a custom Java class loader to read the given template class from the JAR inside the specified jar TBB.

public void doTag() throws JspException, IOException {
    FakePageContext pageContext = getJspContext();
    Engine engine = pageContext.getEngine();
    Package _package = pageContext.getPackage();

    TemplateBuildingBlock jarTbb = (TemplateBuildingBlock) engine.GetObject(jar);
    JarTbbClassLoader classLoader = new JarTbbClassLoader(jarTbb);
    try {
        Class<?> templateClass = classLoader.loadClass(template);
        ITemplate instance = (ITemplate) templateClass.newInstance();
        instance.Transform(engine, _package);
    } catch (Exception e) {
        throw new MediatorException(e);
    }
}

Helper Class JarTbbClassLoader

The entire logic of the custom tag resides in loading the right class from the given JAR bytes dynamically and of course, efficiently.

The class extends java.lang.ClassLoader and only overrides the findClass method. The named class is searched in an LRU cache (detailed further-down), and, if not found, it is loaded from the given Template Building Block object's BinaryContent property. The cache look-up also takes into consideration the last-modified date of the JAR TBB. Any class that was cached before the modification of the JAR TBB is ignored and it gets replaced by the latest version from the JAR.

Java provides a very neat feature of iterating over the entries of a JAR file, using the JarInputStream object and its getNextJarEntry iterator.

Once found by name, a Class object is created from the bytes array read from the JAR and it is pushed into the LRU cache.

public class JarTbbClassLoader extends ClassLoader {

    private final TemplateBuildingBlock tbb;
    private long lastModified;

    public JarTbbClassLoader(TemplateBuildingBlock tbb) {
        this.tbb = tbb;
        lastModified = Utils.getTicksToMillis(tbb.getRevisionDate().ToUniversalTime().getTicks());
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        ClassLoaderCache cache = ClassLoaderCache.getInstance();
        CacheEntry cacheEntry = cache.get(name);
        if (cacheEntry != null && cacheEntry.getLastModified() > lastModified) {
            return cacheEntry.getClazz();
        }

        String className = name.replaceAll("\\.", "/").concat(".class");
        JarInputStream jarInputStream = null;
        try {
            byte[] bytes = tbb.getBinaryContent().GetByteArray();
            jarInputStream = new JarInputStream(new ByteArrayInputStream(bytes));
            for (JarEntry entry = jarInputStream.getNextJarEntry(); entry != null; entry = jarInputStream.getNextJarEntry()) {
                if (entry.getName().equals(className)) {
                    byte[] buffer = getClassBytes(jarInputStream);
                    Class<?> clazz = defineClass(name, buffer, 0, buffer.length);
                    cache.put(name, clazz);

                    return clazz;
                }
            }
        } catch (Exception e) {
            throw new MediatorException(e);
        } finally {
            if (jarInputStream != null) {
                try {
                    jarInputStream.close();
                } catch (IOException ioe) {
                    throw new MediatorException(ioe);
                }
            }
        }

        throw new ClassNotFoundException(name);
    }

    private byte[] getClassBytes(JarInputStream jarInputStream) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            byte[] buffer = new byte[2048];
            for (int read = 0; (read = jarInputStream.read(buffer, 0, buffer.length)) != -1;) {
                out.write(buffer, 0, read);
            }

            return out.toByteArray();
        } finally {
            out.close();
        }
    }
}

Helper Class ClassLoaderCache

This class is a singleton around a LruCache map (detailed further-below). It allows storage and retrieval of CacheEntry objects (i.e. simple data objects consisting of last-modified and class members).

public class ClassLoaderCache {

    private static final int MAX_SIZE = 100;
    private static final ClassLoaderCache instance;
    private final Map<String, CacheEntry> cache;

    private ClassLoaderCache() {
        cache = Collections.synchronizedMap(new LruCache<String, CacheEntry>(MAX_SIZE));
    }

    public CacheEntry get(String className) {
        return cache.get(className);
    }

    public CacheEntry put(String className, Class<?> clazz) {
        CacheEntry cacheEntry = new CacheEntry(System.currentTimeMillis(), clazz);
        return cache.put(className, cacheEntry);
    }

    public static ClassLoaderCache getInstance() {
        if (instance == null) {
            instance = new ClassLoaderCache();
        }

        return instance;
    }
}

Helper Class LruCache

This class represents a LinkedHashMap -- an awesome data structure, which is in fact an out-of-the-box LRU (Least Recently Used) cache provided by Java. The map provides O(1) insertion, contains and removal of cache values, while maintaining the insertion order into a FIFO queue.

The class provides an easy trigger to remove the 'eldest entry' by means of overriding method removeEldestEntry. In the LRU cache case, this should happen when the map reaches a specified (maxEntries) size.


public class LruCache<K, V> extends LinkedHashMap<K, V> {

    private final int maxEntries;

    public LruCache(final int maxEntries) {
        super(maxEntries + 1, 1.0f, true);
        this.maxEntries = maxEntries;
    }

    @Override
    protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) {
        return super.size() > maxEntries;
    }
}

A Java ITemplate Example

The following class formats a java.util.Date according to a specified pattern. The value of the Date object is either read from a Package expression, or, if empty, the value 'now' is used. The pattern is read from a Package parameter.

public class FormatDate implements ITemplate {

    private String paramInputValue;
    private String paramOutputValue;
    private String paramFormat;

    private TemplatingLogger log = TemplatingLogger.GetLogger(Engine.typeof());
    private Package _package;

    public void Transform(Engine engine, Package _package) {
        this._package = _package;

        readParameters();
        Date date = getDate();
        String formattedDate = formatDate(date);

        _package.PushItem(paramOutputValue, _package.CreateStringItem(TomUtil.ContentType.Text, formattedDate));
    }

    private void readParameters() {
        paramInputValue = _package.GetValue("InputValue");
        log.Debug("Parameter 'InputValue': " + paramInputValue);

        paramOutputValue = _package.GetValue("OutputValue");
        log.Debug("Parameter 'OutputValue': " + paramOutputValue);

        paramFormat = _package.GetValue("Format");
        log.Debug("Parameter 'Format': " + paramFormat);
    }

    private Date getDate() {
        Date result;

        if (paramInputValue == null) {
            log.Debug("Parameter 'InputValue' is empty. Using 'now'");
            result = new Date();
        } else {
            String inputDate = _package.GetValue(paramInputValue);
            log.Debug("Input date: " + inputDate);
            try {
                result = DateFormat.getDateTimeInstance().parse(inputDate);
            } catch (ParseException pe) {
                log.Error("ParseException occurred " + pe);
                throw new RuntimeException(pe);
            }
        }

        return result;
    }

    private String formatDate(Date date) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(paramFormat);
        String result = dateFormat.format(date);

        return result;
    }
}

Finally, the execution produces the following Package item:


Comments

Peter Joles said…
Great Article Mihai. Is the Java Mediator complete and do you maintain it? Also if your Template required other library dependencies would you need to combine those jars together with a tool or could you define then in a classpath somewhere?

Popular posts from this blog

Running sp_updatestats on AWS RDS database

Part of the maintenance tasks that I perform on a MSSQL Content Manager database is to run stored procedure sp_updatestats . exec sp_updatestats However, that is not supported on an AWS RDS instance. The error message below indicates that only the sa  account can perform this: Msg 15247 , Level 16 , State 1 , Procedure sp_updatestats, Line 15 [Batch Start Line 0 ] User does not have permission to perform this action. Instead there are several posts that suggest using UPDATE STATISTICS instead: https://dba.stackexchange.com/questions/145982/sp-updatestats-vs-update-statistics I stumbled upon the following post from 2008 (!!!), https://social.msdn.microsoft.com/Forums/sqlserver/en-US/186e3db0-fe37-4c31-b017-8e7c24d19697/spupdatestats-fails-to-run-with-permission-error-under-dbopriveleged-user , which describes a way to wrap the call to sp_updatestats and execute it under a different user: create procedure dbo.sp_updstats with execute as 'dbo' as

Content Delivery Monitoring in AWS with CloudWatch

This post describes a way of monitoring a Tridion 9 combined Deployer by sending the health checks into a custom metric in CloudWatch in AWS. The same approach can also be used for other Content Delivery services. Once the metric is available in CloudWatch, we can create alarms in case the service errors out or becomes unresponsive. The overall architecture is as follows: Content Delivery service sends heartbeat (or exposes HTTP endpoint) for monitoring Monitoring Agent checks heartbeat (or HTTP health check) regularly and stores health state AWS lambda function: runs regularly reads the health state from Monitoring Agent pushes custom metrics into CloudWatch I am running the Deployer ( installation docs ) and Monitoring Agent ( installation docs ) on a t2.medium EC2 instance running CentOS on which I also installed the Systems Manager Agent (SSM Agent) ( installation docs ). In my case I have a combined Deployer that I want to monitor. This consists of an Endpoint and a

Debugging a Tridion 2011 Event System

OK, so you wrote your Tridion Event System. Now it's time to debug it. I know this is a hypothetical situtation -- your code never needs any kind of debugging ;) but indulge me... Recently, Alvin Reyes ( @nivlong ) blogged about being difficult to know how exactly to debug a Tridion Event System. More exactly, the question was " What process do I attach to for debugging even system code? ". Unfortunately, there is no simple or generic answer for it. Different events are fired by different Tridion CM modules. These modules run as different programs (or services) or run inside other programs (e.g. IIS). This means that you will need to monitor (or debug) different processes, based on which events your code handles. So the usual suspects are: dllhost.exe (or dllhost3g.exe ) - running as the MTSUser is the SDL Tridion Content Manager COM+ application and it fires events on generic TOM objects (e.g. events based on Tridion.ContentManager.Extensibility.Events.CrudEven