/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.wicket.request.resource.caching;
import java.util.regex.Pattern;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.request.resource.AbstractResource;
import org.apache.wicket.request.resource.caching.version.CachingResourceVersion;
import org.apache.wicket.request.resource.caching.version.IResourceVersion;
import org.apache.wicket.util.lang.Args;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* resource caching strategy that adds a version for the
* requested resource to the filename.
* <p/>
* versioned_filename := [basename][version-prefix][version](.extension)
* <p/>
* the <code>version</code> must not contain the <code>version-prefix</code> so
* please use an unambiguous value for the <code>version-prefix</code>. The default
* <code>version-prefix</code> is <code>{@value #DEFAULT_VERSION_PREFIX}</code>.
* <p/>
* Since browsers and proxies use the versioned filename of the resource
* as a cache key a change to the version will also change the filename and
* cause a reliable cache miss. This enables us to set the caching duration
* of the resource to a maximum and get best network performance.
* <p/>
*
* @author Peter Ertl
*
* @since 1.5
*/
public class FilenameWithVersionResourceCachingStrategy implements IResourceCachingStrategy
{
private static final Logger LOG = LoggerFactory.getLogger(FilenameWithVersionResourceCachingStrategy.class);
private static final String DEFAULT_VERSION_PREFIX = "-ver-";
/** string that marks the beginning the of the version in the decorated filename */
private final String versionPrefix;
/** resource version provider */
private final IResourceVersion resourceVersion;
/**
* create filename caching strategy with given version provider and
* <code>version-prefix = '{@value #DEFAULT_VERSION_PREFIX}'</code>
*
* @param resourceVersion
* version provider
*
* @see #FilenameWithVersionResourceCachingStrategy(String, org.apache.wicket.request.resource.caching.version.IResourceVersion)
*/
public FilenameWithVersionResourceCachingStrategy(IResourceVersion resourceVersion)
{
this(DEFAULT_VERSION_PREFIX, resourceVersion);
}
/**
* Constructor
*
* @param versionPrefix
* string that marks the beginning the of the version in the decorated filename
* @param resourceVersion
* resource version object
*
* @see #FilenameWithVersionResourceCachingStrategy(org.apache.wicket.request.resource.caching.version.IResourceVersion)
*/
public FilenameWithVersionResourceCachingStrategy(String versionPrefix,
IResourceVersion resourceVersion)
{
this.resourceVersion = Args.notNull(resourceVersion, "resourceVersion");
this.versionPrefix = Args.notEmpty(versionPrefix, "versionPrefix");
}
/**
* @return string appended to the filename before the version string
*/
public final String getVersionPrefix()
{
return versionPrefix;
}
@Override
public void decorateUrl(ResourceUrl url, IStaticCacheableResource resource)
{
// get version string for requested resource
final String version = this.resourceVersion.getVersion(resource);
// ignore resource if no version information is available
if (version == null)
{
return;
}
// get undecorated filename
final String filename = url.getFileName();
// check if resource name has extension
final int extensionAt = filename.lastIndexOf('.');
// create filename with version:
//
// filename :=
// [basename][version-prefix][version](.extension)
//
final StringBuilder versionedFilename = new StringBuilder();
// add filename
if (extensionAt == -1)
{
versionedFilename.append(filename);
}
else
{
versionedFilename.append(filename.substring(0, extensionAt));
}
int pos = versionedFilename.indexOf(getVersionPrefix());
if (pos != -1 && isVersion(versionedFilename.substring(pos + versionPrefix.length())))
{
LOG.error("A resource with name '{}' contains the version prefix '{}' so the un-decoration will not work." +
" Either use a different version prefix or rename this resource.", filename, getVersionPrefix());
}
// add version suffix
versionedFilename.append(versionPrefix);
// add version
versionedFilename.append(version);
// add extension if present
if (extensionAt != -1)
{
versionedFilename.append(filename.substring(extensionAt));
}
// set versioned filename
url.setFileName(versionedFilename.toString());
}
@Override
public void undecorateUrl(ResourceUrl url)
{
final String filename = url.getFileName();
// check for extension
int pos = filename.lastIndexOf('.');
// get name of file without extension (but with version string)
final String fullname = pos == -1 ? filename : filename.substring(0, pos);
// get extension of file if present
final String extension = pos == -1 ? null : filename.substring(pos);
// get position of version string
pos = fullname.lastIndexOf(versionPrefix);
// remove version string if it exists
if (pos != -1 && isVersion(fullname.substring(pos + versionPrefix.length())))
{
// get filename before version string
final String basename = fullname.substring(0, pos);
// create filename without version string
// (required for working resource lookup)
url.setFileName(extension == null? basename : basename + extension);
// store the version in the request cycle
RequestCycle requestCycle = RequestCycle.get();
if (requestCycle != null)
{
int idx = fullname.indexOf(versionPrefix);
String urlVersion = fullname.substring(idx + versionPrefix.length());
requestCycle.setMetaData(URL_VERSION, urlVersion);
}
}
}
private boolean isVersion(String substring)
{
Pattern versionPattern = resourceVersion.getVersionPattern();
return versionPattern == null || versionPattern.matcher(substring).matches();
}
/**
* set resource caching to maximum and set cache-visibility to 'public'
*
* @param response
*/
@Override
public void decorateResponse(AbstractResource.ResourceResponse response, IStaticCacheableResource resource)
{
String requestedVersion = RequestCycle.get().getMetaData(URL_VERSION);
String calculatedVersion = this.resourceVersion.getVersion(resource);
if (calculatedVersion != null && calculatedVersion.equals(requestedVersion))
{
response.setCacheDurationToMaximum();
response.setCacheScope(WebResponse.CacheScope.PUBLIC);
}
}
@Override
public void clearCache()
{
if (resourceVersion instanceof CachingResourceVersion)
{
((CachingResourceVersion) resourceVersion).invalidateAll();
}
}
}