package er.extensions.components; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; import java.net.URL; import java.util.HashMap; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Templates; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WOResourceManager; import com.webobjects.appserver.WOResponse; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSForwardException; import er.extensions.appserver.ERXResponse; import er.extensions.foundation.ERXFileUtilities; import er.extensions.foundation.ERXSimpleTemplateParser; /** * Wrapper that translates its content via XSLT. The content must be valid XML for this to work. * This is pretty usefull in conjunction with DynaReporter when you want to use one of the * zillion PDF libs. You can generate the content via DynaReporter and then transform the content * to a form that the PDF lib understands. Most likely this will be much easier than trying to re-generate * the report with XML. * <p> * Other uses include a simple transformation of the generated front end code to privide for "skinning". * As there is only so much you can do with CSS, you might need to structurally change the generated HTML prior * to handing it to the client. * <p> * Note that XSLT engines vary <em>greatly</em> in speed. The default case of using Xalan which is included by WO * is probably not the best choice for a site with a little bit of traffic. * Therefore there is an option where you can set the transformer factory name to use, you also need to include the * corresponding jar into the classpath. * * @binding enabled flag that decides if the transformation is applied. If not set, then only the content will be shown. * @binding stylesheet name of the XLST stylesheet (mandatory) * @binding transformerFactory name of the class for the XSLT transformer factory (optional, defaults to Xalan) * @binding framework name of the XLST stylesheet's framework (optional) * @binding data will be set to the transformed data (optional) * @binding stream will be set to the transformed data (optional) * @binding nocache flag that if set creates a new transformer instead of using the one in the cache. Useful when deleloping the stylesheet. * * @author ak on 07.04.05 */ public class ERXSLTWrapper extends ERXNonSynchronizingComponent { /** * Do I need to update serialVersionUID? * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a> */ private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(ERXSLTWrapper.class); private long start, current; /** * Public constructor * @param context the context */ public ERXSLTWrapper(WOContext context) { super(context); } private boolean isEnabled() { return booleanValueForBinding("enabled", true); } private static Map cache = new HashMap(); private Transformer transformer() { Transformer transformer; try { synchronized (cache) { String stylesheet = (String)valueForBinding("stylesheet"); String framework = (String)valueForBinding("framework"); NSArray languages = session().languages(); String key = stylesheet + "-" + framework; transformer = (Transformer) cache.get(key); if(transformer == null || booleanValueForBinding("nocache")) { byte bytes[] = application().resourceManager().bytesForResourceNamed(stylesheet, framework, languages); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setValidating(false); documentBuilderFactory.setNamespaceAware(true); DocumentBuilder documentBuilder; documentBuilder = documentBuilderFactory.newDocumentBuilder(); ByteArrayInputStream bis = new ByteArrayInputStream(bytes); Document document = documentBuilder.parse(bis); Source xslt = new DOMSource(document); xslt.setSystemId(key); String transformerFactoryName = (String)valueForBinding("transformerFactory"); String oldTransformerFactoryName = System.getProperty("javax.xml.transform.TransformerFactory"); if(transformerFactoryName != null) { System.setProperty("javax.xml.transform.TransformerFactory", transformerFactoryName); } else { System.setProperty("javax.xml.transform.TransformerFactory", "org.apache.xalan.processor.TransformerFactoryImpl"); } TransformerFactory transformerFactory = TransformerFactory.newInstance(); if(oldTransformerFactoryName != null) { System.setProperty("javax.xml.transform.TransformerFactory", oldTransformerFactoryName); } transformer = transformerFactory.newTransformer(xslt); // transformer.setOutputProperty("indent", "no"); // transformer.setOutputProperty("method", "xml"); cache.put(key, transformer); } } return transformer; } catch(Exception ex) { throw NSForwardException._runtimeExceptionForThrowable(ex); } } private static XMLReader xmlReader; static { try { xmlReader = XMLReaderFactory.createXMLReader(); xmlReader.setFeature("http://xml.org/sax/features/validation", false); if(false) { // FIXME AK: we need real handling for the normal case (HTML->FOP XML) EntityResolver resolver = new EntityResolver() { public InputSource resolveEntity(String arg0, String arg1) throws SAXException, IOException { log.info("{}::{}", arg0, arg1); InputSource source = new InputSource((new URL("file:///Volumes/Home/Desktop/dtd/xhtml1-transitional.dtd")).openStream()); source.setSystemId(arg1); return source; } }; xmlReader.setEntityResolver(resolver); } } catch (SAXException e) { e.printStackTrace(); } } /** * Overridden to get use apply the XLST transformation on the content. * @throws TransformerException */ @Override public void appendToResponse(WOResponse response, WOContext context) { start = System.currentTimeMillis(); current = start; if (isEnabled()) { ERXResponse newResponse = new ERXResponse(); newResponse.setContentEncoding(response.contentEncoding()); super.appendToResponse(newResponse, context); if (log.isDebugEnabled()) { String contentString = newResponse.contentString(); log.debug("Converting content string:\n{}", contentString); } try { NSData data = transform(transformer(), newResponse.content()); if(hasBinding("data") && canSetValueForBinding("data")) { setValueForBinding(data, "data"); } if(hasBinding("stream") && canSetValueForBinding("stream")) { setValueForBinding(data.stream(), "stream"); } response.appendContentData(data); } catch (TransformerException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } else { super.appendToResponse(response, context); } log.debug("Total: {}", System.currentTimeMillis() - start); start = System.currentTimeMillis(); } private static TemplatePool pool = new TemplatePool(); public static Transformer getTransformer(String framework, String filename) { return pool.getTransformer(framework, filename); } public static String transform(Transformer transformer, String xml) throws TransformerException { StringReader stringreader = new StringReader(xml); InputSource inputsource = new InputSource(stringreader); SAXSource s = new SAXSource(inputsource); StringWriter writer = new StringWriter(); StreamResult r = new StreamResult(writer); transformer.transform(s, r); String result = writer.toString(); return result; } public static NSData transform(Transformer transformer, NSData data) throws TransformerException { ByteArrayInputStream bis = new ByteArrayInputStream(data.bytes()); SAXSource saxSource = new SAXSource(); saxSource.setXMLReader(xmlReader); saxSource.setInputSource(new InputSource(bis)); ByteArrayOutputStream os = new ByteArrayOutputStream(); Result r = new StreamResult(os); transformer.transform(saxSource, r); NSData result = new NSData(os.toByteArray()); return result; } public static class TemplatePool { //private static final WeakHashMap templates = new WeakHashMap(); private final Map templates = new HashMap(); private static final Logger log = LoggerFactory.getLogger(TemplatePool.class); private ERXSimpleTemplateParser templateParser = new ERXSimpleTemplateParser(); protected TemplatePool() {} public Map getTemplates() { return templates; } public synchronized Transformer getTransformer(String framework, String filename) { if (filename == null || filename.length() == 0) { throw new IllegalArgumentException("filename cannot be null or empty"); } String key = framework + "-" +filename; Templates t = (Templates) pool.getTemplates().get(key); String s = null; if (t == null) { try { WOApplication app = WOApplication.application(); WOResourceManager rm = app.resourceManager(); TransformerFactory fac = TransformerFactory.newInstance(); log.debug("creating template for file {} in framework {}", filename, framework); InputStream is = rm.inputStreamForResourceNamed(filename, framework, null); if (is == null) { log.debug("trying with framework = null"); is = rm.inputStreamForResourceNamed(filename, null, null); if (is == null) { throw new IllegalArgumentException("inputStream is null"); } } if (is.available() == 0) { throw new IllegalArgumentException("InputStream has 0 bytes available, cannot read xsl file!"); } s = ERXFileUtilities.stringFromInputStream(is); s = templateParser.parseTemplateWithObject(s, "@@", app); t = fac.newTemplates(new StreamSource(new ByteArrayInputStream(s.getBytes()))); if (app.isCachingEnabled()) { templates.put(key, t); } } catch (IOException e1) { throw NSForwardException._runtimeExceptionForThrowable(e1); } catch (TransformerConfigurationException tce) { log.error("could not create template {}", tce.getLocationAsString(), tce); log.error(" cause", tce.getCause()); if (tce.getCause() != null && tce.getCause() instanceof org.xml.sax.SAXParseException) { org.xml.sax.SAXParseException e = (org.xml.sax.SAXParseException) tce.getCause(); log.error("SAXParseException: line {}, column {}", e.getLineNumber(), e.getColumnNumber()); } log.error("this is the incorrect xsl:>>>{}<<<", s); return null; } } try { return t.newTransformer(); } catch (TransformerConfigurationException tce) { log.error("could not create template {}", tce.getLocationAsString(), tce); log.error(" cause", tce.getCause()); return null; } } } }