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...

I Have Gone Dark

Maybe it's the Holidays, but my mood has gone pretty dark. That is, regarding the look and feel of my computer and Tridion CME, of course. What I did was to dim the lights on the operating system, so I installed Placebo themes for Windows 7 . I went for the Ashtray look -- great name :) My VM looks now like this: But, once you change the theme on Windows, you should 'match' the theme of your applications. Some skin easily, some not. The Office suite has an in-built scheme, which can be set to Black , but it doesn't actually dim the ribbon tool bars -- it looks quite weird. Yahoo Messenger is skinnable, but you can't change the big white panels where you actually 'chat'. Skype is not skinnable at all. For Chrome, there are plenty of grey themes. Now i'm using Pro Grey . But then I got into changing the theme of websites. While very few offer skinnable interfaces (as GMail does), I had to find a way to darken the websites... Enter Stylish -- a pl...

REL Standard Tag Library

The RSTL is a library of REL tags providing standard functionality such as iterating collections, conditionals, imports, assignments, XML XSLT transformations, formatting dates, etc. RSTL distributable is available on my Google Code page under  REL Standard Tag Library . Always use the latest JAR . This post describes each RSTL tag in the library explaining its functionality, attributes and providing examples. For understanding the way expressions are evaluated, please read my post about the  Expression Language used by REL Standard Tag Library . <c:choose> / <c:when> / <c:otherwise> Syntax:     <c:choose>         <c:when test="expr1">             Do something         </c:when>         <c:when test="expr2">             Do something else         </c:when...