/*
* Copyright (C) 2005-2010 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
*/
package org.alfresco.repo.template;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.alfresco.repo.processor.BaseProcessor;
import org.alfresco.service.cmr.repository.TemplateException;
import org.alfresco.service.cmr.repository.TemplateProcessor;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.XMLUtil;
import org.apache.bsf.BSFManager;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xml.utils.Constants;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import freemarker.cache.TemplateLoader;
public class XSLTProcessor extends BaseProcessor implements TemplateProcessor
{
private static final Log log = LogFactory.getLog(XSLTProcessor.class);
private static final String MSG_UNABLE_TO_READ_TEMPLATE = "template.xslt.read_error";
private static final String MSG_UNABLE_TO_PARSE_TEMPLATE = "template.xslt.parse_error";
public final static QName ROOT_NAMESPACE = QName.createQName(null, "root_namespace");
private String defaultEncoding = "UTF-8";
private TemplateLoader templateLoader;
public void register()
{
super.register();
templateLoader = new ClassPathRepoTemplateLoader(this.services.getNodeService(), this.services
.getContentService(), defaultEncoding);
}
public void process(String template, Object model, Writer out)
{
TemplateSource templateSource;
try
{
templateSource = (TemplateSource) templateLoader.findTemplateSource(template);
}
catch (IOException ex)
{
throw new TemplateException(MSG_UNABLE_TO_READ_TEMPLATE, new Object[] { ex.getMessage() }, ex);
}
process(templateSource, model, out);
}
public void processString(final String template, Object model, Writer out)
{
TemplateSource stringTemplateSource = new TemplateSource()
{
public long lastModified()
{
return System.currentTimeMillis();
}
public InputStream getResource(String name)
{
return null;
}
public Reader getReader(String encoding) throws IOException
{
return new StringReader(template);
}
public void close() throws IOException
{
}
};
process(stringTemplateSource, model, out);
}
/**
* @param templateSource
* @param xsltModel
* @param out
*/
private void process(TemplateSource templateSource, Object model, Writer out)
{
if ((model == null) || !XSLTemplateModel.class.isAssignableFrom(model.getClass()))
{
throw new IllegalArgumentException("\"model\" must be an XSLTemplateModel object: " + model);
}
XSLTemplateModel xsltModel = (XSLTemplateModel) model;
System.setProperty("org.apache.xalan.extensions.bsf.BSFManager", BSFManager.class.getName());
Document xslTemplate;
try
{
xslTemplate = XMLUtil.parse(templateSource.getReader(defaultEncoding));
}
catch (IOException ex)
{
throw new TemplateException(MSG_UNABLE_TO_READ_TEMPLATE, new Object[] { ex.getMessage() }, ex);
}
catch (SAXException sax)
{
throw new TemplateException(MSG_UNABLE_TO_PARSE_TEMPLATE, new Object[] { sax.getMessage() }, sax);
}
finally
{
try
{
templateSource.close();
}
catch (IOException ex)
{
// There's little to be done here. Log it and carry on
log.warn("Error while trying to close template stream", ex);
}
}
List scriptIds = addScripts(xsltModel, xslTemplate);
addParameters(xsltModel, xslTemplate);
final LinkedList errors = new LinkedList();
final ErrorListener errorListener = new ErrorListener()
{
public void error(final TransformerException te) throws TransformerException
{
log.debug("error " + te.getMessageAndLocation());
errors.add(te);
}
public void fatalError(final TransformerException te) throws TransformerException
{
log.debug("fatalError " + te.getMessageAndLocation());
throw te;
}
public void warning(final TransformerException te) throws TransformerException
{
log.debug("warning " + te.getMessageAndLocation());
errors.add(te);
}
};
final TemplateSource resourceSource = templateSource;
final URIResolver uriResolver = new URIResolver()
{
public Source resolve(final String href, String base) throws TransformerException
{
if (log.isDebugEnabled())
{
log.debug("request to resolve href " + href + " using base " + base);
}
InputStream in = null;
try
{
in = resourceSource.getResource(href);
if (in == null)
{
throw new TransformerException("unable to resolve href " + href);
}
Document d = XMLUtil.parse(in);
if (log.isDebugEnabled())
{
log.debug("loaded " + XMLUtil.toString(d));
}
return new DOMSource(d);
}
catch (TransformerException ex)
{
throw ex;
}
catch (Exception e)
{
throw new TransformerException("unable to load " + href, e);
}
}
};
Source xmlSource = this.getXMLSource(xsltModel);
Transformer t = null;
try
{
final TransformerFactory tf = TransformerFactory.newInstance();
tf.setErrorListener(errorListener);
tf.setURIResolver(uriResolver);
if (log.isDebugEnabled())
{
log.debug("xslTemplate: \n" + XMLUtil.toString(xslTemplate));
}
t = tf.newTransformer(new DOMSource(xslTemplate));
if (errors.size() != 0)
{
final StringBuilder msg = new StringBuilder("errors encountered creating tranformer ... \n");
for (TransformerException te : errors)
{
msg.append(te.getMessageAndLocation()).append("\n");
}
throw new TemplateException(msg.toString());
}
t.setErrorListener(errorListener);
t.setURIResolver(uriResolver);
t.setParameter("versionParam", "2.0");
}
catch (TransformerConfigurationException tce)
{
log.error(tce);
throw new TemplateException(tce.getMessage(), tce);
}
try
{
t.transform(xmlSource, new StreamResult(out));
}
catch (TransformerException te)
{
log.error(te.getMessageAndLocation());
throw new TemplateException(te.getMessageAndLocation(), te);
}
catch (Exception e)
{
log.error("unexpected error " + e);
throw new TemplateException(e.getMessage(), e);
}
finally
{
//Clear out any scripts that were created for this transform
if (!scriptIds.isEmpty())
{
XSLTProcessorMethodInvoker.removeMethods(scriptIds);
}
}
if (errors.size() != 0)
{
final StringBuilder msg = new StringBuilder("errors encountered during transformation ... \n");
for (TransformerException te : errors)
{
msg.append(te.getMessageAndLocation()).append("\n");
}
throw new TemplateException(msg.toString());
}
}
/**
* Adds a script element to the xsl which makes static methods on this object available to the xsl tempalte.
*
* @param xslTemplate
* the xsl template
*/
protected List addScripts(final XSLTemplateModel xsltModel, final Document xslTemplate)
{
final Map>> methods = new HashMap>>();
for (final Map.Entry entry : xsltModel.entrySet())
{
if (entry.getValue() instanceof TemplateProcessorMethod)
{
final String prefix = QName.splitPrefixedQName(entry.getKey().toPrefixString())[0];
final QName qn = QName.createQName(entry.getKey().getNamespaceURI(), prefix);
if (!methods.containsKey(qn))
{
methods.put(qn, new LinkedList>());
}
methods.get(qn).add(entry);
}
}
final Element docEl = xslTemplate.getDocumentElement();
final String XALAN_NS = Constants.S_BUILTIN_EXTENSIONS_URL;
final String XALAN_NS_PREFIX = "xalan";
docEl.setAttribute("xmlns:" + XALAN_NS_PREFIX, XALAN_NS);
final Set excludePrefixes = new HashSet();
if (docEl.hasAttribute("exclude-result-prefixes"))
{
excludePrefixes.addAll(Arrays.asList(docEl.getAttribute("exclude-result-prefixes").split(" ")));
}
excludePrefixes.add(XALAN_NS_PREFIX);
final List result = new LinkedList();
for (QName ns : methods.keySet())
{
final String prefix = ns.getLocalName();
docEl.setAttribute("xmlns:" + prefix, ns.getNamespaceURI());
excludePrefixes.add(prefix);
final Element compEl = xslTemplate.createElementNS(XALAN_NS, XALAN_NS_PREFIX + ":component");
compEl.setAttribute("prefix", prefix);
docEl.appendChild(compEl);
String functionNames = null;
final Element scriptEl = xslTemplate.createElementNS(XALAN_NS, XALAN_NS_PREFIX + ":script");
scriptEl.setAttribute("lang", "javascript");
final StringBuilder js = new StringBuilder("var _xsltp_invoke = java.lang.Class.forName('"
+ XSLTProcessorMethodInvoker.class.getName() + "').newInstance();\n"
+ "function _xsltp_to_java_array(js_array) {\n"
+ "var java_array = java.lang.reflect.Array.newInstance(java.lang.Object, js_array.length);\n"
+ "for (var i = 0; i < js_array.length; i++) { java_array[i] = js_array[i]; }\n"
+ "return java_array; }\n");
for (final Map.Entry entry : methods.get(ns))
{
if (functionNames == null)
{
functionNames = entry.getKey().getLocalName();
}
else
{
functionNames += " " + entry.getKey().getLocalName();
}
final String id = entry.getKey().getLocalName() + entry.getValue().hashCode();
js.append("function " + entry.getKey().getLocalName() + "() { return _xsltp_invoke.invokeMethod('" + id
+ "', _xsltp_to_java_array(arguments)); }\n");
XSLTProcessorMethodInvoker.addMethod(id, (TemplateProcessorMethod) entry.getValue());
result.add(id);
}
log.debug("generated JavaScript bindings:\n" + js);
scriptEl.appendChild(xslTemplate.createTextNode(js.toString()));
compEl.setAttribute("functions", functionNames);
compEl.appendChild(scriptEl);
}
docEl.setAttribute("exclude-result-prefixes", StringUtils.join(excludePrefixes
.toArray(new String[excludePrefixes.size()]), " "));
return result;
}
/**
* Adds the specified parameters to the xsl template as variables within the alfresco namespace.
*
* @param xsltModel
* the variables to place within the xsl template
* @param xslTemplate
* the xsl template
*/
protected void addParameters(final XSLTemplateModel xsltModel, final Document xslTemplate)
{
final Element docEl = xslTemplate.getDocumentElement();
final String XSL_NS = docEl.getNamespaceURI();
final String XSL_NS_PREFIX = docEl.getPrefix();
for (Map.Entry e : xsltModel.entrySet())
{
if (ROOT_NAMESPACE.equals(e.getKey()))
{
continue;
}
final Element el = xslTemplate.createElementNS(XSL_NS, XSL_NS_PREFIX + ":variable");
el.setAttribute("name", e.getKey().toPrefixString());
final Object o = e.getValue();
if (o instanceof String || o instanceof Number || o instanceof Boolean)
{
el.appendChild(xslTemplate.createTextNode(o.toString()));
docEl.insertBefore(el, docEl.getFirstChild());
}
}
}
protected Source getXMLSource(final Map model)
{
if (!model.containsKey(ROOT_NAMESPACE))
{
return null;
}
final Object o = model.get(ROOT_NAMESPACE);
if (!(o instanceof Document))
{
throw new IllegalArgumentException("expected root namespace object to be a " + Document.class.getName()
+ ". found a " + o.getClass().getName());
}
return new DOMSource((Document) o);
}
}