/** * Copyright 2011 meltmedia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.xchain.framework.javascript; import static org.xchain.framework.util.IoUtil.*; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xchain.framework.lifecycle.ContainerLifecycle; import org.xchain.framework.lifecycle.Execution; /** * Decorator <code>IMergeStrategy</code> which caches the result of performing a merge based on the current catalog. * This assumes that for all calls made to <code>merge(...)</code> will have the same result for whatever catalog is the * current catalog. * * @author John Trimble * @author Josh Kennedy */ public class CacheMergeStrategy implements IMergeStrategy { private static Logger log = LoggerFactory.getLogger(CacheMergeStrategy.class); private static final int BUFFER_SIZE = 2048; private static final String TEMP_DIR_ATTRIBUTE_NAME = "javax.servlet.context.tempdir"; // This is used to assist in making a prefix for temporary merged js files. private static final Pattern URL_FILE_NAME_PATTERN = Pattern.compile("\\A.*[/]([a-zA-Z0-9.-]+)([?].*)?\\Z"); // Mapping of catalog system-ids to compressed java script files. private static final Map<String, URL> catalogCacheUrlMap = Collections.synchronizedMap(new HashMap<String, URL>()); // Lock to ensure only on compressed js file created at a time. private static final Object MERGE_LOCK = new Object(); private IMergeStrategy childMergeStrategy; public CacheMergeStrategy(IMergeStrategy strategy) { this.childMergeStrategy = strategy; } /** * If this is the first time this method has been called for the current catalog, then, in a globally * synchronized block, the merge is produced by delegating to the child <code>IMergeStrategy</code> instance. * Otherwise, the cached result is written to the output stream. */ public void merge(String manifestSystemId, OutputStream output) throws Exception { String systemId = Execution.getSystemId(); URL compressedUrl = createOrGetMergeUrl(systemId, manifestSystemId); InputStream in = null; try { in = compressedUrl.openConnection().getInputStream(); copyStream(in, output, BUFFER_SIZE); } finally { close(in, log); } } private URL createOrGetMergeUrl(String systemId, String manifestSystemId) throws Exception { URL compressedUrl = catalogCacheUrlMap.get(systemId); if( compressedUrl == null ) { synchronized(MERGE_LOCK) { // Check to see if some other Thread did the work for us while we were waiting. compressedUrl = catalogCacheUrlMap.get(systemId); if( compressedUrl == null ) { // Okay... lets compress these java script files. File tempFile = createMergeCacheFile(systemId); OutputStream out = null; // compress try { out = new FileOutputStream(tempFile); childMergeStrategy.merge(manifestSystemId, out); compressedUrl = tempFile.toURL(); } catch(IOException e){ // Delete the temporary file if there is an exception... otherwise, we will end up creating a temporary file // each time this apparently broken execution path runs. forceDeleteTempFile(tempFile); throw e; } catch( RuntimeException e) { // Delete the temporary file if there is an exception... otherwise, we will end up creating a temporary file // each time this apparently broken execution path runs. forceDeleteTempFile(tempFile); throw e; } finally { close(out, log); } catalogCacheUrlMap.put(systemId, compressedUrl); } } // end synchronized block } return compressedUrl; } private File createMergeCacheFile(String systemId) throws IOException { File tempDir = null; // We get the temporary directory from a servlet context attribute. If its a string, we create a File object for // that path. If its a File, then we are set. If we get null or the empty string, the we throw an IOException. Object tempDirAttrValueObject = ContainerLifecycle.getServletContext().getAttribute(TEMP_DIR_ATTRIBUTE_NAME); if( tempDirAttrValueObject instanceof String ) { String tempDirAttrValue = (String) tempDirAttrValueObject; if( tempDirAttrValue == null || "".equals(tempDirAttrValue) ) throw new IOException("Servlet context attribte '"+TEMP_DIR_ATTRIBUTE_NAME+"' was not set."); tempDir = new File(tempDirAttrValue); } else if( tempDirAttrValueObject instanceof File ) tempDir = (File) tempDirAttrValueObject; if( tempDir == null ) { throw new IOException("Could not find temporary directory."); } // For the prefix, try to incorporate the name of the catalog if possible. String prefix = "compressed-"; Matcher m = URL_FILE_NAME_PATTERN.matcher(systemId); if( m.matches() ) { String filename = m.group(1); if( filename != null ) { filename = filename.replace('.', '-'); prefix += filename + "-"; } } File tempFile = File.createTempFile(prefix, null, tempDir); return tempFile; } private static boolean forceDeleteTempFile(File tempFile) { boolean result = false; if( tempFile != null ) { try { if( log.isDebugEnabled() ) log.debug("Forcefully removing temporary file '"+tempFile.getAbsolutePath()+"'."); result = tempFile.delete(); } catch( Exception e ) { if( log.isWarnEnabled() ) log.warn("Exception occurred while attempting to delete temporary file '"+tempFile.getAbsolutePath()+"'.", e); } if( log.isDebugEnabled() ) log.debug("Result of deleting temporary file '"+tempFile.getAbsolutePath()+"' was "+result+"."); } return result; } }