001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.servlet.resource;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025
026import javax.servlet.http.HttpServletRequest;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.context.ApplicationContext;
032import org.springframework.context.ApplicationListener;
033import org.springframework.context.event.ContextRefreshedEvent;
034import org.springframework.core.annotation.AnnotationAwareOrderComparator;
035import org.springframework.util.AntPathMatcher;
036import org.springframework.util.PathMatcher;
037import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
038import org.springframework.web.util.UrlPathHelper;
039
040/**
041 * A central component to use to obtain the public URL path that clients should
042 * use to access a static resource.
043 *
044 * <p>This class is aware of Spring MVC handler mappings used to serve static
045 * resources and uses the {@code ResourceResolver} chains of the configured
046 * {@code ResourceHttpRequestHandler}s to make its decisions.
047 *
048 * @author Rossen Stoyanchev
049 * @since 4.1
050 */
051public class ResourceUrlProvider implements ApplicationListener<ContextRefreshedEvent> {
052
053        protected final Log logger = LogFactory.getLog(getClass());
054
055        private UrlPathHelper urlPathHelper = UrlPathHelper.defaultInstance;
056
057        private PathMatcher pathMatcher = new AntPathMatcher();
058
059        private final Map<String, ResourceHttpRequestHandler> handlerMap = new LinkedHashMap<String, ResourceHttpRequestHandler>();
060
061        private boolean autodetect = true;
062
063
064        /**
065         * Configure a {@code UrlPathHelper} to use in
066         * {@link #getForRequestUrl(javax.servlet.http.HttpServletRequest, String)}
067         * in order to derive the lookup path for a target request URL path.
068         */
069        public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
070                this.urlPathHelper = urlPathHelper;
071        }
072
073        /**
074         * Return the configured {@code UrlPathHelper}.
075         * @since 4.2.8
076         */
077        public UrlPathHelper getUrlPathHelper() {
078                return this.urlPathHelper;
079        }
080
081        /**
082         * @deprecated as of Spring 4.2.8, in favor of {@link #getUrlPathHelper}
083         */
084        @Deprecated
085        public UrlPathHelper getPathHelper() {
086                return this.urlPathHelper;
087        }
088
089        /**
090         * Configure a {@code PathMatcher} to use when comparing target lookup path
091         * against resource mappings.
092         */
093        public void setPathMatcher(PathMatcher pathMatcher) {
094                this.pathMatcher = pathMatcher;
095        }
096
097        /**
098         * Return the configured {@code PathMatcher}.
099         */
100        public PathMatcher getPathMatcher() {
101                return this.pathMatcher;
102        }
103
104        /**
105         * Manually configure the resource mappings.
106         * <p><strong>Note:</strong> by default resource mappings are auto-detected
107         * from the Spring {@code ApplicationContext}. However if this property is
108         * used, the auto-detection is turned off.
109         */
110        public void setHandlerMap(Map<String, ResourceHttpRequestHandler> handlerMap) {
111                if (handlerMap != null) {
112                        this.handlerMap.clear();
113                        this.handlerMap.putAll(handlerMap);
114                        this.autodetect = false;
115                }
116        }
117
118        /**
119         * Return the resource mappings, either manually configured or auto-detected
120         * when the Spring {@code ApplicationContext} is refreshed.
121         */
122        public Map<String, ResourceHttpRequestHandler> getHandlerMap() {
123                return this.handlerMap;
124        }
125
126        /**
127         * Return {@code false} if resource mappings were manually configured,
128         * {@code true} otherwise.
129         */
130        public boolean isAutodetect() {
131                return this.autodetect;
132        }
133
134        @Override
135        public void onApplicationEvent(ContextRefreshedEvent event) {
136                if (isAutodetect()) {
137                        this.handlerMap.clear();
138                        detectResourceHandlers(event.getApplicationContext());
139                        if (this.handlerMap.isEmpty() && logger.isDebugEnabled()) {
140                                logger.debug("No resource handling mappings found");
141                        }
142                        if (!this.handlerMap.isEmpty()) {
143                                this.autodetect = false;
144                        }
145                }
146        }
147
148
149        protected void detectResourceHandlers(ApplicationContext appContext) {
150                logger.debug("Looking for resource handler mappings");
151
152                Map<String, SimpleUrlHandlerMapping> beans = appContext.getBeansOfType(SimpleUrlHandlerMapping.class);
153                List<SimpleUrlHandlerMapping> mappings = new ArrayList<SimpleUrlHandlerMapping>(beans.values());
154                AnnotationAwareOrderComparator.sort(mappings);
155
156                for (SimpleUrlHandlerMapping mapping : mappings) {
157                        for (String pattern : mapping.getHandlerMap().keySet()) {
158                                Object handler = mapping.getHandlerMap().get(pattern);
159                                if (handler instanceof ResourceHttpRequestHandler) {
160                                        ResourceHttpRequestHandler resourceHandler = (ResourceHttpRequestHandler) handler;
161                                        if (logger.isDebugEnabled()) {
162                                                logger.debug("Found resource handler mapping: URL pattern=\"" + pattern + "\", " +
163                                                                "locations=" + resourceHandler.getLocations() + ", " +
164                                                                "resolvers=" + resourceHandler.getResourceResolvers());
165                                        }
166                                        this.handlerMap.put(pattern, resourceHandler);
167                                }
168                        }
169                }
170        }
171
172        /**
173         * A variation on {@link #getForLookupPath(String)} that accepts a full request
174         * URL path (i.e. including context and servlet path) and returns the full request
175         * URL path to expose for public use.
176         * @param request the current request
177         * @param requestUrl the request URL path to resolve
178         * @return the resolved public URL path, or {@code null} if unresolved
179         */
180        public final String getForRequestUrl(HttpServletRequest request, String requestUrl) {
181                if (logger.isTraceEnabled()) {
182                        logger.trace("Getting resource URL for request URL \"" + requestUrl + "\"");
183                }
184                int prefixIndex = getLookupPathIndex(request);
185                int suffixIndex = getEndPathIndex(requestUrl);
186                if (prefixIndex >= suffixIndex) {
187                        return null;
188                }
189                String prefix = requestUrl.substring(0, prefixIndex);
190                String suffix = requestUrl.substring(suffixIndex);
191                String lookupPath = requestUrl.substring(prefixIndex, suffixIndex);
192                String resolvedLookupPath = getForLookupPath(lookupPath);
193                return (resolvedLookupPath != null ? prefix + resolvedLookupPath + suffix : null);
194        }
195
196        private int getLookupPathIndex(HttpServletRequest request) {
197                UrlPathHelper pathHelper = getUrlPathHelper();
198                String requestUri = pathHelper.getRequestUri(request);
199                String lookupPath = pathHelper.getLookupPathForRequest(request);
200                return requestUri.indexOf(lookupPath);
201        }
202
203        private int getEndPathIndex(String lookupPath) {
204                int suffixIndex = lookupPath.length();
205                int queryIndex = lookupPath.indexOf('?');
206                if (queryIndex > 0) {
207                        suffixIndex = queryIndex;
208                }
209                int hashIndex = lookupPath.indexOf('#');
210                if (hashIndex > 0) {
211                        suffixIndex = Math.min(suffixIndex, hashIndex);
212                }
213                return suffixIndex;
214        }
215
216        /**
217         * Compare the given path against configured resource handler mappings and
218         * if a match is found use the {@code ResourceResolver} chain of the matched
219         * {@code ResourceHttpRequestHandler} to resolve the URL path to expose for
220         * public use.
221         * <p>It is expected that the given path is what Spring MVC would use for
222         * request mapping purposes, i.e. excluding context and servlet path portions.
223         * <p>If several handler mappings match, the handler used will be the one
224         * configured with the most specific pattern.
225         * @param lookupPath the lookup path to check
226         * @return the resolved public URL path, or {@code null} if unresolved
227         */
228        public final String getForLookupPath(String lookupPath) {
229                if (logger.isTraceEnabled()) {
230                        logger.trace("Getting resource URL for lookup path \"" + lookupPath + "\"");
231                }
232
233                List<String> matchingPatterns = new ArrayList<String>();
234                for (String pattern : this.handlerMap.keySet()) {
235                        if (getPathMatcher().match(pattern, lookupPath)) {
236                                matchingPatterns.add(pattern);
237                        }
238                }
239
240                if (!matchingPatterns.isEmpty()) {
241                        Comparator<String> patternComparator = getPathMatcher().getPatternComparator(lookupPath);
242                        Collections.sort(matchingPatterns, patternComparator);
243                        for (String pattern : matchingPatterns) {
244                                String pathWithinMapping = getPathMatcher().extractPathWithinPattern(pattern, lookupPath);
245                                String pathMapping = lookupPath.substring(0, lookupPath.indexOf(pathWithinMapping));
246                                if (logger.isTraceEnabled()) {
247                                        logger.trace("Invoking ResourceResolverChain for URL pattern \"" + pattern + "\"");
248                                }
249                                ResourceHttpRequestHandler handler = this.handlerMap.get(pattern);
250                                ResourceResolverChain chain = new DefaultResourceResolverChain(handler.getResourceResolvers());
251                                String resolved = chain.resolveUrlPath(pathWithinMapping, handler.getLocations());
252                                if (resolved == null) {
253                                        continue;
254                                }
255                                if (logger.isTraceEnabled()) {
256                                        logger.trace("Resolved public resource URL path \"" + resolved + "\"");
257                                }
258                                return pathMapping + resolved;
259                        }
260                }
261
262                if (logger.isDebugEnabled()) {
263                        logger.debug("No matching resource mapping for lookup path \"" + lookupPath + "\"");
264                }
265                return null;
266        }
267
268}