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.reactive.resource;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027import reactor.core.publisher.Mono;
028
029import org.springframework.context.ApplicationContext;
030import org.springframework.context.ApplicationListener;
031import org.springframework.context.event.ContextRefreshedEvent;
032import org.springframework.core.annotation.AnnotationAwareOrderComparator;
033import org.springframework.http.server.PathContainer;
034import org.springframework.http.server.reactive.ServerHttpRequest;
035import org.springframework.util.StringUtils;
036import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
037import org.springframework.web.server.ServerWebExchange;
038import org.springframework.web.util.pattern.PathPattern;
039import org.springframework.web.util.pattern.PathPatternParser;
040
041/**
042 * A central component to use to obtain the public URL path that clients should
043 * use to access a static resource.
044 *
045 * <p>This class is aware of Spring WebFlux handler mappings used to serve static
046 * resources and uses the {@code ResourceResolver} chains of the configured
047 * {@code ResourceHttpRequestHandler}s to make its decisions.
048 *
049 * @author Rossen Stoyanchev
050 * @since 5.0
051 */
052public class ResourceUrlProvider implements ApplicationListener<ContextRefreshedEvent> {
053
054        private static final Log logger = LogFactory.getLog(ResourceUrlProvider.class);
055
056
057        private final Map<PathPattern, ResourceWebHandler> handlerMap = new LinkedHashMap<>();
058
059
060        /**
061         * Return a read-only view of the resource handler mappings either manually
062         * configured or auto-detected from Spring configuration.
063         */
064        public Map<PathPattern, ResourceWebHandler> getHandlerMap() {
065                return Collections.unmodifiableMap(this.handlerMap);
066        }
067
068
069        /**
070         * Manually configure resource handler mappings.
071         * <p><strong>Note:</strong> by default resource mappings are auto-detected
072         * from the Spring {@code ApplicationContext}. If this property is used,
073         * auto-detection is turned off.
074         */
075        public void registerHandlers(Map<String, ResourceWebHandler> handlerMap) {
076                this.handlerMap.clear();
077                handlerMap.forEach((rawPattern, resourceWebHandler) -> {
078                        rawPattern = prependLeadingSlash(rawPattern);
079                        PathPattern pattern = PathPatternParser.defaultInstance.parse(rawPattern);
080                        this.handlerMap.put(pattern, resourceWebHandler);
081                });
082        }
083
084        @Override
085        public void onApplicationEvent(ContextRefreshedEvent event) {
086                if (this.handlerMap.isEmpty()) {
087                        detectResourceHandlers(event.getApplicationContext());
088                }
089        }
090
091        private void detectResourceHandlers(ApplicationContext context) {
092                Map<String, SimpleUrlHandlerMapping> beans = context.getBeansOfType(SimpleUrlHandlerMapping.class);
093                List<SimpleUrlHandlerMapping> mappings = new ArrayList<>(beans.values());
094                AnnotationAwareOrderComparator.sort(mappings);
095
096                mappings.forEach(mapping ->
097                        mapping.getHandlerMap().forEach((pattern, handler) -> {
098                                if (handler instanceof ResourceWebHandler) {
099                                        ResourceWebHandler resourceHandler = (ResourceWebHandler) handler;
100                                        this.handlerMap.put(pattern, resourceHandler);
101                                }
102                        }));
103
104                if (this.handlerMap.isEmpty()) {
105                        logger.trace("No resource handling mappings found");
106                }
107        }
108
109
110        /**
111         * Get the public resource URL for the given URI string.
112         * <p>The URI string is expected to be a path and if it contains a query or
113         * fragment those will be preserved in the resulting public resource URL.
114         * @param uriString the URI string to transform
115         * @param exchange the current exchange
116         * @return the resolved public resource URL path, or empty if unresolved
117         */
118        public final Mono<String> getForUriString(String uriString, ServerWebExchange exchange) {
119                ServerHttpRequest request = exchange.getRequest();
120                int queryIndex = getQueryIndex(uriString);
121                String lookupPath = uriString.substring(0, queryIndex);
122                String query = uriString.substring(queryIndex);
123                PathContainer parsedLookupPath = PathContainer.parsePath(lookupPath);
124
125                return resolveResourceUrl(exchange, parsedLookupPath).map(resolvedPath ->
126                                request.getPath().contextPath().value() + resolvedPath + query);
127        }
128
129        private int getQueryIndex(String path) {
130                int suffixIndex = path.length();
131                int queryIndex = path.indexOf('?');
132                if (queryIndex > 0) {
133                        suffixIndex = queryIndex;
134                }
135                int hashIndex = path.indexOf('#');
136                if (hashIndex > 0) {
137                        suffixIndex = Math.min(suffixIndex, hashIndex);
138                }
139                return suffixIndex;
140        }
141
142        private Mono<String> resolveResourceUrl(ServerWebExchange exchange, PathContainer lookupPath) {
143                return this.handlerMap.entrySet().stream()
144                                .filter(entry -> entry.getKey().matches(lookupPath))
145                                .min((entry1, entry2) ->
146                                                PathPattern.SPECIFICITY_COMPARATOR.compare(entry1.getKey(), entry2.getKey()))
147                                .map(entry -> {
148                                        PathContainer path = entry.getKey().extractPathWithinPattern(lookupPath);
149                                        int endIndex = lookupPath.elements().size() - path.elements().size();
150                                        PathContainer mapping = lookupPath.subPath(0, endIndex);
151                                        ResourceWebHandler handler = entry.getValue();
152                                        List<ResourceResolver> resolvers = handler.getResourceResolvers();
153                                        ResourceResolverChain chain = new DefaultResourceResolverChain(resolvers);
154                                        return chain.resolveUrlPath(path.value(), handler.getLocations())
155                                                        .map(resolvedPath -> mapping.value() + resolvedPath);
156                                })
157                                .orElseGet(() ->{
158                                        if (logger.isTraceEnabled()) {
159                                                logger.trace(exchange.getLogPrefix() + "No match for \"" + lookupPath + "\"");
160                                        }
161                                        return Mono.empty();
162                                });
163        }
164
165
166        private static String prependLeadingSlash(String pattern) {
167                if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) {
168                        return "/" + pattern;
169                }
170                else {
171                        return pattern;
172                }
173        }
174
175}