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.core.request.mapper;
018
019import java.util.List;
020
021import javax.servlet.http.HttpServletResponse;
022
023import org.apache.wicket.Application;
024import org.apache.wicket.request.IRequestHandler;
025import org.apache.wicket.request.IRequestMapper;
026import org.apache.wicket.request.Request;
027import org.apache.wicket.request.Url;
028import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
029import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException;
030import org.apache.wicket.request.mapper.parameter.INamedParameters;
031import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
032import org.apache.wicket.request.mapper.parameter.PageParameters;
033import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
034import org.apache.wicket.request.resource.IResource;
035import org.apache.wicket.request.resource.ResourceReference;
036import org.apache.wicket.request.resource.caching.IResourceCachingStrategy;
037import org.apache.wicket.request.resource.caching.IStaticCacheableResource;
038import org.apache.wicket.request.resource.caching.ResourceUrl;
039import org.apache.wicket.resource.ResourceUtil;
040import org.apache.wicket.util.lang.Args;
041import org.apache.wicket.util.string.Strings;
042
043/**
044 * A {@link IRequestMapper} to mount resources to a custom mount path
045 * <ul>
046 * <li>maps indexed parameters to path segments</li>
047 * <li>maps named parameters to query string arguments or placeholder path segments</li>
048 * </ul>
049 *
050 * <strong>Sample structure of url</strong>
051 * <p/>
052 * <pre>
053 *    /myresources/${category}/images/[indexed-param-0]/[indexed-param-1]?[named-param-1=value]&amp;[named-param-2=value2]
054 * </pre>
055 *
056 * <strong>Sample usage</strong>
057 *
058 * <p/>
059 * in your wicket application's init() method use a statement like this
060 * <p/>
061 *
062 * <pre>
063 * mountResource(&quot;/images&quot;, new ImagesResourceReference()));
064 * </pre>
065 *
066 * Note: Mounted this way the resource reference has application scope, i.e. it is shared between
067 * all users of the application. It is recommended to not keep any state in it.
068 *
069 * @see org.apache.wicket.protocol.http.WebApplication#mountResource(String,
070 *      org.apache.wicket.request.resource.ResourceReference)
071 *
072 * @author Peter Ertl
073 */
074public class ResourceMapper extends AbstractBookmarkableMapper
075{
076        // encode page parameters into url + decode page parameters from url
077        private final IPageParametersEncoder parametersEncoder;
078
079        // mount path (= segments) the resource is bound to
080        private final String[] mountSegments;
081
082        // resource that the mapper links to
083        private final ResourceReference resourceReference;
084
085        /**
086         * create a resource mapper for a resource
087         *
088         * @param path
089         *            mount path for the resource
090         * @param resourceReference
091         *            resource reference that should be linked to the mount path
092         *
093         * @see #ResourceMapper(String, org.apache.wicket.request.resource.ResourceReference,
094         *      org.apache.wicket.request.mapper.parameter.IPageParametersEncoder)
095         */
096        public ResourceMapper(String path, ResourceReference resourceReference)
097        {
098                this(path, resourceReference, new PageParametersEncoder());
099        }
100
101        /**
102         * create a resource mapper for a resource
103         *
104         * @param path
105         *            mount path for the resource
106         * @param resourceReference
107         *            resource reference that should be linked to the mount path
108         * @param encoder
109         *            encoder for url parameters
110         */
111        public ResourceMapper(String path, ResourceReference resourceReference,
112                IPageParametersEncoder encoder)
113        {
114                super(path, encoder);
115                Args.notNull(resourceReference, "resourceReference");
116
117                this.resourceReference = resourceReference;
118                mountSegments = getMountSegments(path);
119                parametersEncoder = encoder;
120        }
121
122        @Override
123        public IRequestHandler mapRequest(final Request request)
124        {
125                final Url url = new Url(request.getUrl());
126
127                // now extract the page parameters from the request url
128                PageParameters parameters = extractPageParameters(request, mountSegments.length,
129                        parametersEncoder);
130                if (parameters != null)
131                {
132                        parameters.setLocale(resolveLocale());
133                }
134
135                // remove caching information from current request
136                removeCachingDecoration(url, parameters);
137
138                // check if url matches mount path
139                if (urlStartsWith(url, mountSegments) == false)
140                {
141                        return null;
142                }
143
144                // check if there are placeholders in mount segments
145                for (int index = 0; index < mountSegments.length; ++index)
146                {
147                        String placeholder = getPlaceholder(mountSegments[index]);
148
149                        if (placeholder != null)
150                        {
151                                // extract the parameter from URL
152                                if (parameters == null)
153                                {
154                                        parameters = newPageParameters();
155                                }
156                                parameters.add(placeholder, url.getSegments().get(index), INamedParameters.Type.PATH);
157                        }
158                }
159                return new ResourceReferenceRequestHandler(resourceReference, parameters);
160        }
161
162        @Override
163        protected final UrlInfo parseRequest(final Request request) {
164                throw new UnsupportedOperationException();
165        }
166
167        @Override
168        protected final Url buildUrl(final UrlInfo info) {
169                throw new UnsupportedOperationException();
170        }
171
172        @Override
173        protected final boolean pageMustHaveBeenCreatedBookmarkable() {
174                throw new UnsupportedOperationException();
175        }
176
177        @Override
178        public int getCompatibilityScore(Request request)
179        {
180                Url originalUrl = new Url(request.getUrl());
181                PageParameters parameters = extractPageParameters(request, mountSegments.length, parametersEncoder);
182                if (parameters != null)
183                {
184                        parameters.setLocale(resolveLocale());
185                }
186                removeCachingDecoration(originalUrl, parameters);
187                Request requestWithoutDecoration = request.cloneWithUrl(originalUrl);
188
189                int score = super.getCompatibilityScore(requestWithoutDecoration);
190                if (score > 0)
191                {
192                        score--; // pages always have priority over resources
193                }
194                else
195                {
196                        score = -1;
197                }
198                return score;
199        }
200
201        @Override
202        public Url mapHandler(IRequestHandler requestHandler)
203        {
204                if ((requestHandler instanceof ResourceReferenceRequestHandler) == false)
205                {
206                        return null;
207                }
208
209                ResourceReferenceRequestHandler handler = (ResourceReferenceRequestHandler)requestHandler;
210
211                // see if request handler addresses the resource reference we serve
212                if (resourceReference.equals(handler.getResourceReference()) == false)
213                {
214                        return null;
215                }
216
217                Url url = new Url();
218
219                // add mount path segments
220                for (String segment : mountSegments)
221                {
222                        url.getSegments().add(segment);
223                }
224
225                // replace placeholder parameters
226                PageParameters parameters = newPageParameters();
227                parameters.mergeWith(handler.getPageParameters());
228
229                for (int index = 0; index < mountSegments.length; ++index)
230                {
231                        String placeholder = getPlaceholder(mountSegments[index]);
232
233                        if (placeholder != null)
234                        {
235                                url.getSegments().set(index, parameters.get(placeholder).toString(""));
236                                parameters.remove(placeholder);
237                        }
238                }
239
240                // add caching information
241                addCachingDecoration(url, parameters);
242
243                ResourceUtil.encodeResourceReferenceAttributes(url, resourceReference);
244                // create url
245                return encodePageParameters(url, parameters, parametersEncoder);
246        }
247
248        protected IResourceCachingStrategy getCachingStrategy()
249        {
250                return Application.get().getResourceSettings().getCachingStrategy();
251        }
252
253        protected void addCachingDecoration(Url url, PageParameters parameters)
254        {
255                final List<String> segments = url.getSegments();
256                final int lastSegmentAt = segments.size() - 1;
257                final String filename = segments.get(lastSegmentAt);
258
259                if (Strings.isEmpty(filename) == false)
260                {
261                        final IResource resource = resourceReference.getResource();
262
263                        if (resource instanceof IStaticCacheableResource)
264                        {
265                                final IStaticCacheableResource cacheable = (IStaticCacheableResource)resource;
266                                
267                                if(cacheable.isCachingEnabled())
268                                {
269                                        final ResourceUrl cacheUrl = new ResourceUrl(filename, parameters);
270        
271                                        getCachingStrategy().decorateUrl(cacheUrl, cacheable);
272        
273                                        if (Strings.isEmpty(cacheUrl.getFileName()))
274                                        {
275                                                if (Application.exists() && Application.get().usesDeploymentConfig())
276                                                {
277                                                        throw new AbortWithHttpErrorCodeException(HttpServletResponse.SC_NOT_FOUND,
278                                                                "caching strategy returned empty name for " + resource);
279                                                }
280                                                else
281                                                {
282                                                        throw new IllegalStateException(
283                                                                        "caching strategy returned empty name for " + resource);
284                                                }
285                                        }
286                                        segments.set(lastSegmentAt, cacheUrl.getFileName());
287                                }
288                        }
289                }
290        }
291
292        protected void removeCachingDecoration(Url url, PageParameters parameters)
293        {
294                final List<String> segments = url.getSegments();
295
296                if (segments.isEmpty() == false)
297                {
298                        // get filename (the last segment)
299                        final int lastSegmentAt = segments.size() - 1;
300                        String filename = segments.get(lastSegmentAt);
301
302                        // ignore requests with empty filename
303                        if (Strings.isEmpty(filename))
304                        {
305                                return;
306                        }
307
308                        // create resource url from filename and query parameters
309                        final ResourceUrl resourceUrl = new ResourceUrl(filename, parameters);
310
311                        // remove caching information from request
312                        getCachingStrategy().undecorateUrl(resourceUrl);
313
314                        // check for broken caching strategy (this must never happen)
315                        if (Strings.isEmpty(resourceUrl.getFileName()))
316                        {
317                                throw new IllegalStateException("caching strategy returned empty name for " +
318                                        resourceUrl);
319                        }
320
321                        segments.set(lastSegmentAt, resourceUrl.getFileName());
322                }
323        }
324}