org.wicketstuff.mergedresources.ResourceMount.java Source code

Java tutorial

Introduction

Here is the source code for org.wicketstuff.mergedresources.ResourceMount.java

Source

/**
 * Copyright 2010 Molindo GmbH
 *
 * 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.wicketstuff.mergedresources;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;

import org.apache.wicket.Application;
import org.apache.wicket.MetaDataKey;
import org.apache.wicket.Resource;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.WicketAjaxReference;
import org.apache.wicket.behavior.AbstractHeaderContributor;
import org.apache.wicket.markup.html.WicketEventReference;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy;
import org.apache.wicket.request.target.coding.SharedResourceRequestTargetUrlCodingStrategy;
import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wicketstuff.mergedresources.annotations.ContributionInjector;
import org.wicketstuff.mergedresources.annotations.ContributionScanner;
import org.wicketstuff.mergedresources.annotations.ContributionScanner.WeightedResourceSpec;
import org.wicketstuff.mergedresources.annotations.CssContribution;
import org.wicketstuff.mergedresources.annotations.JsContribution;
import org.wicketstuff.mergedresources.preprocess.IResourcePreProcessor;
import org.wicketstuff.mergedresources.resources.CachedCompressedCssResourceReference;
import org.wicketstuff.mergedresources.resources.CachedCompressedJsResourceReference;
import org.wicketstuff.mergedresources.resources.CachedCompressedResourceReference;
import org.wicketstuff.mergedresources.resources.CachedResourceReference;
import org.wicketstuff.mergedresources.resources.CompressedMergedCssResource;
import org.wicketstuff.mergedresources.resources.CompressedMergedCssResourceReference;
import org.wicketstuff.mergedresources.resources.CompressedMergedJsResourceReference;
import org.wicketstuff.mergedresources.resources.CompressedMergedResourceReference;
import org.wicketstuff.mergedresources.resources.ICssCompressor;
import org.wicketstuff.mergedresources.resources.MergedResourceReference;
import org.wicketstuff.mergedresources.util.MergedHeaderContributor;
import org.wicketstuff.mergedresources.util.MergedResourceRequestTargetUrlCodingStrategy;
import org.wicketstuff.mergedresources.util.Pair;
import org.wicketstuff.mergedresources.util.RedirectStrategy;
import org.wicketstuff.mergedresources.versioning.AbstractResourceVersion;
import org.wicketstuff.mergedresources.versioning.AbstractResourceVersion.IncompatibleVersionsException;
import org.wicketstuff.mergedresources.versioning.IResourceVersionProvider;
import org.wicketstuff.mergedresources.versioning.IResourceVersionProvider.VersionException;
import org.wicketstuff.mergedresources.versioning.RevisionVersionProvider;
import org.wicketstuff.mergedresources.versioning.SimpleResourceVersion;
import org.wicketstuff.mergedresources.versioning.WicketVersionProvider;

public class ResourceMount implements Cloneable {

    public enum SuffixMismatchStrategy {
        IGNORE, WARN, EXCEPTION;
    }

    private static final Logger LOG = LoggerFactory.getLogger(ResourceMount.class);

    private static final MetaDataKey<Boolean> ANNOTATIONS_ENABLED_KEY = new MetaDataKey<Boolean>() {
        private static final long serialVersionUID = 1L;
    };

    /**
     * default cache duration is 1 hour
     */
    public static final int DEFAULT_CACHE_DURATION = (int) Duration.hours(1).seconds();

    /**
     * default aggressive cache duration is 1 year
     */
    public static final int DEFAULT_AGGRESSIVE_CACHE_DURATION = (int) Duration.days(365).seconds();

    /**
     * @deprecated typo in name, it's aggressive with ss, use
     *             {@link #DEFAULT_AGGRESSIVE_CACHE_DURATION} instead
     */
    @Deprecated
    public static final int DEFAULT_AGGRESIVE_CACHE_DURATION = DEFAULT_AGGRESSIVE_CACHE_DURATION;

    /**
     * file suffixes to be compressed by default ("css", "js", "html", "xml").
     * For instance, there is no sense in gzipping images
     */
    public static final Set<String> DEFAULT_COMPRESS_SUFFIXES = Collections
            .unmodifiableSet(new HashSet<String>(Arrays.asList("html", "css", "js", "xml")));

    /**
     * file suffixes to be merged by default ("css" and "js"). For instance,
     * there is no sense in merging xml files into a single one by default (you
     * don't want multiple root elements)
     */
    public static final Set<String> DEFAULT_MERGE_SUFFIXES = Collections
            .unmodifiableSet(new HashSet<String>(Arrays.asList("css", "js")));

    /**
     * MetaDataKey used for {@link CompressedMergedCssResource}
     */
    public static final MetaDataKey<ICssCompressor> CSS_COMPRESSOR_KEY = new MetaDataKey<ICssCompressor>() {

        private static final long serialVersionUID = 1L;
    };

    private Integer _cacheDuration = null;
    private String _path = null;
    private AbstractResourceVersion _version = null;
    private AbstractResourceVersion _minVersion = null;
    private boolean _requireVersion = true;
    private IResourceVersionProvider _resourceVersionProvider = null;
    private Boolean _compressed = null;
    private List<ResourceSpec> _resourceSpecs = new ArrayList<ResourceSpec>();
    private Set<String> _compressedSuffixes = new HashSet<String>(DEFAULT_COMPRESS_SUFFIXES);
    private Set<String> _mergedSuffixes = new HashSet<String>(DEFAULT_MERGE_SUFFIXES);
    private Locale _locale;
    private String _style;
    private Boolean _minifyJs;
    private Boolean _minifyCss;
    private boolean _mountRedirect = true;
    private Class<?> _mountScope;
    private Boolean _merge;
    private IResourcePreProcessor _preProcessor;
    private SuffixMismatchStrategy _suffixMismatchStrategy = SuffixMismatchStrategy.EXCEPTION;

    /**
     * Mount wicket-event.js and wicket-ajax.js using wicket's version for
     * aggressive caching (e.g. wicket-ajax-1.3.6.js)
     * 
     * @param mountPrefix
     *            e.g. "script" for "/script/wicket-ajax-1.3.6.js
     * @param application
     *            the application
     */
    public static void mountWicketResources(String mountPrefix, WebApplication application) {
        mountWicketResources(mountPrefix, application, new ResourceMount().setDefaultAggressiveCacheDuration());
    }

    /**
     * Mount wicket-event.js and wicket-ajax.js using wicket's version (e.g.
     * wicket-ajax-1.3.6.js).
     * 
     * @param mountPrefix
     *            e.g. "script" for "/script/wicket-ajax-1.3.6.js
     * @param application
     *            the application
     * @param mount
     *            pre-configured resource mount to use. ResourceVersionProvider
     *            will be overriden
     */
    public static void mountWicketResources(String mountPrefix, WebApplication application, ResourceMount mount) {
        mount = mount.clone().setResourceVersionProvider(new WicketVersionProvider(application))
                .setDefaultAggressiveCacheDuration();

        if (!mountPrefix.endsWith("/")) {
            mountPrefix = mountPrefix + "/";
        }

        for (ResourceReference ref : new ResourceReference[] { WicketAjaxReference.INSTANCE,
                WicketEventReference.INSTANCE }) {
            String path = mountPrefix + ref.getName();

            mount.clone().setPath(path).addResourceSpec(ref).mount(application);
        }
    }

    /**
     * Mount wicket-event.js and wicket-ajax.js merged using wicket's version
     * for aggressive caching (e.g. wicket-1.4.7.js)
     * 
     * @param mountPrefix
     *            e.g. "script" for "/script/wicket-1.4.7.js
     * @param application
     *            the application
     */
    public static void mountWicketResourcesMerged(String mountPrefix, WebApplication application) {
        mountWicketResourcesMerged(mountPrefix, application,
                new ResourceMount().setDefaultAggressiveCacheDuration());
    }

    /**
     * Mount wicket-event.js and wicket-ajax.js merged using wicket's version
     * (e.g. wicket-1.4.7.js).
     * 
     * @param mountPrefix
     *            e.g. "script" for "/script/wicket-1.4.7.js
     * @param application
     *            the application
     * @param mount
     *            pre-configured resource mount to use. ResourceVersionProvider
     *            and Merged will be overridden
     */
    public static void mountWicketResourcesMerged(String mountPrefix, WebApplication application,
            ResourceMount mount) {
        if (!mountPrefix.endsWith("/")) {
            mountPrefix = mountPrefix + "/";
        }

        mount = mount.clone().setResourceVersionProvider(new WicketVersionProvider(application))
                .setPath(mountPrefix + "wicket.js").setMerged(true);

        for (ResourceReference ref : new ResourceReference[] { WicketEventReference.INSTANCE,
                WicketAjaxReference.INSTANCE }) {
            mount.addResourceSpec(ref);
        }

        mount.mount(application);
    }

    /**
     * enable annotation based adding of resources and make sure that the
     * component instantiation listener is only added once
     * 
     * @param application
     * @see JsContribution
     * @see CssContribution
     */
    public static void enableAnnotations(WebApplication application) {
        Boolean enabled = application.getMetaData(ANNOTATIONS_ENABLED_KEY);
        if (!Boolean.TRUE.equals(enabled)) {
            try {
                Class.forName("org.wicketstuff.config.MatchingResources");
                Class.forName("org.springframework.core.io.support.PathMatchingResourcePatternResolver");
            } catch (ClassNotFoundException e) {
                throw new WicketRuntimeException(
                        "in order to enable wicketstuff-merged-resources' annotation support, "
                                + "wicketstuff-annotations and spring-core must be on the path "
                                + "(see http://wicketstuff.org/confluence/display/STUFFWIKI/wicketstuff-annotation for details)");
            }
            application.addComponentInstantiationListener(new ContributionInjector());
            application.setMetaData(ANNOTATIONS_ENABLED_KEY, Boolean.TRUE);
        }
    }

    /**
     * @see #mountAnnotatedPackageResources(String, String, WebApplication,
     *      ResourceMount)
     */
    public static void mountAnnotatedPackageResources(String mountPrefix, Class<?> scope,
            WebApplication application, ResourceMount mount) {
        mountAnnotatedPackageResources(mountPrefix, scope.getPackage(), application, mount);
    }

    /**
     * @see #mountAnnotatedPackageResources(String, String, WebApplication,
     *      ResourceMount)
     */
    public static void mountAnnotatedPackageResources(String mountPrefix, Package pkg, WebApplication application,
            ResourceMount mount) {
        mountAnnotatedPackageResources(mountPrefix, pkg.getName(), application, mount);
    }

    /**
     * mount annotated resources from the given package. resources are mounted
     * beyond the given pathPrefix if resource scope doesn't start with /
     * itself.
     * 
     * @param mount
     *            a preconfigured ResourceMount, won't be changed
     * @param pathPrefix
     *            pathPrefix to mount resources
     * @param packageName
     *            the scanned package
     * @param application
     * 
     * @see JsContribution
     * @see CssContribution
     */
    public static void mountAnnotatedPackageResources(String mountPrefix, String packageName,
            WebApplication application, ResourceMount mount) {
        enableAnnotations(application);

        if (Strings.isEmpty(mountPrefix)) {
            mountPrefix = "/";
        }
        if (!mountPrefix.endsWith("/")) {
            mountPrefix += "/";
        }
        if (!mountPrefix.startsWith("/")) {
            mountPrefix = "/" + mountPrefix;
        }

        for (Map.Entry<String, SortedSet<WeightedResourceSpec>> e : new ContributionScanner(packageName)
                .getContributions().entrySet()) {
            String path = e.getKey();
            if (Strings.isEmpty(path)) {
                throw new WicketRuntimeException("path must not be empty");
            }
            SortedSet<WeightedResourceSpec> specs = e.getValue();

            if (specs.size() > 0) {
                ResourceMount m = mount.clone();
                m.setRequireVersion(false); // TODO do something smarter to
                // allow images etc
                m.setPath(path.startsWith("/") ? path : mountPrefix + path);
                m.addResourceSpecs(specs);
                m.mount(application);
            }
        }
    }

    /**
     * @param path
     * @return everything after last dot '.', ignoring anything before last
     *         slash '/' and leading dots '.' ; <code>null</code> if suffix is
     *         empty
     */
    public static String getSuffix(String path) {
        if (path == null) {
            return null;
        }
        int slash = path.lastIndexOf('/');
        if (slash >= 0) {
            path = path.substring(slash + 1);
        }
        while (path.startsWith(".")) {
            path = path.substring(1);
        }

        int dot = path.lastIndexOf('.');
        if (dot >= 0 && dot < path.length() - 1) {
            return path.substring(dot + 1);
        }
        return null;
    }

    /**
     * set {@link ICssCompressor} used by {@link CompressedMergedCssResource}
     * 
     * @param application
     * @param compressor
     */
    public static void setCssCompressor(Application application, ICssCompressor compressor) {
        application.setMetaData(CSS_COMPRESSOR_KEY, compressor);
    }

    /**
     * get {@link ICssCompressor} used by {@link CompressedMergedCssResource}
     * 
     * @param application
     */
    public static ICssCompressor getCssCompressor(Application application) {
        return application.getMetaData(CSS_COMPRESSOR_KEY);
    }

    /**
     * Create a new ResourceMount with default settings
     */
    public ResourceMount() {
        this(false);
    }

    /**
     * If dCreate a new ResourceMount with default settings
     * 
     * @param development
     *            <code>true</code> if ResourceMount should be configured with
     *            developer-friendly defaults: no caching, no merging, no minify
     */
    public ResourceMount(boolean development) {
        if (development) {
            setCacheDuration(0);
            setMerged(false);
            setMinifyCss(false);
            setMinifyJs(false);
        }
    }

    /**
     * @param compressed
     *            whether this resources should be compressed. default is
     *            autodetect
     * @return this
     * @see ResourceMount#autodetectCompression()
     */
    public ResourceMount setCompressed(boolean compressed) {
        _compressed = compressed;
        return this;
    }

    /**
     * autodetect whether this resource should be compressed using suffix of
     * file name (e.g. ".css") Behavior might be overriden in
     * {@link #doCompress(String)}
     * 
     * @return this
     * @see ResourceMount#setCompressed(boolean)
     */
    public ResourceMount autodetectCompression() {
        _compressed = null;
        return this;
    }

    /**
     * @param merge
     *            whether all {@link ResourceSpec}s should be merged to a single
     *            resource. default is autodetect
     * @return this
     * @see ResourceMount#autodetectMerging()
     */
    public ResourceMount setMerged(boolean merge) {
        _merge = merge;
        return this;
    }

    /**
     * autodetect whether this resource should be merged using suffix of file
     * name (e.g. ".js")
     * 
     * @return this
     * @see #setMerged(boolean)
     */
    public ResourceMount autodetectMerging() {
        _merge = null;
        return this;
    }

    /**
     * force a resource version, any {@link IResourceVersionProvider} (
     * {@link #setResourceVersionProvider(IResourceVersionProvider)}) will be
     * ignored. default is <code>null</code>
     * 
     * @param version
     *            version
     * @return this
     */
    public ResourceMount setVersion(AbstractResourceVersion version) {
        _version = version;
        return this;
    }

    /**
     * same as passing {@link AbstractResourceVersion#NO_VERSION} to
     * {@link #setVersion(AbstractResourceVersion)}
     * 
     * @return this
     * @see #setVersion(AbstractResourceVersion)
     */
    public ResourceMount setNoVersion() {
        return setVersion(AbstractResourceVersion.NO_VERSION);
    }

    /**
     * same as passing <code>null</code> to
     * {@link #setVersion(AbstractResourceVersion)}
     * 
     * @return this
     * @see #setVersion(AbstractResourceVersion)
     */
    public ResourceMount autodetectVersion() {
        return setVersion(null);
    }

    /**
     * force a minimal version. default is <code>null</code>
     * 
     * @param minVersion
     * @return this
     */
    public ResourceMount setMinVersion(AbstractResourceVersion minVersion) {
        _minVersion = minVersion;
        return this;
    }

    /**
     * Convenience method to use a {@link SimpleResourceVersion} as minVersion
     * (e.g. suitable for {@link RevisionVersionProvider})
     * 
     * @param minVersionValue
     *            the minimal version
     * @return this
     */
    public ResourceMount setMinVersion(int minVersionValue) {
        return setMinVersion(new SimpleResourceVersion(minVersionValue));
    }

    /**
     * unset minimal version, same as passing <code>null</code> to
     * {@link #setMinVersion(AbstractResourceVersion)}
     * 
     * @return this
     */
    public ResourceMount unsetMinVersion() {
        return setMinVersion(null);
    }

    /**
     * {@link IResourceVersionProvider} might not always be able to detect the
     * version of a resource. This might be ignored or cause an error depending.
     * default is to cause an error (<code>true</code>)
     * 
     * @param requireVersion
     *            whether version is required (<code>true</code>) or not
     *            (<code>false</code>). default is <code>true</code>
     * @return this
     */
    public ResourceMount setRequireVersion(boolean requireVersion) {
        _requireVersion = requireVersion;
        return this;
    }

    /**
     * the path to user for mounting. this might either be a prefix if multiple
     * resources are mounted or the full name. if used as prefix,
     * {@link ResourceSpec#getFile()} is appended
     * 
     * @param path
     *            name or prefix for mount, with or without leading or trailing
     *            slashes
     * @return this
     */
    public ResourceMount setPath(String path) {
        if (path != null) {
            path = path.trim();
            if ("".equals(path) || "/".equals(path)) {
                throw new IllegalArgumentException("path must not be empty or '/', was " + path);
            }
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (path.endsWith("/")) {
                path = path.substring(0, path.length() - 1);
            }
        }
        _path = path;
        return this;
    }

    /**
     * convenience method to use {@link #setPath(String)} use a prefix and
     * {@link ResourceReference#getName()}.
     * 
     * @param prefix
     *            path prefix prefix for mount, with or without leading or
     *            trailing slashes
     * @param ref
     *            a {@link ResourceReference}
     * @return this
     */
    public ResourceMount setPath(String prefix, ResourceReference ref) {
        if (!prefix.endsWith("/")) {
            prefix = prefix + "/";
        }
        return setPath(prefix + ref.getName());
    }

    /**
     * convenience method to use {@link #setPath(String)} use a prefix and
     * {@link ResourceReference#getName()}.
     * 
     * @param prefix
     *            path prefix prefix for mount, with or without leading or
     *            trailing slashes
     * @param ref
     *            a {@link ResourceReference}
     * @param suffix
     *            suffix to append after {@link ResourceReference#getName()},
     *            might be null
     * @return this
     */
    public ResourceMount setPath(String prefix, ResourceReference ref, String suffix) {
        return setPath(prefix, ref.getName(), suffix);
    }

    /**
     * convenience method to use {@link #setPath(String)} use a prefix and a
     * name
     * 
     * @param prefix
     *            path prefix prefix for mount, with or without leading or
     *            trailing slashes
     * @param name
     *            a name
     * @param suffix
     *            suffix to append after {@link ResourceReference#getName()},
     *            might be null
     * @return this
     */
    public ResourceMount setPath(String prefix, String name, String suffix) {
        if (!prefix.endsWith("/")) {
            prefix = prefix + "/";
        }
        if (Strings.isEmpty(suffix)) {
            suffix = "";
        } else if (!suffix.startsWith(".") && !suffix.startsWith("-")) {
            suffix = "." + suffix;
        }
        return setPath(prefix + name + suffix);
    }

    /**
     * @param mountRedirect
     *            whether a redirected should be mounted from the unversioned
     *            path to the versioned path (only used if there is a version).
     *            default is <code>true</code>
     * @return this
     */
    public ResourceMount setMountRedirect(boolean mountRedirect) {
        _mountRedirect = mountRedirect;
        return this;
    }

    /**
     * Locale might either be detected from added {@link ResourceSpec}s or set
     * manually.
     * 
     * @param locale
     *            Locale for mounted resources
     * @return this
     * @see {@link ResourceReference#setLocale(Locale)}
     */
    public ResourceMount setLocale(Locale locale) {
        _locale = locale;
        return this;
    }

    /**
     * Autodetect the locale. Same as passing <code>null</code> to
     * {@link #setLocale(Locale)}
     * 
     * @return this
     */
    public ResourceMount autodetectLocale() {
        return setLocale(null);
    }

    /**
     * Style might either be detected from added {@link ResourceSpec}s or set
     * manually.
     * 
     * @param style
     *            Style for mounted resources
     * @return this
     * @see {@link ResourceReference#setStyle(String)}
     */
    public ResourceMount setStyle(String style) {
        _style = style;
        return this;
    }

    /**
     * Autodetect the style. Same as passing <code>null</code> to
     * {@link #setStyle(String)}
     * 
     * @return this
     */
    public ResourceMount autodetectStyle() {
        return setStyle(null);
    }

    /**
     * Set cache duration in seconds. default is autodetect ({@link
     * <code>null</code>}). Must be >= 0
     * 
     * @param cacheDuration
     * @return this
     * @see #autodetectCacheDuration()
     */
    public ResourceMount setCacheDuration(int cacheDuration) {
        if (cacheDuration < 0) {
            throw new IllegalArgumentException("cacheDuration must not be < 0, was " + cacheDuration);
        }
        _cacheDuration = cacheDuration;
        return this;
    }

    /**
     * Same as passing {@link ResourceMount#DEFAULT_CACHE_DURATION} to
     * {@link #setCacheDuration(int)}
     * 
     * @return this
     */
    public ResourceMount setDefaultCacheDuration() {
        return setCacheDuration(DEFAULT_CACHE_DURATION);
    }

    /**
     * Same as passing {@link ResourceMount#DEFAULT_AGGRESSIVE_CACHE_DURATION}
     * to {@link #setCacheDuration(int)}
     * 
     * @return this
     */
    public ResourceMount setDefaultAggressiveCacheDuration() {
        return setCacheDuration(DEFAULT_AGGRESSIVE_CACHE_DURATION);
    }

    /**
     * @deprecated typo in name, it's aggressive with ss
     * @see #setDefaultAggressiveCacheDuration()
     */
    @Deprecated
    public ResourceMount setDefaultAggresiveCacheDuration() {
        return setCacheDuration(DEFAULT_AGGRESSIVE_CACHE_DURATION);
    }

    /**
     * autodetect cache duration: use minimum of all resource specs or
     * {@link ResourceMount#DEFAULT_CACHE_DURATION} if not available. Behavior
     * might be overriden using {@link #getCacheDuration()}
     * 
     * @return this
     */
    public ResourceMount autodetectCacheDuration() {
        _cacheDuration = null;
        return this;
    }

    /**
     * Set the {@link IResourceVersionProvider} to use for
     * {@link AbstractResourceVersion} detection
     * 
     * @param resourceVersionProvider
     *            the resource version provider
     * @return this
     */
    public ResourceMount setResourceVersionProvider(IResourceVersionProvider resourceVersionProvider) {
        _resourceVersionProvider = resourceVersionProvider;
        return this;
    }

    /**
     * @param minifyJs
     *            whether js should be minified (<code>true</code>) or not
     *            (<code>false</code>). Default is autodetect
     * @return this
     * @see #autodetectMinifyJs()
     */
    public ResourceMount setMinifyJs(Boolean minifyJs) {
        _minifyJs = minifyJs;
        return this;
    }

    /**
     * Autodetect wheter resource should be minified using a JS compressor.
     * Default is to minify files ending with .js. Behavior might be overriden
     * using {@link #doMinifyJs(String)}
     * 
     * @return this
     */
    public ResourceMount autodetectMinifyJs() {
        _minifyJs = null;
        return this;
    }

    /**
     * @param minifyCss
     *            whether css should be minified (<code>true</code>) or not
     *            (<code>false</code>). Default is autodetect
     * @return this
     * @see #autodetectMinifyCss()
     */
    public ResourceMount setMinifyCss(Boolean minifyCss) {
        _minifyCss = minifyCss;
        return this;
    }

    /**
     * Autodetect wheter resource should be minified using a CSS compressor.
     * Default is to minify files ending with .css. Behavior might be overriden
     * using {@link #doMinifyCss(String)}
     * 
     * @return this
     */
    public ResourceMount autodetectMinifyCss() {
        _minifyCss = null;
        return this;
    }

    /**
     * The mount scope to use. default is autodetect (<code>null</code>)
     * 
     * @param mountScope
     *            mount scope
     * @return this
     * @see ResourceReference#getScope()
     * @see #autodetectMountScope()
     */
    public ResourceMount setMountScope(Class<?> mountScope) {
        _mountScope = mountScope;
        return this;
    }

    /**
     * Same as passing <code>null</code> to {@link #setMountScope(Class)}.
     * Autodetect: either use the scope that all (merged) resources are using or
     * use {@link ResourceMount} as mount scope.
     * 
     * @return this
     */
    public ResourceMount autodetectMountScope() {
        return setMountScope(null);
    }

    /**
     * @return the current {@link IResourcePreProcessor}
     */
    public IResourcePreProcessor getPreProcessor() {
        return _preProcessor;
    }

    /**
     * use an {@link IResourcePreProcessor} to modify resources (e.g. replace
     * properties, change relative to absolute paths, ...)
     * 
     * @param preProcessor
     * @return this
     */
    public ResourceMount setPreProcessor(IResourcePreProcessor preProcessor) {
        _preProcessor = preProcessor;
        return this;
    }

    /**
     * @return current suffixMismatchStrategy
     */
    public SuffixMismatchStrategy getSuffixMismatchStrategy() {
        return _suffixMismatchStrategy;
    }

    /**
     * @param suffixMismatchStrategy
     *            the new strategy
     * @return this
     */
    public ResourceMount setSuffixMismatchStrategy(SuffixMismatchStrategy suffixMismatchStrategy) {
        if (suffixMismatchStrategy == null) {
            throw new NullPointerException("suffixMismatchStrategy");
        }
        _suffixMismatchStrategy = suffixMismatchStrategy;
        return this;
    }

    /**
     * @param resourceSpec
     *            add a new {@link ResourceSpec}
     * @return this
     */
    public ResourceMount addResourceSpec(ResourceSpec resourceSpec) {
        if (_resourceSpecs.contains(resourceSpec)) {
            throw new IllegalArgumentException("aleady added: " + resourceSpec);
        }
        _resourceSpecs.add(resourceSpec);
        return this;
    }

    /**
     * add a new {@link ResourceSpec} with this scope and name
     * 
     * @param scope
     *            scope
     * @param name
     *            name
     * @return this
     */
    public ResourceMount addResourceSpec(Class<?> scope, String name) {
        return addResourceSpec(new ResourceSpec(scope, name));
    }

    /**
     * add a new {@link ResourceSpec} with this scope and each name
     * 
     * @param scope
     *            scope
     * @param names
     *            names
     * @return this
     */
    public ResourceMount addResourceSpecs(Class<?> scope, String... names) {
        for (String name : names) {
            addResourceSpec(new ResourceSpec(scope, name));
        }
        return this;
    }

    /**
     * add a new {@link ResourceSpec} with this scope, name, locale, style and
     * cacheDuration
     * 
     * @param scope
     *            scope
     * @param name
     *            name
     * @param locale
     *            locale
     * @param style
     *            style
     * @param cacheDuration
     *            cache duration
     * @return this
     */
    public ResourceMount addResourceSpec(Class<?> scope, String name, Locale locale, String style,
            Integer cacheDuration) {
        return addResourceSpec(new ResourceSpec(scope, name, locale, style, cacheDuration));
    }

    /**
     * add all resource specs
     * 
     * @param resourceSpecs
     *            array of {@link ResourceSpec}s to add
     * @return this
     */
    public ResourceMount addResourceSpecs(ResourceSpec... resourceSpecs) {
        return addResourceSpecs(Arrays.asList(resourceSpecs));
    }

    /**
     * add all resource specs
     * 
     * @param resourceSpecs
     *            {@link Iterable} of {@link ResourceSpec}s to add
     * @return this
     */
    public ResourceMount addResourceSpecs(Iterable<? extends ResourceSpec> resourceSpecs) {
        for (ResourceSpec resourceSpec : resourceSpecs) {
            addResourceSpec(resourceSpec);
        }
        return this;
    }

    /**
     * Adds a resource spec for a resource with the same name as the scope,
     * adding a suffix. Example: if scope is Foo.class and suffix is "js", name
     * will be "Foo.js"
     * 
     * @param scope
     *            the scope
     * @param suffix
     *            the suffix
     * @return this
     */
    public ResourceMount addResourceSpecMatchingSuffix(Class<?> scope, String suffix) {
        if (!suffix.startsWith(".") && !suffix.startsWith("-")) {
            suffix = "." + suffix;
        }
        return addResourceSpec(new ResourceSpec(scope, scope.getSimpleName() + suffix));
    }

    /**
     * same as {@link #addResourceSpecMatchingSuffix(Class, String)} but using
     * multiple suffixes
     * 
     * @param scope
     *            the scope
     * @param suffixes
     *            the suffixes
     * @return this
     */
    public ResourceMount addResourceSpecsMatchingSuffixes(Class<?> scope, String... suffixes) {
        return addResourceSpecsMatchingSuffix(scope, Arrays.asList(suffixes));
    }

    /**
     * same as {@link #addResourceSpecMatchingSuffix(Class, String)} but using
     * multiple suffixes
     * 
     * @param scope
     *            the scope
     * @param suffixes
     *            the suffixes
     * @return this
     */
    public ResourceMount addResourceSpecsMatchingSuffix(Class<?> scope, Iterable<String> suffixes) {
        for (String suffix : suffixes) {
            addResourceSpecMatchingSuffix(scope, suffix);
        }
        return this;
    }

    /**
     * uses the path (set by {@link #setPath(String)}) to obtain a suffix to use
     * with {@link #addResourceSpecMatchingSuffix(Class, String)}
     * 
     * @param scopes
     * @return this
     */
    public ResourceMount addResourceSpecsMatchingSuffix(Class<?>... scopes) {
        return addResourceSpecsMatchingSuffix(getSuffix(_path), scopes);
    }

    public ResourceMount addResourceSpecsMatchingSuffix(String suffix, Class<?>... scopes) {
        if (_path == null) {
            throw new IllegalStateException("unversionPath must be set for this method to work");
        }
        if (Strings.isEmpty(suffix) || suffix.contains("/")) {
            throw new IllegalStateException(
                    "unversionPath does not have a valid suffix (i.e. does not contain a '.' followed by characterers and no '/')");
        }
        for (Class<?> scope : scopes) {
            addResourceSpecMatchingSuffix(scope, suffix);
        }
        return this;
    }

    /**
     * add a {@link ResourceSpec} using a {@link ResourceReference}
     * 
     * @param ref
     *            the {@link ResourceReference}
     * @return this
     */
    public ResourceMount addResourceSpec(ResourceReference ref) {
        return addResourceSpec(new ResourceSpec(ref));
    }

    /**
     * add a {@link ResourceSpec} for each {@link ResourceReference}
     * 
     * @param refs
     *            the {@link ResourceReference}s
     * @return this
     */
    public ResourceMount addResourceSpecs(ResourceReference... refs) {
        for (ResourceReference ref : refs) {
            addResourceSpec(ref);
        }
        return this;
    }

    /**
     * mount the {@link ResourceSpec}(s) added either as a single
     * {@link Resource} or multiple Resource, depending on {@link #doMerge()}.
     * Might also mount a redirect for versioned path names. (e.g. from
     * "/script/wicket-ajax.js" to "/script/wicket-ajax-1.3.6.js")
     * 
     * @param application
     *            the application
     * @return this
     */
    public ResourceMount mount(WebApplication application) {
        build(application);
        return this;
    }

    /**
     * same as {@link #mount(WebApplication)}, but returns an
     * {@link AbstractHeaderContributor} to use in components
     * 
     * @param application
     *            the application
     * @return {@link AbstractHeaderContributor} to be used in components
     */
    public AbstractHeaderContributor build(final WebApplication application) {
        return build(application, null);
    }

    /**
     * same as {@link #mount(WebApplication)}, but returns an
     * {@link AbstractHeaderContributor} to use in components
     * 
     * @param application
     *            the application
     * @param cssMediaType
     *            CSS media type, e.g. "print" or <code>null</code> for no media
     *            type
     * @return {@link AbstractHeaderContributor} to be used in components, all
     *         files ending with '.css' will be rendered with passed
     *         cssMediaType
     */
    public AbstractHeaderContributor build(final WebApplication application, String cssMediaType) {
        if (_resourceSpecs.size() == 0) {
            // nothing to do
            return null;
        }

        try {
            List<Pair<String, ResourceSpec[]>> specsList;

            boolean merge = doMerge();
            if (merge) {
                specsList = new ArrayList<Pair<String, ResourceSpec[]>>(1);
                specsList.add(new Pair<String, ResourceSpec[]>(null, getResourceSpecs()));
            } else {
                specsList = new ArrayList<Pair<String, ResourceSpec[]>>(_resourceSpecs.size());
                for (ResourceSpec spec : _resourceSpecs) {
                    specsList.add(new Pair<String, ResourceSpec[]>(
                            _resourceSpecs.size() > 1 ? spec.getFile() : null, new ResourceSpec[] { spec }));
                }
            }

            final List<ResourceReference> refs = new ArrayList<ResourceReference>(specsList.size());
            for (Pair<String, ResourceSpec[]> p : specsList) {
                ResourceSpec[] specs = p.getSecond();

                String path = getPath(p.getFirst(), specs);
                String unversionedPath = getPath(p.getFirst(), null);

                checkSuffixes(unversionedPath, Arrays.asList(specs));

                boolean versioned = !unversionedPath.equals(path);

                String name = specs.length == 1 ? specs[0].getFile() : unversionedPath;

                final ResourceReference ref = newResourceReference(getScope(specs), name, getLocale(specs),
                        getStyle(specs), getCacheDuration(specs, versioned), specs, _preProcessor);
                refs.add(ref);
                ref.bind(application);
                application.mount(newStrategy(path, ref, merge));

                if (_mountRedirect && versioned) {
                    application.mount(newRedirectStrategy(unversionedPath, path));
                }

                initResource(ref);
            }
            return newHeaderContributor(refs, cssMediaType);
        } catch (VersionException e) {
            throw new WicketRuntimeException("failed to mount resource ('" + _path + "')", e);
        } catch (IncompatibleVersionsException e) {
            throw new WicketRuntimeException("failed to mount resource ('" + _path + "')", e);
        } catch (ResourceStreamNotFoundException e) {
            throw new WicketRuntimeException("failed to mount resource ('" + _path + "')", e);
        }
    }

    /**
     * @param refs
     *            a list of ResourceReferences
     * @return an {@link AbstractHeaderContributor} that renders references to
     *         all CSS and JS resources contained in refs
     */
    protected AbstractHeaderContributor newHeaderContributor(final List<ResourceReference> refs,
            String cssMediaType) {
        return new MergedHeaderContributor(refs, cssMediaType);
    }

    /**
     * load resource stream once in order to load it into memory
     * 
     * @param ref
     * @throws ResourceStreamNotFoundException
     */
    private void initResource(final ResourceReference ref) throws ResourceStreamNotFoundException {
        boolean gzip = Application.get().getResourceSettings().getDisableGZipCompression();
        try {
            Application.get().getResourceSettings().setDisableGZipCompression(true);
            ref.getResource().getResourceStream().getInputStream();
        } finally {
            Application.get().getResourceSettings().setDisableGZipCompression(gzip);
        }
    }

    /**
     * create a new {@link IRequestTargetUrlCodingStrategy}
     * 
     * @param mountPath
     *            the mount path
     * @param ref
     *            the {@link ResourceReference}
     * @param merge
     *            if <code>true</code>, all resources obtained by
     *            {@link #getResourceSpecs()} should be merged
     * @return this
     */
    protected IRequestTargetUrlCodingStrategy newStrategy(String mountPath, final ResourceReference ref,
            boolean merge) {
        if (merge) {
            final ArrayList<String> mergedKeys = new ArrayList<String>(_resourceSpecs.size());
            for (ResourceSpec spec : _resourceSpecs) {
                mergedKeys.add(new ResourceReference(spec.getScope(), spec.getFile()) {

                    private static final long serialVersionUID = 1L;

                    @Override
                    protected Resource newResource() {
                        Resource r = ref.getResource();
                        if (r == null) {
                            throw new WicketRuntimeException("ResourceReference wasn't bound to application yet");
                        }
                        return r;
                    }

                }.getSharedResourceKey());
            }
            return new MergedResourceRequestTargetUrlCodingStrategy(mountPath, ref.getSharedResourceKey(),
                    mergedKeys);
        } else {
            return new SharedResourceRequestTargetUrlCodingStrategy(mountPath, ref.getSharedResourceKey());
        }
    }

    /**
     * create a new {@link IRequestTargetUrlCodingStrategy} to redirect from
     * mountPath to redirectPath
     * 
     * @param mountPath
     *            the path to redirect from
     * @param redirectPath
     *            the path to redirect to
     * @return a new {@link IRequestTargetUrlCodingStrategy}
     */
    protected IRequestTargetUrlCodingStrategy newRedirectStrategy(String mountPath, String redirectPath) {
        return new RedirectStrategy(mountPath, redirectPath);
    }

    /**
     * @return the path, same as passing <code>null</code> and <code>null</code>
     *         to {@link #getPath(String, ResourceSpec[])}
     * @throws VersionException
     *             if version can't be found
     * @throws IncompatibleVersionsException
     *             if versions can't be compared
     * @see #getPath(String, boolean)
     */
    public final String getPath() throws VersionException, IncompatibleVersionsException {
        return getPath(null, null);
    }

    /**
     * @param appendName
     * @return the path, same as passing <code>appendName</code> and
     *         <code>null</code> to {@link #getPath(String, ResourceSpec[])}
     * @throws VersionException
     *             if version can't be found
     * @throws IncompatibleVersionsException
     *             if versions can't be compared
     * @see #getPath(String, boolean)
     */
    public final String getPath(String appendName) throws VersionException, IncompatibleVersionsException {
        return getPath(appendName, null);
    }

    /**
     * @param appendName
     *            the name to append after path
     * @param specs
     *            a list of specs to get the version from or null
     * @return the path
     * @throws VersionException
     *             if version can't be found
     * @throws IncompatibleVersionsException
     *             if versions can't be compared
     * @throws IllegalStateException
     *             if path not set
     */
    public String getPath(String appendName, ResourceSpec[] specs)
            throws VersionException, IncompatibleVersionsException, IllegalStateException {
        if (_path == null) {
            throw new IllegalStateException("path must be set");
        }

        String path = _path;
        if (appendName != null) {
            if (!path.endsWith("/")) {
                path = path + "/";
            }
            path = path + appendName;
        }

        if (specs != null && specs.length > 0) {
            AbstractResourceVersion version = getVersion(specs);
            if (version != null && version.isValid()) {
                return buildVersionedPath(path, version);
            }
        }

        return path;
    }

    /**
     * create a versioned path out of the given path and the version. default is
     * to append the version after a '-' in front of the last '.' in the path.
     * (e.g. wicket-ajax-1.3.6.js) if there is no '.' in the path or only at the
     * beginning, a '-' and the version will be appended (e.g. foobar-1.3.6 or
     * .something-1.3.6
     * 
     * @param path
     *            the path
     * @param version
     *            the version. must not be null but may be invalid (check
     *            version.isValid()!)
     * @return the versioned path
     */
    protected String buildVersionedPath(String path, AbstractResourceVersion version) {
        if (!version.isValid()) {
            return path;
        }
        int idx = path.lastIndexOf('.');
        if (idx > 0) {
            return path.substring(0, idx) + "-" + version.getVersion() + path.substring(idx);
        } else {
            return path + "-" + version.getVersion();
        }
    }

    /**
     * detect the version. default implementation is to use the manually set
     * version or detect it using {@link IResourceVersionProvider} from all
     * specs.
     * 
     * @param specs
     *            the specs to detect the version from
     * @return the version
     * @throws VersionException
     *             If a version can't be determined from any resource and
     *             version is required ({@link #setRequireVersion(boolean)})
     * @throws IncompatibleVersionsException
     *             if versions can't be compared
     */
    protected AbstractResourceVersion getVersion(ResourceSpec[] specs)
            throws VersionException, IncompatibleVersionsException {
        if (_version != null) {
            return _version;
        }

        if (_resourceVersionProvider != null) {
            AbstractResourceVersion max = _minVersion;
            for (ResourceSpec spec : specs) {
                try {
                    AbstractResourceVersion version = _resourceVersionProvider.getVersion(spec.getScope(),
                            spec.getFile());
                    if (max == null || version.compareTo(max) > 0) {
                        max = version;
                    }
                } catch (VersionException e) {
                    if (_requireVersion) {
                        throw e;
                    }
                }
            }
            return max;
        }

        return null;
    }

    /**
     * get the mount scope. Either use the manually set scope (
     * {@link #setMountScope(Class)} or detect it. Default is to use the scope
     * of all specs if it is common or use {@link ResourceMount}
     * 
     * @param specs
     *            the specs to obtain the scope for
     * @return the scope
     */
    protected Class<?> getScope(ResourceSpec[] specs) {
        if (_mountScope != null) {
            return _mountScope;
        } else {
            Class<?> scope = null;
            for (ResourceSpec resourceSpec : specs) {
                if (scope == null) {
                    scope = resourceSpec.getScope();
                } else if (!scope.equals(resourceSpec.getScope())) {
                    scope = null;
                    break;
                }
            }
            if (scope != null) {
                return scope;
            }
        }

        return ResourceMount.class;
    }

    /**
     * create a new {@link ResourceReference}
     * 
     * @param scope
     *            scope
     * @param name
     *            name
     * @param locale
     *            locale
     * @param style
     *            style
     * @param cacheDuration
     *            cache duration
     * @param resourceSpecs
     *            resource specs
     * @return a new {@link ResourceReference}
     */
    protected ResourceReference newResourceReference(Class<?> scope, final String name, Locale locale, String style,
            int cacheDuration, ResourceSpec[] resourceSpecs, IResourcePreProcessor preProcessor) {
        ResourceReference ref;
        if (resourceSpecs.length > 1) {
            if (doCompress(name)) {
                if (doMinifyCss(name)) {
                    ref = new CompressedMergedCssResourceReference(name, locale, style, resourceSpecs,
                            cacheDuration, preProcessor);
                } else if (doMinifyJs(name)) {
                    ref = new CompressedMergedJsResourceReference(name, locale, style, resourceSpecs, cacheDuration,
                            preProcessor);
                } else {
                    ref = new CompressedMergedResourceReference(name, locale, style, resourceSpecs, cacheDuration,
                            preProcessor);
                }
            } else {
                ref = new MergedResourceReference(name, locale, style, resourceSpecs, cacheDuration, preProcessor);
            }
        } else if (resourceSpecs.length == 1) {
            if (doCompress(name)) {
                if (doMinifyCss(name)) {
                    ref = new CachedCompressedCssResourceReference(scope, name, locale, style, cacheDuration,
                            preProcessor);
                } else if (doMinifyJs(name)) {
                    ref = new CachedCompressedJsResourceReference(scope, name, locale, style, cacheDuration,
                            preProcessor);
                } else {
                    ref = new CachedCompressedResourceReference(scope, name, locale, style, cacheDuration,
                            preProcessor);
                }
            } else {
                ref = new CachedResourceReference(scope, name, locale, style, cacheDuration, preProcessor);
            }
        } else {
            throw new IllegalArgumentException("can't create ResourceReference without ResourceSpec");
        }
        return ref;
    }

    /**
     * detect the locale to use. Either use a manually chosen one (
     * {@link #setLocale(Locale)}) or detect it from the given resource specs.
     * An {@link Exception} will be thrown if locales of added resources aren't
     * compatible. (e.g. 'de' and 'en'). The resource will always use the most
     * specific locale. For instance, if 5 resources are 'en' and one is
     * 'en_US', the locale will be 'en_US'
     * 
     * @param specs
     *            the {@link ResourceSpec}s to get the locale for
     * @return the locale
     */
    protected Locale getLocale(ResourceSpec[] specs) {
        if (_locale != null) {
            return _locale;
        }

        Locale locale = null;
        for (ResourceSpec spec : specs) {
            if (locale != null) {
                Locale newLocale = locale;
                if (spec.getLocale() != null) {
                    if (spec.getLocale().getLanguage() != null) {
                        newLocale = locale;
                        if (locale.getLanguage() != null
                                && !spec.getLocale().getLanguage().equals(locale.getLanguage())) {
                            throw new IllegalStateException(
                                    "languages aren't compatible: '" + locale + "' and '" + spec.getLocale() + "'");
                        }
                    }

                    if (spec.getLocale().getCountry() != null) {
                        if (locale.getCountry() != null
                                && !spec.getLocale().getCountry().equals(locale.getCountry())) {
                            throw new IllegalStateException(
                                    "countries aren't compatible: '" + locale + "' and '" + spec.getLocale() + "'");
                        }
                    } else if (locale.getCountry() != null) {
                        // keep old locale, as it is more restrictive
                        newLocale = locale;
                    }
                }
                locale = newLocale;
            } else {
                locale = spec.getLocale();
            }
        }
        return locale;
    }

    /**
     * detect the style to use. Default implementation is to either use a
     * manually chosen one ({@link #setStyle(String)}) or detect it from the
     * given resource specs. An {@link Exception} will be thrown if styles of
     * added resources aren't compatible. (e.g. 'foo' and 'bar', null and 'foo'
     * are considered compatible). The resource will always use a style if at
     * least one resource uses one. For instance, if 5 resources are don't have
     * a style and one has 'foo', the style will be 'foo'
     * 
     * @param specs
     *            the {@link ResourceSpec}s to get the style for
     * @return the style
     */
    protected String getStyle(ResourceSpec[] specs) {
        if (_style != null) {
            return _style;
        }

        String style = null;
        for (ResourceSpec spec : specs) {
            if (style != null) {
                if (spec.getStyle() != null && !spec.getStyle().equals(style)) {
                    throw new IllegalStateException(
                            "styles aren't compatible: '" + style + "' and '" + spec.getStyle() + "'");
                }
            } else {
                style = spec.getStyle();
            }
        }
        return style;
    }

    /**
     * detect the cache duration to use. Default implementation is to either use
     * a manually chosen one ({@link #setCacheDuration(int)}) or detect it from
     * the given resource specs. The resource will always use the lowest cache
     * duration or {@link ResourceMount#DEFAULT_CACHE_DURATION} if it can't be
     * detected
     * 
     * @param specs
     *            the {@link ResourceSpec}s to get the cache duration for
     * @param if the resource is versioned.
     * @return the cache duration in seconds
     */
    protected int getCacheDuration(ResourceSpec[] specs, boolean versioned) {
        if (_cacheDuration != null) {
            return _cacheDuration;
        }

        if (versioned) {
            return DEFAULT_AGGRESSIVE_CACHE_DURATION;
        }

        Integer cacheDuration = null;
        for (ResourceSpec spec : specs) {
            if (cacheDuration == null) {
                cacheDuration = spec.getCacheDuration();
            } else if (spec.getCacheDuration() != null && spec.getCacheDuration() < cacheDuration) {
                cacheDuration = spec.getCacheDuration();
            }
        }
        if (cacheDuration == null) {
            cacheDuration = DEFAULT_CACHE_DURATION;
        }
        return cacheDuration;
    }

    /**
     * @return the resource specs
     */
    protected ResourceSpec[] getResourceSpecs() {
        return _resourceSpecs.toArray(new ResourceSpec[_resourceSpecs.size()]);
    }

    /**
     * @param file
     *            a file name
     * @return whether this file should use gzip compression. default is to
     *         check the suffix of the file
     * @see #setCompressed(boolean)
     * @see #getCompressedSuffixes()
     */
    protected boolean doCompress(final String file) {
        return _compressed == null ? _compressedSuffixes.contains(getSuffix(file)) : _compressed;
    }

    /**
     * @param file
     *            a file name
     * @return whether this file should be processed by a JS compressor. default
     *         is to minify files ending with '.js'
     * @see #setMinifyJs(Boolean)
     */
    protected boolean doMinifyJs(final String file) {
        return _minifyJs == null ? file.endsWith(".js") : _minifyJs;
    }

    /**
     * @param file
     *            a file name
     * @return whether this file should be processed by a CSS compressor.
     *         default is to minify files ending with '.css'
     * @see #setMinifyJs(Boolean)
     */
    protected boolean doMinifyCss(final String file) {
        return _minifyCss == null ? file.endsWith(".css") : _minifyCss;
    }

    /**
     * should the added {@link ResourceSpec}s be merged to a single resource, or
     * should they be mounted idividually? default is to merge files ending with
     * {@link ResourceMount#DEFAULT_MERGE_SUFFIXES}
     * 
     * @return
     * @see #setMerged(boolean)
     * @see #autodetectMerging()
     * @see #getMergedSuffixes()
     */
    protected boolean doMerge() {
        return _merge == null ? _resourceSpecs.size() > 1 && _mergedSuffixes.contains(getSuffix(_path)) : _merge;
    }

    /**
     * clear all added {@link ResourceSpec}s
     * 
     * @return this
     */
    public ResourceMount clearSpecs() {
        _resourceSpecs.clear();
        return this;
    }

    /**
     * @return the set of suffixes that will be compressed by default
     * @see ResourceMount#DEFAULT_COMPRESS_SUFFIXES
     */
    public Set<String> getCompressedSuffixes() {
        return _compressedSuffixes;
    }

    /**
     * @return the set of suffixes that will be merged by default
     * @see ResourceMount#DEFAULT_MERGE_SUFFIXES
     */
    public Set<String> getMergedSuffixes() {
        return _mergedSuffixes;
    }

    /**
     * check if suffixes of path and each RespurceSpec file match
     * 
     * @throws WicketRuntimeException
     *             if suffixes don't match and strategy is
     *             {@link SuffixMismatchStrategy#EXCEPTION}
     */
    protected void checkSuffixes(String path, Iterable<ResourceSpec> specs) {
        String suffix;
        if (_suffixMismatchStrategy != SuffixMismatchStrategy.IGNORE && (suffix = getSuffix(path)) != null) {
            for (ResourceSpec spec : specs) {
                if (!Strings.isEqual(suffix, getSuffix(spec.getFile()))) {
                    onSuffixMismatch(path, spec.getFile());
                }
            }
        }
    }

    /**
     * apply {@link SuffixMismatchStrategy} without further checking, arguments
     * are for logging only
     * 
     * @throws WicketRuntimeException
     *             if suffixes don't match and strategy is
     *             {@link SuffixMismatchStrategy#EXCEPTION}
     */
    protected void onSuffixMismatch(String resource, String path) {
        switch (_suffixMismatchStrategy) {
        case EXCEPTION:
            throw new WicketRuntimeException(String.format("Suffixes don't match: %s %s", resource, path));
        case WARN:
            LOG.warn(String.format("Suffixes don't match: %s %s", resource, path));
            break;
        case IGNORE:
            break;
        default:
            throw new RuntimeException(
                    String.format("unimplemented suffixMismatchStrategy: %s", _suffixMismatchStrategy));
        }
    }

    /**
     * a copy of the resource mount, with unfolded collections of compressed
     * suffixes, merged suffices and {@link ResourceSpec}s
     */
    @Override
    public ResourceMount clone() {
        try {
            ResourceMount clone = (ResourceMount) super.clone();
            // copy collections
            clone._compressedSuffixes = new HashSet<String>(_compressedSuffixes);
            clone._mergedSuffixes = new HashSet<String>(_mergedSuffixes);
            clone._resourceSpecs = new ArrayList<ResourceSpec>(_resourceSpecs);
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new WicketRuntimeException("clone of Object not supported?", e);
        }
    }

}