Resource handling support for Spring Web Reactive
A straight-forward port of the resource handling support in spring-webmvc to spring-web-reactive. Primarily adapting contracts and implementations to use the reactive request and response and the reactive ResourceHttpMessageWriter. Issue: SPR-14521
This commit is contained in:
parent
108ebe0f2b
commit
1ae64bfbd3
|
@ -801,6 +801,7 @@ project("spring-web-reactive") {
|
|||
optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}")
|
||||
optional("org.freemarker:freemarker:${freemarkerVersion}")
|
||||
optional "org.apache.httpcomponents:httpclient:${httpclientVersion}"
|
||||
optional('org.webjars:webjars-locator:0.32')
|
||||
testCompile("javax.validation:validation-api:${beanvalVersion}")
|
||||
testCompile("org.apache.tomcat:tomcat-util:${tomcatVersion}")
|
||||
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
|
||||
|
@ -822,6 +823,7 @@ project("spring-web-reactive") {
|
|||
testRuntime("javax.activation:activation:${activationApiVersion}")
|
||||
testRuntime("org.jboss.xnio:xnio-nio:${xnioVersion}")
|
||||
testRuntime("org.jboss.logging:jboss-logging:3.3.0.Final")
|
||||
testRuntime("org.webjars:underscorejs:1.8.3")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import java.util.Locale;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
|
@ -44,7 +43,7 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
public abstract class AbstractMappingContentTypeResolver implements MappingContentTypeResolver {
|
||||
|
||||
/** Primary lookup for media types by key (e.g. "json" -> "application/json") */
|
||||
private final ConcurrentMap<String, MediaType> mediaTypeLookup = new ConcurrentHashMap<>(64);
|
||||
private final Map<String, MediaType> mediaTypeLookup = new ConcurrentHashMap<>(64);
|
||||
|
||||
/** Reverse lookup for keys associated with a media type */
|
||||
private final MultiValueMap<MediaType, String> keyLookup = new LinkedMultiValueMap<>(64);
|
||||
|
@ -65,6 +64,10 @@ public abstract class AbstractMappingContentTypeResolver implements MappingConte
|
|||
}
|
||||
|
||||
|
||||
public Map<String, MediaType> getMediaTypes() {
|
||||
return this.mediaTypeLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-classes can use this method to look up a MediaType by key.
|
||||
* @param key the key converted to lower case
|
||||
|
@ -77,7 +80,7 @@ public abstract class AbstractMappingContentTypeResolver implements MappingConte
|
|||
/**
|
||||
* Sub-classes can use this method get all mapped media types.
|
||||
*/
|
||||
protected List<MediaType> getMediaTypes() {
|
||||
protected List<MediaType> getAllMediaTypes() {
|
||||
return new ArrayList<>(this.mediaTypeLookup.values());
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ public class ParameterContentTypeResolver extends AbstractMappingContentTypeReso
|
|||
|
||||
@Override
|
||||
protected MediaType handleNoMatch(String key) throws NotAcceptableStatusException {
|
||||
throw new NotAcceptableStatusException(getMediaTypes());
|
||||
throw new NotAcceptableStatusException(getAllMediaTypes());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ public class PathExtensionContentTypeResolver extends AbstractMappingContentType
|
|||
}
|
||||
}
|
||||
if (!this.ignoreUnknownExtensions) {
|
||||
throw new NotAcceptableStatusException(getMediaTypes());
|
||||
throw new NotAcceptableStatusException(getAllMediaTypes());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -218,7 +218,7 @@ public class RequestedContentTypeResolverBuilder {
|
|||
}
|
||||
|
||||
|
||||
public RequestedContentTypeResolver build() {
|
||||
public CompositeContentTypeResolver build() {
|
||||
List<RequestedContentTypeResolver> resolvers = new ArrayList<>();
|
||||
|
||||
if (this.favorPathExtension) {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Base {@link ResourceResolver} providing consistent logging.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class AbstractResourceResolver implements ResourceResolver {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
|
||||
@Override
|
||||
public Resource resolveResource(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resolving resource for request path \"" + requestPath + "\"");
|
||||
}
|
||||
return resolveResourceInternal(exchange, requestPath, locations, chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveUrlPath(String resourceUrlPath, List<? extends Resource> locations,
|
||||
ResourceResolverChain chain) {
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resolving public URL for resource path \"" + resourceUrlPath + "\"");
|
||||
}
|
||||
|
||||
return resolveUrlPathInternal(resourceUrlPath, locations, chain);
|
||||
}
|
||||
|
||||
|
||||
protected abstract Resource resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain);
|
||||
|
||||
protected abstract String resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain);
|
||||
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link VersionStrategy} implementations.
|
||||
*
|
||||
* <p>Supports versions as:
|
||||
* <ul>
|
||||
* <li>prefix in the request path, like "version/static/myresource.js"
|
||||
* <li>file name suffix in the request path, like "static/myresource-version.js"
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note: This base class does <i>not</i> provide support for generating the
|
||||
* version string.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class AbstractVersionStrategy implements VersionStrategy {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final VersionPathStrategy pathStrategy;
|
||||
|
||||
|
||||
protected AbstractVersionStrategy(VersionPathStrategy pathStrategy) {
|
||||
Assert.notNull(pathStrategy, "VersionPathStrategy is required");
|
||||
this.pathStrategy = pathStrategy;
|
||||
}
|
||||
|
||||
|
||||
public VersionPathStrategy getVersionPathStrategy() {
|
||||
return this.pathStrategy;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String extractVersion(String requestPath) {
|
||||
return this.pathStrategy.extractVersion(requestPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String removeVersion(String requestPath, String version) {
|
||||
return this.pathStrategy.removeVersion(requestPath, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String addVersion(String requestPath, String version) {
|
||||
return this.pathStrategy.addVersion(requestPath, version);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A prefix-based {@code VersionPathStrategy},
|
||||
* e.g. {@code "{version}/path/foo.js"}.
|
||||
*/
|
||||
protected static class PrefixVersionPathStrategy implements VersionPathStrategy {
|
||||
|
||||
private final String prefix;
|
||||
|
||||
public PrefixVersionPathStrategy(String version) {
|
||||
Assert.hasText(version, "'version' must not be empty");
|
||||
this.prefix = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String extractVersion(String requestPath) {
|
||||
return (requestPath.startsWith(this.prefix) ? this.prefix : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String removeVersion(String requestPath, String version) {
|
||||
return requestPath.substring(this.prefix.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String addVersion(String path, String version) {
|
||||
if (path.startsWith(".")) {
|
||||
return path;
|
||||
}
|
||||
else {
|
||||
return (this.prefix.endsWith("/") || path.startsWith("/") ?
|
||||
this.prefix + path : this.prefix + "/" + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* File name-based {@code VersionPathStrategy},
|
||||
* e.g. {@code "path/foo-{version}.css"}.
|
||||
*/
|
||||
protected static class FileNameVersionPathStrategy implements VersionPathStrategy {
|
||||
|
||||
private static final Pattern pattern = Pattern.compile("-(\\S*)\\.");
|
||||
|
||||
@Override
|
||||
public String extractVersion(String requestPath) {
|
||||
Matcher matcher = pattern.matcher(requestPath);
|
||||
if (matcher.find()) {
|
||||
String match = matcher.group(1);
|
||||
return (match.contains("-") ? match.substring(match.lastIndexOf("-") + 1) : match);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String removeVersion(String requestPath, String version) {
|
||||
return StringUtils.delete(requestPath, "-" + version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String addVersion(String requestPath, String version) {
|
||||
String baseFilename = StringUtils.stripFilenameExtension(requestPath);
|
||||
String extension = StringUtils.getFilenameExtension(requestPath);
|
||||
return (baseFilename + "-" + version + "." + extension);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link ResourceTransformer} implementation that helps handling resources
|
||||
* within HTML5 AppCache manifests for HTML5 offline applications.
|
||||
*
|
||||
* <p>This transformer:
|
||||
* <ul>
|
||||
* <li>modifies links to match the public URL paths that should be exposed to
|
||||
* clients, using configured {@code ResourceResolver} strategies
|
||||
* <li>appends a comment in the manifest, containing a Hash
|
||||
* (e.g. "# Hash: 9de0f09ed7caf84e885f1f0f11c7e326"), thus changing the content
|
||||
* of the manifest in order to trigger an appcache reload in the browser.
|
||||
* </ul>
|
||||
*
|
||||
* All files that have the ".manifest" file extension, or the extension given
|
||||
* in the constructor, will be transformed by this class.
|
||||
*
|
||||
* <p>This hash is computed using the content of the appcache manifest and the
|
||||
* content of the linked resources; so changing a resource linked in the manifest
|
||||
* or the manifest itself should invalidate the browser cache.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see <a href="http://www.whatwg.org/specs/web-apps/current-work/multipage/offline.html#offline">HTML5 offline applications spec</a>
|
||||
*/
|
||||
public class AppCacheManifestTransformer extends ResourceTransformerSupport {
|
||||
|
||||
private static final String MANIFEST_HEADER = "CACHE MANIFEST";
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AppCacheManifestTransformer.class);
|
||||
|
||||
|
||||
private final Map<String, SectionTransformer> sectionTransformers = new HashMap<>();
|
||||
|
||||
private final String fileExtension;
|
||||
|
||||
|
||||
/**
|
||||
* Create an AppCacheResourceTransformer that transforms files with extension ".manifest".
|
||||
*/
|
||||
public AppCacheManifestTransformer() {
|
||||
this("manifest");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an AppCacheResourceTransformer that transforms files with the extension
|
||||
* given as a parameter.
|
||||
*/
|
||||
public AppCacheManifestTransformer(String fileExtension) {
|
||||
this.fileExtension = fileExtension;
|
||||
|
||||
SectionTransformer noOpSection = new NoOpSection();
|
||||
this.sectionTransformers.put(MANIFEST_HEADER, noOpSection);
|
||||
this.sectionTransformers.put("NETWORK:", noOpSection);
|
||||
this.sectionTransformers.put("FALLBACK:", noOpSection);
|
||||
this.sectionTransformers.put("CACHE:", new CacheSection());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Resource transform(ServerWebExchange exchange, Resource resource,
|
||||
ResourceTransformerChain transformerChain) throws IOException {
|
||||
|
||||
resource = transformerChain.transform(exchange, resource);
|
||||
if (!this.fileExtension.equals(StringUtils.getFilenameExtension(resource.getFilename()))) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
|
||||
String content = new String(bytes, DEFAULT_CHARSET);
|
||||
|
||||
if (!content.startsWith(MANIFEST_HEADER)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("AppCache manifest does not start with 'CACHE MANIFEST', skipping: " + resource);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Transforming resource: " + resource);
|
||||
}
|
||||
|
||||
StringWriter contentWriter = new StringWriter();
|
||||
HashBuilder hashBuilder = new HashBuilder(content.length());
|
||||
|
||||
Scanner scanner = new Scanner(content);
|
||||
SectionTransformer currentTransformer = this.sectionTransformers.get(MANIFEST_HEADER);
|
||||
while (scanner.hasNextLine()) {
|
||||
String line = scanner.nextLine();
|
||||
if (this.sectionTransformers.containsKey(line.trim())) {
|
||||
currentTransformer = this.sectionTransformers.get(line.trim());
|
||||
contentWriter.write(line + "\n");
|
||||
hashBuilder.appendString(line);
|
||||
}
|
||||
else {
|
||||
contentWriter.write(
|
||||
currentTransformer.transform(
|
||||
line, hashBuilder, resource, transformerChain, exchange) + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
String hash = hashBuilder.build();
|
||||
contentWriter.write("\n" + "# Hash: " + hash);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]");
|
||||
}
|
||||
|
||||
return new TransformedResource(resource, contentWriter.toString().getBytes(DEFAULT_CHARSET));
|
||||
}
|
||||
|
||||
|
||||
@FunctionalInterface
|
||||
private interface SectionTransformer {
|
||||
|
||||
/**
|
||||
* Transforms a line in a section of the manifest.
|
||||
* <p>The actual transformation depends on the chosen transformation strategy
|
||||
* for the current manifest section (CACHE, NETWORK, FALLBACK, etc).
|
||||
*/
|
||||
String transform(String line, HashBuilder builder, Resource resource,
|
||||
ResourceTransformerChain transformerChain, ServerWebExchange exchange) throws IOException;
|
||||
}
|
||||
|
||||
|
||||
private static class NoOpSection implements SectionTransformer {
|
||||
|
||||
public String transform(String line, HashBuilder builder, Resource resource,
|
||||
ResourceTransformerChain transformerChain, ServerWebExchange exchange) throws IOException {
|
||||
|
||||
builder.appendString(line);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class CacheSection implements SectionTransformer {
|
||||
|
||||
private static final String COMMENT_DIRECTIVE = "#";
|
||||
|
||||
@Override
|
||||
public String transform(String line, HashBuilder builder, Resource resource,
|
||||
ResourceTransformerChain transformerChain, ServerWebExchange exchange) throws IOException {
|
||||
|
||||
if (isLink(line) && !hasScheme(line)) {
|
||||
ResourceResolverChain resolverChain = transformerChain.getResolverChain();
|
||||
Resource appCacheResource =
|
||||
resolverChain.resolveResource(null, line, Collections.singletonList(resource));
|
||||
String path = resolveUrlPath(line, exchange, resource, transformerChain);
|
||||
builder.appendResource(appCacheResource);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Link modified: " + path + " (original: " + line + ")");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
builder.appendString(line);
|
||||
return line;
|
||||
}
|
||||
|
||||
private boolean hasScheme(String link) {
|
||||
int schemeIndex = link.indexOf(":");
|
||||
return (link.startsWith("//") || (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/")));
|
||||
}
|
||||
|
||||
private boolean isLink(String line) {
|
||||
return (StringUtils.hasText(line) && !line.startsWith(COMMENT_DIRECTIVE));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class HashBuilder {
|
||||
|
||||
private final ByteArrayOutputStream baos;
|
||||
|
||||
public HashBuilder(int initialSize) {
|
||||
this.baos = new ByteArrayOutputStream(initialSize);
|
||||
}
|
||||
|
||||
public void appendResource(Resource resource) throws IOException {
|
||||
byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream());
|
||||
this.baos.write(DigestUtils.md5Digest(content));
|
||||
}
|
||||
|
||||
public void appendString(String content) throws IOException {
|
||||
this.baos.write(content.getBytes(DEFAULT_CHARSET));
|
||||
}
|
||||
|
||||
public String build() {
|
||||
return DigestUtils.md5DigestAsHex(this.baos.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link ResourceResolver} that resolves resources from a {@link Cache} or
|
||||
* otherwise delegates to the resolver chain and caches the result.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CachingResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
public static final String RESOLVED_RESOURCE_CACHE_KEY_PREFIX = "resolvedResource:";
|
||||
|
||||
public static final String RESOLVED_URL_PATH_CACHE_KEY_PREFIX = "resolvedUrlPath:";
|
||||
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
|
||||
public CachingResourceResolver(CacheManager cacheManager, String cacheName) {
|
||||
this(cacheManager.getCache(cacheName));
|
||||
}
|
||||
|
||||
public CachingResourceResolver(Cache cache) {
|
||||
Assert.notNull(cache, "Cache is required");
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured {@code Cache}.
|
||||
*/
|
||||
public Cache getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Resource resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
String key = computeKey(exchange, requestPath);
|
||||
Resource resource = this.cache.get(key, Resource.class);
|
||||
|
||||
if (resource != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found match: " + resource);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
resource = chain.resolveResource(exchange, requestPath, locations);
|
||||
if (resource != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Putting resolved resource in cache: " + resource);
|
||||
}
|
||||
this.cache.put(key, resource);
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
protected String computeKey(ServerWebExchange exchange, String requestPath) {
|
||||
StringBuilder key = new StringBuilder(RESOLVED_RESOURCE_CACHE_KEY_PREFIX);
|
||||
key.append(requestPath);
|
||||
if (exchange != null) {
|
||||
String encoding = exchange.getRequest().getHeaders().getFirst("Accept-Encoding");
|
||||
if (encoding != null && encoding.contains("gzip")) {
|
||||
key.append("+encoding=gzip");
|
||||
}
|
||||
}
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
String key = RESOLVED_URL_PATH_CACHE_KEY_PREFIX + resourceUrlPath;
|
||||
String resolvedUrlPath = this.cache.get(key, String.class);
|
||||
|
||||
if (resolvedUrlPath != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found match: \"" + resolvedUrlPath + "\"");
|
||||
}
|
||||
return resolvedUrlPath;
|
||||
}
|
||||
|
||||
resolvedUrlPath = chain.resolveUrlPath(resourceUrlPath, locations);
|
||||
if (resolvedUrlPath != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Putting resolved resource URL path in cache: \"" + resolvedUrlPath + "\"");
|
||||
}
|
||||
this.cache.put(key, resolvedUrlPath);
|
||||
}
|
||||
|
||||
return resolvedUrlPath;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link ResourceTransformer} that checks a {@link Cache} to see if a
|
||||
* previously transformed resource exists in the cache and returns it if found,
|
||||
* or otherwise delegates to the resolver chain and caches the result.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CachingResourceTransformer implements ResourceTransformer {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(CachingResourceTransformer.class);
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
|
||||
public CachingResourceTransformer(CacheManager cacheManager, String cacheName) {
|
||||
this(cacheManager.getCache(cacheName));
|
||||
}
|
||||
|
||||
public CachingResourceTransformer(Cache cache) {
|
||||
Assert.notNull(cache, "Cache is required");
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured {@code Cache}.
|
||||
*/
|
||||
public Cache getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Resource transform(ServerWebExchange exchange, Resource resource,
|
||||
ResourceTransformerChain transformerChain) throws IOException {
|
||||
|
||||
Resource transformed = this.cache.get(resource, Resource.class);
|
||||
if (transformed != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found match: " + transformed);
|
||||
}
|
||||
return transformed;
|
||||
}
|
||||
|
||||
transformed = transformerChain.transform(exchange, resource);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Putting transformed resource in cache: " + transformed);
|
||||
}
|
||||
this.cache.put(resource, transformed);
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
/**
|
||||
* A {@code VersionStrategy} that calculates an Hex MD5 hashes from the content
|
||||
* of the resource and appends it to the file name, e.g.
|
||||
* {@code "styles/main-e36d2e05253c6c7085a91522ce43a0b4.css"}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see VersionResourceResolver
|
||||
*/
|
||||
public class ContentVersionStrategy extends AbstractVersionStrategy {
|
||||
|
||||
|
||||
public ContentVersionStrategy() {
|
||||
super(new FileNameVersionPathStrategy());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getResourceVersion(Resource resource) {
|
||||
try {
|
||||
byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream());
|
||||
return DigestUtils.md5DigestAsHex(content);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Failed to calculate hash for " + resource, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link ResourceTransformer} implementation that modifies links in a CSS
|
||||
* file to match the public URL paths that should be exposed to clients (e.g.
|
||||
* with an MD5 content-based hash inserted in the URL).
|
||||
*
|
||||
* <p>The implementation looks for links in CSS {@code @import} statements and
|
||||
* also inside CSS {@code url()} functions. All links are then passed through the
|
||||
* {@link ResourceResolverChain} and resolved relative to the location of the
|
||||
* containing CSS file. If successfully resolved, the link is modified, otherwise
|
||||
* the original link is preserved.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CssLinkResourceTransformer extends ResourceTransformerSupport {
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final Log logger = LogFactory.getLog(CssLinkResourceTransformer.class);
|
||||
|
||||
private final List<CssLinkParser> linkParsers = new ArrayList<>(2);
|
||||
|
||||
|
||||
public CssLinkResourceTransformer() {
|
||||
this.linkParsers.add(new ImportStatementCssLinkParser());
|
||||
this.linkParsers.add(new UrlFunctionCssLinkParser());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Resource transform(ServerWebExchange exchange, Resource resource,
|
||||
ResourceTransformerChain transformerChain) throws IOException {
|
||||
|
||||
resource = transformerChain.transform(exchange, resource);
|
||||
|
||||
String filename = resource.getFilename();
|
||||
if (!"css".equals(StringUtils.getFilenameExtension(filename))) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Transforming resource: " + resource);
|
||||
}
|
||||
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
|
||||
String content = new String(bytes, DEFAULT_CHARSET);
|
||||
|
||||
Set<CssLinkInfo> infos = new HashSet<>(8);
|
||||
for (CssLinkParser parser : this.linkParsers) {
|
||||
parser.parseLink(content, infos);
|
||||
}
|
||||
|
||||
if (infos.isEmpty()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No links found.");
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
List<CssLinkInfo> sortedInfos = new ArrayList<>(infos);
|
||||
Collections.sort(sortedInfos);
|
||||
|
||||
int index = 0;
|
||||
StringWriter writer = new StringWriter();
|
||||
for (CssLinkInfo info : sortedInfos) {
|
||||
writer.write(content.substring(index, info.getStart()));
|
||||
String link = content.substring(info.getStart(), info.getEnd());
|
||||
String newLink = null;
|
||||
if (!hasScheme(link)) {
|
||||
newLink = resolveUrlPath(link, exchange, resource, transformerChain);
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
if (newLink != null && !link.equals(newLink)) {
|
||||
logger.trace("Link modified: " + newLink + " (original: " + link + ")");
|
||||
}
|
||||
else {
|
||||
logger.trace("Link not modified: " + link);
|
||||
}
|
||||
}
|
||||
writer.write(newLink != null ? newLink : link);
|
||||
index = info.getEnd();
|
||||
}
|
||||
writer.write(content.substring(index));
|
||||
|
||||
return new TransformedResource(resource, writer.toString().getBytes(DEFAULT_CHARSET));
|
||||
}
|
||||
|
||||
private boolean hasScheme(String link) {
|
||||
int schemeIndex = link.indexOf(":");
|
||||
return (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/")) || link.indexOf("//") == 0;
|
||||
}
|
||||
|
||||
|
||||
@FunctionalInterface
|
||||
protected interface CssLinkParser {
|
||||
|
||||
void parseLink(String content, Set<CssLinkInfo> linkInfos);
|
||||
}
|
||||
|
||||
|
||||
protected static abstract class AbstractCssLinkParser implements CssLinkParser {
|
||||
|
||||
/**
|
||||
* Return the keyword to use to search for links.
|
||||
*/
|
||||
protected abstract String getKeyword();
|
||||
|
||||
@Override
|
||||
public void parseLink(String content, Set<CssLinkInfo> linkInfos) {
|
||||
int index = 0;
|
||||
do {
|
||||
index = content.indexOf(getKeyword(), index);
|
||||
if (index == -1) {
|
||||
break;
|
||||
}
|
||||
index = skipWhitespace(content, index + getKeyword().length());
|
||||
if (content.charAt(index) == '\'') {
|
||||
index = addLink(index, "'", content, linkInfos);
|
||||
}
|
||||
else if (content.charAt(index) == '"') {
|
||||
index = addLink(index, "\"", content, linkInfos);
|
||||
}
|
||||
else {
|
||||
index = extractLink(index, content, linkInfos);
|
||||
|
||||
}
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
|
||||
private int skipWhitespace(String content, int index) {
|
||||
while (true) {
|
||||
if (Character.isWhitespace(content.charAt(index))) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
protected int addLink(int index, String endKey, String content, Set<CssLinkInfo> linkInfos) {
|
||||
int start = index + 1;
|
||||
int end = content.indexOf(endKey, start);
|
||||
linkInfos.add(new CssLinkInfo(start, end));
|
||||
return end + endKey.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a keyword match, after whitespaces removed, and when
|
||||
* the next char is neither a single nor double quote.
|
||||
*/
|
||||
protected abstract int extractLink(int index, String content, Set<CssLinkInfo> linkInfos);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class ImportStatementCssLinkParser extends AbstractCssLinkParser {
|
||||
|
||||
@Override
|
||||
protected String getKeyword() {
|
||||
return "@import";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int extractLink(int index, String content, Set<CssLinkInfo> linkInfos) {
|
||||
if (content.substring(index, index + 4).equals("url(")) {
|
||||
// Ignore, UrlLinkParser will take care
|
||||
}
|
||||
else if (logger.isErrorEnabled()) {
|
||||
logger.error("Unexpected syntax for @import link at index " + index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class UrlFunctionCssLinkParser extends AbstractCssLinkParser {
|
||||
|
||||
@Override
|
||||
protected String getKeyword() {
|
||||
return "url(";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int extractLink(int index, String content, Set<CssLinkInfo> linkInfos) {
|
||||
// A url() function without unquoted
|
||||
return addLink(index - 1, ")", content, linkInfos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class CssLinkInfo implements Comparable<CssLinkInfo> {
|
||||
|
||||
private final int start;
|
||||
|
||||
private final int end;
|
||||
|
||||
public CssLinkInfo(int start, int end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public int getStart() {
|
||||
return this.start;
|
||||
}
|
||||
|
||||
public int getEnd() {
|
||||
return this.end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CssLinkInfo other) {
|
||||
return (this.start < other.start ? -1 : (this.start == other.start ? 0 : 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj != null && obj instanceof CssLinkInfo) {
|
||||
CssLinkInfo other = (CssLinkInfo) obj;
|
||||
return (this.start == other.start && this.end == other.end);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.start * 31 + this.end;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ResourceResolverChain} for invoking a list
|
||||
* of {@link ResourceResolver}s.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultResourceResolverChain implements ResourceResolverChain {
|
||||
|
||||
private final List<ResourceResolver> resolvers = new ArrayList<>();
|
||||
|
||||
private int index = -1;
|
||||
|
||||
|
||||
public DefaultResourceResolverChain(List<? extends ResourceResolver> resolvers) {
|
||||
if (resolvers != null) {
|
||||
this.resolvers.addAll(resolvers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Resource resolveResource(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations) {
|
||||
|
||||
ResourceResolver resolver = getNext();
|
||||
if (resolver == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return resolver.resolveResource(exchange, requestPath, locations, this);
|
||||
}
|
||||
finally {
|
||||
this.index--;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveUrlPath(String resourcePath, List<? extends Resource> locations) {
|
||||
ResourceResolver resolver = getNext();
|
||||
if (resolver == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return resolver.resolveUrlPath(resourcePath, locations, this);
|
||||
}
|
||||
finally {
|
||||
this.index--;
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceResolver getNext() {
|
||||
Assert.state(this.index <= this.resolvers.size(),
|
||||
"Current index exceeds the number of configured ResourceResolvers");
|
||||
|
||||
if (this.index == (this.resolvers.size() - 1)) {
|
||||
return null;
|
||||
}
|
||||
this.index++;
|
||||
return this.resolvers.get(this.index);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ResourceTransformerChain} for invoking
|
||||
* a list of {@link ResourceTransformer}s.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultResourceTransformerChain implements ResourceTransformerChain {
|
||||
|
||||
private final ResourceResolverChain resolverChain;
|
||||
|
||||
private final List<ResourceTransformer> transformers = new ArrayList<>();
|
||||
|
||||
private int index = -1;
|
||||
|
||||
|
||||
public DefaultResourceTransformerChain(ResourceResolverChain resolverChain,
|
||||
List<ResourceTransformer> transformers) {
|
||||
|
||||
Assert.notNull(resolverChain, "ResourceResolverChain is required");
|
||||
this.resolverChain = resolverChain;
|
||||
if (transformers != null) {
|
||||
this.transformers.addAll(transformers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ResourceResolverChain getResolverChain() {
|
||||
return this.resolverChain;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Resource transform(ServerWebExchange exchange, Resource resource) throws IOException {
|
||||
ResourceTransformer transformer = getNext();
|
||||
if (transformer == null) {
|
||||
return resource;
|
||||
}
|
||||
try {
|
||||
return transformer.transform(exchange, resource, this);
|
||||
}
|
||||
finally {
|
||||
this.index--;
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceTransformer getNext() {
|
||||
Assert.state(this.index <= this.transformers.size(),
|
||||
"Current index exceeds the number of configured ResourceTransformer's");
|
||||
|
||||
if (this.index == (this.transformers.size() - 1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.index++;
|
||||
return this.transformers.get(this.index);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* Interface for a resource descriptor that describes the encoding
|
||||
* applied to the entire resource content.
|
||||
*
|
||||
* <p>This information is required if the client consuming that resource
|
||||
* needs additional decoding capabilities to retrieve the resource's content.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
* @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.2.2">
|
||||
* HTTP/1.1: Semantics and Content, section 3.1.2.2</a>
|
||||
*/
|
||||
public interface EncodedResource extends Resource {
|
||||
|
||||
/**
|
||||
* The content coding value, as defined in the IANA registry
|
||||
* @return the content encoding
|
||||
* @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.2.1">HTTP/1.1: Semantics
|
||||
* and Content, section 3.1.2.1</a>
|
||||
*/
|
||||
String getContentEncoding();
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* A {@code VersionStrategy} that relies on a fixed version applied as a request
|
||||
* path prefix, e.g. reduced SHA, version name, release date, etc.
|
||||
*
|
||||
* <p>This is useful for example when {@link ContentVersionStrategy} cannot be
|
||||
* used such as when using JavaScript module loaders which are in charge of
|
||||
* loading the JavaScript resources and need to know their relative paths.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see VersionResourceResolver
|
||||
*/
|
||||
public class FixedVersionStrategy extends AbstractVersionStrategy {
|
||||
|
||||
private final String version;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new FixedVersionStrategy with the given version string.
|
||||
* @param version the fixed version string to use
|
||||
*/
|
||||
public FixedVersionStrategy(String version) {
|
||||
super(new PrefixVersionPathStrategy(version));
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getResourceVersion(Resource resource) {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.AbstractResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@code ResourceResolver} that delegates to the chain to locate a resource
|
||||
* and then attempts to find a variation with the ".gz" extension.
|
||||
*
|
||||
* <p>The resolver gets involved only if the "Accept-Encoding" request header
|
||||
* contains the value "gzip" indicating the client accepts gzipped responses.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class GzipResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
@Override
|
||||
protected Resource resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
Resource resource = chain.resolveResource(exchange, requestPath, locations);
|
||||
if ((resource == null) || (exchange != null && !isGzipAccepted(exchange))) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
try {
|
||||
Resource gzipped = new GzippedResource(resource);
|
||||
if (gzipped.exists()) {
|
||||
return gzipped;
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
logger.trace("No gzipped resource for [" + resource.getFilename() + "]", ex);
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private boolean isGzipAccepted(ServerWebExchange exchange) {
|
||||
String value = exchange.getRequest().getHeaders().getFirst("Accept-Encoding");
|
||||
return (value != null && value.toLowerCase().contains("gzip"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveUrlPathInternal(String resourceUrlPath, List<? extends Resource> locations,
|
||||
ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveUrlPath(resourceUrlPath, locations);
|
||||
}
|
||||
|
||||
|
||||
private static final class GzippedResource extends AbstractResource implements EncodedResource {
|
||||
|
||||
private final Resource original;
|
||||
|
||||
private final Resource gzipped;
|
||||
|
||||
public GzippedResource(Resource original) throws IOException {
|
||||
this.original = original;
|
||||
this.gzipped = original.createRelative(original.getFilename() + ".gz");
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return this.gzipped.getInputStream();
|
||||
}
|
||||
|
||||
public boolean exists() {
|
||||
return this.gzipped.exists();
|
||||
}
|
||||
|
||||
public boolean isReadable() {
|
||||
return this.gzipped.isReadable();
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return this.gzipped.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return this.gzipped.isFile();
|
||||
}
|
||||
|
||||
public URL getURL() throws IOException {
|
||||
return this.gzipped.getURL();
|
||||
}
|
||||
|
||||
public URI getURI() throws IOException {
|
||||
return this.gzipped.getURI();
|
||||
}
|
||||
|
||||
public File getFile() throws IOException {
|
||||
return this.gzipped.getFile();
|
||||
}
|
||||
|
||||
public long contentLength() throws IOException {
|
||||
return this.gzipped.contentLength();
|
||||
}
|
||||
|
||||
public long lastModified() throws IOException {
|
||||
return this.gzipped.lastModified();
|
||||
}
|
||||
|
||||
public Resource createRelative(String relativePath) throws IOException {
|
||||
return this.gzipped.createRelative(relativePath);
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return this.original.getFilename();
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.gzipped.getDescription();
|
||||
}
|
||||
|
||||
public String getContentEncoding() {
|
||||
return "gzip";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A simple {@code ResourceResolver} that tries to find a resource under the given
|
||||
* locations matching to the request path.
|
||||
*
|
||||
* <p>This resolver does not delegate to the {@code ResourceResolverChain} and is
|
||||
* expected to be configured at the end in a chain of resolvers.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class PathResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private Resource[] allowedLocations;
|
||||
|
||||
|
||||
/**
|
||||
* By default when a Resource is found, the path of the resolved resource is
|
||||
* compared to ensure it's under the input location where it was found.
|
||||
* However sometimes that may not be the case, e.g. when
|
||||
* {@link CssLinkResourceTransformer}
|
||||
* resolves public URLs of links it contains, the CSS file is the location
|
||||
* and the resources being resolved are css files, images, fonts and others
|
||||
* located in adjacent or parent directories.
|
||||
* <p>This property allows configuring a complete list of locations under
|
||||
* which resources must be so that if a resource is not under the location
|
||||
* relative to which it was found, this list may be checked as well.
|
||||
* <p>By default {@link ResourceWebHandler} initializes this property
|
||||
* to match its list of locations.
|
||||
* @param locations the list of allowed locations
|
||||
*/
|
||||
public void setAllowedLocations(Resource... locations) {
|
||||
this.allowedLocations = locations;
|
||||
}
|
||||
|
||||
public Resource[] getAllowedLocations() {
|
||||
return this.allowedLocations;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Resource resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return getResource(requestPath, locations);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveUrlPathInternal(String path, List<? extends Resource> locations,
|
||||
ResourceResolverChain chain) {
|
||||
|
||||
return (StringUtils.hasText(path) && getResource(path, locations) != null ? path : null);
|
||||
}
|
||||
|
||||
private Resource getResource(String resourcePath, List<? extends Resource> locations) {
|
||||
for (Resource location : locations) {
|
||||
try {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Checking location: " + location);
|
||||
}
|
||||
Resource resource = getResource(resourcePath, location);
|
||||
if (resource != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found match: " + resource);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
else if (logger.isTraceEnabled()) {
|
||||
logger.trace("No match for location: " + location);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
logger.trace("Failure checking for relative resource - trying next location", ex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the resource under the given location.
|
||||
* <p>The default implementation checks if there is a readable
|
||||
* {@code Resource} for the given path relative to the location.
|
||||
* @param resourcePath the path to the resource
|
||||
* @param location the location to check
|
||||
* @return the resource, or {@code null} if none found
|
||||
*/
|
||||
protected Resource getResource(String resourcePath, Resource location) throws IOException {
|
||||
Resource resource = location.createRelative(resourcePath);
|
||||
if (resource.exists() && resource.isReadable()) {
|
||||
if (checkResource(resource, location)) {
|
||||
return resource;
|
||||
}
|
||||
else if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resource path=\"" + resourcePath + "\" was successfully resolved " +
|
||||
"but resource=\"" + resource.getURL() + "\" is neither under the " +
|
||||
"current location=\"" + location.getURL() + "\" nor under any of the " +
|
||||
"allowed locations=" + Arrays.asList(getAllowedLocations()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform additional checks on a resolved resource beyond checking whether the
|
||||
* resources exists and is readable. The default implementation also verifies
|
||||
* the resource is either under the location relative to which it was found or
|
||||
* is under one of the {@link #setAllowedLocations allowed locations}.
|
||||
* @param resource the resource to check
|
||||
* @param location the location relative to which the resource was found
|
||||
* @return "true" if resource is in a valid location, "false" otherwise.
|
||||
*/
|
||||
protected boolean checkResource(Resource resource, Resource location) throws IOException {
|
||||
if (isResourceUnderLocation(resource, location)) {
|
||||
return true;
|
||||
}
|
||||
if (getAllowedLocations() != null) {
|
||||
for (Resource current : getAllowedLocations()) {
|
||||
if (isResourceUnderLocation(resource, current)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException {
|
||||
if (resource.getClass() != location.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String resourcePath;
|
||||
String locationPath;
|
||||
|
||||
if (resource instanceof UrlResource) {
|
||||
resourcePath = resource.getURL().toExternalForm();
|
||||
locationPath = StringUtils.cleanPath(location.getURL().toString());
|
||||
}
|
||||
else if (resource instanceof ClassPathResource) {
|
||||
resourcePath = ((ClassPathResource) resource).getPath();
|
||||
locationPath = StringUtils.cleanPath(((ClassPathResource) location).getPath());
|
||||
}
|
||||
else {
|
||||
resourcePath = resource.getURL().getPath();
|
||||
locationPath = StringUtils.cleanPath(location.getURL().getPath());
|
||||
}
|
||||
|
||||
if (locationPath.equals(resourcePath)) {
|
||||
return true;
|
||||
}
|
||||
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
|
||||
if (!resourcePath.startsWith(locationPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resourcePath.contains("%")) {
|
||||
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
|
||||
if (URLDecoder.decode(resourcePath, "UTF-8").contains("../")) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A strategy for resolving a request to a server-side resource.
|
||||
*
|
||||
* <p>Provides mechanisms for resolving an incoming request to an actual
|
||||
* {@link Resource} and for obtaining the
|
||||
* public URL path that clients should use when requesting the resource.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ResourceResolver {
|
||||
|
||||
/**
|
||||
* Resolve the supplied request and request path to a {@link Resource} that
|
||||
* exists under one of the given resource locations.
|
||||
* @param exchange the current exchange
|
||||
* @param requestPath the portion of the request path to use
|
||||
* @param locations the locations to search in when looking up resources
|
||||
* @param chain the chain of remaining resolvers to delegate to
|
||||
* @return the resolved resource or {@code null} if unresolved
|
||||
*/
|
||||
Resource resolveResource(ServerWebExchange exchange, String requestPath, List<? extends Resource> locations,
|
||||
ResourceResolverChain chain);
|
||||
|
||||
/**
|
||||
* Resolve the externally facing <em>public</em> URL path for clients to use
|
||||
* to access the resource that is located at the given <em>internal</em>
|
||||
* resource path.
|
||||
* <p>This is useful when rendering URL links to clients.
|
||||
* @param resourcePath the internal resource path
|
||||
* @param locations the locations to search in when looking up resources
|
||||
* @param chain the chain of resolvers to delegate to
|
||||
* @return the resolved public URL path or {@code null} if unresolved
|
||||
*/
|
||||
String resolveUrlPath(String resourcePath, List<? extends Resource> locations, ResourceResolverChain chain);
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A contract for invoking a chain of {@link ResourceResolver}s where each resolver
|
||||
* is given a reference to the chain allowing it to delegate when necessary.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ResourceResolverChain {
|
||||
|
||||
/**
|
||||
* Resolve the supplied request and request path to a {@link Resource} that
|
||||
* exists under one of the given resource locations.
|
||||
* @param exchange the current exchange
|
||||
* @param requestPath the portion of the request path to use
|
||||
* @param locations the locations to search in when looking up resources
|
||||
* @return the resolved resource or {@code null} if unresolved
|
||||
*/
|
||||
Resource resolveResource(ServerWebExchange exchange, String requestPath, List<? extends Resource> locations);
|
||||
|
||||
/**
|
||||
* Resolve the externally facing <em>public</em> URL path for clients to use
|
||||
* to access the resource that is located at the given <em>internal</em>
|
||||
* resource path.
|
||||
* <p>This is useful when rendering URL links to clients.
|
||||
* @param resourcePath the internal resource path
|
||||
* @param locations the locations to search in when looking up resources
|
||||
* @return the resolved public URL path or {@code null} if unresolved
|
||||
*/
|
||||
String resolveUrlPath(String resourcePath, List<? extends Resource> locations);
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* An abstraction for transforming the content of a resource.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ResourceTransformer {
|
||||
|
||||
/**
|
||||
* Transform the given resource.
|
||||
* @param exchange the current exchange
|
||||
* @param resource the resource to transform
|
||||
* @param transformerChain the chain of remaining transformers to delegate to
|
||||
* @return the transformed resource (never {@code null})
|
||||
* @throws IOException if the transformation fails
|
||||
*/
|
||||
Resource transform(ServerWebExchange exchange, Resource resource,
|
||||
ResourceTransformerChain transformerChain) throws IOException;
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A contract for invoking a chain of {@link ResourceTransformer}s where each resolver
|
||||
* is given a reference to the chain allowing it to delegate when necessary.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ResourceTransformerChain {
|
||||
|
||||
/**
|
||||
* Return the {@code ResourceResolverChain} that was used to resolve the
|
||||
* {@code Resource} being transformed. This may be needed for resolving
|
||||
* related resources, e.g. links to other resources.
|
||||
*/
|
||||
ResourceResolverChain getResolverChain();
|
||||
|
||||
/**
|
||||
* Transform the given resource.
|
||||
* @param exchange the current exchange
|
||||
* @param resource the candidate resource to transform
|
||||
* @return the transformed or the same resource, never {@code null}
|
||||
* @throws IOException if transformation fails
|
||||
*/
|
||||
Resource transform(ServerWebExchange exchange, Resource resource) throws IOException;
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A base class for a {@code ResourceTransformer} with an optional helper method
|
||||
* for resolving public links within a transformed resource.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class ResourceTransformerSupport implements ResourceTransformer {
|
||||
|
||||
private ResourceUrlProvider resourceUrlProvider;
|
||||
|
||||
|
||||
/**
|
||||
* Configure a {@link ResourceUrlProvider} to use when resolving the public
|
||||
* URL of links in a transformed resource (e.g. import links in a CSS file).
|
||||
* This is required only for links expressed as full paths and not for
|
||||
* relative links.
|
||||
* @param resourceUrlProvider the URL provider to use
|
||||
*/
|
||||
public void setResourceUrlProvider(ResourceUrlProvider resourceUrlProvider) {
|
||||
this.resourceUrlProvider = resourceUrlProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the configured {@code ResourceUrlProvider}.
|
||||
*/
|
||||
public ResourceUrlProvider getResourceUrlProvider() {
|
||||
return this.resourceUrlProvider;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A transformer can use this method when a resource being transformed
|
||||
* contains links to other resources. Such links need to be replaced with the
|
||||
* public facing link as determined by the resource resolver chain (e.g. the
|
||||
* public URL may have a version inserted).
|
||||
* @param resourcePath the path to a resource that needs to be re-written
|
||||
* @param exchange the current exchange
|
||||
* @param resource the resource being transformed
|
||||
* @param transformerChain the transformer chain
|
||||
* @return the resolved URL or null
|
||||
*/
|
||||
protected String resolveUrlPath(String resourcePath, ServerWebExchange exchange,
|
||||
Resource resource, ResourceTransformerChain transformerChain) {
|
||||
|
||||
if (resourcePath.startsWith("/")) {
|
||||
// full resource path
|
||||
ResourceUrlProvider urlProvider = getResourceUrlProvider();
|
||||
return (urlProvider != null ? urlProvider.getForRequestUrl(exchange, resourcePath) : null);
|
||||
}
|
||||
else {
|
||||
// try resolving as relative path
|
||||
return transformerChain.getResolverChain()
|
||||
.resolveUrlPath(resourcePath, Collections.singletonList(resource));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.HttpRequestPathHelper;
|
||||
|
||||
/**
|
||||
* A central component to use to obtain the public URL path that clients should
|
||||
* use to access a static resource.
|
||||
*
|
||||
* <p>This class is aware of Spring MVC handler mappings used to serve static
|
||||
* resources and uses the {@code ResourceResolver} chains of the configured
|
||||
* {@code ResourceHttpRequestHandler}s to make its decisions.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResourceUrlProvider implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private HttpRequestPathHelper urlPathHelper = new HttpRequestPathHelper();
|
||||
|
||||
private PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
private final Map<String, ResourceWebHandler> handlerMap = new LinkedHashMap<>();
|
||||
|
||||
private boolean autodetect = true;
|
||||
|
||||
|
||||
/**
|
||||
* Configure a {@code UrlPathHelper} to use in
|
||||
* {@link #getForRequestUrl(ServerWebExchange, String)}
|
||||
* in order to derive the lookup path for a target request URL path.
|
||||
*/
|
||||
public void setUrlPathHelper(HttpRequestPathHelper urlPathHelper) {
|
||||
this.urlPathHelper = urlPathHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@code UrlPathHelper}.
|
||||
*/
|
||||
public HttpRequestPathHelper getUrlPathHelper() {
|
||||
return this.urlPathHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@code PathMatcher} to use when comparing target lookup path
|
||||
* against resource mappings.
|
||||
*/
|
||||
public void setPathMatcher(PathMatcher pathMatcher) {
|
||||
this.pathMatcher = pathMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@code PathMatcher}.
|
||||
*/
|
||||
public PathMatcher getPathMatcher() {
|
||||
return this.pathMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually configure the resource mappings.
|
||||
* <p><strong>Note:</strong> by default resource mappings are auto-detected
|
||||
* from the Spring {@code ApplicationContext}. However if this property is
|
||||
* used, the auto-detection is turned off.
|
||||
*/
|
||||
public void setHandlerMap(Map<String, ResourceWebHandler> handlerMap) {
|
||||
if (handlerMap != null) {
|
||||
this.handlerMap.clear();
|
||||
this.handlerMap.putAll(handlerMap);
|
||||
this.autodetect = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the resource mappings, either manually configured or auto-detected
|
||||
* when the Spring {@code ApplicationContext} is refreshed.
|
||||
*/
|
||||
public Map<String, ResourceWebHandler> getHandlerMap() {
|
||||
return this.handlerMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code false} if resource mappings were manually configured,
|
||||
* {@code true} otherwise.
|
||||
*/
|
||||
public boolean isAutodetect() {
|
||||
return this.autodetect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
if (isAutodetect()) {
|
||||
this.handlerMap.clear();
|
||||
detectResourceHandlers(event.getApplicationContext());
|
||||
if (this.handlerMap.isEmpty() && logger.isDebugEnabled()) {
|
||||
logger.debug("No resource handling mappings found");
|
||||
}
|
||||
if (!this.handlerMap.isEmpty()) {
|
||||
this.autodetect = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void detectResourceHandlers(ApplicationContext appContext) {
|
||||
logger.debug("Looking for resource handler mappings");
|
||||
|
||||
Map<String, SimpleUrlHandlerMapping> map = appContext.getBeansOfType(SimpleUrlHandlerMapping.class);
|
||||
List<SimpleUrlHandlerMapping> handlerMappings = new ArrayList<>(map.values());
|
||||
AnnotationAwareOrderComparator.sort(handlerMappings);
|
||||
|
||||
for (SimpleUrlHandlerMapping hm : handlerMappings) {
|
||||
for (String pattern : hm.getHandlerMap().keySet()) {
|
||||
Object handler = hm.getHandlerMap().get(pattern);
|
||||
if (handler instanceof ResourceWebHandler) {
|
||||
ResourceWebHandler resourceHandler = (ResourceWebHandler) handler;
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Found resource handler mapping: URL pattern=\"" + pattern + "\", " +
|
||||
"locations=" + resourceHandler.getLocations() + ", " +
|
||||
"resolvers=" + resourceHandler.getResourceResolvers());
|
||||
}
|
||||
this.handlerMap.put(pattern, resourceHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A variation on {@link #getForLookupPath(String)} that accepts a full request
|
||||
* URL path and returns the full request URL path to expose for public use.
|
||||
* @param exchange the current exchange
|
||||
* @param requestUrl the request URL path to resolve
|
||||
* @return the resolved public URL path, or {@code null} if unresolved
|
||||
*/
|
||||
public final String getForRequestUrl(ServerWebExchange exchange, String requestUrl) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Getting resource URL for request URL \"" + requestUrl + "\"");
|
||||
}
|
||||
int prefixIndex = getLookupPathIndex(exchange);
|
||||
int suffixIndex = getQueryParamsIndex(requestUrl);
|
||||
String prefix = requestUrl.substring(0, prefixIndex);
|
||||
String suffix = requestUrl.substring(suffixIndex);
|
||||
String lookupPath = requestUrl.substring(prefixIndex, suffixIndex);
|
||||
String resolvedLookupPath = getForLookupPath(lookupPath);
|
||||
return (resolvedLookupPath != null ? prefix + resolvedLookupPath + suffix : null);
|
||||
}
|
||||
|
||||
private int getLookupPathIndex(ServerWebExchange exchange) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String requestPath = request.getURI().getPath();
|
||||
String lookupPath = getUrlPathHelper().getLookupPathForRequest(exchange);
|
||||
return requestPath.indexOf(lookupPath);
|
||||
}
|
||||
|
||||
private int getQueryParamsIndex(String lookupPath) {
|
||||
int index = lookupPath.indexOf("?");
|
||||
return index > 0 ? index : lookupPath.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the given path against configured resource handler mappings and
|
||||
* if a match is found use the {@code ResourceResolver} chain of the matched
|
||||
* {@code ResourceHttpRequestHandler} to resolve the URL path to expose for
|
||||
* public use.
|
||||
* <p>It is expected that the given path is what Spring uses for
|
||||
* request mapping purposes.
|
||||
* <p>If several handler mappings match, the handler used will be the one
|
||||
* configured with the most specific pattern.
|
||||
* @param lookupPath the lookup path to check
|
||||
* @return the resolved public URL path, or {@code null} if unresolved
|
||||
*/
|
||||
public final String getForLookupPath(String lookupPath) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Getting resource URL for lookup path \"" + lookupPath + "\"");
|
||||
}
|
||||
|
||||
List<String> matchingPatterns = new ArrayList<>();
|
||||
for (String pattern : this.handlerMap.keySet()) {
|
||||
if (getPathMatcher().match(pattern, lookupPath)) {
|
||||
matchingPatterns.add(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchingPatterns.isEmpty()) {
|
||||
Comparator<String> patternComparator = getPathMatcher().getPatternComparator(lookupPath);
|
||||
Collections.sort(matchingPatterns, patternComparator);
|
||||
for (String pattern : matchingPatterns) {
|
||||
String pathWithinMapping = getPathMatcher().extractPathWithinPattern(pattern, lookupPath);
|
||||
String pathMapping = lookupPath.substring(0, lookupPath.indexOf(pathWithinMapping));
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Invoking ResourceResolverChain for URL pattern \"" + pattern + "\"");
|
||||
}
|
||||
ResourceWebHandler handler = this.handlerMap.get(pattern);
|
||||
ResourceResolverChain chain = new DefaultResourceResolverChain(handler.getResourceResolvers());
|
||||
String resolved = chain.resolveUrlPath(pathWithinMapping, handler.getLocations());
|
||||
if (resolved == null) {
|
||||
continue;
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resolved public resource URL path \"" + resolved + "\"");
|
||||
}
|
||||
return pathMapping + resolved;
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("No matching resource mapping for lookup path \"" + lookupPath + "\"");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,500 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.PathExtensionContentTypeResolver;
|
||||
import org.springframework.web.server.MethodNotAllowedException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
|
||||
/**
|
||||
* {@code HttpRequestHandler} that serves static resources in an optimized way
|
||||
* according to the guidelines of Page Speed, YSlow, etc.
|
||||
*
|
||||
* <p>The {@linkplain #setLocations "locations"} property takes a list of Spring
|
||||
* {@link Resource} locations from which static resources are allowed to
|
||||
* be served by this handler. Resources could be served from a classpath location,
|
||||
* e.g. "classpath:/META-INF/public-web-resources/", allowing convenient packaging
|
||||
* and serving of resources such as .js, .css, and others in jar files.
|
||||
*
|
||||
* <p>This request handler may also be configured with a
|
||||
* {@link #setResourceResolvers(List) resourcesResolver} and
|
||||
* {@link #setResourceTransformers(List) resourceTransformer} chains to support
|
||||
* arbitrary resolution and transformation of resources being served. By default a
|
||||
* {@link PathResourceResolver} simply finds resources based on the configured
|
||||
* "locations". An application can configure additional resolvers and
|
||||
* transformers such as the {@link VersionResourceResolver} which can resolve
|
||||
* and prepare URLs for resources with a version in the URL.
|
||||
*
|
||||
* <p>This handler also properly evaluates the {@code Last-Modified} header (if
|
||||
* present) so that a {@code 304} status code will be returned as appropriate,
|
||||
* avoiding unnecessary overhead for resources that are already cached by the
|
||||
* client.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResourceWebHandler
|
||||
implements WebHandler, InitializingBean, SmartInitializingSingleton {
|
||||
|
||||
/** Set of supported HTTP methods */
|
||||
private static final Set<String> SUPPORTED_METHODS = new LinkedHashSet<>(2);
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ResourceWebHandler.class);
|
||||
|
||||
static {
|
||||
SUPPORTED_METHODS.addAll(Arrays.asList("GET", "HEAD"));
|
||||
}
|
||||
|
||||
|
||||
private final List<Resource> locations = new ArrayList<>(4);
|
||||
|
||||
private final List<ResourceResolver> resourceResolvers = new ArrayList<>(4);
|
||||
|
||||
private final List<ResourceTransformer> resourceTransformers = new ArrayList<>(4);
|
||||
|
||||
private CacheControl cacheControl;
|
||||
|
||||
private ResourceHttpMessageWriter resourceHttpMessageWriter;
|
||||
|
||||
private CompositeContentTypeResolver contentTypeResolver;
|
||||
|
||||
private PathExtensionContentTypeResolver pathExtensionResolver;
|
||||
|
||||
|
||||
/**
|
||||
* Set the {@code List} of {@code Resource} paths to use as sources
|
||||
* for serving static resources.
|
||||
*/
|
||||
public void setLocations(List<Resource> locations) {
|
||||
Assert.notNull(locations, "Locations list must not be null");
|
||||
this.locations.clear();
|
||||
this.locations.addAll(locations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@code List} of {@code Resource} paths to use as sources
|
||||
* for serving static resources.
|
||||
*/
|
||||
public List<Resource> getLocations() {
|
||||
return this.locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the list of {@link ResourceResolver}s to use.
|
||||
* <p>By default {@link PathResourceResolver} is configured. If using this property,
|
||||
* it is recommended to add {@link PathResourceResolver} as the last resolver.
|
||||
*/
|
||||
public void setResourceResolvers(List<ResourceResolver> resourceResolvers) {
|
||||
this.resourceResolvers.clear();
|
||||
if (resourceResolvers != null) {
|
||||
this.resourceResolvers.addAll(resourceResolvers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of configured resource resolvers.
|
||||
*/
|
||||
public List<ResourceResolver> getResourceResolvers() {
|
||||
return this.resourceResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the list of {@link ResourceTransformer}s to use.
|
||||
* <p>By default no transformers are configured for use.
|
||||
*/
|
||||
public void setResourceTransformers(List<ResourceTransformer> resourceTransformers) {
|
||||
this.resourceTransformers.clear();
|
||||
if (resourceTransformers != null) {
|
||||
this.resourceTransformers.addAll(resourceTransformers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of configured resource transformers.
|
||||
*/
|
||||
public List<ResourceTransformer> getResourceTransformers() {
|
||||
return this.resourceTransformers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link org.springframework.http.CacheControl} instance to build
|
||||
* the Cache-Control HTTP response header.
|
||||
*/
|
||||
public void setCacheControl(CacheControl cacheControl) {
|
||||
this.cacheControl = cacheControl;
|
||||
}
|
||||
|
||||
public CacheControl getCacheControl() {
|
||||
return this.cacheControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ResourceHttpMessageWriter} to use.
|
||||
* <p>By default a {@link ResourceHttpMessageWriter} will be configured.
|
||||
*/
|
||||
public void setResourceHttpMessageWriter(ResourceHttpMessageWriter httpMessageWriter) {
|
||||
this.resourceHttpMessageWriter = httpMessageWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of configured resource converters.
|
||||
*/
|
||||
public ResourceHttpMessageWriter getResourceHttpMessageWriter() {
|
||||
return this.resourceHttpMessageWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link CompositeContentTypeResolver} to help determine the
|
||||
* media types for resources being served. If the manager contains a path
|
||||
* extension resolver it will be checked for registered file extension.
|
||||
* @param contentTypeResolver the resolver in use
|
||||
*/
|
||||
public void setContentTypeResolver(CompositeContentTypeResolver contentTypeResolver) {
|
||||
this.contentTypeResolver = contentTypeResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link CompositeContentTypeResolver}.
|
||||
*/
|
||||
public CompositeContentTypeResolver getContentTypeResolver() {
|
||||
return this.contentTypeResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) {
|
||||
logger.warn("Locations list is empty. No resources will be served unless a " +
|
||||
"custom ResourceResolver is configured as an alternative to PathResourceResolver.");
|
||||
}
|
||||
if (this.resourceResolvers.isEmpty()) {
|
||||
this.resourceResolvers.add(new PathResourceResolver());
|
||||
}
|
||||
initAllowedLocations();
|
||||
if (this.resourceHttpMessageWriter == null) {
|
||||
this.resourceHttpMessageWriter = new ResourceHttpMessageWriter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for a {@code PathResourceResolver} among the configured resource
|
||||
* resolvers and set its {@code allowedLocations} property (if empty) to
|
||||
* match the {@link #setLocations locations} configured on this class.
|
||||
*/
|
||||
protected void initAllowedLocations() {
|
||||
if (CollectionUtils.isEmpty(this.locations)) {
|
||||
return;
|
||||
}
|
||||
for (int i = getResourceResolvers().size() - 1; i >= 0; i--) {
|
||||
if (getResourceResolvers().get(i) instanceof PathResourceResolver) {
|
||||
PathResourceResolver resolver = (PathResourceResolver) getResourceResolvers().get(i);
|
||||
if (ObjectUtils.isEmpty(resolver.getAllowedLocations())) {
|
||||
resolver.setAllowedLocations(getLocations().toArray(new Resource[getLocations().size()]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
this.pathExtensionResolver = initContentNegotiationStrategy();
|
||||
}
|
||||
|
||||
protected PathExtensionContentTypeResolver initContentNegotiationStrategy() {
|
||||
Map<String, MediaType> mediaTypes = null;
|
||||
if (getContentTypeResolver() != null) {
|
||||
PathExtensionContentTypeResolver strategy =
|
||||
getContentTypeResolver().findResolver(PathExtensionContentTypeResolver.class);
|
||||
if (strategy != null) {
|
||||
mediaTypes = new HashMap<>(strategy.getMediaTypes());
|
||||
}
|
||||
}
|
||||
return new PathExtensionContentTypeResolver(mediaTypes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes a resource request.
|
||||
* <p>Checks for the existence of the requested resource in the configured list of locations.
|
||||
* If the resource does not exist, a {@code 404} response will be returned to the client.
|
||||
* If the resource exists, the request will be checked for the presence of the
|
||||
* {@code Last-Modified} header, and its value will be compared against the last-modified
|
||||
* timestamp of the given resource, returning a {@code 304} status code if the
|
||||
* {@code Last-Modified} value is greater. If the resource is newer than the
|
||||
* {@code Last-Modified} value, or the header is not present, the content resource
|
||||
* of the resource will be written to the response with caching headers
|
||||
* set to expire one year in the future.
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange) {
|
||||
try {
|
||||
// For very general mappings (e.g. "/") we need to check 404 first
|
||||
Resource resource = getResource(exchange);
|
||||
if (resource == null) {
|
||||
logger.trace("No matching resource found - returning 404");
|
||||
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
if (HttpMethod.OPTIONS.equals(exchange.getRequest().getMethod())) {
|
||||
exchange.getResponse().getHeaders().add("Allow", "GET,HEAD,OPTIONS");
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
// Supported methods and required session
|
||||
String httpMehtod = exchange.getRequest().getMethod().name();
|
||||
if (!SUPPORTED_METHODS.contains(httpMehtod)) {
|
||||
return Mono.error(new MethodNotAllowedException(httpMehtod, SUPPORTED_METHODS));
|
||||
}
|
||||
|
||||
// Header phase
|
||||
if (exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) {
|
||||
logger.trace("Resource not modified - returning 304");
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
// Apply cache settings, if any
|
||||
if (getCacheControl() != null) {
|
||||
String value = getCacheControl().getHeaderValue();
|
||||
if (value != null) {
|
||||
exchange.getResponse().getHeaders().setCacheControl(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Check the media type for the resource
|
||||
MediaType mediaType = getMediaType(exchange, resource);
|
||||
if (mediaType != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Determined media type '" + mediaType + "' for " + resource);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No media type found for " + resource + " - not sending a content-type header");
|
||||
}
|
||||
}
|
||||
|
||||
// Content phase
|
||||
if (HttpMethod.HEAD.equals(exchange.getRequest().getMethod())) {
|
||||
setHeaders(exchange, resource, mediaType);
|
||||
logger.trace("HEAD request - skipping content");
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
// TODO: range requests
|
||||
|
||||
setHeaders(exchange, resource, mediaType);
|
||||
return this.resourceHttpMessageWriter.write(Mono.just(resource),
|
||||
ResolvableType.forClass(Resource.class), mediaType, exchange.getResponse());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected Resource getResource(ServerWebExchange exchange) throws IOException {
|
||||
String attrName = HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE;
|
||||
Optional<String> optional = exchange.getAttribute(attrName);
|
||||
if (!optional.isPresent()) {
|
||||
throw new IllegalStateException("Required request attribute '" + attrName + "' is not set");
|
||||
}
|
||||
String path = processPath(optional.get());
|
||||
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Ignoring invalid resource path [" + path + "]");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (path.contains("%")) {
|
||||
try {
|
||||
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
|
||||
if (isInvalidPath(URLDecoder.decode(path, "UTF-8"))) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Ignoring invalid resource path with escape sequences [" + path + "].");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
ResourceResolverChain resolveChain = new DefaultResourceResolverChain(getResourceResolvers());
|
||||
Resource resource = resolveChain.resolveResource(exchange, path, getLocations());
|
||||
if (resource == null || getResourceTransformers().isEmpty()) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
ResourceTransformerChain transformChain =
|
||||
new DefaultResourceTransformerChain(resolveChain, getResourceTransformers());
|
||||
resource = transformChain.transform(exchange, resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given resource path to be used.
|
||||
* <p>The default implementation replaces any combination of leading '/' and
|
||||
* control characters (00-1F and 7F) with a single "/" or "". For example
|
||||
* {@code " // /// //// foo/bar"} becomes {@code "/foo/bar"}.
|
||||
*/
|
||||
protected String processPath(String path) {
|
||||
boolean slash = false;
|
||||
for (int i = 0; i < path.length(); i++) {
|
||||
if (path.charAt(i) == '/') {
|
||||
slash = true;
|
||||
}
|
||||
else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
|
||||
if (i == 0 || (i == 1 && slash)) {
|
||||
return path;
|
||||
}
|
||||
path = slash ? "/" + path.substring(i) : path.substring(i);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Path after trimming leading '/' and control characters: " + path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return (slash ? "/" : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies invalid resource paths. By default rejects:
|
||||
* <ul>
|
||||
* <li>Paths that contain "WEB-INF" or "META-INF"
|
||||
* <li>Paths that contain "../" after a call to
|
||||
* {@link StringUtils#cleanPath}.
|
||||
* <li>Paths that represent a {@link ResourceUtils#isUrl
|
||||
* valid URL} or would represent one after the leading slash is removed.
|
||||
* </ul>
|
||||
* <p><strong>Note:</strong> this method assumes that leading, duplicate '/'
|
||||
* or control characters (e.g. white space) have been trimmed so that the
|
||||
* path starts predictably with a single '/' or does not have one.
|
||||
* @param path the path to validate
|
||||
* @return {@code true} if the path is invalid, {@code false} otherwise
|
||||
*/
|
||||
protected boolean isInvalidPath(String path) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Applying \"invalid path\" checks to path: " + path);
|
||||
}
|
||||
if (path.contains("WEB-INF") || path.contains("META-INF")) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Path contains \"WEB-INF\" or \"META-INF\".");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (path.contains(":/")) {
|
||||
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
|
||||
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Path represents URL or has \"url:\" prefix.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (path.contains("..")) {
|
||||
path = StringUtils.cleanPath(path);
|
||||
if (path.contains("../")) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Path contains \"../\" after call to StringUtils#cleanPath.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the media type for the given request and the resource matched
|
||||
* to it. This implementation tries to determine the MediaType based on the
|
||||
* file extension of the Resource via
|
||||
* {@link PathExtensionContentTypeResolver#resolveMediaTypeForResource(Resource)}.
|
||||
* @param exchange the current exchange
|
||||
* @param resource the resource to check
|
||||
* @return the corresponding media type, or {@code null} if none found
|
||||
*/
|
||||
protected MediaType getMediaType(ServerWebExchange exchange, Resource resource) {
|
||||
return this.pathExtensionResolver.resolveMediaTypeForResource(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set headers on the response. Called for both GET and HEAD requests.
|
||||
* @param exchange current exchange
|
||||
* @param resource the identified resource (never {@code null})
|
||||
* @param mediaType the resource's media type (never {@code null})
|
||||
* @throws IOException in case of errors while setting the headers
|
||||
*/
|
||||
protected void setHeaders(ServerWebExchange exchange, Resource resource, MediaType mediaType)
|
||||
throws IOException {
|
||||
|
||||
HttpHeaders headers = exchange.getResponse().getHeaders();
|
||||
|
||||
long length = resource.contentLength();
|
||||
headers.setContentLength(length);
|
||||
|
||||
if (mediaType != null) {
|
||||
headers.setContentType(mediaType);
|
||||
}
|
||||
if (resource instanceof EncodedResource) {
|
||||
headers.set(HttpHeaders.CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding());
|
||||
}
|
||||
if (resource instanceof VersionedResource) {
|
||||
headers.setETag("\"" + ((VersionedResource) resource).getVersion() + "\"");
|
||||
}
|
||||
headers.set(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ResourceWebHandler [locations=" + getLocations() + ", resolvers=" + getResourceResolvers() + "]";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* An extension of {@link ByteArrayResource}
|
||||
* that a {@link ResourceTransformer} can use to represent an original
|
||||
* resource preserving all other information except the content.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class TransformedResource extends ByteArrayResource {
|
||||
|
||||
private final String filename;
|
||||
|
||||
private final long lastModified;
|
||||
|
||||
|
||||
public TransformedResource(Resource original, byte[] transformedContent) {
|
||||
super(transformedContent);
|
||||
this.filename = original.getFilename();
|
||||
try {
|
||||
this.lastModified = original.lastModified();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// should never happen
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return this.filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() throws IOException {
|
||||
return this.lastModified;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
/**
|
||||
* A strategy for extracting and embedding a resource version in its URL path.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface VersionPathStrategy {
|
||||
|
||||
/**
|
||||
* Extract the resource version from the request path.
|
||||
* @param requestPath the request path to check
|
||||
* @return the version string or {@code null} if none was found
|
||||
*/
|
||||
String extractVersion(String requestPath);
|
||||
|
||||
/**
|
||||
* Remove the version from the request path. It is assumed that the given
|
||||
* version was extracted via {@link #extractVersion(String)}.
|
||||
* @param requestPath the request path of the resource being resolved
|
||||
* @param version the version obtained from {@link #extractVersion(String)}
|
||||
* @return the request path with the version removed
|
||||
*/
|
||||
String removeVersion(String requestPath, String version);
|
||||
|
||||
/**
|
||||
* Add a version to the given request path.
|
||||
* @param requestPath the requestPath
|
||||
* @param version the version
|
||||
* @return the requestPath updated with a version string
|
||||
*/
|
||||
String addVersion(String requestPath, String version);
|
||||
|
||||
}
|
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.io.AbstractResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Resolves request paths containing a version string that can be used as part
|
||||
* of an HTTP caching strategy in which a resource is cached with a date in the
|
||||
* distant future (e.g. 1 year) and cached until the version, and therefore the
|
||||
* URL, is changed.
|
||||
*
|
||||
* <p>Different versioning strategies exist, and this resolver must be configured
|
||||
* with one or more such strategies along with path mappings to indicate which
|
||||
* strategy applies to which resources.
|
||||
*
|
||||
* <p>{@code ContentVersionStrategy} is a good default choice except in cases
|
||||
* where it cannot be used. Most notably the {@code ContentVersionStrategy}
|
||||
* cannot be combined with JavaScript module loaders. For such cases the
|
||||
* {@code FixedVersionStrategy} is a better choice.
|
||||
*
|
||||
* <p>Note that using this resolver to serve CSS files means that the
|
||||
* {@link CssLinkResourceTransformer} should also be used in order to modify
|
||||
* links within CSS files to also contain the appropriate versions generated
|
||||
* by this resolver.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see VersionStrategy
|
||||
*/
|
||||
public class VersionResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
/** Map from path pattern -> VersionStrategy */
|
||||
private final Map<String, VersionStrategy> versionStrategyMap = new LinkedHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* Set a Map with URL paths as keys and {@code VersionStrategy} as values.
|
||||
* <p>Supports direct URL matches and Ant-style pattern matches. For syntax
|
||||
* details, see the {@link AntPathMatcher} javadoc.
|
||||
* @param map map with URLs as keys and version strategies as values
|
||||
*/
|
||||
public void setStrategyMap(Map<String, VersionStrategy> map) {
|
||||
this.versionStrategyMap.clear();
|
||||
this.versionStrategyMap.putAll(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the map with version strategies keyed by path pattern.
|
||||
*/
|
||||
public Map<String, VersionStrategy> getStrategyMap() {
|
||||
return this.versionStrategyMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a content-based version in resource URLs that match the given path
|
||||
* patterns. The version is computed from the content of the file, e.g.
|
||||
* {@code "css/main-e36d2e05253c6c7085a91522ce43a0b4.css"}. This is a good
|
||||
* default strategy to use except when it cannot be, for example when using
|
||||
* JavaScript module loaders, use {@link #addFixedVersionStrategy} instead
|
||||
* for serving JavaScript files.
|
||||
* @param pathPatterns one or more resource URL path patterns
|
||||
* @return the current instance for chained method invocation
|
||||
* @see ContentVersionStrategy
|
||||
*/
|
||||
public VersionResourceResolver addContentVersionStrategy(String... pathPatterns) {
|
||||
addVersionStrategy(new ContentVersionStrategy(), pathPatterns);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a fixed, prefix-based version in resource URLs that match the given
|
||||
* path patterns, for example: <code>"{version}/js/main.js"</code>. This is useful (vs.
|
||||
* content-based versions) when using JavaScript module loaders.
|
||||
* <p>The version may be a random number, the current date, or a value
|
||||
* fetched from a git commit sha, a property file, or environment variable
|
||||
* and set with SpEL expressions in the configuration (e.g. see {@code @Value}
|
||||
* in Java config).
|
||||
* <p>If not done already, variants of the given {@code pathPatterns}, prefixed with
|
||||
* the {@code version} will be also configured. For example, adding a {@code "/js/**"} path pattern
|
||||
* will also cofigure automatically a {@code "/v1.0.0/js/**"} with {@code "v1.0.0"} the
|
||||
* {@code version} String given as an argument.
|
||||
* @param version a version string
|
||||
* @param pathPatterns one or more resource URL path patterns
|
||||
* @return the current instance for chained method invocation
|
||||
* @see FixedVersionStrategy
|
||||
*/
|
||||
public VersionResourceResolver addFixedVersionStrategy(String version, String... pathPatterns) {
|
||||
List<String> patternsList = Arrays.asList(pathPatterns);
|
||||
List<String> prefixedPatterns = new ArrayList<>(pathPatterns.length);
|
||||
String versionPrefix = "/" + version;
|
||||
for (String pattern : patternsList) {
|
||||
prefixedPatterns.add(pattern);
|
||||
if (!pattern.startsWith(versionPrefix) && !patternsList.contains(versionPrefix + pattern)) {
|
||||
prefixedPatterns.add(versionPrefix + pattern);
|
||||
}
|
||||
}
|
||||
return addVersionStrategy(new FixedVersionStrategy(version), prefixedPatterns.toArray(new String[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom VersionStrategy to apply to resource URLs that match the
|
||||
* given path patterns.
|
||||
* @param strategy the custom strategy
|
||||
* @param pathPatterns one or more resource URL path patterns
|
||||
* @return the current instance for chained method invocation
|
||||
* @see VersionStrategy
|
||||
*/
|
||||
public VersionResourceResolver addVersionStrategy(VersionStrategy strategy, String... pathPatterns) {
|
||||
for (String pattern : pathPatterns) {
|
||||
getStrategyMap().put(pattern, strategy);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Resource resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
Resource resolved = chain.resolveResource(exchange, requestPath, locations);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
VersionStrategy versionStrategy = getStrategyForPath(requestPath);
|
||||
if (versionStrategy == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String candidateVersion = versionStrategy.extractVersion(requestPath);
|
||||
if (StringUtils.isEmpty(candidateVersion)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No version found in path \"" + requestPath + "\"");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String simplePath = versionStrategy.removeVersion(requestPath, candidateVersion);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Extracted version from path, re-resolving without version: \"" + simplePath + "\"");
|
||||
}
|
||||
|
||||
Resource baseResource = chain.resolveResource(exchange, simplePath, locations);
|
||||
if (baseResource == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String actualVersion = versionStrategy.getResourceVersion(baseResource);
|
||||
if (candidateVersion.equals(actualVersion)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resource matches extracted version [" + candidateVersion + "]");
|
||||
}
|
||||
return new FileNameVersionedResource(baseResource, candidateVersion);
|
||||
}
|
||||
else {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Potential resource found for \"" + requestPath + "\", but version [" +
|
||||
candidateVersion + "] does not match");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveUrlPathInternal(String resourceUrlPath, List<? extends Resource> locations,
|
||||
ResourceResolverChain chain) {
|
||||
|
||||
String baseUrl = chain.resolveUrlPath(resourceUrlPath, locations);
|
||||
if (StringUtils.hasText(baseUrl)) {
|
||||
VersionStrategy versionStrategy = getStrategyForPath(resourceUrlPath);
|
||||
if (versionStrategy == null) {
|
||||
return null;
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Getting the original resource to determine version " +
|
||||
"for path \"" + resourceUrlPath + "\"");
|
||||
}
|
||||
Resource resource = chain.resolveResource(null, baseUrl, locations);
|
||||
String version = versionStrategy.getResourceVersion(resource);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Determined version [" + version + "] for " + resource);
|
||||
}
|
||||
return versionStrategy.addVersion(baseUrl, version);
|
||||
}
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@code VersionStrategy} for the request path of the requested resource.
|
||||
* @return an instance of a {@code VersionStrategy} or null if none matches that request path
|
||||
*/
|
||||
protected VersionStrategy getStrategyForPath(String requestPath) {
|
||||
String path = "/".concat(requestPath);
|
||||
List<String> matchingPatterns = new ArrayList<>();
|
||||
for (String pattern : this.versionStrategyMap.keySet()) {
|
||||
if (this.pathMatcher.match(pattern, path)) {
|
||||
matchingPatterns.add(pattern);
|
||||
}
|
||||
}
|
||||
if (!matchingPatterns.isEmpty()) {
|
||||
Comparator<String> comparator = this.pathMatcher.getPatternComparator(path);
|
||||
Collections.sort(matchingPatterns, comparator);
|
||||
return this.versionStrategyMap.get(matchingPatterns.get(0));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private class FileNameVersionedResource extends AbstractResource implements VersionedResource {
|
||||
|
||||
private final Resource original;
|
||||
|
||||
private final String version;
|
||||
|
||||
public FileNameVersionedResource(Resource original, String version) {
|
||||
this.original = original;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return this.original.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return this.original.isReadable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return this.original.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return this.original.isFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getURL() throws IOException {
|
||||
return this.original.getURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() throws IOException {
|
||||
return this.original.getURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
return this.original.getFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return this.original.getFilename();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
return this.original.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() throws IOException {
|
||||
return this.original.lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) throws IOException {
|
||||
return this.original.createRelative(relativePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return original.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return original.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* An extension of {@link VersionPathStrategy} that adds a method
|
||||
* to determine the actual version of a {@link Resource}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see VersionResourceResolver
|
||||
*/
|
||||
public interface VersionStrategy extends VersionPathStrategy {
|
||||
|
||||
/**
|
||||
* Determine the version for the given resource.
|
||||
* @param resource the resource to check
|
||||
* @return the version (never {@code null})
|
||||
*/
|
||||
String getResourceVersion(Resource resource);
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* Interface for a resource descriptor that describes its version with a
|
||||
* version string that can be derived from its content and/or metadata.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see VersionResourceResolver
|
||||
*/
|
||||
public interface VersionedResource extends Resource {
|
||||
|
||||
String getVersion();
|
||||
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.webjars.MultipleMatchesException;
|
||||
import org.webjars.WebJarAssetLocator;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@code ResourceResolver} that delegates to the chain to locate a resource and then
|
||||
* attempts to find a matching versioned resource contained in a WebJar JAR file.
|
||||
*
|
||||
* <p>This allows WebJars.org users to write version agnostic paths in their templates,
|
||||
* like {@code <script src="/jquery/jquery.min.js"/>}.
|
||||
* This path will be resolved to the unique version {@code <script src="/jquery/1.2.0/jquery.min.js"/>},
|
||||
* which is a better fit for HTTP caching and version management in applications.
|
||||
*
|
||||
* <p>This also resolves resources for version agnostic HTTP requests {@code "GET /jquery/jquery.min.js"}.
|
||||
*
|
||||
* <p>This resolver requires the "org.webjars:webjars-locator" library on classpath,
|
||||
* and is automatically registered if that library is present.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see <a href="http://www.webjars.org">webjars.org</a>
|
||||
*/
|
||||
public class WebJarsResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private final static String WEBJARS_LOCATION = "META-INF/resources/webjars/";
|
||||
|
||||
private final static int WEBJARS_LOCATION_LENGTH = WEBJARS_LOCATION.length();
|
||||
|
||||
|
||||
private final WebJarAssetLocator webJarAssetLocator;
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@code WebJarsResourceResolver} with a default {@code WebJarAssetLocator} instance.
|
||||
*/
|
||||
public WebJarsResourceResolver() {
|
||||
this(new WebJarAssetLocator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code WebJarsResourceResolver} with a custom {@code WebJarAssetLocator} instance,
|
||||
* e.g. with a custom index.
|
||||
*/
|
||||
public WebJarsResourceResolver(WebJarAssetLocator webJarAssetLocator) {
|
||||
this.webJarAssetLocator = webJarAssetLocator;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Resource resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
Resource resolved = chain.resolveResource(exchange, requestPath, locations);
|
||||
if (resolved == null) {
|
||||
String webJarResourcePath = findWebJarResourcePath(requestPath);
|
||||
if (webJarResourcePath != null) {
|
||||
return chain.resolveResource(exchange, webJarResourcePath, locations);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
String path = chain.resolveUrlPath(resourceUrlPath, locations);
|
||||
if (path == null) {
|
||||
String webJarResourcePath = findWebJarResourcePath(resourceUrlPath);
|
||||
if (webJarResourcePath != null) {
|
||||
return chain.resolveUrlPath(webJarResourcePath, locations);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
protected String findWebJarResourcePath(String path) {
|
||||
try {
|
||||
int startOffset = (path.startsWith("/") ? 1 : 0);
|
||||
int endOffset = path.indexOf("/", 1);
|
||||
if (endOffset != -1) {
|
||||
String webjar = path.substring(startOffset, endOffset);
|
||||
String partialPath = path.substring(endOffset);
|
||||
String webJarPath = webJarAssetLocator.getFullPath(webjar, partialPath);
|
||||
return webJarPath.substring(WEBJARS_LOCATION_LENGTH);
|
||||
}
|
||||
}
|
||||
catch (MultipleMatchesException ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("WebJar version conflict for \"" + path + "\"", ex);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No WebJar resource found for \"" + path + "\"");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Support classes for serving static resources.
|
||||
*/
|
||||
package org.springframework.web.reactive.resource;
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
|
||||
/**
|
||||
* Unit tests for
|
||||
* {@link AppCacheManifestTransformer}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class AppCacheManifestTransformerTests {
|
||||
|
||||
private AppCacheManifestTransformer transformer;
|
||||
|
||||
private ResourceTransformerChain chain;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.transformer = new AppCacheManifestTransformer();
|
||||
this.chain = mock(ResourceTransformerChain.class);
|
||||
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "");
|
||||
ServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void noTransformIfExtensionNoMatch() throws Exception {
|
||||
Resource resource = mock(Resource.class);
|
||||
given(resource.getFilename()).willReturn("foobar.file");
|
||||
given(this.chain.transform(this.exchange, resource)).willReturn(resource);
|
||||
|
||||
Resource result = this.transformer.transform(this.exchange, resource, this.chain);
|
||||
assertEquals(resource, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void syntaxErrorInManifest() throws Exception {
|
||||
Resource resource = new ClassPathResource("test/error.manifest", getClass());
|
||||
given(this.chain.transform(this.exchange, resource)).willReturn(resource);
|
||||
|
||||
Resource result = this.transformer.transform(this.exchange, resource, this.chain);
|
||||
assertEquals(resource, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformManifest() throws Exception {
|
||||
|
||||
VersionResourceResolver versionResolver = new VersionResourceResolver();
|
||||
versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
|
||||
|
||||
PathResourceResolver pathResolver = new PathResourceResolver();
|
||||
pathResolver.setAllowedLocations(new ClassPathResource("test/", getClass()));
|
||||
|
||||
List<ResourceResolver> resolvers = Arrays.asList(versionResolver, pathResolver);
|
||||
ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers);
|
||||
|
||||
List<ResourceTransformer> transformers = new ArrayList<>();
|
||||
transformers.add(new CssLinkResourceTransformer());
|
||||
this.chain = new DefaultResourceTransformerChain(resolverChain, transformers);
|
||||
|
||||
Resource resource = new ClassPathResource("test/appcache.manifest", getClass());
|
||||
Resource result = this.transformer.transform(this.exchange, resource, this.chain);
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(result.getInputStream());
|
||||
String content = new String(bytes, "UTF-8");
|
||||
|
||||
assertThat("should rewrite resource links", content,
|
||||
Matchers.containsString("foo-e36d2e05253c6c7085a91522ce43a0b4.css"));
|
||||
assertThat("should rewrite resource links", content,
|
||||
Matchers.containsString("bar-11e16cf79faee7ac698c805cf28248d2.css"));
|
||||
assertThat("should rewrite resource links", content,
|
||||
Matchers.containsString("js/bar-bd508c62235b832d960298ca6c0b7645.js"));
|
||||
|
||||
assertThat("should not rewrite external resources", content,
|
||||
Matchers.containsString("//example.org/style.css"));
|
||||
assertThat("should not rewrite external resources", content,
|
||||
Matchers.containsString("http://example.org/image.png"));
|
||||
|
||||
assertThat("should generate fingerprint", content,
|
||||
Matchers.containsString("# Hash: 4bf0338bcbeb0a5b3a4ec9ed8864107d"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link CachingResourceResolver}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class CachingResourceResolverTests {
|
||||
|
||||
private Cache cache;
|
||||
|
||||
private ResourceResolverChain chain;
|
||||
|
||||
private List<Resource> locations;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private MockServerHttpRequest request;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
|
||||
this.cache = new ConcurrentMapCache("resourceCache");
|
||||
|
||||
List<ResourceResolver> resolvers = new ArrayList<>();
|
||||
resolvers.add(new CachingResourceResolver(this.cache));
|
||||
resolvers.add(new PathResourceResolver());
|
||||
this.chain = new DefaultResourceResolverChain(resolvers);
|
||||
|
||||
this.locations = new ArrayList<>();
|
||||
this.locations.add(new ClassPathResource("test/", getClass()));
|
||||
|
||||
this.request = new MockServerHttpRequest(HttpMethod.GET, "");
|
||||
ServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void resolveResourceInternal() {
|
||||
String file = "bar.css";
|
||||
Resource expected = new ClassPathResource("test/" + file, getClass());
|
||||
Resource actual = this.chain.resolveResource(this.exchange, file, this.locations);
|
||||
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceInternalFromCache() {
|
||||
|
||||
Resource expected = Mockito.mock(Resource.class);
|
||||
this.cache.put(CachingResourceResolver.RESOLVED_RESOURCE_CACHE_KEY_PREFIX + "bar.css", expected);
|
||||
|
||||
String file = "bar.css";
|
||||
Resource actual = this.chain.resolveResource(this.exchange, file, this.locations);
|
||||
|
||||
assertSame(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceInternalNoMatch() {
|
||||
assertNull(this.chain.resolveResource(this.exchange, "invalid.css", this.locations));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolverUrlPath() {
|
||||
String expected = "/foo.css";
|
||||
String actual = this.chain.resolveUrlPath(expected, this.locations);
|
||||
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolverUrlPathFromCache() {
|
||||
String expected = "cached-imaginary.css";
|
||||
this.cache.put(CachingResourceResolver.RESOLVED_URL_PATH_CACHE_KEY_PREFIX + "imaginary.css", expected);
|
||||
String actual = this.chain.resolveUrlPath("imaginary.css", this.locations);
|
||||
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolverUrlPathNoMatch() {
|
||||
assertNull(this.chain.resolveUrlPath("invalid.css", this.locations));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceAcceptEncodingInCacheKey() {
|
||||
String file = "bar.css";
|
||||
this.request.setUri(file).setHeader("Accept-Encoding", "gzip");
|
||||
|
||||
Resource expected = this.chain.resolveResource(this.exchange, file, this.locations);
|
||||
String cacheKey = CachingResourceResolver.RESOLVED_RESOURCE_CACHE_KEY_PREFIX + file + "+encoding=gzip";
|
||||
|
||||
assertEquals(expected, this.cache.get(cacheKey).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceNoAcceptEncodingInCacheKey() {
|
||||
String file = "bar.css";
|
||||
this.request.setUri(file);
|
||||
|
||||
Resource expected = this.chain.resolveResource(this.exchange, file, this.locations);
|
||||
String cacheKey = CachingResourceResolver.RESOLVED_RESOURCE_CACHE_KEY_PREFIX + file;
|
||||
|
||||
assertEquals(expected, this.cache.get(cacheKey).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceMatchingEncoding() {
|
||||
Resource resource = Mockito.mock(Resource.class);
|
||||
Resource gzResource = Mockito.mock(Resource.class);
|
||||
this.cache.put(CachingResourceResolver.RESOLVED_RESOURCE_CACHE_KEY_PREFIX + "bar.css", resource);
|
||||
this.cache.put(CachingResourceResolver.RESOLVED_RESOURCE_CACHE_KEY_PREFIX + "bar.css+encoding=gzip", gzResource);
|
||||
|
||||
String file = "bar.css";
|
||||
this.request.setUri(file);
|
||||
assertSame(resource, this.chain.resolveResource(this.exchange, file, this.locations));
|
||||
|
||||
request.addHeader("Accept-Encoding", "gzip");
|
||||
assertSame(gzResource, this.chain.resolveResource(this.exchange, file, this.locations));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ContentVersionStrategy}.
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class ContentBasedVersionStrategyTests {
|
||||
|
||||
private ContentVersionStrategy strategy = new ContentVersionStrategy();
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
VersionResourceResolver versionResourceResolver = new VersionResourceResolver();
|
||||
versionResourceResolver.setStrategyMap(Collections.singletonMap("/**", this.strategy));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractVersion() throws Exception {
|
||||
String hash = "7fbe76cdac6093784895bb4989203e5a";
|
||||
String path = "font-awesome/css/font-awesome.min-" + hash + ".css";
|
||||
|
||||
assertEquals(hash, this.strategy.extractVersion(path));
|
||||
assertNull(this.strategy.extractVersion("foo/bar.css"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeVersion() throws Exception {
|
||||
String file = "font-awesome/css/font-awesome.min%s%s.css";
|
||||
String hash = "7fbe76cdac6093784895bb4989203e5a";
|
||||
|
||||
assertEquals(String.format(file, "", ""), this.strategy.removeVersion(String.format(file, "-", hash), hash));
|
||||
assertNull(this.strategy.extractVersion("foo/bar.css"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourceVersion() throws Exception {
|
||||
Resource expected = new ClassPathResource("test/bar.css", getClass());
|
||||
String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream()));
|
||||
assertEquals(hash, this.strategy.getResourceVersion(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addVersionToUrl() throws Exception {
|
||||
String requestPath = "test/bar.css";
|
||||
String version = "123";
|
||||
assertEquals("test/bar-123.css", this.strategy.addVersion(requestPath, version));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link CssLinkResourceTransformer}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class CssLinkResourceTransformerTests {
|
||||
|
||||
private ResourceTransformerChain transformerChain;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
VersionResourceResolver versionResolver = new VersionResourceResolver();
|
||||
versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
|
||||
|
||||
PathResourceResolver pathResolver = new PathResourceResolver();
|
||||
pathResolver.setAllowedLocations(new ClassPathResource("test/", getClass()));
|
||||
|
||||
List<ResourceResolver> resolvers = Arrays.asList(versionResolver, pathResolver);
|
||||
List<ResourceTransformer> transformers = Collections.singletonList(new CssLinkResourceTransformer());
|
||||
|
||||
ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers);
|
||||
this.transformerChain = new DefaultResourceTransformerChain(resolverChain, transformers);
|
||||
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "");
|
||||
ServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void transform() throws Exception {
|
||||
Resource css = new ClassPathResource("test/main.css", getClass());
|
||||
TransformedResource actual = (TransformedResource) this.transformerChain.transform(this.exchange, css);
|
||||
|
||||
String expected = "\n" +
|
||||
"@import url(\"bar-11e16cf79faee7ac698c805cf28248d2.css\");\n" +
|
||||
"@import url('bar-11e16cf79faee7ac698c805cf28248d2.css');\n" +
|
||||
"@import url(bar-11e16cf79faee7ac698c805cf28248d2.css);\n\n" +
|
||||
"@import \"foo-e36d2e05253c6c7085a91522ce43a0b4.css\";\n" +
|
||||
"@import 'foo-e36d2e05253c6c7085a91522ce43a0b4.css';\n\n" +
|
||||
"body { background: url(\"images/image-f448cd1d5dba82b774f3202c878230b3.png\") }\n";
|
||||
|
||||
String result = new String(actual.getByteArray(), "UTF-8");
|
||||
result = StringUtils.deleteAny(result, "\r");
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformNoLinks() throws Exception {
|
||||
Resource expected = new ClassPathResource("test/foo.css", getClass());
|
||||
Resource actual = this.transformerChain.transform(this.exchange, expected);
|
||||
assertSame(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformExtLinksNotAllowed() throws Exception {
|
||||
ResourceResolverChain resolverChain = Mockito.mock(DefaultResourceResolverChain.class);
|
||||
ResourceTransformerChain transformerChain = new DefaultResourceTransformerChain(resolverChain,
|
||||
Collections.singletonList(new CssLinkResourceTransformer()));
|
||||
|
||||
Resource externalCss = new ClassPathResource("test/external.css", getClass());
|
||||
Resource resource = transformerChain.transform(this.exchange, externalCss);
|
||||
TransformedResource transformedResource = (TransformedResource) resource;
|
||||
|
||||
String expected = "@import url(\"http://example.org/fonts/css\");\n" +
|
||||
"body { background: url(\"file:///home/spring/image.png\") }\n" +
|
||||
"figure { background: url(\"//example.org/style.css\")}";
|
||||
String result = new String(transformedResource.getByteArray(), "UTF-8");
|
||||
result = StringUtils.deleteAny(result, "\r");
|
||||
assertEquals(expected, result);
|
||||
|
||||
Mockito.verify(resolverChain, Mockito.never())
|
||||
.resolveUrlPath("http://example.org/fonts/css", Collections.singletonList(externalCss));
|
||||
Mockito.verify(resolverChain, Mockito.never())
|
||||
.resolveUrlPath("file:///home/spring/image.png", Collections.singletonList(externalCss));
|
||||
Mockito.verify(resolverChain, Mockito.never())
|
||||
.resolveUrlPath("//example.org/style.css", Collections.singletonList(externalCss));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformWithNonCssResource() throws Exception {
|
||||
Resource expected = new ClassPathResource("test/images/image.png", getClass());
|
||||
Resource actual = this.transformerChain.transform(this.exchange, expected);
|
||||
assertSame(expected, actual);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link FixedVersionStrategy}.
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class FixedVersionStrategyTests {
|
||||
|
||||
private final String version = "1df341f";
|
||||
|
||||
private final String path = "js/foo.js";
|
||||
|
||||
private FixedVersionStrategy strategy;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.strategy = new FixedVersionStrategy(this.version);
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void emptyPrefixVersion() throws Exception {
|
||||
new FixedVersionStrategy(" ");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractVersion() throws Exception {
|
||||
assertEquals(this.version, this.strategy.extractVersion(this.version + "/" + this.path));
|
||||
assertNull(this.strategy.extractVersion(this.path));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeVersion() throws Exception {
|
||||
assertEquals("/" + this.path, this.strategy.removeVersion(this.version + "/" + this.path, this.version));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addVersion() throws Exception {
|
||||
assertEquals(this.version + "/" + this.path, this.strategy.addVersion("/" + this.path, this.version));
|
||||
}
|
||||
|
||||
@Test // SPR-13727
|
||||
public void addVersionRelativePath() throws Exception {
|
||||
String relativePath = "../" + this.path;
|
||||
assertEquals(relativePath, this.strategy.addVersion(relativePath, this.version));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for {@link GzipResourceResolver}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class GzipResourceResolverTests {
|
||||
|
||||
private ResourceResolverChain resolver;
|
||||
|
||||
private List<Resource> locations;
|
||||
|
||||
private Cache cache;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private MockServerHttpRequest request;
|
||||
|
||||
|
||||
@BeforeClass
|
||||
public static void createGzippedResources() throws IOException {
|
||||
createGzFile("/js/foo.js");
|
||||
createGzFile("foo-e36d2e05253c6c7085a91522ce43a0b4.css");
|
||||
}
|
||||
|
||||
private static void createGzFile(String filePath) throws IOException {
|
||||
Resource location = new ClassPathResource("test/", GzipResourceResolverTests.class);
|
||||
Resource fileResource = new FileSystemResource(location.createRelative(filePath).getFile());
|
||||
Resource gzFileResource = location.createRelative(filePath + ".gz");
|
||||
|
||||
if (gzFileResource.getFile().createNewFile()) {
|
||||
GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(gzFileResource.getFile()));
|
||||
FileCopyUtils.copy(fileResource.getInputStream(), out);
|
||||
}
|
||||
|
||||
assertTrue(gzFileResource.exists());
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.cache = new ConcurrentMapCache("resourceCache");
|
||||
|
||||
Map<String, VersionStrategy> versionStrategyMap = new HashMap<>();
|
||||
versionStrategyMap.put("/**", new ContentVersionStrategy());
|
||||
VersionResourceResolver versionResolver = new VersionResourceResolver();
|
||||
versionResolver.setStrategyMap(versionStrategyMap);
|
||||
|
||||
List<ResourceResolver> resolvers = new ArrayList<>();
|
||||
resolvers.add(new CachingResourceResolver(this.cache));
|
||||
resolvers.add(new GzipResourceResolver());
|
||||
resolvers.add(versionResolver);
|
||||
resolvers.add(new PathResourceResolver());
|
||||
this.resolver = new DefaultResourceResolverChain(resolvers);
|
||||
|
||||
this.locations = new ArrayList<>();
|
||||
this.locations.add(new ClassPathResource("test/", getClass()));
|
||||
this.locations.add(new ClassPathResource("testalternatepath/", getClass()));
|
||||
|
||||
this.request = new MockServerHttpRequest(HttpMethod.GET, "");
|
||||
ServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void resolveGzippedFile() throws IOException {
|
||||
this.request.addHeader("Accept-Encoding", "gzip");
|
||||
String file = "js/foo.js";
|
||||
Resource resolved = this.resolver.resolveResource(this.exchange, file, this.locations);
|
||||
|
||||
String gzFile = file+".gz";
|
||||
Resource resource = new ClassPathResource("test/" + gzFile, getClass());
|
||||
assertEquals(resource.getDescription(), resolved.getDescription());
|
||||
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
|
||||
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
|
||||
resolved instanceof EncodedResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveFingerprintedGzippedFile() throws IOException {
|
||||
this.request.addHeader("Accept-Encoding", "gzip");
|
||||
String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css";
|
||||
Resource resolved = this.resolver.resolveResource(this.exchange, file, this.locations);
|
||||
|
||||
String gzFile = file + ".gz";
|
||||
Resource resource = new ClassPathResource("test/" + gzFile, getClass());
|
||||
assertEquals(resource.getDescription(), resolved.getDescription());
|
||||
assertEquals(new ClassPathResource("test/"+file).getFilename(), resolved.getFilename());
|
||||
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
|
||||
resolved instanceof EncodedResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveFromCacheWithEncodingVariants() throws IOException {
|
||||
this.request.addHeader("Accept-Encoding", "gzip");
|
||||
String file = "js/foo.js";
|
||||
Resource resolved = this.resolver.resolveResource(this.exchange, file, this.locations);
|
||||
|
||||
String gzFile = file+".gz";
|
||||
Resource gzResource = new ClassPathResource("test/"+gzFile, getClass());
|
||||
assertEquals(gzResource.getDescription(), resolved.getDescription());
|
||||
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
|
||||
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
|
||||
resolved instanceof EncodedResource);
|
||||
|
||||
// resolved resource is now cached in CachingResourceResolver
|
||||
|
||||
this.request = new MockServerHttpRequest(HttpMethod.GET, "/js/foo.js");
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultServerWebExchange(this.request, response, new DefaultWebSessionManager());
|
||||
|
||||
resolved = this.resolver.resolveResource(this.exchange, file, this.locations);
|
||||
|
||||
Resource resource = new ClassPathResource("test/"+file, getClass());
|
||||
assertEquals(resource.getDescription(), resolved.getDescription());
|
||||
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
|
||||
assertFalse("Expected " + resolved + " to *not* be of type " + EncodedResource.class,
|
||||
resolved instanceof EncodedResource);
|
||||
}
|
||||
|
||||
@Test // SPR-13149
|
||||
public void resolveWithNullRequest() throws IOException {
|
||||
String file = "js/foo.js";
|
||||
Resource resolved = this.resolver.resolveResource(null, file, this.locations);
|
||||
|
||||
String gzFile = file+".gz";
|
||||
Resource gzResource = new ClassPathResource("test/" + gzFile, getClass());
|
||||
assertEquals(gzResource.getDescription(), resolved.getDescription());
|
||||
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
|
||||
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
|
||||
resolved instanceof EncodedResource);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PathResourceResolver}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class PathResourceResolverTests {
|
||||
|
||||
private final PathResourceResolver resolver = new PathResourceResolver();
|
||||
|
||||
|
||||
@Test
|
||||
public void resolveFromClasspath() throws IOException {
|
||||
Resource location = new ClassPathResource("test/", PathResourceResolver.class);
|
||||
String path = "bar.css";
|
||||
Resource actual = this.resolver.resolveResource(null, path, singletonList(location), null);
|
||||
assertEquals(location.createRelative(path), actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveFromClasspathRoot() throws IOException {
|
||||
Resource location = new ClassPathResource("/");
|
||||
String path = "org/springframework/web/reactive/resource/test/bar.css";
|
||||
Resource actual = this.resolver.resolveResource(null, path, singletonList(location), null);
|
||||
assertNotNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkResource() throws IOException {
|
||||
Resource location = new ClassPathResource("test/", PathResourceResolver.class);
|
||||
testCheckResource(location, "../testsecret/secret.txt");
|
||||
testCheckResource(location, "test/../../testsecret/secret.txt");
|
||||
|
||||
location = new UrlResource(getClass().getResource("./test/"));
|
||||
String secretPath = new UrlResource(getClass().getResource("testsecret/secret.txt")).getURL().getPath();
|
||||
testCheckResource(location, "file:" + secretPath);
|
||||
testCheckResource(location, "/file:" + secretPath);
|
||||
testCheckResource(location, "/" + secretPath);
|
||||
testCheckResource(location, "////../.." + secretPath);
|
||||
testCheckResource(location, "/%2E%2E/testsecret/secret.txt");
|
||||
testCheckResource(location, "/%2e%2e/testsecret/secret.txt");
|
||||
testCheckResource(location, " " + secretPath);
|
||||
testCheckResource(location, "/ " + secretPath);
|
||||
testCheckResource(location, "url:" + secretPath);
|
||||
}
|
||||
|
||||
private void testCheckResource(Resource location, String requestPath) throws IOException {
|
||||
Resource actual = this.resolver.resolveResource(null, requestPath, singletonList(location), null);
|
||||
if (!location.createRelative(requestPath).exists() && !requestPath.contains(":")) {
|
||||
fail(requestPath + " doesn't actually exist as a relative path");
|
||||
}
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkResourceWithAllowedLocations() {
|
||||
this.resolver.setAllowedLocations(
|
||||
new ClassPathResource("test/", PathResourceResolver.class),
|
||||
new ClassPathResource("testalternatepath/", PathResourceResolver.class)
|
||||
);
|
||||
|
||||
Resource location = new ClassPathResource("test/main.css", PathResourceResolver.class);
|
||||
String actual = this.resolver.resolveUrlPath("../testalternatepath/bar.css", singletonList(location), null);
|
||||
assertEquals("../testalternatepath/bar.css", actual);
|
||||
}
|
||||
|
||||
@Test // SPR-12624
|
||||
public void checkRelativeLocation() throws Exception {
|
||||
String locationUrl= new UrlResource(getClass().getResource("./test/")).getURL().toExternalForm();
|
||||
Resource location = new UrlResource(locationUrl.replace("/springframework","/../org/springframework"));
|
||||
assertNotNull(this.resolver.resolveResource(null, "main.css", singletonList(location), null));
|
||||
}
|
||||
|
||||
@Test // SPR-12747
|
||||
public void checkFileLocation() throws Exception {
|
||||
Resource resource = new ClassPathResource("test/main.css", PathResourceResolver.class);
|
||||
assertTrue(this.resolver.checkResource(resource, resource));
|
||||
}
|
||||
|
||||
@Test // SPR-13241
|
||||
public void resolvePathRootResource() throws Exception {
|
||||
Resource webjarsLocation = new ClassPathResource("/META-INF/resources/webjars/", PathResourceResolver.class);
|
||||
String path = this.resolver.resolveUrlPathInternal("", singletonList(webjarsLocation), null);
|
||||
assertNull(path);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Unit tests for {@code ResourceTransformerSupport}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class ResourceTransformerSupportTests {
|
||||
|
||||
private ResourceTransformerChain transformerChain;
|
||||
|
||||
private TestResourceTransformerSupport transformer;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private MockServerHttpRequest request;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
VersionResourceResolver versionResolver = new VersionResourceResolver();
|
||||
versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
|
||||
PathResourceResolver pathResolver = new PathResourceResolver();
|
||||
pathResolver.setAllowedLocations(new ClassPathResource("test/", getClass()));
|
||||
List<ResourceResolver> resolvers = Arrays.asList(versionResolver, pathResolver);
|
||||
this.transformerChain = new DefaultResourceTransformerChain(new DefaultResourceResolverChain(resolvers), null);
|
||||
|
||||
this.transformer = new TestResourceTransformerSupport();
|
||||
this.transformer.setResourceUrlProvider(createResourceUrlProvider(resolvers));
|
||||
|
||||
this.request = new MockServerHttpRequest(HttpMethod.GET, "");
|
||||
ServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(this.request, response, manager);
|
||||
}
|
||||
|
||||
private ResourceUrlProvider createResourceUrlProvider(List<ResourceResolver> resolvers) {
|
||||
ResourceWebHandler handler = new ResourceWebHandler();
|
||||
handler.setLocations(Collections.singletonList(new ClassPathResource("test/", getClass())));
|
||||
handler.setResourceResolvers(resolvers);
|
||||
ResourceUrlProvider urlProvider = new ResourceUrlProvider();
|
||||
urlProvider.setHandlerMap(Collections.singletonMap("/resources/**", handler));
|
||||
return urlProvider;
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void resolveUrlPath() throws Exception {
|
||||
this.request.setUri("/resources/main.css");
|
||||
String resourcePath = "/resources/bar.css";
|
||||
Resource css = new ClassPathResource("test/main.css", getClass());
|
||||
String actual = this.transformer.resolveUrlPath(resourcePath, this.exchange, css, this.transformerChain);
|
||||
assertEquals("/resources/bar-11e16cf79faee7ac698c805cf28248d2.css", actual);
|
||||
assertEquals("/resources/bar-11e16cf79faee7ac698c805cf28248d2.css", actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveUrlPathWithRelativePath() throws Exception {
|
||||
Resource css = new ClassPathResource("test/main.css", getClass());
|
||||
String actual = this.transformer.resolveUrlPath("bar.css", this.exchange, css, this.transformerChain);
|
||||
assertEquals("bar-11e16cf79faee7ac698c805cf28248d2.css", actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveUrlPathWithRelativePathInParentDirectory() throws Exception {
|
||||
Resource imagePng = new ClassPathResource("test/images/image.png", getClass());
|
||||
String actual = this.transformer.resolveUrlPath("../bar.css", this.exchange, imagePng, this.transformerChain);
|
||||
assertEquals("../bar-11e16cf79faee7ac698c805cf28248d2.css", actual);
|
||||
}
|
||||
|
||||
|
||||
private static class TestResourceTransformerSupport extends ResourceTransformerSupport {
|
||||
|
||||
@Override
|
||||
public Resource transform(ServerWebExchange exchange, Resource resource, ResourceTransformerChain chain) {
|
||||
throw new IllegalStateException("Should never be called");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.mock.web.test.MockServletContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ResourceUrlProvider}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ResourceUrlProviderTests {
|
||||
|
||||
private final List<Resource> locations = new ArrayList<>();
|
||||
|
||||
private final ResourceWebHandler handler = new ResourceWebHandler();
|
||||
|
||||
private final Map<String, ResourceWebHandler> handlerMap = new HashMap<>();
|
||||
|
||||
private final ResourceUrlProvider urlProvider = new ResourceUrlProvider();
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
this.locations.add(new ClassPathResource("test/", getClass()));
|
||||
this.locations.add(new ClassPathResource("testalternatepath/", getClass()));
|
||||
this.handler.setLocations(locations);
|
||||
this.handler.afterPropertiesSet();
|
||||
this.handlerMap.put("/resources/**", this.handler);
|
||||
this.urlProvider.setHandlerMap(this.handlerMap);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void getStaticResourceUrl() {
|
||||
String url = this.urlProvider.getForLookupPath("/resources/foo.css");
|
||||
assertEquals("/resources/foo.css", url);
|
||||
}
|
||||
|
||||
@Test // SPR-13374
|
||||
public void getStaticResourceUrlRequestWithRequestParams() {
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "/");
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
|
||||
String url = "/resources/foo.css?foo=bar&url=http://example.org";
|
||||
String resolvedUrl = this.urlProvider.getForRequestUrl(exchange, url);
|
||||
assertEquals(url, resolvedUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFingerprintedResourceUrl() {
|
||||
Map<String, VersionStrategy> versionStrategyMap = new HashMap<>();
|
||||
versionStrategyMap.put("/**", new ContentVersionStrategy());
|
||||
VersionResourceResolver versionResolver = new VersionResourceResolver();
|
||||
versionResolver.setStrategyMap(versionStrategyMap);
|
||||
|
||||
List<ResourceResolver> resolvers = new ArrayList<>();
|
||||
resolvers.add(versionResolver);
|
||||
resolvers.add(new PathResourceResolver());
|
||||
this.handler.setResourceResolvers(resolvers);
|
||||
|
||||
String url = this.urlProvider.getForLookupPath("/resources/foo.css");
|
||||
assertEquals("/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", url);
|
||||
}
|
||||
|
||||
@Test // SPR-12647
|
||||
public void bestPatternMatch() throws Exception {
|
||||
ResourceWebHandler otherHandler = new ResourceWebHandler();
|
||||
otherHandler.setLocations(this.locations);
|
||||
Map<String, VersionStrategy> versionStrategyMap = new HashMap<>();
|
||||
versionStrategyMap.put("/**", new ContentVersionStrategy());
|
||||
VersionResourceResolver versionResolver = new VersionResourceResolver();
|
||||
versionResolver.setStrategyMap(versionStrategyMap);
|
||||
|
||||
List<ResourceResolver> resolvers = new ArrayList<>();
|
||||
resolvers.add(versionResolver);
|
||||
resolvers.add(new PathResourceResolver());
|
||||
otherHandler.setResourceResolvers(resolvers);
|
||||
|
||||
this.handlerMap.put("/resources/*.css", otherHandler);
|
||||
this.urlProvider.setHandlerMap(this.handlerMap);
|
||||
|
||||
String url = this.urlProvider.getForLookupPath("/resources/foo.css");
|
||||
assertEquals("/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", url);
|
||||
}
|
||||
|
||||
@Test // SPR-12592
|
||||
public void initializeOnce() throws Exception {
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.setServletContext(new MockServletContext());
|
||||
context.register(HandlerMappingConfiguration.class);
|
||||
context.refresh();
|
||||
|
||||
ResourceUrlProvider urlProviderBean = context.getBean(ResourceUrlProvider.class);
|
||||
assertThat(urlProviderBean.getHandlerMap(), Matchers.hasKey("/resources/**"));
|
||||
assertFalse(urlProviderBean.isAutodetect());
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
static class HandlerMappingConfiguration {
|
||||
|
||||
@Bean
|
||||
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
|
||||
ResourceWebHandler handler = new ResourceWebHandler();
|
||||
HashMap<String, ResourceWebHandler> handlerMap = new HashMap<>();
|
||||
handlerMap.put("/resources/**", handler);
|
||||
SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
|
||||
hm.setUrlMap(handlerMap);
|
||||
return hm;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceUrlProvider resourceUrlProvider() {
|
||||
return new ResourceUrlProvider();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,594 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.tests.TestSubscriber;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.server.MethodNotAllowedException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.springframework.web.reactive.HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ResourceWebHandler}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ResourceWebHandlerTests {
|
||||
|
||||
private ResourceWebHandler handler;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "");
|
||||
|
||||
private MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
|
||||
private WebSessionManager sessionManager = new DefaultWebSessionManager();
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
List<Resource> paths = new ArrayList<>(2);
|
||||
paths.add(new ClassPathResource("test/", getClass()));
|
||||
paths.add(new ClassPathResource("testalternatepath/", getClass()));
|
||||
paths.add(new ClassPathResource("META-INF/resources/webjars/"));
|
||||
|
||||
this.handler = new ResourceWebHandler();
|
||||
this.handler.setLocations(paths);
|
||||
this.handler.setCacheControl(CacheControl.maxAge(3600, TimeUnit.SECONDS));
|
||||
this.handler.afterPropertiesSet();
|
||||
this.handler.afterSingletonsInstantiated();
|
||||
|
||||
this.exchange = new DefaultServerWebExchange(this.request, this.response, this.sessionManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResource() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
HttpHeaders headers = this.response.getHeaders();
|
||||
assertEquals(MediaType.parseMediaType("text/css"), headers.getContentType());
|
||||
assertEquals(17, headers.getContentLength());
|
||||
assertEquals("max-age=3600", headers.getCacheControl());
|
||||
assertTrue(headers.containsKey("Last-Modified"));
|
||||
assertEquals(headers.getLastModified(), resourceLastModifiedDate("test/foo.css"));
|
||||
assertEquals("bytes", headers.getFirst("Accept-Ranges"));
|
||||
assertEquals(1, headers.get("Accept-Ranges").size());
|
||||
assertResponseBody("h1 { color:red; }");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourceHttpHeader() throws Exception {
|
||||
this.request.setHttpMethod(HttpMethod.HEAD);
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertNull(this.response.getStatusCode());
|
||||
HttpHeaders headers = this.response.getHeaders();
|
||||
assertEquals(MediaType.parseMediaType("text/css"), headers.getContentType());
|
||||
assertEquals(17, headers.getContentLength());
|
||||
assertEquals("max-age=3600", headers.getCacheControl());
|
||||
assertTrue(headers.containsKey("Last-Modified"));
|
||||
assertEquals(headers.getLastModified(), resourceLastModifiedDate("test/foo.css"));
|
||||
assertEquals("bytes", headers.getFirst("Accept-Ranges"));
|
||||
assertEquals(1, headers.get("Accept-Ranges").size());
|
||||
assertNull(this.response.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourceHttpOptions() throws Exception {
|
||||
this.request.setHttpMethod(HttpMethod.OPTIONS);
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertNull(this.response.getStatusCode());
|
||||
assertEquals("GET,HEAD,OPTIONS", this.response.getHeaders().getFirst("Allow"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourceNoCache() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
|
||||
this.handler.setCacheControl(CacheControl.noStore());
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals("no-store", this.response.getHeaders().getCacheControl());
|
||||
assertTrue(this.response.getHeaders().containsKey("Last-Modified"));
|
||||
assertEquals(this.response.getHeaders().getLastModified(), resourceLastModifiedDate("test/foo.css"));
|
||||
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges"));
|
||||
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getVersionedResource() throws Exception {
|
||||
VersionResourceResolver versionResolver = new VersionResourceResolver();
|
||||
versionResolver.addFixedVersionStrategy("versionString", "/**");
|
||||
this.handler.setResourceResolvers(Arrays.asList(versionResolver, new PathResourceResolver()));
|
||||
this.handler.afterPropertiesSet();
|
||||
this.handler.afterSingletonsInstantiated();
|
||||
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "versionString/foo.css");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals("\"versionString\"", this.response.getHeaders().getETag());
|
||||
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges"));
|
||||
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourceWithHtmlMediaType() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.html");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
HttpHeaders headers = this.response.getHeaders();
|
||||
assertEquals(MediaType.TEXT_HTML, headers.getContentType());
|
||||
assertEquals("max-age=3600", headers.getCacheControl());
|
||||
assertTrue(headers.containsKey("Last-Modified"));
|
||||
assertEquals(headers.getLastModified(), resourceLastModifiedDate("test/foo.html"));
|
||||
assertEquals("bytes", headers.getFirst("Accept-Ranges"));
|
||||
assertEquals(1, headers.get("Accept-Ranges").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourceFromAlternatePath() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "baz.css");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
HttpHeaders headers = this.response.getHeaders();
|
||||
assertEquals(MediaType.parseMediaType("text/css"), headers.getContentType());
|
||||
assertEquals(17, headers.getContentLength());
|
||||
assertEquals("max-age=3600", headers.getCacheControl());
|
||||
assertTrue(headers.containsKey("Last-Modified"));
|
||||
assertEquals(headers.getLastModified(), resourceLastModifiedDate("testalternatepath/baz.css"));
|
||||
assertEquals("bytes", headers.getFirst("Accept-Ranges"));
|
||||
assertEquals(1, headers.get("Accept-Ranges").size());
|
||||
assertResponseBody("h1 { color:red; }");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourceFromSubDirectory() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "js/foo.js");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(MediaType.parseMediaType("text/javascript"), this.response.getHeaders().getContentType());
|
||||
assertResponseBody("function foo() { console.log(\"hello world\"); }");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourceFromSubDirectoryOfAlternatePath() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "js/baz.js");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(MediaType.parseMediaType("text/javascript"), this.response.getHeaders().getContentType());
|
||||
assertResponseBody("function foo() { console.log(\"hello world\"); }");
|
||||
}
|
||||
|
||||
@Test // SPR-13658
|
||||
public void getResourceWithRegisteredMediaType() throws Exception {
|
||||
CompositeContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder()
|
||||
.mediaType("css", new MediaType("foo", "bar"))
|
||||
.build();
|
||||
|
||||
List<Resource> paths = Collections.singletonList(new ClassPathResource("test/", getClass()));
|
||||
ResourceWebHandler handler = new ResourceWebHandler();
|
||||
handler.setLocations(paths);
|
||||
handler.setContentTypeResolver(contentTypeResolver);
|
||||
handler.afterPropertiesSet();
|
||||
handler.afterSingletonsInstantiated();
|
||||
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
|
||||
handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(MediaType.parseMediaType("foo/bar"), this.response.getHeaders().getContentType());
|
||||
assertResponseBody("h1 { color:red; }");
|
||||
}
|
||||
|
||||
@Test // SPR-14577
|
||||
public void getMediaTypeWithFavorPathExtensionOff() throws Exception {
|
||||
CompositeContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder()
|
||||
.favorPathExtension(false)
|
||||
.build();
|
||||
|
||||
List<Resource> paths = Collections.singletonList(new ClassPathResource("test/", getClass()));
|
||||
ResourceWebHandler handler = new ResourceWebHandler();
|
||||
handler.setLocations(paths);
|
||||
handler.setContentTypeResolver(contentTypeResolver);
|
||||
handler.afterPropertiesSet();
|
||||
handler.afterSingletonsInstantiated();
|
||||
|
||||
this.request.addHeader("Accept", "application/json,text/plain,*/*");
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.html");
|
||||
handler.handle(this.exchange);
|
||||
|
||||
assertEquals(MediaType.TEXT_HTML, this.response.getHeaders().getContentType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidPath() throws Exception {
|
||||
for (HttpMethod method : HttpMethod.values()) {
|
||||
this.request = new MockServerHttpRequest(HttpMethod.GET, "");
|
||||
this.response = new MockServerHttpResponse();
|
||||
this.sessionManager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(this.request, this.response, this.sessionManager);
|
||||
testInvalidPath(method);
|
||||
}
|
||||
}
|
||||
|
||||
private void testInvalidPath(HttpMethod httpMethod) throws Exception {
|
||||
this.request.setHttpMethod(httpMethod);
|
||||
|
||||
Resource location = new ClassPathResource("test/", getClass());
|
||||
this.handler.setLocations(Collections.singletonList(location));
|
||||
|
||||
testInvalidPath(location, "../testsecret/secret.txt");
|
||||
testInvalidPath(location, "test/../../testsecret/secret.txt");
|
||||
testInvalidPath(location, ":/../../testsecret/secret.txt");
|
||||
|
||||
location = new UrlResource(getClass().getResource("./test/"));
|
||||
this.handler.setLocations(Collections.singletonList(location));
|
||||
Resource secretResource = new UrlResource(getClass().getResource("testsecret/secret.txt"));
|
||||
String secretPath = secretResource.getURL().getPath();
|
||||
|
||||
testInvalidPath(location, "file:" + secretPath);
|
||||
testInvalidPath(location, "/file:" + secretPath);
|
||||
testInvalidPath(location, "url:" + secretPath);
|
||||
testInvalidPath(location, "/url:" + secretPath);
|
||||
testInvalidPath(location, "/" + secretPath);
|
||||
testInvalidPath(location, "////../.." + secretPath);
|
||||
testInvalidPath(location, "/%2E%2E/testsecret/secret.txt");
|
||||
testInvalidPath(location, "/ " + secretPath);
|
||||
testInvalidPath(location, "url:" + secretPath);
|
||||
}
|
||||
|
||||
private void testInvalidPath(Resource location, String requestPath) throws Exception {
|
||||
this.response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultServerWebExchange(this.request, this.response, this.sessionManager);
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, requestPath);
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
if (!location.createRelative(requestPath).exists() && !requestPath.contains(":")) {
|
||||
fail(requestPath + " doesn't actually exist as a relative path");
|
||||
}
|
||||
assertEquals(HttpStatus.NOT_FOUND, this.response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoreInvalidEscapeSequence() throws Exception {
|
||||
this.response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultServerWebExchange(this.request, this.response, this.sessionManager);
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/%foo%/bar.txt");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
assertEquals(HttpStatus.NOT_FOUND, this.response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processPath() throws Exception {
|
||||
assertSame("/foo/bar", this.handler.processPath("/foo/bar"));
|
||||
assertSame("foo/bar", this.handler.processPath("foo/bar"));
|
||||
|
||||
// leading whitespace control characters (00-1F)
|
||||
assertEquals("/foo/bar", this.handler.processPath(" /foo/bar"));
|
||||
assertEquals("/foo/bar", this.handler.processPath((char) 1 + "/foo/bar"));
|
||||
assertEquals("/foo/bar", this.handler.processPath((char) 31 + "/foo/bar"));
|
||||
assertEquals("foo/bar", this.handler.processPath(" foo/bar"));
|
||||
assertEquals("foo/bar", this.handler.processPath((char) 31 + "foo/bar"));
|
||||
|
||||
// leading control character 0x7F (DEL)
|
||||
assertEquals("/foo/bar", this.handler.processPath((char) 127 + "/foo/bar"));
|
||||
assertEquals("/foo/bar", this.handler.processPath((char) 127 + "/foo/bar"));
|
||||
|
||||
// leading control and '/' characters
|
||||
assertEquals("/foo/bar", this.handler.processPath(" / foo/bar"));
|
||||
assertEquals("/foo/bar", this.handler.processPath(" / / foo/bar"));
|
||||
assertEquals("/foo/bar", this.handler.processPath(" // /// //// foo/bar"));
|
||||
assertEquals("/foo/bar", this.handler.processPath((char) 1 + " / " + (char) 127 + " // foo/bar"));
|
||||
|
||||
// root or empty path
|
||||
assertEquals("", this.handler.processPath(" "));
|
||||
assertEquals("/", this.handler.processPath("/"));
|
||||
assertEquals("/", this.handler.processPath("///"));
|
||||
assertEquals("/", this.handler.processPath("/ / / "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initAllowedLocations() throws Exception {
|
||||
PathResourceResolver resolver = (PathResourceResolver) this.handler.getResourceResolvers().get(0);
|
||||
Resource[] locations = resolver.getAllowedLocations();
|
||||
|
||||
assertEquals(3, locations.length);
|
||||
assertEquals("test/", ((ClassPathResource) locations[0]).getPath());
|
||||
assertEquals("testalternatepath/", ((ClassPathResource) locations[1]).getPath());
|
||||
assertEquals("META-INF/resources/webjars/", ((ClassPathResource) locations[2]).getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initAllowedLocationsWithExplicitConfiguration() throws Exception {
|
||||
ClassPathResource location1 = new ClassPathResource("test/", getClass());
|
||||
ClassPathResource location2 = new ClassPathResource("testalternatepath/", getClass());
|
||||
|
||||
PathResourceResolver pathResolver = new PathResourceResolver();
|
||||
pathResolver.setAllowedLocations(location1);
|
||||
|
||||
ResourceWebHandler handler = new ResourceWebHandler();
|
||||
handler.setResourceResolvers(Collections.singletonList(pathResolver));
|
||||
handler.setLocations(Arrays.asList(location1, location2));
|
||||
handler.afterPropertiesSet();
|
||||
handler.afterSingletonsInstantiated();
|
||||
|
||||
Resource[] locations = pathResolver.getAllowedLocations();
|
||||
assertEquals(1, locations.length);
|
||||
assertEquals("test/", ((ClassPathResource) locations[0]).getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notModified() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
|
||||
this.request.getHeaders().setIfModifiedSince(resourceLastModified("test/foo.css"));
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
assertEquals(HttpStatus.NOT_MODIFIED, this.response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void modified() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
|
||||
this.request.getHeaders().setIfModifiedSince(resourceLastModified("test/foo.css") / 1000 * 1000 - 1);
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertNull(this.response.getStatusCode());
|
||||
assertResponseBody("h1 { color:red; }");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directory() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "js/");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
assertEquals(HttpStatus.NOT_FOUND, this.response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directoryInJarFile() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "underscorejs/");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertNull(this.response.getStatusCode());
|
||||
assertEquals(0, this.response.getHeaders().getContentLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingResourcePath() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
assertEquals(HttpStatus.NOT_FOUND, this.response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void noPathWithinHandlerMappingAttribute() throws Exception {
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
}
|
||||
|
||||
@Test(expected = MethodNotAllowedException.class)
|
||||
public void unsupportedHttpMethod() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
|
||||
this.request.setHttpMethod(HttpMethod.POST);
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resourceNotFound() throws Exception {
|
||||
for (HttpMethod method : HttpMethod.values()) {
|
||||
this.request = new MockServerHttpRequest(HttpMethod.GET, "");
|
||||
this.response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultServerWebExchange(this.request, this.response, this.sessionManager);
|
||||
resourceNotFound(method);
|
||||
}
|
||||
}
|
||||
|
||||
private void resourceNotFound(HttpMethod httpMethod) throws Exception {
|
||||
this.request.setHttpMethod(httpMethod);
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "not-there.css");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
assertEquals(HttpStatus.NOT_FOUND, this.response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void partialContentByteRange() throws Exception {
|
||||
this.request.addHeader("Range", "bytes=0-1");
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(HttpStatus.PARTIAL_CONTENT, this.response.getStatusCode());
|
||||
assertEquals(MediaType.TEXT_PLAIN, this.response.getHeaders().getContentType());
|
||||
assertEquals(2, this.response.getHeaders().getContentLength());
|
||||
assertEquals("bytes 0-1/10", this.response.getHeaders().getFirst("Content-Range"));
|
||||
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges"));
|
||||
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size());
|
||||
assertResponseBody("So");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void partialContentByteRangeNoEnd() throws Exception {
|
||||
this.request.addHeader("Range", "bytes=9-");
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(HttpStatus.PARTIAL_CONTENT, this.response.getStatusCode());
|
||||
assertEquals(MediaType.TEXT_PLAIN, this.response.getHeaders().getContentType());
|
||||
assertEquals(1, this.response.getHeaders().getContentLength());
|
||||
assertEquals("bytes 9-9/10", this.response.getHeaders().getFirst("Content-Range"));
|
||||
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges"));
|
||||
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size());
|
||||
assertResponseBody(".");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void partialContentByteRangeLargeEnd() throws Exception {
|
||||
this.request.addHeader("Range", "bytes=9-10000");
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(HttpStatus.PARTIAL_CONTENT, this.response.getStatusCode());
|
||||
assertEquals(MediaType.TEXT_PLAIN, this.response.getHeaders().getContentType());
|
||||
assertEquals(1, this.response.getHeaders().getContentLength());
|
||||
assertEquals("bytes 9-9/10", this.response.getHeaders().getFirst("Content-Range"));
|
||||
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges"));
|
||||
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size());
|
||||
assertResponseBody(".");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void partialContentSuffixRange() throws Exception {
|
||||
this.request.addHeader("Range", "bytes=-1");
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(HttpStatus.PARTIAL_CONTENT, this.response.getStatusCode());
|
||||
assertEquals(MediaType.TEXT_PLAIN, this.response.getHeaders().getContentType());
|
||||
assertEquals(1, this.response.getHeaders().getContentLength());
|
||||
assertEquals("bytes 9-9/10", this.response.getHeaders().getFirst("Content-Range"));
|
||||
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges"));
|
||||
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size());
|
||||
assertResponseBody(".");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void partialContentSuffixRangeLargeSuffix() throws Exception {
|
||||
this.request.addHeader("Range", "bytes=-11");
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(HttpStatus.PARTIAL_CONTENT, this.response.getStatusCode());
|
||||
assertEquals(MediaType.TEXT_PLAIN, this.response.getHeaders().getContentType());
|
||||
assertEquals(10, this.response.getHeaders().getContentLength());
|
||||
assertEquals("bytes 0-9/10", this.response.getHeaders().getFirst("Content-Range"));
|
||||
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges"));
|
||||
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size());
|
||||
assertResponseBody("Some text.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void partialContentInvalidRangeHeader() throws Exception {
|
||||
this.request.addHeader("Range", "bytes= foo bar");
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE, this.response.getStatusCode());
|
||||
assertEquals("bytes */10", this.response.getHeaders().getFirst("Content-Range"));
|
||||
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges"));
|
||||
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void partialContentMultipleByteRanges() throws Exception {
|
||||
this.request.addHeader("Range", "bytes=0-1, 4-5, 8-9");
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals(HttpStatus.PARTIAL_CONTENT, this.response.getStatusCode());
|
||||
assertTrue(this.response.getHeaders().getContentType().toString()
|
||||
.startsWith("multipart/byteranges; boundary="));
|
||||
|
||||
String boundary = "--" + this.response.getHeaders().getContentType().toString().substring(31);
|
||||
|
||||
TestSubscriber.subscribe(this.response.getBody())
|
||||
.assertValuesWith(buf -> {
|
||||
|
||||
String content = DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8);
|
||||
String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true);
|
||||
|
||||
assertEquals(boundary, ranges[0]);
|
||||
assertEquals("Content-Type: text/plain", ranges[1]);
|
||||
assertEquals("Content-Range: bytes 0-1/10", ranges[2]);
|
||||
assertEquals("So", ranges[3]);
|
||||
|
||||
assertEquals(boundary, ranges[4]);
|
||||
assertEquals("Content-Type: text/plain", ranges[5]);
|
||||
assertEquals("Content-Range: bytes 4-5/10", ranges[6]);
|
||||
assertEquals(" t", ranges[7]);
|
||||
|
||||
assertEquals(boundary, ranges[8]);
|
||||
assertEquals("Content-Type: text/plain", ranges[9]);
|
||||
assertEquals("Content-Range: bytes 8-9/10", ranges[10]);
|
||||
assertEquals("t.", ranges[11]);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Test // SPR-14005
|
||||
public void doOverwriteExistingCacheControlHeaders() throws Exception {
|
||||
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
|
||||
this.response.getHeaders().setCacheControl(CacheControl.noStore().getHeaderValue());
|
||||
|
||||
this.handler.handle(this.exchange).blockMillis(5000);
|
||||
|
||||
assertEquals("max-age=3600", this.response.getHeaders().getCacheControl());
|
||||
}
|
||||
|
||||
private long resourceLastModified(String resourceName) throws IOException {
|
||||
return new ClassPathResource(resourceName, getClass()).getFile().lastModified();
|
||||
}
|
||||
|
||||
private long resourceLastModifiedDate(String resourceName) throws IOException {
|
||||
return new ClassPathResource(resourceName, getClass()).getFile().lastModified();
|
||||
}
|
||||
|
||||
private void assertResponseBody(String responseBody) {
|
||||
TestSubscriber.subscribe(this.response.getBody())
|
||||
.assertValuesWith(buf -> assertEquals(responseBody,
|
||||
DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.BDDMockito.never;
|
||||
import static org.mockito.BDDMockito.times;
|
||||
import static org.mockito.BDDMockito.verify;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link VersionResourceResolver}.
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class VersionResourceResolverTests {
|
||||
|
||||
private List<Resource> locations;
|
||||
|
||||
private VersionResourceResolver resolver;
|
||||
|
||||
private ResourceResolverChain chain;
|
||||
|
||||
private VersionStrategy versionStrategy;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.locations = new ArrayList<>();
|
||||
this.locations.add(new ClassPathResource("test/", getClass()));
|
||||
this.locations.add(new ClassPathResource("testalternatepath/", getClass()));
|
||||
|
||||
this.resolver = new VersionResourceResolver();
|
||||
this.chain = mock(ResourceResolverChain.class);
|
||||
this.versionStrategy = mock(VersionStrategy.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceExisting() throws Exception {
|
||||
String file = "bar.css";
|
||||
Resource expected = new ClassPathResource("test/" + file, getClass());
|
||||
given(this.chain.resolveResource(null, file, this.locations)).willReturn(expected);
|
||||
|
||||
this.resolver.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
|
||||
Resource actual = this.resolver.resolveResourceInternal(null, file, this.locations, this.chain);
|
||||
assertEquals(expected, actual);
|
||||
verify(this.chain, times(1)).resolveResource(null, file, this.locations);
|
||||
verify(this.versionStrategy, never()).extractVersion(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceNoVersionStrategy() throws Exception {
|
||||
String file = "missing.css";
|
||||
given(this.chain.resolveResource(null, file, this.locations)).willReturn(null);
|
||||
|
||||
this.resolver.setStrategyMap(Collections.emptyMap());
|
||||
Resource actual = this.resolver.resolveResourceInternal(null, file, this.locations, this.chain);
|
||||
assertNull(actual);
|
||||
verify(this.chain, times(1)).resolveResource(null, file, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceNoVersionInPath() throws Exception {
|
||||
String file = "bar.css";
|
||||
given(this.chain.resolveResource(null, file, this.locations)).willReturn(null);
|
||||
given(this.versionStrategy.extractVersion(file)).willReturn("");
|
||||
|
||||
this.resolver.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
|
||||
Resource actual = this.resolver.resolveResourceInternal(null, file, this.locations, this.chain);
|
||||
assertNull(actual);
|
||||
verify(this.chain, times(1)).resolveResource(null, file, this.locations);
|
||||
verify(this.versionStrategy, times(1)).extractVersion(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceNoResourceAfterVersionRemoved() throws Exception {
|
||||
String versionFile = "bar-version.css";
|
||||
String version = "version";
|
||||
String file = "bar.css";
|
||||
given(this.chain.resolveResource(null, versionFile, this.locations)).willReturn(null);
|
||||
given(this.chain.resolveResource(null, file, this.locations)).willReturn(null);
|
||||
given(this.versionStrategy.extractVersion(versionFile)).willReturn(version);
|
||||
given(this.versionStrategy.removeVersion(versionFile, version)).willReturn(file);
|
||||
|
||||
this.resolver.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
|
||||
Resource actual = this.resolver.resolveResourceInternal(null, versionFile, this.locations, this.chain);
|
||||
assertNull(actual);
|
||||
verify(this.versionStrategy, times(1)).removeVersion(versionFile, version);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceVersionDoesNotMatch() throws Exception {
|
||||
String versionFile = "bar-version.css";
|
||||
String version = "version";
|
||||
String file = "bar.css";
|
||||
Resource expected = new ClassPathResource("test/" + file, getClass());
|
||||
given(this.chain.resolveResource(null, versionFile, this.locations)).willReturn(null);
|
||||
given(this.chain.resolveResource(null, file, this.locations)).willReturn(expected);
|
||||
given(this.versionStrategy.extractVersion(versionFile)).willReturn(version);
|
||||
given(this.versionStrategy.removeVersion(versionFile, version)).willReturn(file);
|
||||
given(this.versionStrategy.getResourceVersion(expected)).willReturn("newer-version");
|
||||
|
||||
this.resolver.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
|
||||
Resource actual = this.resolver.resolveResourceInternal(null, versionFile, this.locations, this.chain);
|
||||
assertNull(actual);
|
||||
verify(this.versionStrategy, times(1)).getResourceVersion(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceSuccess() throws Exception {
|
||||
String versionFile = "bar-version.css";
|
||||
String version = "version";
|
||||
String file = "bar.css";
|
||||
Resource expected = new ClassPathResource("test/" + file, getClass());
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "/resources/bar-version.css");
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
|
||||
ServerWebExchange exchange = new DefaultServerWebExchange(request, response, sessionManager);
|
||||
given(this.chain.resolveResource(exchange, versionFile, this.locations)).willReturn(null);
|
||||
given(this.chain.resolveResource(exchange, file, this.locations)).willReturn(expected);
|
||||
given(this.versionStrategy.extractVersion(versionFile)).willReturn(version);
|
||||
given(this.versionStrategy.removeVersion(versionFile, version)).willReturn(file);
|
||||
given(this.versionStrategy.getResourceVersion(expected)).willReturn(version);
|
||||
|
||||
this.resolver.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
|
||||
Resource actual = this.resolver.resolveResourceInternal(exchange, versionFile, this.locations, this.chain);
|
||||
assertEquals(expected.getFilename(), actual.getFilename());
|
||||
verify(this.versionStrategy, times(1)).getResourceVersion(expected);
|
||||
assertThat(actual, instanceOf(VersionedResource.class));
|
||||
assertEquals(version, ((VersionedResource)actual).getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getStrategyForPath() throws Exception {
|
||||
Map<String, VersionStrategy> strategies = new HashMap<>();
|
||||
VersionStrategy jsStrategy = mock(VersionStrategy.class);
|
||||
VersionStrategy catchAllStrategy = mock(VersionStrategy.class);
|
||||
strategies.put("/**", catchAllStrategy);
|
||||
strategies.put("/**/*.js", jsStrategy);
|
||||
this.resolver.setStrategyMap(strategies);
|
||||
|
||||
assertEquals(catchAllStrategy, this.resolver.getStrategyForPath("foo.css"));
|
||||
assertEquals(catchAllStrategy, this.resolver.getStrategyForPath("foo-js.css"));
|
||||
assertEquals(jsStrategy, this.resolver.getStrategyForPath("foo.js"));
|
||||
assertEquals(jsStrategy, this.resolver.getStrategyForPath("bar/foo.js"));
|
||||
}
|
||||
|
||||
@Test // SPR-13883
|
||||
public void shouldConfigureFixedPrefixAutomatically() throws Exception {
|
||||
|
||||
this.resolver.addFixedVersionStrategy("fixedversion", "/js/**", "/css/**", "/fixedversion/css/**");
|
||||
|
||||
assertThat(this.resolver.getStrategyMap().size(), is(4));
|
||||
|
||||
assertThat(this.resolver.getStrategyForPath("js/something.js"),
|
||||
Matchers.instanceOf(FixedVersionStrategy.class));
|
||||
|
||||
assertThat(this.resolver.getStrategyForPath("fixedversion/js/something.js"),
|
||||
Matchers.instanceOf(FixedVersionStrategy.class));
|
||||
|
||||
assertThat(this.resolver.getStrategyForPath("css/something.css"),
|
||||
Matchers.instanceOf(FixedVersionStrategy.class));
|
||||
|
||||
assertThat(this.resolver.getStrategyForPath("fixedversion/css/something.css"),
|
||||
Matchers.instanceOf(FixedVersionStrategy.class));
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.BDDMockito.never;
|
||||
import static org.mockito.BDDMockito.times;
|
||||
import static org.mockito.BDDMockito.verify;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link WebJarsResourceResolver}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class WebJarsResourceResolverTests {
|
||||
|
||||
private List<Resource> locations;
|
||||
|
||||
private WebJarsResourceResolver resolver;
|
||||
|
||||
private ResourceResolverChain chain;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
// for this to work, an actual WebJar must be on the test classpath
|
||||
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars"));
|
||||
this.resolver = new WebJarsResourceResolver();
|
||||
this.chain = mock(ResourceResolverChain.class);
|
||||
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "");
|
||||
ServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void resolveUrlExisting() {
|
||||
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/", getClass()));
|
||||
String file = "/foo/2.3/foo.txt";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(file);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertEquals(file, actual);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveUrlExistingNotInJarFile() {
|
||||
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/", getClass()));
|
||||
String file = "foo/foo.txt";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertNull(actual);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, never()).resolveUrlPath("foo/2.3/foo.txt", this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveUrlWebJarResource() {
|
||||
String file = "underscorejs/underscore.js";
|
||||
String expected = "underscorejs/1.8.3/underscore.js";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(expected);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertEquals(expected, actual);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveUrlWebJarResourceNotFound() {
|
||||
String file = "something/something.js";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertNull(actual);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, never()).resolveUrlPath(null, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceExisting() {
|
||||
Resource expected = mock(Resource.class);
|
||||
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/", getClass()));
|
||||
String file = "foo/2.3/foo.txt";
|
||||
given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(expected);
|
||||
|
||||
Resource actual = this.resolver.resolveResource(this.exchange, file, this.locations, this.chain);
|
||||
|
||||
assertEquals(expected, actual);
|
||||
verify(this.chain, times(1)).resolveResource(this.exchange, file, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceNotFound() {
|
||||
String file = "something/something.js";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
|
||||
Resource actual = this.resolver.resolveResource(this.exchange, file, this.locations, this.chain);
|
||||
|
||||
assertNull(actual);
|
||||
verify(this.chain, times(1)).resolveResource(this.exchange, file, this.locations);
|
||||
verify(this.chain, never()).resolveResource(this.exchange, null, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveResourceWebJar() {
|
||||
Resource expected = mock(Resource.class);
|
||||
String file = "underscorejs/underscore.js";
|
||||
String expectedPath = "underscorejs/1.8.3/underscore.js";
|
||||
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/", getClass()));
|
||||
given(this.chain.resolveResource(this.exchange, expectedPath, this.locations)).willReturn(expected);
|
||||
|
||||
Resource actual = this.resolver.resolveResource(this.exchange, file, this.locations, this.chain);
|
||||
|
||||
assertEquals(expected, actual);
|
||||
verify(this.chain, times(1)).resolveResource(this.exchange, file, this.locations);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
CACHE MANIFEST
|
||||
|
||||
# this is a comment
|
||||
CACHE:
|
||||
bar.css
|
||||
foo.css
|
||||
//example.org/style.css
|
||||
|
||||
NETWORK:
|
||||
*
|
||||
|
||||
CACHE:
|
||||
js/bar.js
|
||||
http://example.org/image.png
|
||||
|
||||
FALLBACK:
|
||||
/main /static.html
|
|
@ -0,0 +1 @@
|
|||
h2 { color:white; }
|
|
@ -0,0 +1,4 @@
|
|||
THIS DOES NOT START WITH "CACHE MANIFEST"
|
||||
|
||||
CACHE:
|
||||
bar.css
|
|
@ -0,0 +1,3 @@
|
|||
@import url("http://example.org/fonts/css");
|
||||
body { background: url("file:///home/spring/image.png") }
|
||||
figure { background: url("//example.org/style.css")}
|
|
@ -0,0 +1 @@
|
|||
h1 { color:red; }
|
|
@ -0,0 +1 @@
|
|||
h1 { color:red; }
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Foo</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 155 B |
|
@ -0,0 +1 @@
|
|||
function foo() { console.log("hello bar"); }
|
|
@ -0,0 +1 @@
|
|||
function foo() { console.log("hello world"); }
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
@import url("bar.css");
|
||||
@import url('bar.css');
|
||||
@import url(bar.css);
|
||||
|
||||
@import "foo.css";
|
||||
@import 'foo.css';
|
||||
|
||||
body { background: url("images/image.png") }
|
|
@ -0,0 +1 @@
|
|||
fail
|
|
@ -0,0 +1 @@
|
|||
h1 { color:red; }
|
|
@ -0,0 +1 @@
|
|||
function foo() { console.log("hello world"); }
|
|
@ -0,0 +1 @@
|
|||
big secret
|
Loading…
Reference in New Issue