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}