Skip to content

Commit 4f8298c

Browse files
committed
DATAREST-490 - Makes sure content negotiation works for manual resource overrides with dedicated media type.
In case a manually implemented controller is registered to add a dedicated media type for a Spring Data REST resource (e.g. an HTML representation for item resources) we now continue to serve JSON in case the request indicates it wants to see it. Previously this didn't work as the manually implemented controller method was detected to partially match (only the produces-clause not matching) and thus an exception being thrown from the HandlerMapping lookup. This caused the HandlerMapping registered for repositories not being considered at all. This is now fixed by hiding the HandlerMapping instances we register behind a DelegatingHandlerMapping that continues to try delegates even in a case of an exception being caused in a particular resolution attempt. Should all resolution attempts fail, we then throw the original exception if one occurred in the first place.
1 parent 83e1ca0 commit 4f8298c

File tree

6 files changed

+187
-16
lines changed

6 files changed

+187
-16
lines changed

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareHandlerMapping.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import javax.servlet.http.HttpSession;
4242
import javax.servlet.http.Part;
4343

44-
import org.springframework.core.Ordered;
4544
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
4645
import org.springframework.util.Assert;
4746
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
@@ -71,7 +70,6 @@ public BasePathAwareHandlerMapping(RepositoryRestConfiguration configuration) {
7170

7271
Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!");
7372
this.configuration = configuration;
74-
setOrder(Ordered.LOWEST_PRECEDENCE - 150);
7573
}
7674

7775
/*

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import javax.servlet.ServletException;
2222
import javax.servlet.http.HttpServletRequest;
2323

24-
import org.springframework.core.Ordered;
2524
import org.springframework.core.annotation.AnnotationUtils;
2625
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
2726
import org.springframework.data.rest.core.mapping.ResourceMappings;
@@ -65,8 +64,6 @@ public RepositoryRestHandlerMapping(ResourceMappings mappings, RepositoryRestCon
6564

6665
this.mappings = mappings;
6766
this.configuration = config;
68-
69-
setOrder(Ordered.LOWEST_PRECEDENCE - 100);
7067
}
7168

7269
/**

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter;
8686
import org.springframework.data.rest.webmvc.support.BackendIdHandlerMethodArgumentResolver;
8787
import org.springframework.data.rest.webmvc.support.DefaultedPageableHandlerMethodArgumentResolver;
88+
import org.springframework.data.rest.webmvc.support.DelegatingHandlerMapping;
8889
import org.springframework.data.rest.webmvc.support.ETagArgumentResolver;
8990
import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver;
9091
import org.springframework.data.rest.webmvc.support.JpaHelper;
@@ -115,9 +116,9 @@
115116
import org.springframework.util.ClassUtils;
116117
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
117118
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
119+
import org.springframework.web.servlet.HandlerMapping;
118120
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
119121
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
120-
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
121122

122123
import com.fasterxml.jackson.databind.DeserializationFeature;
123124
import com.fasterxml.jackson.databind.Module;
@@ -494,23 +495,31 @@ public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {
494495
}
495496

496497
/**
497-
* Special {@link org.springframework.web.servlet.HandlerMapping} that only recognizes handler methods defined in the
498-
* provided controller classes.
498+
* The {@link HandlerMapping} to delegate requests to Spring Data REST controllers. Sets up a
499+
* {@link DelegatingHandlerMapping} to make sure manually implemented {@link BasePathAwareController} instances that
500+
* register custom handlers for certain media types don't cause the {@link RepositoryRestHandlerMapping} to be
501+
* omitted.
499502
*
503+
* @see DATAREST-490
500504
* @return
501505
*/
502506
@Bean
503-
public RequestMappingHandlerMapping repositoryExporterHandlerMapping() {
507+
public DelegatingHandlerMapping restHandlerMapping() {
504508

505-
RepositoryRestHandlerMapping mapping = new RepositoryRestHandlerMapping(resourceMappings(), config());
506-
mapping.setJpaHelper(jpaHelper());
509+
RepositoryRestHandlerMapping repositoryMapping = new RepositoryRestHandlerMapping(resourceMappings(), config());
510+
repositoryMapping.setJpaHelper(jpaHelper());
511+
repositoryMapping.setApplicationContext(applicationContext);
512+
repositoryMapping.afterPropertiesSet();
507513

508-
return mapping;
509-
}
514+
BasePathAwareHandlerMapping basePathMapping = new BasePathAwareHandlerMapping(config());
515+
basePathMapping.setApplicationContext(applicationContext);
516+
basePathMapping.afterPropertiesSet();
510517

511-
@Bean
512-
public RequestMappingHandlerMapping fallbackMapping() {
513-
return new BasePathAwareHandlerMapping(config());
518+
List<HandlerMapping> mappings = new ArrayList<HandlerMapping>();
519+
mappings.add(basePathMapping);
520+
mappings.add(repositoryMapping);
521+
522+
return new DelegatingHandlerMapping(mappings);
514523
}
515524

516525
@Bean
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2015 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+
package org.springframework.data.rest.webmvc.support;
17+
18+
import java.util.List;
19+
20+
import javax.servlet.http.HttpServletRequest;
21+
22+
import org.springframework.core.Ordered;
23+
import org.springframework.util.Assert;
24+
import org.springframework.web.HttpMediaTypeException;
25+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
26+
import org.springframework.web.servlet.HandlerExecutionChain;
27+
import org.springframework.web.servlet.HandlerMapping;
28+
29+
/**
30+
* A {@link HandlerMapping} that considers a {@link List} of delegates. It will keep on traversing the delegates in case
31+
* an {@link HttpMediaTypeNotAcceptableException} is thrown while trying to lookup the handler on a particular delegate.
32+
*
33+
* @author Oliver Gierke
34+
* @soundtrack Benny Greb - Stabila (Moving Parts)
35+
*/
36+
public class DelegatingHandlerMapping implements HandlerMapping, Ordered {
37+
38+
private final List<HandlerMapping> delegates;
39+
40+
/**
41+
* Creates a new {@link DelegatingHandlerMapping} for the given delegates.
42+
*
43+
* @param delegates must not be {@literal null}.
44+
*/
45+
public DelegatingHandlerMapping(List<HandlerMapping> delegates) {
46+
47+
Assert.notNull(delegates, "Delegates must not be null!");
48+
49+
this.delegates = delegates;
50+
}
51+
52+
/*
53+
* (non-Javadoc)
54+
* @see org.springframework.core.Ordered#getOrder()
55+
*/
56+
@Override
57+
public int getOrder() {
58+
return Ordered.LOWEST_PRECEDENCE - 100;
59+
}
60+
61+
/*
62+
* (non-Javadoc)
63+
* @see org.springframework.web.servlet.HandlerMapping#getHandler(javax.servlet.http.HttpServletRequest)
64+
*/
65+
@Override
66+
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
67+
68+
HttpMediaTypeException exception = null;
69+
70+
for (HandlerMapping delegate : delegates) {
71+
72+
try {
73+
74+
HandlerExecutionChain result = delegate.getHandler(request);
75+
76+
if (result != null) {
77+
return result;
78+
}
79+
80+
} catch (HttpMediaTypeNotAcceptableException o_O) {
81+
exception = o_O;
82+
}
83+
}
84+
85+
if (exception != null) {
86+
throw exception;
87+
}
88+
89+
return null;
90+
}
91+
}

spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
import org.springframework.context.annotation.Bean;
1919
import org.springframework.context.annotation.Configuration;
2020
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
21+
import org.springframework.data.rest.webmvc.BasePathAwareController;
22+
import org.springframework.http.MediaType;
2123
import org.springframework.transaction.annotation.EnableTransactionManagement;
24+
import org.springframework.web.bind.annotation.PathVariable;
25+
import org.springframework.web.bind.annotation.RequestMapping;
26+
import org.springframework.web.bind.annotation.RequestMethod;
2227

2328
/**
2429
* Test configuration for JPA.
@@ -40,4 +45,11 @@ public BookIdConverter bookIdConverter() {
4045
public TestDataPopulator testDataPopulator() {
4146
return new TestDataPopulator();
4247
}
48+
49+
@BasePathAwareController
50+
static class BooksHtmlController {
51+
52+
@RequestMapping(value = "/books/{id}", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
53+
void person(@PathVariable String id) {}
54+
}
4355
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2015 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+
package org.springframework.data.rest.webmvc.support;
17+
18+
import static org.junit.Assert.*;
19+
import static org.mockito.Mockito.*;
20+
21+
import java.util.Arrays;
22+
23+
import javax.servlet.http.HttpServletRequest;
24+
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
import org.mockito.Mock;
28+
import org.mockito.runners.MockitoJUnitRunner;
29+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
30+
import org.springframework.web.servlet.HandlerMapping;
31+
32+
/**
33+
* Unit tests for {@link DelegatingHandlerMapping}.
34+
*
35+
* @author Oliver Gierke
36+
* @soundtrack Benny Greb - Stabila (Moving Parts)
37+
*/
38+
@RunWith(MockitoJUnitRunner.class)
39+
public class DelegatingHandlerMappingUnitTests {
40+
41+
@Mock HandlerMapping first, second;
42+
@Mock HttpServletRequest request;
43+
44+
/**
45+
* @see DATAREST-490
46+
*/
47+
@Test
48+
@SuppressWarnings("unchecked")
49+
public void testname() throws Exception {
50+
51+
HandlerMapping handlerMapping = new DelegatingHandlerMapping(Arrays.asList(first, second));
52+
53+
when(first.getHandler(request)).thenThrow(HttpMediaTypeNotAcceptableException.class);
54+
55+
try {
56+
57+
handlerMapping.getHandler(request);
58+
fail(String.format("Expected %s!", HttpMediaTypeNotAcceptableException.class.getSimpleName()));
59+
60+
} catch (HttpMediaTypeNotAcceptableException o_O) {
61+
verify(second, times(1)).getHandler(request);
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)