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