ParameterContentNegotiationStrategy uses MediaTypeFactory

Issue: SPR-15649
This commit is contained in:
Rossen Stoyanchev 2017-07-11 17:50:34 +02:00
parent c9a3b863c4
commit befacf4a35
7 changed files with 92 additions and 93 deletions

View File

@ -19,8 +19,13 @@ package org.springframework.web.accept;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotAcceptableException;
@ -47,6 +52,13 @@ import org.springframework.web.context.request.NativeWebRequest;
public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeFileExtensionResolver public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeFileExtensionResolver
implements ContentNegotiationStrategy { implements ContentNegotiationStrategy {
protected final Log logger = LogFactory.getLog(getClass());
private boolean useRegisteredExtensionsOnly = false;
private boolean ignoreUnknownExtensions = false;
/** /**
* Create an instance with the given map of file extensions and media types. * Create an instance with the given map of file extensions and media types.
*/ */
@ -55,6 +67,34 @@ public abstract class AbstractMappingContentNegotiationStrategy extends MappingM
} }
/**
* Whether to only use the registered mappings to look up file extensions,
* or also to use dynamic resolution (e.g. via {@link MediaTypeFactory}.
* <p>By default this is set to {@code false}.
*/
public void setUseRegisteredExtensionsOnly(boolean useRegisteredExtensionsOnly) {
this.useRegisteredExtensionsOnly = useRegisteredExtensionsOnly;
}
public boolean isUseRegisteredExtensionsOnly() {
return this.useRegisteredExtensionsOnly;
}
/**
* Whether to ignore requests with unknown file extension. Setting this to
* {@code false} results in {@code HttpMediaTypeNotAcceptableException}.
* <p>By default this is set to {@literal false} but is overridden in
* {@link PathExtensionContentNegotiationStrategy} to {@literal true}.
*/
public void setIgnoreUnknownExtensions(boolean ignoreUnknownExtensions) {
this.ignoreUnknownExtensions = ignoreUnknownExtensions;
}
public boolean isIgnoreUnknownExtensions() {
return this.ignoreUnknownExtensions;
}
@Override @Override
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
throws HttpMediaTypeNotAcceptableException { throws HttpMediaTypeNotAcceptableException {
@ -98,6 +138,9 @@ public abstract class AbstractMappingContentNegotiationStrategy extends MappingM
* {@link #lookupMediaType}. * {@link #lookupMediaType}.
*/ */
protected void handleMatch(String key, MediaType mediaType) { protected void handleMatch(String key, MediaType mediaType) {
if (logger.isTraceEnabled()) {
logger.trace("Requested MediaType='" + mediaType + "' based on key='" + key + "'.");
}
} }
/** /**
@ -110,7 +153,16 @@ public abstract class AbstractMappingContentNegotiationStrategy extends MappingM
protected MediaType handleNoMatch(NativeWebRequest request, String key) protected MediaType handleNoMatch(NativeWebRequest request, String key)
throws HttpMediaTypeNotAcceptableException { throws HttpMediaTypeNotAcceptableException {
return null; if (!isUseRegisteredExtensionsOnly()) {
Optional<MediaType> mediaType = MediaTypeFactory.getMediaType("file." + key);
if (mediaType.isPresent()) {
return mediaType.get();
}
}
if (isIgnoreUnknownExtensions()) {
return null;
}
throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
} }
} }

View File

@ -44,7 +44,7 @@ import org.springframework.web.context.request.NativeWebRequest;
*/ */
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver { public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
private static final List<MediaType> MEDIA_TYPE_ALL = Collections.<MediaType>singletonList(MediaType.ALL); private static final List<MediaType> MEDIA_TYPE_ALL = Collections.singletonList(MediaType.ALL);
private final List<ContentNegotiationStrategy> strategies = new ArrayList<>(); private final List<ContentNegotiationStrategy> strategies = new ArrayList<>();

View File

@ -191,11 +191,11 @@ public class ContentNegotiationManagerFactoryBean
} }
/** /**
* When {@link #setFavorPathExtension favorPathExtension} is set, this * When {@link #setFavorPathExtension favorPathExtension} or
* property determines whether to use only registered {@code MediaType} mappings * {@link #setFavorParameter(boolean)} is set, this property determines
* to resolve a path extension to a specific MediaType. * whether to use only registered {@code MediaType} mappings or to allow
* <p>By default this is not set in which case * dynamic resolution, e.g. via {@link MediaTypeFactory}.
* {@code PathExtensionContentNegotiationStrategy} will use defaults if available. * <p>By default this is not set in which case dynamic resolution is on.
*/ */
public void setUseRegisteredExtensionsOnly(boolean useRegisteredExtensionsOnly) { public void setUseRegisteredExtensionsOnly(boolean useRegisteredExtensionsOnly) {
this.useRegisteredExtensionsOnly = useRegisteredExtensionsOnly; this.useRegisteredExtensionsOnly = useRegisteredExtensionsOnly;
@ -300,6 +300,12 @@ public class ContentNegotiationManagerFactoryBean
ParameterContentNegotiationStrategy strategy = ParameterContentNegotiationStrategy strategy =
new ParameterContentNegotiationStrategy(this.mediaTypes); new ParameterContentNegotiationStrategy(this.mediaTypes);
strategy.setParameterName(this.parameterName); strategy.setParameterName(this.parameterName);
if (this.useRegisteredExtensionsOnly != null) {
strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
}
else {
strategy.setUseRegisteredExtensionsOnly(true); // backwards compatibility
}
strategies.add(strategy); strategies.add(strategy);
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-201 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,25 +18,24 @@ package org.springframework.web.accept;
import java.util.Map; import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
/** /**
* A {@code ContentNegotiationStrategy} that resolves a query parameter to a key * Strategy that resolves the requested content type from a query parameter.
* to be used to look up a media type. The default parameter name is {@code format}. * The default query parameter name is {@literal "format"}.
*
* <p>You can register static mappings between keys (i.e. the expected value of
* the query parameter) and MediaType's via {@link #addMapping(String, MediaType)}.
* As of 5.0 this strategy also supports dynamic lookups of keys via
* {@link org.springframework.http.MediaTypeFactory#getMediaType}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
*/ */
public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
private static final Log logger = LogFactory.getLog(ParameterContentNegotiationStrategy.class);
private String parameterName = "format"; private String parameterName = "format";
@ -67,19 +66,4 @@ public class ParameterContentNegotiationStrategy extends AbstractMappingContentN
return request.getParameter(getParameterName()); return request.getParameter(getParameterName());
} }
@Override
protected void handleMatch(String mediaTypeKey, MediaType mediaType) {
if (logger.isDebugEnabled()) {
logger.debug("Requested media type: '" + mediaType + "' based on '" +
getParameterName() + "'='" + mediaTypeKey + "'");
}
}
@Override
protected MediaType handleNoMatch(NativeWebRequest request, String key)
throws HttpMediaTypeNotAcceptableException {
throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
}
} }

View File

@ -18,20 +18,14 @@ package org.springframework.web.accept;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory; import org.springframework.http.MediaTypeFactory;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.util.UriUtils; import org.springframework.web.util.UriUtils;
import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.UrlPathHelper;
@ -49,14 +43,8 @@ import org.springframework.web.util.UrlPathHelper;
*/ */
public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class);
private UrlPathHelper urlPathHelper = new UrlPathHelper(); private UrlPathHelper urlPathHelper = new UrlPathHelper();
private boolean useRegisteredExtensionsOnly = false;
private boolean ignoreUnknownExtensions = true;
/** /**
* Create an instance without any mappings to start with. Mappings may be added * Create an instance without any mappings to start with. Mappings may be added
@ -71,6 +59,8 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
*/ */
public PathExtensionContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) { public PathExtensionContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) {
super(mediaTypes); super(mediaTypes);
setUseRegisteredExtensionsOnly(false);
setIgnoreUnknownExtensions(true);
this.urlPathHelper.setUrlDecode(false); this.urlPathHelper.setUrlDecode(false);
} }
@ -92,25 +82,6 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
setUseRegisteredExtensionsOnly(!useJaf); setUseRegisteredExtensionsOnly(!useJaf);
} }
/**
* Whether to only use the registered mappings to look up file extensions, or also refer to
* defaults.
* <p>By default this is set to {@code false}, meaning that defaults are used.
*/
public void setUseRegisteredExtensionsOnly(boolean useRegisteredExtensionsOnly) {
this.useRegisteredExtensionsOnly = useRegisteredExtensionsOnly;
}
/**
* Whether to ignore requests with unknown file extension. Setting this to
* {@code false} results in {@code HttpMediaTypeNotAcceptableException}.
* <p>By default this is set to {@code true}.
*/
public void setIgnoreUnknownExtensions(boolean ignoreUnknownExtensions) {
this.ignoreUnknownExtensions = ignoreUnknownExtensions;
}
@Override @Override
protected String getMediaTypeKey(NativeWebRequest webRequest) { protected String getMediaTypeKey(NativeWebRequest webRequest) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
@ -123,22 +94,6 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ENGLISH) : null); return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ENGLISH) : null);
} }
@Override
protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension)
throws HttpMediaTypeNotAcceptableException {
if (!this.useRegisteredExtensionsOnly) {
Optional<MediaType> mediaType = MediaTypeFactory.getMediaType("file." + extension);
if (mediaType.isPresent()) {
return mediaType.get();
}
}
if (this.ignoreUnknownExtensions) {
return null;
}
throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
}
/** /**
* A public method exposing the knowledge of the path extension strategy to * A public method exposing the knowledge of the path extension strategy to
* resolve file extensions to a {@link MediaType} in this case for a given * resolve file extensions to a {@link MediaType} in this case for a given

View File

@ -146,7 +146,7 @@ public class ContentNegotiationManagerFactoryBeanTests {
ContentNegotiationManager manager = this.factoryBean.getObject(); ContentNegotiationManager manager = this.factoryBean.getObject();
this.servletRequest.setRequestURI("/flower"); this.servletRequest.setRequestURI("/flower");
this.servletRequest.setParameter("format", "xyz"); this.servletRequest.setParameter("format", "invalid");
manager.resolveMediaTypes(this.webRequest); manager.resolveMediaTypes(this.webRequest);
} }

View File

@ -30,16 +30,15 @@ import org.springframework.lang.Nullable;
/** /**
* Builder for a composite {@link RequestedContentTypeResolver} that delegates * Builder for a composite {@link RequestedContentTypeResolver} that delegates
* to one or more other resolvers each implementing a different strategy to * to other resolvers each implementing a different strategy to determine the
* determine the requested content type(s), e.g. from the Accept header, * requested content type -- e.g. Accept header, query parameter, or other.
* through a query parameter, or other custom strategy.
* *
* <p>Use methods of this builder to add resolvers in the desired order. * <p>Use builder methods to add resolvers in the desired order. For a given
* The result of the first resolver to return a non-empty list of media types * request he first resolver to return a list that is not empty and does not
* is used. * consist of just {@link MediaType#ALL}, will be used.
* *
* <p>If no resolvers are configured, by default the builder will configure * <p>By default, if no resolvers are explicitly configured, the builder will
* {@link HeaderContentTypeResolver} only. * add {@link HeaderContentTypeResolver}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 5.0 * @since 5.0
@ -50,8 +49,8 @@ public class RequestedContentTypeResolverBuilder {
/** /**
* Add resolver extracting the requested content type from a query parameter. * Add a resolver to get the requested content type from a query parameter.
* By default the expected query parameter name is {@code "format"}. * By default the query parameter name is {@code "format"}.
*/ */
public ParameterResolverConfigurer parameterResolver() { public ParameterResolverConfigurer parameterResolver() {
ParameterResolverConfigurer parameterBuilder = new ParameterResolverConfigurer(); ParameterResolverConfigurer parameterBuilder = new ParameterResolverConfigurer();
@ -60,7 +59,7 @@ public class RequestedContentTypeResolverBuilder {
} }
/** /**
* Add resolver extracting the requested content type from the * Add resolver to get the requested content type from the
* {@literal "Accept"} header. * {@literal "Accept"} header.
*/ */
public void headerResolver() { public void headerResolver() {
@ -68,7 +67,7 @@ public class RequestedContentTypeResolverBuilder {
} }
/** /**
* Add resolver that always returns a fixed set of media types. * Add resolver that returns a fixed set of media types.
* @param mediaTypes the media types to use * @param mediaTypes the media types to use
*/ */
public void fixedResolver(MediaType... mediaTypes) { public void fixedResolver(MediaType... mediaTypes) {
@ -108,7 +107,7 @@ public class RequestedContentTypeResolverBuilder {
/** /**
* Helps to create a {@link ParameterContentTypeResolver}. * Helper to create and configure {@link ParameterContentTypeResolver}.
*/ */
public static class ParameterResolverConfigurer { public static class ParameterResolverConfigurer {
@ -146,7 +145,10 @@ public class RequestedContentTypeResolverBuilder {
return this; return this;
} }
RequestedContentTypeResolver createResolver() { /**
* Private factory method to create the resolver.
*/
private RequestedContentTypeResolver createResolver() {
ParameterContentTypeResolver resolver = new ParameterContentTypeResolver(this.mediaTypes); ParameterContentTypeResolver resolver = new ParameterContentTypeResolver(this.mediaTypes);
if (this.parameterName != null) { if (this.parameterName != null) {
resolver.setParameterName(this.parameterName); resolver.setParameterName(this.parameterName);