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.
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>:
Note the contentHandler has to be specified. Without it, errors are thrown upon save. The content handler implementation is empty.
Finally, the execution produces the following Package item:
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>
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.
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.
I found this code at http://stackoverflow.com/questions/221525/how-would-you-implement-an-lru-cache-in-java-6.
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