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 jakarta.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]&[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("/images", 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}