/******************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2001, ThoughtWorks, Inc. * 200 E. Randolph, 25th Floor * Chicago, IL 60601 USA * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * + Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * + Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ********************************************************************************/ package net.sourceforge.cruisecontrol.taglib; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; import java.net.URL; import java.net.URLConnection; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspTagException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import net.sourceforge.cruisecontrol.LogFile; import net.sourceforge.cruisecontrol.util.CCTagException; /** * JSP custom tag to handle xsl transforms. This tag also caches the output of the transform to disk, reducing the * number of transforms necessary. * * @author alden almagro, ThoughtWorks, Inc. 2002 * @author <a href="mailto:hak@2mba.dk">Hack Kampbjorn</a> */ public class XSLTag extends CruiseControlTagSupport { private static final long serialVersionUID = -948954553781627362L; private static final String XSLT_PARAMETER_PREFIX = "xslt."; private static final String SAXON_VERSION_WARNING = "http://saxon.sf.net/feature/version-warning"; private String xslFileName; private static final String CACHE_DIR = "_cache"; public void release() { xslFileName = null; } /** * Perform an xsl transform. This body of this method is based upon the xalan sample code. * * @param xmlFile the xml file to be transformed * @param style resource containing the xsl stylesheet * @param out stream to output the results of the transformation * @throws JspTagException if any error occurs */ protected void transform(final LogFile xmlFile, final URL style, final OutputStream out) throws JspTagException { try { final Transformer transformer = newTransformer(style); final InputStream in; try { in = xmlFile.getInputStream(); } catch (IOException ioex) { err(ioex); throw new CCTagException("Cannot read logfile: " + ioex.getMessage(), ioex); } try { transformer.transform(new StreamSource(in), new StreamResult(out)); } finally { closeQuietly(in); } } catch (ArrayIndexOutOfBoundsException e) { String message = "Error transforming '" + xmlFile.getName() + "'. You might be experiencing XML parser issues." + " Are your xalan & xerces jar files mismatched? Check your JVM version. " + e.getMessage(); err(message, e); throw new CCTagException(message, e); } catch (TransformerException e) { err(e); throw new CCTagException("Error transforming '" + xmlFile.getName() + "': " + e.getMessage(), e); } } private Transformer newTransformer(final URL style) throws TransformerException { final TransformerFactory tFactory = TransformerFactory.newInstance(); try { tFactory.setAttribute(SAXON_VERSION_WARNING, Boolean.FALSE); } catch (IllegalArgumentException iaex) { debug("could not silence Saxon XSLT 2.0 warning, processor is probably not saxon: " + iaex.getMessage()); } final Transformer transformer = tFactory.newTransformer(new StreamSource(style.toExternalForm())); final Map<String, String> parameters = getXSLTParameters(); if (!parameters.isEmpty()) { transformer.clearParameters(); for (final Map.Entry<String, String> entry : parameters.entrySet()) { transformer.setParameter(entry.getKey(), entry.getValue()); } } return transformer; } /** * Determine whether the cache file is current or not. The file will be current if it is newer than both the * xml log file and the xsl file used to create it. * * @param xmlFile xml log file * @param cacheFile cached file * @return true if the cache file is current. */ protected boolean isCacheFileCurrent(final File xmlFile, final File cacheFile) { if (!cacheFile.exists() || cacheFile.length() == 0) { return false; } final long xmlLastModified = xmlFile.lastModified(); final long cacheLastModified = cacheFile.lastModified(); try { final URL xslUrl = getPageContext().getServletContext().getResource(xslFileName); final URLConnection con = xslUrl.openConnection(); final long xslLastModified = con.getLastModified(); return (cacheLastModified > xmlLastModified) && (cacheLastModified > xslLastModified); } catch (Exception e) { err("Failed to retrieve lastModified of xsl file " + xslFileName); return false; } } /** * Serves the cached copy rather than re-performing the xsl transform for every request. * * @param cacheFile The filename of the cached copy of the transform. * @param out The writer to write to * @throws JspTagException if an error occurs. */ protected void serveCachedCopy(final File cacheFile, final Writer out) throws JspTagException { try { final InputStream input = new FileInputStream(cacheFile); copy(input, out); } catch (IOException e) { err(e); throw new CCTagException("Error reading file '" + cacheFile.getName() + "': " + e.getMessage(), e); } } private void copy(final InputStream input, final Writer out) throws IOException { final BufferedReader in = new BufferedReader(new InputStreamReader(input, "UTF-8")); try { final char[] cbuf = new char[8192]; while (true) { final int charsRead = in.read(cbuf); if (charsRead == -1) { break; } out.write(cbuf, 0, charsRead); } } finally { closeQuietly(in); } } /** * Create a filename for the cached copy of this transform. This filename will be the concatenation of the * log file and the xsl file used to create it. * * @param xmlFile The log file used as input to the transform * @return The filename for the cached file */ protected String getCachedCopyFileName(final File xmlFile) { final String xmlFileName = xmlFile.getName().substring(0, xmlFile.getName().lastIndexOf(".")); // The use of '/' is correct, xslFileName is a resource URL so it will // always start with a slash and only always use normal slashes final int slashIndex = xslFileName.lastIndexOf("/"); final String styleSheetName = xslFileName.substring(slashIndex + 1, xslFileName.lastIndexOf(".")); return xmlFileName + "-" + styleSheetName + ".html"; } // we know ServletConfig.getInitParameterNames() and ServletContext.getInitParameterNames() // both return Enumeration<String> @SuppressWarnings("unchecked") Map<String, String> getXSLTParameters() { final Map<String, String> xsltParameters = new HashMap<String, String>(); final ServletConfig config = pageContext.getServletConfig(); final Enumeration<String> configNames = config.getInitParameterNames(); while (configNames.hasMoreElements()) { final String parameterName = configNames.nextElement(); if (parameterName.startsWith(XSLT_PARAMETER_PREFIX)) { final String value = config.getInitParameter(parameterName); final String name = parameterName.substring(XSLT_PARAMETER_PREFIX.length()); info("using XSLT parameter: " + name + "=" + value); xsltParameters.put(name, value); } } final ServletContext context = config.getServletContext(); final Enumeration<String> contextNames = context.getInitParameterNames(); while (contextNames.hasMoreElements()) { final String parameterName = contextNames.nextElement(); if (parameterName.startsWith(XSLT_PARAMETER_PREFIX)) { final String value = context.getInitParameter(parameterName); final String name = parameterName.substring(XSLT_PARAMETER_PREFIX.length()); info("using XSLT parameter: " + name + "=" + value); xsltParameters.put(name, value); } } return xsltParameters; } /** * Sets the xsl file to use. It is expected that this can be found by the <code>ServletContext</code> for this * web application. * * @param xslFile The path to the xslFile. */ public void setXslFile(final String xslFile) { xslFileName = xslFile; } /** * Prepare the content if there's need to. * The content must be prepared if a transformation is required. * * @return the file to serve * @throws JspException if an error occurs */ File prepareContent() throws JspException { final LogFile xmlFile = findLogFile(); final File cacheFile = findCacheFile(xmlFile); if (!isCacheFileCurrent(xmlFile.getFile(), cacheFile)) { info("Updating cached copy: " + cacheFile.getAbsolutePath()); updateCacheFile(xmlFile, cacheFile); } else { info("Using cached copy: " + cacheFile.getAbsolutePath()); } return cacheFile; } protected void updateCacheFile(final LogFile xmlFile, final File cacheFile) throws JspTagException { final OutputStream out; try { out = new FileOutputStream(cacheFile); } catch (FileNotFoundException e) { err(e); throw new CCTagException("Error saving a cached transformation '" + cacheFile.getName() + "': " + e.getMessage(), e); } try { final URL style = getPageContext().getServletContext().getResource(xslFileName); transform(xmlFile, style, out); } catch (IOException e) { err(e); throw new CCTagException("Error saving a cached transformation '" + cacheFile.getName() + "': " + e.getMessage(), e); } finally { closeQuietly(out); } } private File findCacheFile(final LogFile xmlFile) { final String cacheRoot = getContextParam("cacheRoot"); final File cacheDir = cacheRoot == null ? new File(xmlFile.getLogDirectory(), CACHE_DIR) : new File(cacheRoot + File.separator + getProject()); if (!cacheDir.exists()) { cacheDir.mkdir(); } return new File(cacheDir, getCachedCopyFileName(xmlFile.getFile())); } public int doEndTag() throws JspException { final File cachedFile = prepareContent(); serveCachedCopy(cachedFile, getPageContext().getOut()); return EVAL_PAGE; } private void closeQuietly(final InputStream in) { if (in != null) { try { in.close(); } catch (IOException ioex) { info("Ignored " + ioex.getMessage() + " while closing stream"); } } } private void closeQuietly(final Reader in) { if (in != null) { try { in.close(); } catch (IOException ioex) { info("Ignored " + ioex.getMessage() + " while closing reader"); } } } private void closeQuietly(final OutputStream out) { if (out != null) { try { out.close(); } catch (IOException ioex) { info("Ignored " + ioex.getMessage() + " while closing stream"); } } } }