package org.apache.sling.webresource.impl; import java.io.InputStream; import java.io.SequenceInputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.query.Query; import javax.jcr.query.QueryResult; import org.apache.commons.lang.time.StopWatch; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.webresource.WebResourceInventoryManager; import org.apache.sling.webresource.WebResourceScriptCache; import org.apache.sling.webresource.WebResourceScriptCompiler; import org.apache.sling.webresource.WebResourceScriptCompilerProvider; import org.apache.sling.webresource.exception.WebResourceCompileException; import org.apache.sling.webresource.exception.WebResourceCompilerNotFoundException; import org.apache.sling.webresource.model.GlobalCompileOptions; import org.apache.sling.webresource.model.WebResourceGroup; import org.apache.sling.webresource.postprocessors.PostCompileProcessProvider; import org.apache.sling.webresource.postprocessors.PostConsolidationProcessProvider; import org.apache.sling.webresource.util.JCRUtils; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * Implementation of the Web Resource Cache * * * @author bpaulin * */ @Component(label = "Web Resource Cache Service", immediate = true) @Service public class WebResourceScriptCacheImpl implements WebResourceScriptCache { @Reference private ResourceResolverFactory resourceResolverFactory; @Reference private WebResourceScriptCompilerProvider webResourceScriptCompilerProvider; @Reference private WebResourceInventoryManager webResourceInventoryManager; @Reference private PostCompileProcessProvider postCompileProcessProvider; @Reference private PostConsolidationProcessProvider postConsolidationProcessProvider; private final Logger log = LoggerFactory.getLogger(getClass()); private static final String WEB_RESOURCE_GROUP_CACHE_PATH = "/var/webresource/groups"; private Map<String, ReentrantLock> compileLockMap; public void activate(final ComponentContext context) { compileLockMap = new HashMap<String, ReentrantLock>(); } /** * * Compiles web resource source content node to a new node representing the * compiled source content * * @param sourceNode * @param webResourceGroupNode * @param compiler * @param compileOptions * @return * @throws WebResourceCompileException */ protected Node compileWebResourceToNode(Node sourceNode, WebResourceGroup webResourceGroup, WebResourceScriptCompiler compiler) throws WebResourceCompileException { Node result = null; try { Map<String, Object> compileOptions = new HashMap<String, Object>(); GlobalCompileOptions globalCompileOptions = new GlobalCompileOptions(); globalCompileOptions.setSourcePath(sourceNode.getPath()); compileOptions.put("global", globalCompileOptions); if (webResourceGroup != null) { compileOptions.putAll(webResourceGroup.getCompileOptions()); } InputStream compiledStream = compiler.compile( JCRUtils.getFileNodeAsStream(sourceNode), compileOptions); compiledStream = postCompileProcessProvider .applyPostCompileProcesses(sourceNode, compiledStream); String destinationPath = getCachedCompiledScriptPath(sourceNode, webResourceGroup, compiler); createWebResourceNode(destinationPath, compiledStream); Session currentSession = sourceNode.getSession(); result = currentSession.getNode(destinationPath); } catch (Exception e) { throw new WebResourceCompileException( "Error Compiling Web Resource", e); } return result; } /** * * Helper for creating compiled web resource content node * * @param destinationPath * @param result * @throws RepositoryException * @throws WebResourceCompileException */ protected void createWebResourceNode(String destinationPath, InputStream result) throws RepositoryException, WebResourceCompileException { ResourceResolver resolver = null; log.info("Creating Web Resource Node at path: " + destinationPath); try { resolver = resourceResolverFactory .getAdministrativeResourceResolver(null); Session session = resolver.adaptTo(Session.class); JCRUtils.createFileContentNode(destinationPath, result, session); session.save(); } catch (Exception e) { throw new WebResourceCompileException( "Error Creating Compiled Web Resource", e); } finally { if (resolver != null) { resolver.close(); } } } public Map<String, List<String>> getCompiledWebResourceGroupPaths( Session session, String webResourceGroupName, boolean consolidate) throws WebResourceCompileException { StopWatch stopWatch = new StopWatch(); stopWatch.start(); Map<String, List<String>> result = new HashMap<String, List<String>>(); WebResourceGroup webResourceGroup = null; try { List<String> webResourcePathList = webResourceInventoryManager .getSourceWebResources(webResourceGroupName); log.debug("Compiling: " + webResourcePathList); webResourceGroup = new WebResourceGroup( session.getNode(webResourceInventoryManager .getWebResourcePathLookup(webResourceGroupName))); for (String currentWebResourcePath : webResourcePathList) { Node currentResult = session.getNode(currentWebResourcePath); try { Node currentCompiledScript = getCompiledScriptNode(session, currentResult, webResourceGroup); String compiledScriptPath = currentCompiledScript.getPath(); String currentExtension = JCRUtils .getNodeExtension(currentCompiledScript); List<String> extentionPathList = result .get(currentExtension); if (extentionPathList == null) { extentionPathList = new ArrayList<String>(); result.put(currentExtension, extentionPathList); } extentionPathList.add(compiledScriptPath); } catch (WebResourceCompilerNotFoundException e) { log.info("Compiler Not Found for Node at Path: " + currentResult.getPath()); } } if (consolidate) { result = consolidateWebResources(session, webResourceGroup, result); } } catch (RepositoryException e) { throw new WebResourceCompileException( "Error consolidating Web Resource Group", e); } stopWatch.stop(); log.info("Compilation of Web Resource Group " + webResourceGroupName + " completed in: " + stopWatch); return result; } /** * * Consolidates several web resource files into one. * * @param session * @param webResourceGroup * @param compiledWebResourcePaths * @return * @throws RepositoryException * @throws WebResourceCompileException */ public Map<String, List<String>> consolidateWebResources(Session session, WebResourceGroup webResourceGroup, Map<String, List<String>> compiledWebResourcePaths) throws RepositoryException, WebResourceCompileException { Map<String, List<String>> resultPaths = new HashMap<String, List<String>>(); // Find out if there is a cached copy StringBuffer webResourceGroupPathBuffer = new StringBuffer(); if (webResourceGroup.getCachePath() != null) { webResourceGroupPathBuffer.append(webResourceGroup.getCachePath()); } else { webResourceGroupPathBuffer.append(WEB_RESOURCE_GROUP_CACHE_PATH); } webResourceGroupPathBuffer.append("/"); webResourceGroupPathBuffer.append(webResourceGroup.getName()); webResourceGroupPathBuffer.append("."); for (String currentExtention : compiledWebResourcePaths.keySet()) { String cachedWebResourcePath = webResourceGroupPathBuffer .toString() + currentExtention; aquireLock(cachedWebResourcePath); try { createConsolidatedSource(session, compiledWebResourcePaths, currentExtention, cachedWebResourcePath); } finally { releaseLock(cachedWebResourcePath); } List<String> consolidatedPathListForExtention = new ArrayList<String>(); consolidatedPathListForExtention.add(cachedWebResourcePath); resultPaths.put(currentExtention, consolidatedPathListForExtention); } return resultPaths; } protected void createConsolidatedSource(Session session, Map<String, List<String>> compiledWebResourcePaths, String currentExtention, String cachedWebResourcePath) throws RepositoryException, WebResourceCompileException { // Cached copy is out of date InputStream consolidatedInputStream = null; for (String currentResourcePath : compiledWebResourcePaths .get(currentExtention)) { Node currentCompiledNode = session.getNode(currentResourcePath); InputStream currentInputStream = JCRUtils .getFileNodeAsStream(currentCompiledNode); if (consolidatedInputStream == null) { consolidatedInputStream = currentInputStream; } else { consolidatedInputStream = new SequenceInputStream( consolidatedInputStream, currentInputStream); } } postConsolidationProcessProvider.applyPostConsolidationProcesses( cachedWebResourcePath, consolidatedInputStream); // Write createWebResourceNode(cachedWebResourcePath, consolidatedInputStream); } public Map<String, List<String>> getWebResourceCachedInventoryPaths( Session session, String webResourceGroupName) throws RepositoryException { Map<String, List<String>> result = new HashMap<String, List<String>>(); Query query = session .getWorkspace() .getQueryManager() .createQuery( "SELECT * FROM [webresource:WebResourceGroup] as webResourceGroupSet WHERE webResourceGroupSet.[webresource:name] = $webResourceName", Query.JCR_SQL2); query.bindValue("webResourceName", session.getValueFactory() .createValue(webResourceGroupName)); QueryResult queryResult = query.execute(); NodeIterator queryIt = queryResult.getNodes(); if (queryIt.hasNext()) { WebResourceGroup webResourceGroup = new WebResourceGroup( queryIt.nextNode()); result.putAll(webResourceGroup.getInventory()); } return result; } public String getCompiledScriptPath(Session session, String path) throws WebResourceCompileException, WebResourceCompilerNotFoundException { String result = null; try { Node sourceNode = session.getNode(path); Node compiledNode = getCompiledScriptNode(session, sourceNode, null); result = compiledNode.getPath(); } catch (RepositoryException e) { throw new WebResourceCompileException( "Web Resource Source not Found", e); } return result; } /** * * Obtains Node of a compiled web resource. If needed compiles the resource. * * @param session * @param sourceNode * @param webResourceGroup * @return * @throws WebResourceCompileException * @throws WebResourceCompilerNotFoundException */ public Node getCompiledScriptNode(Session session, Node sourceNode, WebResourceGroup webResourceGroup) throws WebResourceCompileException, WebResourceCompilerNotFoundException { Node result = null; WebResourceScriptCompiler compiler = webResourceScriptCompilerProvider .getWebResourceCompilerForNode(sourceNode); String cachedCompiledScriptPath = null; try { cachedCompiledScriptPath = getCachedCompiledScriptPath(sourceNode, webResourceGroup, compiler); aquireLock(cachedCompiledScriptPath); if (session.nodeExists(cachedCompiledScriptPath)) { Node compiledScriptNode = session .getNode(cachedCompiledScriptPath); if (isCacheFresh(compiledScriptNode, sourceNode)) { result = compiledScriptNode; } } // Script is either not compiled or out of date. if (result == null) { result = compileWebResourceToNode(sourceNode, webResourceGroup, compiler); } } catch (Exception e) { throw new WebResourceCompileException(e); } finally { releaseLock(cachedCompiledScriptPath); } return result; } /** * * Create locks on cached scripts to prevent overcompiling. * * @param cachedCompiledScriptPath * @return */ protected ReentrantLock aquireLock(String cachedCompiledScriptPath) { ReentrantLock pathLock; synchronized (this) { pathLock = compileLockMap.get(cachedCompiledScriptPath); if (pathLock == null) { log.debug("Created lock for Path: " + cachedCompiledScriptPath); pathLock = new ReentrantLock(); compileLockMap.put(cachedCompiledScriptPath, pathLock); pathLock.lock(); } } if (!pathLock.isHeldByCurrentThread()) { pathLock.lock(); pathLock = aquireLock(cachedCompiledScriptPath); } return pathLock; } /** * * Release lock on compiled scripts. * * @param cachedCompiledScriptPath */ protected void releaseLock(String cachedCompiledScriptPath) { synchronized (this) { ReentrantLock pathLock = compileLockMap .get(cachedCompiledScriptPath); if (pathLock != null) { log.debug("Releasing lock for Path: " + cachedCompiledScriptPath); pathLock.unlock(); // Cleans out the compile lock map to prevent memory leak // Queued threads method is an estimate. However it should be an // overestimate // since it may return true with cancelled threads if (!pathLock.hasQueuedThreads()) { compileLockMap.remove(cachedCompiledScriptPath); } } } } /** * * Determines if the cache is fresher than the source node. * * @param cacheScriptNode * @param sourceNode * @return * @throws RepositoryException */ protected boolean isCacheFresh(Node cacheScriptNode, Node sourceNode) throws RepositoryException { boolean cacheFresh = false; Node cacheScriptContent = cacheScriptNode.getNode(Property.JCR_CONTENT); Node webResourceScriptContent = sourceNode .getNode(Property.JCR_CONTENT); Property cacheScriptLastModified = cacheScriptContent .getProperty(Property.JCR_LAST_MODIFIED); Property webResouceScriptLastModified = webResourceScriptContent .getProperty(Property.JCR_LAST_MODIFIED); if (!cacheScriptLastModified.getDate().before( webResouceScriptLastModified.getDate())) { cacheFresh = true; } return cacheFresh; } protected String getCachedCompiledScriptPath(Node sourceNode, WebResourceGroup webResourceGroup, WebResourceScriptCompiler compiler) throws RepositoryException { String cachedCompiledScriptPath = null; // If the web resource has a custom cache path then override compiler // default. if (webResourceGroup != null && webResourceGroup.getCachePath() != null) { String relativePath = JCRUtils.convertPathToRelative( webResourceGroup.getGroupPath(), JCRUtils.convertNodeExtensionPath(sourceNode, compiler.compiledScriptExtension())); cachedCompiledScriptPath = webResourceGroup.getCachePath() + relativePath; } else if (webResourceGroup != null) { String relativePath = JCRUtils.convertPathToRelative( webResourceGroup.getGroupPath(), JCRUtils.convertNodeExtensionPath(sourceNode, compiler.compiledScriptExtension())); cachedCompiledScriptPath = WEB_RESOURCE_GROUP_CACHE_PATH + relativePath; } else { String relativePath = JCRUtils.convertNodeExtensionPath(sourceNode, compiler.compiledScriptExtension()); cachedCompiledScriptPath = compiler.getCacheRoot() + relativePath; } return cachedCompiledScriptPath; } protected WebResourceScriptCompiler getCompilerForPath(Session session, String path) throws WebResourceCompilerNotFoundException, WebResourceCompileException { WebResourceScriptCompiler compiler = null; try { Node sourceNode = session.getNode(path); compiler = webResourceScriptCompilerProvider .getWebResourceCompilerForNode(sourceNode); } catch (RepositoryException e) { throw new WebResourceCompileException(e); } return compiler; } @Override public InputStream getGlobalWebResourceScripts() throws RepositoryException, LoginException { InputStream result = getClass().getClassLoader().getResourceAsStream("META-INF/webresource-overrides.js"); return result; } }