Skip to content

Commit f05e2bc

Browse files
committed
Add abstractions for content negotiation
Introduced ContentNeogtiationStrategy for resolving the requested media types from an incoming request. The available implementations are based on path extension, request parameter, 'Accept' header, and a fixed default content type. The logic for these implementations is based on equivalent options, previously available only in the ContentNegotiatingViewResolver. Also in this commit is ContentNegotiationManager, the central class to use when configuring content negotiation options. It accepts one or more ContentNeogtiationStrategy instances and delegates to them. The ContentNeogiationManager can now be used to configure the following classes: - RequestMappingHandlerMappingm - RequestMappingHandlerAdapter - ExceptionHandlerExceptionResolver - ContentNegotiatingViewResolver Issue: SPR-8410, SPR-8417, SPR-8418,SPR-8416, SPR-8419,SPR-7722
1 parent 35055fd commit f05e2bc

29 files changed

+1470
-490
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ project('spring-web') {
368368
optional dep
369369
exclude group: 'org.mortbay.jetty', module: 'servlet-api-2.5'
370370
}
371+
testCompile project(":spring-context-support") // for JafMediaTypeFactory
371372
testCompile "xmlunit:xmlunit:1.2"
372373
}
373374

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.accept;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import org.springframework.http.MediaType;
24+
import org.springframework.util.StringUtils;
25+
import org.springframework.web.context.request.NativeWebRequest;
26+
27+
/**
28+
* A base class for ContentNegotiationStrategy types that maintain a map with keys
29+
* such as "json" and media types such as "application/json".
30+
*
31+
* @author Rossen Stoyanchev
32+
* @since 3.2
33+
*/
34+
public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeExtensionsResolver
35+
implements ContentNegotiationStrategy, MediaTypeExtensionsResolver {
36+
37+
/**
38+
* Create an instance with the given extension-to-MediaType lookup.
39+
* @throws IllegalArgumentException if a media type string cannot be parsed
40+
*/
41+
public AbstractMappingContentNegotiationStrategy(Map<String, String> mediaTypes) {
42+
super(mediaTypes);
43+
}
44+
45+
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) {
46+
String key = getMediaTypeKey(webRequest);
47+
if (StringUtils.hasText(key)) {
48+
MediaType mediaType = lookupMediaType(key);
49+
if (mediaType != null) {
50+
handleMatch(key, mediaType);
51+
return Collections.singletonList(mediaType);
52+
}
53+
mediaType = handleNoMatch(webRequest, key);
54+
if (mediaType != null) {
55+
addMapping(key, mediaType);
56+
return Collections.singletonList(mediaType);
57+
}
58+
}
59+
return Collections.emptyList();
60+
}
61+
62+
/**
63+
* Sub-classes must extract the key to use to look up a media type.
64+
* @return the lookup key or {@code null} if the key cannot be derived
65+
*/
66+
protected abstract String getMediaTypeKey(NativeWebRequest request);
67+
68+
/**
69+
* Invoked when a matching media type is found in the lookup map.
70+
*/
71+
protected void handleMatch(String mappingKey, MediaType mediaType) {
72+
}
73+
74+
/**
75+
* Invoked when no matching media type is found in the lookup map.
76+
* Sub-classes can take further steps to determine the media type.
77+
*/
78+
protected MediaType handleNoMatch(NativeWebRequest request, String mappingKey) {
79+
return null;
80+
}
81+
82+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.accept;
18+
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.Collections;
22+
import java.util.LinkedHashSet;
23+
import java.util.List;
24+
import java.util.Set;
25+
26+
import org.springframework.http.MediaType;
27+
import org.springframework.util.Assert;
28+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
29+
import org.springframework.web.context.request.NativeWebRequest;
30+
31+
/**
32+
* This class is used to determine the requested {@linkplain MediaType media types}
33+
* in a request by delegating to a list of {@link ContentNegotiationStrategy} instances.
34+
*
35+
* <p>It may also be used to determine the extensions associated with a MediaType by
36+
* delegating to a list of {@link MediaTypeExtensionsResolver} instances.
37+
*
38+
* @author Rossen Stoyanchev
39+
* @since 3.2
40+
*/
41+
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeExtensionsResolver {
42+
43+
private final List<ContentNegotiationStrategy> contentNegotiationStrategies = new ArrayList<ContentNegotiationStrategy>();
44+
45+
private final Set<MediaTypeExtensionsResolver> extensionResolvers = new LinkedHashSet<MediaTypeExtensionsResolver>();
46+
47+
/**
48+
* Create an instance with the given ContentNegotiationStrategy instances.
49+
* <p>Each instance is checked to see if it is also an implementation of
50+
* MediaTypeExtensionsResolver, and if so it is registered as such.
51+
*/
52+
public ContentNegotiationManager(ContentNegotiationStrategy... strategies) {
53+
Assert.notEmpty(strategies, "At least one ContentNegotiationStrategy is expected");
54+
this.contentNegotiationStrategies.addAll(Arrays.asList(strategies));
55+
for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) {
56+
if (strategy instanceof MediaTypeExtensionsResolver) {
57+
this.extensionResolvers.add((MediaTypeExtensionsResolver) strategy);
58+
}
59+
}
60+
}
61+
62+
/**
63+
* Create an instance with a {@link HeaderContentNegotiationStrategy}.
64+
*/
65+
public ContentNegotiationManager() {
66+
this(new HeaderContentNegotiationStrategy());
67+
}
68+
69+
/**
70+
* Add MediaTypeExtensionsResolver instances.
71+
*/
72+
public void addExtensionsResolver(MediaTypeExtensionsResolver... resolvers) {
73+
this.extensionResolvers.addAll(Arrays.asList(resolvers));
74+
}
75+
76+
/**
77+
* Delegate to all configured ContentNegotiationStrategy instances until one
78+
* returns a non-empty list.
79+
* @param request the current request
80+
* @return the requested media types or an empty list, never {@code null}
81+
* @throws HttpMediaTypeNotAcceptableException if the requested media types cannot be parsed
82+
*/
83+
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
84+
for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) {
85+
List<MediaType> mediaTypes = strategy.resolveMediaTypes(webRequest);
86+
if (!mediaTypes.isEmpty()) {
87+
return mediaTypes;
88+
}
89+
}
90+
return Collections.emptyList();
91+
}
92+
93+
/**
94+
* Delegate to all configured MediaTypeExtensionsResolver instances and aggregate
95+
* the list of all extensions found.
96+
*/
97+
public List<String> resolveExtensions(MediaType mediaType) {
98+
Set<String> extensions = new LinkedHashSet<String>();
99+
for (MediaTypeExtensionsResolver resolver : this.extensionResolvers) {
100+
extensions.addAll(resolver.resolveExtensions(mediaType));
101+
}
102+
return new ArrayList<String>(extensions);
103+
}
104+
105+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.accept;
18+
19+
import java.util.List;
20+
21+
import org.springframework.http.MediaType;
22+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
23+
import org.springframework.web.context.request.NativeWebRequest;
24+
25+
/**
26+
* A strategy for resolving the requested media types in a request.
27+
*
28+
* @author Rossen Stoyanchev
29+
* @since 3.2
30+
*/
31+
public interface ContentNegotiationStrategy {
32+
33+
/**
34+
* Resolve the given request to a list of media types. The returned list is
35+
* ordered by specificity first and by quality parameter second.
36+
*
37+
* @param request the current request
38+
* @return the requested media types or an empty list, never {@code null}
39+
*
40+
* @throws HttpMediaTypeNotAcceptableException if the requested media types cannot be parsed
41+
*/
42+
List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException;
43+
44+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.accept;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
import org.springframework.http.MediaType;
25+
import org.springframework.web.context.request.NativeWebRequest;
26+
27+
/**
28+
* A ContentNegotiationStrategy that returns a fixed content type.
29+
*
30+
* @author Rossen Stoyanchev
31+
* @since 3.2
32+
*/
33+
public class FixedContentNegotiationStrategy implements ContentNegotiationStrategy {
34+
35+
private static final Log logger = LogFactory.getLog(FixedContentNegotiationStrategy.class);
36+
37+
private final MediaType defaultContentType;
38+
39+
/**
40+
* Create an instance that always returns the given content type.
41+
*/
42+
public FixedContentNegotiationStrategy(MediaType defaultContentType) {
43+
this.defaultContentType = defaultContentType;
44+
}
45+
46+
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) {
47+
if (logger.isDebugEnabled()) {
48+
logger.debug("Requested media types is " + this.defaultContentType + " (based on default MediaType)");
49+
}
50+
return Collections.singletonList(this.defaultContentType);
51+
}
52+
53+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.accept;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import org.springframework.http.MediaType;
23+
import org.springframework.util.StringUtils;
24+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
25+
import org.springframework.web.context.request.NativeWebRequest;
26+
27+
/**
28+
* A ContentNegotiationStrategy that parses the 'Accept' header of the request.
29+
*
30+
* @author Rossen Stoyanchev
31+
* @since 3.2
32+
*/
33+
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
34+
35+
private static final String ACCEPT_HEADER = "Accept";
36+
37+
/**
38+
* {@inheritDoc}
39+
* @throws HttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed.
40+
*/
41+
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
42+
String acceptHeader = webRequest.getHeader(ACCEPT_HEADER);
43+
try {
44+
if (StringUtils.hasText(acceptHeader)) {
45+
List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
46+
MediaType.sortBySpecificityAndQuality(mediaTypes);
47+
return mediaTypes;
48+
}
49+
}
50+
catch (IllegalArgumentException ex) {
51+
throw new HttpMediaTypeNotAcceptableException(
52+
"Could not parse accept header [" + acceptHeader + "]: " + ex.getMessage());
53+
}
54+
return Collections.emptyList();
55+
}
56+
57+
}

0 commit comments

Comments
 (0)