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

Scaling Policies

This post is part of a bigger topic Autoscaling Publishers in AWS . In a previous post we talked about the Auto Scaling Groups , but we didn't go into details on the Scaling Policies. This is the purpose of this blog post. As defined earlier, the Scaling Policies define the rules according to which the group size is increased or decreased. These rules are based on instance metrics (e.g. CPU), CloudWatch custom metrics, or even CloudWatch alarms and their states and values. We defined a Scaling Policy with Steps, called 'increase_group_size', which is triggered first by the CloudWatch Alarm 'Publish_Alarm' defined earlier. Also depending on the size of the monitored CloudWatch custom metric 'Waiting for Publish', the Scaling Policy with Steps can add a difference number of instances to the group. The scaling policy sets the number of instances in group to 1 if there are between 1000 and 2000 items Waiting for Publish in the queue. It also sets the

Toolkit - Dynamic Content Queries

This post if part of a series about the  File System Toolkit  - a custom content delivery API for SDL Tridion. This post presents the Dynamic Content Query capability. The requirements for the Toolkit API are that it should be able to provide CustomMeta queries, pagination, and sorting -- all on the file system, without the use third party tools (database, search engines, indexers, etc). Therefore I had to implement a simple database engine and indexer -- which is described in more detail in post Writing My Own Database Engine . The querying logic does not make use of cache. This means the query logic is executed every time. When models are requested, the models are however retrieved using the ModelFactory and those are cached. Query Class This is the main class for dynamic content queries. It is the entry point into the execution logic of a query. The class takes as parameter a Criterion (presented below) which triggers the execution of query in all sub-criteria of a Criterio

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