001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.request.resource.caching; 018 019import java.util.regex.Pattern; 020 021import org.apache.wicket.request.cycle.RequestCycle; 022import org.apache.wicket.request.http.WebResponse; 023import org.apache.wicket.request.resource.AbstractResource; 024import org.apache.wicket.request.resource.caching.version.CachingResourceVersion; 025import org.apache.wicket.request.resource.caching.version.IResourceVersion; 026import org.apache.wicket.util.lang.Args; 027import org.slf4j.Logger; 028import org.slf4j.LoggerFactory; 029 030/** 031 * resource caching strategy that adds a version for the 032 * requested resource to the filename. 033 * <p/> 034 * versioned_filename := [basename][version-prefix][version](.extension) 035 * <p/> 036 * the <code>version</code> must not contain the <code>version-prefix</code> so 037 * please use an unambiguous value for the <code>version-prefix</code>. The default 038 * <code>version-prefix</code> is <code>{@value #DEFAULT_VERSION_PREFIX}</code>. 039 * <p/> 040 * Since browsers and proxies use the versioned filename of the resource 041 * as a cache key a change to the version will also change the filename and 042 * cause a reliable cache miss. This enables us to set the caching duration 043 * of the resource to a maximum and get best network performance. 044 * <p/> 045 * 046 * @author Peter Ertl 047 * 048 * @since 1.5 049 */ 050public class FilenameWithVersionResourceCachingStrategy implements IResourceCachingStrategy 051{ 052 private static final Logger LOG = LoggerFactory.getLogger(FilenameWithVersionResourceCachingStrategy.class); 053 054 private static final String DEFAULT_VERSION_PREFIX = "-ver-"; 055 056 /** string that marks the beginning the of the version in the decorated filename */ 057 private final String versionPrefix; 058 059 /** resource version provider */ 060 private final IResourceVersion resourceVersion; 061 062 /** 063 * create filename caching strategy with given version provider and 064 * <code>version-prefix = '{@value #DEFAULT_VERSION_PREFIX}'</code> 065 * 066 * @param resourceVersion 067 * version provider 068 * 069 * @see #FilenameWithVersionResourceCachingStrategy(String, org.apache.wicket.request.resource.caching.version.IResourceVersion) 070 */ 071 public FilenameWithVersionResourceCachingStrategy(IResourceVersion resourceVersion) 072 { 073 this(DEFAULT_VERSION_PREFIX, resourceVersion); 074 } 075 076 /** 077 * Constructor 078 * 079 * @param versionPrefix 080 * string that marks the beginning the of the version in the decorated filename 081 * @param resourceVersion 082 * resource version object 083 * 084 * @see #FilenameWithVersionResourceCachingStrategy(org.apache.wicket.request.resource.caching.version.IResourceVersion) 085 */ 086 public FilenameWithVersionResourceCachingStrategy(String versionPrefix, 087 IResourceVersion resourceVersion) 088 { 089 this.resourceVersion = Args.notNull(resourceVersion, "resourceVersion"); 090 this.versionPrefix = Args.notEmpty(versionPrefix, "versionPrefix"); 091 } 092 093 /** 094 * @return string appended to the filename before the version string 095 */ 096 public final String getVersionPrefix() 097 { 098 return versionPrefix; 099 } 100 101 @Override 102 public void decorateUrl(ResourceUrl url, IStaticCacheableResource resource) 103 { 104 // get version string for requested resource 105 final String version = this.resourceVersion.getVersion(resource); 106 107 // ignore resource if no version information is available 108 if (version == null) 109 { 110 return; 111 } 112 113 // get undecorated filename 114 final String filename = url.getFileName(); 115 116 // check if resource name has extension 117 final int extensionAt = filename.lastIndexOf('.'); 118 119 // create filename with version: 120 // 121 // filename := 122 // [basename][version-prefix][version](.extension) 123 // 124 final StringBuilder versionedFilename = new StringBuilder(); 125 126 // add filename 127 if (extensionAt == -1) 128 { 129 versionedFilename.append(filename); 130 } 131 else 132 { 133 versionedFilename.append(filename.substring(0, extensionAt)); 134 } 135 136 int pos = versionedFilename.indexOf(getVersionPrefix()); 137 if (pos != -1 && isVersion(versionedFilename.substring(pos + versionPrefix.length()))) 138 { 139 LOG.error("A resource with name '{}' contains the version prefix '{}' so the un-decoration will not work." + 140 " Either use a different version prefix or rename this resource.", filename, getVersionPrefix()); 141 } 142 143 // add version suffix 144 versionedFilename.append(versionPrefix); 145 146 // add version 147 versionedFilename.append(version); 148 149 // add extension if present 150 if (extensionAt != -1) 151 { 152 versionedFilename.append(filename.substring(extensionAt)); 153 } 154 // set versioned filename 155 url.setFileName(versionedFilename.toString()); 156 } 157 158 @Override 159 public void undecorateUrl(ResourceUrl url) 160 { 161 final String filename = url.getFileName(); 162 163 // check for extension 164 int pos = filename.lastIndexOf('.'); 165 166 // get name of file without extension (but with version string) 167 final String fullname = pos == -1 ? filename : filename.substring(0, pos); 168 169 // get extension of file if present 170 final String extension = pos == -1 ? null : filename.substring(pos); 171 172 // get position of version string 173 pos = fullname.lastIndexOf(versionPrefix); 174 175 // remove version string if it exists 176 if (pos != -1 && isVersion(fullname.substring(pos + versionPrefix.length()))) 177 { 178 // get filename before version string 179 final String basename = fullname.substring(0, pos); 180 181 // create filename without version string 182 // (required for working resource lookup) 183 url.setFileName(extension == null? basename : basename + extension); 184 185 // store the version in the request cycle 186 RequestCycle requestCycle = RequestCycle.get(); 187 if (requestCycle != null) 188 { 189 int idx = fullname.indexOf(versionPrefix); 190 String urlVersion = fullname.substring(idx + versionPrefix.length()); 191 requestCycle.setMetaData(URL_VERSION, urlVersion); 192 } 193 } 194 } 195 196 private boolean isVersion(String substring) 197 { 198 Pattern versionPattern = resourceVersion.getVersionPattern(); 199 return versionPattern == null || versionPattern.matcher(substring).matches(); 200 } 201 202 /** 203 * set resource caching to maximum and set cache-visibility to 'public' 204 * 205 * @param response 206 */ 207 @Override 208 public void decorateResponse(AbstractResource.ResourceResponse response, IStaticCacheableResource resource) 209 { 210 String requestedVersion = RequestCycle.get().getMetaData(URL_VERSION); 211 String calculatedVersion = this.resourceVersion.getVersion(resource); 212 if (calculatedVersion != null && calculatedVersion.equals(requestedVersion)) 213 { 214 response.setCacheDurationToMaximum(); 215 response.setCacheScope(WebResponse.CacheScope.PUBLIC); 216 } 217 } 218 219 @Override 220 public void clearCache() 221 { 222 if (resourceVersion instanceof CachingResourceVersion) 223 { 224 ((CachingResourceVersion) resourceVersion).invalidateAll(); 225 } 226 } 227}