Update MockMvcConfigurer support

This is a follow-up on the commit introducing MockMvcConfigurer:
c2b0fac852

This commit refines the MockMvcConfigurer contract to use (the new)
ConfigurableMockMvcBuilder hence not requiring downcasting to
AbstractMockMvcBuilder.

The same also no longer passes the "default" RequestBuilder which would
also require a downcast, but rather allows a RequestPostProcessor to be
returned so that a 3rd party framework or application can modify any
property of every performed MockHttpServletRequest.

To make this possible the new SmartRequestBuilder interface separates
request building from request post processing while the new
ConfigurableSmartRequestBuilder allows adding a RequestPostProcessor
to a MockMvcBuilder.

Issue: SPR-11497
This commit is contained in:
Rossen Stoyanchev
2014-07-21 13:43:45 -04:00
parent 988499f7dc
commit 71b63cd972
10 changed files with 375 additions and 136 deletions

View File

@@ -134,6 +134,10 @@ public final class MockMvc {
MockHttpServletRequest request = requestBuilder.buildRequest(this.servletContext);
MockHttpServletResponse response = new MockHttpServletResponse();
if (requestBuilder instanceof SmartRequestBuilder) {
request = ((SmartRequestBuilder) requestBuilder).postProcessRequest(request);
}
final MvcResult mvcResult = new DefaultMvcResult(request, response);
request.setAttribute(MVC_RESULT_ATTRIBUTE, mvcResult);

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2002-2012 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.test.web.servlet;
import javax.servlet.ServletContext;

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2014 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.test.web.servlet;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
/**
* Extended variant of a {@link RequestBuilder} that applies its
* {@link org.springframework.test.web.servlet.request.RequestPostProcessor}s
* as a separate step from the {@link #buildRequest} method.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public interface SmartRequestBuilder extends RequestBuilder {
/**
* Apply request post processing. Typically that means invoking one or more
* {@link org.springframework.test.web.servlet.request.RequestPostProcessor}s.
*
* @param request the request to initialize
* @return the request to use, either the one passed in or a wrapped one
*/
MockHttpServletRequest postProcessRequest(MockHttpServletRequest request);
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2002-2014 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.test.web.servlet.request;
import org.springframework.test.web.servlet.SmartRequestBuilder;
/**
* An extension of {@link org.springframework.test.web.servlet.SmartRequestBuilder
* SmartRequestBuilder} that can be configured with {@link RequestPostProcessor}s.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public interface ConfigurableSmartRequestBuilder<B extends ConfigurableSmartRequestBuilder<B>>
extends SmartRequestBuilder {
/**
* Add the given {@code RequestPostProcessor}.
*/
B with(RequestPostProcessor requestPostProcessor);
}

View File

@@ -39,7 +39,6 @@ import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
@@ -68,7 +67,9 @@ import org.springframework.web.util.UriUtils;
* @author Arjen Poutsma
* @since 3.2
*/
public class MockHttpServletRequestBuilder implements RequestBuilder, Mergeable {
public class MockHttpServletRequestBuilder
implements ConfigurableSmartRequestBuilder<MockHttpServletRequestBuilder>, Mergeable {
private final HttpMethod method;
@@ -421,6 +422,7 @@ public class MockHttpServletRequestBuilder implements RequestBuilder, Mergeable
* and be made accessible through static factory methods.
* @param postProcessor a post-processor to add
*/
@Override
public MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor) {
Assert.notNull(postProcessor, "postProcessor is required");
this.postProcessors.add(postProcessor);
@@ -621,14 +623,6 @@ public class MockHttpServletRequestBuilder implements RequestBuilder, Mergeable
FlashMapManager flashMapManager = getFlashMapManager(request);
flashMapManager.saveOutputFlashMap(flashMap, request, new MockHttpServletResponse());
// Apply post-processors at the very end
for (RequestPostProcessor postProcessor : this.postProcessors) {
request = postProcessor.postProcessRequest(request);
if (request == null) {
throw new IllegalStateException("Post-processor [" + postProcessor.getClass().getName() + "] returned null");
}
}
request.setAsyncSupported(true);
return request;
@@ -675,6 +669,18 @@ public class MockHttpServletRequestBuilder implements RequestBuilder, Mergeable
return (flashMapManager != null ? flashMapManager : new SessionFlashMapManager());
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
for (RequestPostProcessor postProcessor : this.postProcessors) {
request = postProcessor.postProcessRequest(request);
if (request == null) {
throw new IllegalStateException(
"Post-processor [" + postProcessor.getClass().getName() + "] returned null");
}
}
return request;
}
private static <T> void addToMultiValueMap(MultiValueMap<String, T> map, String name, T[] values) {
Assert.hasLength(name, "'name' must not be empty");
Assert.notNull(values, "'values' is required");

View File

@@ -17,7 +17,14 @@
package org.springframework.test.web.servlet.setup;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.test.web.servlet.*;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MockMvcBuilderSupport;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.ConfigurableSmartRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
@@ -38,7 +45,7 @@ import java.util.List;
* @since 4.0
*/
public abstract class AbstractMockMvcBuilder<B extends AbstractMockMvcBuilder<B>>
extends MockMvcBuilderSupport implements MockMvcBuilder {
extends MockMvcBuilderSupport implements ConfigurableMockMvcBuilder<B> {
private List<Filter> filters = new ArrayList<Filter>();
@@ -53,27 +60,6 @@ public abstract class AbstractMockMvcBuilder<B extends AbstractMockMvcBuilder<B>
private final List<MockMvcConfigurer> configurers = new ArrayList<MockMvcConfigurer>(4);
/**
* Add filters mapped to any request (i.e. "/*"). For example:
*
* <pre class="code">
* mockMvcBuilder.addFilters(springSecurityFilterChain);
* </pre>
*
* <p>is the equivalent of the following web.xml configuration:
*
* <pre class="code">
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;springSecurityFilterChain&lt;/filter-name&gt;
* &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
* &lt;/filter-mapping&gt;
* </pre>
*
* <p>Filters will be invoked in the order in which they are provided.
*
* @param filters the filters to add
*/
@SuppressWarnings("unchecked")
public final <T extends B> T addFilters(Filter... filters) {
Assert.notNull(filters, "filters cannot be null");
@@ -85,27 +71,6 @@ public abstract class AbstractMockMvcBuilder<B extends AbstractMockMvcBuilder<B>
return (T) this;
}
/**
* Add a filter mapped to a specific set of patterns. For example:
*
* <pre class="code">
* mockMvcBuilder.addFilters(myResourceFilter, "/resources/*");
* </pre>
*
* <p>is the equivalent of:
*
* <pre class="code">
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;myResourceFilter&lt;/filter-name&gt;
* &lt;url-pattern&gt;/resources/*&lt;/url-pattern&gt;
* &lt;/filter-mapping&gt;
* </pre>
*
* <p>Filters will be invoked in the order in which they are provided.
*
* @param filter the filter to add
* @param urlPatterns URL patterns to map to; if empty, "/*" is used by default
*/
@SuppressWarnings("unchecked")
public final <T extends B> T addFilter(Filter filter, String... urlPatterns) {
@@ -120,70 +85,32 @@ public abstract class AbstractMockMvcBuilder<B extends AbstractMockMvcBuilder<B>
return (T) this;
}
/**
* Define default request properties that should be merged into all
* performed requests. In effect this provides a mechanism for defining
* common initialization for all requests such as the content type, request
* parameters, session attributes, and any other request property.
*
* <p>Properties specified at the time of performing a request override the
* default properties defined here.
*
* @param requestBuilder a RequestBuilder; see static factory methods in
* {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders}
* .
*/
@SuppressWarnings("unchecked")
public final <T extends B> T defaultRequest(RequestBuilder requestBuilder) {
this.defaultRequestBuilder = requestBuilder;
return (T) this;
}
/**
* Define a global expectation that should <em>always</em> be applied to
* every response. For example, status code 200 (OK), content type
* {@code "application/json"}, etc.
*
* @param resultMatcher a ResultMatcher; see static factory methods in
* {@link org.springframework.test.web.servlet.result.MockMvcResultMatchers}
*/
@SuppressWarnings("unchecked")
public final <T extends B> T alwaysExpect(ResultMatcher resultMatcher) {
this.globalResultMatchers.add(resultMatcher);
return (T) this;
}
/**
* Define a global action that should <em>always</em> be applied to every
* response. For example, writing detailed information about the performed
* request and resulting response to {@code System.out}.
*
* @param resultHandler a ResultHandler; see static factory methods in
* {@link org.springframework.test.web.servlet.result.MockMvcResultHandlers}
*/
@SuppressWarnings("unchecked")
public final <T extends B> T alwaysDo(ResultHandler resultHandler) {
this.globalResultHandlers.add(resultHandler);
return (T) this;
}
/**
* Whether to enable the DispatcherServlet property
* {@link org.springframework.web.servlet.DispatcherServlet#setDispatchOptionsRequest
* dispatchOptionsRequest} which allows processing of HTTP OPTIONS requests.
*/
@SuppressWarnings("unchecked")
public final <T extends B> T dispatchOptions(boolean dispatchOptions) {
this.dispatchOptions = dispatchOptions;
return (T) this;
}
/**
* Add a {@code MockMvcConfigurer} which encapsulates ways to further configure
* this MockMvcBuilder with some specific purpose in mind.
*/
@SuppressWarnings("unchecked")
public final <T extends B> T add(MockMvcConfigurer configurer) {
public final <T extends B> T apply(MockMvcConfigurer configurer) {
configurer.afterConfigurerAdded(this);
this.configurers.add(configurer);
return (T) this;
@@ -202,7 +129,15 @@ public abstract class AbstractMockMvcBuilder<B extends AbstractMockMvcBuilder<B>
MockServletConfig mockServletConfig = new MockServletConfig(servletContext);
for (MockMvcConfigurer configurer : this.configurers) {
configurer.beforeMockMvcCreated(this, this.defaultRequestBuilder, wac);
RequestPostProcessor processor = configurer.beforeMockMvcCreated(this, wac);
if (processor != null) {
if (this.defaultRequestBuilder == null) {
this.defaultRequestBuilder = MockMvcRequestBuilders.get("/");
}
if (this.defaultRequestBuilder instanceof ConfigurableSmartRequestBuilder) {
((ConfigurableSmartRequestBuilder) this.defaultRequestBuilder).with(processor);
}
}
}
Filter[] filterArray = this.filters.toArray(new Filter[this.filters.size()]);
@@ -218,4 +153,4 @@ public abstract class AbstractMockMvcBuilder<B extends AbstractMockMvcBuilder<B>
*/
protected abstract WebApplicationContext initWebAppContext();
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2002-2014 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.test.web.servlet.setup;
import org.springframework.test.web.servlet.MockMvcBuilder;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.test.web.servlet.ResultMatcher;
import javax.servlet.Filter;
/**
* Defines common methods for building a {@code MockMvc}.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public interface ConfigurableMockMvcBuilder<B extends ConfigurableMockMvcBuilder<B>> extends MockMvcBuilder {
/**
* Add filters mapped to any request (i.e. "/*"). For example:
*
* <pre class="code">
* mockMvcBuilder.addFilters(springSecurityFilterChain);
* </pre>
*
* <p>is the equivalent of the following web.xml configuration:
*
* <pre class="code">
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;springSecurityFilterChain&lt;/filter-name&gt;
* &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
* &lt;/filter-mapping&gt;
* </pre>
*
* <p>Filters will be invoked in the order in which they are provided.
*
* @param filters the filters to add
*/
<T extends B> T addFilters(Filter... filters);
/**
* Add a filter mapped to a specific set of patterns. For example:
*
* <pre class="code">
* mockMvcBuilder.addFilters(myResourceFilter, "/resources/*");
* </pre>
*
* <p>is the equivalent of:
*
* <pre class="code">
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;myResourceFilter&lt;/filter-name&gt;
* &lt;url-pattern&gt;/resources/*&lt;/url-pattern&gt;
* &lt;/filter-mapping&gt;
* </pre>
*
* <p>Filters will be invoked in the order in which they are provided.
*
* @param filter the filter to add
* @param urlPatterns URL patterns to map to; if empty, "/*" is used by default
*/
<T extends B> T addFilter(Filter filter, String... urlPatterns);
/**
* Define default request properties that should be merged into all
* performed requests. In effect this provides a mechanism for defining
* common initialization for all requests such as the content type, request
* parameters, session attributes, and any other request property.
*
* <p>Properties specified at the time of performing a request override the
* default properties defined here.
*
* @param requestBuilder a RequestBuilder; see static factory methods in
* {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders}
* .
*/
<T extends B> T defaultRequest(RequestBuilder requestBuilder);
/**
* Define a global expectation that should <em>always</em> be applied to
* every response. For example, status code 200 (OK), content type
* {@code "application/json"}, etc.
*
* @param resultMatcher a ResultMatcher; see static factory methods in
* {@link org.springframework.test.web.servlet.result.MockMvcResultMatchers}
*/
<T extends B> T alwaysExpect(ResultMatcher resultMatcher);
/**
* Define a global action that should <em>always</em> be applied to every
* response. For example, writing detailed information about the performed
* request and resulting response to {@code System.out}.
*
* @param resultHandler a ResultHandler; see static factory methods in
* {@link org.springframework.test.web.servlet.result.MockMvcResultHandlers}
*/
<T extends B> T alwaysDo(ResultHandler resultHandler);
/**
* Whether to enable the DispatcherServlet property
* {@link org.springframework.web.servlet.DispatcherServlet#setDispatchOptionsRequest
* dispatchOptionsRequest} which allows processing of HTTP OPTIONS requests.
*/
<T extends B> T dispatchOptions(boolean dispatchOptions);
/**
* Add a {@code MockMvcConfigurer} that automates MockMvc setup and
* configures it for some specific purpose (e.g. security).
*/
<T extends B> T apply(MockMvcConfigurer configurer);
}

View File

@@ -5,7 +5,7 @@
* 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
* 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,
@@ -13,45 +13,42 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.servlet.setup;
import org.springframework.test.web.servlet.MockMvcBuilder;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.web.context.WebApplicationContext;
/**
* A contract that allows the encapsulation of a "recipe" for configuring a
* MockMvcBuilder for some specific purpose. For example a 3rd party library
* may use this to provide convenient, easy ways to set up MockMvc.
* Allows a sub-class to encapsulate logic for pre-configuring a
* {@code ConfigurableMockMvcBuilder} for some specific purpose. A 3rd party
* library may use this to provide shortcuts for setting up MockMvc.
*
* <p>Supported via {@link AbstractMockMvcBuilder#add(MockMvcConfigurer)}
* with instances of class likely created via static methods, e.g.:
* <p>Can be plugged in via {@link ConfigurableMockMvcBuilder#apply} with
* instances of this type likely created via static methods, e.g.:
*
* <pre class="code">
* MockMvcBuilders.webAppContextSetup(context)
* .add(myLibrary("foo","bar").myProperty("foo"))
* .build();
* MockMvcBuilders.webAppContextSetup(context).apply(mySetup("foo","bar")).build();
* </pre>
*
* @author Rossen Stoyanchev
* @since 4.1
* @see org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter
*/
public interface MockMvcConfigurer {
/**
* Invoked immediately after a {@code MockMvcConfigurer} is added via
* {@link ConfigurableMockMvcBuilder#apply}.
*/
void afterConfigurerAdded(ConfigurableMockMvcBuilder<?> builder);
/**
* Invoked immediately after a {@code MockMvcConfigurer} is configured via
* {@link AbstractMockMvcBuilder#add(MockMvcConfigurer)}.
* Invoked just before the MockMvc instance is created. Implementations may
* return a RequestPostProcessor to be applied to every request performed
* through the created {@code MockMvc} instance.
*/
void afterConfigurerAdded(MockMvcBuilder mockMvcBuilder);
/**
* Invoked just before the MockMvc instance is built providing access to the
* configured "default" RequestBuilder. If a "default" RequestBuilder is
* needed but was not configured and is {@code null}), it can still be added
* via {@link AbstractMockMvcBuilder#defaultRequest}.
*/
void beforeMockMvcCreated(MockMvcBuilder mockMvcBuilder, RequestBuilder defaultRequestBuilder,
WebApplicationContext applicationContext);
RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder<?> builder, WebApplicationContext context);
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2002-2014 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.test.web.servlet.setup;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.web.context.WebApplicationContext;
/**
* An empty method implementation of {@link MockMvcConfigurer}.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public abstract class MockMvcConfigurerAdapter implements MockMvcConfigurer {
@Override
public void afterConfigurerAdded(ConfigurableMockMvcBuilder<?> builder) {
}
@Override
public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder<?> builder, WebApplicationContext cxt) {
return null;
}
}