org.broadleafcommerce.common.resource.service.ResourceBundlingServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.broadleafcommerce.common.resource.service.ResourceBundlingServiceImpl.java

Source

/*
 * #%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;
        }
    }
}