/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE and NOTICE files at the root of the source * tree and available online at * * http://www.dspace.org/license/ */ package org.dspace.app.xmlui.cocoon; import com.yahoo.platform.yui.compressor.CssCompressor; import com.yahoo.platform.yui.compressor.JavaScriptCompressor; import org.apache.avalon.framework.parameters.ParameterException; import org.apache.avalon.framework.parameters.Parameters; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.environment.*; import org.apache.cocoon.reading.ResourceReader; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.source.impl.validity.TimeStampValidity; import org.apache.log4j.Logger; import org.dspace.core.ConfigurationManager; import org.mozilla.javascript.EvaluatorException; import org.xml.sax.SAXException; import java.io.*; import java.util.*; /** * Concatenates and Minifies CSS, JS and JSON files * * The URL of the resource can contain references to multiple * files: e.g."themes/Mirage/lib/css/reset,base,helper,style,print.css" * The Reader will concatenate all these files, and output them as * a single resource. * * If "xmlui.theme.enableMinification" is set to true, the * output will also be minified prior to returning the resource. * * Validity is determined based upon last modified date of * the most recently edited file. * * @author Roel Van Reeth (roel at atmire dot com) * @author Art Lowel (art dot lowel at atmire dot com) * @author Ben Bosman (ben at atmire dot com) */ public class ConcatenationReader extends ResourceReader { private static final int MINIFY_LINEBREAKPOS = 8000; protected List<Source> inputSources; private String key; private StreamEnumeration streamEnumeration; private static Logger log = Logger.getLogger(ConcatenationReader.class); private boolean doMinify = true; /** * Setup the reader. * The resource is opened to get an <code>InputStream</code>, * the length and the last modification date */ public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) throws ProcessingException, SAXException, IOException { // save key this.key = src; // don't support byte ranges (resumed downloads) this.setByteRanges(false); // setup list of sources, get relevant parts of path this.inputSources = new ArrayList<Source>(); String path = src.substring(0, src.lastIndexOf('/')); String file = src.substring(src.lastIndexOf('/')+1); // now build own list of inputsources String[] files = file.split(","); for (String f : files) { if (file.endsWith(".json") && !f.endsWith(".json")) { f += ".json"; } if (file.endsWith(".js") && !f.endsWith(".js")) { f += ".js"; } if (file.endsWith(".css") && !f.endsWith(".css")) { f += ".css"; } String fullPath = path + "/" + f; this.inputSources.add(resolver.resolveURI(fullPath)); } // do super stuff super.setup(resolver, objectModel, path+"/"+files[files.length-1], par); // add stream enumerator this.streamEnumeration = new StreamEnumeration(); // check minify parameter try { if("nominify".equals(par.getParameter("requestQueryString"))) { this.doMinify = false; } else { // modify key! this.key += "?minify"; } } catch (ParameterException e) { log.error("ParameterException in setup when retrieving parameter requestQueryString", e); } } /** * Recyclable */ public void recycle() { if (this.inputSources != null) { for(Source s : this.inputSources) { super.resolver.release(s); } this.inputSources = null; this.streamEnumeration = null; this.key = null; } super.recycle(); } /** * Generate the unique key. * This key must be unique inside the space of this component. * * @return The generated key hashes the src */ public Serializable getKey() { return key; } /** * Generate the validity object. * * @return The generated validity object or <code>null</code> if the * component is currently not cacheable. */ public SourceValidity getValidity() { final long lm = getLastModified(); if(lm > 0) { return new TimeStampValidity(lm); } return null; } /** * @return the time the read source was last modified or 0 if it is not * possible to detect */ public long getLastModified() { // get latest modified value long modified = 0; for(Source s : this.inputSources) { if(s.getLastModified() > modified) { modified = s.getLastModified(); } } return modified; } /** * Generates the requested resource. */ public void generate() throws IOException, ProcessingException { InputStream inputStream; // create one single inputstream from all files inputStream = new SequenceInputStream(streamEnumeration); try { if (ConfigurationManager.getBooleanProperty("xmlui.theme.enableMinification",false) && this.doMinify) { compressedOutput(inputStream); } else { normalOutput(inputStream); } // Bugzilla Bug #25069: Close inputStream in finally block. } finally { if (inputStream != null) { inputStream.close(); } } out.flush(); } private void compressedOutput(InputStream inputStream) throws IOException { // prepare streams ByteArrayOutputStream bytes = new ByteArrayOutputStream(); Writer outWriter = new OutputStreamWriter(bytes); // do compression Reader in = new BufferedReader(new InputStreamReader(inputStream)); if (this.key.endsWith(".js?minify") || this.key.endsWith(".json?minify")) { try { JavaScriptCompressor compressor = new JavaScriptCompressor(in, null); // boolean options: munge, verbose, preserveAllSemiColons, disableOptimizations compressor.compress(outWriter, MINIFY_LINEBREAKPOS, true, false, false, false); } catch (EvaluatorException e) { // fail gracefully on malformed javascript: send it without compressing normalOutput(inputStream); return; } } else if (this.key.endsWith(".css?minify")) { CssCompressor compressor = new CssCompressor(in); compressor.compress(outWriter, MINIFY_LINEBREAKPOS); } else { // or not if not right type normalOutput(inputStream); return; } // first send content-length header outWriter.flush(); response.setHeader("Content-Length", Long.toString(bytes.size())); // then send output and clean up bytes.writeTo(out); in.close(); } private void normalOutput(InputStream inputStream) throws IOException { boolean validContentLength = true; byte[] buffer = new byte[bufferSize]; int length; long contentLength = 0; // calculate content length for (Source s : this.inputSources) { if(s.getContentLength() < 0) { validContentLength = false; } contentLength += s.getContentLength(); } if(validContentLength) { response.setHeader("Content-Length", Long.toString(contentLength)); } // send contents while ((length = inputStream.read(buffer)) > -1) { out.write(buffer, 0, length); } } private final class StreamEnumeration implements Enumeration { private int index; private StreamEnumeration() { this.index = 0; } public boolean hasMoreElements() { return index < inputSources.size(); } public InputStream nextElement() { try { InputStream elem = inputSources.get(index).getInputStream(); index++; return elem; } catch (IOException e) { log.error("IOException in StreamEnumeration.nextElement when retrieving InputStream of a Source; index = " + index + ", inputSources.size = " + inputSources.size(), e); return null; } } } }