Package org.wicketstuff.mergedresources

Source Code of org.wicketstuff.mergedresources.ResourceMount

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

}
TOP

Related Classes of org.wicketstuff.mergedresources.ResourceMount

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.