All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.wicketstuff.mergedresources.ResourceMount Maven / Gradle / Ivy

Go to download

Resource merging for Apache Wicket (http://wicket.apache.org), see http://talk-on-tech.blogspot.com/2008/08/wicket-interface-speed-up-merging.html

There is a newer version: 4.0.1
Show newest version
/**
 * 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 ANNOTATIONS_ENABLED_KEY = new MetaDataKey() {
		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 DEFAULT_COMPRESS_SUFFIXES = Collections.unmodifiableSet(new HashSet(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 DEFAULT_MERGE_SUFFIXES = Collections.unmodifiableSet(new HashSet(Arrays
			.asList("css", "js")));

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

		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 _resourceSpecs = new ArrayList();
	private Set _compressedSuffixes = new HashSet(DEFAULT_COMPRESS_SUFFIXES);
	private Set _mergedSuffixes = new HashSet(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> e : new ContributionScanner(packageName)
				.getContributions().entrySet()) {
			String path = e.getKey();
			if (Strings.isEmpty(path)) {
				throw new WicketRuntimeException("path must not be empty");
			}
			SortedSet 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 '.' ; null 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
	 *            true 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 null
	 * 
	 * @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 null to
	 * {@link #setVersion(AbstractResourceVersion)}
	 * 
	 * @return this
	 * @see #setVersion(AbstractResourceVersion)
	 */
	public ResourceMount autodetectVersion() {
		return setVersion(null);
	}

	/**
	 * force a minimal version. default is null
	 * 
	 * @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 null 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 (true)
	 * 
	 * @param requireVersion
	 *            whether version is required (true) or not
	 *            (false). default is true
	 * @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 true
	 * @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 null 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 null to
	 * {@link #setStyle(String)}
	 * 
	 * @return this
	 */
	public ResourceMount autodetectStyle() {
		return setStyle(null);
	}

	/**
	 * Set cache duration in seconds. default is autodetect ({@link
	 * null}). 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 (true) or not
	 *            (false). 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 (true) or not
	 *            (false). 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 (null)
	 * 
	 * @param mountScope
	 *            mount scope
	 * @return this
	 * @see ResourceReference#getScope()
	 * @see #autodetectMountScope()
	 */
	public ResourceMount setMountScope(Class mountScope) {
		_mountScope = mountScope;
		return this;
	}

	/**
	 * Same as passing null 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 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 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 null 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> specsList;

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

			final List refs = new ArrayList(specsList.size());
			for (Pair 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 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 true, 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 mergedKeys = new ArrayList(_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 null and null
	 *         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 appendName and
	 *         null 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 getCompressedSuffixes() {
		return _compressedSuffixes;
	}

	/**
	 * @return the set of suffixes that will be merged by default
	 * @see ResourceMount#DEFAULT_MERGE_SUFFIXES
	 */
	public Set 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 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(_compressedSuffixes);
			clone._mergedSuffixes = new HashSet(_mergedSuffixes);
			clone._resourceSpecs = new ArrayList(_resourceSpecs);
			return clone;
		} catch (CloneNotSupportedException e) {
			throw new WicketRuntimeException("clone of Object not supported?", e);
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy