com.ibm.jaggr.core.impl.layer.LayerImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.ibm.jaggr.core.impl.layer.LayerImpl.java

Source

/*
 * (C) Copyright 2012, IBM Corporation
 *
 * 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 com.ibm.jaggr.core.impl.layer;

import com.ibm.jaggr.core.BadRequestException;
import com.ibm.jaggr.core.IAggregator;
import com.ibm.jaggr.core.cache.ICacheManager;
import com.ibm.jaggr.core.cachekeygenerator.AbstractCacheKeyGenerator;
import com.ibm.jaggr.core.cachekeygenerator.FeatureSetCacheKeyGenerator;
import com.ibm.jaggr.core.cachekeygenerator.ICacheKeyGenerator;
import com.ibm.jaggr.core.cachekeygenerator.KeyGenUtil;
import com.ibm.jaggr.core.deps.ModuleDepInfo;
import com.ibm.jaggr.core.deps.ModuleDeps;
import com.ibm.jaggr.core.layer.ILayer;
import com.ibm.jaggr.core.layer.ILayerCache;
import com.ibm.jaggr.core.module.IModule;
import com.ibm.jaggr.core.module.ModuleIdentifier;
import com.ibm.jaggr.core.module.ModuleSpecifier;
import com.ibm.jaggr.core.modulebuilder.ModuleBuildFuture;
import com.ibm.jaggr.core.options.IOptions;
import com.ibm.jaggr.core.readers.ModuleBuildReader;
import com.ibm.jaggr.core.resource.IResource;
import com.ibm.jaggr.core.transport.IHttpTransport;
import com.ibm.jaggr.core.transport.IRequestedModuleNames;
import com.ibm.jaggr.core.util.CopyUtil;
import com.ibm.jaggr.core.util.DependencyList;
import com.ibm.jaggr.core.util.Features;
import com.ibm.jaggr.core.util.RequestUtil;
import com.ibm.jaggr.core.util.TypeUtil;

import org.apache.commons.lang3.mutable.MutableObject;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.Writer;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.Deflater;
import java.util.zip.GZIPInputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * A LayerImpl is a collection of LayerBuild objects that are composed using the same
 * list of modules, but vary according to build options, compilation level, has filtering,
 * etc.
 */
public class LayerImpl implements ILayer {
    private static final long serialVersionUID = 2491460740123061848L;
    static final String sourceClass = LayerImpl.class.getName();
    static final Logger log = Logger.getLogger(sourceClass);

    static final String LAST_MODIFIED_PROPNAME = LayerImpl.class.getName() + ".LAST_MODIFIED_FILES"; //$NON-NLS-1$
    static final String MODULE_FILES_PROPNAME = LayerImpl.class.getName() + ".MODULE_FILES"; //$NON-NLS-1$

    // The following request attributes are used by unit tests
    static final String LAYERCACHEINFO_PROPNAME = LayerImpl.class.getName() + ".LAYER_CACHEIFNO"; //$NON-NLS-1$
    static final String LAYERBUILDCACHEKEY_PROPNAME = LayerImpl.class.getName() + ".LAYERBUILD_CACHEKEY"; //$NON-NLS-1$
    static final String BOOTLAYERDEPS_PROPNAME = LayerImpl.class.getName() + ".BOOT_LAYER_DEPS"; //$NON-NLS-1$

    static final String DEPSOURCE_REQDEPS = " URL - deps"; //$NON-NLS-1$
    static final String DEPSOURCE_REQPRELOADS = "URL - preloads"; //$NON-NLS-1$
    static final String DEPSOURCE_EXCLUDES = "URL - excludes"; //$NON-NLS-1$

    static final Pattern nlsPat = Pattern.compile("^.*(^|\\/)nls(\\/|$)"); //$NON-NLS-1$

    protected static final List<ICacheKeyGenerator> s_layerCacheKeyGenerators = Collections
            .unmodifiableList(Arrays.asList(new ICacheKeyGenerator[] { new AbstractCacheKeyGenerator() {
                // This is a singleton, so default equals() will do
                private static final long serialVersionUID = 2013098945317787755L;
                private static final String eyeCatcher = "lyr"; //$NON-NLS-1$

                @Override
                public String generateKey(HttpServletRequest request) {
                    boolean showFilenames = TypeUtil
                            .asBoolean(request.getAttribute(IHttpTransport.SHOWFILENAMES_REQATTRNAME));
                    return new StringBuffer(eyeCatcher).append(":") //$NON-NLS-1$
                            .append(RequestUtil.isGzipEncoding(request) ? "1" : "0").append(":") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                            .append(showFilenames ? "1" : "0").append(":") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                            .append(RequestUtil.isIncludeRequireDeps(request) ? "1" : "0").append(":") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                            .append(RequestUtil.isIncludeUndefinedFeatureDeps(request) ? "1" : "0").toString(); //$NON-NLS-1$ //$NON-NLS-2$

                }

                @Override
                public String toString() {
                    return eyeCatcher;
                }
            } }));

    public static final Pattern GZIPFLAG_KEY_PATTERN = Pattern
            .compile(s_layerCacheKeyGenerators.get(0).toString() + ":([01]):"); //$NON-NLS-1$

    static int LAYERBUILD_REMOVE_DELAY_SECONDS = 10;

    /**
     * Map of cache dependency objects for module classes included in this layer.
     * Cloned by reference since cache key generators are immutable.
     */
    private volatile Map<String, ICacheKeyGenerator> _cacheKeyGenerators = null;

    /**
     * The last modified date of the layer.  This is the latest of the last modified dates
     * of all the modules in the module list
     */
    private volatile long _lastModified = -1;

    /**
     * Flag to indicate whether or not last modified time needs to be validated.  Transient
     * because we always want last-modified validated on startup.
     */
    private transient AtomicBoolean _validateLastModified = new AtomicBoolean(true);

    /** The map of key/builds for this layer. */
    private transient LayerBuildsAccessor _layerBuilds;

    /**
     * Flag to indicate that addition information used unit tests should be added to the
     * request object
     */
    private transient boolean _isReportCacheInfo = false;

    private final String _cacheKey;

    final int _id;

    /**
     * @param cacheKey The folded module list as specified in the request
     * @param id unique identifier
     */
    public LayerImpl(String cacheKey, int id) {
        _cacheKey = cacheKey;
        _id = id;
    }

    /* (non-Javadoc)
     * @see com.ibm.servlets.amd.aggregator.layer.ILayer#getKey()
     */
    @Override
    public String getKey() {
        return _cacheKey;
    }

    int getId() {
        return _id;
    }

    /**
     * @param isReportCacheInfo
     */
    public void setReportCacheInfo(boolean isReportCacheInfo) {
        this._isReportCacheInfo = isReportCacheInfo;
    }

    /* (non-Javadoc)
     * @see com.ibm.jaggr.service.layer.ILayer#getInputStream(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @SuppressWarnings("unchecked")
    @Override
    public InputStream getInputStream(HttpServletRequest request, HttpServletResponse response) throws IOException {

        CacheEntry entry = null;
        String key = null;
        IAggregator aggr = (IAggregator) request.getAttribute(IAggregator.AGGREGATOR_REQATTRNAME);
        List<String> cacheInfoReport = null;
        if (_isReportCacheInfo) {
            cacheInfoReport = (List<String>) request.getAttribute(LAYERCACHEINFO_PROPNAME);
            if (cacheInfoReport != null) {
                cacheInfoReport.clear();
            }
        }
        if (log.isLoggable(Level.FINEST) && cacheInfoReport == null) {
            cacheInfoReport = new LinkedList<String>();
        }
        try {
            IOptions options = aggr.getOptions();
            ICacheManager mgr = aggr.getCacheManager();
            boolean ignoreCached = RequestUtil.isIgnoreCached(request);
            InputStream result;
            long lastModified = getLastModified(request);
            CacheEntry newEntry = new CacheEntry(_id, _cacheKey, lastModified);
            CacheEntry existingEntry = null;

            if (ignoreCached) {
                request.setAttribute(NOCACHE_RESPONSE_REQATTRNAME, Boolean.TRUE);
            }
            if (options.isDevelopmentMode()) {
                synchronized (this) {
                    // See if we need to discard previously built LayerBuilds
                    if (lastModified > _lastModified) {
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add("update_lastmod2"); //$NON-NLS-1$
                        }
                        if (lastModified != Long.MAX_VALUE) {
                            // max value means missing requested source
                            _lastModified = lastModified;
                        }
                        _cacheKeyGenerators = null;
                    }
                }
            }
            Map<String, ICacheKeyGenerator> cacheKeyGenerators = _cacheKeyGenerators;

            // Creata a cache key.
            key = generateCacheKey(request, cacheKeyGenerators);

            if (!ignoreCached && key != null) {
                int loopGuard = 5;
                do {
                    // Try to retrieve an existing layer build using the blocking putIfAbsent.  If the return
                    // value is null, then the newEntry was successfully added to the map, otherwise the
                    // existing entry is returned in the buildReader and newEntry was not added.
                    existingEntry = _layerBuilds.putIfAbsent(key, newEntry, options.isDevelopmentMode());
                    if (cacheInfoReport != null) {
                        cacheInfoReport.add(existingEntry != null ? "hit_1" : "added"); //$NON-NLS-1$ //$NON-NLS-2$
                    }
                    if (existingEntry != null) {
                        if ((result = existingEntry.tryGetInputStream(request)) != null) {
                            setResponseHeaders(request, response, existingEntry.getSize());
                            if (log.isLoggable(Level.FINEST)) {
                                log.finest(cacheInfoReport.toString() + "\n" + //$NON-NLS-1$
                                        "key:" + key + //$NON-NLS-1$
                                        "\n" + existingEntry.toString()); //$NON-NLS-1$
                            }
                            if (_isReportCacheInfo) {
                                request.setAttribute(LAYERBUILDCACHEKEY_PROPNAME, key);
                            }
                            return result;
                        } else if (existingEntry.isDeleted()) {
                            if (_layerBuilds.replace(key, existingEntry, newEntry)) {
                                // entry was replaced, use newEntry
                                if (cacheInfoReport != null) {
                                    cacheInfoReport.add("replace_1"); //$NON-NLS-1$
                                }
                                existingEntry = null;
                            } else {
                                // Existing entry was removed from the cache by another thread
                                // between the time we retrieved it and the time we tried to
                                // replace it.  Try to add the new entry again.
                                if (cacheInfoReport != null) {
                                    cacheInfoReport.add("retry_add"); //$NON-NLS-1$
                                }
                                if (--loopGuard == 0) {
                                    // Should never happen, but just in case
                                    throw new IllegalStateException();
                                }
                                continue;
                            }
                        }
                    }
                    break;
                } while (true);
            }

            // putIfAbsent() succeeded and the new entry was added to the cache
            entry = (existingEntry != null) ? existingEntry : newEntry;

            LayerBuilder layerBuilder = null;

            // List of Future<IModule.ModuleReader> objects that will be used to read the module
            // data from
            List<ICacheKeyGenerator> moduleKeyGens = null;

            // Synchronize on the LayerBuild object for the build.  This will prevent multiple
            // threads from building the same output.  If more than one thread requests the same
            // output (same cache key), then the first one to grab the sync object will win and
            // the rest will wait for the first thread to finish building and then just return
            // the output from the first thread when they wake.
            synchronized (entry) {

                // Check to see if data is available one more time in case a different thread finished
                // building the output while we were blocked on the sync object.
                if (!ignoreCached && key != null && (result = entry.tryGetInputStream(request)) != null) {
                    if (cacheInfoReport != null) {
                        cacheInfoReport.add("hit_2"); //$NON-NLS-1$
                    }
                    setResponseHeaders(request, response, entry.getSize());
                    if (log.isLoggable(Level.FINEST)) {
                        log.finest(cacheInfoReport.toString() + "\n" + //$NON-NLS-1$
                                "key:" + key + //$NON-NLS-1$
                                "\n" + entry.toString()); //$NON-NLS-1$
                    }
                    if (_isReportCacheInfo) {
                        request.setAttribute(LAYERBUILDCACHEKEY_PROPNAME, key);
                    }
                    return result;
                }

                boolean isGzip = RequestUtil.isGzipEncoding(request);
                ByteArrayOutputStream bos = new ByteArrayOutputStream();

                // See if we already have a cached response that uses a different gzip
                // encoding option.  If we do, then just zip (or unzip) the cached
                // response
                CacheEntry otherEntry = null;
                if (key != null) {
                    StringBuffer sb = new StringBuffer();
                    Matcher m = GZIPFLAG_KEY_PATTERN.matcher(key);
                    m.find();
                    m.appendReplacement(sb,
                            new StringBuffer(s_layerCacheKeyGenerators.get(0).toString()).append(":") //$NON-NLS-1$
                                    .append("1".equals(m.group(1)) ? "0" : "1") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                                    .append(":").toString() //$NON-NLS-1$
                    ).appendTail(sb);
                    otherEntry = _layerBuilds.get(sb.toString());
                }
                if (otherEntry != null) {
                    if (isGzip) {
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add("zip_unzipped"); //$NON-NLS-1$
                        }
                        // We need gzipped and the cached entry is unzipped
                        // Create the compression stream for the output
                        VariableGZIPOutputStream compress = new VariableGZIPOutputStream(bos, 10240); // is 10k too big?
                        compress.setLevel(Deflater.BEST_COMPRESSION);
                        Writer writer = new OutputStreamWriter(compress, "UTF-8"); //$NON-NLS-1$

                        // Copy the data from the input stream to the output, compressing as we go.
                        CopyUtil.copy(otherEntry.getInputStream(request), writer);
                    } else {
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add("unzip_zipped"); //$NON-NLS-1$
                        }
                        // We need unzipped and the cached entry is zipped.  Just unzip it
                        CopyUtil.copy(new GZIPInputStream(otherEntry.getInputStream(request)), bos);
                    }
                    // Set the buildReader to the LayerBuild and release the lock by exiting the sync block
                    entry.setBytes(bos.toByteArray());
                    if (!ignoreCached) {
                        _layerBuilds.replace(key, entry, entry); // updates entry weight in map
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add("update_weights_1"); //$NON-NLS-1$
                        }
                        entry.persist(mgr);
                    }
                } else {
                    moduleKeyGens = new LinkedList<ICacheKeyGenerator>();

                    ModuleList moduleList = getModules(request);

                    // Remove the module list from the request to safe-guard it now that we don't
                    // need it there anymore
                    request.removeAttribute(MODULE_FILES_PROPNAME);

                    // Create a BuildListReader from the list of Futures.  This reader will obtain a
                    // ModuleReader from each of the Futures in the list and read data from each one in
                    // succession until all the data has been read, blocking on each Future until the
                    // reader becomes available.
                    layerBuilder = new LayerBuilder(request, moduleKeyGens, moduleList);
                    String layer = layerBuilder.build();

                    if (isGzip) {
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add("zip"); //$NON-NLS-1$
                        }
                        VariableGZIPOutputStream compress = new VariableGZIPOutputStream(bos, 10240); // is 10k too big?
                        compress.setLevel(Deflater.BEST_COMPRESSION);
                        Writer writer = new OutputStreamWriter(compress, "UTF-8"); //$NON-NLS-1$

                        // Copy the data from the input stream to the output, compressing as we go.
                        CopyUtil.copy(new StringReader(layer), writer);
                        // Set the buildReader to the LayerBuild and release the lock by exiting the sync block
                        entry.setBytes(bos.toByteArray());
                    } else {
                        entry.setBytes(layer.getBytes());
                    }

                    // entry will be persisted below after we determine if cache key
                    // generator needs to be updated
                }
            }

            // if any of the readers included an error response, then don't cache the layer.
            if (layerBuilder != null && layerBuilder.hasErrors()) {
                request.setAttribute(NOCACHE_RESPONSE_REQATTRNAME, Boolean.TRUE);
                if (cacheInfoReport != null) {
                    cacheInfoReport.add(key == null ? "error_noaction" : "error_remove"); //$NON-NLS-1$ //$NON-NLS-2$
                }
                if (key != null) {
                    _layerBuilds.remove(key, entry);
                }
            } else if (layerBuilder != null) {
                if (!ignoreCached) {
                    // See if we need to create or update the cache key generators
                    Map<String, ICacheKeyGenerator> newKeyGens = new HashMap<String, ICacheKeyGenerator>();
                    Set<String> requiredModuleListDeps = getModules(request).getDependentFeatures();
                    addCacheKeyGenerators(newKeyGens, s_layerCacheKeyGenerators);
                    addCacheKeyGenerators(newKeyGens, aggr.getTransport().getCacheKeyGenerators());
                    addCacheKeyGenerators(newKeyGens, Arrays.asList(new ICacheKeyGenerator[] {
                            new FeatureSetCacheKeyGenerator(requiredModuleListDeps, false) }));
                    addCacheKeyGenerators(newKeyGens, moduleKeyGens);

                    boolean cacheKeyGeneratorsUpdated = false;
                    if (!newKeyGens.equals(cacheKeyGenerators)) {
                        // If we don't yet have a cache key for this layer, then get one
                        // from the cache key generators, and then update the cache key for this
                        // cache entry.

                        synchronized (this) {
                            if (_cacheKeyGenerators != null) {
                                addCacheKeyGenerators(newKeyGens, _cacheKeyGenerators.values());
                            }
                            _cacheKeyGenerators = Collections.unmodifiableMap(newKeyGens);
                        }
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add("update_keygen"); //$NON-NLS-1$
                        }
                        cacheKeyGeneratorsUpdated = true;
                    }
                    final String originalKey = key;
                    if (key == null || cacheKeyGeneratorsUpdated) {
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add("update_key"); //$NON-NLS-1$
                        }
                        key = generateCacheKey(request, newKeyGens);
                    }
                    if (originalKey == null || !originalKey.equals(key)) {
                        /*
                         * The cache key has changed from what was originally used to put the
                         * un-built entry into the cache.  Add the LayerBuild to the cache
                         * using the new key.
                         */
                        if (log.isLoggable(Level.FINE)) {
                            log.fine("Key changed!  Adding layer to cache with key: " + key); //$NON-NLS-1$
                        }
                        final CacheEntry originalEntry = entry;
                        CacheEntry updateEntry = (originalKey == null) ? entry : new CacheEntry(entry);
                        CacheEntry previousEntry = _layerBuilds.putIfAbsent(key, updateEntry,
                                options.isDevelopmentMode());
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add(previousEntry == null ? "update_add" : "update_hit"); //$NON-NLS-1$ //$NON-NLS-2$
                        }
                        // Write the file to disk only if the LayerBuild was successfully added to the cache
                        if (previousEntry == null) {
                            // Updated entry was added to the cache.
                            entry = updateEntry;
                            entry.persist(mgr);
                        }
                        // If the key changed, then remove the entry under the old key.  Use a
                        // delay to give other threads a chance to start using the new cache
                        // key generator.  No need to update entry weight in map
                        if (originalKey != null) {
                            aggr.getExecutors().getScheduledExecutor().schedule(new Runnable() {
                                public void run() {
                                    _layerBuilds.remove(originalKey, originalEntry);
                                }
                            }, LAYERBUILD_REMOVE_DELAY_SECONDS, TimeUnit.SECONDS);
                        }
                    } else {
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add("update_weights_2"); //$NON-NLS-1$
                        }
                        _layerBuilds.replace(key, entry, entry); // updates entry weight in map
                        entry.persist(mgr);
                    }
                }
            }
            result = entry.getInputStream(request);
            setResponseHeaders(request, response, entry.getSize());

            // return the input stream to the LayerBuild
            if (log.isLoggable(Level.FINEST)) {
                log.finest(cacheInfoReport.toString() + "\n" + //$NON-NLS-1$
                        "key:" + key + //$NON-NLS-1$
                        "\n" + entry.toString()); //$NON-NLS-1$
            }
            if (_isReportCacheInfo) {
                request.setAttribute(LAYERBUILDCACHEKEY_PROPNAME, key);
            }
            return result;
        } catch (IOException e) {
            _layerBuilds.remove(key, entry);
            throw e;
        } catch (RuntimeException e) {
            _layerBuilds.remove(key, entry);
            throw e;
        } finally {
            if (_layerBuilds.isLayerEvicted()) {
                _layerBuilds.removeLayerFromCache(this);
            }
        }
    }

    /**
     * Adds the cache key generators specified in {@code gens} to the map of
     * classname/key-generator pairs, combining key-generators as needed.
     *
     * @param cacheKeyGenerators
     *            Map of classname/key-generator pairs to add to.
     * @param gens
     *            the cache key generators to add.
     */
    protected void addCacheKeyGenerators(Map<String, ICacheKeyGenerator> cacheKeyGenerators,
            Iterable<ICacheKeyGenerator> gens) {
        if (gens != null) {
            for (ICacheKeyGenerator gen : gens) {
                String className = gen.getClass().getName();
                ICacheKeyGenerator current = cacheKeyGenerators.get(className);
                cacheKeyGenerators.put(className, (current == null) ? gen : current.combine(gen));
            }
        }
    }

    /**
     * Generates a cache key for the layer.
     *
     * @param request
     *            the request object
     * @param cacheKeyGenerators
     *            map of cache key generator class names to instance objects
     * @return the cache key
     * @throws IOException
     */
    protected String generateCacheKey(HttpServletRequest request,
            Map<String, ICacheKeyGenerator> cacheKeyGenerators) throws IOException {
        String cacheKey = null;
        if (cacheKeyGenerators != null) {
            // First, decompose any composite cache key generators into their
            // constituent cache key generators so that we can combine them
            // more effectively.  Use TreeMap to get predictable ordering of
            // keys.
            Map<String, ICacheKeyGenerator> gens = new TreeMap<String, ICacheKeyGenerator>();
            for (ICacheKeyGenerator gen : cacheKeyGenerators.values()) {
                List<ICacheKeyGenerator> constituentGens = gen.getCacheKeyGenerators(request);
                addCacheKeyGenerators(gens,
                        constituentGens == null ? Arrays.asList(new ICacheKeyGenerator[] { gen })
                                : constituentGens);
            }
            cacheKey = KeyGenUtil.generateKey(request, gens.values());
        }
        return cacheKey;
    }

    /* (non-Javadoc)
     * @see com.ibm.jaggr.service.layer.ILayer#getLastModified(javax.servlet.http.HttpServletRequest)
     */
    @SuppressWarnings("unchecked")
    @Override
    public long getLastModified(HttpServletRequest request) throws IOException {
        long lastModified = _lastModified;
        IAggregator aggregator = (IAggregator) request.getAttribute(IAggregator.AGGREGATOR_REQATTRNAME);
        IOptions options = aggregator.getOptions();
        // Don't check last modified dates of source files on every request in production mode
        // for performance reasons.  _validateLastModified is a transient that gets initialize
        // to true whenever this object is de-serialized (i.e. on server startup).
        if (lastModified == -1 || _validateLastModified.getAndSet(false) || options.isDevelopmentMode()) {
            // see if we already determined the last modified time for this request
            Object obj = request.getAttribute(LAST_MODIFIED_PROPNAME);
            if (obj == null) {
                // Determine latest last-modified time from source files in moduleList
                ModuleList moduleFiles = getModules(request);
                lastModified = getLastModified(aggregator, moduleFiles);
                // Get last-modified date of config file
                lastModified = Math.max(lastModified, aggregator.getConfig().lastModified());
                List<String> cacheInfoReport = null;
                if (_isReportCacheInfo) {
                    cacheInfoReport = (List<String>) request.getAttribute(LAYERCACHEINFO_PROPNAME);
                }
                synchronized (this) {
                    if (_lastModified == -1) {
                        // Initialize value of instance property
                        _lastModified = lastModified;
                        if (cacheInfoReport != null) {
                            cacheInfoReport.add("update_lastmod1"); //$NON-NLS-1$
                        }
                    }
                }
                request.setAttribute(LAST_MODIFIED_PROPNAME, lastModified);
                if (log.isLoggable(Level.FINER)) {
                    log.finer("Returning calculated last modified " //$NON-NLS-1$
                            + lastModified + " for layer " + //$NON-NLS-1$
                            request.getAttribute(IHttpTransport.REQUESTEDMODULENAMES_REQATTRNAME).toString());
                }
            } else {
                lastModified = (Long) obj;
                if (log.isLoggable(Level.FINER)) {
                    log.finer("Returning last modified " //$NON-NLS-1$
                            + lastModified + " from request for layer " + //$NON-NLS-1$
                            request.getAttribute(IHttpTransport.REQUESTEDMODULENAMES_REQATTRNAME).toString());
                }
            }
        } else {
            if (log.isLoggable(Level.FINER)) {
                log.finer("Returning cached last modified " //$NON-NLS-1$
                        + lastModified + " for layer " + //$NON-NLS-1$
                        request.getAttribute(IHttpTransport.REQUESTEDMODULENAMES_REQATTRNAME).toString());
            }
        }
        return lastModified;
    }

    LayerImpl cloneForSerialization() {
        LayerImpl clone;
        // The clone lock is owned (as a write lock) by the caller, so no need to
        // acquire it here.
        // _cacheKeyGenerators is an immutable set, so a reference clone is ok
        // _validateLastModified is transient, so no need to bother cloning
        // Layer cache is responsible for setting new _cloneLock and _layerBuilds
        // objects by calling setters subsequent to this call
        clone = new LayerImpl(_cacheKey, _id);
        clone._cacheKeyGenerators = _cacheKeyGenerators;
        clone._lastModified = _lastModified;
        return clone;
    }

    void setLayerBuildsAccessor(LayerBuildsAccessor layerBuilds) {
        if (_layerBuilds != null) {
            throw new IllegalStateException();
        }
        _layerBuilds = layerBuilds;
    }

    /**
     * De-serialize this object from an ObjectInputStream
     * @param in The ObjectInputStream
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // Call the default implementation to de-serialize our object
        in.defaultReadObject();
        // init transients
        _validateLastModified = new AtomicBoolean(true);
        _isReportCacheInfo = false;
    }

    /**
     * Calls the filtered version with a null filter
     */
    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        String linesep = System.getProperty("line.separator"); //$NON-NLS-1$
        StringBuffer sb = new StringBuffer();
        sb.append("\nModified: ") //$NON-NLS-1$
                .append(new Date(_lastModified).toString()).append(linesep).append("KeyGen: ").append( //$NON-NLS-1$
                        _cacheKeyGenerators != null ? KeyGenUtil.toString(_cacheKeyGenerators.values()) : "null") //$NON-NLS-1$
                .append(linesep);
        if (_layerBuilds != null) {
            for (Map.Entry<String, CacheEntry> entry : _layerBuilds.entrySet()) {
                sb.append("\t").append(entry.getKey()) //$NON-NLS-1$
                        .append(" : ").append(entry.getValue().getFilename()).append(linesep); //$NON-NLS-1$
            }
        }
        sb.append(linesep);
        return sb.toString();
    }

    /**
     * Unfolds a folded module list and returns a list of Source objects
     *
     * @param request The request
     * @return A list of Source objects
     * @throws IOException
     */
    protected ModuleList getModules(HttpServletRequest request) throws IOException {
        final String sourceMethod = "getModules"; //$NON-NLS-1$
        final boolean isTraceLogging = log.isLoggable(Level.FINER);
        if (isTraceLogging) {
            log.entering(sourceMethod, sourceMethod, new Object[] { request });
        }
        ModuleList result = (ModuleList) request.getAttribute(MODULE_FILES_PROPNAME);
        if (result == null) {
            IAggregator aggr = (IAggregator) request.getAttribute(IAggregator.AGGREGATOR_REQATTRNAME);
            IRequestedModuleNames requestedModuleNames = (IRequestedModuleNames) request
                    .getAttribute(IHttpTransport.REQUESTEDMODULENAMES_REQATTRNAME);

            result = new ModuleList();
            if (requestedModuleNames != null) {
                Features features = (Features) request.getAttribute(IHttpTransport.FEATUREMAP_REQATTRNAME);
                Set<String> dependentFeatures = new HashSet<String>();
                List<String> names = requestedModuleNames.getModules();
                if (!names.isEmpty()) {
                    for (String name : names) {
                        if (name != null) {
                            name = aggr.getConfig().resolve(name, features, dependentFeatures, null, false, // Don't resolve aliases when locating modules requested by the loader
                                    //  because the loader should have already done alias resolution and
                                    //  we can't rename a requested module.
                                    false // Loader doesn't request modules with has! plugin
                            );
                            result.add(new ModuleList.ModuleListEntry(newModule(request, name),
                                    ModuleSpecifier.MODULES));
                        }
                    }
                } else {
                    boolean includeRequireDeps = RequestUtil.isIncludeRequireDeps(request);
                    names = requestedModuleNames.getScripts();
                    for (String name : names) {
                        if (name != null) {
                            name = aggr.getConfig().resolve(name, features, dependentFeatures, null, false, // Don't resolve aliases when locating modules requested by the loader
                                    //  because the loader should have already done alias resolution and
                                    //  we can't rename a requested module.
                                    true // Resolve has! loader plugin
                            );
                            result.add(new ModuleList.ModuleListEntry(newModule(request, name),
                                    ModuleSpecifier.SCRIPTS));
                        }
                    }
                    // See if we need to add required modules.
                    DependencyList requiredList = null, preloadList = null, excludeList = null;
                    ModuleDeps combined = new ModuleDeps(), explicit = new ModuleDeps();
                    if (!requestedModuleNames.getDeps().isEmpty()) {

                        // If there's a required module, then add it and its dependencies
                        // to the module list.
                        requiredList = new DependencyList(DEPSOURCE_REQDEPS, requestedModuleNames.getDeps(), aggr,
                                features, true, // resolveAliases
                                RequestUtil.isRequireExpLogging(request), // include details
                                includeRequireDeps);
                        dependentFeatures.addAll(requiredList.getDependentFeatures());

                        result.setRequiredModules(requiredList.getExplicitDeps().getModuleIds());

                        explicit.addAll(requiredList.getExplicitDeps());
                        combined.addAll(requiredList.getExplicitDeps());
                        combined.addAll(requiredList.getExpandedDeps());
                    }
                    if (!requestedModuleNames.getPreloads().isEmpty()) {
                        preloadList = new DependencyList(DEPSOURCE_REQPRELOADS, requestedModuleNames.getPreloads(),
                                aggr, features, true, // resolveAliases
                                RequestUtil.isRequireExpLogging(request), // include details
                                includeRequireDeps);
                        dependentFeatures.addAll(preloadList.getDependentFeatures());

                        explicit.addAll(preloadList.getExplicitDeps());
                        combined.addAll(preloadList.getExplicitDeps());
                        combined.addAll(preloadList.getExpandedDeps());
                    }
                    if (!requestedModuleNames.getExcludes().isEmpty()) {
                        excludeList = new DependencyList(DEPSOURCE_EXCLUDES, requestedModuleNames.getExcludes(),
                                aggr, features, true, // resolveAliases
                                RequestUtil.isRequireExpLogging(request), // include details
                                false);
                        dependentFeatures.addAll(excludeList.getDependentFeatures());
                        combined.subtractAll(excludeList.getExplicitDeps());
                        combined.subtractAll(excludeList.getExpandedDeps());
                    }
                    boolean isAssertNoNLS = RequestUtil.isAssertNoNLS(request);
                    for (Map.Entry<String, ModuleDepInfo> entry : combined.entrySet()) {
                        String name = entry.getKey();
                        if (isAssertNoNLS && nlsPat.matcher(name).find()) {
                            throw new BadRequestException("AssertNoNLS: " + name); //$NON-NLS-1$
                        }
                        ModuleDepInfo info = entry.getValue();
                        if (aggr.getTransport().isServerExpandable(request, name)) {
                            int idx = name.indexOf("!"); //$NON-NLS-1$
                            if (idx != -1) {
                                // convert name to a delegate plugin if necessary
                                String plugin = name.substring(0, idx);
                                if (aggr.getConfig().getTextPluginDelegators().contains(plugin)) {
                                    name = aggr.getTransport().getAggregatorTextPluginName() + name.substring(idx);
                                } else if (aggr.getConfig().getJsPluginDelegators().contains(plugin)) {
                                    name = name.substring(idx + 1);
                                }
                            }
                            Collection<String> prefixes = info.getHasPluginPrefixes();
                            if (prefixes == null || // condition is TRUE
                                    RequestUtil.isIncludeUndefinedFeatureDeps(request) && !prefixes.isEmpty()) {
                                IModule module = newModule(request, name);
                                if (!explicit.containsKey(name) && aggr
                                        .getResourceFactory(new MutableObject<URI>(module.getURI())) == null) {
                                    // Module is server-expanded and it's not a server resource type that we
                                    // know how handle, so just ignore it.
                                    if (isTraceLogging) {
                                        log.logp(Level.FINER, sourceClass, sourceMethod,
                                                "Ignoring module " + name + " due to no resource factory found."); //$NON-NLS-1$ //$NON-NLS-2$
                                    }
                                    continue;
                                }
                                result.add(new ModuleList.ModuleListEntry(module, ModuleSpecifier.LAYER,
                                        !explicit.containsKey(name)));
                            }
                        }
                    }
                    if ((requiredList != null || preloadList != null) && RequestUtil.isRequireExpLogging(request)) {
                        ModuleDeps expanded = new ModuleDeps();
                        if (requiredList != null) {
                            expanded.addAll(requiredList.getExpandedDeps());
                        }
                        if (preloadList != null) {
                            expanded.addAll(preloadList.getExpandedDeps());
                        }
                        if (excludeList != null) {
                            explicit.subtractAll(excludeList.getExplicitDeps());
                            explicit.subtractAll(excludeList.getExpandedDeps());
                            expanded.subtractAll(excludeList.getExplicitDeps());
                            expanded.subtractAll(excludeList.getExpandedDeps());
                        }
                        request.setAttribute(BOOTLAYERDEPS_PROPNAME,
                                new DependencyList(explicit, expanded, dependentFeatures));
                    }
                    result.setDependenentFeatures(dependentFeatures);
                }
            }
            if (result.isEmpty()) {
                throw new BadRequestException(request.getQueryString());
            }
            request.setAttribute(MODULE_FILES_PROPNAME, result);
        }
        if (isTraceLogging) {
            log.exiting(sourceClass, sourceMethod, result);
        }
        return result;
    }

    protected IModule newModule(HttpServletRequest request, String mid) {
        if (mid.endsWith("/.")) { //$NON-NLS-1$
            mid = mid.substring(0, mid.length() - 2);
        }
        IAggregator aggr = (IAggregator) request.getAttribute(IAggregator.AGGREGATOR_REQATTRNAME);

        URI uri = aggr.getConfig().locateModuleResource(new ModuleIdentifier(mid).getModuleName());

        return aggr.newModule(mid, uri);
    }

    /**
     * Returns the newest last modified time of the files in the list
     *
     * @param aggregator
     * @param modules
     *            The list of ModuleFile objects
     * @return The newest last modified time of all the files in the list
     */
    protected long getLastModified(IAggregator aggregator, ModuleList modules) {
        long result = 0L;
        for (ModuleList.ModuleListEntry entry : modules) {
            IResource resource = entry.getModule().getResource(aggregator);
            long lastMod = resource.lastModified();
            if (lastMod == 0 && !resource.exists()) {
                lastMod = Long.MAX_VALUE;
            }
            result = Math.max(result, lastMod);
        }
        return result;
    }

    protected List<ICacheKeyGenerator> getCacheKeyGenerators(List<ModuleBuildFuture> futures) throws IOException {
        List<ICacheKeyGenerator> result = new LinkedList<ICacheKeyGenerator>();
        for (Future<ModuleBuildReader> future : futures) {
            ModuleBuildReader reader;
            try {
                reader = future.get();
            } catch (InterruptedException e) {
                throw new IOException(e);
            } catch (ExecutionException e) {
                throw new IOException(e);
            }
            List<ICacheKeyGenerator> keyGens = reader.getCacheKeyGenerators();
            if (keyGens != null) {
                result.addAll(keyGens);
            }
        }
        return result;

    }

    /**
     * Called by the layer cache manager when a layer build is evicted from the
     * eviction map do to size limitations.
     *
     * @param cacheEntry
     *            The cache entry that was evicted
     * @return true if this layer has no more builds and the layer should be
     *         removed from the layer cache
     */
    protected boolean cacheEntryEvicted(CacheEntry cacheEntry) {
        return _layerBuilds.cacheEntryEvicted(cacheEntry);
    }

    /**
     * This method is provided for unit testing
     *
     * @return The cacheKeyGenerators for this layer
     */
    Map<String, ICacheKeyGenerator> getCacheKeyGenerators() {
        return _cacheKeyGenerators;
    }

    /**
     * Used by unit test cases.
     *
     * @return The current layer build map.
     */
    Map<String, CacheEntry> getLayerBuildMap() {
        return _layerBuilds.getMap();
    }

    /**
     * Static factory method for layer cache objects
     *
     * @param aggregator the aggregator this layer cache belongs to
     * @return a new layer cache
     */
    public static ILayerCache newLayerCache(IAggregator aggregator) {
        return new LayerCacheImpl(aggregator);
    }

    protected void setResponseHeaders(HttpServletRequest request, HttpServletResponse response, int size) {
        response.setContentType("application/x-javascript; charset=utf-8"); //$NON-NLS-1$
        response.setContentLength(size);
        if (RequestUtil.isGzipEncoding(request)) {
            response.setHeader("Content-Encoding", "gzip"); //$NON-NLS-1$ //$NON-NLS-2$
        }

    }
}