mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-08-07 17:49:17 +00:00
git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@18963 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
524 lines
19 KiB
Java
524 lines
19 KiB
Java
/*
|
|
* 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 <http://www.gnu.org/licenses/>. */
|
|
package org.alfresco.web.forms;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
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.Result;
|
|
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.service.namespace.QName;
|
|
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.dtm.ref.DTMNodeProxy;
|
|
import org.apache.xml.utils.Constants;
|
|
import org.w3c.dom.DOMException;
|
|
import org.w3c.dom.Document;
|
|
import org.w3c.dom.Element;
|
|
import org.w3c.dom.Node;
|
|
import org.w3c.dom.traversal.NodeFilter;
|
|
import org.w3c.dom.traversal.NodeIterator;
|
|
import org.xml.sax.SAXException;
|
|
|
|
/**
|
|
* A rendering engine which uses xsl templates to render renditions of
|
|
* form instance data.
|
|
*
|
|
* @author Ariel Backenroth
|
|
*/
|
|
public class XSLTRenderingEngine
|
|
implements RenderingEngine
|
|
{
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
public static class ProcessorMethodInvoker
|
|
{
|
|
private final static HashMap<String, TemplateProcessorMethod> PROCESSOR_METHODS =
|
|
new HashMap<String, TemplateProcessorMethod>();
|
|
|
|
public ProcessorMethodInvoker() { }
|
|
|
|
private Object[] convertArguments(final Object[] arguments)
|
|
{
|
|
final List result = new LinkedList();
|
|
for (int i = 0; i < arguments.length; i++)
|
|
{
|
|
LOGGER.debug("args[" + i + "] = " + arguments[i] +
|
|
"(" + (arguments[i] != null
|
|
? arguments[i].getClass().getName()
|
|
: "null") + ")");
|
|
if (arguments[i] == null ||
|
|
arguments[i] instanceof String ||
|
|
arguments[i] instanceof Number)
|
|
{
|
|
result.add(arguments[i]);
|
|
}
|
|
else if (arguments[i] instanceof DTMNodeProxy)
|
|
{
|
|
result.add(((DTMNodeProxy)arguments[i]).getStringValue());
|
|
}
|
|
else if (arguments[i] instanceof Node)
|
|
{
|
|
LOGGER.debug("node type is " + ((Node)arguments[i]).getNodeType() +
|
|
" content " + ((Node)arguments[i]).getTextContent());
|
|
result.add(((Node)arguments[i]).getNodeValue());
|
|
}
|
|
else if (arguments[i] instanceof NodeIterator)
|
|
{
|
|
Node n = ((NodeIterator)arguments[i]).nextNode();
|
|
while (n != null)
|
|
{
|
|
LOGGER.debug("iterated to node " + n + " type " + n.getNodeType() +
|
|
" value " + n.getNodeValue() +
|
|
" tc " + n.getTextContent() +
|
|
" nn " + n.getNodeName() +
|
|
" sv " + ((org.apache.xml.dtm.ref.DTMNodeProxy)n).getStringValue());
|
|
if (n instanceof DTMNodeProxy)
|
|
{
|
|
result.add(((DTMNodeProxy)n).getStringValue());
|
|
}
|
|
else
|
|
{
|
|
result.add(n);
|
|
}
|
|
n = ((NodeIterator)arguments[i]).nextNode();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new IllegalArgumentException("unable to convert argument " + arguments[i]);
|
|
}
|
|
}
|
|
|
|
return result.toArray(new Object[result.size()]);
|
|
}
|
|
|
|
public Object invokeMethod(final String id, Object[] arguments)
|
|
throws Exception
|
|
{
|
|
if (!PROCESSOR_METHODS.containsKey(id))
|
|
{
|
|
throw new NullPointerException("unable to find method " + id);
|
|
}
|
|
|
|
final TemplateProcessorMethod method = PROCESSOR_METHODS.get(id);
|
|
arguments = this.convertArguments(arguments);
|
|
LOGGER.debug("invoking " + id + " with " + arguments.length);
|
|
|
|
Object result = method.exec(arguments);
|
|
LOGGER.debug(id + " returned a " + result);
|
|
if (result == null)
|
|
{
|
|
return null;
|
|
}
|
|
else if (result.getClass().isArray() &&
|
|
Node.class.isAssignableFrom(result.getClass().getComponentType()))
|
|
{
|
|
LOGGER.debug("converting " + result + " to a node iterator");
|
|
final Node[] array = (Node[])result;
|
|
return new NodeIterator()
|
|
{
|
|
private int index = 0;
|
|
private boolean detached = false;
|
|
|
|
public void detach()
|
|
{
|
|
if (LOGGER.isDebugEnabled())
|
|
LOGGER.debug("detaching NodeIterator");
|
|
this.detached = true;
|
|
}
|
|
|
|
public boolean getExpandEntityReferences() { return true; }
|
|
public int getWhatToShow() { return NodeFilter.SHOW_ALL; }
|
|
|
|
public Node getRoot()
|
|
{
|
|
return (array.length == 0
|
|
? null
|
|
: array[0].getOwnerDocument().getDocumentElement());
|
|
}
|
|
|
|
public NodeFilter getFilter()
|
|
{
|
|
return new NodeFilter()
|
|
{
|
|
public short acceptNode(final Node n)
|
|
{
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
}
|
|
};
|
|
}
|
|
|
|
public Node nextNode()
|
|
throws DOMException
|
|
{
|
|
if (LOGGER.isDebugEnabled())
|
|
LOGGER.debug("NodeIterator.nextNode(" + index + ")");
|
|
if (this.detached)
|
|
throw new DOMException(DOMException.INVALID_STATE_ERR, null);
|
|
return index == array.length ? null : array[index++];
|
|
}
|
|
|
|
public Node previousNode()
|
|
throws DOMException
|
|
{
|
|
if (LOGGER.isDebugEnabled())
|
|
LOGGER.debug("NodeIterator.previousNode(" + index + ")");
|
|
if (this.detached)
|
|
throw new DOMException(DOMException.INVALID_STATE_ERR, null);
|
|
return index == -1 ? null : array[index--];
|
|
}
|
|
};
|
|
}
|
|
else if (result instanceof String ||
|
|
result instanceof Number ||
|
|
result instanceof Node)
|
|
{
|
|
LOGGER.debug("returning " + result + " as is");
|
|
return result;
|
|
}
|
|
else
|
|
{
|
|
throw new IllegalArgumentException("unable to convert " + result.getClass().getName());
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
private static final Log LOGGER = LogFactory.getLog(XSLTRenderingEngine.class);
|
|
|
|
public XSLTRenderingEngine() { }
|
|
|
|
public String getName() { return "XSLT"; }
|
|
|
|
public String getDefaultTemplateFileExtension() { return "xsl"; }
|
|
|
|
/**
|
|
* 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<String> addScripts(final Map<QName, Object> model,
|
|
final Document xslTemplate)
|
|
{
|
|
final Map<QName, List<Map.Entry<QName, Object>>> methods =
|
|
new HashMap<QName, List<Map.Entry<QName, Object>>>();
|
|
for (final Map.Entry<QName, Object> entry : model.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<String> excludePrefixes = new HashSet<String>();
|
|
if (docEl.hasAttribute("exclude-result-prefixes"))
|
|
{
|
|
excludePrefixes.addAll(Arrays.asList(docEl.getAttribute("exclude-result-prefixes").split(" ")));
|
|
}
|
|
excludePrefixes.add(XALAN_NS_PREFIX);
|
|
|
|
final List<String> result = new LinkedList<String>();
|
|
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('" + ProcessorMethodInvoker.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<QName, Object> 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");
|
|
ProcessorMethodInvoker.PROCESSOR_METHODS.put(id, (TemplateProcessorMethod)entry.getValue());
|
|
result.add(id);
|
|
}
|
|
LOGGER.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 model the variables to place within the xsl template
|
|
* @param xslTemplate the xsl template
|
|
*/
|
|
protected void addParameters(final Map<QName, Object> model,
|
|
final Document xslTemplate)
|
|
{
|
|
final Element docEl = xslTemplate.getDocumentElement();
|
|
final String XSL_NS = docEl.getNamespaceURI();
|
|
final String XSL_NS_PREFIX = docEl.getPrefix();
|
|
|
|
for (Map.Entry<QName, Object> e : model.entrySet())
|
|
{
|
|
if (RenderingEngine.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<QName, Object> model)
|
|
{
|
|
if (!model.containsKey(RenderingEngine.ROOT_NAMESPACE))
|
|
{
|
|
return null;
|
|
}
|
|
final Object o = model.get(RenderingEngine.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);
|
|
}
|
|
|
|
public void render(final Map<QName, Object> model,
|
|
final RenderingEngineTemplate ret,
|
|
final OutputStream out)
|
|
throws IOException,
|
|
RenderingEngine.RenderingException,
|
|
SAXException
|
|
{
|
|
this.render(model, ret, new StreamResult(out));
|
|
}
|
|
|
|
public void render(final Map<QName, Object> model,
|
|
final RenderingEngineTemplate ret,
|
|
final Result result)
|
|
throws IOException,
|
|
RenderingEngine.RenderingException,
|
|
SAXException
|
|
{
|
|
System.setProperty("org.apache.xalan.extensions.bsf.BSFManager",
|
|
BSFManager.class.getName());
|
|
Document xslTemplate = null;
|
|
try
|
|
{
|
|
xslTemplate = XMLUtil.parse(ret.getInputStream());
|
|
}
|
|
catch (final SAXException sax)
|
|
{
|
|
throw new RenderingEngine.RenderingException(sax);
|
|
}
|
|
this.addScripts(model, xslTemplate);
|
|
this.addParameters(model, xslTemplate);
|
|
|
|
final LinkedList<TransformerException> errors = new LinkedList<TransformerException>();
|
|
final ErrorListener errorListener = new ErrorListener()
|
|
{
|
|
public void error(final TransformerException te)
|
|
throws TransformerException
|
|
{
|
|
LOGGER.debug("error " + te.getMessageAndLocation());
|
|
errors.add(te);
|
|
}
|
|
|
|
public void fatalError(final TransformerException te)
|
|
throws TransformerException
|
|
{
|
|
LOGGER.debug("fatalError " + te.getMessageAndLocation());
|
|
throw te;
|
|
}
|
|
|
|
public void warning(final TransformerException te)
|
|
throws TransformerException
|
|
{
|
|
LOGGER.debug("warning " + te.getMessageAndLocation());
|
|
errors.add(te);
|
|
}
|
|
};
|
|
|
|
// create a uri resolver to resolve document() calls to the virtualized
|
|
// web application
|
|
final URIResolver uriResolver = new URIResolver()
|
|
{
|
|
public Source resolve(final String href, String base)
|
|
throws TransformerException
|
|
{
|
|
LOGGER.debug("request to resolve href " + href +
|
|
" using base " + base);
|
|
final RenderingEngine.TemplateResourceResolver trr = (RenderingEngine.TemplateResourceResolver)
|
|
model.get(RenderingEngineTemplateImpl.PROP_RESOURCE_RESOLVER);
|
|
|
|
InputStream in = null;
|
|
try
|
|
{
|
|
in = trr.resolve(href);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new TransformerException("unable to load " + href, e);
|
|
}
|
|
|
|
if (in == null)
|
|
{
|
|
throw new TransformerException("unable to resolve href " + href);
|
|
}
|
|
|
|
try
|
|
{
|
|
final Document d = XMLUtil.parse(in);
|
|
if (LOGGER.isDebugEnabled())
|
|
LOGGER.debug("loaded " + XMLUtil.toString(d));
|
|
return new DOMSource(d);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new TransformerException("unable to load " + href, e);
|
|
}
|
|
}
|
|
};
|
|
|
|
Source xmlSource = this.getXMLSource(model);
|
|
|
|
Transformer t = null;
|
|
try
|
|
{
|
|
final TransformerFactory tf = TransformerFactory.newInstance();
|
|
tf.setErrorListener(errorListener);
|
|
tf.setURIResolver(uriResolver);
|
|
|
|
if (LOGGER.isDebugEnabled())
|
|
{
|
|
LOGGER.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 RenderingEngine.RenderingException(msg.toString());
|
|
}
|
|
|
|
t.setErrorListener(errorListener);
|
|
t.setURIResolver(uriResolver);
|
|
t.setParameter("versionParam", "2.0");
|
|
}
|
|
catch (TransformerConfigurationException tce)
|
|
{
|
|
LOGGER.error(tce);
|
|
throw new RenderingEngine.RenderingException(tce);
|
|
}
|
|
|
|
try
|
|
{
|
|
t.transform(xmlSource, result);
|
|
}
|
|
catch (TransformerException te)
|
|
{
|
|
LOGGER.error(te.getMessageAndLocation());
|
|
throw new RenderingEngine.RenderingException(te);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LOGGER.error("unexpected error " + e);
|
|
throw new RenderingEngine.RenderingException(e);
|
|
}
|
|
|
|
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 RenderingEngine.RenderingException(msg.toString());
|
|
}
|
|
}
|
|
}
|