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