/*
* #%L
* BroadleafCommerce Common Libraries
* %%
* Copyright (C) 2009 - 2013 Broadleaf Commerce
* %%
* 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.
* #L%
*/
package org.broadleafcommerce.common.resource.service;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadleafcommerce.common.cache.StatisticsService;
import org.broadleafcommerce.common.file.domain.FileWorkArea;
import org.broadleafcommerce.common.file.service.BroadleafFileService;
import org.broadleafcommerce.common.resource.GeneratedResource;
import org.broadleafcommerce.common.web.BroadleafRequestContext;
import org.broadleafcommerce.common.web.resource.BroadleafDefaultResourceResolverChain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.servlet.resource.ResourceResolverChain;
import de.jkeylockmanager.manager.KeyLockManager;
import de.jkeylockmanager.manager.KeyLockManagers;
import de.jkeylockmanager.manager.LockCallback;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
/**
* @see ResourceBundlingService
* @author Andre Azzolini (apazzolini)
* @author Brian Polster (bpolster)
*/
@Service("blResourceBundlingService")
public class ResourceBundlingServiceImpl implements ResourceBundlingService {
protected static final Log LOG = LogFactory.getLog(ResourceBundlingServiceImpl.class);
// Map of known unversioned bundle names ==> additional files that should be included
// Configured via XML
// ex: "global.js" ==> ["classpath:/file1.js", "/js/file2.js"]
protected Map<String, List<String>> additionalBundleFiles = new HashMap<String, List<String>>();
@javax.annotation.Resource(name = "blFileService")
protected BroadleafFileService fileService;
@Autowired(required = false)
@Qualifier("blJsResources")
protected ResourceHttpRequestHandler jsResourceHandler;
@Autowired(required = false)
@Qualifier("blCssResources")
protected ResourceHttpRequestHandler cssResourceHandler;
@javax.annotation.Resource(name="blStatisticsService")
protected StatisticsService statisticsService;
private KeyLockManager keyLockManager = KeyLockManagers.newLock();
private ConcurrentHashMap<String, Resource> createdBundles = new ConcurrentHashMap<String, Resource>();
@Override
public String resolveBundleResourceName(String requestedBundleName, String mappingPrefix, List<String> files) {
ResourceHttpRequestHandler resourceRequestHandler = findResourceHttpRequestHandler(requestedBundleName);
if (resourceRequestHandler != null && CollectionUtils.isNotEmpty(files)) {
ResourceResolverChain resolverChain = new BroadleafDefaultResourceResolverChain(
resourceRequestHandler.getResourceResolvers());
List<Resource> locations = resourceRequestHandler.getLocations();
StringBuilder combinedPathString = new StringBuilder();
List<String> filePaths = new ArrayList<String>();
for (String file : files) {
String resourcePath = resolverChain.resolveUrlPath(file, locations);
if (resourcePath == null) {
// can't find the exact name specified in the bundle, try it with the mappingPrefix
resourcePath = resolverChain.resolveUrlPath(mappingPrefix + file, locations);
}
if (resourcePath != null) {
filePaths.add(resourcePath);
combinedPathString.append(resourcePath);
} else {
LOG.warn(new StringBuilder().append("Could not resolve resource path specified in bundle as [")
.append(file)
.append("] or as [")
.append(mappingPrefix + file)
.append("]. Skipping file.")
.toString());
}
}
int version = Math.abs(combinedPathString.toString().hashCode());
String versionedBundleName = mappingPrefix + addVersion(requestedBundleName, "-" + String.valueOf(version));
createBundleIfNeeded(versionedBundleName, filePaths, resolverChain, locations);
return versionedBundleName;
} else {
if (LOG.isWarnEnabled()) {
LOG.warn("");
}
return null;
}
}
@Override
public Resource resolveBundleResource(String versionedBundleResourceName) {
return createdBundles.get(lookupBundlePath(versionedBundleResourceName));
}
@Override
public boolean checkForRegisteredBundleFile(String versionedBundleName) {
versionedBundleName = lookupBundlePath(versionedBundleName);
boolean bundleRegistered = createdBundles.containsKey(versionedBundleName);
if (LOG.isTraceEnabled()) {
LOG.trace("Checking for registered bundle file, versionedBundleName=\"" + versionedBundleName + "\" bundleRegistered=\"" + bundleRegistered + "\"");
}
return bundleRegistered;
}
protected String lookupBundlePath(String requestPath) {
if (requestPath.contains(".css")) {
if (!requestPath.startsWith("/css/")) {
requestPath = "/css/" + requestPath;
}
} else if (requestPath.contains(".js")) {
if (!requestPath.startsWith("/js/")) {
requestPath = "/js/" + requestPath;
}
}
return requestPath;
}
protected void createBundleIfNeeded(final String versionedBundleName, final List<String> filePaths,
final ResourceResolverChain resolverChain, final List<Resource> locations) {
if (!createdBundles.containsKey(versionedBundleName)) {
keyLockManager.executeLocked(versionedBundleName, new LockCallback() {
@Override
public void doInLock() {
Resource bundleResource = createdBundles.get(versionedBundleName);
if (bundleResource == null || !bundleResource.exists()) {
bundleResource = createBundle(versionedBundleName, filePaths, resolverChain, locations);
if (bundleResource != null) {
saveBundle(bundleResource);
}
Resource savedResource = readBundle(versionedBundleName);
createdBundles.put(versionedBundleName, savedResource);
}
}
});
}
}
protected Resource createBundle(String versionedBundleName, List<String> filePaths,
ResourceResolverChain resolverChain, List<Resource> locations) {
HttpServletRequest req = BroadleafRequestContext.getBroadleafRequestContext().getRequest();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bytes = null;
// Join all of the resources for this bundle together into a byte[]
try {
for (String fileName : filePaths) {
Resource r = resolverChain.resolveResource(req, fileName, locations);
InputStream is = null;
if (r == null) {
LOG.warn(new StringBuilder().append("Could not resolve resource specified in bundle as [")
.append(fileName)
.append("]. Turn on trace logging to determine resolution failure. Skipping file.")
.toString());
} else {
try {
is = r.getInputStream();
StreamUtils.copy(is, baos);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
IOUtils.closeQuietly(is);
}
// If we're creating a JavaScript bundle, we'll put a semicolon between each
// file to ensure it won't fail to compile.
if (versionedBundleName.endsWith(".js")) {
baos.write(";".getBytes());
}
baos.write(System.getProperty("line.separator").getBytes());
}
}
bytes = baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
IOUtils.closeQuietly(baos);
}
// Create our GenerateResource that holds our combined bundle
GeneratedResource r = new GeneratedResource(bytes, versionedBundleName);
return r;
}
protected void saveBundle(Resource resource) {
FileWorkArea tempWorkArea = fileService.initializeWorkArea();
String fileToSave = FilenameUtils.separatorsToSystem(getResourcePath(resource.getDescription()));
String tempFilename = FilenameUtils.concat(tempWorkArea.getFilePathLocation(), fileToSave);
File tempFile = new File(tempFilename);
if (!tempFile.getParentFile().exists()) {
if (!tempFile.getParentFile().mkdirs()) {
if (!tempFile.getParentFile().exists()) {
throw new RuntimeException("Unable to create parent directories for file: " + tempFilename);
}
}
}
BufferedOutputStream out = null;
InputStream ris = null;
try {
ris = resource.getInputStream();
out = new BufferedOutputStream(new FileOutputStream(tempFile));
StreamUtils.copy(ris, out);
ris.close();
out.close();
fileService.addOrUpdateResourceForPath(tempWorkArea, tempFile, true);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
IOUtils.closeQuietly(ris);
IOUtils.closeQuietly(out);
fileService.closeWorkArea(tempWorkArea);
}
}
protected String getCacheKey(String unversionedBundleName, List<String> files) {
return unversionedBundleName;
}
protected String getBundleName(String bundleName, String version) {
String bundleWithoutExtension = bundleName.substring(0, bundleName.lastIndexOf('.'));
String bundleExtension = bundleName.substring(bundleName.lastIndexOf('.'));
String versionedName = bundleWithoutExtension + version + bundleExtension;
return versionedName;
}
protected String getBundleVersion(LinkedHashMap<String, Resource> foundResources) throws IOException {
StringBuilder sb = new StringBuilder();
for (Entry<String, Resource> entry : foundResources.entrySet()) {
sb.append(entry.getKey());
if (entry.getValue() instanceof GeneratedResource) {
sb.append(((GeneratedResource) entry.getValue()).getHashRepresentation());
} else {
sb.append(entry.getValue().lastModified());
}
sb.append("\r\n");
}
String version = String.valueOf(sb.toString().hashCode());
return version;
}
@Override
public List<String> getAdditionalBundleFiles(String bundleName) {
return additionalBundleFiles.get(bundleName);
}
public void setAdditionalBundleFiles(Map<String, List<String>> additionalBundleFiles) {
this.additionalBundleFiles = additionalBundleFiles;
}
/**
* Copied from Spring 4.1 AbstractVersionStrategy
* @param requestPath
* @param version
* @return
*/
protected String addVersion(String requestPath, String version) {
String baseFilename = StringUtils.stripFilenameExtension(requestPath);
String extension = StringUtils.getFilenameExtension(requestPath);
return baseFilename + version + "." + extension;
}
protected Resource readBundle(String versionedBundleName) {
File bundleFile = fileService.getResource("/" + getResourcePath(versionedBundleName));
return bundleFile == null ? null : new FileSystemResource(bundleFile);
}
protected ResourceHttpRequestHandler findResourceHttpRequestHandler(String resourceName) {
resourceName = resourceName.toLowerCase();
if (isJavaScriptResource(resourceName)) {
return jsResourceHandler;
} else if (isCSSResource(resourceName)) {
return cssResourceHandler;
} else {
return null;
}
}
protected boolean isJavaScriptResource(String resourceName) {
return (resourceName != null && resourceName.contains(".js"));
}
protected boolean isCSSResource(String resourceName) {
return (resourceName != null && resourceName.contains(".css"));
}
/**
* Returns the resource path for the given <b>name</b> in URL-format (meaning, / separators)
* @param name
* @return
*/
protected String getResourcePath(String name) {
if (name.startsWith("/")) {
return "bundles" + name;
} else {
return "bundles/" + name;
}
}
}