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}