summaryrefslogtreecommitdiff
path: root/spring-test/src/main
diff options
context:
space:
mode:
authorEmmanuel Bourg <ebourg@apache.org>2014-12-03 14:31:16 +0100
committerEmmanuel Bourg <ebourg@apache.org>2014-12-03 14:31:16 +0100
commitc56370beb0a2bfa263e125fce107dceccee89fd3 (patch)
tree7ee611ceb0acbbdf7f83abcd72adb854b7d77225 /spring-test/src/main
parentaa5221b73661fa728dc4e62e1230e9104528c4eb (diff)
Imported Upstream version 3.2.12
Diffstat (limited to 'spring-test/src/main')
-rw-r--r--spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java61
-rw-r--r--spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java104
-rw-r--r--spring-test/src/main/java/org/springframework/mock/env/package-info.java10
-rw-r--r--spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java56
-rw-r--r--spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java86
-rw-r--r--spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.java122
-rw-r--r--spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java70
-rw-r--r--spring-test/src/main/java/org/springframework/mock/http/client/package-info.java23
-rw-r--r--spring-test/src/main/java/org/springframework/mock/http/package-info.java23
-rw-r--r--spring-test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java78
-rw-r--r--spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java345
-rw-r--r--spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java230
-rw-r--r--spring-test/src/main/java/org/springframework/mock/jndi/package-info.java13
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java66
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java71
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java96
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java197
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java96
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java183
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java104
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java917
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java603
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java263
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java191
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java131
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java131
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java353
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java93
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java103
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java499
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java85
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/package-info.java15
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java86
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java122
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockBaseURL.java152
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockCacheControl.java70
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockClientDataRequest.java128
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockEvent.java88
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventRequest.java88
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventResponse.java34
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockMimeResponse.java240
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java95
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java101
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java183
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java278
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java114
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java531
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java84
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java195
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java232
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java115
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java93
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java88
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceRequest.java128
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceResponse.java40
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceURL.java69
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/MockStateAwareResponse.java154
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java145
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/package-info.java14
-rw-r--r--spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java294
-rw-r--r--spring-test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java362
-rw-r--r--spring-test/src/main/java/org/springframework/test/AbstractSpringContextTests.java171
-rw-r--r--spring-test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java199
-rw-r--r--spring-test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java359
-rw-r--r--spring-test/src/main/java/org/springframework/test/AssertThrows.java255
-rw-r--r--spring-test/src/main/java/org/springframework/test/ConditionalTestCase.java99
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java313
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java142
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/ExpectedException.java43
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java105
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/NotTransactional.java43
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java52
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java56
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java209
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/Repeat.java43
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/Rollback.java45
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java58
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/Timed.java57
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/package-info.java6
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java109
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java121
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextCache.java256
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java312
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java420
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java157
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextLoader.java95
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java707
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java376
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java126
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/TestContext.java220
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/TestContextManager.java463
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java120
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java100
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java399
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java158
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit38/package-info.java7
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java102
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java175
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java474
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java7
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java92
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java105
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java64
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java77
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java76
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java76
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java7
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/package-info.java16
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java286
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java286
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java266
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java73
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java192
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java112
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java47
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java115
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java146
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java52
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java50
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/package-info.java6
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java210
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java166
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/testng/package-info.java7
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java45
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java45
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java70
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java77
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java596
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java7
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java267
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java172
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java48
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java194
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java66
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java48
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java206
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/package-info.java6
-rw-r--r--spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java419
-rw-r--r--spring-test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java187
-rw-r--r--spring-test/src/main/java/org/springframework/test/jdbc/package-info.java6
-rw-r--r--spring-test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java61
-rw-r--r--spring-test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java386
-rw-r--r--spring-test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java55
-rw-r--r--spring-test/src/main/java/org/springframework/test/jpa/package-info.java9
-rw-r--r--spring-test/src/main/java/org/springframework/test/package-info.java9
-rw-r--r--spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java93
-rw-r--r--spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java315
-rw-r--r--spring-test/src/main/java/org/springframework/test/util/package-info.java5
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java169
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java230
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/package-info.java6
-rw-r--r--spring-test/src/main/java/overview.html7
152 files changed, 23231 insertions, 0 deletions
diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java b/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java
new file mode 100644
index 00000000..8042e6d1
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java
@@ -0,0 +1,61 @@
+/*
+ * 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.mock.env;
+
+import org.springframework.core.env.AbstractEnvironment;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * Simple {@link ConfigurableEnvironment} implementation exposing
+ * {@link #setProperty(String, String)} and {@link #withProperty(String, String)}
+ * methods for testing purposes.
+ *
+ * @author Chris Beams
+ * @author Sam Brannen
+ * @since 3.2
+ * @see org.springframework.mock.env.MockPropertySource
+ */
+public class MockEnvironment extends AbstractEnvironment {
+
+ private MockPropertySource propertySource = new MockPropertySource();
+
+ /**
+ * Create a new {@code MockEnvironment} with a single {@link MockPropertySource}.
+ */
+ public MockEnvironment() {
+ getPropertySources().addLast(propertySource);
+ }
+
+ /**
+ * Set a property on the underlying {@link MockPropertySource} for this environment.
+ */
+ public void setProperty(String key, String value) {
+ propertySource.setProperty(key, value);
+ }
+
+ /**
+ * Convenient synonym for {@link #setProperty} that returns the current instance.
+ * Useful for method chaining and fluent-style use.
+ * @return this {@link MockEnvironment} instance
+ * @see MockPropertySource#withProperty
+ */
+ public MockEnvironment withProperty(String key, String value) {
+ this.setProperty(key, value);
+ return this;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java
new file mode 100644
index 00000000..124637e9
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java
@@ -0,0 +1,104 @@
+/*
+ * 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.mock.env;
+
+import java.util.Properties;
+
+import org.springframework.core.env.PropertiesPropertySource;
+import org.springframework.core.env.PropertySource;
+
+/**
+ * Simple {@link PropertySource} implementation for use in testing. Accepts
+ * a user-provided {@link Properties} object, or if omitted during construction,
+ * the implementation will initialize its own.
+ *
+ * The {@link #setProperty} and {@link #withProperty} methods are exposed for
+ * convenience, for example:
+ * <pre class="code">
+ * {@code
+ * PropertySource<?> source = new MockPropertySource().withProperty("foo", "bar");
+ * }
+ * </pre>
+ *
+ * @author Chris Beams
+ * @since 3.1
+ * @see org.springframework.mock.env.MockEnvironment
+ */
+public class MockPropertySource extends PropertiesPropertySource {
+
+ /**
+ * {@value} is the default name for {@link MockPropertySource} instances not
+ * otherwise given an explicit name.
+ * @see #MockPropertySource()
+ * @see #MockPropertySource(String)
+ */
+ public static final String MOCK_PROPERTIES_PROPERTY_SOURCE_NAME = "mockProperties";
+
+ /**
+ * Create a new {@code MockPropertySource} named {@value #MOCK_PROPERTIES_PROPERTY_SOURCE_NAME}
+ * that will maintain its own internal {@link Properties} instance.
+ */
+ public MockPropertySource() {
+ this(new Properties());
+ }
+
+ /**
+ * Create a new {@code MockPropertySource} with the given name that will
+ * maintain its own internal {@link Properties} instance.
+ * @param name the {@linkplain #getName() name} of the property source
+ */
+ public MockPropertySource(String name) {
+ this(name, new Properties());
+ }
+
+ /**
+ * Create a new {@code MockPropertySource} named {@value #MOCK_PROPERTIES_PROPERTY_SOURCE_NAME}
+ * and backed by the given {@link Properties} object.
+ * @param properties the properties to use
+ */
+ public MockPropertySource(Properties properties) {
+ this(MOCK_PROPERTIES_PROPERTY_SOURCE_NAME, properties);
+ }
+
+ /**
+ * Create a new {@code MockPropertySource} with the given name and backed by the given
+ * {@link Properties} object.
+ * @param name the {@linkplain #getName() name} of the property source
+ * @param properties the properties to use
+ */
+ public MockPropertySource(String name, Properties properties) {
+ super(name, properties);
+ }
+
+ /**
+ * Set the given property on the underlying {@link Properties} object.
+ */
+ public void setProperty(String name, Object value) {
+ this.source.put(name, value);
+ }
+
+ /**
+ * Convenient synonym for {@link #setProperty} that returns the current instance.
+ * Useful for method chaining and fluent-style use.
+ * @return this {@link MockPropertySource} instance
+ */
+ public MockPropertySource withProperty(String name, Object value) {
+ this.setProperty(name, value);
+ return this;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/env/package-info.java b/spring-test/src/main/java/org/springframework/mock/env/package-info.java
new file mode 100644
index 00000000..1772b5c9
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/env/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * This package contains mock implementations of the
+ * {@link org.springframework.core.env.Environment Environment} and
+ * {@link org.springframework.core.env.PropertySource PropertySource}
+ * abstractions introduced in Spring 3.1.
+ *
+ * <p>These <em>mocks</em> are useful for developing <em>out-of-container</em>
+ * unit tests for code that depends on environment-specific properties.
+ */
+package org.springframework.mock.env;
diff --git a/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java b/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java
new file mode 100644
index 00000000..5de14e99
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java
@@ -0,0 +1,56 @@
+/*
+ * 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.mock.http;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of {@link HttpInputMessage}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 3.2
+ */
+public class MockHttpInputMessage implements HttpInputMessage {
+
+ private final HttpHeaders headers = new HttpHeaders();
+
+ private final InputStream body;
+
+
+ public MockHttpInputMessage(byte[] contents) {
+ this.body = (contents != null) ? new ByteArrayInputStream(contents) : null;
+ }
+
+ public MockHttpInputMessage(InputStream body) {
+ Assert.notNull(body, "'body' must not be null");
+ this.body = body;
+ }
+
+ public HttpHeaders getHeaders() {
+ return this.headers;
+ }
+
+ public InputStream getBody() throws IOException {
+ return this.body;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java b/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java
new file mode 100644
index 00000000..a4ef87e7
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java
@@ -0,0 +1,86 @@
+/*
+ * 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.mock.http;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpOutputMessage;
+
+/**
+ * Mock implementation of {@link HttpOutputMessage}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 3.2
+ */
+public class MockHttpOutputMessage implements HttpOutputMessage {
+
+ private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+ private final HttpHeaders headers = new HttpHeaders();
+
+ private final ByteArrayOutputStream body = new ByteArrayOutputStream(1024);
+
+
+ /**
+ * Return the headers.
+ */
+ public HttpHeaders getHeaders() {
+ return this.headers;
+ }
+
+ /**
+ * Return the body content.
+ */
+ public OutputStream getBody() throws IOException {
+ return this.body;
+ }
+
+ /**
+ * Return body content as a byte array.
+ */
+ public byte[] getBodyAsBytes() {
+ return this.body.toByteArray();
+ }
+
+ /**
+ * Return the body content interpreted as a UTF-8 string.
+ */
+ public String getBodyAsString() {
+ return getBodyAsString(DEFAULT_CHARSET);
+ }
+
+ /**
+ * Return the body content as a string.
+ * @param charset the charset to use to turn the body content to a String
+ */
+ public String getBodyAsString(Charset charset) {
+ byte[] bytes = getBodyAsBytes();
+ try {
+ return new String(bytes, charset.name());
+ }
+ catch (UnsupportedEncodingException ex) {
+ // should not occur
+ throw new IllegalStateException(ex);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.java
new file mode 100644
index 00000000..0f46bbbc
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.mock.http.client;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.mock.http.MockHttpOutputMessage;
+
+/**
+ * Mock implementation of {@link ClientHttpRequest}.
+ *
+ * @author Rossen Stoyanchev
+ * @author Sam Brannen
+ * @since 3.2
+ */
+public class MockClientHttpRequest extends MockHttpOutputMessage implements ClientHttpRequest {
+
+ private URI uri;
+
+ private HttpMethod httpMethod;
+
+ private boolean executed = false;
+
+ private ClientHttpResponse clientHttpResponse;
+
+
+ /**
+ * Default constructor.
+ */
+ public MockClientHttpRequest() {
+ }
+
+ /**
+ * Create an instance with the given HttpMethod and URI.
+ */
+ public MockClientHttpRequest(HttpMethod httpMethod, URI uri) {
+ this.httpMethod = httpMethod;
+ this.uri = uri;
+ }
+
+ public URI getURI() {
+ return this.uri;
+ }
+
+ public void setURI(URI uri) {
+ this.uri = uri;
+ }
+
+ public HttpMethod getMethod() {
+ return this.httpMethod;
+ }
+
+ public void setMethod(HttpMethod httpMethod) {
+ this.httpMethod = httpMethod;
+ }
+
+ public void setResponse(ClientHttpResponse clientHttpResponse) {
+ this.clientHttpResponse = clientHttpResponse;
+ }
+
+ public boolean isExecuted() {
+ return this.executed;
+ }
+
+ /**
+ * Set the {@link #isExecuted() executed} flag to {@code true} and return the
+ * configured {@link #setResponse(ClientHttpResponse) response}.
+ * @see #executeInternal()
+ */
+ public final ClientHttpResponse execute() throws IOException {
+ this.executed = true;
+ return executeInternal();
+ }
+
+ /**
+ * The default implementation returns the configured
+ * {@link #setResponse(ClientHttpResponse) response}.
+ *
+ * <p>Override this method to execute the request and provide a response,
+ * potentially different than the configured response.
+ */
+ protected ClientHttpResponse executeInternal() throws IOException {
+ return this.clientHttpResponse;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ if (this.httpMethod != null) {
+ sb.append(this.httpMethod);
+ }
+ if (this.uri != null) {
+ sb.append(" ").append(this.uri);
+ }
+ if (!getHeaders().isEmpty()) {
+ sb.append(", headers : ").append(getHeaders());
+ }
+ if (sb.length() == 0) {
+ sb.append("Not yet initialized");
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java
new file mode 100644
index 00000000..9ed40774
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java
@@ -0,0 +1,70 @@
+/*
+ * 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.mock.http.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.mock.http.MockHttpInputMessage;
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of {@link ClientHttpResponse}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 3.2
+ */
+public class MockClientHttpResponse extends MockHttpInputMessage implements ClientHttpResponse {
+
+ private final HttpStatus status;
+
+
+ /**
+ * Constructor with response body as a byte array.
+ */
+ public MockClientHttpResponse(byte[] body, HttpStatus statusCode) {
+ super(body);
+ Assert.notNull(statusCode, "statisCode is required");
+ this.status = statusCode;
+ }
+
+ /**
+ * Constructor with response body as InputStream.
+ */
+ public MockClientHttpResponse(InputStream body, HttpStatus statusCode) {
+ super(body);
+ Assert.notNull(statusCode, "statisCode is required");
+ this.status = statusCode;
+ }
+
+ public HttpStatus getStatusCode() throws IOException {
+ return this.status;
+ }
+
+ public int getRawStatusCode() throws IOException {
+ return this.status.value();
+ }
+
+ public String getStatusText() throws IOException {
+ return this.status.getReasonPhrase();
+ }
+
+ public void close() {
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/package-info.java b/spring-test/src/main/java/org/springframework/mock/http/client/package-info.java
new file mode 100644
index 00000000..927dd34e
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/http/client/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Mock implementations of client-side HTTP abstractions.
+ * This package contains the {@code MockClientHttpRequest} and
+ * {@code MockClientHttpResponse}.
+ */
+package org.springframework.mock.http.client;
+
diff --git a/spring-test/src/main/java/org/springframework/mock/http/package-info.java b/spring-test/src/main/java/org/springframework/mock/http/package-info.java
new file mode 100644
index 00000000..13cf4967
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/http/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Mock implementations of client/server-side HTTP abstractions.
+ * This package contains {@code MockHttpInputMessage} and
+ * {@code MockHttpOutputMessage}.
+ */
+package org.springframework.mock.http;
+
diff --git a/spring-test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java b/spring-test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java
new file mode 100644
index 00000000..b19aaf5c
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java
@@ -0,0 +1,78 @@
+/*
+ * 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.mock.jndi;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.naming.NamingException;
+
+import org.springframework.jndi.JndiTemplate;
+
+/**
+ * Simple extension of the JndiTemplate class that always returns a given object.
+ *
+ * <p>Very useful for testing. Effectively a mock object.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ */
+public class ExpectedLookupTemplate extends JndiTemplate {
+
+ private final Map<String, Object> jndiObjects = new ConcurrentHashMap<String, Object>(16);
+
+
+ /**
+ * Construct a new JndiTemplate that will always return given objects for
+ * given names. To be populated through {@code addObject} calls.
+ * @see #addObject(String, Object)
+ */
+ public ExpectedLookupTemplate() {
+ }
+
+ /**
+ * Construct a new JndiTemplate that will always return the given object,
+ * but honour only requests for the given name.
+ * @param name the name the client is expected to look up
+ * @param object the object that will be returned
+ */
+ public ExpectedLookupTemplate(String name, Object object) {
+ addObject(name, object);
+ }
+
+ /**
+ * Add the given object to the list of JNDI objects that this template will expose.
+ * @param name the name the client is expected to look up
+ * @param object the object that will be returned
+ */
+ public void addObject(String name, Object object) {
+ this.jndiObjects.put(name, object);
+ }
+
+ /**
+ * If the name is the expected name specified in the constructor, return the
+ * object provided in the constructor. If the name is unexpected, a
+ * respective NamingException gets thrown.
+ */
+ public Object lookup(String name) throws NamingException {
+ Object object = this.jndiObjects.get(name);
+ if (object == null) {
+ throw new NamingException("Unexpected JNDI name '" + name + "': expecting " + this.jndiObjects.keySet());
+ }
+ return object;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java
new file mode 100644
index 00000000..d35fbdc8
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java
@@ -0,0 +1,345 @@
+/*
+ * 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.mock.jndi;
+
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map;
+import javax.naming.Binding;
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.NameClassPair;
+import javax.naming.NameNotFoundException;
+import javax.naming.NameParser;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.OperationNotSupportedException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * Simple implementation of a JNDI naming context.
+ * Only supports binding plain Objects to String names.
+ * Mainly for test environments, but also usable for standalone applications.
+ *
+ * <p>This class is not intended for direct usage by applications, although it
+ * can be used for example to override JndiTemplate's {@code createInitialContext}
+ * method in unit tests. Typically, SimpleNamingContextBuilder will be used to
+ * set up a JVM-level JNDI environment.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @see SimpleNamingContextBuilder
+ * @see org.springframework.jndi.JndiTemplate#createInitialContext
+ */
+public class SimpleNamingContext implements Context {
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final String root;
+
+ private final Hashtable<String, Object> boundObjects;
+
+ private final Hashtable<String, Object> environment = new Hashtable<String, Object>();
+
+
+ /**
+ * Create a new naming context.
+ */
+ public SimpleNamingContext() {
+ this("");
+ }
+
+ /**
+ * Create a new naming context with the given naming root.
+ */
+ public SimpleNamingContext(String root) {
+ this.root = root;
+ this.boundObjects = new Hashtable<String, Object>();
+ }
+
+ /**
+ * Create a new naming context with the given naming root,
+ * the given name/object map, and the JNDI environment entries.
+ */
+ public SimpleNamingContext(String root, Hashtable<String, Object> boundObjects, Hashtable<String, Object> env) {
+ this.root = root;
+ this.boundObjects = boundObjects;
+ if (env != null) {
+ this.environment.putAll(env);
+ }
+ }
+
+
+ // Actual implementations of Context methods follow
+
+ public NamingEnumeration<NameClassPair> list(String root) throws NamingException {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Listing name/class pairs under [" + root + "]");
+ }
+ return new NameClassPairEnumeration(this, root);
+ }
+
+ public NamingEnumeration<Binding> listBindings(String root) throws NamingException {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Listing bindings under [" + root + "]");
+ }
+ return new BindingEnumeration(this, root);
+ }
+
+ /**
+ * Look up the object with the given name.
+ * <p>Note: Not intended for direct use by applications.
+ * Will be used by any standard InitialContext JNDI lookups.
+ * @throws javax.naming.NameNotFoundException if the object could not be found
+ */
+ public Object lookup(String lookupName) throws NameNotFoundException {
+ String name = this.root + lookupName;
+ if (logger.isDebugEnabled()) {
+ logger.debug("Static JNDI lookup: [" + name + "]");
+ }
+ if ("".equals(name)) {
+ return new SimpleNamingContext(this.root, this.boundObjects, this.environment);
+ }
+ Object found = this.boundObjects.get(name);
+ if (found == null) {
+ if (!name.endsWith("/")) {
+ name = name + "/";
+ }
+ for (String boundName : this.boundObjects.keySet()) {
+ if (boundName.startsWith(name)) {
+ return new SimpleNamingContext(name, this.boundObjects, this.environment);
+ }
+ }
+ throw new NameNotFoundException(
+ "Name [" + this.root + lookupName + "] not bound; " + this.boundObjects.size() + " bindings: [" +
+ StringUtils.collectionToDelimitedString(this.boundObjects.keySet(), ",") + "]");
+ }
+ return found;
+ }
+
+ public Object lookupLink(String name) throws NameNotFoundException {
+ return lookup(name);
+ }
+
+ /**
+ * Bind the given object to the given name.
+ * Note: Not intended for direct use by applications
+ * if setting up a JVM-level JNDI environment.
+ * Use SimpleNamingContextBuilder to set up JNDI bindings then.
+ * @see org.springframework.mock.jndi.SimpleNamingContextBuilder#bind
+ */
+ public void bind(String name, Object obj) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Static JNDI binding: [" + this.root + name + "] = [" + obj + "]");
+ }
+ this.boundObjects.put(this.root + name, obj);
+ }
+
+ public void unbind(String name) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Static JNDI remove: [" + this.root + name + "]");
+ }
+ this.boundObjects.remove(this.root + name);
+ }
+
+ public void rebind(String name, Object obj) {
+ bind(name, obj);
+ }
+
+ public void rename(String oldName, String newName) throws NameNotFoundException {
+ Object obj = lookup(oldName);
+ unbind(oldName);
+ bind(newName, obj);
+ }
+
+ public Context createSubcontext(String name) {
+ String subcontextName = this.root + name;
+ if (!subcontextName.endsWith("/")) {
+ subcontextName += "/";
+ }
+ Context subcontext = new SimpleNamingContext(subcontextName, this.boundObjects, this.environment);
+ bind(name, subcontext);
+ return subcontext;
+ }
+
+ public void destroySubcontext(String name) {
+ unbind(name);
+ }
+
+ public String composeName(String name, String prefix) {
+ return prefix + name;
+ }
+
+ public Hashtable<String, Object> getEnvironment() {
+ return this.environment;
+ }
+
+ public Object addToEnvironment(String propName, Object propVal) {
+ return this.environment.put(propName, propVal);
+ }
+
+ public Object removeFromEnvironment(String propName) {
+ return this.environment.remove(propName);
+ }
+
+ public void close() {
+ }
+
+
+ // Unsupported methods follow: no support for javax.naming.Name
+
+ public NamingEnumeration<NameClassPair> list(Name name) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public NamingEnumeration<Binding> listBindings(Name name) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public Object lookup(Name name) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public Object lookupLink(Name name) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public void bind(Name name, Object obj) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public void unbind(Name name) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public void rebind(Name name, Object obj) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public void rename(Name oldName, Name newName) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public Context createSubcontext(Name name) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public void destroySubcontext(Name name) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public String getNameInNamespace() throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public NameParser getNameParser(Name name) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public NameParser getNameParser(String name) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+ public Name composeName(Name name, Name prefix) throws NamingException {
+ throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]");
+ }
+
+
+ private static abstract class AbstractNamingEnumeration<T> implements NamingEnumeration<T> {
+
+ private Iterator<T> iterator;
+
+ private AbstractNamingEnumeration(SimpleNamingContext context, String proot) throws NamingException {
+ if (!"".equals(proot) && !proot.endsWith("/")) {
+ proot = proot + "/";
+ }
+ String root = context.root + proot;
+ Map<String, T> contents = new HashMap<String, T>();
+ for (String boundName : context.boundObjects.keySet()) {
+ if (boundName.startsWith(root)) {
+ int startIndex = root.length();
+ int endIndex = boundName.indexOf('/', startIndex);
+ String strippedName =
+ (endIndex != -1 ? boundName.substring(startIndex, endIndex) : boundName.substring(startIndex));
+ if (!contents.containsKey(strippedName)) {
+ try {
+ contents.put(strippedName, createObject(strippedName, context.lookup(proot + strippedName)));
+ }
+ catch (NameNotFoundException ex) {
+ // cannot happen
+ }
+ }
+ }
+ }
+ if (contents.size() == 0) {
+ throw new NamingException("Invalid root: [" + context.root + proot + "]");
+ }
+ this.iterator = contents.values().iterator();
+ }
+
+ protected abstract T createObject(String strippedName, Object obj);
+
+ public boolean hasMore() {
+ return this.iterator.hasNext();
+ }
+
+ public T next() {
+ return this.iterator.next();
+ }
+
+ public boolean hasMoreElements() {
+ return this.iterator.hasNext();
+ }
+
+ public T nextElement() {
+ return this.iterator.next();
+ }
+
+ public void close() {
+ }
+ }
+
+
+ private static class NameClassPairEnumeration extends AbstractNamingEnumeration<NameClassPair> {
+
+ private NameClassPairEnumeration(SimpleNamingContext context, String root) throws NamingException {
+ super(context, root);
+ }
+
+ protected NameClassPair createObject(String strippedName, Object obj) {
+ return new NameClassPair(strippedName, obj.getClass().getName());
+ }
+ }
+
+
+ private static class BindingEnumeration extends AbstractNamingEnumeration<Binding> {
+
+ private BindingEnumeration(SimpleNamingContext context, String root) throws NamingException {
+ super(context, root);
+ }
+
+ protected Binding createObject(String strippedName, Object obj) {
+ return new Binding(strippedName, obj);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java
new file mode 100644
index 00000000..f813203b
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java
@@ -0,0 +1,230 @@
+/*
+ * 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.mock.jndi;
+
+import java.util.Hashtable;
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.spi.InitialContextFactory;
+import javax.naming.spi.InitialContextFactoryBuilder;
+import javax.naming.spi.NamingManager;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.util.ClassUtils;
+
+/**
+ * Simple implementation of a JNDI naming context builder.
+ *
+ * <p>Mainly targeted at test environments, where each test case can
+ * configure JNDI appropriately, so that {@code new InitialContext()}
+ * will expose the required objects. Also usable for standalone applications,
+ * e.g. for binding a JDBC DataSource to a well-known JNDI location, to be
+ * able to use traditional J2EE data access code outside of a J2EE container.
+ *
+ * <p>There are various choices for DataSource implementations:
+ * <ul>
+ * <li>{@code SingleConnectionDataSource} (using the same Connection for all getConnection calls)
+ * <li>{@code DriverManagerDataSource} (creating a new Connection on each getConnection call)
+ * <li>Apache's Jakarta Commons DBCP offers {@code org.apache.commons.dbcp.BasicDataSource} (a real pool)
+ * </ul>
+ *
+ * <p>Typical usage in bootstrap code:
+ *
+ * <pre class="code">
+ * SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
+ * DataSource ds = new DriverManagerDataSource(...);
+ * builder.bind("java:comp/env/jdbc/myds", ds);
+ * builder.activate();</pre>
+ *
+ * Note that it's impossible to activate multiple builders within the same JVM,
+ * due to JNDI restrictions. Thus to configure a fresh builder repeatedly, use
+ * the following code to get a reference to either an already activated builder
+ * or a newly activated one:
+ *
+ * <pre class="code">
+ * SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.emptyActivatedContextBuilder();
+ * DataSource ds = new DriverManagerDataSource(...);
+ * builder.bind("java:comp/env/jdbc/myds", ds);</pre>
+ *
+ * Note that you <i>should not</i> call {@code activate()} on a builder from
+ * this factory method, as there will already be an activated one in any case.
+ *
+ * <p>An instance of this class is only necessary at setup time.
+ * An application does not need to keep a reference to it after activation.
+ *
+ * @author Juergen Hoeller
+ * @author Rod Johnson
+ * @see #emptyActivatedContextBuilder()
+ * @see #bind(String, Object)
+ * @see #activate()
+ * @see SimpleNamingContext
+ * @see org.springframework.jdbc.datasource.SingleConnectionDataSource
+ * @see org.springframework.jdbc.datasource.DriverManagerDataSource
+ */
+public class SimpleNamingContextBuilder implements InitialContextFactoryBuilder {
+
+ /** An instance of this class bound to JNDI */
+ private static volatile SimpleNamingContextBuilder activated;
+
+ private static boolean initialized = false;
+
+ private static final Object initializationLock = new Object();
+
+
+ /**
+ * Checks if a SimpleNamingContextBuilder is active.
+ * @return the current SimpleNamingContextBuilder instance,
+ * or {@code null} if none
+ */
+ public static SimpleNamingContextBuilder getCurrentContextBuilder() {
+ return activated;
+ }
+
+ /**
+ * If no SimpleNamingContextBuilder is already configuring JNDI,
+ * create and activate one. Otherwise take the existing activate
+ * SimpleNamingContextBuilder, clear it and return it.
+ * <p>This is mainly intended for test suites that want to
+ * reinitialize JNDI bindings from scratch repeatedly.
+ * @return an empty SimpleNamingContextBuilder that can be used
+ * to control JNDI bindings
+ */
+ public static SimpleNamingContextBuilder emptyActivatedContextBuilder() throws NamingException {
+ if (activated != null) {
+ // Clear already activated context builder.
+ activated.clear();
+ }
+ else {
+ // Create and activate new context builder.
+ SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
+ // The activate() call will cause an assignment to the activated variable.
+ builder.activate();
+ }
+ return activated;
+ }
+
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final Hashtable<String,Object> boundObjects = new Hashtable<String,Object>();
+
+
+ /**
+ * Register the context builder by registering it with the JNDI NamingManager.
+ * Note that once this has been done, {@code new InitialContext()} will always
+ * return a context from this factory. Use the {@code emptyActivatedContextBuilder()}
+ * static method to get an empty context (for example, in test methods).
+ * @throws IllegalStateException if there's already a naming context builder
+ * registered with the JNDI NamingManager
+ */
+ public void activate() throws IllegalStateException, NamingException {
+ logger.info("Activating simple JNDI environment");
+ synchronized (initializationLock) {
+ if (!initialized) {
+ if (NamingManager.hasInitialContextFactoryBuilder()) {
+ throw new IllegalStateException(
+ "Cannot activate SimpleNamingContextBuilder: there is already a JNDI provider registered. " +
+ "Note that JNDI is a JVM-wide service, shared at the JVM system class loader level, " +
+ "with no reset option. As a consequence, a JNDI provider must only be registered once per JVM.");
+ }
+ NamingManager.setInitialContextFactoryBuilder(this);
+ initialized = true;
+ }
+ }
+ activated = this;
+ }
+
+ /**
+ * Temporarily deactivate this context builder. It will remain registered with
+ * the JNDI NamingManager but will delegate to the standard JNDI InitialContextFactory
+ * (if configured) instead of exposing its own bound objects.
+ * <p>Call {@code activate()} again in order to expose this context builder's own
+ * bound objects again. Such activate/deactivate sequences can be applied any number
+ * of times (e.g. within a larger integration test suite running in the same VM).
+ * @see #activate()
+ */
+ public void deactivate() {
+ logger.info("Deactivating simple JNDI environment");
+ activated = null;
+ }
+
+ /**
+ * Clear all bindings in this context builder, while keeping it active.
+ */
+ public void clear() {
+ this.boundObjects.clear();
+ }
+
+ /**
+ * Bind the given object under the given name, for all naming contexts
+ * that this context builder will generate.
+ * @param name the JNDI name of the object (e.g. "java:comp/env/jdbc/myds")
+ * @param obj the object to bind (e.g. a DataSource implementation)
+ */
+ public void bind(String name, Object obj) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Static JNDI binding: [" + name + "] = [" + obj + "]");
+ }
+ this.boundObjects.put(name, obj);
+ }
+
+
+ /**
+ * Simple InitialContextFactoryBuilder implementation,
+ * creating a new SimpleNamingContext instance.
+ * @see SimpleNamingContext
+ */
+ public InitialContextFactory createInitialContextFactory(Hashtable<?,?> environment) {
+ if (activated == null && environment != null) {
+ Object icf = environment.get(Context.INITIAL_CONTEXT_FACTORY);
+ if (icf != null) {
+ Class<?> icfClass;
+ if (icf instanceof Class) {
+ icfClass = (Class<?>) icf;
+ }
+ else if (icf instanceof String) {
+ icfClass = ClassUtils.resolveClassName((String) icf, getClass().getClassLoader());
+ }
+ else {
+ throw new IllegalArgumentException("Invalid value type for environment key [" +
+ Context.INITIAL_CONTEXT_FACTORY + "]: " + icf.getClass().getName());
+ }
+ if (!InitialContextFactory.class.isAssignableFrom(icfClass)) {
+ throw new IllegalArgumentException(
+ "Specified class does not implement [" + InitialContextFactory.class.getName() + "]: " + icf);
+ }
+ try {
+ return (InitialContextFactory) icfClass.newInstance();
+ }
+ catch (Throwable ex) {
+ throw new IllegalStateException("Cannot instantiate specified InitialContextFactory: " + icf, ex);
+ }
+ }
+ }
+
+ // Default case...
+ return new InitialContextFactory() {
+ @SuppressWarnings("unchecked")
+ public Context getInitialContext(Hashtable<?,?> environment) {
+ return new SimpleNamingContext("", boundObjects, (Hashtable<String, Object>) environment);
+ }
+ };
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/jndi/package-info.java b/spring-test/src/main/java/org/springframework/mock/jndi/package-info.java
new file mode 100644
index 00000000..992e2369
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/jndi/package-info.java
@@ -0,0 +1,13 @@
+
+/**
+ *
+ * The simplest implementation of the JNDI SPI that could possibly work.
+ *
+ * <p>Useful for setting up a simple JNDI environment for test suites
+ * or stand-alone applications. If, for example, JDBC DataSources get bound to the
+ * same JNDI names as within a Java EE container, both application code and
+ * configuration can be reused without changes.
+ *
+ */
+package org.springframework.mock.jndi;
+
diff --git a/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java b/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java
new file mode 100644
index 00000000..f97e60e2
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java
@@ -0,0 +1,66 @@
+/*
+ * 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.mock.web;
+
+import java.io.IOException;
+import java.io.InputStream;
+import javax.servlet.ServletInputStream;
+
+import org.springframework.util.Assert;
+
+/**
+ * Delegating implementation of {@link javax.servlet.ServletInputStream}.
+ *
+ * <p>Used by {@link MockHttpServletRequest}; typically not directly
+ * used for testing application controllers.
+ *
+ * @author Juergen Hoeller
+ * @since 1.0.2
+ * @see MockHttpServletRequest
+ */
+public class DelegatingServletInputStream extends ServletInputStream {
+
+ private final InputStream sourceStream;
+
+
+ /**
+ * Create a DelegatingServletInputStream for the given source stream.
+ * @param sourceStream the source stream (never {@code null})
+ */
+ public DelegatingServletInputStream(InputStream sourceStream) {
+ Assert.notNull(sourceStream, "Source InputStream must not be null");
+ this.sourceStream = sourceStream;
+ }
+
+ /**
+ * Return the underlying source stream (never {@code null}).
+ */
+ public final InputStream getSourceStream() {
+ return this.sourceStream;
+ }
+
+
+ public int read() throws IOException {
+ return this.sourceStream.read();
+ }
+
+ public void close() throws IOException {
+ super.close();
+ this.sourceStream.close();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java b/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java
new file mode 100644
index 00000000..23694170
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java
@@ -0,0 +1,71 @@
+/*
+ * 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.mock.web;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import javax.servlet.ServletOutputStream;
+
+import org.springframework.util.Assert;
+
+/**
+ * Delegating implementation of {@link javax.servlet.ServletOutputStream}.
+ *
+ * <p>Used by {@link MockHttpServletResponse}; typically not directly
+ * used for testing application controllers.
+ *
+ * @author Juergen Hoeller
+ * @since 1.0.2
+ * @see MockHttpServletResponse
+ */
+public class DelegatingServletOutputStream extends ServletOutputStream {
+
+ private final OutputStream targetStream;
+
+
+ /**
+ * Create a DelegatingServletOutputStream for the given target stream.
+ * @param targetStream the target stream (never {@code null})
+ */
+ public DelegatingServletOutputStream(OutputStream targetStream) {
+ Assert.notNull(targetStream, "Target OutputStream must not be null");
+ this.targetStream = targetStream;
+ }
+
+ /**
+ * Return the underlying target stream (never {@code null}).
+ */
+ public final OutputStream getTargetStream() {
+ return this.targetStream;
+ }
+
+
+ public void write(int b) throws IOException {
+ this.targetStream.write(b);
+ }
+
+ public void flush() throws IOException {
+ super.flush();
+ this.targetStream.flush();
+ }
+
+ public void close() throws IOException {
+ super.close();
+ this.targetStream.close();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java b/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java
new file mode 100644
index 00000000..e8f5e91d
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java
@@ -0,0 +1,96 @@
+/*
+ * 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.mock.web;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Internal helper class that serves as value holder for request headers.
+ *
+ * @author Juergen Hoeller
+ * @author Rick Evans
+ * @since 2.0.1
+ */
+class HeaderValueHolder {
+
+ private final List<Object> values = new LinkedList<Object>();
+
+
+ public void setValue(Object value) {
+ this.values.clear();
+ this.values.add(value);
+ }
+
+ public void addValue(Object value) {
+ this.values.add(value);
+ }
+
+ public void addValues(Collection<?> values) {
+ this.values.addAll(values);
+ }
+
+ public void addValueArray(Object values) {
+ CollectionUtils.mergeArrayIntoCollection(values, this.values);
+ }
+
+ public List<Object> getValues() {
+ return Collections.unmodifiableList(this.values);
+ }
+
+ public List<String> getStringValues() {
+ List<String> stringList = new ArrayList<String>(this.values.size());
+ for (Object value : this.values) {
+ stringList.add(value.toString());
+ }
+ return Collections.unmodifiableList(stringList);
+ }
+
+ public Object getValue() {
+ return (!this.values.isEmpty() ? this.values.get(0) : null);
+ }
+
+ public String getStringValue() {
+ return (!this.values.isEmpty() ? this.values.get(0).toString() : null);
+ }
+
+
+ /**
+ * Find a HeaderValueHolder by name, ignoring casing.
+ * @param headers the Map of header names to HeaderValueHolders
+ * @param name the name of the desired header
+ * @return the corresponding HeaderValueHolder,
+ * or {@code null} if none found
+ */
+ public static HeaderValueHolder getByName(Map<String, HeaderValueHolder> headers, String name) {
+ Assert.notNull(name, "Header name must not be null");
+ for (String headerName : headers.keySet()) {
+ if (headerName.equalsIgnoreCase(name)) {
+ return headers.get(headerName);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java b/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java
new file mode 100644
index 00000000..3fb6ca77
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2002-2009 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.mock.web;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.jsp.JspWriter;
+import javax.servlet.jsp.tagext.BodyContent;
+
+/**
+ * Mock implementation of the {@link javax.servlet.jsp.tagext.BodyContent} class.
+ *
+ * <p>Used for testing the web framework; only necessary for testing
+ * applications when testing custom JSP tags.
+ *
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+public class MockBodyContent extends BodyContent {
+
+ private final String content;
+
+
+ /**
+ * Create a MockBodyContent for the given response.
+ * @param content the body content to expose
+ * @param response the servlet response to wrap
+ */
+ public MockBodyContent(String content, HttpServletResponse response) {
+ this(content, response, null);
+ }
+
+ /**
+ * Create a MockBodyContent for the given response.
+ * @param content the body content to expose
+ * @param targetWriter the target Writer to wrap
+ */
+ public MockBodyContent(String content, Writer targetWriter) {
+ this(content, null, targetWriter);
+ }
+
+ /**
+ * Create a MockBodyContent for the given response.
+ * @param content the body content to expose
+ * @param response the servlet response to wrap
+ * @param targetWriter the target Writer to wrap
+ */
+ public MockBodyContent(String content, HttpServletResponse response, Writer targetWriter) {
+ super(adaptJspWriter(targetWriter, response));
+ this.content = content;
+ }
+
+ private static JspWriter adaptJspWriter(Writer targetWriter, HttpServletResponse response) {
+ if (targetWriter instanceof JspWriter) {
+ return (JspWriter) targetWriter;
+ }
+ else {
+ return new MockJspWriter(response, targetWriter);
+ }
+ }
+
+
+ public Reader getReader() {
+ return new StringReader(this.content);
+ }
+
+ public String getString() {
+ return this.content;
+ }
+
+ public void writeOut(Writer writer) throws IOException {
+ writer.write(this.content);
+ }
+
+
+ //---------------------------------------------------------------------
+ // Delegating implementations of JspWriter's abstract methods
+ //---------------------------------------------------------------------
+
+ public void clear() throws IOException {
+ getEnclosingWriter().clear();
+ }
+
+ public void clearBuffer() throws IOException {
+ getEnclosingWriter().clearBuffer();
+ }
+
+ public void close() throws IOException {
+ getEnclosingWriter().close();
+ }
+
+ public int getRemaining() {
+ return getEnclosingWriter().getRemaining();
+ }
+
+ public void newLine() throws IOException {
+ getEnclosingWriter().println();
+ }
+
+ public void write(char value[], int offset, int length) throws IOException {
+ getEnclosingWriter().write(value, offset, length);
+ }
+
+ public void print(boolean value) throws IOException {
+ getEnclosingWriter().print(value);
+ }
+
+ public void print(char value) throws IOException {
+ getEnclosingWriter().print(value);
+ }
+
+ public void print(char[] value) throws IOException {
+ getEnclosingWriter().print(value);
+ }
+
+ public void print(double value) throws IOException {
+ getEnclosingWriter().print(value);
+ }
+
+ public void print(float value) throws IOException {
+ getEnclosingWriter().print(value);
+ }
+
+ public void print(int value) throws IOException {
+ getEnclosingWriter().print(value);
+ }
+
+ public void print(long value) throws IOException {
+ getEnclosingWriter().print(value);
+ }
+
+ public void print(Object value) throws IOException {
+ getEnclosingWriter().print(value);
+ }
+
+ public void print(String value) throws IOException {
+ getEnclosingWriter().print(value);
+ }
+
+ public void println() throws IOException {
+ getEnclosingWriter().println();
+ }
+
+ public void println(boolean value) throws IOException {
+ getEnclosingWriter().println(value);
+ }
+
+ public void println(char value) throws IOException {
+ getEnclosingWriter().println(value);
+ }
+
+ public void println(char[] value) throws IOException {
+ getEnclosingWriter().println(value);
+ }
+
+ public void println(double value) throws IOException {
+ getEnclosingWriter().println(value);
+ }
+
+ public void println(float value) throws IOException {
+ getEnclosingWriter().println(value);
+ }
+
+ public void println(int value) throws IOException {
+ getEnclosingWriter().println(value);
+ }
+
+ public void println(long value) throws IOException {
+ getEnclosingWriter().println(value);
+ }
+
+ public void println(Object value) throws IOException {
+ getEnclosingWriter().println(value);
+ }
+
+ public void println(String value) throws IOException {
+ getEnclosingWriter().println(value);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java b/spring-test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java
new file mode 100644
index 00000000..6b69fdb7
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java
@@ -0,0 +1,96 @@
+/*
+ * 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.mock.web;
+
+import javax.servlet.jsp.JspException;
+import javax.servlet.jsp.PageContext;
+import javax.servlet.jsp.el.ELException;
+import javax.servlet.jsp.el.Expression;
+import javax.servlet.jsp.el.ExpressionEvaluator;
+import javax.servlet.jsp.el.FunctionMapper;
+import javax.servlet.jsp.el.VariableResolver;
+
+import org.apache.taglibs.standard.lang.support.ExpressionEvaluatorManager;
+
+/**
+ * Mock implementation of the JSP 2.0
+ * {@link javax.servlet.jsp.el.ExpressionEvaluator} interface, delegating to the
+ * Jakarta JSTL ExpressionEvaluatorManager.
+ * <p>
+ * Used for testing the web framework; only necessary for testing applications
+ * when testing custom JSP tags.
+ * <p>
+ * Note that the Jakarta JSTL implementation (jstl.jar, standard.jar) has to be
+ * available on the class path to use this expression evaluator.
+ *
+ * @author Juergen Hoeller
+ * @since 1.1.5
+ * @see org.apache.taglibs.standard.lang.support.ExpressionEvaluatorManager
+ */
+@SuppressWarnings("deprecation")
+public class MockExpressionEvaluator extends ExpressionEvaluator {
+
+ private final PageContext pageContext;
+
+
+ /**
+ * Create a new MockExpressionEvaluator for the given PageContext.
+ *
+ * @param pageContext the JSP PageContext to run in
+ */
+ public MockExpressionEvaluator(PageContext pageContext) {
+ this.pageContext = pageContext;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Expression parseExpression(final String expression, final Class expectedType,
+ final FunctionMapper functionMapper) throws ELException {
+
+ return new Expression() {
+
+ public Object evaluate(VariableResolver variableResolver) throws ELException {
+ return doEvaluate(expression, expectedType, functionMapper);
+ }
+ };
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Object evaluate(String expression, Class expectedType, VariableResolver variableResolver,
+ FunctionMapper functionMapper) throws ELException {
+
+ if (variableResolver != null) {
+ throw new IllegalArgumentException("Custom VariableResolver not supported");
+ }
+ return doEvaluate(expression, expectedType, functionMapper);
+ }
+
+ @SuppressWarnings("rawtypes")
+ protected Object doEvaluate(String expression, Class expectedType, FunctionMapper functionMapper)
+ throws ELException {
+
+ if (functionMapper != null) {
+ throw new IllegalArgumentException("Custom FunctionMapper not supported");
+ }
+ try {
+ return ExpressionEvaluatorManager.evaluate("JSP EL expression", expression, expectedType, this.pageContext);
+ }
+ catch (JspException ex) {
+ throw new ELException("Parsing of JSP EL expression \"" + expression + "\" failed", ex);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java b/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java
new file mode 100644
index 00000000..a80a2fb4
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java
@@ -0,0 +1,183 @@
+/*
+ * 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.mock.web;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.springframework.mock.web.MockFilterConfig;
+import org.springframework.mock.web.PassThroughFilterChain;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * <p>Mock implementation of the {@link javax.servlet.FilterChain} interface. Used
+ * for testing the web framework; also useful for testing custom
+ * {@link javax.servlet.Filter} implementations.
+ *
+ * <p>A {@link MockFilterChain} can be configured with one or more filters and a
+ * Servlet to invoke. The first time the chain is called, it invokes all filters
+ * and the Servlet, and saves the request and response. Subsequent invocations
+ * raise an {@link IllegalStateException} unless {@link #reset()} is called.
+ *
+ * @author Juergen Hoeller
+ * @author Rob Winch
+ * @author Rossen Stoyanchev
+ *
+ * @since 2.0.3
+ * @see MockFilterConfig
+ * @see PassThroughFilterChain
+ */
+public class MockFilterChain implements FilterChain {
+
+ private ServletRequest request;
+
+ private ServletResponse response;
+
+ private final List<Filter> filters;
+
+ private Iterator<Filter> iterator;
+
+
+ /**
+ * Register a single do-nothing {@link Filter} implementation. The first
+ * invocation saves the request and response. Subsequent invocations raise
+ * an {@link IllegalStateException} unless {@link #reset()} is called.
+ */
+ public MockFilterChain() {
+ this.filters = Collections.emptyList();
+ }
+
+ /**
+ * Create a FilterChain with a Servlet.
+ *
+ * @param servlet the Servlet to invoke
+ * @since 3.2
+ */
+ public MockFilterChain(Servlet servlet) {
+ this.filters = initFilterList(servlet);
+ }
+
+ /**
+ * Create a {@code FilterChain} with Filter's and a Servlet.
+ *
+ * @param servlet the {@link Servlet} to invoke in this {@link FilterChain}
+ * @param filters the {@link Filter}'s to invoke in this {@link FilterChain}
+ * @since 3.2
+ */
+ public MockFilterChain(Servlet servlet, Filter... filters) {
+ Assert.notNull(filters, "filters cannot be null");
+ Assert.noNullElements(filters, "filters cannot contain null values");
+ this.filters = initFilterList(servlet, filters);
+ }
+
+ private static List<Filter> initFilterList(Servlet servlet, Filter... filters) {
+ Filter[] allFilters = ObjectUtils.addObjectToArray(filters, new ServletFilterProxy(servlet));
+ return Arrays.asList(allFilters);
+ }
+
+ /**
+ * Return the request that {@link #doFilter} has been called with.
+ */
+ public ServletRequest getRequest() {
+ return this.request;
+ }
+
+ /**
+ * Return the response that {@link #doFilter} has been called with.
+ */
+ public ServletResponse getResponse() {
+ return this.response;
+ }
+
+ /**
+ * Invoke registered {@link Filter}s and/or {@link Servlet} also saving the
+ * request and response.
+ */
+ public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
+ Assert.notNull(request, "Request must not be null");
+ Assert.notNull(response, "Response must not be null");
+
+ if (this.request != null) {
+ throw new IllegalStateException("This FilterChain has already been called!");
+ }
+
+ if (this.iterator == null) {
+ this.iterator = this.filters.iterator();
+ }
+
+ if (this.iterator.hasNext()) {
+ Filter nextFilter = this.iterator.next();
+ nextFilter.doFilter(request, response, this);
+ }
+
+ this.request = request;
+ this.response = response;
+ }
+
+ /**
+ * Reset the {@link MockFilterChain} allowing it to be invoked again.
+ */
+ public void reset() {
+ this.request = null;
+ this.response = null;
+ this.iterator = null;
+ }
+
+
+ /**
+ * A filter that simply delegates to a Servlet.
+ */
+ private static class ServletFilterProxy implements Filter {
+
+ private final Servlet delegateServlet;
+
+ private ServletFilterProxy(Servlet servlet) {
+ Assert.notNull(servlet, "servlet cannot be null");
+ this.delegateServlet = servlet;
+ }
+
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+
+ this.delegateServlet.service(request, response);
+ }
+
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ public void destroy() {
+ }
+
+ @Override
+ public String toString() {
+ return this.delegateServlet.toString();
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java b/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java
new file mode 100644
index 00000000..34cafd55
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2009 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.mock.web;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.servlet.FilterConfig} interface.
+ *
+ * <p>Used for testing the web framework; also useful for testing
+ * custom {@link javax.servlet.Filter} implementations.
+ *
+ * @author Juergen Hoeller
+ * @since 1.0.2
+ * @see MockFilterChain
+ * @see PassThroughFilterChain
+ */
+public class MockFilterConfig implements FilterConfig {
+
+ private final ServletContext servletContext;
+
+ private final String filterName;
+
+ private final Map<String, String> initParameters = new LinkedHashMap<String, String>();
+
+
+ /**
+ * Create a new MockFilterConfig with a default {@link MockServletContext}.
+ */
+ public MockFilterConfig() {
+ this(null, "");
+ }
+
+ /**
+ * Create a new MockFilterConfig with a default {@link MockServletContext}.
+ * @param filterName the name of the filter
+ */
+ public MockFilterConfig(String filterName) {
+ this(null, filterName);
+ }
+
+ /**
+ * Create a new MockFilterConfig.
+ * @param servletContext the ServletContext that the servlet runs in
+ */
+ public MockFilterConfig(ServletContext servletContext) {
+ this(servletContext, "");
+ }
+
+ /**
+ * Create a new MockFilterConfig.
+ * @param servletContext the ServletContext that the servlet runs in
+ * @param filterName the name of the filter
+ */
+ public MockFilterConfig(ServletContext servletContext, String filterName) {
+ this.servletContext = (servletContext != null ? servletContext : new MockServletContext());
+ this.filterName = filterName;
+ }
+
+
+ public String getFilterName() {
+ return filterName;
+ }
+
+ public ServletContext getServletContext() {
+ return servletContext;
+ }
+
+ public void addInitParameter(String name, String value) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.initParameters.put(name, value);
+ }
+
+ public String getInitParameter(String name) {
+ Assert.notNull(name, "Parameter name must not be null");
+ return this.initParameters.get(name);
+ }
+
+ public Enumeration<String> getInitParameterNames() {
+ return Collections.enumeration(this.initParameters.keySet());
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java
new file mode 100644
index 00000000..178dffd6
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java
@@ -0,0 +1,917 @@
+/*
+ * 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.mock.web;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.springframework.util.Assert;
+import org.springframework.util.LinkedCaseInsensitiveMap;
+import org.springframework.util.StringUtils;
+
+/**
+ * Mock implementation of the {@link javax.servlet.http.HttpServletRequest} interface.
+ *
+ * <p>Compatible with Servlet 2.5 and partially with Servlet 3.0 (notable exceptions:
+ * the {@code getPart(s)} and {@code startAsync} families of methods).
+ *
+ * @author Juergen Hoeller
+ * @author Rod Johnson
+ * @author Rick Evans
+ * @author Mark Fisher
+ * @author Sam Brannen
+ * @since 1.0.2
+ */
+public class MockHttpServletRequest implements HttpServletRequest {
+
+ /**
+ * The default protocol: 'http'.
+ */
+ public static final String DEFAULT_PROTOCOL = "http";
+
+ /**
+ * The default server address: '127.0.0.1'.
+ */
+ public static final String DEFAULT_SERVER_ADDR = "127.0.0.1";
+
+ /**
+ * The default server name: 'localhost'.
+ */
+ public static final String DEFAULT_SERVER_NAME = "localhost";
+
+ /**
+ * The default server port: '80'.
+ */
+ public static final int DEFAULT_SERVER_PORT = 80;
+
+ /**
+ * The default remote address: '127.0.0.1'.
+ */
+ public static final String DEFAULT_REMOTE_ADDR = "127.0.0.1";
+
+ /**
+ * The default remote host: 'localhost'.
+ */
+ public static final String DEFAULT_REMOTE_HOST = "localhost";
+
+ private static final String CONTENT_TYPE_HEADER = "Content-Type";
+
+ private static final String CHARSET_PREFIX = "charset=";
+
+
+ private boolean active = true;
+
+
+ // ---------------------------------------------------------------------
+ // ServletRequest properties
+ // ---------------------------------------------------------------------
+
+ private final Map<String, Object> attributes = new LinkedHashMap<String, Object>();
+
+ private String characterEncoding;
+
+ private byte[] content;
+
+ private String contentType;
+
+ private final Map<String, String[]> parameters = new LinkedHashMap<String, String[]>(16);
+
+ private String protocol = DEFAULT_PROTOCOL;
+
+ private String scheme = DEFAULT_PROTOCOL;
+
+ private String serverName = DEFAULT_SERVER_NAME;
+
+ private int serverPort = DEFAULT_SERVER_PORT;
+
+ private String remoteAddr = DEFAULT_REMOTE_ADDR;
+
+ private String remoteHost = DEFAULT_REMOTE_HOST;
+
+ /** List of locales in descending order */
+ private final List<Locale> locales = new LinkedList<Locale>();
+
+ private boolean secure = false;
+
+ private final ServletContext servletContext;
+
+ private int remotePort = DEFAULT_SERVER_PORT;
+
+ private String localName = DEFAULT_SERVER_NAME;
+
+ private String localAddr = DEFAULT_SERVER_ADDR;
+
+ private int localPort = DEFAULT_SERVER_PORT;
+
+
+ // ---------------------------------------------------------------------
+ // HttpServletRequest properties
+ // ---------------------------------------------------------------------
+
+ private String authType;
+
+ private Cookie[] cookies;
+
+ private final Map<String, HeaderValueHolder> headers = new LinkedCaseInsensitiveMap<HeaderValueHolder>();
+
+ private String method;
+
+ private String pathInfo;
+
+ private String contextPath = "";
+
+ private String queryString;
+
+ private String remoteUser;
+
+ private final Set<String> userRoles = new HashSet<String>();
+
+ private Principal userPrincipal;
+
+ private String requestedSessionId;
+
+ private String requestURI;
+
+ private String servletPath = "";
+
+ private HttpSession session;
+
+ private boolean requestedSessionIdValid = true;
+
+ private boolean requestedSessionIdFromCookie = true;
+
+ private boolean requestedSessionIdFromURL = false;
+
+
+ // ---------------------------------------------------------------------
+ // Constructors
+ // ---------------------------------------------------------------------
+
+ /**
+ * Create a new {@code MockHttpServletRequest} with a default
+ * {@link MockServletContext}.
+ * @see #MockHttpServletRequest(ServletContext, String, String)
+ */
+ public MockHttpServletRequest() {
+ this(null, "", "");
+ }
+
+ /**
+ * Create a new {@code MockHttpServletRequest} with a default
+ * {@link MockServletContext}.
+ * @param method the request method (may be {@code null})
+ * @param requestURI the request URI (may be {@code null})
+ * @see #setMethod
+ * @see #setRequestURI
+ * @see #MockHttpServletRequest(ServletContext, String, String)
+ */
+ public MockHttpServletRequest(String method, String requestURI) {
+ this(null, method, requestURI);
+ }
+
+ /**
+ * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext}.
+ * @param servletContext the ServletContext that the request runs in
+ * (may be {@code null} to use a default {@link MockServletContext})
+ * @see #MockHttpServletRequest(ServletContext, String, String)
+ */
+ public MockHttpServletRequest(ServletContext servletContext) {
+ this(servletContext, "", "");
+ }
+
+ /**
+ * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext},
+ * {@code method}, and {@code requestURI}.
+ * <p>The preferred locale will be set to {@link Locale#ENGLISH}.
+ * @param servletContext the ServletContext that the request runs in (may be
+ * {@code null} to use a default {@link MockServletContext})
+ * @param method the request method (may be {@code null})
+ * @param requestURI the request URI (may be {@code null})
+ * @see #setMethod
+ * @see #setRequestURI
+ * @see #setPreferredLocales
+ * @see MockServletContext
+ */
+ public MockHttpServletRequest(ServletContext servletContext, String method, String requestURI) {
+ this.servletContext = (servletContext != null ? servletContext : new MockServletContext());
+ this.method = method;
+ this.requestURI = requestURI;
+ this.locales.add(Locale.ENGLISH);
+ }
+
+
+ // ---------------------------------------------------------------------
+ // Lifecycle methods
+ // ---------------------------------------------------------------------
+
+ /**
+ * Return the ServletContext that this request is associated with. (Not
+ * available in the standard HttpServletRequest interface for some reason.)
+ */
+ public ServletContext getServletContext() {
+ return this.servletContext;
+ }
+
+ /**
+ * Return whether this request is still active (that is, not completed yet).
+ */
+ public boolean isActive() {
+ return this.active;
+ }
+
+ /**
+ * Mark this request as completed, keeping its state.
+ */
+ public void close() {
+ this.active = false;
+ }
+
+ /**
+ * Invalidate this request, clearing its state.
+ */
+ public void invalidate() {
+ close();
+ clearAttributes();
+ }
+
+ /**
+ * Check whether this request is still active (that is, not completed yet),
+ * throwing an IllegalStateException if not active anymore.
+ */
+ protected void checkActive() throws IllegalStateException {
+ if (!this.active) {
+ throw new IllegalStateException("Request is not active anymore");
+ }
+ }
+
+
+ // ---------------------------------------------------------------------
+ // ServletRequest interface
+ // ---------------------------------------------------------------------
+
+ public Object getAttribute(String name) {
+ checkActive();
+ return this.attributes.get(name);
+ }
+
+ public Enumeration<String> getAttributeNames() {
+ checkActive();
+ return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet()));
+ }
+
+ public String getCharacterEncoding() {
+ return this.characterEncoding;
+ }
+
+ public void setCharacterEncoding(String characterEncoding) {
+ this.characterEncoding = characterEncoding;
+ updateContentTypeHeader();
+ }
+
+ private void updateContentTypeHeader() {
+ if (StringUtils.hasLength(this.contentType)) {
+ StringBuilder sb = new StringBuilder(this.contentType);
+ if (!this.contentType.toLowerCase().contains(CHARSET_PREFIX) &&
+ StringUtils.hasLength(this.characterEncoding)) {
+ sb.append(";").append(CHARSET_PREFIX).append(this.characterEncoding);
+ }
+ doAddHeaderValue(CONTENT_TYPE_HEADER, sb.toString(), true);
+ }
+ }
+
+ public void setContent(byte[] content) {
+ this.content = content;
+ }
+
+ public int getContentLength() {
+ return (this.content != null ? this.content.length : -1);
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ if (contentType != null) {
+ int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX);
+ if (charsetIndex != -1) {
+ this.characterEncoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length());
+ }
+ updateContentTypeHeader();
+ }
+ }
+
+ public String getContentType() {
+ return this.contentType;
+ }
+
+ public ServletInputStream getInputStream() {
+ if (this.content != null) {
+ return new DelegatingServletInputStream(new ByteArrayInputStream(this.content));
+ }
+ else {
+ return null;
+ }
+ }
+
+ /**
+ * Set a single value for the specified HTTP parameter.
+ * <p>If there are already one or more values registered for the given
+ * parameter name, they will be replaced.
+ */
+ public void setParameter(String name, String value) {
+ setParameter(name, new String[] {value});
+ }
+
+ /**
+ * Set an array of values for the specified HTTP parameter.
+ * <p>If there are already one or more values registered for the given
+ * parameter name, they will be replaced.
+ */
+ public void setParameter(String name, String[] values) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.parameters.put(name, values);
+ }
+
+ /**
+ * Sets all provided parameters <strong>replacing</strong> any existing
+ * values for the provided parameter names. To add without replacing
+ * existing values, use {@link #addParameters(java.util.Map)}.
+ */
+ @SuppressWarnings("rawtypes")
+ public void setParameters(Map params) {
+ Assert.notNull(params, "Parameter map must not be null");
+ for (Object key : params.keySet()) {
+ Assert.isInstanceOf(String.class, key,
+ "Parameter map key must be of type [" + String.class.getName() + "]");
+ Object value = params.get(key);
+ if (value instanceof String) {
+ this.setParameter((String) key, (String) value);
+ }
+ else if (value instanceof String[]) {
+ this.setParameter((String) key, (String[]) value);
+ }
+ else {
+ throw new IllegalArgumentException(
+ "Parameter map value must be single value " + " or array of type [" + String.class.getName() + "]");
+ }
+ }
+ }
+
+ /**
+ * Add a single value for the specified HTTP parameter.
+ * <p>If there are already one or more values registered for the given
+ * parameter name, the given value will be added to the end of the list.
+ */
+ public void addParameter(String name, String value) {
+ addParameter(name, new String[] {value});
+ }
+
+ /**
+ * Add an array of values for the specified HTTP parameter.
+ * <p>If there are already one or more values registered for the given
+ * parameter name, the given values will be added to the end of the list.
+ */
+ public void addParameter(String name, String[] values) {
+ Assert.notNull(name, "Parameter name must not be null");
+ String[] oldArr = this.parameters.get(name);
+ if (oldArr != null) {
+ String[] newArr = new String[oldArr.length + values.length];
+ System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
+ System.arraycopy(values, 0, newArr, oldArr.length, values.length);
+ this.parameters.put(name, newArr);
+ }
+ else {
+ this.parameters.put(name, values);
+ }
+ }
+
+ /**
+ * Adds all provided parameters <strong>without</strong> replacing any
+ * existing values. To replace existing values, use
+ * {@link #setParameters(java.util.Map)}.
+ */
+ @SuppressWarnings("rawtypes")
+ public void addParameters(Map params) {
+ Assert.notNull(params, "Parameter map must not be null");
+ for (Object key : params.keySet()) {
+ Assert.isInstanceOf(String.class, key,
+ "Parameter map key must be of type [" + String.class.getName() + "]");
+ Object value = params.get(key);
+ if (value instanceof String) {
+ this.addParameter((String) key, (String) value);
+ }
+ else if (value instanceof String[]) {
+ this.addParameter((String) key, (String[]) value);
+ }
+ else {
+ throw new IllegalArgumentException("Parameter map value must be single value " +
+ " or array of type [" + String.class.getName() + "]");
+ }
+ }
+ }
+
+ /**
+ * Remove already registered values for the specified HTTP parameter, if any.
+ */
+ public void removeParameter(String name) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.parameters.remove(name);
+ }
+
+ /**
+ * Removes all existing parameters.
+ */
+ public void removeAllParameters() {
+ this.parameters.clear();
+ }
+
+ public String getParameter(String name) {
+ String[] arr = (name != null ? this.parameters.get(name) : null);
+ return (arr != null && arr.length > 0 ? arr[0] : null);
+ }
+
+ public Enumeration<String> getParameterNames() {
+ return Collections.enumeration(this.parameters.keySet());
+ }
+
+ public String[] getParameterValues(String name) {
+ return (name != null ? this.parameters.get(name) : null);
+ }
+
+ public Map<String, String[]> getParameterMap() {
+ return Collections.unmodifiableMap(this.parameters);
+ }
+
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ public String getProtocol() {
+ return this.protocol;
+ }
+
+ public void setScheme(String scheme) {
+ this.scheme = scheme;
+ }
+
+ public String getScheme() {
+ return this.scheme;
+ }
+
+ public void setServerName(String serverName) {
+ this.serverName = serverName;
+ }
+
+ public String getServerName() {
+ return this.serverName;
+ }
+
+ public void setServerPort(int serverPort) {
+ this.serverPort = serverPort;
+ }
+
+ public int getServerPort() {
+ return this.serverPort;
+ }
+
+ public BufferedReader getReader() throws UnsupportedEncodingException {
+ if (this.content != null) {
+ InputStream sourceStream = new ByteArrayInputStream(this.content);
+ Reader sourceReader = (this.characterEncoding != null) ?
+ new InputStreamReader(sourceStream, this.characterEncoding) : new InputStreamReader(sourceStream);
+ return new BufferedReader(sourceReader);
+ }
+ else {
+ return null;
+ }
+ }
+
+ public void setRemoteAddr(String remoteAddr) {
+ this.remoteAddr = remoteAddr;
+ }
+
+ public String getRemoteAddr() {
+ return this.remoteAddr;
+ }
+
+ public void setRemoteHost(String remoteHost) {
+ this.remoteHost = remoteHost;
+ }
+
+ public String getRemoteHost() {
+ return this.remoteHost;
+ }
+
+ public void setAttribute(String name, Object value) {
+ checkActive();
+ Assert.notNull(name, "Attribute name must not be null");
+ if (value != null) {
+ this.attributes.put(name, value);
+ }
+ else {
+ this.attributes.remove(name);
+ }
+ }
+
+ public void removeAttribute(String name) {
+ checkActive();
+ Assert.notNull(name, "Attribute name must not be null");
+ this.attributes.remove(name);
+ }
+
+ /**
+ * Clear all of this request's attributes.
+ */
+ public void clearAttributes() {
+ this.attributes.clear();
+ }
+
+ /**
+ * Add a new preferred locale, before any existing locales.
+ * @see #setPreferredLocales
+ */
+ public void addPreferredLocale(Locale locale) {
+ Assert.notNull(locale, "Locale must not be null");
+ this.locales.add(0, locale);
+ }
+
+ /**
+ * Set the list of preferred locales, in descending order, effectively replacing
+ * any existing locales.
+ * @see #addPreferredLocale
+ * @since 3.2
+ */
+ public void setPreferredLocales(List<Locale> locales) {
+ Assert.notEmpty(locales, "Locale list must not be empty");
+ this.locales.clear();
+ this.locales.addAll(locales);
+ }
+
+ public Locale getLocale() {
+ return this.locales.get(0);
+ }
+
+ public Enumeration<Locale> getLocales() {
+ return Collections.enumeration(this.locales);
+ }
+
+ public void setSecure(boolean secure) {
+ this.secure = secure;
+ }
+
+ public boolean isSecure() {
+ return this.secure;
+ }
+
+ public RequestDispatcher getRequestDispatcher(String path) {
+ return new MockRequestDispatcher(path);
+ }
+
+ public String getRealPath(String path) {
+ return this.servletContext.getRealPath(path);
+ }
+
+ public void setRemotePort(int remotePort) {
+ this.remotePort = remotePort;
+ }
+
+ public int getRemotePort() {
+ return this.remotePort;
+ }
+
+ public void setLocalName(String localName) {
+ this.localName = localName;
+ }
+
+ public String getLocalName() {
+ return this.localName;
+ }
+
+ public void setLocalAddr(String localAddr) {
+ this.localAddr = localAddr;
+ }
+
+ public String getLocalAddr() {
+ return this.localAddr;
+ }
+
+ public void setLocalPort(int localPort) {
+ this.localPort = localPort;
+ }
+
+ public int getLocalPort() {
+ return this.localPort;
+ }
+
+
+ // ---------------------------------------------------------------------
+ // HttpServletRequest interface
+ // ---------------------------------------------------------------------
+
+ public void setAuthType(String authType) {
+ this.authType = authType;
+ }
+
+ public String getAuthType() {
+ return this.authType;
+ }
+
+ public void setCookies(Cookie... cookies) {
+ this.cookies = cookies;
+ }
+
+ public Cookie[] getCookies() {
+ return this.cookies;
+ }
+
+ /**
+ * Add a header entry for the given name.
+ * <p>If there was no entry for that header name before, the value will be used
+ * as-is. In case of an existing entry, a String array will be created,
+ * adding the given value (more specifically, its toString representation)
+ * as further element.
+ * <p>Multiple values can only be stored as list of Strings, following the
+ * Servlet spec (see {@code getHeaders} accessor). As alternative to
+ * repeated {@code addHeader} calls for individual elements, you can
+ * use a single call with an entire array or Collection of values as
+ * parameter.
+ * @see #getHeaderNames
+ * @see #getHeader
+ * @see #getHeaders
+ * @see #getDateHeader
+ * @see #getIntHeader
+ */
+ public void addHeader(String name, Object value) {
+ if (CONTENT_TYPE_HEADER.equalsIgnoreCase(name)) {
+ setContentType((String) value);
+ return;
+ }
+ doAddHeaderValue(name, value, false);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void doAddHeaderValue(String name, Object value, boolean replace) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ Assert.notNull(value, "Header value must not be null");
+ if (header == null || replace) {
+ header = new HeaderValueHolder();
+ this.headers.put(name, header);
+ }
+ if (value instanceof Collection) {
+ header.addValues((Collection) value);
+ }
+ else if (value.getClass().isArray()) {
+ header.addValueArray(value);
+ }
+ else {
+ header.addValue(value);
+ }
+ }
+
+ public long getDateHeader(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ Object value = (header != null ? header.getValue() : null);
+ if (value instanceof Date) {
+ return ((Date) value).getTime();
+ }
+ else if (value instanceof Number) {
+ return ((Number) value).longValue();
+ }
+ else if (value != null) {
+ throw new IllegalArgumentException(
+ "Value for header '" + name + "' is neither a Date nor a Number: " + value);
+ }
+ else {
+ return -1L;
+ }
+ }
+
+ public String getHeader(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ return (header != null ? header.getStringValue() : null);
+ }
+
+ public Enumeration<String> getHeaders(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ return Collections.enumeration(header != null ? header.getStringValues() : new LinkedList<String>());
+ }
+
+ public Enumeration<String> getHeaderNames() {
+ return Collections.enumeration(this.headers.keySet());
+ }
+
+ public int getIntHeader(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ Object value = (header != null ? header.getValue() : null);
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ else if (value instanceof String) {
+ return Integer.parseInt((String) value);
+ }
+ else if (value != null) {
+ throw new NumberFormatException("Value for header '" + name + "' is not a Number: " + value);
+ }
+ else {
+ return -1;
+ }
+ }
+
+ public void setMethod(String method) {
+ this.method = method;
+ }
+
+ public String getMethod() {
+ return this.method;
+ }
+
+ public void setPathInfo(String pathInfo) {
+ this.pathInfo = pathInfo;
+ }
+
+ public String getPathInfo() {
+ return this.pathInfo;
+ }
+
+ public String getPathTranslated() {
+ return (this.pathInfo != null ? getRealPath(this.pathInfo) : null);
+ }
+
+ public void setContextPath(String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ public String getContextPath() {
+ return this.contextPath;
+ }
+
+ public void setQueryString(String queryString) {
+ this.queryString = queryString;
+ }
+
+ public String getQueryString() {
+ return this.queryString;
+ }
+
+ public void setRemoteUser(String remoteUser) {
+ this.remoteUser = remoteUser;
+ }
+
+ public String getRemoteUser() {
+ return this.remoteUser;
+ }
+
+ public void addUserRole(String role) {
+ this.userRoles.add(role);
+ }
+
+ public boolean isUserInRole(String role) {
+ return (this.userRoles.contains(role) || (this.servletContext instanceof MockServletContext &&
+ ((MockServletContext) this.servletContext).getDeclaredRoles().contains(role)));
+ }
+
+ public void setUserPrincipal(Principal userPrincipal) {
+ this.userPrincipal = userPrincipal;
+ }
+
+ public Principal getUserPrincipal() {
+ return this.userPrincipal;
+ }
+
+ public void setRequestedSessionId(String requestedSessionId) {
+ this.requestedSessionId = requestedSessionId;
+ }
+
+ public String getRequestedSessionId() {
+ return this.requestedSessionId;
+ }
+
+ public void setRequestURI(String requestURI) {
+ this.requestURI = requestURI;
+ }
+
+ public String getRequestURI() {
+ return this.requestURI;
+ }
+
+ public StringBuffer getRequestURL() {
+ StringBuffer url = new StringBuffer(this.scheme);
+ url.append("://").append(this.serverName).append(':').append(this.serverPort);
+ url.append(getRequestURI());
+ return url;
+ }
+
+ public void setServletPath(String servletPath) {
+ this.servletPath = servletPath;
+ }
+
+ public String getServletPath() {
+ return this.servletPath;
+ }
+
+ public void setSession(HttpSession session) {
+ this.session = session;
+ if (session instanceof MockHttpSession) {
+ MockHttpSession mockSession = ((MockHttpSession) session);
+ mockSession.access();
+ }
+ }
+
+ public HttpSession getSession(boolean create) {
+ checkActive();
+ // Reset session if invalidated.
+ if (this.session instanceof MockHttpSession && ((MockHttpSession) this.session).isInvalid()) {
+ this.session = null;
+ }
+ // Create new session if necessary.
+ if (this.session == null && create) {
+ this.session = new MockHttpSession(this.servletContext);
+ }
+ return this.session;
+ }
+
+ public HttpSession getSession() {
+ return getSession(true);
+ }
+
+ public void setRequestedSessionIdValid(boolean requestedSessionIdValid) {
+ this.requestedSessionIdValid = requestedSessionIdValid;
+ }
+
+ public boolean isRequestedSessionIdValid() {
+ return this.requestedSessionIdValid;
+ }
+
+ public void setRequestedSessionIdFromCookie(boolean requestedSessionIdFromCookie) {
+ this.requestedSessionIdFromCookie = requestedSessionIdFromCookie;
+ }
+
+ public boolean isRequestedSessionIdFromCookie() {
+ return this.requestedSessionIdFromCookie;
+ }
+
+ public void setRequestedSessionIdFromURL(boolean requestedSessionIdFromURL) {
+ this.requestedSessionIdFromURL = requestedSessionIdFromURL;
+ }
+
+ public boolean isRequestedSessionIdFromURL() {
+ return this.requestedSessionIdFromURL;
+ }
+
+ public boolean isRequestedSessionIdFromUrl() {
+ return isRequestedSessionIdFromURL();
+ }
+
+ public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
+ return (this.userPrincipal != null && this.remoteUser != null && this.authType != null);
+ }
+
+ public void login(String username, String password) throws ServletException {
+ throw new ServletException("Username-password authentication not supported - override the login method");
+ }
+
+ public void logout() throws ServletException {
+ this.userPrincipal = null;
+ this.remoteUser = null;
+ this.authType = null;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java
new file mode 100644
index 00000000..b575c402
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java
@@ -0,0 +1,603 @@
+/*
+ * 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.mock.web;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.util.Assert;
+import org.springframework.util.LinkedCaseInsensitiveMap;
+import org.springframework.web.util.WebUtils;
+
+/**
+ * Mock implementation of the {@link javax.servlet.http.HttpServletResponse} interface.
+ *
+ * <p>Compatible with Servlet 2.5 as well as Servlet 3.0.
+ *
+ * @author Juergen Hoeller
+ * @author Rod Johnson
+ * @since 1.0.2
+ */
+public class MockHttpServletResponse implements HttpServletResponse {
+
+ private static final String CHARSET_PREFIX = "charset=";
+
+ private static final String CONTENT_TYPE_HEADER = "Content-Type";
+
+ private static final String CONTENT_LENGTH_HEADER = "Content-Length";
+
+ private static final String LOCATION_HEADER = "Location";
+
+
+ //---------------------------------------------------------------------
+ // ServletResponse properties
+ //---------------------------------------------------------------------
+
+ private boolean outputStreamAccessAllowed = true;
+
+ private boolean writerAccessAllowed = true;
+
+ private String characterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
+
+ private boolean charset = false;
+
+ private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024);
+
+ private final ServletOutputStream outputStream = new ResponseServletOutputStream(this.content);
+
+ private PrintWriter writer;
+
+ private int contentLength = 0;
+
+ private String contentType;
+
+ private int bufferSize = 4096;
+
+ private boolean committed;
+
+ private Locale locale = Locale.getDefault();
+
+
+ //---------------------------------------------------------------------
+ // HttpServletResponse properties
+ //---------------------------------------------------------------------
+
+ private final List<Cookie> cookies = new ArrayList<Cookie>();
+
+ private final Map<String, HeaderValueHolder> headers = new LinkedCaseInsensitiveMap<HeaderValueHolder>();
+
+ private int status = HttpServletResponse.SC_OK;
+
+ private String errorMessage;
+
+ private String forwardedUrl;
+
+ private final List<String> includedUrls = new ArrayList<String>();
+
+
+ //---------------------------------------------------------------------
+ // ServletResponse interface
+ //---------------------------------------------------------------------
+
+ /**
+ * Set whether {@link #getOutputStream()} access is allowed.
+ * <p>Default is {@code true}.
+ */
+ public void setOutputStreamAccessAllowed(boolean outputStreamAccessAllowed) {
+ this.outputStreamAccessAllowed = outputStreamAccessAllowed;
+ }
+
+ /**
+ * Return whether {@link #getOutputStream()} access is allowed.
+ */
+ public boolean isOutputStreamAccessAllowed() {
+ return this.outputStreamAccessAllowed;
+ }
+
+ /**
+ * Set whether {@link #getWriter()} access is allowed.
+ * <p>Default is {@code true}.
+ */
+ public void setWriterAccessAllowed(boolean writerAccessAllowed) {
+ this.writerAccessAllowed = writerAccessAllowed;
+ }
+
+ /**
+ * Return whether {@link #getOutputStream()} access is allowed.
+ */
+ public boolean isWriterAccessAllowed() {
+ return this.writerAccessAllowed;
+ }
+
+ public void setCharacterEncoding(String characterEncoding) {
+ this.characterEncoding = characterEncoding;
+ this.charset = true;
+ updateContentTypeHeader();
+ }
+
+ private void updateContentTypeHeader() {
+ if (this.contentType != null) {
+ StringBuilder sb = new StringBuilder(this.contentType);
+ if (!this.contentType.toLowerCase().contains(CHARSET_PREFIX) && this.charset) {
+ sb.append(";").append(CHARSET_PREFIX).append(this.characterEncoding);
+ }
+ doAddHeaderValue(CONTENT_TYPE_HEADER, sb.toString(), true);
+ }
+ }
+
+ public String getCharacterEncoding() {
+ return this.characterEncoding;
+ }
+
+ public ServletOutputStream getOutputStream() {
+ if (!this.outputStreamAccessAllowed) {
+ throw new IllegalStateException("OutputStream access not allowed");
+ }
+ return this.outputStream;
+ }
+
+ public PrintWriter getWriter() throws UnsupportedEncodingException {
+ if (!this.writerAccessAllowed) {
+ throw new IllegalStateException("Writer access not allowed");
+ }
+ if (this.writer == null) {
+ Writer targetWriter = (this.characterEncoding != null ?
+ new OutputStreamWriter(this.content, this.characterEncoding) : new OutputStreamWriter(this.content));
+ this.writer = new ResponsePrintWriter(targetWriter);
+ }
+ return this.writer;
+ }
+
+ public byte[] getContentAsByteArray() {
+ flushBuffer();
+ return this.content.toByteArray();
+ }
+
+ public String getContentAsString() throws UnsupportedEncodingException {
+ flushBuffer();
+ return (this.characterEncoding != null ?
+ this.content.toString(this.characterEncoding) : this.content.toString());
+ }
+
+ public void setContentLength(int contentLength) {
+ this.contentLength = contentLength;
+ doAddHeaderValue(CONTENT_LENGTH_HEADER, contentLength, true);
+ }
+
+ public int getContentLength() {
+ return this.contentLength;
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ if (contentType != null) {
+ int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX);
+ if (charsetIndex != -1) {
+ this.characterEncoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length());
+ this.charset = true;
+ }
+ updateContentTypeHeader();
+ }
+ }
+
+ public String getContentType() {
+ return this.contentType;
+ }
+
+ public void setBufferSize(int bufferSize) {
+ this.bufferSize = bufferSize;
+ }
+
+ public int getBufferSize() {
+ return this.bufferSize;
+ }
+
+ public void flushBuffer() {
+ setCommitted(true);
+ }
+
+ public void resetBuffer() {
+ if (isCommitted()) {
+ throw new IllegalStateException("Cannot reset buffer - response is already committed");
+ }
+ this.content.reset();
+ }
+
+ private void setCommittedIfBufferSizeExceeded() {
+ int bufSize = getBufferSize();
+ if (bufSize > 0 && this.content.size() > bufSize) {
+ setCommitted(true);
+ }
+ }
+
+ public void setCommitted(boolean committed) {
+ this.committed = committed;
+ }
+
+ public boolean isCommitted() {
+ return this.committed;
+ }
+
+ public void reset() {
+ resetBuffer();
+ this.characterEncoding = null;
+ this.contentLength = 0;
+ this.contentType = null;
+ this.locale = null;
+ this.cookies.clear();
+ this.headers.clear();
+ this.status = HttpServletResponse.SC_OK;
+ this.errorMessage = null;
+ }
+
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
+ public Locale getLocale() {
+ return this.locale;
+ }
+
+
+ //---------------------------------------------------------------------
+ // HttpServletResponse interface
+ //---------------------------------------------------------------------
+
+ public void addCookie(Cookie cookie) {
+ Assert.notNull(cookie, "Cookie must not be null");
+ this.cookies.add(cookie);
+ }
+
+ public Cookie[] getCookies() {
+ return this.cookies.toArray(new Cookie[this.cookies.size()]);
+ }
+
+ public Cookie getCookie(String name) {
+ Assert.notNull(name, "Cookie name must not be null");
+ for (Cookie cookie : this.cookies) {
+ if (name.equals(cookie.getName())) {
+ return cookie;
+ }
+ }
+ return null;
+ }
+
+ public boolean containsHeader(String name) {
+ return (HeaderValueHolder.getByName(this.headers, name) != null);
+ }
+
+ /**
+ * Return the names of all specified headers as a Set of Strings.
+ * <p>As of Servlet 3.0, this method is also defined HttpServletResponse.
+ * @return the {@code Set} of header name {@code Strings}, or an empty {@code Set} if none
+ */
+ public Collection<String> getHeaderNames() {
+ return this.headers.keySet();
+ }
+
+ /**
+ * Return the primary value for the given header as a String, if any.
+ * Will return the first value in case of multiple values.
+ * <p>As of Servlet 3.0, this method is also defined in HttpServletResponse.
+ * As of Spring 3.1, it returns a stringified value for Servlet 3.0 compatibility.
+ * Consider using {@link #getHeaderValue(String)} for raw Object access.
+ * @param name the name of the header
+ * @return the associated header value, or {@code null} if none
+ */
+ public String getHeader(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ return (header != null ? header.getStringValue() : null);
+ }
+
+ /**
+ * Return all values for the given header as a List of Strings.
+ * <p>As of Servlet 3.0, this method is also defined in HttpServletResponse.
+ * As of Spring 3.1, it returns a List of stringified values for Servlet 3.0 compatibility.
+ * Consider using {@link #getHeaderValues(String)} for raw Object access.
+ * @param name the name of the header
+ * @return the associated header values, or an empty List if none
+ */
+ public List<String> getHeaders(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ if (header != null) {
+ return header.getStringValues();
+ }
+ else {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Return the primary value for the given header, if any.
+ * <p>Will return the first value in case of multiple values.
+ * @param name the name of the header
+ * @return the associated header value, or {@code null} if none
+ */
+ public Object getHeaderValue(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ return (header != null ? header.getValue() : null);
+ }
+
+ /**
+ * Return all values for the given header as a List of value objects.
+ * @param name the name of the header
+ * @return the associated header values, or an empty List if none
+ */
+ public List<Object> getHeaderValues(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ if (header != null) {
+ return header.getValues();
+ }
+ else {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * The default implementation returns the given URL String as-is.
+ * <p>Can be overridden in subclasses, appending a session id or the like.
+ */
+ public String encodeURL(String url) {
+ return url;
+ }
+
+ /**
+ * The default implementation delegates to {@link #encodeURL},
+ * returning the given URL String as-is.
+ * <p>Can be overridden in subclasses, appending a session id or the like
+ * in a redirect-specific fashion. For general URL encoding rules,
+ * override the common {@link #encodeURL} method instead, applying
+ * to redirect URLs as well as to general URLs.
+ */
+ public String encodeRedirectURL(String url) {
+ return encodeURL(url);
+ }
+
+ public String encodeUrl(String url) {
+ return encodeURL(url);
+ }
+
+ public String encodeRedirectUrl(String url) {
+ return encodeRedirectURL(url);
+ }
+
+ public void sendError(int status, String errorMessage) throws IOException {
+ if (isCommitted()) {
+ throw new IllegalStateException("Cannot set error status - response is already committed");
+ }
+ this.status = status;
+ this.errorMessage = errorMessage;
+ setCommitted(true);
+ }
+
+ public void sendError(int status) throws IOException {
+ if (isCommitted()) {
+ throw new IllegalStateException("Cannot set error status - response is already committed");
+ }
+ this.status = status;
+ setCommitted(true);
+ }
+
+ public void sendRedirect(String url) throws IOException {
+ if (isCommitted()) {
+ throw new IllegalStateException("Cannot send redirect - response is already committed");
+ }
+ Assert.notNull(url, "Redirect URL must not be null");
+ setHeader(LOCATION_HEADER, url);
+ setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
+ setCommitted(true);
+ }
+
+ public String getRedirectedUrl() {
+ return getHeader(LOCATION_HEADER);
+ }
+
+ public void setDateHeader(String name, long value) {
+ setHeaderValue(name, value);
+ }
+
+ public void addDateHeader(String name, long value) {
+ addHeaderValue(name, value);
+ }
+
+ public void setHeader(String name, String value) {
+ setHeaderValue(name, value);
+ }
+
+ public void addHeader(String name, String value) {
+ addHeaderValue(name, value);
+ }
+
+ public void setIntHeader(String name, int value) {
+ setHeaderValue(name, value);
+ }
+
+ public void addIntHeader(String name, int value) {
+ addHeaderValue(name, value);
+ }
+
+ private void setHeaderValue(String name, Object value) {
+ if (setSpecialHeader(name, value)) {
+ return;
+ }
+ doAddHeaderValue(name, value, true);
+ }
+
+ private void addHeaderValue(String name, Object value) {
+ if (setSpecialHeader(name, value)) {
+ return;
+ }
+ doAddHeaderValue(name, value, false);
+ }
+
+ private boolean setSpecialHeader(String name, Object value) {
+ if (CONTENT_TYPE_HEADER.equalsIgnoreCase(name)) {
+ setContentType((String) value);
+ return true;
+ }
+ else if (CONTENT_LENGTH_HEADER.equalsIgnoreCase(name)) {
+ setContentLength(Integer.parseInt((String) value));
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ private void doAddHeaderValue(String name, Object value, boolean replace) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ Assert.notNull(value, "Header value must not be null");
+ if (header == null) {
+ header = new HeaderValueHolder();
+ this.headers.put(name, header);
+ }
+ if (replace) {
+ header.setValue(value);
+ }
+ else {
+ header.addValue(value);
+ }
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public void setStatus(int status, String errorMessage) {
+ this.status = status;
+ this.errorMessage = errorMessage;
+ }
+
+ public int getStatus() {
+ return this.status;
+ }
+
+ public String getErrorMessage() {
+ return this.errorMessage;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Methods for MockRequestDispatcher
+ //---------------------------------------------------------------------
+
+ public void setForwardedUrl(String forwardedUrl) {
+ this.forwardedUrl = forwardedUrl;
+ }
+
+ public String getForwardedUrl() {
+ return this.forwardedUrl;
+ }
+
+ public void setIncludedUrl(String includedUrl) {
+ this.includedUrls.clear();
+ if (includedUrl != null) {
+ this.includedUrls.add(includedUrl);
+ }
+ }
+
+ public String getIncludedUrl() {
+ int count = this.includedUrls.size();
+ if (count > 1) {
+ throw new IllegalStateException(
+ "More than 1 URL included - check getIncludedUrls instead: " + this.includedUrls);
+ }
+ return (count == 1 ? this.includedUrls.get(0) : null);
+ }
+
+ public void addIncludedUrl(String includedUrl) {
+ Assert.notNull(includedUrl, "Included URL must not be null");
+ this.includedUrls.add(includedUrl);
+ }
+
+ public List<String> getIncludedUrls() {
+ return this.includedUrls;
+ }
+
+
+ /**
+ * Inner class that adapts the ServletOutputStream to mark the
+ * response as committed once the buffer size is exceeded.
+ */
+ private class ResponseServletOutputStream extends DelegatingServletOutputStream {
+
+ public ResponseServletOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ public void write(int b) throws IOException {
+ super.write(b);
+ super.flush();
+ setCommittedIfBufferSizeExceeded();
+ }
+
+ public void flush() throws IOException {
+ super.flush();
+ setCommitted(true);
+ }
+ }
+
+
+ /**
+ * Inner class that adapts the PrintWriter to mark the
+ * response as committed once the buffer size is exceeded.
+ */
+ private class ResponsePrintWriter extends PrintWriter {
+
+ public ResponsePrintWriter(Writer out) {
+ super(out, true);
+ }
+
+ public void write(char buf[], int off, int len) {
+ super.write(buf, off, len);
+ super.flush();
+ setCommittedIfBufferSizeExceeded();
+ }
+
+ public void write(String s, int off, int len) {
+ super.write(s, off, len);
+ super.flush();
+ setCommittedIfBufferSizeExceeded();
+ }
+
+ public void write(int c) {
+ super.write(c);
+ super.flush();
+ setCommittedIfBufferSizeExceeded();
+ }
+
+ public void flush() {
+ super.flush();
+ setCommitted(true);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java
new file mode 100644
index 00000000..548c2016
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2002-2013 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.mock.web;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+import javax.servlet.http.HttpSessionContext;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.servlet.http.HttpSession} interface.
+ *
+ * <p>Compatible with Servlet 2.5 as well as Servlet 3.0.
+ *
+ * <p>Used for testing the web framework; also useful for testing application
+ * controllers.
+ *
+ * @author Juergen Hoeller
+ * @author Rod Johnson
+ * @author Mark Fisher
+ * @author Sam Brannen
+ * @since 1.0.2
+ */
+@SuppressWarnings("deprecation")
+public class MockHttpSession implements HttpSession {
+
+ public static final String SESSION_COOKIE_NAME = "JSESSION";
+
+
+ private static int nextId = 1;
+
+ private final String id;
+
+ private final long creationTime = System.currentTimeMillis();
+
+ private int maxInactiveInterval;
+
+ private long lastAccessedTime = System.currentTimeMillis();
+
+ private final ServletContext servletContext;
+
+ private final Map<String, Object> attributes = new LinkedHashMap<String, Object>();
+
+ private boolean invalid = false;
+
+ private boolean isNew = true;
+
+
+ /**
+ * Create a new MockHttpSession with a default {@link MockServletContext}.
+ *
+ * @see MockServletContext
+ */
+ public MockHttpSession() {
+ this(null);
+ }
+
+ /**
+ * Create a new MockHttpSession.
+ *
+ * @param servletContext the ServletContext that the session runs in
+ */
+ public MockHttpSession(ServletContext servletContext) {
+ this(servletContext, null);
+ }
+
+ /**
+ * Create a new MockHttpSession.
+ *
+ * @param servletContext the ServletContext that the session runs in
+ * @param id a unique identifier for this session
+ */
+ public MockHttpSession(ServletContext servletContext, String id) {
+ this.servletContext = (servletContext != null ? servletContext : new MockServletContext());
+ this.id = (id != null ? id : Integer.toString(nextId++));
+ }
+
+ public long getCreationTime() {
+ return this.creationTime;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void access() {
+ this.lastAccessedTime = System.currentTimeMillis();
+ this.isNew = false;
+ }
+
+ public long getLastAccessedTime() {
+ return this.lastAccessedTime;
+ }
+
+ public ServletContext getServletContext() {
+ return this.servletContext;
+ }
+
+ public void setMaxInactiveInterval(int interval) {
+ this.maxInactiveInterval = interval;
+ }
+
+ public int getMaxInactiveInterval() {
+ return this.maxInactiveInterval;
+ }
+
+ public HttpSessionContext getSessionContext() {
+ throw new UnsupportedOperationException("getSessionContext");
+ }
+
+ public Object getAttribute(String name) {
+ Assert.notNull(name, "Attribute name must not be null");
+ return this.attributes.get(name);
+ }
+
+ public Object getValue(String name) {
+ return getAttribute(name);
+ }
+
+ public Enumeration<String> getAttributeNames() {
+ return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet()));
+ }
+
+ public String[] getValueNames() {
+ return this.attributes.keySet().toArray(new String[this.attributes.size()]);
+ }
+
+ public void setAttribute(String name, Object value) {
+ Assert.notNull(name, "Attribute name must not be null");
+ if (value != null) {
+ this.attributes.put(name, value);
+ if (value instanceof HttpSessionBindingListener) {
+ ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value));
+ }
+ }
+ else {
+ removeAttribute(name);
+ }
+ }
+
+ public void putValue(String name, Object value) {
+ setAttribute(name, value);
+ }
+
+ public void removeAttribute(String name) {
+ Assert.notNull(name, "Attribute name must not be null");
+ Object value = this.attributes.remove(name);
+ if (value instanceof HttpSessionBindingListener) {
+ ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value));
+ }
+ }
+
+ public void removeValue(String name) {
+ removeAttribute(name);
+ }
+
+ /**
+ * Clear all of this session's attributes.
+ */
+ public void clearAttributes() {
+ for (Iterator<Map.Entry<String, Object>> it = this.attributes.entrySet().iterator(); it.hasNext();) {
+ Map.Entry<String, Object> entry = it.next();
+ String name = entry.getKey();
+ Object value = entry.getValue();
+ it.remove();
+ if (value instanceof HttpSessionBindingListener) {
+ ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value));
+ }
+ }
+ }
+
+ /**
+ * Invalidates this session then unbinds any objects bound to it.
+ *
+ * @throws IllegalStateException if this method is called on an already invalidated session
+ */
+ public void invalidate() {
+ if (this.invalid) {
+ throw new IllegalStateException("The session has already been invalidated");
+ }
+
+ // else
+ this.invalid = true;
+ clearAttributes();
+ }
+
+ public boolean isInvalid() {
+ return this.invalid;
+ }
+
+ public void setNew(boolean value) {
+ this.isNew = value;
+ }
+
+ public boolean isNew() {
+ return this.isNew;
+ }
+
+ /**
+ * Serialize the attributes of this session into an object that can be
+ * turned into a byte array with standard Java serialization.
+ *
+ * @return a representation of this session's serialized state
+ */
+ public Serializable serializeState() {
+ HashMap<String, Serializable> state = new HashMap<String, Serializable>();
+ for (Iterator<Map.Entry<String, Object>> it = this.attributes.entrySet().iterator(); it.hasNext();) {
+ Map.Entry<String, Object> entry = it.next();
+ String name = entry.getKey();
+ Object value = entry.getValue();
+ it.remove();
+ if (value instanceof Serializable) {
+ state.put(name, (Serializable) value);
+ }
+ else {
+ // Not serializable... Servlet containers usually automatically
+ // unbind the attribute in this case.
+ if (value instanceof HttpSessionBindingListener) {
+ ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value));
+ }
+ }
+ }
+ return state;
+ }
+
+ /**
+ * Deserialize the attributes of this session from a state object created by
+ * {@link #serializeState()}.
+ *
+ * @param state a representation of this session's serialized state
+ */
+ @SuppressWarnings("unchecked")
+ public void deserializeState(Serializable state) {
+ Assert.isTrue(state instanceof Map, "Serialized state needs to be of type [java.util.Map]");
+ this.attributes.putAll((Map<String, Object>) state);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java b/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java
new file mode 100644
index 00000000..1d5bf928
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2002-2009 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.mock.web;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.jsp.JspWriter;
+
+/**
+ * Mock implementation of the {@link javax.servlet.jsp.JspWriter} class.
+ *
+ * <p>Used for testing the web framework; only necessary for testing
+ * applications when testing custom JSP tags.
+ *
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+public class MockJspWriter extends JspWriter {
+
+ private final HttpServletResponse response;
+
+ private PrintWriter targetWriter;
+
+
+ /**
+ * Create a MockJspWriter for the given response,
+ * using the response's default Writer.
+ * @param response the servlet response to wrap
+ */
+ public MockJspWriter(HttpServletResponse response) {
+ this(response, null);
+ }
+
+ /**
+ * Create a MockJspWriter for the given plain Writer.
+ * @param targetWriter the target Writer to wrap
+ */
+ public MockJspWriter(Writer targetWriter) {
+ this(null, targetWriter);
+ }
+
+ /**
+ * Create a MockJspWriter for the given response.
+ * @param response the servlet response to wrap
+ * @param targetWriter the target Writer to wrap
+ */
+ public MockJspWriter(HttpServletResponse response, Writer targetWriter) {
+ super(DEFAULT_BUFFER, true);
+ this.response = (response != null ? response : new MockHttpServletResponse());
+ if (targetWriter instanceof PrintWriter) {
+ this.targetWriter = (PrintWriter) targetWriter;
+ }
+ else if (targetWriter != null) {
+ this.targetWriter = new PrintWriter(targetWriter);
+ }
+ }
+
+ /**
+ * Lazily initialize the target Writer.
+ */
+ protected PrintWriter getTargetWriter() throws IOException {
+ if (this.targetWriter == null) {
+ this.targetWriter = this.response.getWriter();
+ }
+ return this.targetWriter;
+ }
+
+
+ public void clear() throws IOException {
+ if (this.response.isCommitted()) {
+ throw new IOException("Response already committed");
+ }
+ this.response.resetBuffer();
+ }
+
+ public void clearBuffer() throws IOException {
+ }
+
+ public void flush() throws IOException {
+ this.response.flushBuffer();
+ }
+
+ public void close() throws IOException {
+ flush();
+ }
+
+ public int getRemaining() {
+ return Integer.MAX_VALUE;
+ }
+
+ public void newLine() throws IOException {
+ getTargetWriter().println();
+ }
+
+ public void write(char value[], int offset, int length) throws IOException {
+ getTargetWriter().write(value, offset, length);
+ }
+
+ public void print(boolean value) throws IOException {
+ getTargetWriter().print(value);
+ }
+
+ public void print(char value) throws IOException {
+ getTargetWriter().print(value);
+ }
+
+ public void print(char[] value) throws IOException {
+ getTargetWriter().print(value);
+ }
+
+ public void print(double value) throws IOException {
+ getTargetWriter().print(value);
+ }
+
+ public void print(float value) throws IOException {
+ getTargetWriter().print(value);
+ }
+
+ public void print(int value) throws IOException {
+ getTargetWriter().print(value);
+ }
+
+ public void print(long value) throws IOException {
+ getTargetWriter().print(value);
+ }
+
+ public void print(Object value) throws IOException {
+ getTargetWriter().print(value);
+ }
+
+ public void print(String value) throws IOException {
+ getTargetWriter().print(value);
+ }
+
+ public void println() throws IOException {
+ getTargetWriter().println();
+ }
+
+ public void println(boolean value) throws IOException {
+ getTargetWriter().println(value);
+ }
+
+ public void println(char value) throws IOException {
+ getTargetWriter().println(value);
+ }
+
+ public void println(char[] value) throws IOException {
+ getTargetWriter().println(value);
+ }
+
+ public void println(double value) throws IOException {
+ getTargetWriter().println(value);
+ }
+
+ public void println(float value) throws IOException {
+ getTargetWriter().println(value);
+ }
+
+ public void println(int value) throws IOException {
+ getTargetWriter().println(value);
+ }
+
+ public void println(long value) throws IOException {
+ getTargetWriter().println(value);
+ }
+
+ public void println(Object value) throws IOException {
+ getTargetWriter().println(value);
+ }
+
+ public void println(String value) throws IOException {
+ getTargetWriter().println(value);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java
new file mode 100644
index 00000000..fb57d1f1
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2002-2011 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.mock.web;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.springframework.util.Assert;
+import org.springframework.util.FileCopyUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * Mock implementation of the {@link org.springframework.web.multipart.MultipartFile}
+ * interface.
+ *
+ * <p>Useful in conjunction with a {@link MockMultipartHttpServletRequest}
+ * for testing application controllers that access multipart uploads.
+ *
+ * @author Juergen Hoeller
+ * @author Eric Crampton
+ * @since 2.0
+ * @see MockMultipartHttpServletRequest
+ */
+public class MockMultipartFile implements MultipartFile {
+
+ private final String name;
+
+ private String originalFilename;
+
+ private String contentType;
+
+ private final byte[] content;
+
+
+ /**
+ * Create a new MockMultipartFile with the given content.
+ * @param name the name of the file
+ * @param content the content of the file
+ */
+ public MockMultipartFile(String name, byte[] content) {
+ this(name, "", null, content);
+ }
+
+ /**
+ * Create a new MockMultipartFile with the given content.
+ * @param name the name of the file
+ * @param contentStream the content of the file as stream
+ * @throws IOException if reading from the stream failed
+ */
+ public MockMultipartFile(String name, InputStream contentStream) throws IOException {
+ this(name, "", null, FileCopyUtils.copyToByteArray(contentStream));
+ }
+
+ /**
+ * Create a new MockMultipartFile with the given content.
+ * @param name the name of the file
+ * @param originalFilename the original filename (as on the client's machine)
+ * @param contentType the content type (if known)
+ * @param content the content of the file
+ */
+ public MockMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
+ Assert.hasLength(name, "Name must not be null");
+ this.name = name;
+ this.originalFilename = (originalFilename != null ? originalFilename : "");
+ this.contentType = contentType;
+ this.content = (content != null ? content : new byte[0]);
+ }
+
+ /**
+ * Create a new MockMultipartFile with the given content.
+ * @param name the name of the file
+ * @param originalFilename the original filename (as on the client's machine)
+ * @param contentType the content type (if known)
+ * @param contentStream the content of the file as stream
+ * @throws IOException if reading from the stream failed
+ */
+ public MockMultipartFile(String name, String originalFilename, String contentType, InputStream contentStream)
+ throws IOException {
+
+ this(name, originalFilename, contentType, FileCopyUtils.copyToByteArray(contentStream));
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getOriginalFilename() {
+ return this.originalFilename;
+ }
+
+ public String getContentType() {
+ return this.contentType;
+ }
+
+ public boolean isEmpty() {
+ return (this.content.length == 0);
+ }
+
+ public long getSize() {
+ return this.content.length;
+ }
+
+ public byte[] getBytes() throws IOException {
+ return this.content;
+ }
+
+ public InputStream getInputStream() throws IOException {
+ return new ByteArrayInputStream(this.content);
+ }
+
+ public void transferTo(File dest) throws IOException, IllegalStateException {
+ FileCopyUtils.copy(this.content, dest);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java
new file mode 100644
index 00000000..b01c4fdc
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2002-2011 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.mock.web;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.Assert;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.MultipartHttpServletRequest;
+
+/**
+ * Mock implementation of the
+ * {@link org.springframework.web.multipart.MultipartHttpServletRequest} interface.
+ *
+ * <p>Useful for testing application controllers that access multipart uploads.
+ * The {@link MockMultipartFile} can be used to populate these mock requests
+ * with files.
+ *
+ * @author Juergen Hoeller
+ * @author Eric Crampton
+ * @author Arjen Poutsma
+ * @since 2.0
+ * @see MockMultipartFile
+ */
+public class MockMultipartHttpServletRequest extends MockHttpServletRequest implements MultipartHttpServletRequest {
+
+ private final MultiValueMap<String, MultipartFile> multipartFiles =
+ new LinkedMultiValueMap<String, MultipartFile>();
+
+
+ public MockMultipartHttpServletRequest() {
+ setMethod("POST");
+ setContentType("multipart/form-data");
+ }
+
+
+ /**
+ * Add a file to this request. The parameter name from the multipart
+ * form is taken from the {@link MultipartFile#getName()}.
+ * @param file multipart file to be added
+ */
+ public void addFile(MultipartFile file) {
+ Assert.notNull(file, "MultipartFile must not be null");
+ this.multipartFiles.add(file.getName(), file);
+ }
+
+ public Iterator<String> getFileNames() {
+ return this.multipartFiles.keySet().iterator();
+ }
+
+ public MultipartFile getFile(String name) {
+ return this.multipartFiles.getFirst(name);
+ }
+
+ public List<MultipartFile> getFiles(String name) {
+ List<MultipartFile> multipartFiles = this.multipartFiles.get(name);
+ if (multipartFiles != null) {
+ return multipartFiles;
+ }
+ else {
+ return Collections.emptyList();
+ }
+ }
+
+ public Map<String, MultipartFile> getFileMap() {
+ return this.multipartFiles.toSingleValueMap();
+ }
+
+ public MultiValueMap<String, MultipartFile> getMultiFileMap() {
+ return new LinkedMultiValueMap<String, MultipartFile>(this.multipartFiles);
+ }
+
+ public String getMultipartContentType(String paramOrFileName) {
+ MultipartFile file = getFile(paramOrFileName);
+ if (file != null) {
+ return file.getContentType();
+ }
+ else {
+ return null;
+ }
+ }
+
+ public HttpMethod getRequestMethod() {
+ return HttpMethod.valueOf(getMethod());
+ }
+
+ public HttpHeaders getRequestHeaders() {
+ HttpHeaders headers = new HttpHeaders();
+ Enumeration<String> headerNames = getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+ headers.put(headerName, Collections.list(getHeaders(headerName)));
+ }
+ return headers;
+ }
+
+ public HttpHeaders getMultipartHeaders(String paramOrFileName) {
+ String contentType = getMultipartContentType(paramOrFileName);
+ if (contentType != null) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("Content-Type", contentType);
+ return headers;
+ }
+ else {
+ return null;
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java
new file mode 100644
index 00000000..2c5d07e8
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2002-2013 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.mock.web;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import javax.el.ELContext;
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.jsp.JspWriter;
+import javax.servlet.jsp.PageContext;
+import javax.servlet.jsp.el.ExpressionEvaluator;
+import javax.servlet.jsp.el.VariableResolver;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.servlet.jsp.PageContext} interface.
+ *
+ * <p>Used for testing the web framework; only necessary for testing
+ * applications when testing custom JSP tags.
+ *
+ * <p>Note: Expects initialization via the constructor rather than via the
+ * {@code PageContext.initialize} method. Does not support writing to
+ * a JspWriter, request dispatching, and {@code handlePageException} calls.
+ *
+ * @author Juergen Hoeller
+ * @since 1.0.2
+ */
+@SuppressWarnings("deprecation")
+public class MockPageContext extends PageContext {
+
+ private final ServletContext servletContext;
+
+ private final HttpServletRequest request;
+
+ private final HttpServletResponse response;
+
+ private final ServletConfig servletConfig;
+
+ private final Map<String, Object> attributes = new LinkedHashMap<String, Object>();
+
+ private JspWriter out;
+
+
+ /**
+ * Create new MockPageContext with a default {@link MockServletContext},
+ * {@link MockHttpServletRequest}, {@link MockHttpServletResponse},
+ * {@link MockServletConfig}.
+ */
+ public MockPageContext() {
+ this(null, null, null, null);
+ }
+
+ /**
+ * Create new MockPageContext with a default {@link MockHttpServletRequest},
+ * {@link MockHttpServletResponse}, {@link MockServletConfig}.
+ * @param servletContext the ServletContext that the JSP page runs in
+ * (only necessary when actually accessing the ServletContext)
+ */
+ public MockPageContext(ServletContext servletContext) {
+ this(servletContext, null, null, null);
+ }
+
+ /**
+ * Create new MockPageContext with a MockHttpServletResponse,
+ * MockServletConfig.
+ * @param servletContext the ServletContext that the JSP page runs in
+ * @param request the current HttpServletRequest
+ * (only necessary when actually accessing the request)
+ */
+ public MockPageContext(ServletContext servletContext, HttpServletRequest request) {
+ this(servletContext, request, null, null);
+ }
+
+ /**
+ * Create new MockPageContext with a MockServletConfig.
+ * @param servletContext the ServletContext that the JSP page runs in
+ * @param request the current HttpServletRequest
+ * @param response the current HttpServletResponse
+ * (only necessary when actually writing to the response)
+ */
+ public MockPageContext(ServletContext servletContext, HttpServletRequest request, HttpServletResponse response) {
+ this(servletContext, request, response, null);
+ }
+
+ /**
+ * Create new MockServletConfig.
+ * @param servletContext the ServletContext that the JSP page runs in
+ * @param request the current HttpServletRequest
+ * @param response the current HttpServletResponse
+ * @param servletConfig the ServletConfig (hardly ever accessed from within a tag)
+ */
+ public MockPageContext(ServletContext servletContext, HttpServletRequest request,
+ HttpServletResponse response, ServletConfig servletConfig) {
+
+ this.servletContext = (servletContext != null ? servletContext : new MockServletContext());
+ this.request = (request != null ? request : new MockHttpServletRequest(servletContext));
+ this.response = (response != null ? response : new MockHttpServletResponse());
+ this.servletConfig = (servletConfig != null ? servletConfig : new MockServletConfig(servletContext));
+ }
+
+
+ public void initialize(
+ Servlet servlet, ServletRequest request, ServletResponse response,
+ String errorPageURL, boolean needsSession, int bufferSize, boolean autoFlush) {
+
+ throw new UnsupportedOperationException("Use appropriate constructor");
+ }
+
+ public void release() {
+ }
+
+ public void setAttribute(String name, Object value) {
+ Assert.notNull(name, "Attribute name must not be null");
+ if (value != null) {
+ this.attributes.put(name, value);
+ }
+ else {
+ this.attributes.remove(name);
+ }
+ }
+
+ public void setAttribute(String name, Object value, int scope) {
+ Assert.notNull(name, "Attribute name must not be null");
+ switch (scope) {
+ case PAGE_SCOPE:
+ setAttribute(name, value);
+ break;
+ case REQUEST_SCOPE:
+ this.request.setAttribute(name, value);
+ break;
+ case SESSION_SCOPE:
+ this.request.getSession().setAttribute(name, value);
+ break;
+ case APPLICATION_SCOPE:
+ this.servletContext.setAttribute(name, value);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid scope: " + scope);
+ }
+ }
+
+ public Object getAttribute(String name) {
+ Assert.notNull(name, "Attribute name must not be null");
+ return this.attributes.get(name);
+ }
+
+ public Object getAttribute(String name, int scope) {
+ Assert.notNull(name, "Attribute name must not be null");
+ switch (scope) {
+ case PAGE_SCOPE:
+ return getAttribute(name);
+ case REQUEST_SCOPE:
+ return this.request.getAttribute(name);
+ case SESSION_SCOPE:
+ HttpSession session = this.request.getSession(false);
+ return (session != null ? session.getAttribute(name) : null);
+ case APPLICATION_SCOPE:
+ return this.servletContext.getAttribute(name);
+ default:
+ throw new IllegalArgumentException("Invalid scope: " + scope);
+ }
+ }
+
+ public Object findAttribute(String name) {
+ Object value = getAttribute(name);
+ if (value == null) {
+ value = getAttribute(name, REQUEST_SCOPE);
+ if (value == null) {
+ value = getAttribute(name, SESSION_SCOPE);
+ if (value == null) {
+ value = getAttribute(name, APPLICATION_SCOPE);
+ }
+ }
+ }
+ return value;
+ }
+
+ public void removeAttribute(String name) {
+ Assert.notNull(name, "Attribute name must not be null");
+ this.removeAttribute(name, PageContext.PAGE_SCOPE);
+ this.removeAttribute(name, PageContext.REQUEST_SCOPE);
+ this.removeAttribute(name, PageContext.SESSION_SCOPE);
+ this.removeAttribute(name, PageContext.APPLICATION_SCOPE);
+ }
+
+ public void removeAttribute(String name, int scope) {
+ Assert.notNull(name, "Attribute name must not be null");
+ switch (scope) {
+ case PAGE_SCOPE:
+ this.attributes.remove(name);
+ break;
+ case REQUEST_SCOPE:
+ this.request.removeAttribute(name);
+ break;
+ case SESSION_SCOPE:
+ this.request.getSession().removeAttribute(name);
+ break;
+ case APPLICATION_SCOPE:
+ this.servletContext.removeAttribute(name);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid scope: " + scope);
+ }
+ }
+
+ public int getAttributesScope(String name) {
+ if (getAttribute(name) != null) {
+ return PAGE_SCOPE;
+ }
+ else if (getAttribute(name, REQUEST_SCOPE) != null) {
+ return REQUEST_SCOPE;
+ }
+ else if (getAttribute(name, SESSION_SCOPE) != null) {
+ return SESSION_SCOPE;
+ }
+ else if (getAttribute(name, APPLICATION_SCOPE) != null) {
+ return APPLICATION_SCOPE;
+ }
+ else {
+ return 0;
+ }
+ }
+
+ public Enumeration<String> getAttributeNames() {
+ return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet()));
+ }
+
+ @SuppressWarnings("unchecked")
+ public Enumeration<String> getAttributeNamesInScope(int scope) {
+ switch (scope) {
+ case PAGE_SCOPE:
+ return getAttributeNames();
+ case REQUEST_SCOPE:
+ return this.request.getAttributeNames();
+ case SESSION_SCOPE:
+ HttpSession session = this.request.getSession(false);
+ return (session != null ? session.getAttributeNames() : null);
+ case APPLICATION_SCOPE:
+ return this.servletContext.getAttributeNames();
+ default:
+ throw new IllegalArgumentException("Invalid scope: " + scope);
+ }
+ }
+
+ public JspWriter getOut() {
+ if (this.out == null) {
+ this.out = new MockJspWriter(this.response);
+ }
+ return this.out;
+ }
+
+ public ExpressionEvaluator getExpressionEvaluator() {
+ return new MockExpressionEvaluator(this);
+ }
+
+ public ELContext getELContext() {
+ return null;
+ }
+
+ public VariableResolver getVariableResolver() {
+ return null;
+ }
+
+ public HttpSession getSession() {
+ return this.request.getSession();
+ }
+
+ public Object getPage() {
+ return this;
+ }
+
+ public ServletRequest getRequest() {
+ return this.request;
+ }
+
+ public ServletResponse getResponse() {
+ return this.response;
+ }
+
+ public Exception getException() {
+ return null;
+ }
+
+ public ServletConfig getServletConfig() {
+ return this.servletConfig;
+ }
+
+ public ServletContext getServletContext() {
+ return this.servletContext;
+ }
+
+ public void forward(String path) throws ServletException, IOException {
+ this.request.getRequestDispatcher(path).forward(this.request, this.response);
+ }
+
+ public void include(String path) throws ServletException, IOException {
+ this.request.getRequestDispatcher(path).include(this.request, this.response);
+ }
+
+ public void include(String path, boolean flush) throws ServletException, IOException {
+ this.request.getRequestDispatcher(path).include(this.request, this.response);
+ if (flush) {
+ this.response.flushBuffer();
+ }
+ }
+
+ public byte[] getContentAsByteArray() {
+ Assert.isTrue(this.response instanceof MockHttpServletResponse);
+ return ((MockHttpServletResponse) this.response).getContentAsByteArray();
+ }
+
+ public String getContentAsString() throws UnsupportedEncodingException {
+ Assert.isTrue(this.response instanceof MockHttpServletResponse);
+ return ((MockHttpServletResponse) this.response).getContentAsString();
+ }
+
+ public void handlePageException(Exception ex) throws ServletException, IOException {
+ throw new ServletException("Page exception", ex);
+ }
+
+ public void handlePageException(Throwable ex) throws ServletException, IOException {
+ throw new ServletException("Page exception", ex);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java b/spring-test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java
new file mode 100644
index 00000000..88660bb3
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java
@@ -0,0 +1,93 @@
+/*
+ * 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.mock.web;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.servlet.RequestDispatcher} interface.
+ *
+ * <p>Used for testing the web framework; typically not necessary for
+ * testing application controllers.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @author Sam Brannen
+ * @since 1.0.2
+ */
+public class MockRequestDispatcher implements RequestDispatcher {
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final String resource;
+
+
+ /**
+ * Create a new MockRequestDispatcher for the given resource.
+ * @param resource the server resource to dispatch to, located at a
+ * particular path or given by a particular name
+ */
+ public MockRequestDispatcher(String resource) {
+ Assert.notNull(resource, "resource must not be null");
+ this.resource = resource;
+ }
+
+
+ public void forward(ServletRequest request, ServletResponse response) {
+ Assert.notNull(request, "Request must not be null");
+ Assert.notNull(response, "Response must not be null");
+ if (response.isCommitted()) {
+ throw new IllegalStateException("Cannot perform forward - response is already committed");
+ }
+ getMockHttpServletResponse(response).setForwardedUrl(this.resource);
+ if (logger.isDebugEnabled()) {
+ logger.debug("MockRequestDispatcher: forwarding to [" + this.resource + "]");
+ }
+ }
+
+ public void include(ServletRequest request, ServletResponse response) {
+ Assert.notNull(request, "Request must not be null");
+ Assert.notNull(response, "Response must not be null");
+ getMockHttpServletResponse(response).addIncludedUrl(this.resource);
+ if (logger.isDebugEnabled()) {
+ logger.debug("MockRequestDispatcher: including [" + this.resource + "]");
+ }
+ }
+
+ /**
+ * Obtain the underlying {@link MockHttpServletResponse}, unwrapping
+ * {@link HttpServletResponseWrapper} decorators if necessary.
+ */
+ protected MockHttpServletResponse getMockHttpServletResponse(ServletResponse response) {
+ if (response instanceof MockHttpServletResponse) {
+ return (MockHttpServletResponse) response;
+ }
+ if (response instanceof HttpServletResponseWrapper) {
+ return getMockHttpServletResponse(((HttpServletResponseWrapper) response).getResponse());
+ }
+ throw new IllegalArgumentException("MockRequestDispatcher requires MockHttpServletResponse");
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java
new file mode 100644
index 00000000..c62ec152
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2002-2009 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.mock.web;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.servlet.ServletConfig} interface.
+ *
+ * <p>Used for testing the web framework; typically not necessary for
+ * testing application controllers.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @since 1.0.2
+ */
+public class MockServletConfig implements ServletConfig {
+
+ private final ServletContext servletContext;
+
+ private final String servletName;
+
+ private final Map<String, String> initParameters = new LinkedHashMap<String, String>();
+
+
+ /**
+ * Create a new MockServletConfig with a default {@link MockServletContext}.
+ */
+ public MockServletConfig() {
+ this(null, "");
+ }
+
+ /**
+ * Create a new MockServletConfig with a default {@link MockServletContext}.
+ * @param servletName the name of the servlet
+ */
+ public MockServletConfig(String servletName) {
+ this(null, servletName);
+ }
+
+ /**
+ * Create a new MockServletConfig.
+ * @param servletContext the ServletContext that the servlet runs in
+ */
+ public MockServletConfig(ServletContext servletContext) {
+ this(servletContext, "");
+ }
+
+ /**
+ * Create a new MockServletConfig.
+ * @param servletContext the ServletContext that the servlet runs in
+ * @param servletName the name of the servlet
+ */
+ public MockServletConfig(ServletContext servletContext, String servletName) {
+ this.servletContext = (servletContext != null ? servletContext : new MockServletContext());
+ this.servletName = servletName;
+ }
+
+
+ public String getServletName() {
+ return this.servletName;
+ }
+
+ public ServletContext getServletContext() {
+ return this.servletContext;
+ }
+
+ public void addInitParameter(String name, String value) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.initParameters.put(name, value);
+ }
+
+ public String getInitParameter(String name) {
+ Assert.notNull(name, "Parameter name must not be null");
+ return this.initParameters.get(name);
+ }
+
+ public Enumeration<String> getInitParameterNames() {
+ return Collections.enumeration(this.initParameters.keySet());
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java
new file mode 100644
index 00000000..49b49e58
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright 2002-2013 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.mock.web;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.activation.FileTypeMap;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.util.WebUtils;
+
+/**
+ * Mock implementation of the {@link javax.servlet.ServletContext} interface.
+ *
+ * <p>Compatible with Servlet 2.5 and partially with Servlet 3.0. Can be configured to
+ * expose a specific version through {@link #setMajorVersion}/{@link #setMinorVersion};
+ * default is 2.5. Note that Servlet 3.0 support is limited: servlet, filter and listener
+ * registration methods are not supported; neither is cookie or JSP configuration.
+ * We generally do not recommend to unit-test your ServletContainerInitializers and
+ * WebApplicationInitializers which is where those registration methods would be used.
+ *
+ * <p>Used for testing the Spring web framework; only rarely necessary for testing
+ * application controllers. As long as application components don't explicitly
+ * access the {@code ServletContext}, {@code ClassPathXmlApplicationContext} or
+ * {@code FileSystemXmlApplicationContext} can be used to load the context files
+ * for testing, even for {@code DispatcherServlet} context definitions.
+ *
+ * <p>For setting up a full {@code WebApplicationContext} in a test environment,
+ * you can use {@code AnnotationConfigWebApplicationContext},
+ * {@code XmlWebApplicationContext}, or {@code GenericWebApplicationContext},
+ * passing in an appropriate {@code MockServletContext} instance. You might want
+ * to configure your {@code MockServletContext} with a {@code FileSystemResourceLoader}
+ * in that case to ensure that resource paths are interpreted as relative filesystem
+ * locations.
+ *
+ * <p>A common setup is to point your JVM working directory to the root of your
+ * web application directory, in combination with filesystem-based resource loading.
+ * This allows to load the context files as used in the web application, with
+ * relative paths getting interpreted correctly. Such a setup will work with both
+ * {@code FileSystemXmlApplicationContext} (which will load straight from the
+ * filesystem) and {@code XmlWebApplicationContext} with an underlying
+ * {@code MockServletContext} (as long as the {@code MockServletContext} has been
+ * configured with a {@code FileSystemResourceLoader}).
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @author Sam Brannen
+ * @since 1.0.2
+ * @see #MockServletContext(org.springframework.core.io.ResourceLoader)
+ * @see org.springframework.web.context.support.AnnotationConfigWebApplicationContext
+ * @see org.springframework.web.context.support.XmlWebApplicationContext
+ * @see org.springframework.web.context.support.GenericWebApplicationContext
+ * @see org.springframework.context.support.ClassPathXmlApplicationContext
+ * @see org.springframework.context.support.FileSystemXmlApplicationContext
+ */
+public class MockServletContext implements ServletContext {
+
+ /** Default Servlet name used by Tomcat, Jetty, JBoss, and GlassFish: {@value}. */
+ private static final String COMMON_DEFAULT_SERVLET_NAME = "default";
+
+ private static final String TEMP_DIR_SYSTEM_PROPERTY = "java.io.tmpdir";
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final Map<String, ServletContext> contexts = new HashMap<String, ServletContext>();
+
+ private final Map<String, String> initParameters = new LinkedHashMap<String, String>();
+
+ private final Map<String, Object> attributes = new LinkedHashMap<String, Object>();
+
+ private final Set<String> declaredRoles = new HashSet<String>();
+
+ private final Map<String, RequestDispatcher> namedRequestDispatchers = new HashMap<String, RequestDispatcher>();
+
+ private final ResourceLoader resourceLoader;
+
+ private final String resourceBasePath;
+
+ private String contextPath = "";
+
+ private int majorVersion = 2;
+
+ private int minorVersion = 5;
+
+ private int effectiveMajorVersion = 2;
+
+ private int effectiveMinorVersion = 5;
+
+ private String servletContextName = "MockServletContext";
+
+ private String defaultServletName = COMMON_DEFAULT_SERVLET_NAME;
+
+
+ /**
+ * Create a new MockServletContext, using no base path and a
+ * DefaultResourceLoader (i.e. the classpath root as WAR root).
+ * @see org.springframework.core.io.DefaultResourceLoader
+ */
+ public MockServletContext() {
+ this("", null);
+ }
+
+ /**
+ * Create a new MockServletContext, using a DefaultResourceLoader.
+ * @param resourceBasePath the root directory of the WAR (should not end with a slash)
+ * @see org.springframework.core.io.DefaultResourceLoader
+ */
+ public MockServletContext(String resourceBasePath) {
+ this(resourceBasePath, null);
+ }
+
+ /**
+ * Create a new MockServletContext, using the specified ResourceLoader
+ * and no base path.
+ * @param resourceLoader the ResourceLoader to use (or null for the default)
+ */
+ public MockServletContext(ResourceLoader resourceLoader) {
+ this("", resourceLoader);
+ }
+
+ /**
+ * Create a new MockServletContext using the supplied resource base path and
+ * resource loader.
+ * <p>Registers a {@link MockRequestDispatcher} for the Servlet named
+ * {@linkplain #COMMON_DEFAULT_SERVLET_NAME "default"}.
+ * @param resourceBasePath the root directory of the WAR (should not end with a slash)
+ * @param resourceLoader the ResourceLoader to use (or null for the default)
+ * @see #registerNamedDispatcher
+ */
+ @SuppressWarnings("javadoc")
+ public MockServletContext(String resourceBasePath, ResourceLoader resourceLoader) {
+ this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
+ this.resourceBasePath = (resourceBasePath != null ? resourceBasePath : "");
+
+ // Use JVM temp dir as ServletContext temp dir.
+ String tempDir = System.getProperty(TEMP_DIR_SYSTEM_PROPERTY);
+ if (tempDir != null) {
+ this.attributes.put(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE, new File(tempDir));
+ }
+
+ registerNamedDispatcher(this.defaultServletName, new MockRequestDispatcher(this.defaultServletName));
+ }
+
+ /**
+ * Build a full resource location for the given path,
+ * prepending the resource base path of this MockServletContext.
+ * @param path the path as specified
+ * @return the full resource path
+ */
+ protected String getResourceLocation(String path) {
+ if (!path.startsWith("/")) {
+ path = "/" + path;
+ }
+ return this.resourceBasePath + path;
+ }
+
+ public void setContextPath(String contextPath) {
+ this.contextPath = (contextPath != null ? contextPath : "");
+ }
+
+ /* This is a Servlet API 2.5 method. */
+ public String getContextPath() {
+ return this.contextPath;
+ }
+
+ public void registerContext(String contextPath, ServletContext context) {
+ this.contexts.put(contextPath, context);
+ }
+
+ public ServletContext getContext(String contextPath) {
+ if (this.contextPath.equals(contextPath)) {
+ return this;
+ }
+ return this.contexts.get(contextPath);
+ }
+
+ public void setMajorVersion(int majorVersion) {
+ this.majorVersion = majorVersion;
+ }
+
+ public int getMajorVersion() {
+ return this.majorVersion;
+ }
+
+ public void setMinorVersion(int minorVersion) {
+ this.minorVersion = minorVersion;
+ }
+
+ public int getMinorVersion() {
+ return this.minorVersion;
+ }
+
+ public void setEffectiveMajorVersion(int effectiveMajorVersion) {
+ this.effectiveMajorVersion = effectiveMajorVersion;
+ }
+
+ public int getEffectiveMajorVersion() {
+ return this.effectiveMajorVersion;
+ }
+
+ public void setEffectiveMinorVersion(int effectiveMinorVersion) {
+ this.effectiveMinorVersion = effectiveMinorVersion;
+ }
+
+ public int getEffectiveMinorVersion() {
+ return this.effectiveMinorVersion;
+ }
+
+ /**
+ * This method uses the Java Activation framework, which returns "application/octet-stream"
+ * when the mime type is unknown (i.e. it never returns {@code null}). In order to maintain
+ * the {@link ServletContext#getMimeType(String)} contract, this method returns {@code null}
+ * if the mimeType is "application/octet-stream", as of Spring 3.2.2.
+ */
+ public String getMimeType(String filePath) {
+ String mimeType = MimeTypeResolver.getMimeType(filePath);
+ return ("application/octet-stream".equals(mimeType)) ? null : mimeType;
+ }
+
+ public Set<String> getResourcePaths(String path) {
+ String actualPath = (path.endsWith("/") ? path : path + "/");
+ Resource resource = this.resourceLoader.getResource(getResourceLocation(actualPath));
+ try {
+ File file = resource.getFile();
+ String[] fileList = file.list();
+ if (ObjectUtils.isEmpty(fileList)) {
+ return null;
+ }
+ Set<String> resourcePaths = new LinkedHashSet<String>(fileList.length);
+ for (String fileEntry : fileList) {
+ String resultPath = actualPath + fileEntry;
+ if (resource.createRelative(fileEntry).getFile().isDirectory()) {
+ resultPath += "/";
+ }
+ resourcePaths.add(resultPath);
+ }
+ return resourcePaths;
+ }
+ catch (IOException ex) {
+ logger.warn("Couldn't get resource paths for " + resource, ex);
+ return null;
+ }
+ }
+
+ public URL getResource(String path) throws MalformedURLException {
+ Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
+ if (!resource.exists()) {
+ return null;
+ }
+ try {
+ return resource.getURL();
+ }
+ catch (MalformedURLException ex) {
+ throw ex;
+ }
+ catch (IOException ex) {
+ logger.warn("Couldn't get URL for " + resource, ex);
+ return null;
+ }
+ }
+
+ public InputStream getResourceAsStream(String path) {
+ Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
+ if (!resource.exists()) {
+ return null;
+ }
+ try {
+ return resource.getInputStream();
+ }
+ catch (IOException ex) {
+ logger.warn("Couldn't open InputStream for " + resource, ex);
+ return null;
+ }
+ }
+
+ public RequestDispatcher getRequestDispatcher(String path) {
+ if (!path.startsWith("/")) {
+ throw new IllegalArgumentException("RequestDispatcher path at ServletContext level must start with '/'");
+ }
+ return new MockRequestDispatcher(path);
+ }
+
+ public RequestDispatcher getNamedDispatcher(String path) {
+ return this.namedRequestDispatchers.get(path);
+ }
+
+ /**
+ * Register a {@link RequestDispatcher} (typically a {@link MockRequestDispatcher})
+ * that acts as a wrapper for the named Servlet.
+ *
+ * @param name the name of the wrapped Servlet
+ * @param requestDispatcher the dispatcher that wraps the named Servlet
+ * @see #getNamedDispatcher
+ * @see #unregisterNamedDispatcher
+ */
+ public void registerNamedDispatcher(String name, RequestDispatcher requestDispatcher) {
+ Assert.notNull(name, "RequestDispatcher name must not be null");
+ Assert.notNull(requestDispatcher, "RequestDispatcher must not be null");
+ this.namedRequestDispatchers.put(name, requestDispatcher);
+ }
+
+ /**
+ * Unregister the {@link RequestDispatcher} with the given name.
+ *
+ * @param name the name of the dispatcher to unregister
+ * @see #getNamedDispatcher
+ * @see #registerNamedDispatcher
+ */
+ public void unregisterNamedDispatcher(String name) {
+ Assert.notNull(name, "RequestDispatcher name must not be null");
+ this.namedRequestDispatchers.remove(name);
+ }
+
+ /**
+ * Get the name of the <em>default</em> {@code Servlet}.
+ * <p>Defaults to {@linkplain #COMMON_DEFAULT_SERVLET_NAME "default"}.
+ * @see #setDefaultServletName
+ */
+ @SuppressWarnings("javadoc")
+ public String getDefaultServletName() {
+ return this.defaultServletName;
+ }
+
+ /**
+ * Set the name of the <em>default</em> {@code Servlet}.
+ * <p>Also {@link #unregisterNamedDispatcher unregisters} the current default
+ * {@link RequestDispatcher} and {@link #registerNamedDispatcher replaces}
+ * it with a {@link MockRequestDispatcher} for the provided
+ * {@code defaultServletName}.
+ * @param defaultServletName the name of the <em>default</em> {@code Servlet};
+ * never {@code null} or empty
+ * @see #getDefaultServletName
+ */
+ public void setDefaultServletName(String defaultServletName) {
+ Assert.hasText(defaultServletName, "defaultServletName must not be null or empty");
+ unregisterNamedDispatcher(this.defaultServletName);
+ this.defaultServletName = defaultServletName;
+ registerNamedDispatcher(this.defaultServletName, new MockRequestDispatcher(this.defaultServletName));
+ }
+
+ public Servlet getServlet(String name) {
+ return null;
+ }
+
+ public Enumeration<Servlet> getServlets() {
+ return Collections.enumeration(new HashSet<Servlet>());
+ }
+
+ public Enumeration<String> getServletNames() {
+ return Collections.enumeration(new HashSet<String>());
+ }
+
+ public void log(String message) {
+ logger.info(message);
+ }
+
+ public void log(Exception ex, String message) {
+ logger.info(message, ex);
+ }
+
+ public void log(String message, Throwable ex) {
+ logger.info(message, ex);
+ }
+
+ public String getRealPath(String path) {
+ Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
+ try {
+ return resource.getFile().getAbsolutePath();
+ }
+ catch (IOException ex) {
+ logger.warn("Couldn't determine real path of resource " + resource, ex);
+ return null;
+ }
+ }
+
+ public String getServerInfo() {
+ return "MockServletContext";
+ }
+
+ public String getInitParameter(String name) {
+ Assert.notNull(name, "Parameter name must not be null");
+ return this.initParameters.get(name);
+ }
+
+ public Enumeration<String> getInitParameterNames() {
+ return Collections.enumeration(this.initParameters.keySet());
+ }
+
+ public boolean setInitParameter(String name, String value) {
+ Assert.notNull(name, "Parameter name must not be null");
+ if (this.initParameters.containsKey(name)) {
+ return false;
+ }
+ this.initParameters.put(name, value);
+ return true;
+ }
+
+ public void addInitParameter(String name, String value) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.initParameters.put(name, value);
+ }
+
+ public Object getAttribute(String name) {
+ Assert.notNull(name, "Attribute name must not be null");
+ return this.attributes.get(name);
+ }
+
+ public Enumeration<String> getAttributeNames() {
+ return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet()));
+ }
+
+ public void setAttribute(String name, Object value) {
+ Assert.notNull(name, "Attribute name must not be null");
+ if (value != null) {
+ this.attributes.put(name, value);
+ }
+ else {
+ this.attributes.remove(name);
+ }
+ }
+
+ public void removeAttribute(String name) {
+ Assert.notNull(name, "Attribute name must not be null");
+ this.attributes.remove(name);
+ }
+
+ public void setServletContextName(String servletContextName) {
+ this.servletContextName = servletContextName;
+ }
+
+ public String getServletContextName() {
+ return this.servletContextName;
+ }
+
+ public ClassLoader getClassLoader() {
+ return ClassUtils.getDefaultClassLoader();
+ }
+
+ public void declareRoles(String... roleNames) {
+ Assert.notNull(roleNames, "Role names array must not be null");
+ for (String roleName : roleNames) {
+ Assert.hasLength(roleName, "Role name must not be empty");
+ this.declaredRoles.add(roleName);
+ }
+ }
+
+ public Set<String> getDeclaredRoles() {
+ return Collections.unmodifiableSet(this.declaredRoles);
+ }
+
+
+ /**
+ * Inner factory class used to introduce a Java Activation Framework
+ * dependency when actually asked to resolve a MIME type.
+ */
+ private static class MimeTypeResolver {
+
+ public static String getMimeType(String filePath) {
+ return FileTypeMap.getDefaultFileTypeMap().getContentType(filePath);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java b/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java
new file mode 100644
index 00000000..b3484a0a
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2002-2009 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.mock.web;
+
+import java.io.IOException;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.springframework.util.Assert;
+
+/**
+ * Implementation of the {@link javax.servlet.FilterConfig} interface which
+ * simply passes the call through to a given Filter/FilterChain combination
+ * (indicating the next Filter in the chain along with the FilterChain that it is
+ * supposed to work on) or to a given Servlet (indicating the end of the chain).
+ *
+ * @author Juergen Hoeller
+ * @since 2.0.3
+ * @see javax.servlet.Filter
+ * @see javax.servlet.Servlet
+ * @see MockFilterChain
+ */
+public class PassThroughFilterChain implements FilterChain {
+
+ private Filter filter;
+
+ private FilterChain nextFilterChain;
+
+ private Servlet servlet;
+
+
+ /**
+ * Create a new PassThroughFilterChain that delegates to the given Filter,
+ * calling it with the given FilterChain.
+ * @param filter the Filter to delegate to
+ * @param nextFilterChain the FilterChain to use for that next Filter
+ */
+ public PassThroughFilterChain(Filter filter, FilterChain nextFilterChain) {
+ Assert.notNull(filter, "Filter must not be null");
+ Assert.notNull(nextFilterChain, "'FilterChain must not be null");
+ this.filter = filter;
+ this.nextFilterChain = nextFilterChain;
+ }
+
+ /**
+ * Create a new PassThroughFilterChain that delegates to the given Servlet.
+ * @param servlet the Servlet to delegate to
+ */
+ public PassThroughFilterChain(Servlet servlet) {
+ Assert.notNull(servlet, "Servlet must not be null");
+ this.servlet = servlet;
+ }
+
+
+ /**
+ * Pass the call on to the Filter/Servlet.
+ */
+ public void doFilter(ServletRequest request, ServletResponse response) throws ServletException, IOException {
+ if (this.filter != null) {
+ this.filter.doFilter(request, response, this.nextFilterChain);
+ }
+ else {
+ this.servlet.service(request, response);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/package-info.java b/spring-test/src/main/java/org/springframework/mock/web/package-info.java
new file mode 100644
index 00000000..be8cb860
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/package-info.java
@@ -0,0 +1,15 @@
+
+/**
+ *
+ * A comprehensive set of Servlet API 2.5 mock objects,
+ * targeted at usage with Spring's web MVC framework.
+ * Useful for testing web contexts and controllers.
+ *
+ * <p>More convenient to use than dynamic mock objects
+ * (<a href="http://www.easymock.org">EasyMock</a>) or
+ * existing Servlet API mock objects
+ * (<a href="http://www.mockobjects.com">MockObjects</a>).
+ *
+ */
+package org.springframework.mock.web;
+
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java
new file mode 100644
index 00000000..a687ccb6
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletContext;
+import javax.portlet.PortletMode;
+
+/**
+ * Mock implementation of the {@link javax.portlet.ActionRequest} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockActionRequest extends MockClientDataRequest implements ActionRequest {
+
+ /**
+ * Create a new MockActionRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @see org.springframework.mock.web.portlet.MockPortalContext
+ * @see org.springframework.mock.web.portlet.MockPortletContext
+ */
+ public MockActionRequest() {
+ super();
+ }
+
+ /**
+ * Create a new MockActionRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @param actionName the name of the action to trigger
+ */
+ public MockActionRequest(String actionName) {
+ super();
+ setParameter(ActionRequest.ACTION_NAME, actionName);
+ }
+
+ /**
+ * Create a new MockActionRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @param portletMode the mode that the portlet runs in
+ */
+ public MockActionRequest(PortletMode portletMode) {
+ super();
+ setPortletMode(portletMode);
+ }
+
+ /**
+ * Create a new MockActionRequest with a default {@link MockPortalContext}.
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockActionRequest(PortletContext portletContext) {
+ super(portletContext);
+ }
+
+ /**
+ * Create a new MockActionRequest.
+ * @param portalContext the PortalContext that the request runs in
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockActionRequest(PortalContext portalContext, PortletContext portletContext) {
+ super(portalContext, portletContext);
+ }
+
+
+ @Override
+ protected String getLifecyclePhase() {
+ return ACTION_PHASE;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java
new file mode 100644
index 00000000..bec66878
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.io.IOException;
+import java.util.Map;
+import javax.portlet.ActionResponse;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletMode;
+import javax.portlet.PortletModeException;
+import javax.portlet.WindowState;
+import javax.portlet.WindowStateException;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.portlet.ActionResponse} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockActionResponse extends MockStateAwareResponse implements ActionResponse {
+
+ private boolean redirectAllowed = true;
+
+ private String redirectedUrl;
+
+
+ /**
+ * Create a new MockActionResponse with a default {@link MockPortalContext}.
+ * @see MockPortalContext
+ */
+ public MockActionResponse() {
+ super();
+ }
+
+ /**
+ * Create a new MockActionResponse.
+ * @param portalContext the PortalContext defining the supported
+ * PortletModes and WindowStates
+ */
+ public MockActionResponse(PortalContext portalContext) {
+ super(portalContext);
+ }
+
+
+ public void setWindowState(WindowState windowState) throws WindowStateException {
+ if (this.redirectedUrl != null) {
+ throw new IllegalStateException("Cannot set WindowState after sendRedirect has been called");
+ }
+ super.setWindowState(windowState);
+ this.redirectAllowed = false;
+ }
+
+ public void setPortletMode(PortletMode portletMode) throws PortletModeException {
+ if (this.redirectedUrl != null) {
+ throw new IllegalStateException("Cannot set PortletMode after sendRedirect has been called");
+ }
+ super.setPortletMode(portletMode);
+ this.redirectAllowed = false;
+ }
+
+ public void setRenderParameters(Map<String, String[]> parameters) {
+ if (this.redirectedUrl != null) {
+ throw new IllegalStateException("Cannot set render parameters after sendRedirect has been called");
+ }
+ super.setRenderParameters(parameters);
+ this.redirectAllowed = false;
+ }
+
+ public void setRenderParameter(String key, String value) {
+ if (this.redirectedUrl != null) {
+ throw new IllegalStateException("Cannot set render parameters after sendRedirect has been called");
+ }
+ super.setRenderParameter(key, value);
+ this.redirectAllowed = false;
+ }
+
+ public void setRenderParameter(String key, String[] values) {
+ if (this.redirectedUrl != null) {
+ throw new IllegalStateException("Cannot set render parameters after sendRedirect has been called");
+ }
+ super.setRenderParameter(key, values);
+ this.redirectAllowed = false;
+ }
+
+ public void sendRedirect(String location) throws IOException {
+ if (!this.redirectAllowed) {
+ throw new IllegalStateException(
+ "Cannot call sendRedirect after windowState, portletMode, or renderParameters have been set");
+ }
+ Assert.notNull(location, "Redirect URL must not be null");
+ this.redirectedUrl = location;
+ }
+
+ public void sendRedirect(String location, String renderUrlParamName) throws IOException {
+ sendRedirect(location);
+ if (renderUrlParamName != null) {
+ setRenderParameter(renderUrlParamName, location);
+ }
+ }
+
+ public String getRedirectedUrl() {
+ return this.redirectedUrl;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockBaseURL.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockBaseURL.java
new file mode 100644
index 00000000..aad99205
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockBaseURL.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.portlet.BaseURL;
+import javax.portlet.PortletSecurityException;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Mock implementation of the {@link javax.portlet.BaseURL} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public abstract class MockBaseURL implements BaseURL {
+
+ public static final String URL_TYPE_RENDER = "render";
+
+ public static final String URL_TYPE_ACTION = "action";
+
+ private static final String ENCODING = "UTF-8";
+
+
+ protected final Map<String, String[]> parameters = new LinkedHashMap<String, String[]>();
+
+ private boolean secure = false;
+
+ private final Map<String, String[]> properties = new LinkedHashMap<String, String[]>();
+
+
+ //---------------------------------------------------------------------
+ // BaseURL methods
+ //---------------------------------------------------------------------
+
+ public void setParameter(String key, String value) {
+ Assert.notNull(key, "Parameter key must be null");
+ Assert.notNull(value, "Parameter value must not be null");
+ this.parameters.put(key, new String[] {value});
+ }
+
+ public void setParameter(String key, String[] values) {
+ Assert.notNull(key, "Parameter key must be null");
+ Assert.notNull(values, "Parameter values must not be null");
+ this.parameters.put(key, values);
+ }
+
+ public void setParameters(Map<String, String[]> parameters) {
+ Assert.notNull(parameters, "Parameters Map must not be null");
+ this.parameters.clear();
+ this.parameters.putAll(parameters);
+ }
+
+ public Set<String> getParameterNames() {
+ return this.parameters.keySet();
+ }
+
+ public String getParameter(String name) {
+ String[] arr = this.parameters.get(name);
+ return (arr != null && arr.length > 0 ? arr[0] : null);
+ }
+
+ public String[] getParameterValues(String name) {
+ return this.parameters.get(name);
+ }
+
+ public Map<String, String[]> getParameterMap() {
+ return Collections.unmodifiableMap(this.parameters);
+ }
+
+ public void setSecure(boolean secure) throws PortletSecurityException {
+ this.secure = secure;
+ }
+
+ public boolean isSecure() {
+ return this.secure;
+ }
+
+ public void write(Writer out) throws IOException {
+ out.write(toString());
+ }
+
+ public void write(Writer out, boolean escapeXML) throws IOException {
+ out.write(toString());
+ }
+
+ public void addProperty(String key, String value) {
+ String[] values = this.properties.get(key);
+ if (values != null) {
+ this.properties.put(key, StringUtils.addStringToArray(values, value));
+ }
+ else {
+ this.properties.put(key, new String[] {value});
+ }
+ }
+
+ public void setProperty(String key, String value) {
+ this.properties.put(key, new String[] {value});
+ }
+
+ public Map<String, String[]> getProperties() {
+ return Collections.unmodifiableMap(this.properties);
+ }
+
+
+ protected String encodeParameter(String name, String value) {
+ try {
+ return URLEncoder.encode(name, ENCODING) + "=" + URLEncoder.encode(value, ENCODING);
+ }
+ catch (UnsupportedEncodingException ex) {
+ return null;
+ }
+ }
+
+ protected String encodeParameter(String name, String[] values) {
+ try {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, n = values.length; i < n; i++) {
+ sb.append(i > 0 ? ";" : "").append(URLEncoder.encode(name, ENCODING)).append("=")
+ .append(URLEncoder.encode(values[i], ENCODING));
+ }
+ return sb.toString();
+ }
+ catch (UnsupportedEncodingException ex) {
+ return null;
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockCacheControl.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockCacheControl.java
new file mode 100644
index 00000000..0ee9fbeb
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockCacheControl.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import javax.portlet.CacheControl;
+
+/**
+ * Mock implementation of the {@link javax.portlet.CacheControl} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public class MockCacheControl implements CacheControl {
+
+ private int expirationTime = 0;
+
+ private boolean publicScope = false;
+
+ private String etag;
+
+ private boolean useCachedContent = false;
+
+
+ public int getExpirationTime() {
+ return this.expirationTime;
+ }
+
+ public void setExpirationTime(int time) {
+ this.expirationTime = time;
+ }
+
+ public boolean isPublicScope() {
+ return this.publicScope;
+ }
+
+ public void setPublicScope(boolean publicScope) {
+ this.publicScope = publicScope;
+ }
+
+ public String getETag() {
+ return this.etag;
+ }
+
+ public void setETag(String token) {
+ this.etag = token;
+ }
+
+ public boolean useCachedContent() {
+ return this.useCachedContent;
+ }
+
+ public void setUseCachedContent(boolean useCachedContent) {
+ this.useCachedContent = useCachedContent;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockClientDataRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockClientDataRequest.java
new file mode 100644
index 00000000..406c263e
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockClientDataRequest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import javax.portlet.ClientDataRequest;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletContext;
+
+/**
+ * Mock implementation of the {@link javax.portlet.ClientDataRequest} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public class MockClientDataRequest extends MockPortletRequest implements ClientDataRequest {
+
+ private String characterEncoding;
+
+ private byte[] content;
+
+ private String contentType;
+
+ private String method;
+
+
+ /**
+ * Create a new MockClientDataRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @see org.springframework.mock.web.portlet.MockPortalContext
+ * @see org.springframework.mock.web.portlet.MockPortletContext
+ */
+ public MockClientDataRequest() {
+ super();
+ }
+
+ /**
+ * Create a new MockClientDataRequest with a default {@link MockPortalContext}.
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockClientDataRequest(PortletContext portletContext) {
+ super(portletContext);
+ }
+
+ /**
+ * Create a new MockClientDataRequest.
+ * @param portalContext the PortalContext that the request runs in
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockClientDataRequest(PortalContext portalContext, PortletContext portletContext) {
+ super(portalContext, portletContext);
+ }
+
+
+ public void setContent(byte[] content) {
+ this.content = content;
+ }
+
+ public InputStream getPortletInputStream() throws IOException {
+ if (this.content != null) {
+ return new ByteArrayInputStream(this.content);
+ }
+ else {
+ return null;
+ }
+ }
+
+ public void setCharacterEncoding(String characterEncoding) {
+ this.characterEncoding = characterEncoding;
+ }
+
+ public BufferedReader getReader() throws UnsupportedEncodingException {
+ if (this.content != null) {
+ InputStream sourceStream = new ByteArrayInputStream(this.content);
+ Reader sourceReader = (this.characterEncoding != null) ?
+ new InputStreamReader(sourceStream, this.characterEncoding) : new InputStreamReader(sourceStream);
+ return new BufferedReader(sourceReader);
+ }
+ else {
+ return null;
+ }
+ }
+
+ public String getCharacterEncoding() {
+ return this.characterEncoding;
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ public String getContentType() {
+ return this.contentType;
+ }
+
+ public int getContentLength() {
+ return (this.content != null ? content.length : -1);
+ }
+
+ public void setMethod(String method) {
+ this.method = method;
+ }
+
+ public String getMethod() {
+ return this.method;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEvent.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEvent.java
new file mode 100644
index 00000000..41e72da9
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEvent.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.io.Serializable;
+import javax.portlet.Event;
+import javax.xml.namespace.QName;
+
+/**
+ * Mock implementation of the {@link javax.portlet.Event} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ * @see MockEventRequest
+ */
+public class MockEvent implements Event {
+
+ private final QName name;
+
+ private final Serializable value;
+
+
+ /**
+ * Create a new MockEvent with the given name.
+ * @param name the name of the event
+ */
+ public MockEvent(QName name) {
+ this.name = name;
+ this.value = null;
+ }
+
+ /**
+ * Create a new MockEvent with the given name and value.
+ * @param name the name of the event
+ * @param value the associated payload of the event
+ */
+ public MockEvent(QName name, Serializable value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ /**
+ * Create a new MockEvent with the given name.
+ * @param name the name of the event
+ */
+ public MockEvent(String name) {
+ this.name = new QName(name);
+ this.value = null;
+ }
+
+ /**
+ * Create a new MockEvent with the given name and value.
+ * @param name the name of the event
+ * @param value the associated payload of the event
+ */
+ public MockEvent(String name, Serializable value) {
+ this.name = new QName(name);
+ this.value = value;
+ }
+
+
+ public QName getQName() {
+ return this.name;
+ }
+
+ public String getName() {
+ return this.name.getLocalPart();
+ }
+
+ public Serializable getValue() {
+ return this.value;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventRequest.java
new file mode 100644
index 00000000..a2850e3c
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventRequest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import javax.portlet.Event;
+import javax.portlet.EventRequest;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletContext;
+
+/**
+ * Mock implementation of the {@link javax.portlet.EventRequest} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public class MockEventRequest extends MockPortletRequest implements EventRequest {
+
+ private final Event event;
+
+ private String method;
+
+
+ /**
+ * Create a new MockEventRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @param event the event that this request wraps
+ * @see MockEvent
+ */
+ public MockEventRequest(Event event) {
+ super();
+ this.event = event;
+ }
+
+ /**
+ * Create a new MockEventRequest with a default {@link MockPortalContext}.
+ * @param event the event that this request wraps
+ * @param portletContext the PortletContext that the request runs in
+ * @see MockEvent
+ */
+ public MockEventRequest(Event event, PortletContext portletContext) {
+ super(portletContext);
+ this.event = event;
+ }
+
+ /**
+ * Create a new MockEventRequest.
+ * @param event the event that this request wraps
+ * @param portalContext the PortletContext that the request runs in
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockEventRequest(Event event, PortalContext portalContext, PortletContext portletContext) {
+ super(portalContext, portletContext);
+ this.event = event;
+ }
+
+
+ @Override
+ protected String getLifecyclePhase() {
+ return EVENT_PHASE;
+ }
+
+ public Event getEvent() {
+ return this.event;
+ }
+
+ public void setMethod(String method) {
+ this.method = method;
+ }
+
+ public String getMethod() {
+ return this.method;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventResponse.java
new file mode 100644
index 00000000..1a00e8b2
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventResponse.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import javax.portlet.EventRequest;
+import javax.portlet.EventResponse;
+
+/**
+ * Mock implementation of the {@link javax.portlet.EventResponse} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public class MockEventResponse extends MockStateAwareResponse implements EventResponse {
+
+ public void setRenderParameters(EventRequest request) {
+ setRenderParameters(request.getParameterMap());
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMimeResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMimeResponse.java
new file mode 100644
index 00000000..4944ab64
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMimeResponse.java
@@ -0,0 +1,240 @@
+/*
+ * 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.mock.web.portlet;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Locale;
+import javax.portlet.CacheControl;
+import javax.portlet.MimeResponse;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletRequest;
+import javax.portlet.PortletURL;
+import javax.portlet.ResourceURL;
+
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.util.WebUtils;
+
+/**
+ * Mock implementation of the {@link javax.portlet.MimeResponse} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public class MockMimeResponse extends MockPortletResponse implements MimeResponse {
+
+ private PortletRequest request;
+
+ private String contentType;
+
+ private String characterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
+
+ private PrintWriter writer;
+
+ private Locale locale = Locale.getDefault();
+
+ private int bufferSize = 4096;
+
+ private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
+
+ private final CacheControl cacheControl = new MockCacheControl();
+
+ private boolean committed;
+
+ private String includedUrl;
+
+ private String forwardedUrl;
+
+
+ /**
+ * Create a new MockMimeResponse with a default {@link MockPortalContext}.
+ * @see org.springframework.mock.web.portlet.MockPortalContext
+ */
+ public MockMimeResponse() {
+ super();
+ }
+
+ /**
+ * Create a new MockMimeResponse.
+ * @param portalContext the PortalContext defining the supported
+ * PortletModes and WindowStates
+ */
+ public MockMimeResponse(PortalContext portalContext) {
+ super(portalContext);
+ }
+
+ /**
+ * Create a new MockMimeResponse.
+ * @param portalContext the PortalContext defining the supported
+ * PortletModes and WindowStates
+ * @param request the corresponding render/resource request that this response
+ * is being generated for
+ */
+ public MockMimeResponse(PortalContext portalContext, PortletRequest request) {
+ super(portalContext);
+ this.request = request;
+ }
+
+
+ //---------------------------------------------------------------------
+ // RenderResponse methods
+ //---------------------------------------------------------------------
+
+ public void setContentType(String contentType) {
+ if (this.request != null) {
+ Enumeration<String> supportedTypes = this.request.getResponseContentTypes();
+ if (!CollectionUtils.contains(supportedTypes, contentType)) {
+ throw new IllegalArgumentException("Content type [" + contentType + "] not in supported list: " +
+ Collections.list(supportedTypes));
+ }
+ }
+ this.contentType = contentType;
+ }
+
+ public String getContentType() {
+ return this.contentType;
+ }
+
+ public void setCharacterEncoding(String characterEncoding) {
+ this.characterEncoding = characterEncoding;
+ }
+
+ public String getCharacterEncoding() {
+ return this.characterEncoding;
+ }
+
+ public PrintWriter getWriter() throws UnsupportedEncodingException {
+ if (this.writer == null) {
+ Writer targetWriter = (this.characterEncoding != null ?
+ new OutputStreamWriter(this.outputStream, this.characterEncoding) :
+ new OutputStreamWriter(this.outputStream));
+ this.writer = new PrintWriter(targetWriter);
+ }
+ return this.writer;
+ }
+
+ public byte[] getContentAsByteArray() {
+ flushBuffer();
+ return this.outputStream.toByteArray();
+ }
+
+ public String getContentAsString() throws UnsupportedEncodingException {
+ flushBuffer();
+ return (this.characterEncoding != null ?
+ this.outputStream.toString(this.characterEncoding) : this.outputStream.toString());
+ }
+
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
+ public Locale getLocale() {
+ return this.locale;
+ }
+
+ public void setBufferSize(int bufferSize) {
+ this.bufferSize = bufferSize;
+ }
+
+ public int getBufferSize() {
+ return this.bufferSize;
+ }
+
+ public void flushBuffer() {
+ if (this.writer != null) {
+ this.writer.flush();
+ }
+ try {
+ this.outputStream.flush();
+ }
+ catch (IOException ex) {
+ throw new IllegalStateException("Could not flush OutputStream: " + ex.getMessage());
+ }
+ this.committed = true;
+ }
+
+ public void resetBuffer() {
+ if (this.committed) {
+ throw new IllegalStateException("Cannot reset buffer - response is already committed");
+ }
+ this.outputStream.reset();
+ }
+
+ public void setCommitted(boolean committed) {
+ this.committed = committed;
+ }
+
+ public boolean isCommitted() {
+ return this.committed;
+ }
+
+ public void reset() {
+ resetBuffer();
+ this.characterEncoding = null;
+ this.contentType = null;
+ this.locale = null;
+ }
+
+ public OutputStream getPortletOutputStream() throws IOException {
+ return this.outputStream;
+ }
+
+ public PortletURL createRenderURL() {
+ return new MockPortletURL(getPortalContext(), MockPortletURL.URL_TYPE_RENDER);
+ }
+
+ public PortletURL createActionURL() {
+ return new MockPortletURL(getPortalContext(), MockPortletURL.URL_TYPE_ACTION);
+ }
+
+ public ResourceURL createResourceURL() {
+ return new MockResourceURL();
+ }
+
+ public CacheControl getCacheControl() {
+ return this.cacheControl;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Methods for MockPortletRequestDispatcher
+ //---------------------------------------------------------------------
+
+ public void setIncludedUrl(String includedUrl) {
+ this.includedUrl = includedUrl;
+ }
+
+ public String getIncludedUrl() {
+ return this.includedUrl;
+ }
+
+ public void setForwardedUrl(String forwardedUrl) {
+ this.forwardedUrl = forwardedUrl;
+ }
+
+ public String getForwardedUrl() {
+ return this.forwardedUrl;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java
new file mode 100644
index 00000000..7df8b547
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2002-2011 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.mock.web.portlet;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.util.Assert;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.portlet.multipart.MultipartActionRequest;
+
+/**
+ * Mock implementation of the
+ * {@link org.springframework.web.portlet.multipart.MultipartActionRequest} interface.
+ *
+ * <p>Useful for testing application controllers that access multipart uploads.
+ * The {@link org.springframework.mock.web.MockMultipartFile} can be used to
+ * populate these mock requests with files.
+ *
+ * @author Juergen Hoeller
+ * @author Arjen Poutsma
+ * @since 2.0
+ * @see org.springframework.mock.web.MockMultipartFile
+ */
+public class MockMultipartActionRequest extends MockActionRequest implements MultipartActionRequest {
+
+ private final MultiValueMap<String, MultipartFile> multipartFiles =
+ new LinkedMultiValueMap<String, MultipartFile>();
+
+
+ /**
+ * Add a file to this request. The parameter name from the multipart
+ * form is taken from the {@link org.springframework.web.multipart.MultipartFile#getName()}.
+ * @param file multipart file to be added
+ */
+ public void addFile(MultipartFile file) {
+ Assert.notNull(file, "MultipartFile must not be null");
+ this.multipartFiles.add(file.getName(), file);
+ }
+
+ public Iterator<String> getFileNames() {
+ return this.multipartFiles.keySet().iterator();
+ }
+
+ public MultipartFile getFile(String name) {
+ return this.multipartFiles.getFirst(name);
+ }
+
+ public List<MultipartFile> getFiles(String name) {
+ List<MultipartFile> multipartFiles = this.multipartFiles.get(name);
+ if (multipartFiles != null) {
+ return multipartFiles;
+ }
+ else {
+ return Collections.emptyList();
+ }
+ }
+
+ public Map<String, MultipartFile> getFileMap() {
+ return this.multipartFiles.toSingleValueMap();
+ }
+
+ public MultiValueMap<String, MultipartFile> getMultiFileMap() {
+ return new LinkedMultiValueMap<String, MultipartFile>(this.multipartFiles);
+ }
+
+ public String getMultipartContentType(String paramOrFileName) {
+ MultipartFile file = getFile(paramOrFileName);
+ if (file != null) {
+ return file.getContentType();
+ }
+ else {
+ return null;
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java
new file mode 100644
index 00000000..949d077b
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletMode;
+import javax.portlet.WindowState;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortalContext} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockPortalContext implements PortalContext {
+
+ private final Map<String, String> properties = new HashMap<String, String>();
+
+ private final List<PortletMode> portletModes;
+
+ private final List<WindowState> windowStates;
+
+
+ /**
+ * Create a new MockPortalContext
+ * with default PortletModes (VIEW, EDIT, HELP)
+ * and default WindowStates (NORMAL, MAXIMIZED, MINIMIZED).
+ * @see javax.portlet.PortletMode
+ * @see javax.portlet.WindowState
+ */
+ public MockPortalContext() {
+ this.portletModes = new ArrayList<PortletMode>(3);
+ this.portletModes.add(PortletMode.VIEW);
+ this.portletModes.add(PortletMode.EDIT);
+ this.portletModes.add(PortletMode.HELP);
+
+ this.windowStates = new ArrayList<WindowState>(3);
+ this.windowStates.add(WindowState.NORMAL);
+ this.windowStates.add(WindowState.MAXIMIZED);
+ this.windowStates.add(WindowState.MINIMIZED);
+ }
+
+ /**
+ * Create a new MockPortalContext with the given PortletModes and WindowStates.
+ * @param supportedPortletModes the List of supported PortletMode instances
+ * @param supportedWindowStates the List of supported WindowState instances
+ * @see javax.portlet.PortletMode
+ * @see javax.portlet.WindowState
+ */
+ public MockPortalContext(List<PortletMode> supportedPortletModes, List<WindowState> supportedWindowStates) {
+ this.portletModes = new ArrayList<PortletMode>(supportedPortletModes);
+ this.windowStates = new ArrayList<WindowState>(supportedWindowStates);
+ }
+
+
+ public String getPortalInfo() {
+ return "MockPortal/1.0";
+ }
+
+ public void setProperty(String name, String value) {
+ this.properties.put(name, value);
+ }
+
+ public String getProperty(String name) {
+ return this.properties.get(name);
+ }
+
+ public Enumeration<String> getPropertyNames() {
+ return Collections.enumeration(this.properties.keySet());
+ }
+
+ public Enumeration<PortletMode> getSupportedPortletModes() {
+ return Collections.enumeration(this.portletModes);
+ }
+
+ public Enumeration<WindowState> getSupportedWindowStates() {
+ return Collections.enumeration(this.windowStates);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java
new file mode 100644
index 00000000..9d3a8780
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java
@@ -0,0 +1,183 @@
+/*
+ * 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.mock.web.portlet;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.Set;
+import javax.portlet.PortletConfig;
+import javax.portlet.PortletContext;
+import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortletConfig} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockPortletConfig implements PortletConfig {
+
+ private final PortletContext portletContext;
+
+ private final String portletName;
+
+ private final Map<Locale, ResourceBundle> resourceBundles = new HashMap<Locale, ResourceBundle>();
+
+ private final Map<String, String> initParameters = new LinkedHashMap<String, String>();
+
+ private final Set<String> publicRenderParameterNames = new LinkedHashSet<String>();
+
+ private String defaultNamespace = XMLConstants.NULL_NS_URI;
+
+ private final Set<QName> publishingEventQNames = new LinkedHashSet<QName>();
+
+ private final Set<QName> processingEventQNames = new LinkedHashSet<QName>();
+
+ private final Set<Locale> supportedLocales = new LinkedHashSet<Locale>();
+
+ private final Map<String, String[]> containerRuntimeOptions = new LinkedHashMap<String, String[]>();
+
+
+ /**
+ * Create a new MockPortletConfig with a default {@link MockPortletContext}.
+ */
+ public MockPortletConfig() {
+ this(null, "");
+ }
+
+ /**
+ * Create a new MockPortletConfig with a default {@link MockPortletContext}.
+ * @param portletName the name of the portlet
+ */
+ public MockPortletConfig(String portletName) {
+ this(null, portletName);
+ }
+
+ /**
+ * Create a new MockPortletConfig.
+ * @param portletContext the PortletContext that the portlet runs in
+ */
+ public MockPortletConfig(PortletContext portletContext) {
+ this(portletContext, "");
+ }
+
+ /**
+ * Create a new MockPortletConfig.
+ * @param portletContext the PortletContext that the portlet runs in
+ * @param portletName the name of the portlet
+ */
+ public MockPortletConfig(PortletContext portletContext, String portletName) {
+ this.portletContext = (portletContext != null ? portletContext : new MockPortletContext());
+ this.portletName = portletName;
+ }
+
+
+ public String getPortletName() {
+ return this.portletName;
+ }
+
+ public PortletContext getPortletContext() {
+ return this.portletContext;
+ }
+
+ public void setResourceBundle(Locale locale, ResourceBundle resourceBundle) {
+ Assert.notNull(locale, "Locale must not be null");
+ this.resourceBundles.put(locale, resourceBundle);
+ }
+
+ public ResourceBundle getResourceBundle(Locale locale) {
+ Assert.notNull(locale, "Locale must not be null");
+ return this.resourceBundles.get(locale);
+ }
+
+ public void addInitParameter(String name, String value) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.initParameters.put(name, value);
+ }
+
+ public String getInitParameter(String name) {
+ Assert.notNull(name, "Parameter name must not be null");
+ return this.initParameters.get(name);
+ }
+
+ public Enumeration<String> getInitParameterNames() {
+ return Collections.enumeration(this.initParameters.keySet());
+ }
+
+ public void addPublicRenderParameterName(String name) {
+ this.publicRenderParameterNames.add(name);
+ }
+
+ public Enumeration<String> getPublicRenderParameterNames() {
+ return Collections.enumeration(this.publicRenderParameterNames);
+ }
+
+ public void setDefaultNamespace(String defaultNamespace) {
+ this.defaultNamespace = defaultNamespace;
+ }
+
+ public String getDefaultNamespace() {
+ return this.defaultNamespace;
+ }
+
+ public void addPublishingEventQName(QName name) {
+ this.publishingEventQNames.add(name);
+ }
+
+ public Enumeration<QName> getPublishingEventQNames() {
+ return Collections.enumeration(this.publishingEventQNames);
+ }
+
+ public void addProcessingEventQName(QName name) {
+ this.processingEventQNames.add(name);
+ }
+
+ public Enumeration<QName> getProcessingEventQNames() {
+ return Collections.enumeration(this.processingEventQNames);
+ }
+
+ public void addSupportedLocale(Locale locale) {
+ this.supportedLocales.add(locale);
+ }
+
+ public Enumeration<Locale> getSupportedLocales() {
+ return Collections.enumeration(this.supportedLocales);
+ }
+
+ public void addContainerRuntimeOption(String key, String value) {
+ this.containerRuntimeOptions.put(key, new String[] {value});
+ }
+
+ public void addContainerRuntimeOption(String key, String[] values) {
+ this.containerRuntimeOptions.put(key, values);
+ }
+
+ public Map<String, String[]> getContainerRuntimeOptions() {
+ return Collections.unmodifiableMap(this.containerRuntimeOptions);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java
new file mode 100644
index 00000000..39dc9f5b
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2002-2013 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.mock.web.portlet;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.activation.FileTypeMap;
+import javax.portlet.PortletContext;
+import javax.portlet.PortletRequestDispatcher;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.util.Assert;
+import org.springframework.web.util.WebUtils;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortletContext} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockPortletContext implements PortletContext {
+
+ private static final String TEMP_DIR_SYSTEM_PROPERTY = "java.io.tmpdir";
+
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final String resourceBasePath;
+
+ private final ResourceLoader resourceLoader;
+
+ private final Map<String, Object> attributes = new LinkedHashMap<String, Object>();
+
+ private final Map<String, String> initParameters = new LinkedHashMap<String, String>();
+
+ private String portletContextName = "MockPortletContext";
+
+ private Set<String> containerRuntimeOptions = new LinkedHashSet<String>();
+
+
+ /**
+ * Create a new MockPortletContext with no base path and a
+ * DefaultResourceLoader (i.e. the classpath root as WAR root).
+ * @see org.springframework.core.io.DefaultResourceLoader
+ */
+ public MockPortletContext() {
+ this("", null);
+ }
+
+ /**
+ * Create a new MockPortletContext using a DefaultResourceLoader.
+ * @param resourceBasePath the WAR root directory (should not end with a slash)
+ * @see org.springframework.core.io.DefaultResourceLoader
+ */
+ public MockPortletContext(String resourceBasePath) {
+ this(resourceBasePath, null);
+ }
+
+ /**
+ * Create a new MockPortletContext, using the specified ResourceLoader
+ * and no base path.
+ * @param resourceLoader the ResourceLoader to use (or null for the default)
+ */
+ public MockPortletContext(ResourceLoader resourceLoader) {
+ this("", resourceLoader);
+ }
+
+ /**
+ * Create a new MockPortletContext.
+ * @param resourceBasePath the WAR root directory (should not end with a slash)
+ * @param resourceLoader the ResourceLoader to use (or null for the default)
+ */
+ public MockPortletContext(String resourceBasePath, ResourceLoader resourceLoader) {
+ this.resourceBasePath = (resourceBasePath != null ? resourceBasePath : "");
+ this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
+
+ // Use JVM temp dir as PortletContext temp dir.
+ String tempDir = System.getProperty(TEMP_DIR_SYSTEM_PROPERTY);
+ if (tempDir != null) {
+ this.attributes.put(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE, new File(tempDir));
+ }
+ }
+
+ /**
+ * Build a full resource location for the given path,
+ * prepending the resource base path of this MockPortletContext.
+ * @param path the path as specified
+ * @return the full resource path
+ */
+ protected String getResourceLocation(String path) {
+ if (!path.startsWith("/")) {
+ path = "/" + path;
+ }
+ return this.resourceBasePath + path;
+ }
+
+
+ public String getServerInfo() {
+ return "MockPortal/1.0";
+ }
+
+ public PortletRequestDispatcher getRequestDispatcher(String path) {
+ if (!path.startsWith("/")) {
+ throw new IllegalArgumentException(
+ "PortletRequestDispatcher path at PortletContext level must start with '/'");
+ }
+ return new MockPortletRequestDispatcher(path);
+ }
+
+ public PortletRequestDispatcher getNamedDispatcher(String path) {
+ return null;
+ }
+
+ public InputStream getResourceAsStream(String path) {
+ Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
+ try {
+ return resource.getInputStream();
+ }
+ catch (IOException ex) {
+ logger.info("Couldn't open InputStream for " + resource, ex);
+ return null;
+ }
+ }
+
+ public int getMajorVersion() {
+ return 2;
+ }
+
+ public int getMinorVersion() {
+ return 0;
+ }
+
+ public String getMimeType(String filePath) {
+ return MimeTypeResolver.getMimeType(filePath);
+ }
+
+ public String getRealPath(String path) {
+ Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
+ try {
+ return resource.getFile().getAbsolutePath();
+ }
+ catch (IOException ex) {
+ logger.info("Couldn't determine real path of resource " + resource, ex);
+ return null;
+ }
+ }
+
+ public Set<String> getResourcePaths(String path) {
+ Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
+ try {
+ File file = resource.getFile();
+ String[] fileList = file.list();
+ String prefix = (path.endsWith("/") ? path : path + "/");
+ Set<String> resourcePaths = new HashSet<String>(fileList.length);
+ for (String fileEntry : fileList) {
+ resourcePaths.add(prefix + fileEntry);
+ }
+ return resourcePaths;
+ }
+ catch (IOException ex) {
+ logger.info("Couldn't get resource paths for " + resource, ex);
+ return null;
+ }
+ }
+
+ public URL getResource(String path) throws MalformedURLException {
+ Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
+ try {
+ return resource.getURL();
+ }
+ catch (IOException ex) {
+ logger.info("Couldn't get URL for " + resource, ex);
+ return null;
+ }
+ }
+
+ public Object getAttribute(String name) {
+ return this.attributes.get(name);
+ }
+
+ public Enumeration<String> getAttributeNames() {
+ return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet()));
+ }
+
+ public void setAttribute(String name, Object value) {
+ if (value != null) {
+ this.attributes.put(name, value);
+ }
+ else {
+ this.attributes.remove(name);
+ }
+ }
+
+ public void removeAttribute(String name) {
+ this.attributes.remove(name);
+ }
+
+ public void addInitParameter(String name, String value) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.initParameters.put(name, value);
+ }
+
+ public String getInitParameter(String name) {
+ Assert.notNull(name, "Parameter name must not be null");
+ return this.initParameters.get(name);
+ }
+
+ public Enumeration<String> getInitParameterNames() {
+ return Collections.enumeration(this.initParameters.keySet());
+ }
+
+ public void log(String message) {
+ logger.info(message);
+ }
+
+ public void log(String message, Throwable t) {
+ logger.info(message, t);
+ }
+
+ public void setPortletContextName(String portletContextName) {
+ this.portletContextName = portletContextName;
+ }
+
+ public String getPortletContextName() {
+ return this.portletContextName;
+ }
+
+ public void addContainerRuntimeOption(String key) {
+ this.containerRuntimeOptions.add(key);
+ }
+
+ public Enumeration<String> getContainerRuntimeOptions() {
+ return Collections.enumeration(this.containerRuntimeOptions);
+ }
+
+
+ /**
+ * Inner factory class used to just introduce a Java Activation Framework
+ * dependency when actually asked to resolve a MIME type.
+ */
+ private static class MimeTypeResolver {
+
+ public static String getMimeType(String filePath) {
+ return FileTypeMap.getDefaultFileTypeMap().getContentType(filePath);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java
new file mode 100644
index 00000000..92ee6c0a
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.portlet.PortletPreferences;
+import javax.portlet.PreferencesValidator;
+import javax.portlet.ReadOnlyException;
+import javax.portlet.ValidatorException;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortletPreferences} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockPortletPreferences implements PortletPreferences {
+
+ private PreferencesValidator preferencesValidator;
+
+ private final Map<String, String[]> preferences = new LinkedHashMap<String, String[]>();
+
+ private final Set<String> readOnly = new HashSet<String>();
+
+
+ public void setReadOnly(String key, boolean readOnly) {
+ Assert.notNull(key, "Key must not be null");
+ if (readOnly) {
+ this.readOnly.add(key);
+ }
+ else {
+ this.readOnly.remove(key);
+ }
+ }
+
+ public boolean isReadOnly(String key) {
+ Assert.notNull(key, "Key must not be null");
+ return this.readOnly.contains(key);
+ }
+
+ public String getValue(String key, String def) {
+ Assert.notNull(key, "Key must not be null");
+ String[] values = this.preferences.get(key);
+ return (values != null && values.length > 0 ? values[0] : def);
+ }
+
+ public String[] getValues(String key, String[] def) {
+ Assert.notNull(key, "Key must not be null");
+ String[] values = this.preferences.get(key);
+ return (values != null && values.length > 0 ? values : def);
+ }
+
+ public void setValue(String key, String value) throws ReadOnlyException {
+ setValues(key, new String[] {value});
+ }
+
+ public void setValues(String key, String[] values) throws ReadOnlyException {
+ Assert.notNull(key, "Key must not be null");
+ if (isReadOnly(key)) {
+ throw new ReadOnlyException("Preference '" + key + "' is read-only");
+ }
+ this.preferences.put(key, values);
+ }
+
+ public Enumeration<String> getNames() {
+ return Collections.enumeration(this.preferences.keySet());
+ }
+
+ public Map<String, String[]> getMap() {
+ return Collections.unmodifiableMap(this.preferences);
+ }
+
+ public void reset(String key) throws ReadOnlyException {
+ Assert.notNull(key, "Key must not be null");
+ if (isReadOnly(key)) {
+ throw new ReadOnlyException("Preference '" + key + "' is read-only");
+ }
+ this.preferences.remove(key);
+ }
+
+ public void setPreferencesValidator(PreferencesValidator preferencesValidator) {
+ this.preferencesValidator = preferencesValidator;
+ }
+
+ public void store() throws IOException, ValidatorException {
+ if (this.preferencesValidator != null) {
+ this.preferencesValidator.validate(this);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java
new file mode 100644
index 00000000..38b51735
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java
@@ -0,0 +1,531 @@
+/*
+ * Copyright 2002-2013 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.mock.web.portlet;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletContext;
+import javax.portlet.PortletMode;
+import javax.portlet.PortletPreferences;
+import javax.portlet.PortletRequest;
+import javax.portlet.PortletSession;
+import javax.portlet.WindowState;
+import javax.servlet.http.Cookie;
+
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortletRequest} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockPortletRequest implements PortletRequest {
+
+ private boolean active = true;
+
+ private final PortalContext portalContext;
+
+ private final PortletContext portletContext;
+
+ private PortletSession session;
+
+ private WindowState windowState = WindowState.NORMAL;
+
+ private PortletMode portletMode = PortletMode.VIEW;
+
+ private PortletPreferences portletPreferences = new MockPortletPreferences();
+
+ private final Map<String, List<String>> properties = new LinkedHashMap<String, List<String>>();
+
+ private final Map<String, Object> attributes = new LinkedHashMap<String, Object>();
+
+ private final Map<String, String[]> parameters = new LinkedHashMap<String, String[]>();
+
+ private String authType = null;
+
+ private String contextPath = "";
+
+ private String remoteUser = null;
+
+ private Principal userPrincipal = null;
+
+ private final Set<String> userRoles = new HashSet<String>();
+
+ private boolean secure = false;
+
+ private boolean requestedSessionIdValid = true;
+
+ private final List<String> responseContentTypes = new LinkedList<String>();
+
+ private final List<Locale> locales = new LinkedList<Locale>();
+
+ private String scheme = "http";
+
+ private String serverName = "localhost";
+
+ private int serverPort = 80;
+
+ private String windowID;
+
+ private Cookie[] cookies;
+
+ private final Set<String> publicParameterNames = new HashSet<String>();
+
+
+ /**
+ * Create a new MockPortletRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ *
+ * @see MockPortalContext
+ * @see MockPortletContext
+ */
+ public MockPortletRequest() {
+ this(null, null);
+ }
+
+ /**
+ * Create a new MockPortletRequest with a default {@link MockPortalContext}.
+ *
+ * @param portletContext the PortletContext that the request runs in
+ * @see MockPortalContext
+ */
+ public MockPortletRequest(PortletContext portletContext) {
+ this(null, portletContext);
+ }
+
+ /**
+ * Create a new MockPortletRequest.
+ *
+ * @param portalContext the PortalContext that the request runs in
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockPortletRequest(PortalContext portalContext, PortletContext portletContext) {
+ this.portalContext = (portalContext != null ? portalContext : new MockPortalContext());
+ this.portletContext = (portletContext != null ? portletContext : new MockPortletContext());
+ this.responseContentTypes.add("text/html");
+ this.locales.add(Locale.ENGLISH);
+ this.attributes.put(LIFECYCLE_PHASE, getLifecyclePhase());
+ }
+
+ // ---------------------------------------------------------------------
+ // Lifecycle methods
+ // ---------------------------------------------------------------------
+
+ /**
+ * Return the Portlet 2.0 lifecycle id for the current phase.
+ */
+ protected String getLifecyclePhase() {
+ return null;
+ }
+
+ /**
+ * Return whether this request is still active (that is, not completed yet).
+ */
+ public boolean isActive() {
+ return this.active;
+ }
+
+ /**
+ * Mark this request as completed.
+ */
+ public void close() {
+ this.active = false;
+ }
+
+ /**
+ * Check whether this request is still active (that is, not completed yet),
+ * throwing an IllegalStateException if not active anymore.
+ */
+ protected void checkActive() throws IllegalStateException {
+ if (!this.active) {
+ throw new IllegalStateException("Request is not active anymore");
+ }
+ }
+
+ // ---------------------------------------------------------------------
+ // PortletRequest methods
+ // ---------------------------------------------------------------------
+
+ public boolean isWindowStateAllowed(WindowState windowState) {
+ return CollectionUtils.contains(this.portalContext.getSupportedWindowStates(), windowState);
+ }
+
+ public boolean isPortletModeAllowed(PortletMode portletMode) {
+ return CollectionUtils.contains(this.portalContext.getSupportedPortletModes(), portletMode);
+ }
+
+ public void setPortletMode(PortletMode portletMode) {
+ Assert.notNull(portletMode, "PortletMode must not be null");
+ this.portletMode = portletMode;
+ }
+
+ public PortletMode getPortletMode() {
+ return this.portletMode;
+ }
+
+ public void setWindowState(WindowState windowState) {
+ Assert.notNull(windowState, "WindowState must not be null");
+ this.windowState = windowState;
+ }
+
+ public WindowState getWindowState() {
+ return this.windowState;
+ }
+
+ public void setPreferences(PortletPreferences preferences) {
+ Assert.notNull(preferences, "PortletPreferences must not be null");
+ this.portletPreferences = preferences;
+ }
+
+ public PortletPreferences getPreferences() {
+ return this.portletPreferences;
+ }
+
+ public void setSession(PortletSession session) {
+ this.session = session;
+ if (session instanceof MockPortletSession) {
+ MockPortletSession mockSession = ((MockPortletSession) session);
+ mockSession.access();
+ }
+ }
+
+ public PortletSession getPortletSession() {
+ return getPortletSession(true);
+ }
+
+ public PortletSession getPortletSession(boolean create) {
+ checkActive();
+ // Reset session if invalidated.
+ if (this.session instanceof MockPortletSession && ((MockPortletSession) this.session).isInvalid()) {
+ this.session = null;
+ }
+ // Create new session if necessary.
+ if (this.session == null && create) {
+ this.session = new MockPortletSession(this.portletContext);
+ }
+ return this.session;
+ }
+
+ /**
+ * Set a single value for the specified property.
+ * <p>
+ * If there are already one or more values registered for the given property
+ * key, they will be replaced.
+ */
+ public void setProperty(String key, String value) {
+ Assert.notNull(key, "Property key must not be null");
+ List<String> list = new LinkedList<String>();
+ list.add(value);
+ this.properties.put(key, list);
+ }
+
+ /**
+ * Add a single value for the specified property.
+ * <p>
+ * If there are already one or more values registered for the given property
+ * key, the given value will be added to the end of the list.
+ */
+ public void addProperty(String key, String value) {
+ Assert.notNull(key, "Property key must not be null");
+ List<String> oldList = this.properties.get(key);
+ if (oldList != null) {
+ oldList.add(value);
+ }
+ else {
+ List<String> list = new LinkedList<String>();
+ list.add(value);
+ this.properties.put(key, list);
+ }
+ }
+
+ public String getProperty(String key) {
+ Assert.notNull(key, "Property key must not be null");
+ List<String> list = this.properties.get(key);
+ return (list != null && list.size() > 0 ? list.get(0) : null);
+ }
+
+ public Enumeration<String> getProperties(String key) {
+ Assert.notNull(key, "property key must not be null");
+ return Collections.enumeration(this.properties.get(key));
+ }
+
+ public Enumeration<String> getPropertyNames() {
+ return Collections.enumeration(this.properties.keySet());
+ }
+
+ public PortalContext getPortalContext() {
+ return this.portalContext;
+ }
+
+ public void setAuthType(String authType) {
+ this.authType = authType;
+ }
+
+ public String getAuthType() {
+ return this.authType;
+ }
+
+ public void setContextPath(String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ public String getContextPath() {
+ return this.contextPath;
+ }
+
+ public void setRemoteUser(String remoteUser) {
+ this.remoteUser = remoteUser;
+ }
+
+ public String getRemoteUser() {
+ return this.remoteUser;
+ }
+
+ public void setUserPrincipal(Principal userPrincipal) {
+ this.userPrincipal = userPrincipal;
+ }
+
+ public Principal getUserPrincipal() {
+ return this.userPrincipal;
+ }
+
+ public void addUserRole(String role) {
+ this.userRoles.add(role);
+ }
+
+ public boolean isUserInRole(String role) {
+ return this.userRoles.contains(role);
+ }
+
+ public Object getAttribute(String name) {
+ checkActive();
+ return this.attributes.get(name);
+ }
+
+ public Enumeration<String> getAttributeNames() {
+ checkActive();
+ return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet()));
+ }
+
+ public void setParameters(Map<String, String[]> parameters) {
+ Assert.notNull(parameters, "Parameters Map must not be null");
+ this.parameters.clear();
+ this.parameters.putAll(parameters);
+ }
+
+ public void setParameter(String key, String value) {
+ Assert.notNull(key, "Parameter key must be null");
+ Assert.notNull(value, "Parameter value must not be null");
+ this.parameters.put(key, new String[] { value });
+ }
+
+ public void setParameter(String key, String[] values) {
+ Assert.notNull(key, "Parameter key must be null");
+ Assert.notNull(values, "Parameter values must not be null");
+ this.parameters.put(key, values);
+ }
+
+ public void addParameter(String name, String value) {
+ addParameter(name, new String[] { value });
+ }
+
+ public void addParameter(String name, String[] values) {
+ String[] oldArr = this.parameters.get(name);
+ if (oldArr != null) {
+ String[] newArr = new String[oldArr.length + values.length];
+ System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
+ System.arraycopy(values, 0, newArr, oldArr.length, values.length);
+ this.parameters.put(name, newArr);
+ }
+ else {
+ this.parameters.put(name, values);
+ }
+ }
+
+ public String getParameter(String name) {
+ String[] arr = this.parameters.get(name);
+ return (arr != null && arr.length > 0 ? arr[0] : null);
+ }
+
+ public Enumeration<String> getParameterNames() {
+ return Collections.enumeration(this.parameters.keySet());
+ }
+
+ public String[] getParameterValues(String name) {
+ return this.parameters.get(name);
+ }
+
+ public Map<String, String[]> getParameterMap() {
+ return Collections.unmodifiableMap(this.parameters);
+ }
+
+ public void setSecure(boolean secure) {
+ this.secure = secure;
+ }
+
+ public boolean isSecure() {
+ return this.secure;
+ }
+
+ public void setAttribute(String name, Object value) {
+ checkActive();
+ if (value != null) {
+ this.attributes.put(name, value);
+ }
+ else {
+ this.attributes.remove(name);
+ }
+ }
+
+ public void removeAttribute(String name) {
+ checkActive();
+ this.attributes.remove(name);
+ }
+
+ public String getRequestedSessionId() {
+ PortletSession session = this.getPortletSession();
+ return (session != null ? session.getId() : null);
+ }
+
+ public void setRequestedSessionIdValid(boolean requestedSessionIdValid) {
+ this.requestedSessionIdValid = requestedSessionIdValid;
+ }
+
+ public boolean isRequestedSessionIdValid() {
+ return this.requestedSessionIdValid;
+ }
+
+ public void addResponseContentType(String responseContentType) {
+ this.responseContentTypes.add(responseContentType);
+ }
+
+ public void addPreferredResponseContentType(String responseContentType) {
+ this.responseContentTypes.add(0, responseContentType);
+ }
+
+ public String getResponseContentType() {
+ return this.responseContentTypes.get(0);
+ }
+
+ public Enumeration<String> getResponseContentTypes() {
+ return Collections.enumeration(this.responseContentTypes);
+ }
+
+ public void addLocale(Locale locale) {
+ this.locales.add(locale);
+ }
+
+ public void addPreferredLocale(Locale locale) {
+ this.locales.add(0, locale);
+ }
+
+ public Locale getLocale() {
+ return this.locales.get(0);
+ }
+
+ public Enumeration<Locale> getLocales() {
+ return Collections.enumeration(this.locales);
+ }
+
+ public void setScheme(String scheme) {
+ this.scheme = scheme;
+ }
+
+ public String getScheme() {
+ return this.scheme;
+ }
+
+ public void setServerName(String serverName) {
+ this.serverName = serverName;
+ }
+
+ public String getServerName() {
+ return this.serverName;
+ }
+
+ public void setServerPort(int serverPort) {
+ this.serverPort = serverPort;
+ }
+
+ public int getServerPort() {
+ return this.serverPort;
+ }
+
+ public void setWindowID(String windowID) {
+ this.windowID = windowID;
+ }
+
+ public String getWindowID() {
+ return this.windowID;
+ }
+
+ public void setCookies(Cookie... cookies) {
+ this.cookies = cookies;
+ }
+
+ public Cookie[] getCookies() {
+ return this.cookies;
+ }
+
+ public Map<String, String[]> getPrivateParameterMap() {
+ if (!this.publicParameterNames.isEmpty()) {
+ Map<String, String[]> filtered = new LinkedHashMap<String, String[]>();
+ for (String key : this.parameters.keySet()) {
+ if (!this.publicParameterNames.contains(key)) {
+ filtered.put(key, this.parameters.get(key));
+ }
+ }
+ return filtered;
+ }
+ else {
+ return Collections.unmodifiableMap(this.parameters);
+ }
+ }
+
+ public Map<String, String[]> getPublicParameterMap() {
+ if (!this.publicParameterNames.isEmpty()) {
+ Map<String, String[]> filtered = new LinkedHashMap<String, String[]>();
+ for (String key : this.parameters.keySet()) {
+ if (this.publicParameterNames.contains(key)) {
+ filtered.put(key, this.parameters.get(key));
+ }
+ }
+ return filtered;
+ }
+ else {
+ return Collections.emptyMap();
+ }
+ }
+
+ public void registerPublicParameter(String name) {
+ this.publicParameterNames.add(name);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java
new file mode 100644
index 00000000..ca8695f9
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.io.IOException;
+import javax.portlet.PortletException;
+import javax.portlet.PortletRequest;
+import javax.portlet.PortletRequestDispatcher;
+import javax.portlet.PortletResponse;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortletRequestDispatcher} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockPortletRequestDispatcher implements PortletRequestDispatcher {
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final String url;
+
+
+ /**
+ * Create a new MockPortletRequestDispatcher for the given URL.
+ * @param url the URL to dispatch to.
+ */
+ public MockPortletRequestDispatcher(String url) {
+ Assert.notNull(url, "URL must not be null");
+ this.url = url;
+ }
+
+
+ public void include(RenderRequest request, RenderResponse response) throws PortletException, IOException {
+ include((PortletRequest) request, (PortletResponse) response);
+ }
+
+ public void include(PortletRequest request, PortletResponse response) throws PortletException, IOException {
+ Assert.notNull(request, "Request must not be null");
+ Assert.notNull(response, "Response must not be null");
+ if (!(response instanceof MockMimeResponse)) {
+ throw new IllegalArgumentException("MockPortletRequestDispatcher requires MockMimeResponse");
+ }
+ ((MockMimeResponse) response).setIncludedUrl(this.url);
+ if (logger.isDebugEnabled()) {
+ logger.debug("MockPortletRequestDispatcher: including URL [" + this.url + "]");
+ }
+ }
+
+ public void forward(PortletRequest request, PortletResponse response) throws PortletException, IOException {
+ Assert.notNull(request, "Request must not be null");
+ Assert.notNull(response, "Response must not be null");
+ if (!(response instanceof MockMimeResponse)) {
+ throw new IllegalArgumentException("MockPortletRequestDispatcher requires MockMimeResponse");
+ }
+ ((MockMimeResponse) response).setForwardedUrl(this.url);
+ if (logger.isDebugEnabled()) {
+ logger.debug("MockPortletRequestDispatcher: forwarding to URL [" + this.url + "]");
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java
new file mode 100644
index 00000000..b556209f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletResponse;
+import javax.servlet.http.Cookie;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortletResponse} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockPortletResponse implements PortletResponse {
+
+ private final PortalContext portalContext;
+
+ private final Map<String, String[]> properties = new LinkedHashMap<String, String[]>();
+
+ private String namespace = "";
+
+ private final Set<Cookie> cookies = new LinkedHashSet<Cookie>();
+
+ private final Map<String, Element[]> xmlProperties = new LinkedHashMap<String, Element[]>();
+
+ private Document xmlDocument;
+
+
+ /**
+ * Create a new MockPortletResponse with a default {@link MockPortalContext}.
+ * @see MockPortalContext
+ */
+ public MockPortletResponse() {
+ this(null);
+ }
+
+ /**
+ * Create a new MockPortletResponse.
+ * @param portalContext the PortalContext defining the supported
+ * PortletModes and WindowStates
+ */
+ public MockPortletResponse(PortalContext portalContext) {
+ this.portalContext = (portalContext != null ? portalContext : new MockPortalContext());
+ }
+
+ /**
+ * Return the PortalContext that this MockPortletResponse runs in,
+ * defining the supported PortletModes and WindowStates.
+ */
+ public PortalContext getPortalContext() {
+ return this.portalContext;
+ }
+
+
+ //---------------------------------------------------------------------
+ // PortletResponse methods
+ //---------------------------------------------------------------------
+
+ public void addProperty(String key, String value) {
+ Assert.notNull(key, "Property key must not be null");
+ String[] oldArr = this.properties.get(key);
+ if (oldArr != null) {
+ String[] newArr = new String[oldArr.length + 1];
+ System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
+ newArr[oldArr.length] = value;
+ this.properties.put(key, newArr);
+ }
+ else {
+ this.properties.put(key, new String[] {value});
+ }
+ }
+
+ public void setProperty(String key, String value) {
+ Assert.notNull(key, "Property key must not be null");
+ this.properties.put(key, new String[] {value});
+ }
+
+ public Set<String> getPropertyNames() {
+ return Collections.unmodifiableSet(this.properties.keySet());
+ }
+
+ public String getProperty(String key) {
+ Assert.notNull(key, "Property key must not be null");
+ String[] arr = this.properties.get(key);
+ return (arr != null && arr.length > 0 ? arr[0] : null);
+ }
+
+ public String[] getProperties(String key) {
+ Assert.notNull(key, "Property key must not be null");
+ return this.properties.get(key);
+ }
+
+ public String encodeURL(String path) {
+ return path;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ public String getNamespace() {
+ return this.namespace;
+ }
+
+ public void addProperty(Cookie cookie) {
+ Assert.notNull(cookie, "Cookie must not be null");
+ this.cookies.add(cookie);
+ }
+
+ public Cookie[] getCookies() {
+ return this.cookies.toArray(new Cookie[this.cookies.size()]);
+ }
+
+ public Cookie getCookie(String name) {
+ Assert.notNull(name, "Cookie name must not be null");
+ for (Cookie cookie : this.cookies) {
+ if (name.equals(cookie.getName())) {
+ return cookie;
+ }
+ }
+ return null;
+ }
+
+ public void addProperty(String key, Element value) {
+ Assert.notNull(key, "Property key must not be null");
+ Element[] oldArr = this.xmlProperties.get(key);
+ if (oldArr != null) {
+ Element[] newArr = new Element[oldArr.length + 1];
+ System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
+ newArr[oldArr.length] = value;
+ this.xmlProperties.put(key, newArr);
+ }
+ else {
+ this.xmlProperties.put(key, new Element[] {value});
+ }
+ }
+
+
+ public Set<String> getXmlPropertyNames() {
+ return Collections.unmodifiableSet(this.xmlProperties.keySet());
+ }
+
+ public Element getXmlProperty(String key) {
+ Assert.notNull(key, "Property key must not be null");
+ Element[] arr = this.xmlProperties.get(key);
+ return (arr != null && arr.length > 0 ? arr[0] : null);
+ }
+
+ public Element[] getXmlProperties(String key) {
+ Assert.notNull(key, "Property key must not be null");
+ return this.xmlProperties.get(key);
+ }
+
+ public Element createElement(String tagName) throws DOMException {
+ if (this.xmlDocument == null) {
+ try {
+ this.xmlDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+ }
+ catch (ParserConfigurationException ex) {
+ throw new DOMException(DOMException.INVALID_STATE_ERR, ex.toString());
+ }
+ }
+ return this.xmlDocument.createElement(tagName);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java
new file mode 100644
index 00000000..5eaec799
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2002-2013 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.mock.web.portlet;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import javax.portlet.PortletContext;
+import javax.portlet.PortletSession;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+
+import org.springframework.mock.web.MockHttpSession;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortletSession} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockPortletSession implements PortletSession {
+
+ private static int nextId = 1;
+
+
+ private final String id = Integer.toString(nextId++);
+
+ private final long creationTime = System.currentTimeMillis();
+
+ private int maxInactiveInterval;
+
+ private long lastAccessedTime = System.currentTimeMillis();
+
+ private final PortletContext portletContext;
+
+ private final Map<String, Object> portletAttributes = new LinkedHashMap<String, Object>();
+
+ private final Map<String, Object> applicationAttributes = new LinkedHashMap<String, Object>();
+
+ private boolean invalid = false;
+
+ private boolean isNew = true;
+
+
+ /**
+ * Create a new MockPortletSession with a default {@link MockPortletContext}.
+ * @see MockPortletContext
+ */
+ public MockPortletSession() {
+ this(null);
+ }
+
+ /**
+ * Create a new MockPortletSession.
+ * @param portletContext the PortletContext that the session runs in
+ */
+ public MockPortletSession(PortletContext portletContext) {
+ this.portletContext = (portletContext != null ? portletContext : new MockPortletContext());
+ }
+
+
+ public Object getAttribute(String name) {
+ return this.portletAttributes.get(name);
+ }
+
+ public Object getAttribute(String name, int scope) {
+ if (scope == PortletSession.PORTLET_SCOPE) {
+ return this.portletAttributes.get(name);
+ }
+ else if (scope == PortletSession.APPLICATION_SCOPE) {
+ return this.applicationAttributes.get(name);
+ }
+ return null;
+ }
+
+ public Enumeration<String> getAttributeNames() {
+ return Collections.enumeration(new LinkedHashSet<String>(this.portletAttributes.keySet()));
+ }
+
+ public Enumeration<String> getAttributeNames(int scope) {
+ if (scope == PortletSession.PORTLET_SCOPE) {
+ return Collections.enumeration(new LinkedHashSet<String>(this.portletAttributes.keySet()));
+ }
+ else if (scope == PortletSession.APPLICATION_SCOPE) {
+ return Collections.enumeration(new LinkedHashSet<String>(this.applicationAttributes.keySet()));
+ }
+ return null;
+ }
+
+ public long getCreationTime() {
+ return this.creationTime;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void access() {
+ this.lastAccessedTime = System.currentTimeMillis();
+ setNew(false);
+ }
+
+ public long getLastAccessedTime() {
+ return this.lastAccessedTime;
+ }
+
+ public int getMaxInactiveInterval() {
+ return this.maxInactiveInterval;
+ }
+
+ /**
+ * Clear all of this session's attributes.
+ */
+ public void clearAttributes() {
+ doClearAttributes(this.portletAttributes);
+ doClearAttributes(this.applicationAttributes);
+ }
+
+ protected void doClearAttributes(Map<String, Object> attributes) {
+ for (Iterator<Map.Entry<String, Object>> it = attributes.entrySet().iterator(); it.hasNext();) {
+ Map.Entry<String, Object> entry = it.next();
+ String name = entry.getKey();
+ Object value = entry.getValue();
+ it.remove();
+ if (value instanceof HttpSessionBindingListener) {
+ ((HttpSessionBindingListener) value).valueUnbound(
+ new HttpSessionBindingEvent(new MockHttpSession(), name, value));
+ }
+ }
+ }
+
+ public void invalidate() {
+ this.invalid = true;
+ clearAttributes();
+ }
+
+ public boolean isInvalid() {
+ return this.invalid;
+ }
+
+ public void setNew(boolean value) {
+ this.isNew = value;
+ }
+
+ public boolean isNew() {
+ return this.isNew;
+ }
+
+ public void removeAttribute(String name) {
+ this.portletAttributes.remove(name);
+ }
+
+ public void removeAttribute(String name, int scope) {
+ if (scope == PortletSession.PORTLET_SCOPE) {
+ this.portletAttributes.remove(name);
+ }
+ else if (scope == PortletSession.APPLICATION_SCOPE) {
+ this.applicationAttributes.remove(name);
+ }
+ }
+
+ public void setAttribute(String name, Object value) {
+ if (value != null) {
+ this.portletAttributes.put(name, value);
+ }
+ else {
+ this.portletAttributes.remove(name);
+ }
+ }
+
+ public void setAttribute(String name, Object value, int scope) {
+ if (scope == PortletSession.PORTLET_SCOPE) {
+ if (value != null) {
+ this.portletAttributes.put(name, value);
+ }
+ else {
+ this.portletAttributes.remove(name);
+ }
+ }
+ else if (scope == PortletSession.APPLICATION_SCOPE) {
+ if (value != null) {
+ this.applicationAttributes.put(name, value);
+ }
+ else {
+ this.applicationAttributes.remove(name);
+ }
+ }
+ }
+
+ public void setMaxInactiveInterval(int interval) {
+ this.maxInactiveInterval = interval;
+ }
+
+ public PortletContext getPortletContext() {
+ return this.portletContext;
+ }
+
+ public Map<String, Object> getAttributeMap() {
+ return Collections.unmodifiableMap(this.portletAttributes);
+ }
+
+ public Map<String, Object> getAttributeMap(int scope) {
+ if (scope == PortletSession.PORTLET_SCOPE) {
+ return Collections.unmodifiableMap(this.portletAttributes);
+ }
+ else if (scope == PortletSession.APPLICATION_SCOPE) {
+ return Collections.unmodifiableMap(this.applicationAttributes);
+ }
+ else {
+ return Collections.emptyMap();
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java
new file mode 100644
index 00000000..12abdf57
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.util.Map;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletMode;
+import javax.portlet.PortletModeException;
+import javax.portlet.PortletURL;
+import javax.portlet.WindowState;
+import javax.portlet.WindowStateException;
+
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortletURL} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockPortletURL extends MockBaseURL implements PortletURL {
+
+ public static final String URL_TYPE_RENDER = "render";
+
+ public static final String URL_TYPE_ACTION = "action";
+
+
+ private final PortalContext portalContext;
+
+ private final String urlType;
+
+ private WindowState windowState;
+
+ private PortletMode portletMode;
+
+
+ /**
+ * Create a new MockPortletURL for the given URL type.
+ * @param portalContext the PortalContext defining the supported
+ * PortletModes and WindowStates
+ * @param urlType the URL type, for example "render" or "action"
+ * @see #URL_TYPE_RENDER
+ * @see #URL_TYPE_ACTION
+ */
+ public MockPortletURL(PortalContext portalContext, String urlType) {
+ Assert.notNull(portalContext, "PortalContext is required");
+ this.portalContext = portalContext;
+ this.urlType = urlType;
+ }
+
+
+ //---------------------------------------------------------------------
+ // PortletURL methods
+ //---------------------------------------------------------------------
+
+ public void setWindowState(WindowState windowState) throws WindowStateException {
+ if (!CollectionUtils.contains(this.portalContext.getSupportedWindowStates(), windowState)) {
+ throw new WindowStateException("WindowState not supported", windowState);
+ }
+ this.windowState = windowState;
+ }
+
+ public WindowState getWindowState() {
+ return this.windowState;
+ }
+
+ public void setPortletMode(PortletMode portletMode) throws PortletModeException {
+ if (!CollectionUtils.contains(this.portalContext.getSupportedPortletModes(), portletMode)) {
+ throw new PortletModeException("PortletMode not supported", portletMode);
+ }
+ this.portletMode = portletMode;
+ }
+
+ public PortletMode getPortletMode() {
+ return this.portletMode;
+ }
+
+ public void removePublicRenderParameter(String name) {
+ this.parameters.remove(name);
+ }
+
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(encodeParameter("urlType", this.urlType));
+ if (this.windowState != null) {
+ sb.append(";").append(encodeParameter("windowState", this.windowState.toString()));
+ }
+ if (this.portletMode != null) {
+ sb.append(";").append(encodeParameter("portletMode", this.portletMode.toString()));
+ }
+ for (Map.Entry<String, String[]> entry : this.parameters.entrySet()) {
+ sb.append(";").append(encodeParameter("param_" + entry.getKey(), entry.getValue()));
+ }
+ return (isSecure() ? "https:" : "http:") +
+ "//localhost/mockportlet?" + sb.toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java
new file mode 100644
index 00000000..d86ba7f2
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import javax.portlet.PortalContext;
+import javax.portlet.PortletContext;
+import javax.portlet.PortletMode;
+import javax.portlet.RenderRequest;
+import javax.portlet.WindowState;
+
+/**
+ * Mock implementation of the {@link javax.portlet.RenderRequest} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockRenderRequest extends MockPortletRequest implements RenderRequest {
+
+ /**
+ * Create a new MockRenderRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @see MockPortalContext
+ * @see MockPortletContext
+ */
+ public MockRenderRequest() {
+ super();
+ }
+
+ /**
+ * Create a new MockRenderRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @param portletMode the mode that the portlet runs in
+ */
+ public MockRenderRequest(PortletMode portletMode) {
+ super();
+ setPortletMode(portletMode);
+ }
+
+ /**
+ * Create a new MockRenderRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @param portletMode the mode that the portlet runs in
+ * @param windowState the window state to run the portlet in
+ */
+ public MockRenderRequest(PortletMode portletMode, WindowState windowState) {
+ super();
+ setPortletMode(portletMode);
+ setWindowState(windowState);
+ }
+
+ /**
+ * Create a new MockRenderRequest with a default {@link MockPortalContext}.
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockRenderRequest(PortletContext portletContext) {
+ super(portletContext);
+ }
+
+ /**
+ * Create a new MockRenderRequest.
+ * @param portalContext the PortletContext that the request runs in
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockRenderRequest(PortalContext portalContext, PortletContext portletContext) {
+ super(portalContext, portletContext);
+ }
+
+
+ @Override
+ protected String getLifecyclePhase() {
+ return RENDER_PHASE;
+ }
+
+ public String getETag() {
+ return getProperty(RenderRequest.ETAG);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java
new file mode 100644
index 00000000..a2feeb37
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.util.Collection;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletMode;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+
+/**
+ * Mock implementation of the {@link javax.portlet.RenderResponse} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockRenderResponse extends MockMimeResponse implements RenderResponse {
+
+ private String title;
+
+ private Collection<PortletMode> nextPossiblePortletModes;
+
+
+ /**
+ * Create a new MockRenderResponse with a default {@link MockPortalContext}.
+ * @see MockPortalContext
+ */
+ public MockRenderResponse() {
+ super();
+ }
+
+ /**
+ * Create a new MockRenderResponse.
+ * @param portalContext the PortalContext defining the supported
+ * PortletModes and WindowStates
+ */
+ public MockRenderResponse(PortalContext portalContext) {
+ super(portalContext);
+ }
+
+ /**
+ * Create a new MockRenderResponse.
+ * @param portalContext the PortalContext defining the supported
+ * PortletModes and WindowStates
+ * @param request the corresponding render request that this response
+ * is generated for
+ */
+ public MockRenderResponse(PortalContext portalContext, RenderRequest request) {
+ super(portalContext, request);
+ }
+
+
+ //---------------------------------------------------------------------
+ // RenderResponse methods
+ //---------------------------------------------------------------------
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return this.title;
+ }
+
+ public void setNextPossiblePortletModes(Collection<PortletMode> portletModes) {
+ this.nextPossiblePortletModes = portletModes;
+ }
+
+ public Collection<PortletMode> getNextPossiblePortletModes() {
+ return this.nextPossiblePortletModes;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceRequest.java
new file mode 100644
index 00000000..7cce4ba3
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceRequest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletContext;
+import javax.portlet.RenderRequest;
+import javax.portlet.ResourceRequest;
+
+/**
+ * Mock implementation of the {@link javax.portlet.ResourceRequest} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public class MockResourceRequest extends MockClientDataRequest implements ResourceRequest {
+
+ private String resourceID;
+
+ private String cacheability;
+
+ private final Map<String, String[]> privateRenderParameterMap = new LinkedHashMap<String, String[]>();
+
+
+ /**
+ * Create a new MockResourceRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @see org.springframework.mock.web.portlet.MockPortalContext
+ * @see org.springframework.mock.web.portlet.MockPortletContext
+ */
+ public MockResourceRequest() {
+ super();
+ }
+
+ /**
+ * Create a new MockResourceRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @param resourceID the resource id for this request
+ */
+ public MockResourceRequest(String resourceID) {
+ super();
+ this.resourceID = resourceID;
+ }
+
+ /**
+ * Create a new MockResourceRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @param url the resource URL for this request
+ */
+ public MockResourceRequest(MockResourceURL url) {
+ super();
+ this.resourceID = url.getResourceID();
+ this.cacheability = url.getCacheability();
+ }
+
+ /**
+ * Create a new MockResourceRequest with a default {@link MockPortalContext}.
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockResourceRequest(PortletContext portletContext) {
+ super(portletContext);
+ }
+
+ /**
+ * Create a new MockResourceRequest.
+ * @param portalContext the PortalContext that the request runs in
+ * @param portletContext the PortletContext that the request runs in
+ */
+ public MockResourceRequest(PortalContext portalContext, PortletContext portletContext) {
+ super(portalContext, portletContext);
+ }
+
+
+ @Override
+ protected String getLifecyclePhase() {
+ return RESOURCE_PHASE;
+ }
+
+ public void setResourceID(String resourceID) {
+ this.resourceID = resourceID;
+ }
+
+ public String getResourceID() {
+ return this.resourceID;
+ }
+
+ public void setCacheability(String cacheLevel) {
+ this.cacheability = cacheLevel;
+ }
+
+ public String getCacheability() {
+ return this.cacheability;
+ }
+
+ public String getETag() {
+ return getProperty(RenderRequest.ETAG);
+ }
+
+ public void addPrivateRenderParameter(String key, String value) {
+ this.privateRenderParameterMap.put(key, new String[] {value});
+ }
+
+ public void addPrivateRenderParameter(String key, String[] values) {
+ this.privateRenderParameterMap.put(key, values);
+ }
+
+ public Map<String, String[]> getPrivateRenderParameterMap() {
+ return Collections.unmodifiableMap(this.privateRenderParameterMap);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceResponse.java
new file mode 100644
index 00000000..297a1968
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceResponse.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import javax.portlet.ResourceResponse;
+
+/**
+ * Mock implementation of the {@link javax.portlet.ResourceResponse} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public class MockResourceResponse extends MockMimeResponse implements ResourceResponse {
+
+ private int contentLength = 0;
+
+
+ public void setContentLength(int len) {
+ this.contentLength = len;
+ }
+
+ public int getContentLength() {
+ return this.contentLength;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceURL.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceURL.java
new file mode 100644
index 00000000..cce36b18
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceURL.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.util.Map;
+import javax.portlet.ResourceURL;
+
+/**
+ * Mock implementation of the {@link javax.portlet.ResourceURL} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public class MockResourceURL extends MockBaseURL implements ResourceURL {
+
+ private String resourceID;
+
+ private String cacheability;
+
+
+ //---------------------------------------------------------------------
+ // ResourceURL methods
+ //---------------------------------------------------------------------
+
+ public void setResourceID(String resourceID) {
+ this.resourceID = resourceID;
+ }
+
+ public String getResourceID() {
+ return this.resourceID;
+ }
+
+ public void setCacheability(String cacheLevel) {
+ this.cacheability = cacheLevel;
+ }
+
+ public String getCacheability() {
+ return this.cacheability;
+ }
+
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(encodeParameter("resourceID", this.resourceID));
+ if (this.cacheability != null) {
+ sb.append(";").append(encodeParameter("cacheability", this.cacheability));
+ }
+ for (Map.Entry<String, String[]> entry : this.parameters.entrySet()) {
+ sb.append(";").append(encodeParameter("param_" + entry.getKey(), entry.getValue()));
+ }
+ return (isSecure() ? "https:" : "http:") +
+ "//localhost/mockportlet?" + sb.toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockStateAwareResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockStateAwareResponse.java
new file mode 100644
index 00000000..95df6ed0
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockStateAwareResponse.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletMode;
+import javax.portlet.PortletModeException;
+import javax.portlet.StateAwareResponse;
+import javax.portlet.WindowState;
+import javax.portlet.WindowStateException;
+import javax.xml.namespace.QName;
+
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Mock implementation of the {@link javax.portlet.StateAwareResponse} interface.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ */
+public class MockStateAwareResponse extends MockPortletResponse implements StateAwareResponse {
+
+ private WindowState windowState;
+
+ private PortletMode portletMode;
+
+ private final Map<String, String[]> renderParameters = new LinkedHashMap<String, String[]>();
+
+ private final Map<QName, Serializable> events = new HashMap<QName, Serializable>();
+
+
+ /**
+ * Create a new MockActionResponse with a default {@link MockPortalContext}.
+ * @see org.springframework.mock.web.portlet.MockPortalContext
+ */
+ public MockStateAwareResponse() {
+ super();
+ }
+
+ /**
+ * Create a new MockActionResponse.
+ * @param portalContext the PortalContext defining the supported
+ * PortletModes and WindowStates
+ */
+ public MockStateAwareResponse(PortalContext portalContext) {
+ super(portalContext);
+ }
+
+
+ public void setWindowState(WindowState windowState) throws WindowStateException {
+ if (!CollectionUtils.contains(getPortalContext().getSupportedWindowStates(), windowState)) {
+ throw new WindowStateException("WindowState not supported", windowState);
+ }
+ this.windowState = windowState;
+ }
+
+ public WindowState getWindowState() {
+ return this.windowState;
+ }
+
+ public void setPortletMode(PortletMode portletMode) throws PortletModeException {
+ if (!CollectionUtils.contains(getPortalContext().getSupportedPortletModes(), portletMode)) {
+ throw new PortletModeException("PortletMode not supported", portletMode);
+ }
+ this.portletMode = portletMode;
+ }
+
+ public PortletMode getPortletMode() {
+ return this.portletMode;
+ }
+
+ public void setRenderParameters(Map<String, String[]> parameters) {
+ Assert.notNull(parameters, "Parameters Map must not be null");
+ this.renderParameters.clear();
+ this.renderParameters.putAll(parameters);
+ }
+
+ public void setRenderParameter(String key, String value) {
+ Assert.notNull(key, "Parameter key must not be null");
+ Assert.notNull(value, "Parameter value must not be null");
+ this.renderParameters.put(key, new String[] {value});
+ }
+
+ public void setRenderParameter(String key, String[] values) {
+ Assert.notNull(key, "Parameter key must not be null");
+ Assert.notNull(values, "Parameter values must not be null");
+ this.renderParameters.put(key, values);
+ }
+
+ public String getRenderParameter(String key) {
+ Assert.notNull(key, "Parameter key must not be null");
+ String[] arr = this.renderParameters.get(key);
+ return (arr != null && arr.length > 0 ? arr[0] : null);
+ }
+
+ public String[] getRenderParameterValues(String key) {
+ Assert.notNull(key, "Parameter key must not be null");
+ return this.renderParameters.get(key);
+ }
+
+ public Iterator<String> getRenderParameterNames() {
+ return this.renderParameters.keySet().iterator();
+ }
+
+ public Map<String, String[]> getRenderParameterMap() {
+ return Collections.unmodifiableMap(this.renderParameters);
+ }
+
+ public void removePublicRenderParameter(String name) {
+ this.renderParameters.remove(name);
+ }
+
+ public void setEvent(QName name, Serializable value) {
+ this.events.put(name, value);
+ }
+
+ public void setEvent(String name, Serializable value) {
+ this.events.put(new QName(name), value);
+ }
+
+ public Iterator<QName> getEventNames() {
+ return this.events.keySet().iterator();
+ }
+
+ public Serializable getEvent(QName name) {
+ return this.events.get(name);
+ }
+
+ public Serializable getEvent(String name) {
+ return this.events.get(new QName(name));
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java
new file mode 100644
index 00000000..8a88066c
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2002-2009 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.mock.web.portlet;
+
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+import javax.portlet.PortletContext;
+import javax.portlet.PortletRequestDispatcher;
+import javax.servlet.ServletContext;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.portlet.PortletContext} interface,
+ * wrapping an underlying {@link javax.servlet.ServletContext}.
+ *
+ * @author Juergen Hoeller
+ * @since 3.0
+ * @see MockPortletContext
+ */
+public class ServletWrappingPortletContext implements PortletContext {
+
+ private final ServletContext servletContext;
+
+
+ /**
+ * Create a new PortletContext wrapping the given ServletContext.
+ * @param servletContext the ServletContext to wrap
+ */
+ public ServletWrappingPortletContext(ServletContext servletContext) {
+ Assert.notNull(servletContext, "ServletContext must not be null");
+ this.servletContext = servletContext;
+ }
+
+ /**
+ * Return the underlying ServletContext that this PortletContext wraps.
+ */
+ public final ServletContext getServletContext() {
+ return this.servletContext;
+ }
+
+
+ public String getServerInfo() {
+ return this.servletContext.getServerInfo();
+ }
+
+ public PortletRequestDispatcher getRequestDispatcher(String path) {
+ return null;
+ }
+
+ public PortletRequestDispatcher getNamedDispatcher(String name) {
+ return null;
+ }
+
+ public InputStream getResourceAsStream(String path) {
+ return this.servletContext.getResourceAsStream(path);
+ }
+
+ public int getMajorVersion() {
+ return 2;
+ }
+
+ public int getMinorVersion() {
+ return 0;
+ }
+
+ public String getMimeType(String file) {
+ return this.servletContext.getMimeType(file);
+ }
+
+ public String getRealPath(String path) {
+ return this.servletContext.getRealPath(path);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Set<String> getResourcePaths(String path) {
+ return this.servletContext.getResourcePaths(path);
+ }
+
+ public URL getResource(String path) throws MalformedURLException {
+ return this.servletContext.getResource(path);
+ }
+
+ public Object getAttribute(String name) {
+ return this.servletContext.getAttribute(name);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Enumeration<String> getAttributeNames() {
+ return this.servletContext.getAttributeNames();
+ }
+
+ public String getInitParameter(String name) {
+ return this.servletContext.getInitParameter(name);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Enumeration<String> getInitParameterNames() {
+ return this.servletContext.getInitParameterNames();
+ }
+
+ public void log(String msg) {
+ this.servletContext.log(msg);
+ }
+
+ public void log(String message, Throwable throwable) {
+ this.servletContext.log(message, throwable);
+ }
+
+ public void removeAttribute(String name) {
+ this.servletContext.removeAttribute(name);
+ }
+
+ public void setAttribute(String name, Object object) {
+ this.servletContext.setAttribute(name, object);
+ }
+
+ public String getPortletContextName() {
+ return this.servletContext.getServletContextName();
+ }
+
+ public Enumeration<String> getContainerRuntimeOptions() {
+ return Collections.enumeration(new HashSet<String>());
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/package-info.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/package-info.java
new file mode 100644
index 00000000..3c28fee4
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/package-info.java
@@ -0,0 +1,14 @@
+
+/**
+ *
+ * A comprehensive set of Portlet API 2.0 mock objects,
+ * targeted at usage with Spring's web MVC framework.
+ * Useful for testing web contexts and controllers.
+ *
+ * <p>More convenient to use than dynamic mock objects
+ * (<a href="http://www.easymock.org">EasyMock</a>) or
+ * existing Portlet API mock objects.
+ *
+ */
+package org.springframework.mock.web.portlet;
+
diff --git a/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java
new file mode 100644
index 00000000..6229a8d3
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java
@@ -0,0 +1,294 @@
+/*
+ * 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;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.util.Assert;
+
+/**
+ * <p>
+ * Convenient superclass for JUnit 3.8 based tests depending on a Spring
+ * context. The test instance itself is populated by Dependency Injection.
+ * </p>
+ * <p>
+ * Really for integration testing, not unit testing. You should <i>not</i>
+ * normally use the Spring container for unit tests: simply populate your POJOs
+ * in plain JUnit tests!
+ * </p>
+ * <p>
+ * This supports two modes of populating the test:
+ * </p>
+ * <ul>
+ * <li>Via Setter Dependency Injection. Simply express dependencies on objects
+ * in the test fixture, and they will be satisfied by autowiring by type.
+ * <li>Via Field Injection. Declare protected variables of the required type
+ * which match named beans in the context. This is autowire by name, rather than
+ * type. This approach is based on an approach originated by Ara Abrahmian.
+ * Setter Dependency Injection is the default: set the
+ * {@code populateProtectedVariables} property to {@code true} in
+ * the constructor to switch on Field Injection.
+ * </ul>
+ *
+ * @author Rod Johnson
+ * @author Rob Harrop
+ * @author Rick Evans
+ * @author Sam Brannen
+ * @since 1.1.1
+ * @see #setDirty
+ * @see #contextKey
+ * @see #getContext
+ * @see #getConfigLocations
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests})
+ */
+@Deprecated
+@SuppressWarnings({ "unchecked", "rawtypes" })
+public abstract class AbstractDependencyInjectionSpringContextTests extends AbstractSingleSpringContextTests {
+
+ /**
+ * Constant that indicates no autowiring at all.
+ *
+ * @see #setAutowireMode
+ */
+ public static final int AUTOWIRE_NO = 0;
+
+ /**
+ * Constant that indicates autowiring bean properties by name.
+ *
+ * @see #setAutowireMode
+ */
+ public static final int AUTOWIRE_BY_NAME = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME;
+
+ /**
+ * Constant that indicates autowiring bean properties by type.
+ *
+ * @see #setAutowireMode
+ */
+ public static final int AUTOWIRE_BY_TYPE = AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE;
+
+ private boolean populateProtectedVariables = false;
+
+ private int autowireMode = AUTOWIRE_BY_TYPE;
+
+ private boolean dependencyCheck = true;
+
+ private String[] managedVariableNames;
+
+
+ /**
+ * Default constructor for AbstractDependencyInjectionSpringContextTests.
+ */
+ public AbstractDependencyInjectionSpringContextTests() {
+ }
+
+ /**
+ * Constructor for AbstractDependencyInjectionSpringContextTests with a
+ * JUnit name.
+ * @param name the name of this text fixture
+ */
+ public AbstractDependencyInjectionSpringContextTests(String name) {
+ super(name);
+ }
+
+
+ /**
+ * Set whether to populate protected variables of this test case. Default is
+ * {@code false}.
+ */
+ public final void setPopulateProtectedVariables(boolean populateFields) {
+ this.populateProtectedVariables = populateFields;
+ }
+
+ /**
+ * Return whether to populate protected variables of this test case.
+ */
+ public final boolean isPopulateProtectedVariables() {
+ return this.populateProtectedVariables;
+ }
+
+ /**
+ * Set the autowire mode for test properties set by Dependency Injection.
+ * <p>The default is {@link #AUTOWIRE_BY_TYPE}. Can be set to
+ * {@link #AUTOWIRE_BY_NAME} or {@link #AUTOWIRE_NO} instead.
+ * @see #AUTOWIRE_BY_TYPE
+ * @see #AUTOWIRE_BY_NAME
+ * @see #AUTOWIRE_NO
+ */
+ public final void setAutowireMode(final int autowireMode) {
+ this.autowireMode = autowireMode;
+ }
+
+ /**
+ * Return the autowire mode for test properties set by Dependency Injection.
+ */
+ public final int getAutowireMode() {
+ return this.autowireMode;
+ }
+
+ /**
+ * Set whether or not dependency checking should be performed for test
+ * properties set by Dependency Injection.
+ * <p>The default is {@code true}, meaning that tests cannot be run
+ * unless all properties are populated.
+ */
+ public final void setDependencyCheck(final boolean dependencyCheck) {
+ this.dependencyCheck = dependencyCheck;
+ }
+
+ /**
+ * Return whether or not dependency checking should be performed for test
+ * properties set by Dependency Injection.
+ */
+ public final boolean isDependencyCheck() {
+ return this.dependencyCheck;
+ }
+
+ /**
+ * Prepare this test instance, injecting dependencies into its protected
+ * fields and its bean properties.
+ * <p>Note: if the {@link ApplicationContext} for this test instance has not
+ * been configured (e.g., is {@code null}), dependency injection
+ * will naturally <strong>not</strong> be performed, but an informational
+ * message will be written to the log.
+ * @see #injectDependencies()
+ */
+ protected void prepareTestInstance() throws Exception {
+ if (getApplicationContext() == null) {
+ if (this.logger.isInfoEnabled()) {
+ this.logger.info("ApplicationContext has not been configured for test [" + getClass().getName()
+ + "]: dependency injection will NOT be performed.");
+ }
+ }
+ else {
+ injectDependencies();
+ }
+ }
+
+ /**
+ * Inject dependencies into 'this' instance (that is, this test instance).
+ * <p>The default implementation populates protected variables if the
+ * {@link #populateProtectedVariables() appropriate flag is set}, else uses
+ * autowiring if autowiring is switched on (which it is by default).
+ * <p>Override this method if you need full control over how dependencies are
+ * injected into the test instance.
+ * @throws Exception in case of dependency injection failure
+ * @throws IllegalStateException if the {@link ApplicationContext} for this
+ * test instance has not been configured
+ * @see #populateProtectedVariables()
+ */
+ @SuppressWarnings("javadoc")
+ protected void injectDependencies() throws Exception {
+ Assert.state(getApplicationContext() != null,
+ "injectDependencies() called without first configuring an ApplicationContext");
+ if (isPopulateProtectedVariables()) {
+ if (this.managedVariableNames == null) {
+ initManagedVariableNames();
+ }
+ populateProtectedVariables();
+ }
+ getApplicationContext().getBeanFactory().autowireBeanProperties(this, getAutowireMode(), isDependencyCheck());
+ }
+
+ private void initManagedVariableNames() throws IllegalAccessException {
+ List managedVarNames = new LinkedList();
+ Class clazz = getClass();
+ do {
+ Field[] fields = clazz.getDeclaredFields();
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Found " + fields.length + " fields on " + clazz);
+ }
+ for (int i = 0; i < fields.length; i++) {
+ Field field = fields[i];
+ field.setAccessible(true);
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Candidate field: " + field);
+ }
+ if (isProtectedInstanceField(field)) {
+ Object oldValue = field.get(this);
+ if (oldValue == null) {
+ managedVarNames.add(field.getName());
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Added managed variable '" + field.getName() + "'");
+ }
+ }
+ else {
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Rejected managed variable '" + field.getName() + "'");
+ }
+ }
+ }
+ }
+ clazz = clazz.getSuperclass();
+ } while (!clazz.equals(AbstractDependencyInjectionSpringContextTests.class));
+
+ this.managedVariableNames = (String[]) managedVarNames.toArray(new String[managedVarNames.size()]);
+ }
+
+ private boolean isProtectedInstanceField(Field field) {
+ int modifiers = field.getModifiers();
+ return !Modifier.isStatic(modifiers) && Modifier.isProtected(modifiers);
+ }
+
+ private void populateProtectedVariables() throws IllegalAccessException {
+ for (int i = 0; i < this.managedVariableNames.length; i++) {
+ String varName = this.managedVariableNames[i];
+ Object bean = null;
+ try {
+ Field field = findField(getClass(), varName);
+ bean = getApplicationContext().getBean(varName, field.getType());
+ field.setAccessible(true);
+ field.set(this, bean);
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Populated field: " + field);
+ }
+ }
+ catch (NoSuchFieldException ex) {
+ if (this.logger.isWarnEnabled()) {
+ this.logger.warn("No field with name '" + varName + "'");
+ }
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ if (this.logger.isWarnEnabled()) {
+ this.logger.warn("No bean with name '" + varName + "'");
+ }
+ }
+ }
+ }
+
+ private Field findField(Class clazz, String name) throws NoSuchFieldException {
+ try {
+ return clazz.getDeclaredField(name);
+ }
+ catch (NoSuchFieldException ex) {
+ Class superclass = clazz.getSuperclass();
+ if (superclass != AbstractSpringContextTests.class) {
+ return findField(superclass, name);
+ }
+ else {
+ throw ex;
+ }
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java
new file mode 100644
index 00000000..0d5d0784
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java
@@ -0,0 +1,362 @@
+/*
+ * 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;
+
+import org.springframework.beans.factory.support.BeanDefinitionReader;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ResourceUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * <p>
+ * Abstract JUnit 3.8 test class that holds and exposes a single Spring
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}.
+ * </p>
+ * <p>
+ * This class will cache contexts based on a <i>context key</i>: normally the
+ * config locations String array describing the Spring resource descriptors
+ * making up the context. Unless the {@link #setDirty()} method is called by a
+ * test, the context will not be reloaded, even across different subclasses of
+ * this test. This is particularly beneficial if your context is slow to
+ * construct, for example if you are using Hibernate and the time taken to load
+ * the mappings is an issue.
+ * </p>
+ * <p>
+ * For such standard usage, simply override the {@link #getConfigLocations()}
+ * method and provide the desired config files. For alternative configuration
+ * options, see {@link #getConfigPath()} and {@link #getConfigPaths()}.
+ * </p>
+ * <p>
+ * If you don't want to load a standard context from an array of config
+ * locations, you can override the {@link #contextKey()} method. In conjunction
+ * with this you typically need to override the {@link #loadContext(Object)}
+ * method, which by default loads the locations specified in the
+ * {@link #getConfigLocations()} method.
+ * </p>
+ * <p>
+ * <b>WARNING:</b> When doing integration tests from within Eclipse, only use
+ * classpath resource URLs. Else, you may see misleading failures when changing
+ * context locations.
+ * </p>
+ *
+ * @author Juergen Hoeller
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ * @see #getConfigLocations()
+ * @see #contextKey()
+ * @see #loadContext(Object)
+ * @see #getApplicationContext()
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests})
+ */
+@Deprecated
+public abstract class AbstractSingleSpringContextTests extends AbstractSpringContextTests {
+
+ /** Application context this test will run against */
+ protected ConfigurableApplicationContext applicationContext;
+
+ private int loadCount = 0;
+
+
+ /**
+ * Default constructor for AbstractSingleSpringContextTests.
+ */
+ public AbstractSingleSpringContextTests() {
+ }
+
+ /**
+ * Constructor for AbstractSingleSpringContextTests with a JUnit name.
+ * @param name the name of this text fixture
+ */
+ public AbstractSingleSpringContextTests(String name) {
+ super(name);
+ }
+
+ /**
+ * This implementation is final. Override {@code onSetUp} for custom behavior.
+ * @see #onSetUp()
+ */
+ protected final void setUp() throws Exception {
+ // lazy load, in case getApplicationContext() has not yet been called.
+ if (this.applicationContext == null) {
+ this.applicationContext = getContext(contextKey());
+ }
+ prepareTestInstance();
+ onSetUp();
+ }
+
+ /**
+ * Prepare this test instance, for example populating its fields.
+ * The context has already been loaded at the time of this callback.
+ * <p>The default implementation does nothing.
+ * @throws Exception in case of preparation failure
+ */
+ protected void prepareTestInstance() throws Exception {
+ }
+
+ /**
+ * Subclasses can override this method in place of the {@code setUp()}
+ * method, which is final in this class.
+ * <p>The default implementation does nothing.
+ * @throws Exception simply let any exception propagate
+ */
+ protected void onSetUp() throws Exception {
+ }
+
+ /**
+ * Called to say that the "applicationContext" instance variable is dirty
+ * and should be reloaded. We need to do this if a test has modified the
+ * context (for example, by replacing a bean definition).
+ */
+ protected void setDirty() {
+ setDirty(contextKey());
+ }
+
+ /**
+ * This implementation is final. Override {@code onTearDown} for
+ * custom behavior.
+ * @see #onTearDown()
+ */
+ protected final void tearDown() throws Exception {
+ onTearDown();
+ }
+
+ /**
+ * Subclasses can override this to add custom behavior on teardown.
+ * @throws Exception simply let any exception propagate
+ */
+ protected void onTearDown() throws Exception {
+ }
+
+ /**
+ * Return a key for this context. Default is the config location array as
+ * determined by {@link #getConfigLocations()}.
+ * <p>If you override this method, you will typically have to override
+ * {@link #loadContext(Object)} as well, being able to handle the key type
+ * that this method returns.
+ * @return the context key
+ * @see #getConfigLocations()
+ */
+ protected Object contextKey() {
+ return getConfigLocations();
+ }
+
+ /**
+ * This implementation assumes a key of type String array and loads a
+ * context from the given locations.
+ * <p>If you override {@link #contextKey()}, you will typically have to
+ * override this method as well, being able to handle the key type that
+ * {@code contextKey()} returns.
+ * @see #getConfigLocations()
+ */
+ protected ConfigurableApplicationContext loadContext(Object key) throws Exception {
+ return loadContextLocations((String[]) key);
+ }
+
+ /**
+ * Load a Spring ApplicationContext from the given config locations.
+ * <p>The default implementation creates a standard
+ * {@link #createApplicationContext GenericApplicationContext}, allowing
+ * for customizing the internal bean factory through
+ * {@link #customizeBeanFactory}.
+ * @param locations the config locations (as Spring resource locations,
+ * e.g. full classpath locations or any kind of URL)
+ * @return the corresponding ApplicationContext instance (potentially cached)
+ * @throws Exception if context loading failed
+ * @see #createApplicationContext(String[])
+ */
+ protected ConfigurableApplicationContext loadContextLocations(String[] locations) throws Exception {
+ ++this.loadCount;
+ if (this.logger.isInfoEnabled()) {
+ this.logger.info("Loading context for locations: " + StringUtils.arrayToCommaDelimitedString(locations));
+ }
+ return createApplicationContext(locations);
+ }
+
+ /**
+ * Create a Spring {@link ConfigurableApplicationContext} for use by this test.
+ * <p>The default implementation creates a standard {@link GenericApplicationContext}
+ * instance, calls the {@link #prepareApplicationContext} prepareApplicationContext}
+ * method and the {@link #customizeBeanFactory customizeBeanFactory} method to allow
+ * for customizing the context and its DefaultListableBeanFactory, populates the
+ * context from the specified config {@code locations} through the configured
+ * {@link #createBeanDefinitionReader(GenericApplicationContext) BeanDefinitionReader},
+ * and finally {@link ConfigurableApplicationContext#refresh() refreshes} the context.
+ * @param locations the config locations (as Spring resource locations,
+ * e.g. full classpath locations or any kind of URL)
+ * @return the GenericApplicationContext instance
+ * @see #loadContextLocations(String[])
+ * @see #customizeBeanFactory(DefaultListableBeanFactory)
+ * @see #createBeanDefinitionReader(GenericApplicationContext)
+ */
+ protected ConfigurableApplicationContext createApplicationContext(String[] locations) {
+ GenericApplicationContext context = new GenericApplicationContext();
+ prepareApplicationContext(context);
+ customizeBeanFactory(context.getDefaultListableBeanFactory());
+ createBeanDefinitionReader(context).loadBeanDefinitions(locations);
+ context.refresh();
+ return context;
+ }
+
+ /**
+ * Prepare the GenericApplicationContext used by this test.
+ * Called before bean definitions are read.
+ * <p>The default implementation is empty. Can be overridden in subclasses to
+ * customize GenericApplicationContext's standard settings.
+ * @param context the context for which the BeanDefinitionReader should be created
+ * @see #createApplicationContext
+ * @see org.springframework.context.support.GenericApplicationContext#setResourceLoader
+ * @see org.springframework.context.support.GenericApplicationContext#setId
+ */
+ protected void prepareApplicationContext(GenericApplicationContext context) {
+ }
+
+ /**
+ * Customize the internal bean factory of the ApplicationContext used by
+ * this test. Called before bean definitions are read.
+ * <p>The default implementation is empty. Can be overridden in subclasses to
+ * customize DefaultListableBeanFactory's standard settings.
+ * @param beanFactory the newly created bean factory for this context
+ * @see #loadContextLocations
+ * @see #createApplicationContext
+ * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowBeanDefinitionOverriding
+ * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowEagerClassLoading
+ * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowCircularReferences
+ * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping
+ */
+ protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
+ }
+
+ /**
+ * Factory method for creating new {@link BeanDefinitionReader}s for
+ * loading bean definitions into the supplied
+ * {@link GenericApplicationContext context}.
+ * <p>The default implementation creates a new {@link XmlBeanDefinitionReader}.
+ * Can be overridden in subclasses to provide a different
+ * BeanDefinitionReader implementation.
+ * @param context the context for which the BeanDefinitionReader should be created
+ * @return a BeanDefinitionReader for the supplied context
+ * @see #createApplicationContext(String[])
+ * @see BeanDefinitionReader
+ * @see XmlBeanDefinitionReader
+ */
+ protected BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context) {
+ return new XmlBeanDefinitionReader(context);
+ }
+
+ /**
+ * Subclasses can override this method to return the locations of their
+ * config files, unless they override {@link #contextKey()} and
+ * {@link #loadContext(Object)} instead.
+ * <p>A plain path will be treated as class path location, e.g.:
+ * "org/springframework/whatever/foo.xml". Note however that you may prefix
+ * path locations with standard Spring resource prefixes. Therefore, a
+ * config location path prefixed with "classpath:" with behave the same as a
+ * plain path, but a config location such as
+ * "file:/some/path/path/location/appContext.xml" will be treated as a
+ * filesystem location.
+ * <p>The default implementation builds config locations for the config paths
+ * specified through {@link #getConfigPaths()}.
+ * @return an array of config locations
+ * @see #getConfigPaths()
+ * @see org.springframework.core.io.ResourceLoader#getResource(String)
+ */
+ protected String[] getConfigLocations() {
+ String[] paths = getConfigPaths();
+ String[] locations = new String[paths.length];
+ for (int i = 0; i < paths.length; i++) {
+ String path = paths[i];
+ if (path.startsWith("/")) {
+ locations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + path;
+ }
+ else {
+ locations[i] = ResourceUtils.CLASSPATH_URL_PREFIX +
+ StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(getClass()) + "/" + path);
+ }
+ }
+ return locations;
+ }
+
+ /**
+ * Subclasses can override this method to return paths to their config
+ * files, relative to the concrete test class.
+ * <p>A plain path, e.g. "context.xml", will be loaded as classpath resource
+ * from the same package that the concrete test class is defined in. A path
+ * starting with a slash is treated as fully qualified class path location,
+ * e.g.: "/org/springframework/whatever/foo.xml".
+ * <p>The default implementation builds an array for the config path specified
+ * through {@link #getConfigPath()}.
+ * @return an array of config locations
+ * @see #getConfigPath()
+ * @see java.lang.Class#getResource(String)
+ */
+ protected String[] getConfigPaths() {
+ String path = getConfigPath();
+ return (path != null ? new String[] { path } : new String[0]);
+ }
+
+ /**
+ * Subclasses can override this method to return a single path to a config
+ * file, relative to the concrete test class.
+ * <p>A plain path, e.g. "context.xml", will be loaded as classpath resource
+ * from the same package that the concrete test class is defined in. A path
+ * starting with a slash is treated as fully qualified class path location,
+ * e.g.: "/org/springframework/whatever/foo.xml".
+ * <p>The default implementation simply returns {@code null}.
+ * @return an array of config locations
+ * @see #getConfigPath()
+ * @see Class#getResource(String)
+ */
+ protected String getConfigPath() {
+ return null;
+ }
+
+ /**
+ * Return the ApplicationContext that this base class manages; may be
+ * {@code null}.
+ */
+ public final ConfigurableApplicationContext getApplicationContext() {
+ // lazy load, in case setUp() has not yet been called.
+ if (this.applicationContext == null) {
+ try {
+ this.applicationContext = getContext(contextKey());
+ }
+ catch (Exception e) {
+ // log and continue...
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Caught exception while retrieving the ApplicationContext for test [" +
+ getClass().getName() + "." + getName() + "].", e);
+ }
+ }
+ }
+
+ return this.applicationContext;
+ }
+
+ /**
+ * Return the current number of context load attempts.
+ */
+ public final int getLoadCount() {
+ return this.loadCount;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/AbstractSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractSpringContextTests.java
new file mode 100644
index 00000000..d01144ed
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/AbstractSpringContextTests.java
@@ -0,0 +1,171 @@
+/*
+ * 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;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * <p>
+ * Superclass for JUnit 3.8 test cases using Spring
+ * {@link org.springframework.context.ApplicationContext ApplicationContexts}.
+ * </p>
+ * <p>
+ * Maintains a static cache of contexts by key. This has significant performance
+ * benefit if initializing the context would take time. While initializing a
+ * Spring context itself is very quick, some beans in a context, such as a
+ * LocalSessionFactoryBean for working with Hibernate, may take some time to
+ * initialize. Hence it often makes sense to do that initializing once.
+ * </p>
+ * <p>
+ * Any ApplicationContext created by this class will be asked to register a JVM
+ * shutdown hook for itself. Unless the context gets closed early, all context
+ * instances will be automatically closed on JVM shutdown. This allows for
+ * freeing external resources held by beans within the context, e.g. temporary
+ * files.
+ * </p>
+ * <p>
+ * Normally you won't extend this class directly but rather one of its
+ * subclasses.
+ * </p>
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @author Sam Brannen
+ * @since 1.1.1
+ * @see AbstractSingleSpringContextTests
+ * @see AbstractDependencyInjectionSpringContextTests
+ * @see AbstractTransactionalSpringContextTests
+ * @see AbstractTransactionalDataSourceSpringContextTests
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests})
+ */
+@Deprecated
+public abstract class AbstractSpringContextTests extends ConditionalTestCase {
+
+ /**
+ * Map of context keys returned by subclasses of this class, to Spring
+ * contexts. This needs to be static, as JUnit tests are destroyed and
+ * recreated between running individual test methods.
+ */
+ private static Map<String, ConfigurableApplicationContext> contextKeyToContextMap =
+ new HashMap<String, ConfigurableApplicationContext>();
+
+
+ /**
+ * Default constructor for AbstractSpringContextTests.
+ */
+ public AbstractSpringContextTests() {
+ }
+
+ /**
+ * Constructor for AbstractSpringContextTests with a JUnit name.
+ */
+ public AbstractSpringContextTests(String name) {
+ super(name);
+ }
+
+
+ /**
+ * Explicitly add an ApplicationContext instance under a given key.
+ * <p>This is not meant to be used by subclasses. It is rather exposed for
+ * special test suite environments.
+ * @param key the context key
+ * @param context the ApplicationContext instance
+ */
+ public final void addContext(Object key, ConfigurableApplicationContext context) {
+ Assert.notNull(context, "ApplicationContext must not be null");
+ contextKeyToContextMap.put(contextKeyString(key), context);
+ }
+
+ /**
+ * Return whether there is a cached context for the given key.
+ * @param key the context key
+ */
+ protected final boolean hasCachedContext(Object key) {
+ return contextKeyToContextMap.containsKey(contextKeyString(key));
+ }
+
+ /**
+ * Determine if the supplied context {@code key} is <em>empty</em>.
+ * <p>By default, {@code null} values, empty strings, and zero-length
+ * arrays are considered <em>empty</em>.
+ * @param key the context key to check
+ * @return {@code true} if the supplied context key is empty
+ */
+ protected boolean isContextKeyEmpty(Object key) {
+ return (key == null) || ((key instanceof String) && !StringUtils.hasText((String) key)) ||
+ ((key instanceof Object[]) && ObjectUtils.isEmpty((Object[]) key));
+ }
+
+ /**
+ * Obtain an ApplicationContext for the given key, potentially cached.
+ * @param key the context key; may be {@code null}.
+ * @return the corresponding ApplicationContext instance (potentially cached),
+ * or {@code null} if the provided {@code key} is <em>empty</em>
+ */
+ protected final ConfigurableApplicationContext getContext(Object key) throws Exception {
+ if (isContextKeyEmpty(key)) {
+ return null;
+ }
+ String keyString = contextKeyString(key);
+ ConfigurableApplicationContext ctx = contextKeyToContextMap.get(keyString);
+ if (ctx == null) {
+ ctx = loadContext(key);
+ ctx.registerShutdownHook();
+ contextKeyToContextMap.put(keyString, ctx);
+ }
+ return ctx;
+ }
+
+ /**
+ * Mark the context with the given key as dirty. This will cause the cached
+ * context to be reloaded before the next test case is executed.
+ * <p>Call this method only if you change the state of a singleton bean,
+ * potentially affecting future tests.
+ */
+ protected final void setDirty(Object contextKey) {
+ String keyString = contextKeyString(contextKey);
+ ConfigurableApplicationContext ctx = contextKeyToContextMap.remove(keyString);
+ if (ctx != null) {
+ ctx.close();
+ }
+ }
+
+ /**
+ * Subclasses can override this to return a String representation of their
+ * context key for use in caching and logging.
+ * @param contextKey the context key
+ */
+ protected String contextKeyString(Object contextKey) {
+ return ObjectUtils.nullSafeToString(contextKey);
+ }
+
+ /**
+ * Load a new ApplicationContext for the given key.
+ * <p>To be implemented by subclasses.
+ * @param key the context key
+ * @return the corresponding ApplicationContext instance (new)
+ */
+ protected abstract ConfigurableApplicationContext loadContext(Object key) throws Exception;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java
new file mode 100644
index 00000000..0f697d11
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java
@@ -0,0 +1,199 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.jdbc.JdbcTestUtils;
+
+/**
+ * Subclass of AbstractTransactionalSpringContextTests that adds some convenience
+ * functionality for JDBC access. Expects a {@link javax.sql.DataSource} bean
+ * to be defined in the Spring application context.
+ *
+ * <p>This class exposes a {@link org.springframework.jdbc.core.JdbcTemplate}
+ * and provides an easy way to delete from the database in a new transaction.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @author Thomas Risberg
+ * @since 1.1.1
+ * @see #setDataSource(javax.sql.DataSource)
+ * @see #getJdbcTemplate()
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests})
+ */
+@Deprecated
+@SuppressWarnings({ "unchecked", "rawtypes" })
+public abstract class AbstractTransactionalDataSourceSpringContextTests
+ extends AbstractTransactionalSpringContextTests {
+
+ protected JdbcTemplate jdbcTemplate;
+
+ private String sqlScriptEncoding;
+
+ /**
+ * Did this test delete any tables? If so, we forbid transaction completion,
+ * and only allow rollback.
+ */
+ private boolean zappedTables;
+
+
+ /**
+ * Default constructor for AbstractTransactionalDataSourceSpringContextTests.
+ */
+ public AbstractTransactionalDataSourceSpringContextTests() {
+ }
+
+ /**
+ * Constructor for AbstractTransactionalDataSourceSpringContextTests with a JUnit name.
+ */
+ public AbstractTransactionalDataSourceSpringContextTests(String name) {
+ super(name);
+ }
+
+
+ /**
+ * Setter: DataSource is provided by Dependency Injection.
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ /**
+ * Return the JdbcTemplate that this base class manages.
+ */
+ public final JdbcTemplate getJdbcTemplate() {
+ return this.jdbcTemplate;
+ }
+
+ /**
+ * Specify the encoding for SQL scripts, if different from the platform encoding.
+ * @see #executeSqlScript
+ */
+ public void setSqlScriptEncoding(String sqlScriptEncoding) {
+ this.sqlScriptEncoding = sqlScriptEncoding;
+ }
+
+
+ /**
+ * Convenient method to delete all rows from these tables.
+ * Calling this method will make avoidance of rollback by calling
+ * {@code setComplete()} impossible.
+ * @see #setComplete
+ */
+ protected void deleteFromTables(String[] names) {
+ for (int i = 0; i < names.length; i++) {
+ int rowCount = this.jdbcTemplate.update("DELETE FROM " + names[i]);
+ if (logger.isInfoEnabled()) {
+ logger.info("Deleted " + rowCount + " rows from table " + names[i]);
+ }
+ }
+ this.zappedTables = true;
+ }
+
+ /**
+ * Overridden to prevent the transaction committing if a number of tables have been
+ * cleared, as a defensive measure against accidental <i>permanent</i> wiping of a database.
+ * @see org.springframework.test.AbstractTransactionalSpringContextTests#setComplete()
+ */
+ protected final void setComplete() {
+ if (this.zappedTables) {
+ throw new IllegalStateException("Cannot set complete after deleting tables");
+ }
+ super.setComplete();
+ }
+
+ /**
+ * Count the rows in the given table
+ * @param tableName table name to count rows in
+ * @return the number of rows in the table
+ */
+ protected int countRowsInTable(String tableName) {
+ return this.jdbcTemplate.queryForInt("SELECT COUNT(0) FROM " + tableName);
+ }
+
+
+ /**
+ * Execute the given SQL script. Will be rolled back by default,
+ * according to the fate of the current transaction.
+ * @param sqlResourcePath Spring resource path for the SQL script.
+ * Should normally be loaded by classpath.
+ * <p>Statements should be delimited with a semicolon. If statements are not delimited with
+ * a semicolon then there should be one statement per line. Statements are allowed to span
+ * lines only if they are delimited with a semicolon.
+ * <p><b>Do not use this method to execute DDL if you expect rollback.</b>
+ * @param continueOnError whether or not to continue without throwing
+ * an exception in the event of an error
+ * @throws DataAccessException if there is an error executing a statement
+ * and continueOnError was false
+ */
+ protected void executeSqlScript(String sqlResourcePath, boolean continueOnError) throws DataAccessException {
+ if (logger.isInfoEnabled()) {
+ logger.info("Executing SQL script '" + sqlResourcePath + "'");
+ }
+
+ EncodedResource resource =
+ new EncodedResource(getApplicationContext().getResource(sqlResourcePath), this.sqlScriptEncoding);
+ long startTime = System.currentTimeMillis();
+ List statements = new LinkedList();
+ try {
+ LineNumberReader lnr = new LineNumberReader(resource.getReader());
+ String script = JdbcTestUtils.readScript(lnr);
+ char delimiter = ';';
+ if (!JdbcTestUtils.containsSqlScriptDelimiters(script, delimiter)) {
+ delimiter = '\n';
+ }
+ JdbcTestUtils.splitSqlScript(script, delimiter, statements);
+ for (Iterator itr = statements.iterator(); itr.hasNext(); ) {
+ String statement = (String) itr.next();
+ try {
+ int rowsAffected = this.jdbcTemplate.update(statement);
+ if (logger.isDebugEnabled()) {
+ logger.debug(rowsAffected + " rows affected by SQL: " + statement);
+ }
+ }
+ catch (DataAccessException ex) {
+ if (continueOnError) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("SQL: " + statement + " failed", ex);
+ }
+ }
+ else {
+ throw ex;
+ }
+ }
+ }
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ logger.info("Done executing SQL scriptBuilder '" + sqlResourcePath + "' in " + elapsedTime + " ms");
+ }
+ catch (IOException ex) {
+ throw new DataAccessResourceFailureException("Failed to open SQL script '" + sqlResourcePath + "'", ex);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java
new file mode 100644
index 00000000..af3c6866
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java
@@ -0,0 +1,359 @@
+/*
+ * 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;
+
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionException;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+
+/**
+ * Convenient base class for JUnit 3.8 based tests that should occur in a
+ * transaction, but normally will roll the transaction back on the completion of
+ * each test.
+ *
+ * <p>This is useful in a range of circumstances, allowing the following benefits:
+ * <ul>
+ * <li>Ability to delete or insert any data in the database, without affecting
+ * other tests
+ * <li>Providing a transactional context for any code requiring a transaction
+ * <li>Ability to write anything to the database without any need to clean up.
+ * </ul>
+ *
+ * <p>This class is typically very fast, compared to traditional setup/teardown
+ * scripts.
+ *
+ * <p>If data should be left in the database, call the {@link #setComplete()}
+ * method in each test. The {@link #setDefaultRollback "defaultRollback"}
+ * property, which defaults to "true", determines whether transactions will
+ * complete by default.
+ *
+ * <p>It is even possible to end the transaction early; for example, to verify lazy
+ * loading behavior of an O/R mapping tool. (This is a valuable away to avoid
+ * unexpected errors when testing a web UI, for example.) Simply call the
+ * {@link #endTransaction()} method. Execution will then occur without a
+ * transactional context.
+ *
+ * <p>The {@link #startNewTransaction()} method may be called after a call to
+ * {@link #endTransaction()} if you wish to create a new transaction, quite
+ * independent of the old transaction. The new transaction's default fate will
+ * be to roll back, unless {@link #setComplete()} is called again during the
+ * scope of the new transaction. Any number of transactions may be created and
+ * ended in this way. The final transaction will automatically be rolled back
+ * when the test case is torn down.
+ *
+ * <p>Transactional behavior requires a single bean in the context implementing the
+ * {@link PlatformTransactionManager} interface. This will be set by the
+ * superclass's Dependency Injection mechanism. If using the superclass's Field
+ * Injection mechanism, the implementation should be named "transactionManager".
+ * This mechanism allows the use of the
+ * {@link AbstractDependencyInjectionSpringContextTests} superclass even when
+ * there is more than one transaction manager in the context.
+ *
+ * <p><b>This base class can also be used without transaction management, if no
+ * PlatformTransactionManager bean is found in the context provided.</b> Be
+ * careful about using this mode, as it allows the potential to permanently
+ * modify data. This mode is available only if dependency checking is turned off
+ * in the {@link AbstractDependencyInjectionSpringContextTests} superclass. The
+ * non-transactional capability is provided to enable use of the same subclass
+ * in different environments.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @author Sam Brannen
+ * @since 1.1.1
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests})
+ */
+@Deprecated
+public abstract class AbstractTransactionalSpringContextTests extends AbstractDependencyInjectionSpringContextTests {
+
+ /** The transaction manager to use */
+ protected PlatformTransactionManager transactionManager;
+
+ /** Should we roll back by default? */
+ private boolean defaultRollback = true;
+
+ /** Should we commit the current transaction? */
+ private boolean complete = false;
+
+ /** Number of transactions started */
+ private int transactionsStarted = 0;
+
+ /**
+ * Transaction definition used by this test class: by default, a plain
+ * DefaultTransactionDefinition. Subclasses can change this to cause
+ * different behavior.
+ */
+ protected TransactionDefinition transactionDefinition= new DefaultTransactionDefinition();
+
+ /**
+ * TransactionStatus for this test. Typical subclasses won't need to use it.
+ */
+ protected TransactionStatus transactionStatus;
+
+
+ /**
+ * Default constructor for AbstractTransactionalSpringContextTests.
+ */
+ public AbstractTransactionalSpringContextTests() {
+ }
+
+ /**
+ * Constructor for AbstractTransactionalSpringContextTests with a JUnit name.
+ */
+ public AbstractTransactionalSpringContextTests(String name) {
+ super(name);
+ }
+
+
+ /**
+ * Specify the transaction manager to use. No transaction management will be
+ * available if this is not set. Populated through dependency injection by
+ * the superclass.
+ * <p>
+ * This mode works only if dependency checking is turned off in the
+ * {@link AbstractDependencyInjectionSpringContextTests} superclass.
+ */
+ public void setTransactionManager(PlatformTransactionManager transactionManager) {
+ this.transactionManager = transactionManager;
+ }
+
+ /**
+ * Subclasses can set this value in their constructor to change the default,
+ * which is always to roll the transaction back.
+ */
+ public void setDefaultRollback(final boolean defaultRollback) {
+ this.defaultRollback = defaultRollback;
+ }
+ /**
+ * Get the <em>default rollback</em> flag for this test.
+ * @see #setDefaultRollback(boolean)
+ * @return The <em>default rollback</em> flag.
+ */
+ protected boolean isDefaultRollback() {
+ return this.defaultRollback;
+ }
+
+ /**
+ * Determines whether or not to rollback transactions for the current test.
+ * <p>The default implementation delegates to {@link #isDefaultRollback()}.
+ * Subclasses can override as necessary.
+ */
+ protected boolean isRollback() {
+ return isDefaultRollback();
+ }
+
+ /**
+ * Call this method in an overridden {@link #runBare()} method to prevent
+ * transactional execution.
+ */
+ protected void preventTransaction() {
+ this.transactionDefinition = null;
+ }
+
+ /**
+ * Call this method in an overridden {@link #runBare()} method to override
+ * the transaction attributes that will be used, so that {@link #setUp()}
+ * and {@link #tearDown()} behavior is modified.
+ * @param customDefinition the custom transaction definition
+ */
+ protected void setTransactionDefinition(TransactionDefinition customDefinition) {
+ this.transactionDefinition = customDefinition;
+ }
+
+ /**
+ * This implementation creates a transaction before test execution.
+ * <p>Override {@link #onSetUpBeforeTransaction()} and/or
+ * {@link #onSetUpInTransaction()} to add custom set-up behavior for
+ * transactional execution. Alternatively, override this method for general
+ * set-up behavior, calling {@code super.onSetUp()} as part of your
+ * method implementation.
+ * @throws Exception simply let any exception propagate
+ * @see #onTearDown()
+ */
+ protected void onSetUp() throws Exception {
+ this.complete = !this.isRollback();
+
+ if (this.transactionManager == null) {
+ this.logger.info("No transaction manager set: test will NOT run within a transaction");
+ }
+ else if (this.transactionDefinition == null) {
+ this.logger.info("No transaction definition set: test will NOT run within a transaction");
+ }
+ else {
+ onSetUpBeforeTransaction();
+ startNewTransaction();
+ try {
+ onSetUpInTransaction();
+ }
+ catch (final Exception ex) {
+ endTransaction();
+ throw ex;
+ }
+ }
+ }
+
+ /**
+ * Subclasses can override this method to perform any setup operations, such
+ * as populating a database table, <i>before</i> the transaction created by
+ * this class. Only invoked if there <i>is</i> a transaction: that is, if
+ * {@link #preventTransaction()} has not been invoked in an overridden
+ * {@link #runTest()} method.
+ * @throws Exception simply let any exception propagate
+ */
+ protected void onSetUpBeforeTransaction() throws Exception {
+ }
+
+ /**
+ * Subclasses can override this method to perform any setup operations, such
+ * as populating a database table, <i>within</i> the transaction created by
+ * this class.
+ * <p><b>NB:</b> Not called if there is no transaction management, due to no
+ * transaction manager being provided in the context.
+ * <p>If any {@link Throwable} is thrown, the transaction that has been started
+ * prior to the execution of this method will be
+ * {@link #endTransaction() ended} (or rather an attempt will be made to
+ * {@link #endTransaction() end it gracefully}); The offending
+ * {@link Throwable} will then be rethrown.
+ * @throws Exception simply let any exception propagate
+ */
+ protected void onSetUpInTransaction() throws Exception {
+ }
+
+ /**
+ * This implementation ends the transaction after test execution.
+ * <p>Override {@link #onTearDownInTransaction()} and/or
+ * {@link #onTearDownAfterTransaction()} to add custom tear-down behavior
+ * for transactional execution. Alternatively, override this method for
+ * general tear-down behavior, calling {@code super.onTearDown()} as
+ * part of your method implementation.
+ * <p>Note that {@link #onTearDownInTransaction()} will only be called if a
+ * transaction is still active at the time of the test shutdown. In
+ * particular, it will <i>not</i> be called if the transaction has been
+ * completed with an explicit {@link #endTransaction()} call before.
+ * @throws Exception simply let any exception propagate
+ * @see #onSetUp()
+ */
+ protected void onTearDown() throws Exception {
+ // Call onTearDownInTransaction and end transaction if the transaction
+ // is still active.
+ if (this.transactionStatus != null && !this.transactionStatus.isCompleted()) {
+ try {
+ onTearDownInTransaction();
+ }
+ finally {
+ endTransaction();
+ }
+ }
+
+ // Call onTearDownAfterTransaction if there was at least one
+ // transaction, even if it has been completed early through an
+ // endTransaction() call.
+ if (this.transactionsStarted > 0) {
+ onTearDownAfterTransaction();
+ }
+ }
+
+ /**
+ * Subclasses can override this method to run invariant tests here. The
+ * transaction is <i>still active</i> at this point, so any changes made in
+ * the transaction will still be visible. However, there is no need to clean
+ * up the database, as a rollback will follow automatically.
+ * <p><b>NB:</b> Not called if there is no actual transaction, for example due
+ * to no transaction manager being provided in the application context.
+ * @throws Exception simply let any exception propagate
+ */
+ protected void onTearDownInTransaction() throws Exception {
+ }
+
+ /**
+ * Subclasses can override this method to perform cleanup after a
+ * transaction here. At this point, the transaction is <i>not active anymore</i>.
+ * @throws Exception simply let any exception propagate
+ */
+ protected void onTearDownAfterTransaction() throws Exception {
+ }
+
+ /**
+ * Cause the transaction to commit for this test method, even if the test
+ * method is configured to {@link #isRollback() rollback}.
+ * @throws IllegalStateException if the operation cannot be set to complete
+ * as no transaction manager was provided
+ */
+ protected void setComplete() {
+ if (this.transactionManager == null) {
+ throw new IllegalStateException("No transaction manager set");
+ }
+ this.complete = true;
+ }
+
+ /**
+ * Immediately force a commit or rollback of the transaction, according to
+ * the {@code complete} and {@link #isRollback() rollback} flags.
+ * <p>Can be used to explicitly let the transaction end early, for example to
+ * check whether lazy associations of persistent objects work outside of a
+ * transaction (that is, have been initialized properly).
+ * @see #setComplete()
+ */
+ protected void endTransaction() {
+ final boolean commit = this.complete || !isRollback();
+ if (this.transactionStatus != null) {
+ try {
+ if (commit) {
+ this.transactionManager.commit(this.transactionStatus);
+ this.logger.debug("Committed transaction after execution of test [" + getName() + "].");
+ }
+ else {
+ this.transactionManager.rollback(this.transactionStatus);
+ this.logger.debug("Rolled back transaction after execution of test [" + getName() + "].");
+ }
+ }
+ finally {
+ this.transactionStatus = null;
+ }
+ }
+ }
+
+ /**
+ * Start a new transaction. Only call this method if
+ * {@link #endTransaction()} has been called. {@link #setComplete()} can be
+ * used again in the new transaction. The fate of the new transaction, by
+ * default, will be the usual rollback.
+ * @throws TransactionException if starting the transaction failed
+ */
+ protected void startNewTransaction() throws TransactionException {
+ if (this.transactionStatus != null) {
+ throw new IllegalStateException("Cannot start new transaction without ending existing transaction: "
+ + "Invoke endTransaction() before startNewTransaction()");
+ }
+ if (this.transactionManager == null) {
+ throw new IllegalStateException("No transaction manager set");
+ }
+
+ this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
+ ++this.transactionsStarted;
+ this.complete = !this.isRollback();
+
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Began transaction (" + this.transactionsStarted + "): transaction manager ["
+ + this.transactionManager + "]; rollback [" + this.isRollback() + "].");
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/AssertThrows.java b/spring-test/src/main/java/org/springframework/test/AssertThrows.java
new file mode 100644
index 00000000..2958fe02
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/AssertThrows.java
@@ -0,0 +1,255 @@
+/*
+ * 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;
+
+/**
+ * Simple method object encapsulation of the 'test-for-Exception' scenario (for JUnit).
+ *
+ * <p>Used like so:
+ *
+ * <pre class="code">
+ * // the class under test
+ * public class Foo {
+ * public void someBusinessLogic(String name) {
+ * if (name == null) {
+ * throw new IllegalArgumentException("The 'name' argument is required");
+ * }
+ * // rest of business logic here...
+ * }
+ * }</pre>
+ *
+ * The test for the above bad argument path can be expressed using the
+ * {@link AssertThrows} class like so:
+ *
+ * <pre class="code">
+ * public class FooTest {
+ * public void testSomeBusinessLogicBadArgumentPath() {
+ * new AssertThrows(IllegalArgumentException.class) {
+ * public void test() {
+ * new Foo().someBusinessLogic(null);
+ * }
+ * }.runTest();
+ * }
+ * }</pre>
+ *
+ * This will result in the test passing if the {@code Foo.someBusinessLogic(..)}
+ * method threw an {@link IllegalArgumentException}; if it did not, the
+ * test would fail with the following message:
+ *
+ * <pre class="code">
+ * "Must have thrown a [class java.lang.IllegalArgumentException]"</pre>
+ *
+ * If the <b>wrong</b> type of {@link Exception} was thrown, the
+ * test will also fail, this time with a message similar to the following:
+ *
+ * <pre class="code">
+ * "junit.framework.AssertionFailedError: Was expecting a [class java.lang.UnsupportedOperationException] to be thrown, but instead a [class java.lang.IllegalArgumentException] was thrown"</pre>
+ *
+ * The test for the correct {@link Exception} respects polymorphism,
+ * so you can test that any old {@link Exception} is thrown like so:
+ *
+ * <pre class="code">
+ * public class FooTest {
+ * public void testSomeBusinessLogicBadArgumentPath() {
+ * // any Exception will do...
+ * new AssertThrows(Exception.class) {
+ * public void test() {
+ * new Foo().someBusinessLogic(null);
+ * }
+ * }.runTest();
+ * }
+ * }</pre>
+ *
+ * Intended for use with JUnit 4 and TestNG (as of Spring 3.0).
+ * You might want to compare this class with the
+ * {@code junit.extensions.ExceptionTestCase} class.
+ *
+ * @author Rick Evans
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @deprecated favor use of JUnit 4's {@code @Test(expected=...)} support
+ */
+@Deprecated
+@SuppressWarnings({ "unchecked", "rawtypes" })
+public abstract class AssertThrows {
+
+ private final Class expectedException;
+
+ private String failureMessage;
+
+ private Exception actualException;
+
+
+ /**
+ * Create a new instance of the {@link AssertThrows} class.
+ * @param expectedException the {@link Exception} expected to be
+ * thrown during the execution of the surrounding test
+ * @throws IllegalArgumentException if the supplied {@code expectedException} is
+ * {@code null}; or if said argument is not an {@link Exception}-derived class
+ */
+ public AssertThrows(Class expectedException) {
+ this(expectedException, null);
+ }
+
+ /**
+ * Create a new instance of the {@link AssertThrows} class.
+ * @param expectedException the {@link Exception} expected to be
+ * thrown during the execution of the surrounding test
+ * @param failureMessage the extra, contextual failure message that will be
+ * included in the failure text if the text fails (can be {@code null})
+ * @throws IllegalArgumentException if the supplied {@code expectedException} is
+ * {@code null}; or if said argument is not an {@link Exception}-derived class
+ */
+ public AssertThrows(Class expectedException, String failureMessage) {
+ if (expectedException == null) {
+ throw new IllegalArgumentException("The 'expectedException' argument is required");
+ }
+ if (!Exception.class.isAssignableFrom(expectedException)) {
+ throw new IllegalArgumentException(
+ "The 'expectedException' argument is not an Exception type (it obviously must be)");
+ }
+ this.expectedException = expectedException;
+ this.failureMessage = failureMessage;
+ }
+
+
+ /**
+ * Return the {@link java.lang.Exception} expected to be thrown during
+ * the execution of the surrounding test.
+ */
+ protected Class getExpectedException() {
+ return this.expectedException;
+ }
+
+ /**
+ * Set the extra, contextual failure message that will be included
+ * in the failure text if the text fails.
+ */
+ public void setFailureMessage(String failureMessage) {
+ this.failureMessage = failureMessage;
+ }
+
+ /**
+ * Return the extra, contextual failure message that will be included
+ * in the failure text if the text fails.
+ */
+ protected String getFailureMessage() {
+ return this.failureMessage;
+ }
+
+
+ /**
+ * Subclass must override this {@code abstract} method and
+ * provide the test logic.
+ * @throws Exception if an error occurs during the execution of the
+ * aformentioned test logic
+ */
+ public abstract void test() throws Exception;
+
+
+ /**
+ * The main template method that drives the running of the
+ * {@link #test() test logic} and the
+ * {@link #checkExceptionExpectations(Exception) checking} of the
+ * resulting (expected) {@link java.lang.Exception}.
+ * @see #test()
+ * @see #doFail()
+ * @see #checkExceptionExpectations(Exception)
+ */
+ public void runTest() {
+ try {
+ test();
+ doFail();
+ }
+ catch (Exception actualException) {
+ this.actualException = actualException;
+ checkExceptionExpectations(actualException);
+ }
+ }
+
+ /**
+ * Template method called when the test fails; i.e. the expected
+ * {@link java.lang.Exception} is <b>not</b> thrown.
+ * <p>The default implementation simply fails the test via a call to
+ * {@link org.junit.Assert#fail(String)}.
+ * <p>If you want to customise the failure message, consider overriding
+ * {@link #createMessageForNoExceptionThrown()}, and / or supplying an
+ * extra, contextual failure message via the appropriate constructor overload.
+ * @see #getFailureMessage()
+ */
+ protected void doFail() {
+ throw new AssertionError(createMessageForNoExceptionThrown());
+ }
+
+ /**
+ * Creates the failure message used if the test fails
+ * (i.e. the expected exception is not thrown in the body of the test).
+ * @return the failure message used if the test fails
+ * @see #getFailureMessage()
+ */
+ protected String createMessageForNoExceptionThrown() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Should have thrown a [").append(this.getExpectedException()).append("]");
+ if (getFailureMessage() != null) {
+ sb.append(": ").append(getFailureMessage());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Does the donkey work of checking (verifying) that the
+ * {@link Exception} that was thrown in the body of a test is
+ * an instance of the {@link #getExpectedException()} class (or an
+ * instance of a subclass).
+ * <p>If you want to customise the failure message, consider overriding
+ * {@link #createMessageForWrongThrownExceptionType(Exception)}.
+ * @param actualException the {@link Exception} that has been thrown
+ * in the body of a test method (will never be {@code null})
+ */
+ protected void checkExceptionExpectations(Exception actualException) {
+ if (!getExpectedException().isAssignableFrom(actualException.getClass())) {
+ AssertionError error =
+ new AssertionError(createMessageForWrongThrownExceptionType(actualException));
+ error.initCause(actualException);
+ throw error;
+ }
+ }
+
+ /**
+ * Creates the failure message used if the wrong type
+ * of {@link java.lang.Exception} is thrown in the body of the test.
+ * @param actualException the actual exception thrown
+ * @return the message for the given exception
+ */
+ protected String createMessageForWrongThrownExceptionType(Exception actualException) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Was expecting a [").append(getExpectedException().getName());
+ sb.append("] to be thrown, but instead a [").append(actualException.getClass().getName());
+ sb.append("] was thrown.");
+ return sb.toString();
+ }
+
+
+ /**
+ * Expose the actual exception thrown from {@link #test}, if any.
+ * @return the actual exception, or {@code null} if none
+ */
+ public final Exception getActualException() {
+ return this.actualException;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/ConditionalTestCase.java b/spring-test/src/main/java/org/springframework/test/ConditionalTestCase.java
new file mode 100644
index 00000000..a8448f22
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/ConditionalTestCase.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2002-2007 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;
+
+import junit.framework.TestCase;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Superclass for JUnit 3.8 based tests that allows conditional test execution
+ * at the individual test method level. The
+ * {@link #isDisabledInThisEnvironment(String) isDisabledInThisEnvironment()}
+ * method is invoked before the execution of each test method. Subclasses can
+ * override that method to return whether or not the given test should be
+ * executed. Note that the tests will still appear to have executed and passed;
+ * however, log output will show that the test was not executed.
+ *
+ * @author Rod Johnson
+ * @since 2.0
+ * @see #isDisabledInThisEnvironment
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests})
+ */
+@Deprecated
+public abstract class ConditionalTestCase extends TestCase {
+
+ private static int disabledTestCount;
+
+
+ /**
+ * Return the number of tests disabled in this environment.
+ */
+ public static int getDisabledTestCount() {
+ return disabledTestCount;
+ }
+
+
+ /** Logger available to subclasses */
+ protected final Log logger = LogFactory.getLog(getClass());
+
+
+ /**
+ * Default constructor for ConditionalTestCase.
+ */
+ public ConditionalTestCase() {
+ }
+
+ /**
+ * Constructor for ConditionalTestCase with a JUnit name.
+ */
+ public ConditionalTestCase(String name) {
+ super(name);
+ }
+
+ public void runBare() throws Throwable {
+ // getName will return the name of the method being run
+ if (isDisabledInThisEnvironment(getName())) {
+ recordDisabled();
+ this.logger.info("**** " + getClass().getName() + "." + getName() + " is disabled in this environment: "
+ + "Total disabled tests = " + getDisabledTestCount());
+ return;
+ }
+
+ // Let JUnit handle execution
+ super.runBare();
+ }
+
+ /**
+ * Should this test run?
+ * @param testMethodName name of the test method
+ * @return whether the test should execute in the current environment
+ */
+ protected boolean isDisabledInThisEnvironment(String testMethodName) {
+ return false;
+ }
+
+ /**
+ * Record a disabled test.
+ * @return the current disabled test count
+ */
+ protected int recordDisabled() {
+ return ++disabledTestCount;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java b/spring-test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java
new file mode 100644
index 00000000..30c08a22
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java
@@ -0,0 +1,313 @@
+/*
+ * 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.annotation;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import junit.framework.AssertionFailedError;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
+import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
+import org.springframework.transaction.interceptor.TransactionAttributeSource;
+import org.springframework.util.Assert;
+
+/**
+ * <p>
+ * Java 5 specific subclass of
+ * {@link AbstractTransactionalDataSourceSpringContextTests}, exposing a
+ * {@link SimpleJdbcTemplate} and obeying annotations for transaction control.
+ * </p>
+ * <p>
+ * For example, test methods can be annotated with the regular Spring
+ * {@link org.springframework.transaction.annotation.Transactional @Transactional}
+ * annotation (e.g., to force execution in a read-only transaction) or with the
+ * {@link NotTransactional @NotTransactional} annotation to prevent any
+ * transaction being created at all. In addition, individual test methods can be
+ * annotated with {@link Rollback @Rollback} to override the
+ * {@link #isDefaultRollback() default rollback} settings.
+ * </p>
+ * <p>
+ * The following list constitutes all annotations currently supported by
+ * AbstractAnnotationAwareTransactionalTests:
+ * </p>
+ * <ul>
+ * <li>{@link DirtiesContext @DirtiesContext}</li>
+ * <li>{@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}</li>
+ * <li>{@link IfProfileValue @IfProfileValue}</li>
+ * <li>{@link ExpectedException @ExpectedException}</li>
+ * <li>{@link Timed @Timed}</li>
+ * <li>{@link Repeat @Repeat}</li>
+ * <li>{@link org.springframework.transaction.annotation.Transactional @Transactional}</li>
+ * <li>{@link NotTransactional @NotTransactional}</li>
+ * <li>{@link Rollback @Rollback}</li>
+ * </ul>
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests})
+ */
+@Deprecated
+public abstract class AbstractAnnotationAwareTransactionalTests extends
+ AbstractTransactionalDataSourceSpringContextTests {
+
+ protected SimpleJdbcTemplate simpleJdbcTemplate;
+
+ private final TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource();
+
+ /**
+ * {@link ProfileValueSource} available to subclasses but primarily intended
+ * for use in {@link #isDisabledInThisEnvironment(Method)}.
+ * <p>Set to {@link SystemProfileValueSource} by default for backwards
+ * compatibility; however, the value may be changed in the
+ * {@link #AbstractAnnotationAwareTransactionalTests(String)} constructor.
+ */
+ protected ProfileValueSource profileValueSource = SystemProfileValueSource.getInstance();
+
+
+ /**
+ * Default constructor for AbstractAnnotationAwareTransactionalTests, which
+ * delegates to {@link #AbstractAnnotationAwareTransactionalTests(String)}.
+ */
+ public AbstractAnnotationAwareTransactionalTests() {
+ this(null);
+ }
+
+ /**
+ * Constructs a new AbstractAnnotationAwareTransactionalTests instance with
+ * the specified JUnit {@code name} and retrieves the configured (or
+ * default) {@link ProfileValueSource}.
+ * @param name the name of the current test
+ * @see ProfileValueUtils#retrieveProfileValueSource(Class)
+ */
+ public AbstractAnnotationAwareTransactionalTests(String name) {
+ super(name);
+ this.profileValueSource = ProfileValueUtils.retrieveProfileValueSource(getClass());
+ }
+
+
+ @Override
+ public void setDataSource(DataSource dataSource) {
+ super.setDataSource(dataSource);
+ // JdbcTemplate will be identically configured
+ this.simpleJdbcTemplate = new SimpleJdbcTemplate(this.jdbcTemplate);
+ }
+
+ /**
+ * Search for a unique {@link ProfileValueSource} in the supplied
+ * {@link ApplicationContext}. If found, the
+ * {@code profileValueSource} for this test will be set to the unique
+ * {@link ProfileValueSource}.
+ * @param applicationContext the ApplicationContext in which to search for
+ * the ProfileValueSource; may not be {@code null}
+ * @deprecated Use {@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration} instead.
+ */
+ @Deprecated
+ protected void findUniqueProfileValueSourceFromContext(ApplicationContext applicationContext) {
+ Assert.notNull(applicationContext, "Can not search for a ProfileValueSource in a null ApplicationContext.");
+ ProfileValueSource uniqueProfileValueSource = null;
+ Map<?, ?> beans = applicationContext.getBeansOfType(ProfileValueSource.class);
+ if (beans.size() == 1) {
+ uniqueProfileValueSource = (ProfileValueSource) beans.values().iterator().next();
+ }
+ if (uniqueProfileValueSource != null) {
+ this.profileValueSource = uniqueProfileValueSource;
+ }
+ }
+
+ /**
+ * Overridden to populate transaction definition from annotations.
+ */
+ @Override
+ public void runBare() throws Throwable {
+
+ // getName will return the name of the method being run.
+ if (isDisabledInThisEnvironment(getName())) {
+ // Let superclass log that we didn't run the test.
+ super.runBare();
+ return;
+ }
+
+ final Method testMethod = getTestMethod();
+
+ if (isDisabledInThisEnvironment(testMethod)) {
+ recordDisabled();
+ this.logger.info("**** " + getClass().getName() + "." + getName() + " is disabled in this environment: "
+ + "Total disabled tests=" + getDisabledTestCount());
+ return;
+ }
+
+ TransactionDefinition explicitTransactionDefinition =
+ this.transactionAttributeSource.getTransactionAttribute(testMethod, getClass());
+ if (explicitTransactionDefinition != null) {
+ this.logger.info("Custom transaction definition [" + explicitTransactionDefinition + "] for test method ["
+ + getName() + "].");
+ setTransactionDefinition(explicitTransactionDefinition);
+ }
+ else if (testMethod.isAnnotationPresent(NotTransactional.class)) {
+ // Don't have any transaction...
+ preventTransaction();
+ }
+
+ // Let JUnit handle execution. We're just changing the state of the test class first.
+ runTestTimed(new TestExecutionCallback() {
+ public void run() throws Throwable {
+ try {
+ AbstractAnnotationAwareTransactionalTests.super.runBare();
+ }
+ finally {
+ // Mark the context to be blown away if the test was
+ // annotated to result in setDirty being invoked
+ // automatically.
+ if (testMethod.isAnnotationPresent(DirtiesContext.class)) {
+ AbstractAnnotationAwareTransactionalTests.this.setDirty();
+ }
+ }
+ }
+ }, testMethod);
+ }
+
+ /**
+ * Determine if the test for the supplied {@code testMethod} should
+ * run in the current environment.
+ * <p>The default implementation is based on
+ * {@link IfProfileValue @IfProfileValue} semantics.
+ * @param testMethod the test method
+ * @return {@code true} if the test is <em>disabled</em> in the current environment
+ * @see ProfileValueUtils#isTestEnabledInThisEnvironment
+ */
+ protected boolean isDisabledInThisEnvironment(Method testMethod) {
+ return !ProfileValueUtils.isTestEnabledInThisEnvironment(this.profileValueSource, testMethod, getClass());
+ }
+
+ /**
+ * Get the current test method.
+ */
+ protected Method getTestMethod() {
+ assertNotNull("TestCase.getName() cannot be null", getName());
+ Method testMethod = null;
+ try {
+ // Use same algorithm as JUnit itself to retrieve the test method
+ // about to be executed (the method name is returned by getName). It
+ // has to be public so we can retrieve it.
+ testMethod = getClass().getMethod(getName(), (Class[]) null);
+ }
+ catch (NoSuchMethodException ex) {
+ fail("Method '" + getName() + "' not found");
+ }
+ if (!Modifier.isPublic(testMethod.getModifiers())) {
+ fail("Method '" + getName() + "' should be public");
+ }
+ return testMethod;
+ }
+
+ /**
+ * Determine whether or not to rollback transactions for the current test
+ * by taking into consideration the
+ * {@link #isDefaultRollback() default rollback} flag and a possible
+ * method-level override via the {@link Rollback @Rollback} annotation.
+ * @return the <em>rollback</em> flag for the current test
+ */
+ @Override
+ protected boolean isRollback() {
+ boolean rollback = isDefaultRollback();
+ Rollback rollbackAnnotation = getTestMethod().getAnnotation(Rollback.class);
+ if (rollbackAnnotation != null) {
+ boolean rollbackOverride = rollbackAnnotation.value();
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Method-level @Rollback(" + rollbackOverride + ") overrides default rollback ["
+ + rollback + "] for test [" + getName() + "].");
+ }
+ rollback = rollbackOverride;
+ }
+ else {
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("No method-level @Rollback override: using default rollback [" + rollback
+ + "] for test [" + getName() + "].");
+ }
+ }
+ return rollback;
+ }
+
+ private void runTestTimed(TestExecutionCallback tec, Method testMethod) throws Throwable {
+ Timed timed = testMethod.getAnnotation(Timed.class);
+ if (timed == null) {
+ runTest(tec, testMethod);
+ }
+ else {
+ long startTime = System.currentTimeMillis();
+ try {
+ runTest(tec, testMethod);
+ }
+ finally {
+ long elapsed = System.currentTimeMillis() - startTime;
+ if (elapsed > timed.millis()) {
+ fail("Took " + elapsed + " ms; limit was " + timed.millis());
+ }
+ }
+ }
+ }
+
+ private void runTest(TestExecutionCallback tec, Method testMethod) throws Throwable {
+ ExpectedException expectedExceptionAnnotation = testMethod.getAnnotation(ExpectedException.class);
+ boolean exceptionIsExpected = (expectedExceptionAnnotation != null && expectedExceptionAnnotation.value() != null);
+ Class<? extends Throwable> expectedException = (exceptionIsExpected ? expectedExceptionAnnotation.value() : null);
+
+ Repeat repeat = testMethod.getAnnotation(Repeat.class);
+ int runs = ((repeat != null) && (repeat.value() > 1)) ? repeat.value() : 1;
+
+ for (int i = 0; i < runs; i++) {
+ try {
+ if (runs > 1 && this.logger != null && this.logger.isInfoEnabled()) {
+ this.logger.info("Repetition " + (i + 1) + " of test " + testMethod.getName());
+ }
+ tec.run();
+ if (exceptionIsExpected) {
+ fail("Expected exception: " + expectedException.getName());
+ }
+ }
+ catch (Throwable t) {
+ if (!exceptionIsExpected) {
+ throw t;
+ }
+ if (!expectedException.isAssignableFrom(t.getClass())) {
+ // Wrap the unexpected throwable with an explicit message.
+ AssertionFailedError assertionError = new AssertionFailedError("Unexpected exception, expected<" +
+ expectedException.getName() + "> but was<" + t.getClass().getName() + ">");
+ assertionError.initCause(t);
+ throw assertionError;
+ }
+ }
+ }
+ }
+
+
+ private static interface TestExecutionCallback {
+
+ void run() throws Throwable;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java
new file mode 100644
index 00000000..aa6ed211
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2002-2013 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Test annotation which indicates that the
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}
+ * associated with a test is <em>dirty</em> and should be closed:
+ * <ul>
+ * <li>after the current test, when declared at the method level</li>
+ * <li>after each test method in the current test class, when declared at the
+ * class level with class mode set to {@link ClassMode#AFTER_EACH_TEST_METHOD
+ * AFTER_EACH_TEST_METHOD}</li>
+ * <li>after the current test class, when declared at the class level with class
+ * mode set to {@link ClassMode#AFTER_CLASS AFTER_CLASS}</li>
+ * </ul>
+ * <p>
+ * Use this annotation if a test has modified the context &mdash; for example, by
+ * replacing a bean definition or changing the state of a singleton bean.
+ * Subsequent tests will be supplied a new context.
+ * </p>
+ * <p>
+ * {@code @DirtiesContext} may be used as a class-level and method-level
+ * annotation within the same class. In such scenarios, the
+ * {@code ApplicationContext} will be marked as <em>dirty</em> after any
+ * such annotated method as well as after the entire class. If the
+ * {@link ClassMode} is set to {@link ClassMode#AFTER_EACH_TEST_METHOD
+ * AFTER_EACH_TEST_METHOD}, the context will be marked dirty after each test
+ * method in the class.
+ * </p>
+ *
+ * @author Sam Brannen
+ * @author Rod Johnson
+ * @since 2.0
+ * @see org.springframework.test.context.ContextConfiguration
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+public @interface DirtiesContext {
+
+ /**
+ * Defines <i>modes</i> which determine how {@code @DirtiesContext} is
+ * interpreted when used to annotate a test class.
+ *
+ * @since 3.0
+ */
+ static enum ClassMode {
+
+ /**
+ * The associated {@code ApplicationContext} will be marked as
+ * <em>dirty</em> after the test class.
+ */
+ AFTER_CLASS,
+
+ /**
+ * The associated {@code ApplicationContext} will be marked as
+ * <em>dirty</em> after each test method in the class.
+ */
+ AFTER_EACH_TEST_METHOD;
+ }
+
+ /**
+ * Defines <i>modes</i> which determine how the context cache is cleared
+ * when {@code @DirtiesContext} is used in a test whose context is
+ * configured as part of a hierarchy via
+ * {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}.
+ *
+ * @since 3.2.2
+ */
+ static enum HierarchyMode {
+
+ /**
+ * The context cache will be cleared using an <em>exhaustive</em> algorithm
+ * that includes not only the {@linkplain HierarchyMode#CURRENT_LEVEL current level}
+ * but also all other context hierarchies that share an ancestor context
+ * common to the current test.
+ *
+ * <p>All {@code ApplicationContexts} that reside in a subhierarchy of
+ * the common ancestor context will be removed from the context cache and
+ * closed.
+ */
+ EXHAUSTIVE,
+
+ /**
+ * The {@code ApplicationContext} for the <em>current level</em> in the
+ * context hierarchy and all contexts in subhierarchies of the current
+ * level will be removed from the context cache and closed.
+ *
+ * <p>The <em>current level</em> refers to the {@code ApplicationContext}
+ * at the lowest level in the context hierarchy that is visible from the
+ * current test.
+ */
+ CURRENT_LEVEL;
+ }
+
+
+ /**
+ * The <i>mode</i> to use when a test class is annotated with
+ * {@code @DirtiesContext}.
+ * <p>Defaults to {@link ClassMode#AFTER_CLASS AFTER_CLASS}.
+ * <p>Note: Setting the class mode on an annotated test method has no meaning,
+ * since the mere presence of the {@code @DirtiesContext} annotation on a
+ * test method is sufficient.
+ *
+ * @since 3.0
+ */
+ ClassMode classMode() default ClassMode.AFTER_CLASS;
+
+ /**
+ * The context cache clearing <em>mode</em> to use when a context is
+ * configured as part of a hierarchy via
+ * {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}.
+ * <p>Defaults to {@link HierarchyMode#EXHAUSTIVE EXHAUSTIVE}.
+ *
+ * @since 3.2.2
+ */
+ HierarchyMode hierarchyMode() default HierarchyMode.EXHAUSTIVE;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ExpectedException.java b/spring-test/src/main/java/org/springframework/test/annotation/ExpectedException.java
new file mode 100644
index 00000000..944f3aea
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/ExpectedException.java
@@ -0,0 +1,43 @@
+/*
+ * 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Test annotation to indicate that a test method is required to throw the
+ * specified exception.
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ * @deprecated as of Spring 3.1 in favor of using built-in support for declaring
+ * expected exceptions in the underlying testing framework (e.g., JUnit, TestNG, etc.)
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Deprecated
+public @interface ExpectedException {
+
+ Class<? extends Throwable> value();
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java b/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java
new file mode 100644
index 00000000..c5c71846
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java
@@ -0,0 +1,105 @@
+/*
+ * 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Test annotation to indicate that a test is enabled for a specific testing
+ * profile or environment. If the configured {@link ProfileValueSource} returns
+ * a matching {@link #value() value} for the provided {@link #name() name}, the
+ * test will be enabled.
+ * </p>
+ * <p>
+ * Note: {@code &#064;IfProfileValue} can be applied at the class level,
+ * the method level, or both. {@code &#064;IfProfileValue} at the class
+ * level overrides method-level usage of {@code &#064;IfProfileValue} for
+ * any methods within that class.
+ * </p>
+ * <p>
+ * Examples: when using {@link SystemProfileValueSource} as the
+ * {@link ProfileValueSource} implementation, you can configure a test method to
+ * run only on Java VMs from Sun Microsystems as follows:
+ * </p>
+ *
+ * <pre class="code">
+ * &#064;IfProfileValue(name = &quot;java.vendor&quot;, value = &quot;Sun Microsystems Inc.&quot;)
+ * public void testSomething() {
+ * // ...
+ * }
+ * </pre>
+ * <p>
+ * You can alternatively configure {@code &#064;IfProfileValue} with
+ * <em>OR</em> semantics for multiple {@link #values() values} as follows
+ * (assuming a {@link ProfileValueSource} has been appropriately configured for
+ * the &quot;test-groups&quot; name):
+ * </p>
+ *
+ * <pre class="code">
+ * &#064;IfProfileValue(name = &quot;test-groups&quot;, values = { &quot;unit-tests&quot;, &quot;integration-tests&quot; })
+ * public void testWhichRunsForUnitOrIntegrationTestGroups() {
+ * // ...
+ * }
+ * </pre>
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ * @see ProfileValueSource
+ * @see ProfileValueSourceConfiguration
+ * @see ProfileValueUtils
+ * @see org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests
+ * @see org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests
+ * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target( { ElementType.TYPE, ElementType.METHOD })
+public @interface IfProfileValue {
+
+ /**
+ * The {@code name} of the <em>profile value</em> against which to
+ * test.
+ */
+ String name();
+
+ /**
+ * A single, permissible {@code value} of the <em>profile value</em>
+ * for the given {@link #name() name}.
+ * <p>
+ * Note: Assigning values to both {@link #value()} and {@link #values()}
+ * will lead to a configuration conflict.
+ */
+ String value() default "";
+
+ /**
+ * A list of all permissible {@code values} of the
+ * <em>profile value</em> for the given {@link #name() name}.
+ * <p>
+ * Note: Assigning values to both {@link #value()} and {@link #values()}
+ * will lead to a configuration conflict.
+ */
+ String[] values() default {};
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/NotTransactional.java b/spring-test/src/main/java/org/springframework/test/annotation/NotTransactional.java
new file mode 100644
index 00000000..c8539186
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/NotTransactional.java
@@ -0,0 +1,43 @@
+/*
+ * 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Test annotation to indicate that a method is <i>not transactional</i>.
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ * @deprecated as of Spring 3.0, in favor of moving the non-transactional test
+ * method to a separate (non-transactional) test class or to a
+ * {@link org.springframework.test.context.transaction.BeforeTransaction
+ * &#64;BeforeTransaction} or
+ * {@link org.springframework.test.context.transaction.AfterTransaction
+ * &#64;AfterTransaction} method.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Deprecated
+public @interface NotTransactional {
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java
new file mode 100644
index 00000000..05ee407f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java
@@ -0,0 +1,52 @@
+/*
+ * 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.annotation;
+
+/**
+ * <p>
+ * Strategy interface for retrieving <em>profile values</em> for a given
+ * testing environment.
+ * </p>
+ * <p>
+ * Concrete implementations must provide a {@code public} no-args
+ * constructor.
+ * </p>
+ * <p>
+ * Spring provides the following out-of-the-box implementations:
+ * </p>
+ * <ul>
+ * <li>{@link SystemProfileValueSource}</li>
+ * </ul>
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ * @see ProfileValueSourceConfiguration
+ * @see IfProfileValue
+ * @see ProfileValueUtils
+ */
+public interface ProfileValueSource {
+
+ /**
+ * Get the <em>profile value</em> indicated by the specified key.
+ * @param key the name of the <em>profile value</em>
+ * @return the String value of the <em>profile value</em>, or {@code null}
+ * if there is no <em>profile value</em> with that key
+ */
+ String get(String key);
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java
new file mode 100644
index 00000000..4e240aa6
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java
@@ -0,0 +1,56 @@
+/*
+ * 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * ProfileValueSourceConfiguration is a class-level annotation which is used to
+ * specify what type of {@link ProfileValueSource} to use when retrieving
+ * <em>profile values</em> configured via the {@link IfProfileValue
+ * &#064;IfProfileValue} annotation.
+ * </p>
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ * @see ProfileValueSource
+ * @see IfProfileValue
+ * @see ProfileValueUtils
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ProfileValueSourceConfiguration {
+
+ /**
+ * <p>
+ * The type of {@link ProfileValueSource} to use when retrieving
+ * <em>profile values</em>.
+ * </p>
+ *
+ * @see SystemProfileValueSource
+ */
+ Class<? extends ProfileValueSource> value() default SystemProfileValueSource.class;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java
new file mode 100644
index 00000000..922e34c6
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java
@@ -0,0 +1,209 @@
+/*
+ * 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.annotation;
+
+import java.lang.reflect.Method;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * General utility methods for working with <em>profile values</em>.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see ProfileValueSource
+ * @see ProfileValueSourceConfiguration
+ * @see IfProfileValue
+ */
+public abstract class ProfileValueUtils {
+
+ private static final Log logger = LogFactory.getLog(ProfileValueUtils.class);
+
+
+ /**
+ * Retrieves the {@link ProfileValueSource} type for the specified
+ * {@link Class test class} as configured via the
+ * {@link ProfileValueSourceConfiguration
+ * &#064;ProfileValueSourceConfiguration} annotation and instantiates a new
+ * instance of that type.
+ * <p>
+ * If {@link ProfileValueSourceConfiguration
+ * &#064;ProfileValueSourceConfiguration} is not present on the specified
+ * class or if a custom {@link ProfileValueSource} is not declared, the
+ * default {@link SystemProfileValueSource} will be returned instead.
+ *
+ * @param testClass The test class for which the ProfileValueSource should
+ * be retrieved
+ * @return the configured (or default) ProfileValueSource for the specified
+ * class
+ * @see SystemProfileValueSource
+ */
+ @SuppressWarnings("unchecked")
+ public static ProfileValueSource retrieveProfileValueSource(Class<?> testClass) {
+ Assert.notNull(testClass, "testClass must not be null");
+
+ Class<ProfileValueSourceConfiguration> annotationType = ProfileValueSourceConfiguration.class;
+ ProfileValueSourceConfiguration config = testClass.getAnnotation(annotationType);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Retrieved @ProfileValueSourceConfiguration [" + config + "] for test class ["
+ + testClass.getName() + "]");
+ }
+
+ Class<? extends ProfileValueSource> profileValueSourceType;
+ if (config != null) {
+ profileValueSourceType = config.value();
+ }
+ else {
+ profileValueSourceType = (Class<? extends ProfileValueSource>) AnnotationUtils.getDefaultValue(annotationType);
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug("Retrieved ProfileValueSource type [" + profileValueSourceType + "] for class ["
+ + testClass.getName() + "]");
+ }
+
+ ProfileValueSource profileValueSource;
+ if (SystemProfileValueSource.class.equals(profileValueSourceType)) {
+ profileValueSource = SystemProfileValueSource.getInstance();
+ }
+ else {
+ try {
+ profileValueSource = profileValueSourceType.newInstance();
+ }
+ catch (Exception e) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Could not instantiate a ProfileValueSource of type [" + profileValueSourceType
+ + "] for class [" + testClass.getName() + "]: using default.", e);
+ }
+ profileValueSource = SystemProfileValueSource.getInstance();
+ }
+ }
+
+ return profileValueSource;
+ }
+
+ /**
+ * Determine if the supplied {@code testClass} is <em>enabled</em> in
+ * the current environment, as specified by the {@link IfProfileValue
+ * &#064;IfProfileValue} annotation at the class level.
+ * <p>
+ * Defaults to {@code true} if no {@link IfProfileValue
+ * &#064;IfProfileValue} annotation is declared.
+ *
+ * @param testClass the test class
+ * @return {@code true} if the test is <em>enabled</em> in the current
+ * environment
+ */
+ public static boolean isTestEnabledInThisEnvironment(Class<?> testClass) {
+ IfProfileValue ifProfileValue = testClass.getAnnotation(IfProfileValue.class);
+ return isTestEnabledInThisEnvironment(retrieveProfileValueSource(testClass), ifProfileValue);
+ }
+
+ /**
+ * Determine if the supplied {@code testMethod} is <em>enabled</em> in
+ * the current environment, as specified by the {@link IfProfileValue
+ * &#064;IfProfileValue} annotation, which may be declared on the test
+ * method itself or at the class level. Class-level usage overrides
+ * method-level usage.
+ * <p>
+ * Defaults to {@code true} if no {@link IfProfileValue
+ * &#064;IfProfileValue} annotation is declared.
+ *
+ * @param testMethod the test method
+ * @param testClass the test class
+ * @return {@code true} if the test is <em>enabled</em> in the current
+ * environment
+ */
+ public static boolean isTestEnabledInThisEnvironment(Method testMethod, Class<?> testClass) {
+ return isTestEnabledInThisEnvironment(retrieveProfileValueSource(testClass), testMethod, testClass);
+ }
+
+ /**
+ * Determine if the supplied {@code testMethod} is <em>enabled</em> in
+ * the current environment, as specified by the {@link IfProfileValue
+ * &#064;IfProfileValue} annotation, which may be declared on the test
+ * method itself or at the class level. Class-level usage overrides
+ * method-level usage.
+ * <p>
+ * Defaults to {@code true} if no {@link IfProfileValue
+ * &#064;IfProfileValue} annotation is declared.
+ *
+ * @param profileValueSource the ProfileValueSource to use to determine if
+ * the test is enabled
+ * @param testMethod the test method
+ * @param testClass the test class
+ * @return {@code true} if the test is <em>enabled</em> in the current
+ * environment
+ */
+ public static boolean isTestEnabledInThisEnvironment(ProfileValueSource profileValueSource, Method testMethod,
+ Class<?> testClass) {
+
+ IfProfileValue ifProfileValue = testClass.getAnnotation(IfProfileValue.class);
+ boolean classLevelEnabled = isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue);
+
+ if (classLevelEnabled) {
+ ifProfileValue = testMethod.getAnnotation(IfProfileValue.class);
+ return isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue);
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine if the {@code value} (or one of the {@code values})
+ * in the supplied {@link IfProfileValue &#064;IfProfileValue} annotation is
+ * <em>enabled</em> in the current environment.
+ *
+ * @param profileValueSource the ProfileValueSource to use to determine if
+ * the test is enabled
+ * @param ifProfileValue the annotation to introspect; may be
+ * {@code null}
+ * @return {@code true} if the test is <em>enabled</em> in the current
+ * environment or if the supplied {@code ifProfileValue} is
+ * {@code null}
+ */
+ private static boolean isTestEnabledInThisEnvironment(ProfileValueSource profileValueSource,
+ IfProfileValue ifProfileValue) {
+
+ if (ifProfileValue == null) {
+ return true;
+ }
+
+ String environmentValue = profileValueSource.get(ifProfileValue.name());
+ String[] annotatedValues = ifProfileValue.values();
+ if (StringUtils.hasLength(ifProfileValue.value())) {
+ if (annotatedValues.length > 0) {
+ throw new IllegalArgumentException("Setting both the 'value' and 'values' attributes "
+ + "of @IfProfileValue is not allowed: choose one or the other.");
+ }
+ annotatedValues = new String[] { ifProfileValue.value() };
+ }
+
+ for (String value : annotatedValues) {
+ if (ObjectUtils.nullSafeEquals(value, environmentValue)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java
new file mode 100644
index 00000000..4c3dcd3a
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java
@@ -0,0 +1,43 @@
+/*
+ * 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Test annotation to indicate that a test method should be invoked repeatedly.
+ * <p />
+ * Note that the scope of execution to be repeated includes execution of the
+ * test method itself as well as any <em>set up</em> or <em>tear down</em> of
+ * the test fixture.
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Repeat {
+
+ int value() default 1;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java
new file mode 100644
index 00000000..bb0bc88c
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java
@@ -0,0 +1,45 @@
+/*
+ * 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Test annotation to indicate whether or not the transaction for the annotated
+ * test method should be <em>rolled back</em> after the test method has
+ * completed. If {@code true}, the transaction will be rolled back;
+ * otherwise, the transaction will be committed.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Rollback {
+
+ /**
+ * Whether or not the transaction for the annotated method should be rolled
+ * back after the method has completed.
+ */
+ boolean value() default true;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java b/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java
new file mode 100644
index 00000000..f806acc1
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2007 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.annotation;
+
+import org.springframework.util.Assert;
+
+/**
+ * Implementation of {@link ProfileValueSource} which uses system properties as
+ * the underlying source.
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ */
+public class SystemProfileValueSource implements ProfileValueSource {
+
+ private static final SystemProfileValueSource INSTANCE = new SystemProfileValueSource();
+
+
+ /**
+ * Obtain the canonical instance of this ProfileValueSource.
+ */
+ public static final SystemProfileValueSource getInstance() {
+ return INSTANCE;
+ }
+
+
+ /**
+ * Private constructor, enforcing the singleton pattern.
+ */
+ private SystemProfileValueSource() {
+ }
+
+ /**
+ * Get the <em>profile value</em> indicated by the specified key from the
+ * system properties.
+ * @see System#getProperty(String)
+ */
+ public String get(String key) {
+ Assert.hasText(key, "'key' must not be empty");
+ return System.getProperty(key);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Timed.java b/spring-test/src/main/java/org/springframework/test/annotation/Timed.java
new file mode 100644
index 00000000..a78960ce
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/Timed.java
@@ -0,0 +1,57 @@
+/*
+ * 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Test-specific annotation to indicate that a test method has to finish
+ * execution in a {@link #millis() specified time period}.
+ * </p>
+ * <p>
+ * If the text execution takes longer than the specified time period, then the
+ * test is to be considered failed.
+ * </p>
+ * <p>
+ * Note that the time period includes execution of the test method itself, any
+ * {@link Repeat repetitions} of the test, and any <em>set up</em> or
+ * <em>tear down</em> of the test fixture.
+ * </p>
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ * @see Repeat
+ * @see AbstractAnnotationAwareTransactionalTests
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Timed {
+
+ /**
+ * The maximum amount of time (in milliseconds) that a test execution can
+ * take without being marked as failed due to taking too long.
+ */
+ long millis();
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/package-info.java b/spring-test/src/main/java/org/springframework/test/annotation/package-info.java
new file mode 100644
index 00000000..1695b36c
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/annotation/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Support classes for annotation-driven tests.
+ */
+
+package org.springframework.test.annotation;
+
diff --git a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java
new file mode 100644
index 00000000..39bfb5d3
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java
@@ -0,0 +1,109 @@
+/*
+ * 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.context;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * {@code ActiveProfiles} is a class-level annotation that is used to declare
+ * which <em>active bean definition profiles</em> should be used when loading
+ * an {@link org.springframework.context.ApplicationContext ApplicationContext}
+ * for test classes.
+ *
+ * @author Sam Brannen
+ * @since 3.1
+ * @see SmartContextLoader
+ * @see MergedContextConfiguration
+ * @see ContextConfiguration
+ * @see org.springframework.context.ApplicationContext
+ * @see org.springframework.context.annotation.Profile
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ActiveProfiles {
+
+ /**
+ * Alias for {@link #profiles}.
+ *
+ * <p>This attribute may <strong>not</strong> be used in conjunction
+ * with {@link #profiles}, but it may be used <em>instead</em> of
+ * {@link #profiles}.
+ */
+ String[] value() default {};
+
+ /**
+ * The bean definition profiles to activate.
+ *
+ * <p>This attribute may <strong>not</strong> be used in conjunction
+ * with {@link #value}, but it may be used <em>instead</em> of
+ * {@link #value}.
+ */
+ String[] profiles() default {};
+
+ /**
+ * Whether or not bean definition profiles from superclasses should be
+ * <em>inherited</em>.
+ *
+ * <p>The default value is {@code true}, which means that a test
+ * class will <em>inherit</em> bean definition profiles defined by a
+ * test superclass. Specifically, the bean definition profiles for a test
+ * class will be appended to the list of bean definition profiles
+ * defined by a test superclass. Thus, subclasses have the option of
+ * <em>extending</em> the list of bean definition profiles.
+ *
+ * <p>If {@code inheritProfiles} is set to {@code false}, the bean
+ * definition profiles for the test class will <em>shadow</em> and
+ * effectively replace any bean definition profiles defined by a superclass.
+ *
+ * <p>In the following example, the {@code ApplicationContext} for
+ * {@code BaseTest} will be loaded using only the &quot;base&quot;
+ * bean definition profile; beans defined in the &quot;extended&quot; profile
+ * will therefore not be loaded. In contrast, the {@code ApplicationContext}
+ * for {@code ExtendedTest} will be loaded using the &quot;base&quot;
+ * <strong>and</strong> &quot;extended&quot; bean definition profiles.
+ * <pre class="code">
+ * &#064;ActiveProfiles(&quot;base&quot;)
+ * &#064;ContextConfiguration
+ * public class BaseTest {
+ * // ...
+ * }
+ *
+ * &#064;ActiveProfiles(&quot;extended&quot;)
+ * &#064;ContextConfiguration
+ * public class ExtendedTest extends BaseTest {
+ * // ...
+ * }
+ * </pre>
+ *
+ * <p>Note: {@code @ActiveProfiles} can be used when loading an
+ * {@code ApplicationContext} from path-based resource locations or
+ * annotated classes.
+ *
+ * @see ContextConfiguration#locations
+ * @see ContextConfiguration#classes
+ * @see ContextConfiguration#inheritLocations
+ */
+ boolean inheritProfiles() default true;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java
new file mode 100644
index 00000000..507d2555
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java
@@ -0,0 +1,121 @@
+/*
+ * 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.context;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.util.Assert;
+
+/**
+ * {@code CacheAwareContextLoaderDelegate} loads application contexts from
+ * {@link MergedContextConfiguration} by delegating to the
+ * {@link ContextLoader} configured in the {@code MergedContextConfiguration}
+ * and interacting transparently with the {@link ContextCache} behind the scenes.
+ *
+ * <p>Note: {@code CacheAwareContextLoaderDelegate} does not implement the
+ * {@link ContextLoader} or {@link SmartContextLoader} interface.
+ *
+ * @author Sam Brannen
+ * @since 3.2.2
+ */
+public class CacheAwareContextLoaderDelegate {
+
+ private static final Log logger = LogFactory.getLog(CacheAwareContextLoaderDelegate.class);
+
+ private static final Log statsLogger = LogFactory.getLog("org.springframework.test.context.cache");
+
+ private final ContextCache contextCache;
+
+
+ CacheAwareContextLoaderDelegate(ContextCache contextCache) {
+ Assert.notNull(contextCache, "ContextCache must not be null");
+ this.contextCache = contextCache;
+ }
+
+
+ /**
+ * Load the {@code ApplicationContext} for the supplied merged context
+ * configuration. Supports both the {@link SmartContextLoader} and
+ * {@link ContextLoader} SPIs.
+ * @throws Exception if an error occurs while loading the application context
+ */
+ private ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration)
+ throws Exception {
+
+ ContextLoader contextLoader = mergedContextConfiguration.getContextLoader();
+ Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. " +
+ "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy.");
+
+ ApplicationContext applicationContext;
+
+ if (contextLoader instanceof SmartContextLoader) {
+ SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
+ applicationContext = smartContextLoader.loadContext(mergedContextConfiguration);
+ }
+ else {
+ String[] locations = mergedContextConfiguration.getLocations();
+ Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " +
+ "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy.");
+ applicationContext = contextLoader.loadContext(locations);
+ }
+
+ return applicationContext;
+ }
+
+ /**
+ * Load the {@link ApplicationContext application context} for the supplied
+ * merged context configuration.
+ * <p>If the context is present in the cache it will simply be returned;
+ * otherwise, it will be loaded, stored in the cache, and returned.
+ * @return the application context
+ * @throws IllegalStateException if an error occurs while retrieving or
+ * loading the application context
+ */
+ public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) {
+ synchronized (this.contextCache) {
+ ApplicationContext context = this.contextCache.get(mergedContextConfiguration);
+ if (context == null) {
+ try {
+ context = loadContextInternal(mergedContextConfiguration);
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Storing ApplicationContext in cache under key [%s]",
+ mergedContextConfiguration));
+ }
+ this.contextCache.put(mergedContextConfiguration, context);
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException("Failed to load ApplicationContext", ex);
+ }
+ }
+ else {
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Retrieved ApplicationContext from cache with key [%s]",
+ mergedContextConfiguration));
+ }
+ }
+
+ if (statsLogger.isDebugEnabled()) {
+ statsLogger.debug("Spring test ApplicationContext cache statistics: " + this.contextCache);
+ }
+
+ return context;
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java
new file mode 100644
index 00000000..576021bd
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java
@@ -0,0 +1,256 @@
+/*
+ * 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.context;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
+import org.springframework.util.Assert;
+
+/**
+ * Cache for Spring {@link ApplicationContext ApplicationContexts} in a test environment.
+ *
+ * <p>Maintains a cache of {@code ApplicationContexts} keyed by
+ * {@link MergedContextConfiguration} instances.
+ *
+ * <p>This has significant performance benefits if initializing the context would take time.
+ * While initializing a Spring context itself is very quick, some beans in a context, such
+ * as a {@code LocalSessionFactoryBean} for working with Hibernate, may take some time to
+ * initialize. Hence it often makes sense to perform that initialization only once per
+ * test suite.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+class ContextCache {
+
+ /**
+ * Map of context keys to Spring {@code ApplicationContext} instances.
+ */
+ private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
+ new ConcurrentHashMap<MergedContextConfiguration, ApplicationContext>(64);
+
+ /**
+ * Map of parent keys to sets of children keys, representing a top-down <em>tree</em>
+ * of context hierarchies. This information is used for determining which subtrees
+ * need to be recursively removed and closed when removing a context that is a parent
+ * of other contexts.
+ */
+ private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap =
+ new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64);
+
+ private final AtomicInteger hitCount = new AtomicInteger();
+
+ private final AtomicInteger missCount = new AtomicInteger();
+
+
+ /**
+ * Clear all contexts from the cache and clears context hierarchy information as well.
+ */
+ public void clear() {
+ this.contextMap.clear();
+ this.hierarchyMap.clear();
+ }
+
+ /**
+ * Clear hit and miss count statistics for the cache (i.e., resets counters to zero).
+ */
+ public void clearStatistics() {
+ this.hitCount.set(0);
+ this.missCount.set(0);
+ }
+
+ /**
+ * Return whether there is a cached context for the given key.
+ * @param key the context key (never {@code null})
+ */
+ public boolean contains(MergedContextConfiguration key) {
+ Assert.notNull(key, "Key must not be null");
+ return this.contextMap.containsKey(key);
+ }
+
+ /**
+ * Obtain a cached {@code ApplicationContext} for the given key.
+ * <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will
+ * be updated accordingly.
+ * @param key the context key (never {@code null})
+ * @return the corresponding {@code ApplicationContext} instance, or {@code null}
+ * if not found in the cache
+ * @see #remove
+ */
+ public ApplicationContext get(MergedContextConfiguration key) {
+ Assert.notNull(key, "Key must not be null");
+ ApplicationContext context = this.contextMap.get(key);
+ if (context == null) {
+ this.missCount.incrementAndGet();
+ }
+ else {
+ this.hitCount.incrementAndGet();
+ }
+ return context;
+ }
+
+ /**
+ * Get the overall hit count for this cache.
+ * <p>A <em>hit</em> is an access to the cache, which returned a non-null context
+ * for a queried key.
+ */
+ public int getHitCount() {
+ return this.hitCount.get();
+ }
+
+ /**
+ * Get the overall miss count for this cache.
+ * <p>A <em>miss</em> is an access to the cache, which returned a {@code null} context
+ * for a queried key.
+ */
+ public int getMissCount() {
+ return this.missCount.get();
+ }
+
+ /**
+ * Explicitly add an {@code ApplicationContext} instance to the cache under the given key.
+ * @param key the context key (never {@code null})
+ * @param context the {@code ApplicationContext} instance (never {@code null})
+ */
+ public void put(MergedContextConfiguration key, ApplicationContext context) {
+ Assert.notNull(key, "Key must not be null");
+ Assert.notNull(context, "ApplicationContext must not be null");
+
+ this.contextMap.put(key, context);
+ MergedContextConfiguration child = key;
+ MergedContextConfiguration parent = child.getParent();
+ while (parent != null) {
+ Set<MergedContextConfiguration> list = this.hierarchyMap.get(parent);
+ if (list == null) {
+ list = new HashSet<MergedContextConfiguration>();
+ this.hierarchyMap.put(parent, list);
+ }
+ list.add(child);
+ child = parent;
+ parent = child.getParent();
+ }
+ }
+
+ /**
+ * Remove the context with the given key from the cache and explicitly
+ * {@linkplain ConfigurableApplicationContext#close() close} it if it is an
+ * instance of {@link ConfigurableApplicationContext}.
+ * <p>Generally speaking, you would only call this method if you change the
+ * state of a singleton bean, potentially affecting future interaction with
+ * the context.
+ * <p>In addition, the semantics of the supplied {@code HierarchyMode} will
+ * be honored. See the Javadoc for {@link HierarchyMode} for details.
+ * @param key the context key; never {@code null}
+ * @param hierarchyMode the hierarchy mode; may be {@code null} if the context
+ * is not part of a hierarchy
+ */
+ public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) {
+ Assert.notNull(key, "Key must not be null");
+
+ // startKey is the level at which to begin clearing the cache, depending
+ // on the configured hierarchy mode.
+ MergedContextConfiguration startKey = key;
+ if (hierarchyMode == HierarchyMode.EXHAUSTIVE) {
+ while (startKey.getParent() != null) {
+ startKey = startKey.getParent();
+ }
+ }
+
+ List<MergedContextConfiguration> removedContexts = new ArrayList<MergedContextConfiguration>();
+ remove(removedContexts, startKey);
+
+ // Remove all remaining references to any removed contexts from the
+ // hierarchy map.
+ for (MergedContextConfiguration currentKey : removedContexts) {
+ for (Set<MergedContextConfiguration> children : this.hierarchyMap.values()) {
+ children.remove(currentKey);
+ }
+ }
+
+ // Remove empty entries from the hierarchy map.
+ for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) {
+ if (this.hierarchyMap.get(currentKey).isEmpty()) {
+ this.hierarchyMap.remove(currentKey);
+ }
+ }
+ }
+
+ private void remove(List<MergedContextConfiguration> removedContexts, MergedContextConfiguration key) {
+ Assert.notNull(key, "Key must not be null");
+
+ Set<MergedContextConfiguration> children = this.hierarchyMap.get(key);
+ if (children != null) {
+ for (MergedContextConfiguration child : children) {
+ // Recurse through lower levels
+ remove(removedContexts, child);
+ }
+ // Remove the set of children for the current context from the hierarchy map.
+ this.hierarchyMap.remove(key);
+ }
+
+ // Physically remove and close leaf nodes first (i.e., on the way back up the
+ // stack as opposed to prior to the recursive call).
+ ApplicationContext context = this.contextMap.remove(key);
+ if (context instanceof ConfigurableApplicationContext) {
+ ((ConfigurableApplicationContext) context).close();
+ }
+ removedContexts.add(key);
+ }
+
+ /**
+ * Determine the number of contexts currently stored in the cache. If the cache
+ * contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
+ * <tt>Integer.MAX_VALUE</tt>.
+ */
+ public int size() {
+ return this.contextMap.size();
+ }
+
+ /**
+ * Determine the number of parent contexts currently tracked within the cache.
+ */
+ public int getParentContextCount() {
+ return this.hierarchyMap.size();
+ }
+
+ /**
+ * Generates a text string, which contains the {@linkplain #size() size} as well
+ * as the {@linkplain #getHitCount() hit}, {@linkplain #getMissCount() miss},
+ * and {@linkplain #getParentContextCount() parent context} counts.
+ */
+ @Override
+ public String toString() {
+ return new ToStringCreator(this)
+ .append("size", size())
+ .append("hitCount", getHitCount())
+ .append("missCount", getMissCount())
+ .append("parentContextCount", getParentContextCount())
+ .toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java
new file mode 100644
index 00000000..7033765c
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2002-2013 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.context;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+
+/**
+ * {@code @ContextConfiguration} defines class-level metadata that is
+ * used to determine how to load and configure an
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}
+ * for integration tests.
+ *
+ * <h3>Supported Resource Types</h3>
+ *
+ * <p>Prior to Spring 3.1, only path-based resource locations were supported.
+ * As of Spring 3.1, {@linkplain #loader context loaders} may choose to support
+ * either path-based or class-based resources (but not both). Consequently
+ * {@code @ContextConfiguration} can be used to declare either path-based
+ * resource locations (via the {@link #locations} or {@link #value}
+ * attribute) <i>or</i> annotated classes (via the {@link #classes}
+ * attribute).
+ *
+ * <h3>Annotated Classes</h3>
+ *
+ * <p>The term <em>annotated class</em> can refer to any of the following.
+ *
+ * <ul>
+ * <li>A class annotated with
+ * {@link org.springframework.context.annotation.Configuration @Configuration}</li>
+ * <li>A component (i.e., a class annotated with
+ * {@link org.springframework.stereotype.Component @Component},
+ * {@link org.springframework.stereotype.Service @Service},
+ * {@link org.springframework.stereotype.Repository @Repository}, etc.)</li>
+ * <li>A JSR-330 compliant class that is annotated with {@code javax.inject} annotations</li>
+ * <li>Any other class that contains
+ * {@link org.springframework.context.annotation.Bean @Bean}-methods</li>
+ * </ul>
+ *
+ * <p>Consult the Javadoc for
+ * {@link org.springframework.context.annotation.Configuration @Configuration} and
+ * {@link org.springframework.context.annotation.Bean @Bean}
+ * for further information regarding the configuration and semantics of
+ * <em>annotated classes</em>.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ * @see ContextHierarchy
+ * @see ActiveProfiles
+ * @see ContextLoader
+ * @see SmartContextLoader
+ * @see ContextConfigurationAttributes
+ * @see MergedContextConfiguration
+ * @see org.springframework.context.ApplicationContext
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ContextConfiguration {
+
+ /**
+ * Alias for {@link #locations}.
+ *
+ * <p>This attribute may <strong>not</strong> be used in conjunction
+ * with {@link #locations} or {@link #classes}, but it may be used
+ * instead of {@link #locations}.
+ * @since 3.0
+ * @see #inheritLocations
+ */
+ String[] value() default {};
+
+ /**
+ * The resource locations to use for loading an
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}.
+ *
+ * <p>Check out the Javadoc for
+ * {@link org.springframework.test.context.support.AbstractContextLoader#modifyLocations
+ * AbstractContextLoader.modifyLocations()} for details on how a location
+ * String will be interpreted at runtime, in particular in case of a relative
+ * path. Also, check out the documentation on
+ * {@link org.springframework.test.context.support.AbstractContextLoader#generateDefaultLocations
+ * AbstractContextLoader.generateDefaultLocations()} for details on the default
+ * locations that are going to be used if none are specified.
+ *
+ * <p>Note that the above-mentioned default rules only apply for a standard
+ * {@link org.springframework.test.context.support.AbstractContextLoader
+ * AbstractContextLoader} subclass such as
+ * {@link org.springframework.test.context.support.GenericXmlContextLoader
+ * GenericXmlContextLoader} which is the effective default implementation
+ * used at runtime if {@code locations} are configured. See the
+ * documentation for {@link #loader} for further details regarding default
+ * loaders.
+ *
+ * <p>This attribute may <strong>not</strong> be used in conjunction with
+ * {@link #value} or {@link #classes}, but it may be used instead of
+ * {@link #value}.
+ * @since 2.5
+ * @see #inheritLocations
+ */
+ String[] locations() default {};
+
+ /**
+ * The <em>annotated classes</em> to use for loading an
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}.
+ *
+ * <p>Check out the Javadoc for
+ * {@link org.springframework.test.context.support.AnnotationConfigContextLoader#detectDefaultConfigurationClasses
+ * AnnotationConfigContextLoader.detectDefaultConfigurationClasses()} for details
+ * on how default configuration classes will be detected if no
+ * <em>annotated classes</em> are specified. See the documentation for
+ * {@link #loader} for further details regarding default loaders.
+ *
+ * <p>This attribute may <strong>not</strong> be used in conjunction with
+ * {@link #locations} or {@link #value}.
+ *
+ * @since 3.1
+ * @see org.springframework.context.annotation.Configuration
+ * @see org.springframework.test.context.support.AnnotationConfigContextLoader
+ * @see #inheritLocations
+ */
+ Class<?>[] classes() default {};
+
+ /**
+ * The application context <em>initializer classes</em> to use for initializing
+ * a {@link ConfigurableApplicationContext}.
+ *
+ * <p>The concrete {@code ConfigurableApplicationContext} type supported by each
+ * declared initializer must be compatible with the type of {@code ApplicationContext}
+ * created by the {@link SmartContextLoader} in use.
+ *
+ * <p>{@code SmartContextLoader} implementations typically detect whether
+ * Spring's {@link org.springframework.core.Ordered Ordered} interface has been
+ * implemented or if the @{@link org.springframework.core.annotation.Order Order}
+ * annotation is present and sort instances accordingly prior to invoking them.
+ *
+ * @since 3.2
+ * @see org.springframework.context.ApplicationContextInitializer
+ * @see org.springframework.context.ConfigurableApplicationContext
+ * @see #inheritInitializers
+ * @see #loader
+ */
+ Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers() default {};
+
+ /**
+ * Whether or not {@link #locations resource locations} or <em>annotated
+ * classes</em> from test superclasses should be <em>inherited</em>.
+ *
+ * <p>The default value is {@code true}. This means that an annotated
+ * class will <em>inherit</em> the resource locations or annotated classes
+ * defined by test superclasses. Specifically, the resource locations or
+ * annotated classes for a given test class will be appended to the list of
+ * resource locations or annotated classes defined by test superclasses.
+ * Thus, subclasses have the option of <em>extending</em> the list of resource
+ * locations or annotated classes.
+ *
+ * <p>If {@code inheritLocations} is set to {@code false}, the
+ * resource locations or annotated classes for the annotated class
+ * will <em>shadow</em> and effectively replace any resource locations
+ * or annotated classes defined by superclasses.
+ *
+ * <p>In the following example that uses path-based resource locations, the
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}
+ * for {@code ExtendedTest} will be loaded from
+ * &quot;base-context.xml&quot; <strong>and</strong>
+ * &quot;extended-context.xml&quot;, in that order. Beans defined in
+ * &quot;extended-context.xml&quot; may therefore override those defined in
+ * &quot;base-context.xml&quot;.
+ * <pre class="code">
+ * &#064;ContextConfiguration(&quot;base-context.xml&quot;)
+ * public class BaseTest {
+ * // ...
+ * }
+ *
+ * &#064;ContextConfiguration(&quot;extended-context.xml&quot;)
+ * public class ExtendedTest extends BaseTest {
+ * // ...
+ * }
+ * </pre>
+ *
+ * <p>Similarly, in the following example that uses annotated
+ * classes, the
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}
+ * for {@code ExtendedTest} will be loaded from the
+ * {@code BaseConfig} <strong>and</strong> {@code ExtendedConfig}
+ * configuration classes, in that order. Beans defined in
+ * {@code ExtendedConfig} may therefore override those defined in
+ * {@code BaseConfig}.
+ * <pre class="code">
+ * &#064;ContextConfiguration(classes=BaseConfig.class)
+ * public class BaseTest {
+ * // ...
+ * }
+ *
+ * &#064;ContextConfiguration(classes=ExtendedConfig.class)
+ * public class ExtendedTest extends BaseTest {
+ * // ...
+ * }
+ * </pre>
+ * @since 2.5
+ */
+ boolean inheritLocations() default true;
+
+ /**
+ * Whether or not {@linkplain #initializers context initializers} from test
+ * superclasses should be <em>inherited</em>.
+ *
+ * <p>The default value is {@code true}. This means that an annotated
+ * class will <em>inherit</em> the application context initializers defined
+ * by test superclasses. Specifically, the initializers for a given test
+ * class will be added to the set of initializers defined by test
+ * superclasses. Thus, subclasses have the option of <em>extending</em> the
+ * set of initializers.
+ *
+ * <p>If {@code inheritInitializers} is set to {@code false}, the
+ * initializers for the annotated class will <em>shadow</em> and effectively
+ * replace any initializers defined by superclasses.
+ *
+ * <p>In the following example, the
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}
+ * for {@code ExtendedTest} will be initialized using
+ * {@code BaseInitializer} <strong>and</strong> {@code ExtendedInitializer}.
+ * Note, however, that the order in which the initializers are invoked
+ * depends on whether they implement {@link org.springframework.core.Ordered
+ * Ordered} or are annotated with {@link org.springframework.core.annotation.Order
+ * &#064;Order}.
+ * <pre class="code">
+ * &#064;ContextConfiguration(initializers = BaseInitializer.class)
+ * public class BaseTest {
+ * // ...
+ * }
+ *
+ * &#064;ContextConfiguration(initializers = ExtendedInitializer.class)
+ * public class ExtendedTest extends BaseTest {
+ * // ...
+ * }
+ * </pre>
+ * @since 3.2
+ */
+ boolean inheritInitializers() default true;
+
+ /**
+ * The type of {@link SmartContextLoader} (or {@link ContextLoader}) to use
+ * for loading an {@link org.springframework.context.ApplicationContext
+ * ApplicationContext}.
+ *
+ * <p>If not specified, the loader will be inherited from the first superclass
+ * that is annotated with {@code @ContextConfiguration} and specifies an
+ * explicit loader. If no class in the hierarchy specifies an explicit
+ * loader, a default loader will be used instead.
+ *
+ * <p>The default concrete implementation chosen at runtime will be either
+ * {@link org.springframework.test.context.support.DelegatingSmartContextLoader
+ * DelegatingSmartContextLoader} or
+ * {@link org.springframework.test.context.web.WebDelegatingSmartContextLoader
+ * WebDelegatingSmartContextLoader} depending on the absence or presence of
+ * {@link org.springframework.test.context.web.WebAppConfiguration
+ * &#064;WebAppConfiguration}. For further details on the default behavior
+ * of various concrete {@code SmartContextLoaders}, check out the Javadoc for
+ * {@link org.springframework.test.context.support.AbstractContextLoader
+ * AbstractContextLoader},
+ * {@link org.springframework.test.context.support.GenericXmlContextLoader
+ * GenericXmlContextLoader},
+ * {@link org.springframework.test.context.support.AnnotationConfigContextLoader
+ * AnnotationConfigContextLoader},
+ * {@link org.springframework.test.context.web.GenericXmlWebContextLoader
+ * GenericXmlWebContextLoader}, and
+ * {@link org.springframework.test.context.web.AnnotationConfigWebContextLoader
+ * AnnotationConfigWebContextLoader}.
+ *
+ * @since 2.5
+ */
+ Class<? extends ContextLoader> loader() default ContextLoader.class;
+
+ /**
+ * The name of the context hierarchy level represented by this configuration.
+ *
+ * <p>If not specified the name will be inferred based on the numerical level
+ * within all declared contexts within the hierarchy.
+ *
+ * <p>This attribute is only applicable when used within a test class hierarchy
+ * that is configured using {@code @ContextHierarchy}, in which case the name
+ * can be used for <em>merging</em> or <em>overriding</em> this configuration
+ * with configuration of the same name in hierarchy levels defined in superclasses.
+ * See the Javadoc for {@link ContextHierarchy @ContextHierarchy} for details.
+ *
+ * @since 3.2.2
+ */
+ String name() default "";
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java
new file mode 100644
index 00000000..16c724a8
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java
@@ -0,0 +1,420 @@
+/*
+ * 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.context;
+
+import java.util.Arrays;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@code ContextConfigurationAttributes} encapsulates the context configuration
+ * attributes declared via {@link ContextConfiguration @ContextConfiguration}.
+ *
+ * @author Sam Brannen
+ * @since 3.1
+ * @see ContextConfiguration
+ * @see SmartContextLoader#processContextConfiguration(ContextConfigurationAttributes)
+ * @see MergedContextConfiguration
+ */
+public class ContextConfigurationAttributes {
+
+ private static final Log logger = LogFactory.getLog(ContextConfigurationAttributes.class);
+
+ private final Class<?> declaringClass;
+
+ private Class<?>[] classes;
+
+ private String[] locations;
+
+ private final boolean inheritLocations;
+
+ private final Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers;
+
+ private final boolean inheritInitializers;
+
+ private final String name;
+
+ private final Class<? extends ContextLoader> contextLoaderClass;
+
+
+ /**
+ * Construct a new {@link ContextConfigurationAttributes} instance for the
+ * supplied {@link ContextConfiguration @ContextConfiguration} annotation and
+ * the {@linkplain Class test class} that declared it.
+ * @param declaringClass the test class that declared {@code @ContextConfiguration}
+ * @param contextConfiguration the annotation from which to retrieve the attributes
+ */
+ public ContextConfigurationAttributes(Class<?> declaringClass, ContextConfiguration contextConfiguration) {
+ this(declaringClass, resolveLocations(declaringClass, contextConfiguration), contextConfiguration.classes(),
+ contextConfiguration.inheritLocations(), contextConfiguration.initializers(),
+ contextConfiguration.inheritInitializers(), contextConfiguration.name(), contextConfiguration.loader());
+ }
+
+ /**
+ * Construct a new {@link ContextConfigurationAttributes} instance for the
+ * supplied {@link AnnotationAttributes} (parsed from a
+ * {@link ContextConfiguration @ContextConfiguration} annotation) and
+ * the {@linkplain Class test class} that declared them.
+ * @param declaringClass the test class that declared {@code @ContextConfiguration}
+ * @param annAttrs the annotation attributes from which to retrieve the attributes
+ */
+ @SuppressWarnings("unchecked")
+ public ContextConfigurationAttributes(Class<?> declaringClass, AnnotationAttributes annAttrs) {
+ this(declaringClass,
+ resolveLocations(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getStringArray("value")),
+ annAttrs.getClassArray("classes"), annAttrs.getBoolean("inheritLocations"),
+ (Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[]) annAttrs.getClassArray("initializers"),
+ annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"),
+ (Class<? extends ContextLoader>) annAttrs.getClass("loader"));
+ }
+
+ /**
+ * Construct a new {@link ContextConfigurationAttributes} instance for the
+ * {@linkplain Class test class} that declared the
+ * {@link ContextConfiguration @ContextConfiguration} annotation and its
+ * corresponding attributes.
+ * @param declaringClass the test class that declared {@code @ContextConfiguration}
+ * @param locations the resource locations declared via {@code @ContextConfiguration}
+ * @param classes the annotated classes declared via {@code @ContextConfiguration}
+ * @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration}
+ * @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration}
+ * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is
+ * {@code null}
+ * @deprecated as of Spring 3.2, use
+ * {@link #ContextConfigurationAttributes(Class, String[], Class[], boolean, Class[], boolean, String, Class)}
+ * instead
+ */
+ @Deprecated
+ public ContextConfigurationAttributes(Class<?> declaringClass, String[] locations, Class<?>[] classes,
+ boolean inheritLocations, Class<? extends ContextLoader> contextLoaderClass) {
+
+ this(declaringClass, locations, classes, inheritLocations, null, true, null, contextLoaderClass);
+ }
+
+ /**
+ * Construct a new {@link ContextConfigurationAttributes} instance for the
+ * {@linkplain Class test class} that declared the
+ * {@link ContextConfiguration @ContextConfiguration} annotation and its
+ * corresponding attributes.
+ * @param declaringClass the test class that declared {@code @ContextConfiguration}
+ * @param locations the resource locations declared via {@code @ContextConfiguration}
+ * @param classes the annotated classes declared via {@code @ContextConfiguration}
+ * @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration}
+ * @param initializers the context initializers declared via {@code @ContextConfiguration}
+ * @param inheritInitializers the {@code inheritInitializers} flag declared via {@code @ContextConfiguration}
+ * @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration}
+ * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is
+ * {@code null}
+ */
+ public ContextConfigurationAttributes(
+ Class<?> declaringClass, String[] locations, Class<?>[] classes, boolean inheritLocations,
+ Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers,
+ boolean inheritInitializers, Class<? extends ContextLoader> contextLoaderClass) {
+
+ this(declaringClass, locations, classes, inheritLocations, initializers, inheritInitializers, null,
+ contextLoaderClass);
+ }
+
+ /**
+ * Construct a new {@link ContextConfigurationAttributes} instance for the
+ * {@linkplain Class test class} that declared the
+ * {@link ContextConfiguration @ContextConfiguration} annotation and its
+ * corresponding attributes.
+ * @param declaringClass the test class that declared {@code @ContextConfiguration}
+ * @param locations the resource locations declared via {@code @ContextConfiguration}
+ * @param classes the annotated classes declared via {@code @ContextConfiguration}
+ * @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration}
+ * @param initializers the context initializers declared via {@code @ContextConfiguration}
+ * @param inheritInitializers the {@code inheritInitializers} flag declared via {@code @ContextConfiguration}
+ * @param name the name of level in the context hierarchy, or {@code null} if not applicable
+ * @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration}
+ * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is
+ * {@code null}
+ */
+ public ContextConfigurationAttributes(
+ Class<?> declaringClass, String[] locations, Class<?>[] classes, boolean inheritLocations,
+ Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers,
+ boolean inheritInitializers, String name, Class<? extends ContextLoader> contextLoaderClass) {
+
+ Assert.notNull(declaringClass, "declaringClass must not be null");
+ Assert.notNull(contextLoaderClass, "contextLoaderClass must not be null");
+
+ if (!ObjectUtils.isEmpty(locations) && !ObjectUtils.isEmpty(classes) && logger.isDebugEnabled()) {
+ logger.debug(String.format(
+ "Test class [%s] has been configured with @ContextConfiguration's 'locations' (or 'value') %s " +
+ "and 'classes' %s attributes. Most SmartContextLoader implementations support " +
+ "only one declaration of resources per @ContextConfiguration annotation.",
+ declaringClass.getName(), ObjectUtils.nullSafeToString(locations),
+ ObjectUtils.nullSafeToString(classes)));
+ }
+
+ this.declaringClass = declaringClass;
+ this.locations = locations;
+ this.classes = classes;
+ this.inheritLocations = inheritLocations;
+ this.initializers = initializers;
+ this.inheritInitializers = inheritInitializers;
+ this.name = (StringUtils.hasText(name) ? name : null);
+ this.contextLoaderClass = contextLoaderClass;
+ }
+
+
+ /**
+ * Resolve resource locations from the {@link ContextConfiguration#locations() locations}
+ * and {@link ContextConfiguration#value() value} attributes of the supplied
+ * {@link ContextConfiguration} annotation.
+ * @throws IllegalStateException if both the locations and value attributes have been declared
+ */
+ private static String[] resolveLocations(Class<?> declaringClass, ContextConfiguration contextConfiguration) {
+ return resolveLocations(declaringClass, contextConfiguration.locations(), contextConfiguration.value());
+ }
+
+ /**
+ * Resolve resource locations from the supplied {@code locations} and
+ * {@code value} arrays, which correspond to attributes of the same names in
+ * the {@link ContextConfiguration} annotation.
+ * @throws IllegalStateException if both the locations and value attributes have been declared
+ */
+ private static String[] resolveLocations(Class<?> declaringClass, String[] locations, String[] value) {
+ Assert.notNull(declaringClass, "declaringClass must not be null");
+ if (!ObjectUtils.isEmpty(value) && !ObjectUtils.isEmpty(locations)) {
+ throw new IllegalStateException(String.format("Test class [%s] has been configured with " +
+ "@ContextConfiguration's 'value' %s and 'locations' %s attributes. Only one declaration " +
+ "of resource locations is permitted per @ContextConfiguration annotation.",
+ declaringClass.getName(), ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(locations)));
+ }
+ else if (!ObjectUtils.isEmpty(value)) {
+ locations = value;
+ }
+ return locations;
+ }
+
+
+ /**
+ * Get the {@linkplain Class class} that declared the
+ * {@link ContextConfiguration @ContextConfiguration} annotation.
+ * @return the declaring class (never {@code null})
+ */
+ public Class<?> getDeclaringClass() {
+ return this.declaringClass;
+ }
+
+ /**
+ * Set the <em>processed</em> annotated classes, effectively overriding the
+ * original value declared via {@link ContextConfiguration @ContextConfiguration}.
+ * @see #getClasses()
+ */
+ public void setClasses(Class<?>... classes) {
+ this.classes = classes;
+ }
+
+ /**
+ * Get the annotated classes that were declared via
+ * {@link ContextConfiguration @ContextConfiguration}.
+ * <p>Note: this is a mutable property. The returned value may therefore
+ * represent a <em>processed</em> value that does not match the original value
+ * declared via {@link ContextConfiguration @ContextConfiguration}.
+ * @return the annotated classes; potentially {@code null} or <em>empty</em>
+ * @see ContextConfiguration#classes
+ * @see #setClasses(Class[])
+ */
+ public Class<?>[] getClasses() {
+ return this.classes;
+ }
+
+ /**
+ * Determine if this {@code ContextConfigurationAttributes} instance has
+ * class-based resources.
+ * @return {@code true} if the {@link #getClasses() classes} array is not empty
+ * @see #hasResources()
+ * @see #hasLocations()
+ */
+ public boolean hasClasses() {
+ return !ObjectUtils.isEmpty(getClasses());
+ }
+
+ /**
+ * Set the <em>processed</em> resource locations, effectively overriding the
+ * original value declared via {@link ContextConfiguration @ContextConfiguration}.
+ * @see #getLocations()
+ */
+ public void setLocations(String... locations) {
+ this.locations = locations;
+ }
+
+ /**
+ * Get the resource locations that were declared via
+ * {@link ContextConfiguration @ContextConfiguration}.
+ * <p>Note: this is a mutable property. The returned value may therefore
+ * represent a <em>processed</em> value that does not match the original value
+ * declared via {@link ContextConfiguration @ContextConfiguration}.
+ * @return the resource locations; potentially {@code null} or <em>empty</em>
+ * @see ContextConfiguration#value
+ * @see ContextConfiguration#locations
+ * @see #setLocations(String[])
+ */
+ public String[] getLocations() {
+ return this.locations;
+ }
+
+ /**
+ * Determine if this {@code ContextConfigurationAttributes} instance has
+ * path-based resource locations.
+ * @return {@code true} if the {@link #getLocations() locations} array is not empty
+ * @see #hasResources()
+ * @see #hasClasses()
+ */
+ public boolean hasLocations() {
+ return !ObjectUtils.isEmpty(getLocations());
+ }
+
+ /**
+ * Determine if this {@code ContextConfigurationAttributes} instance has
+ * either path-based resource locations or class-based resources.
+ * @return {@code true} if either the {@link #getLocations() locations}
+ * or the {@link #getClasses() classes} array is not empty
+ * @see #hasLocations()
+ * @see #hasClasses()
+ */
+ public boolean hasResources() {
+ return (hasLocations() || hasClasses());
+ }
+
+ /**
+ * Get the {@code inheritLocations} flag that was declared via
+ * {@link ContextConfiguration @ContextConfiguration}.
+ * @return the {@code inheritLocations} flag
+ * @see ContextConfiguration#inheritLocations
+ */
+ public boolean isInheritLocations() {
+ return this.inheritLocations;
+ }
+
+ /**
+ * Get the {@code ApplicationContextInitializer} classes that were declared via
+ * {@link ContextConfiguration @ContextConfiguration}.
+ * @return the {@code ApplicationContextInitializer} classes
+ * @since 3.2
+ */
+ public Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] getInitializers() {
+ return this.initializers;
+ }
+
+ /**
+ * Get the {@code inheritInitializers} flag that was declared via
+ * {@link ContextConfiguration @ContextConfiguration}.
+ * @return the {@code inheritInitializers} flag
+ * @since 3.2
+ */
+ public boolean isInheritInitializers() {
+ return this.inheritInitializers;
+ }
+
+ /**
+ * Get the name of the context hierarchy level that was declared via
+ * {@link ContextConfiguration @ContextConfiguration}.
+ * @return the name of the context hierarchy level or {@code null} if not applicable
+ * @see ContextConfiguration#name()
+ * @since 3.2.2
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Get the {@code ContextLoader} class that was declared via
+ * {@link ContextConfiguration @ContextConfiguration}.
+ * @return the {@code ContextLoader} class
+ * @see ContextConfiguration#loader
+ */
+ public Class<? extends ContextLoader> getContextLoaderClass() {
+ return this.contextLoaderClass;
+ }
+
+
+ /**
+ * Determine if the supplied object is equal to this
+ * {@code ContextConfigurationAttributes} instance by comparing both object's
+ * {@linkplain #getDeclaringClass() declaring class},
+ * {@linkplain #getLocations() locations},
+ * {@linkplain #getClasses() annotated classes},
+ * {@linkplain #isInheritLocations() inheritLocations flag},
+ * {@linkplain #getInitializers() context initializer classes},
+ * {@linkplain #isInheritInitializers() inheritInitializers flag}, and the
+ * {@link #getContextLoaderClass() ContextLoader class}.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ContextConfigurationAttributes)) {
+ return false;
+ }
+ ContextConfigurationAttributes otherAttr = (ContextConfigurationAttributes) other;
+ return (ObjectUtils.nullSafeEquals(this.declaringClass, otherAttr.declaringClass) &&
+ Arrays.equals(this.classes, otherAttr.classes)) &&
+ Arrays.equals(this.locations, otherAttr.locations) &&
+ this.inheritLocations == otherAttr.inheritLocations &&
+ Arrays.equals(this.initializers, otherAttr.initializers) &&
+ this.inheritInitializers == otherAttr.inheritInitializers &&
+ ObjectUtils.nullSafeEquals(this.name, otherAttr.name) &&
+ ObjectUtils.nullSafeEquals(this.contextLoaderClass, otherAttr.contextLoaderClass);
+ }
+
+ /**
+ * Generate a unique hash code for all properties of this
+ * {@code ContextConfigurationAttributes} instance excluding the
+ * {@linkplain #getName() name}.
+ */
+ @Override
+ public int hashCode() {
+ int result = this.declaringClass.hashCode();
+ result = 31 * result + Arrays.hashCode(this.classes);
+ result = 31 * result + Arrays.hashCode(this.locations);
+ result = 31 * result + Arrays.hashCode(this.initializers);
+ return result;
+ }
+
+ /**
+ * Provide a String representation of the context configuration attributes
+ * and declaring class.
+ */
+ @Override
+ public String toString() {
+ return new ToStringCreator(this)
+ .append("declaringClass", this.declaringClass.getName())
+ .append("classes", ObjectUtils.nullSafeToString(this.classes))
+ .append("locations", ObjectUtils.nullSafeToString(this.locations))
+ .append("inheritLocations", this.inheritLocations)
+ .append("initializers", ObjectUtils.nullSafeToString(this.initializers))
+ .append("inheritInitializers", this.inheritInitializers)
+ .append("name", this.name)
+ .append("contextLoaderClass", this.contextLoaderClass.getName())
+ .toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java
new file mode 100644
index 00000000..7eac62d6
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2002-2013 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.context;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * {@code @ContextHierarchy} is a class-level annotation that is used to define
+ * a hierarchy of {@link org.springframework.context.ApplicationContext
+ * ApplicationContexts} for integration tests.
+ *
+ * <h3>Examples</h3>
+ * <p>The following JUnit-based examples demonstrate common configuration
+ * scenarios for integration tests that require the use of context hierarchies.
+ *
+ * <h4>Single Test Class with Context Hierarchy</h4>
+ * <p>{@code ControllerIntegrationTests} represents a typical integration testing
+ * scenario for a Spring MVC web application by declaring a context hierarchy
+ * consisting of two levels, one for the <em>root</em> {@code WebApplicationContext}
+ * (with {@code TestAppConfig}) and one for the <em>dispatcher servlet</em>
+ * {@code WebApplicationContext} (with {@code WebConfig}). The {@code
+ * WebApplicationContext} that is <em>autowired</em> into the test instance is
+ * the one for the child context (i.e., the lowest context in the hierarchy).
+ *
+ * <pre class="code">
+ * &#064;RunWith(SpringJUnit4ClassRunner.class)
+ * &#064;WebAppConfiguration
+ * &#064;ContextHierarchy({
+ * &#064;ContextConfiguration(classes = TestAppConfig.class),
+ * &#064;ContextConfiguration(classes = WebConfig.class)
+ * })
+ * public class ControllerIntegrationTests {
+ *
+ * &#064;Autowired
+ * private WebApplicationContext wac;
+ *
+ * // ...
+ * }</pre>
+ *
+ * <h4>Class Hierarchy with Implicit Parent Context</h4>
+ * <p>The following test classes define a context hierarchy within a test class
+ * hierarchy. {@code AbstractWebTests} declares the configuration for a root
+ * {@code WebApplicationContext} in a Spring-powered web application. Note,
+ * however, that {@code AbstractWebTests} does not declare {@code @ContextHierarchy};
+ * consequently, subclasses of {@code AbstractWebTests} can optionally participate
+ * in a context hierarchy or follow the standard semantics for {@code @ContextConfiguration}.
+ * {@code SoapWebServiceTests} and {@code RestWebServiceTests} both extend
+ * {@code AbstractWebTests} and define a context hierarchy via {@code @ContextHierarchy}.
+ * The result is that three application contexts will be loaded (one for each
+ * declaration of {@code @ContextConfiguration}, and the application context
+ * loaded based on the configuration in {@code AbstractWebTests} will be set as
+ * the parent context for each of the contexts loaded for the concrete subclasses.
+ *
+ * <pre class="code">
+ * &#064;RunWith(SpringJUnit4ClassRunner.class)
+ * &#064;WebAppConfiguration
+ * &#064;ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
+ * public abstract class AbstractWebTests {}
+ *
+ * &#064;ContextHierarchy(&#064;ContextConfiguration("/spring/soap-ws-config.xml")
+ * public class SoapWebServiceTests extends AbstractWebTests {}
+ *
+ * &#064;ContextHierarchy(&#064;ContextConfiguration("/spring/rest-ws-config.xml")
+ * public class RestWebServiceTests extends AbstractWebTests {}</pre>
+ *
+ * <h4>Class Hierarchy with Merged Context Hierarchy Configuration</h4>
+ * <p>The following classes demonstrate the use of <em>named</em> hierarchy levels
+ * in order to <em>merge</em> the configuration for specific levels in a context
+ * hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code parent}
+ * and {@code child}. {@code ExtendedTests} extends {@code BaseTests} and instructs
+ * the Spring TestContext Framework to merge the context configuration for the
+ * {@code child} hierarchy level, simply by ensuring that the names declared via
+ * {@link ContextConfiguration#name} are both {@code "child"}. The result is that
+ * three application contexts will be loaded: one for {@code "/app-config.xml"},
+ * one for {@code "/user-config.xml"}, and one for <code>{"/user-config.xml",
+ * "/order-config.xml"}</code>. As with the previous example, the application
+ * context loaded from {@code "/app-config.xml"} will be set as the parent context
+ * for the contexts loaded from {@code "/user-config.xml"} and <code>{"/user-config.xml",
+ * "/order-config.xml"}</code>.
+ *
+ * <pre class="code">
+ * &#064;RunWith(SpringJUnit4ClassRunner.class)
+ * &#064;ContextHierarchy({
+ * &#064;ContextConfiguration(name = "parent", locations = "/app-config.xml"),
+ * &#064;ContextConfiguration(name = "child", locations = "/user-config.xml")
+ * })
+ * public class BaseTests {}
+ *
+ * &#064;ContextHierarchy(
+ * &#064;ContextConfiguration(name = "child", locations = "/order-config.xml")
+ * )
+ * public class ExtendedTests extends BaseTests {}</pre>
+ *
+ * <h4>Class Hierarchy with Overridden Context Hierarchy Configuration</h4>
+ * <p>In contrast to the previous example, this example demonstrates how to
+ * <em>override</em> the configuration for a given named level in a context hierarchy
+ * by setting the {@link ContextConfiguration#inheritLocations} flag to {@code false}.
+ * Consequently, the application context for {@code ExtendedTests} will be loaded
+ * only from {@code "/test-user-config.xml"} and will have its parent set to the
+ * context loaded from {@code "/app-config.xml"}.
+ *
+ * <pre class="code">
+ * &#064;RunWith(SpringJUnit4ClassRunner.class)
+ * &#064;ContextHierarchy({
+ * &#064;ContextConfiguration(name = "parent", locations = "/app-config.xml"),
+ * &#064;ContextConfiguration(name = "child", locations = "/user-config.xml")
+ * })
+ * public class BaseTests {}
+ *
+ * &#064;ContextHierarchy(
+ * &#064;ContextConfiguration(name = "child", locations = "/test-user-config.xml", inheritLocations=false)
+ * )
+ * public class ExtendedTests extends BaseTests {}</pre>
+ *
+ * @author Sam Brannen
+ * @since 3.2.2
+ * @see ContextConfiguration
+ * @see org.springframework.context.ApplicationContext
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ContextHierarchy {
+
+ /**
+ * A list of {@link ContextConfiguration @ContextConfiguration} instances,
+ * each of which defines a level in the context hierarchy.
+ *
+ * <p>If you need to merge or override the configuration for a given level
+ * of the context hierarchy within a test class hierarchy, you must explicitly
+ * name that level by supplying the same value to the {@link ContextConfiguration#name
+ * name} attribute in {@code @ContextConfiguration} at each level in the
+ * class hierarchy. See the class-level Javadoc for examples.
+ */
+ ContextConfiguration[] value();
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java
new file mode 100644
index 00000000..71faf5e5
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java
@@ -0,0 +1,95 @@
+/*
+ * 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.context;
+
+import org.springframework.context.ApplicationContext;
+
+/**
+ * Strategy interface for loading an {@link ApplicationContext application context}
+ * for an integration test managed by the Spring TestContext Framework.
+ *
+ * <p><b>Note</b>: as of Spring 3.1, implement {@link SmartContextLoader} instead
+ * of this interface in order to provide support for annotated classes, active
+ * bean definition profiles, and application context initializers.
+ *
+ * <p>Clients of a ContextLoader should call
+ * {@link #processLocations(Class, String...) processLocations()} prior to
+ * calling {@link #loadContext(String...) loadContext()} in case the
+ * ContextLoader provides custom support for modifying or generating locations.
+ * The results of {@link #processLocations(Class, String...) processLocations()}
+ * should then be supplied to {@link #loadContext(String...) loadContext()}.
+ *
+ * <p>Concrete implementations must provide a {@code public} no-args
+ * constructor.
+ *
+ * <p>Spring provides the following out-of-the-box implementations:
+ * <ul>
+ * <li>{@link org.springframework.test.context.support.GenericXmlContextLoader GenericXmlContextLoader}</li>
+ * <li>{@link org.springframework.test.context.support.GenericPropertiesContextLoader GenericPropertiesContextLoader}</li>
+ * </ul>
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see SmartContextLoader
+ * @see org.springframework.test.context.support.AnnotationConfigContextLoader AnnotationConfigContextLoader
+ */
+public interface ContextLoader {
+
+ /**
+ * Processes application context resource locations for a specified class.
+ *
+ * <p>Concrete implementations may choose to modify the supplied locations,
+ * generate new locations, or simply return the supplied locations unchanged.
+ *
+ * @param clazz the class with which the locations are associated: used to
+ * determine how to process the supplied locations
+ * @param locations the unmodified locations to use for loading the
+ * application context (can be {@code null} or empty)
+ * @return an array of application context resource locations
+ */
+ String[] processLocations(Class<?> clazz, String... locations);
+
+ /**
+ * Loads a new {@link ApplicationContext context} based on the supplied
+ * {@code locations}, configures the context, and finally returns
+ * the context in fully <em>refreshed</em> state.
+ *
+ * <p>Configuration locations are generally considered to be classpath
+ * resources by default.
+ *
+ * <p>Concrete implementations should register annotation configuration
+ * processors with bean factories of {@link ApplicationContext application
+ * contexts} loaded by this ContextLoader. Beans will therefore automatically
+ * be candidates for annotation-based dependency injection using
+ * {@link org.springframework.beans.factory.annotation.Autowired @Autowired},
+ * {@link javax.annotation.Resource @Resource}, and
+ * {@link javax.inject.Inject @Inject}.
+ *
+ * <p>Any ApplicationContext loaded by a ContextLoader <strong>must</strong>
+ * register a JVM shutdown hook for itself. Unless the context gets closed
+ * early, all context instances will be automatically closed on JVM
+ * shutdown. This allows for freeing external resources held by beans within
+ * the context, e.g. temporary files.
+ *
+ * @param locations the resource locations to use to load the application context
+ * @return a new application context
+ * @throws Exception if context loading failed
+ */
+ ApplicationContext loadContext(String... locations) throws Exception;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java
new file mode 100644
index 00000000..f71e447b
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java
@@ -0,0 +1,707 @@
+/*
+ * Copyright 2002-2013 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.context;
+
+import static org.springframework.beans.BeanUtils.instantiateClass;
+import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClass;
+import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClassForTypes;
+import static org.springframework.core.annotation.AnnotationUtils.isAnnotationDeclaredLocally;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Utility methods for working with {@link ContextLoader ContextLoaders} and
+ * {@link SmartContextLoader SmartContextLoaders} and resolving resource locations,
+ * annotated classes, active bean definition profiles, and application context
+ * initializers.
+ *
+ * @author Sam Brannen
+ * @since 3.1
+ * @see ContextLoader
+ * @see SmartContextLoader
+ * @see ContextConfiguration
+ * @see ContextConfigurationAttributes
+ * @see ActiveProfiles
+ * @see ApplicationContextInitializer
+ * @see ContextHierarchy
+ * @see MergedContextConfiguration
+ */
+abstract class ContextLoaderUtils {
+
+ static final String GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX = "ContextHierarchyLevel#";
+
+ private static final Log logger = LogFactory.getLog(ContextLoaderUtils.class);
+
+ private static final String DEFAULT_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.support.DelegatingSmartContextLoader";
+ private static final String DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.web.WebDelegatingSmartContextLoader";
+
+ private static final String WEB_APP_CONFIGURATION_CLASS_NAME = "org.springframework.test.context.web.WebAppConfiguration";
+ private static final String WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME = "org.springframework.test.context.web.WebMergedContextConfiguration";
+
+
+ private ContextLoaderUtils() {
+ /* no-op */
+ }
+
+ /**
+ * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied
+ * list of {@link ContextConfigurationAttributes} and then instantiate and return that
+ * {@code ContextLoader}.
+ *
+ * <p>If the supplied {@code defaultContextLoaderClassName} is {@code null} or
+ * <em>empty</em>, depending on the absence or presence of
+ * {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} either
+ * {@code "org.springframework.test.context.support.DelegatingSmartContextLoader"} or
+ * {@code "org.springframework.test.context.web.WebDelegatingSmartContextLoader"} will
+ * be used as the default context loader class name. For details on the class
+ * resolution process, see {@link #resolveContextLoaderClass}.
+ *
+ * @param testClass the test class for which the {@code ContextLoader} should be
+ * resolved; must not be {@code null}
+ * @param configAttributesList the list of configuration attributes to process; must
+ * not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em>
+ * (i.e., as if we were traversing up the class hierarchy)
+ * @param defaultContextLoaderClassName the name of the default {@code ContextLoader}
+ * class to use; may be {@code null} or <em>empty</em>
+ * @return the resolved {@code ContextLoader} for the supplied {@code testClass}
+ * (never {@code null})
+ * @see #resolveContextLoaderClass
+ */
+ static ContextLoader resolveContextLoader(Class<?> testClass,
+ List<ContextConfigurationAttributes> configAttributesList, String defaultContextLoaderClassName) {
+ Assert.notNull(testClass, "Class must not be null");
+ Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty");
+
+ if (!StringUtils.hasText(defaultContextLoaderClassName)) {
+ Class<? extends Annotation> webAppConfigClass = loadWebAppConfigurationClass();
+ defaultContextLoaderClassName = webAppConfigClass != null
+ && testClass.isAnnotationPresent(webAppConfigClass) ? DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME
+ : DEFAULT_CONTEXT_LOADER_CLASS_NAME;
+ }
+
+ Class<? extends ContextLoader> contextLoaderClass = resolveContextLoaderClass(testClass, configAttributesList,
+ defaultContextLoaderClassName);
+
+ return instantiateClass(contextLoaderClass, ContextLoader.class);
+ }
+
+ /**
+ * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied
+ * list of {@link ContextConfigurationAttributes}.
+ *
+ * <p>Beginning with the first level in the context configuration attributes hierarchy:
+ *
+ * <ol>
+ * <li>If the {@link ContextConfigurationAttributes#getContextLoaderClass()
+ * contextLoaderClass} property of {@link ContextConfigurationAttributes} is
+ * configured with an explicit class, that class will be returned.</li>
+ * <li>If an explicit {@code ContextLoader} class is not specified at the current
+ * level in the hierarchy, traverse to the next level in the hierarchy and return to
+ * step #1.</li>
+ * <li>If no explicit {@code ContextLoader} class is found after traversing the
+ * hierarchy, an attempt will be made to load and return the class with the supplied
+ * {@code defaultContextLoaderClassName}.</li>
+ * </ol>
+ *
+ * @param testClass the class for which to resolve the {@code ContextLoader} class;
+ * must not be {@code null}; only used for logging purposes
+ * @param configAttributesList the list of configuration attributes to process; must
+ * not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em>
+ * (i.e., as if we were traversing up the class hierarchy)
+ * @param defaultContextLoaderClassName the name of the default {@code ContextLoader}
+ * class to use; must not be {@code null} or empty
+ * @return the {@code ContextLoader} class to use for the supplied test class
+ * @throws IllegalArgumentException if {@code @ContextConfiguration} is not
+ * <em>present</em> on the supplied test class
+ * @throws IllegalStateException if the default {@code ContextLoader} class could not
+ * be loaded
+ */
+ @SuppressWarnings("unchecked")
+ static Class<? extends ContextLoader> resolveContextLoaderClass(Class<?> testClass,
+ List<ContextConfigurationAttributes> configAttributesList, String defaultContextLoaderClassName) {
+ Assert.notNull(testClass, "Class must not be null");
+ Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty");
+ Assert.hasText(defaultContextLoaderClassName, "Default ContextLoader class name must not be null or empty");
+
+ for (ContextConfigurationAttributes configAttributes : configAttributesList) {
+ if (logger.isTraceEnabled()) {
+ logger.trace(String.format("Processing ContextLoader for context configuration attributes %s",
+ configAttributes));
+ }
+
+ Class<? extends ContextLoader> contextLoaderClass = configAttributes.getContextLoaderClass();
+ if (!ContextLoader.class.equals(contextLoaderClass)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format(
+ "Found explicit ContextLoader class [%s] for context configuration attributes %s",
+ contextLoaderClass.getName(), configAttributes));
+ }
+ return contextLoaderClass;
+ }
+ }
+
+ try {
+ if (logger.isTraceEnabled()) {
+ logger.trace(String.format("Using default ContextLoader class [%s] for test class [%s]",
+ defaultContextLoaderClassName, testClass.getName()));
+ }
+ return (Class<? extends ContextLoader>) ClassUtils.forName(defaultContextLoaderClassName,
+ ContextLoaderUtils.class.getClassLoader());
+ }
+ catch (Throwable t) {
+ throw new IllegalStateException("Could not load default ContextLoader class ["
+ + defaultContextLoaderClassName + "]. Specify @ContextConfiguration's 'loader' "
+ + "attribute or make the default loader class available.", t);
+ }
+ }
+
+ /**
+ * Convenience method for creating a {@link ContextConfigurationAttributes} instance
+ * from the supplied {@link ContextConfiguration} and declaring class and then adding
+ * the attributes to the supplied list.
+ */
+ private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration,
+ Class<?> declaringClass, final List<ContextConfigurationAttributes> attributesList) {
+ if (logger.isTraceEnabled()) {
+ logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].",
+ contextConfiguration, declaringClass.getName()));
+ }
+
+ ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass,
+ contextConfiguration);
+ if (logger.isTraceEnabled()) {
+ logger.trace("Resolved context configuration attributes: " + attributes);
+ }
+ attributesList.add(attributes);
+ }
+
+ /**
+ * Resolve the list of lists of {@linkplain ContextConfigurationAttributes context
+ * configuration attributes} for the supplied {@linkplain Class test class} and its
+ * superclasses, taking into account context hierarchies declared via
+ * {@link ContextHierarchy @ContextHierarchy} and
+ * {@link ContextConfiguration @ContextConfiguration}.
+ *
+ * <p>The outer list represents a top-down ordering of context configuration
+ * attributes, where each element in the list represents the context configuration
+ * declared on a given test class in the class hierarchy. Each nested list
+ * contains the context configuration attributes declared either via a single
+ * instance of {@code @ContextConfiguration} on the particular class or via
+ * multiple instances of {@code @ContextConfiguration} declared within a
+ * single {@code @ContextHierarchy} instance on the particular class.
+ * Furthermore, each nested list maintains the order in which
+ * {@code @ContextConfiguration} instances are declared.
+ *
+ * <p>Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and
+ * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of
+ * {@link ContextConfiguration @ContextConfiguration} will <strong>not</strong>
+ * be taken into consideration. If these flags need to be honored, that must be
+ * handled manually when traversing the nested lists returned by this method.
+ *
+ * @param testClass the class for which to resolve the context hierarchy attributes
+ * (must not be {@code null})
+ * @return the list of lists of configuration attributes for the specified class;
+ * never {@code null}
+ * @throws IllegalArgumentException if the supplied class is {@code null}; if
+ * neither {@code @ContextConfiguration} nor {@code @ContextHierarchy} is
+ * <em>present</em> on the supplied class; or if a given class in the class hierarchy
+ * declares both {@code @ContextConfiguration} and {@code @ContextHierarchy} as
+ * top-level annotations.
+ *
+ * @since 3.2.2
+ * @see #buildContextHierarchyMap(Class)
+ * @see #resolveContextConfigurationAttributes(Class)
+ */
+ static List<List<ContextConfigurationAttributes>> resolveContextHierarchyAttributes(Class<?> testClass) {
+ Assert.notNull(testClass, "Class must not be null");
+
+ final Class<ContextConfiguration> contextConfigType = ContextConfiguration.class;
+ final Class<ContextHierarchy> contextHierarchyType = ContextHierarchy.class;
+ final List<Class<? extends Annotation>> annotationTypes = Arrays.asList(contextConfigType, contextHierarchyType);
+
+ final List<List<ContextConfigurationAttributes>> hierarchyAttributes = new ArrayList<List<ContextConfigurationAttributes>>();
+
+ Class<?> declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, testClass);
+ Assert.notNull(declaringClass, String.format(
+ "Could not find an 'annotation declaring class' for annotation type [%s] or [%s] and test class [%s]",
+ contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName()));
+
+ while (declaringClass != null) {
+
+ boolean contextConfigDeclaredLocally = isAnnotationDeclaredLocally(contextConfigType, declaringClass);
+ boolean contextHierarchyDeclaredLocally = isAnnotationDeclaredLocally(contextHierarchyType, declaringClass);
+
+ if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) {
+ String msg = String.format("Test class [%s] has been configured with both @ContextConfiguration "
+ + "and @ContextHierarchy as class-level annotations. Only one of these annotations may "
+ + "be declared as a top-level annotation per test class.", declaringClass.getName());
+ logger.error(msg);
+ throw new IllegalStateException(msg);
+ }
+
+ final List<ContextConfigurationAttributes> configAttributesList = new ArrayList<ContextConfigurationAttributes>();
+
+ if (contextConfigDeclaredLocally) {
+ ContextConfiguration contextConfiguration = declaringClass.getAnnotation(contextConfigType);
+ convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass,
+ configAttributesList);
+ }
+ else if (contextHierarchyDeclaredLocally) {
+ ContextHierarchy contextHierarchy = declaringClass.getAnnotation(contextHierarchyType);
+ for (ContextConfiguration contextConfiguration : contextHierarchy.value()) {
+ convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass,
+ configAttributesList);
+ }
+ }
+ else {
+ // This should theoretically actually never happen...
+ String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration "
+ + "nor @ContextHierarchy as a class-level annotation.", declaringClass.getName());
+ logger.error(msg);
+ throw new IllegalStateException(msg);
+ }
+
+ hierarchyAttributes.add(0, configAttributesList);
+
+ declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, declaringClass.getSuperclass());
+ }
+
+ return hierarchyAttributes;
+ }
+
+ /**
+ * Build a <em>context hierarchy map</em> for the supplied {@linkplain Class
+ * test class} and its superclasses, taking into account context hierarchies
+ * declared via {@link ContextHierarchy @ContextHierarchy} and
+ * {@link ContextConfiguration @ContextConfiguration}.
+ *
+ * <p>Each value in the map represents the consolidated list of {@linkplain
+ * ContextConfigurationAttributes context configuration attributes} for a
+ * given level in the context hierarchy (potentially across the test class
+ * hierarchy), keyed by the {@link ContextConfiguration#name() name} of the
+ * context hierarchy level.
+ *
+ * <p>If a given level in the context hierarchy does not have an explicit
+ * name (i.e., configured via {@link ContextConfiguration#name}), a name will
+ * be generated for that hierarchy level by appending the numerical level to
+ * the {@link #GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX}.
+ *
+ * @param testClass the class for which to resolve the context hierarchy map
+ * (must not be {@code null})
+ * @return a map of context configuration attributes for the context hierarchy,
+ * keyed by context hierarchy level name; never {@code null}
+ * @throws IllegalArgumentException if the lists of context configuration
+ * attributes for each level in the {@code @ContextHierarchy} do not define
+ * unique context configuration within the overall hierarchy.
+ *
+ * @since 3.2.2
+ * @see #resolveContextHierarchyAttributes(Class)
+ */
+ static Map<String, List<ContextConfigurationAttributes>> buildContextHierarchyMap(Class<?> testClass) {
+ final Map<String, List<ContextConfigurationAttributes>> map = new LinkedHashMap<String, List<ContextConfigurationAttributes>>();
+ int hierarchyLevel = 1;
+
+ for (List<ContextConfigurationAttributes> configAttributesList : resolveContextHierarchyAttributes(testClass)) {
+ for (ContextConfigurationAttributes configAttributes : configAttributesList) {
+ String name = configAttributes.getName();
+
+ // Assign a generated name?
+ if (!StringUtils.hasText(name)) {
+ name = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + hierarchyLevel;
+ }
+
+ // Encountered a new context hierarchy level?
+ if (!map.containsKey(name)) {
+ hierarchyLevel++;
+ map.put(name, new ArrayList<ContextConfigurationAttributes>());
+ }
+
+ map.get(name).add(configAttributes);
+ }
+ }
+
+ // Check for uniqueness
+ Set<List<ContextConfigurationAttributes>> set = new HashSet<List<ContextConfigurationAttributes>>(map.values());
+ if (set.size() != map.size()) {
+ String msg = String.format("The @ContextConfiguration elements configured via "
+ + "@ContextHierarchy in test class [%s] and its superclasses must "
+ + "define unique contexts per hierarchy level.", testClass.getName());
+ logger.error(msg);
+ throw new IllegalStateException(msg);
+ }
+
+ return map;
+ }
+
+ /**
+ * Resolve the list of {@linkplain ContextConfigurationAttributes context
+ * configuration attributes} for the supplied {@linkplain Class test class} and its
+ * superclasses.
+ *
+ * <p>Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and
+ * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of
+ * {@link ContextConfiguration @ContextConfiguration} will <strong>not</strong>
+ * be taken into consideration. If these flags need to be honored, that must be
+ * handled manually when traversing the list returned by this method.
+ *
+ * @param testClass the class for which to resolve the configuration attributes (must
+ * not be {@code null})
+ * @return the list of configuration attributes for the specified class, ordered
+ * <em>bottom-up</em> (i.e., as if we were traversing up the class hierarchy);
+ * never {@code null}
+ * @throws IllegalArgumentException if the supplied class is {@code null} or if
+ * {@code @ContextConfiguration} is not <em>present</em> on the supplied class
+ */
+ static List<ContextConfigurationAttributes> resolveContextConfigurationAttributes(Class<?> testClass) {
+ Assert.notNull(testClass, "Class must not be null");
+
+ final List<ContextConfigurationAttributes> attributesList = new ArrayList<ContextConfigurationAttributes>();
+
+ Class<ContextConfiguration> annotationType = ContextConfiguration.class;
+ Class<?> declaringClass = findAnnotationDeclaringClass(annotationType, testClass);
+ Assert.notNull(declaringClass, String.format(
+ "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
+ annotationType.getName(), testClass.getName()));
+
+ while (declaringClass != null) {
+ ContextConfiguration contextConfiguration = declaringClass.getAnnotation(annotationType);
+ convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass, attributesList);
+ declaringClass = findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass());
+ }
+
+ return attributesList;
+ }
+
+ /**
+ * Resolve the list of merged {@code ApplicationContextInitializer} classes for the
+ * supplied list of {@code ContextConfigurationAttributes}.
+ *
+ * <p>Note that the {@link ContextConfiguration#inheritInitializers inheritInitializers}
+ * flag of {@link ContextConfiguration @ContextConfiguration} will be taken into
+ * consideration. Specifically, if the {@code inheritInitializers} flag is set to
+ * {@code true} for a given level in the class hierarchy represented by the provided
+ * configuration attributes, context initializer classes defined at the given level
+ * will be merged with those defined in higher levels of the class hierarchy.
+ *
+ * @param configAttributesList the list of configuration attributes to process; must
+ * not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em>
+ * (i.e., as if we were traversing up the class hierarchy)
+ * @return the set of merged context initializer classes, including those from
+ * superclasses if appropriate (never {@code null})
+ * @since 3.2
+ */
+ static Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> resolveInitializerClasses(
+ List<ContextConfigurationAttributes> configAttributesList) {
+ Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty");
+
+ final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = //
+ new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>();
+
+ for (ContextConfigurationAttributes configAttributes : configAttributesList) {
+ if (logger.isTraceEnabled()) {
+ logger.trace(String.format("Processing context initializers for context configuration attributes %s",
+ configAttributes));
+ }
+
+ initializerClasses.addAll(Arrays.asList(configAttributes.getInitializers()));
+
+ if (!configAttributes.isInheritInitializers()) {
+ break;
+ }
+ }
+
+ return initializerClasses;
+ }
+
+ /**
+ * Resolve <em>active bean definition profiles</em> for the supplied {@link Class}.
+ *
+ * <p>Note that the {@link ActiveProfiles#inheritProfiles inheritProfiles} flag of
+ * {@link ActiveProfiles @ActiveProfiles} will be taken into consideration.
+ * Specifically, if the {@code inheritProfiles} flag is set to {@code true}, profiles
+ * defined in the test class will be merged with those defined in superclasses.
+ *
+ * @param testClass the class for which to resolve the active profiles (must not be
+ * {@code null})
+ * @return the set of active profiles for the specified class, including active
+ * profiles from superclasses if appropriate (never {@code null})
+ * @see ActiveProfiles
+ * @see org.springframework.context.annotation.Profile
+ */
+ static String[] resolveActiveProfiles(Class<?> testClass) {
+ Assert.notNull(testClass, "Class must not be null");
+
+ Class<ActiveProfiles> annotationType = ActiveProfiles.class;
+ Class<?> declaringClass = findAnnotationDeclaringClass(annotationType, testClass);
+
+ if (declaringClass == null && logger.isDebugEnabled()) {
+ logger.debug(String.format(
+ "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
+ annotationType.getName(), testClass.getName()));
+ }
+
+ final Set<String> activeProfiles = new HashSet<String>();
+
+ while (declaringClass != null) {
+ ActiveProfiles annotation = declaringClass.getAnnotation(annotationType);
+
+ if (logger.isTraceEnabled()) {
+ logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation,
+ declaringClass.getName()));
+ }
+
+ String[] profiles = annotation.profiles();
+ String[] valueProfiles = annotation.value();
+
+ if (!ObjectUtils.isEmpty(valueProfiles) && !ObjectUtils.isEmpty(profiles)) {
+ String msg = String.format("Test class [%s] has been configured with @ActiveProfiles' 'value' [%s] "
+ + "and 'profiles' [%s] attributes. Only one declaration of active bean "
+ + "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(),
+ ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles));
+ logger.error(msg);
+ throw new IllegalStateException(msg);
+ }
+ else if (!ObjectUtils.isEmpty(valueProfiles)) {
+ profiles = valueProfiles;
+ }
+
+ for (String profile : profiles) {
+ if (StringUtils.hasText(profile)) {
+ activeProfiles.add(profile.trim());
+ }
+ }
+
+ declaringClass = annotation.inheritProfiles() ? findAnnotationDeclaringClass(annotationType,
+ declaringClass.getSuperclass()) : null;
+ }
+
+ return StringUtils.toStringArray(activeProfiles);
+ }
+
+ /**
+ * Build the {@link MergedContextConfiguration merged context configuration} for
+ * the supplied {@link Class testClass} and {@code defaultContextLoaderClassName},
+ * taking into account context hierarchies declared via
+ * {@link ContextHierarchy @ContextHierarchy} and
+ * {@link ContextConfiguration @ContextConfiguration}.
+ *
+ * @param testClass the test class for which the {@code MergedContextConfiguration}
+ * should be built (must not be {@code null})
+ * @param defaultContextLoaderClassName the name of the default {@code ContextLoader}
+ * class to use (may be {@code null})
+ * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to
+ * be passed to the {@code MergedContextConfiguration} constructor
+ * @return the merged context configuration
+ * @see #buildContextHierarchyMap(Class)
+ * @see #buildMergedContextConfiguration(Class, List, String, MergedContextConfiguration, CacheAwareContextLoaderDelegate)
+ */
+ @SuppressWarnings("javadoc")
+ static MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass,
+ String defaultContextLoaderClassName, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
+
+ if (testClass.isAnnotationPresent(ContextHierarchy.class)) {
+ Map<String, List<ContextConfigurationAttributes>> hierarchyMap = buildContextHierarchyMap(testClass);
+
+ MergedContextConfiguration parentConfig = null;
+ MergedContextConfiguration mergedConfig = null;
+
+ for (List<ContextConfigurationAttributes> list : hierarchyMap.values()) {
+ List<ContextConfigurationAttributes> reversedList = new ArrayList<ContextConfigurationAttributes>(list);
+ Collections.reverse(reversedList);
+
+ // Don't use the supplied testClass; instead ensure that we are
+ // building the MCC for the actual test class that declared the
+ // configuration for the current level in the context hierarchy.
+ Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty");
+ Class<?> declaringClass = reversedList.get(0).getDeclaringClass();
+
+ mergedConfig = buildMergedContextConfiguration(declaringClass, reversedList,
+ defaultContextLoaderClassName, parentConfig, cacheAwareContextLoaderDelegate);
+ parentConfig = mergedConfig;
+ }
+
+ // Return the last level in the context hierarchy
+ return mergedConfig;
+ }
+ else {
+ return buildMergedContextConfiguration(testClass, resolveContextConfigurationAttributes(testClass),
+ defaultContextLoaderClassName, null, cacheAwareContextLoaderDelegate);
+ }
+ }
+
+ /**
+ * Build the {@link MergedContextConfiguration merged context configuration} for the
+ * supplied {@link Class testClass}, context configuration attributes,
+ * {@code defaultContextLoaderClassName}, and parent context configuration.
+ *
+ * @param testClass the test class for which the {@code MergedContextConfiguration}
+ * should be built (must not be {@code null})
+ * @param configAttributesList the list of context configuration attributes for the
+ * specified test class, ordered <em>bottom-up</em> (i.e., as if we were
+ * traversing up the class hierarchy); never {@code null} or empty
+ * @param defaultContextLoaderClassName the name of the default {@code ContextLoader}
+ * class to use (may be {@code null})
+ * @param parentConfig the merged context configuration for the parent application
+ * context in a context hierarchy, or {@code null} if there is no parent
+ * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to
+ * be passed to the {@code MergedContextConfiguration} constructor
+ * @return the merged context configuration
+ * @see #resolveContextLoader
+ * @see #resolveContextConfigurationAttributes
+ * @see SmartContextLoader#processContextConfiguration
+ * @see ContextLoader#processLocations
+ * @see #resolveActiveProfiles
+ * @see MergedContextConfiguration
+ */
+ private static MergedContextConfiguration buildMergedContextConfiguration(final Class<?> testClass,
+ final List<ContextConfigurationAttributes> configAttributesList,
+ final String defaultContextLoaderClassName, MergedContextConfiguration parentConfig,
+ CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
+
+ final ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList,
+ defaultContextLoaderClassName);
+ final List<String> locationsList = new ArrayList<String>();
+ final List<Class<?>> classesList = new ArrayList<Class<?>>();
+
+ for (ContextConfigurationAttributes configAttributes : configAttributesList) {
+ if (logger.isTraceEnabled()) {
+ logger.trace(String.format("Processing locations and classes for context configuration attributes %s",
+ configAttributes));
+ }
+
+ if (contextLoader instanceof SmartContextLoader) {
+ SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
+ smartContextLoader.processContextConfiguration(configAttributes);
+ locationsList.addAll(0, Arrays.asList(configAttributes.getLocations()));
+ classesList.addAll(0, Arrays.asList(configAttributes.getClasses()));
+ }
+ else {
+ String[] processedLocations = contextLoader.processLocations(configAttributes.getDeclaringClass(),
+ configAttributes.getLocations());
+ locationsList.addAll(0, Arrays.asList(processedLocations));
+ // Legacy ContextLoaders don't know how to process classes
+ }
+
+ if (!configAttributes.isInheritLocations()) {
+ break;
+ }
+ }
+
+ String[] locations = StringUtils.toStringArray(locationsList);
+ Class<?>[] classes = ClassUtils.toClassArray(classesList);
+ Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = resolveInitializerClasses(configAttributesList);
+ String[] activeProfiles = resolveActiveProfiles(testClass);
+
+ MergedContextConfiguration mergedConfig = buildWebMergedContextConfiguration(testClass, locations, classes,
+ initializerClasses, activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
+
+ if (mergedConfig == null) {
+ mergedConfig = new MergedContextConfiguration(testClass, locations, classes, initializerClasses,
+ activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
+ }
+
+ return mergedConfig;
+ }
+
+ /**
+ * Load the {@link org.springframework.test.context.web.WebAppConfiguration}
+ * class, using reflection in order to avoid package cycles.
+ *
+ * @return the {@code @WebAppConfiguration} class or {@code null} if it cannot be loaded
+ * @since 3.2
+ */
+ @SuppressWarnings("unchecked")
+ private static Class<? extends Annotation> loadWebAppConfigurationClass() {
+ Class<? extends Annotation> webAppConfigClass = null;
+ try {
+ webAppConfigClass = (Class<? extends Annotation>) ClassUtils.forName(WEB_APP_CONFIGURATION_CLASS_NAME,
+ ContextLoaderUtils.class.getClassLoader());
+ }
+ catch (Throwable t) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not load @WebAppConfiguration class [" + WEB_APP_CONFIGURATION_CLASS_NAME + "].", t);
+ }
+ }
+ return webAppConfigClass;
+ }
+
+ /**
+ * Attempt to build a {@link org.springframework.test.context.web.WebMergedContextConfiguration}
+ * from the supplied arguments, using reflection in order to avoid package cycles.
+ *
+ * @return the {@code WebMergedContextConfiguration} or {@code null} if it could not be built
+ * @since 3.2
+ */
+ @SuppressWarnings("unchecked")
+ private static MergedContextConfiguration buildWebMergedContextConfiguration(
+ Class<?> testClass,
+ String[] locations,
+ Class<?>[] classes,
+ Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses,
+ String[] activeProfiles, ContextLoader contextLoader,
+ CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parentConfig) {
+
+ Class<? extends Annotation> webAppConfigClass = loadWebAppConfigurationClass();
+
+ if (webAppConfigClass != null && testClass.isAnnotationPresent(webAppConfigClass)) {
+ Annotation annotation = testClass.getAnnotation(webAppConfigClass);
+ String resourceBasePath = (String) AnnotationUtils.getValue(annotation);
+
+ try {
+ Class<? extends MergedContextConfiguration> webMergedConfigClass = (Class<? extends MergedContextConfiguration>) ClassUtils.forName(
+ WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME, ContextLoaderUtils.class.getClassLoader());
+
+ Constructor<? extends MergedContextConfiguration> constructor = ClassUtils.getConstructorIfAvailable(
+ webMergedConfigClass, Class.class, String[].class, Class[].class, Set.class, String[].class,
+ String.class, ContextLoader.class, CacheAwareContextLoaderDelegate.class,
+ MergedContextConfiguration.class);
+
+ if (constructor != null) {
+ return instantiateClass(constructor, testClass, locations, classes, initializerClasses,
+ activeProfiles, resourceBasePath, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
+ }
+ }
+ catch (Throwable t) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not instantiate [" + WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME + "].", t);
+ }
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java
new file mode 100644
index 00000000..befe85c7
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2002-2013 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.context;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@code MergedContextConfiguration} encapsulates the <em>merged</em>
+ * context configuration declared on a test class and all of its superclasses
+ * via {@link ContextConfiguration @ContextConfiguration} and
+ * {@link ActiveProfiles @ActiveProfiles}.
+ *
+ * <p>Merged resource locations, annotated classes, and active profiles
+ * represent all declared values in the test class hierarchy taking into
+ * consideration the semantics of the
+ * {@link ContextConfiguration#inheritLocations inheritLocations} and
+ * {@link ActiveProfiles#inheritProfiles inheritProfiles} flags in
+ * {@code @ContextConfiguration} and {@code @ActiveProfiles}, respectively.
+ *
+ * <p>A {@link SmartContextLoader} uses {@code MergedContextConfiguration}
+ * to load an {@link org.springframework.context.ApplicationContext ApplicationContext}.
+ *
+ * <p>{@code MergedContextConfiguration} is also used by the {@link TestContext}
+ * as the context cache key for caching an
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}
+ * that was loaded using properties of this {@code MergedContextConfiguration}.
+ *
+ * @author Sam Brannen
+ * @since 3.1
+ * @see ContextConfiguration
+ * @see ActiveProfiles
+ * @see ContextConfigurationAttributes
+ * @see SmartContextLoader#loadContext(MergedContextConfiguration)
+ */
+public class MergedContextConfiguration implements Serializable {
+
+ private static final long serialVersionUID = -3290560718464957422L;
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
+ private static final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> EMPTY_INITIALIZER_CLASSES = //
+ Collections.<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> emptySet();
+
+ private final Class<?> testClass;
+ private final String[] locations;
+ private final Class<?>[] classes;
+ private final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses;
+ private final String[] activeProfiles;
+ private final ContextLoader contextLoader;
+ private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;
+ private final MergedContextConfiguration parent;
+
+
+ private static String[] processLocations(String[] locations) {
+ return locations == null ? EMPTY_STRING_ARRAY : locations;
+ }
+
+ private static Class<?>[] processClasses(Class<?>[] classes) {
+ return classes == null ? EMPTY_CLASS_ARRAY : classes;
+ }
+
+ private static Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> processContextInitializerClasses(
+ Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses) {
+ return contextInitializerClasses == null ? EMPTY_INITIALIZER_CLASSES
+ : Collections.unmodifiableSet(contextInitializerClasses);
+ }
+
+ private static String[] processActiveProfiles(String[] activeProfiles) {
+ if (activeProfiles == null) {
+ return EMPTY_STRING_ARRAY;
+ }
+
+ // Active profiles must be unique and sorted in order to support proper
+ // cache key generation. Specifically, profile sets {foo,bar} and
+ // {bar,foo} must both result in the same array (e.g., [bar,foo]).
+ SortedSet<String> sortedProfilesSet = new TreeSet<String>(Arrays.asList(activeProfiles));
+ return StringUtils.toStringArray(sortedProfilesSet);
+ }
+
+ /**
+ * Generate a null-safe {@link String} representation of the supplied
+ * {@link ContextLoader} based solely on the fully qualified name of the
+ * loader or &quot;null&quot; if the supplied loaded is {@code null}.
+ */
+ protected static String nullSafeToString(ContextLoader contextLoader) {
+ return contextLoader == null ? "null" : contextLoader.getClass().getName();
+ }
+
+ /**
+ * Create a new {@code MergedContextConfiguration} instance for the
+ * supplied test class, resource locations, annotated classes, active
+ * profiles, and {@code ContextLoader}.
+ *
+ * <p>If a {@code null} value is supplied for {@code locations},
+ * {@code classes}, or {@code activeProfiles} an empty array will
+ * be stored instead. Furthermore, active profiles will be sorted, and duplicate
+ * profiles will be removed.
+ *
+ * @param testClass the test class for which the configuration was merged
+ * @param locations the merged resource locations
+ * @param classes the merged annotated classes
+ * @param activeProfiles the merged active bean definition profiles
+ * @param contextLoader the resolved {@code ContextLoader}
+ * @see #MergedContextConfiguration(Class, String[], Class[], Set, String[], ContextLoader)
+ */
+ public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes,
+ String[] activeProfiles, ContextLoader contextLoader) {
+ this(testClass, locations, classes, null, activeProfiles, contextLoader);
+ }
+
+ /**
+ * Create a new {@code MergedContextConfiguration} instance for the
+ * supplied test class, resource locations, annotated classes, context
+ * initializers, active profiles, and {@code ContextLoader}.
+ *
+ * <p>If a {@code null} value is supplied for {@code locations},
+ * {@code classes}, or {@code activeProfiles} an empty array will
+ * be stored instead. If a {@code null} value is supplied for the
+ * {@code contextInitializerClasses} an empty set will be stored instead.
+ * Furthermore, active profiles will be sorted, and duplicate profiles will
+ * be removed.
+ *
+ * @param testClass the test class for which the configuration was merged
+ * @param locations the merged resource locations
+ * @param classes the merged annotated classes
+ * @param contextInitializerClasses the merged context initializer classes
+ * @param activeProfiles the merged active bean definition profiles
+ * @param contextLoader the resolved {@code ContextLoader}
+ * @see #MergedContextConfiguration(Class, String[], Class[], Set, String[], ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)
+ */
+ public MergedContextConfiguration(
+ Class<?> testClass,
+ String[] locations,
+ Class<?>[] classes,
+ Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
+ String[] activeProfiles, ContextLoader contextLoader) {
+ this(testClass, locations, classes, contextInitializerClasses, activeProfiles, contextLoader, null, null);
+ }
+
+ /**
+ * Create a new {@code MergedContextConfiguration} instance for the
+ * supplied test class, resource locations, annotated classes, context
+ * initializers, active profiles, {@code ContextLoader}, and parent
+ * configuration.
+ *
+ * <p>If a {@code null} value is supplied for {@code locations},
+ * {@code classes}, or {@code activeProfiles} an empty array will
+ * be stored instead. If a {@code null} value is supplied for the
+ * {@code contextInitializerClasses} an empty set will be stored instead.
+ * Furthermore, active profiles will be sorted, and duplicate profiles will
+ * be removed.
+ *
+ * @param testClass the test class for which the configuration was merged
+ * @param locations the merged resource locations
+ * @param classes the merged annotated classes
+ * @param contextInitializerClasses the merged context initializer classes
+ * @param activeProfiles the merged active bean definition profiles
+ * @param contextLoader the resolved {@code ContextLoader}
+ * @param cacheAwareContextLoaderDelegate a cache-aware context loader
+ * delegate with which to retrieve the parent context
+ * @param parent the parent configuration or {@code null} if there is no parent
+ * @since 3.2.2
+ */
+ public MergedContextConfiguration(
+ Class<?> testClass,
+ String[] locations,
+ Class<?>[] classes,
+ Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
+ String[] activeProfiles, ContextLoader contextLoader,
+ CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) {
+ this.testClass = testClass;
+ this.locations = processLocations(locations);
+ this.classes = processClasses(classes);
+ this.contextInitializerClasses = processContextInitializerClasses(contextInitializerClasses);
+ this.activeProfiles = processActiveProfiles(activeProfiles);
+ this.contextLoader = contextLoader;
+ this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate;
+ this.parent = parent;
+ }
+
+ /**
+ * Get the {@linkplain Class test class} associated with this {@code MergedContextConfiguration}.
+ */
+ public Class<?> getTestClass() {
+ return testClass;
+ }
+
+ /**
+ * Get the merged resource locations for the {@linkplain #getTestClass() test class}.
+ */
+ public String[] getLocations() {
+ return locations;
+ }
+
+ /**
+ * Get the merged annotated classes for the {@linkplain #getTestClass() test class}.
+ */
+ public Class<?>[] getClasses() {
+ return classes;
+ }
+
+ /**
+ * Get the merged {@code ApplicationContextInitializer} classes for the
+ * {@linkplain #getTestClass() test class}.
+ */
+ public Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> getContextInitializerClasses() {
+ return contextInitializerClasses;
+ }
+
+ /**
+ * Get the merged active bean definition profiles for the {@linkplain #getTestClass() test class}.
+ */
+ public String[] getActiveProfiles() {
+ return activeProfiles;
+ }
+
+ /**
+ * Get the resolved {@link ContextLoader} for the {@linkplain #getTestClass() test class}.
+ */
+ public ContextLoader getContextLoader() {
+ return contextLoader;
+ }
+
+ /**
+ * Get the {@link MergedContextConfiguration} for the parent application context in a
+ * context hierarchy.
+ *
+ * @return the parent configuration or {@code null} if there is no parent
+ * @see #getParentApplicationContext()
+ * @since 3.2.2
+ */
+ public MergedContextConfiguration getParent() {
+ return this.parent;
+ }
+
+ /**
+ * Get the parent {@link ApplicationContext} for the context defined by this
+ * {@code MergedContextConfiguration} from the context cache.
+ * <p>
+ * If the parent context has not yet been loaded, it will be loaded, stored in the
+ * cache, and then returned.
+ *
+ * @return the parent {@code ApplicationContext} or {@code null} if there is no parent
+ * @see #getParent()
+ * @since 3.2.2
+ */
+ public ApplicationContext getParentApplicationContext() {
+ if (parent == null) {
+ return null;
+ }
+
+ Assert.state(cacheAwareContextLoaderDelegate != null,
+ "Cannot retrieve a parent application context without access to the CacheAwareContextLoaderDelegate.");
+ return cacheAwareContextLoaderDelegate.loadContext(parent);
+ }
+
+ /**
+ * Generate a unique hash code for all properties of this
+ * {@code MergedContextConfiguration} excluding the
+ * {@linkplain #getTestClass() test class}.
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Arrays.hashCode(locations);
+ result = prime * result + Arrays.hashCode(classes);
+ result = prime * result + contextInitializerClasses.hashCode();
+ result = prime * result + Arrays.hashCode(activeProfiles);
+ result = prime * result + (parent == null ? 0 : parent.hashCode());
+ result = prime * result + nullSafeToString(contextLoader).hashCode();
+ return result;
+ }
+
+ /**
+ * Determine if the supplied object is equal to this {@code MergedContextConfiguration}
+ * instance by comparing both object's {@linkplain #getLocations() locations},
+ * {@linkplain #getClasses() annotated classes},
+ * {@linkplain #getContextInitializerClasses() context initializer classes},
+ * {@linkplain #getActiveProfiles() active profiles},
+ * {@linkplain #getParent() parents}, and the fully qualified names of their
+ * {@link #getContextLoader() ContextLoaders}.
+ */
+ @Override
+ public boolean equals(Object obj) {
+
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof MergedContextConfiguration)) {
+ return false;
+ }
+
+ final MergedContextConfiguration that = (MergedContextConfiguration) obj;
+
+ if (!Arrays.equals(this.locations, that.locations)) {
+ return false;
+ }
+
+ if (!Arrays.equals(this.classes, that.classes)) {
+ return false;
+ }
+
+ if (!this.contextInitializerClasses.equals(that.contextInitializerClasses)) {
+ return false;
+ }
+
+ if (!Arrays.equals(this.activeProfiles, that.activeProfiles)) {
+ return false;
+ }
+
+ if (this.parent == null) {
+ if (that.parent != null) {
+ return false;
+ }
+ }
+ else if (!this.parent.equals(that.parent)) {
+ return false;
+ }
+
+ if (!nullSafeToString(this.contextLoader).equals(nullSafeToString(that.contextLoader))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Provide a String representation of the {@linkplain #getTestClass() test class},
+ * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes},
+ * {@linkplain #getContextInitializerClasses() context initializer classes},
+ * {@linkplain #getActiveProfiles() active profiles}, the name of the
+ * {@link #getContextLoader() ContextLoader}, and the
+ * {@linkplain #getParent() parent configuration}.
+ */
+ @Override
+ public String toString() {
+ return new ToStringCreator(this)//
+ .append("testClass", testClass)//
+ .append("locations", ObjectUtils.nullSafeToString(locations))//
+ .append("classes", ObjectUtils.nullSafeToString(classes))//
+ .append("contextInitializerClasses", ObjectUtils.nullSafeToString(contextInitializerClasses))//
+ .append("activeProfiles", ObjectUtils.nullSafeToString(activeProfiles))//
+ .append("contextLoader", nullSafeToString(contextLoader))//
+ .append("parent", parent)//
+ .toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java
new file mode 100644
index 00000000..d0495ca0
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2002-2013 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.context;
+
+import org.springframework.context.ApplicationContext;
+
+/**
+ * Strategy interface for loading an {@link ApplicationContext application context}
+ * for an integration test managed by the Spring TestContext Framework.
+ *
+ * <p>The {@code SmartContextLoader} SPI supersedes the {@link ContextLoader} SPI
+ * introduced in Spring 2.5: a {@code SmartContextLoader} can choose to process
+ * either resource locations or annotated classes. Furthermore, a
+ * {@code SmartContextLoader} can set active bean definition profiles in the
+ * context that it loads (see {@link MergedContextConfiguration#getActiveProfiles()}
+ * and {@link #loadContext(MergedContextConfiguration)}).
+ *
+ * <p>See the Javadoc for {@link ContextConfiguration @ContextConfiguration}
+ * for a definition of <em>annotated class</em>.
+ *
+ * <p>Clients of a {@code SmartContextLoader} should call
+ * {@link #processContextConfiguration(ContextConfigurationAttributes)
+ * processContextConfiguration()} prior to calling
+ * {@link #loadContext(MergedContextConfiguration) loadContext()}. This gives a
+ * {@code SmartContextLoader} the opportunity to provide custom support for
+ * modifying resource locations or detecting default resource locations or
+ * default configuration classes. The results of
+ * {@link #processContextConfiguration(ContextConfigurationAttributes)
+ * processContextConfiguration()} should be merged for all classes in the
+ * hierarchy of the root test class and then supplied to
+ * {@link #loadContext(MergedContextConfiguration) loadContext()}.
+ *
+ * <p>Even though {@code SmartContextLoader} extends {@code ContextLoader},
+ * clients should favor {@code SmartContextLoader}-specific methods over those
+ * defined in {@code ContextLoader}, particularly because a
+ * {@code SmartContextLoader} may choose not to support methods defined in the
+ * {@code ContextLoader} SPI.
+ *
+ * <p>Concrete implementations must provide a {@code public} no-args constructor.
+ *
+ * <p>Spring provides the following out-of-the-box implementations:
+ * <ul>
+ * <li>{@link org.springframework.test.context.support.DelegatingSmartContextLoader DelegatingSmartContextLoader}</li>
+ * <li>{@link org.springframework.test.context.support.AnnotationConfigContextLoader AnnotationConfigContextLoader}</li>
+ * <li>{@link org.springframework.test.context.support.GenericXmlContextLoader GenericXmlContextLoader}</li>
+ * <li>{@link org.springframework.test.context.support.GenericPropertiesContextLoader GenericPropertiesContextLoader}</li>
+ * <li>{@link org.springframework.test.context.web.WebDelegatingSmartContextLoader WebDelegatingSmartContextLoader}</li>
+ * <li>{@link org.springframework.test.context.web.AnnotationConfigWebContextLoader AnnotationConfigWebContextLoader}</li>
+ * <li>{@link org.springframework.test.context.web.GenericXmlWebContextLoader GenericXmlWebContextLoader}</li>
+ * </ul>
+ *
+ * @author Sam Brannen
+ * @since 3.1
+ * @see ContextConfiguration
+ * @see ActiveProfiles
+ * @see ContextConfigurationAttributes
+ * @see MergedContextConfiguration
+ */
+public interface SmartContextLoader extends ContextLoader {
+
+ /**
+ * Processes the {@link ContextConfigurationAttributes} for a given test class.
+ * <p>Concrete implementations may choose to <em>modify</em> the {@code locations}
+ * or {@code classes} in the supplied {@link ContextConfigurationAttributes},
+ * <em>generate</em> default configuration locations, or <em>detect</em>
+ * default configuration classes if the supplied values are {@code null}
+ * or empty.
+ * <p><b>Note</b>: in contrast to a standard {@code ContextLoader}, a
+ * {@code SmartContextLoader} <b>must</b> <em>preemptively</em> verify that
+ * a generated or detected default actually exists before setting the corresponding
+ * {@code locations} or {@code classes} property in the supplied
+ * {@link ContextConfigurationAttributes}. Consequently, leaving the
+ * {@code locations} or {@code classes} property empty signals that
+ * this {@code SmartContextLoader} was not able to generate or detect defaults.
+ * @param configAttributes the context configuration attributes to process
+ */
+ void processContextConfiguration(ContextConfigurationAttributes configAttributes);
+
+ /**
+ * Loads a new {@link ApplicationContext context} based on the supplied
+ * {@link MergedContextConfiguration merged context configuration},
+ * configures the context, and finally returns the context in a fully
+ * <em>refreshed</em> state.
+ * <p>Concrete implementations should register annotation configuration
+ * processors with bean factories of
+ * {@link ApplicationContext application contexts} loaded by this
+ * {@code SmartContextLoader}. Beans will therefore automatically be
+ * candidates for annotation-based dependency injection using
+ * {@link org.springframework.beans.factory.annotation.Autowired @Autowired},
+ * {@link javax.annotation.Resource @Resource}, and
+ * {@link javax.inject.Inject @Inject}. In addition, concrete implementations
+ * should set the active bean definition profiles in the context's
+ * {@link org.springframework.core.env.Environment Environment}.
+ * <p>Any {@code ApplicationContext} loaded by a
+ * {@code SmartContextLoader} <strong>must</strong> register a JVM
+ * shutdown hook for itself. Unless the context gets closed early, all context
+ * instances will be automatically closed on JVM shutdown. This allows for
+ * freeing of external resources held by beans within the context (e.g.,
+ * temporary files).
+ * @param mergedConfig the merged context configuration to use to load the
+ * application context
+ * @return a new application context
+ * @throws Exception if context loading failed
+ * @see #processContextConfiguration(ContextConfigurationAttributes)
+ * @see org.springframework.context.annotation.AnnotationConfigUtils
+ * #registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry)
+ * @see MergedContextConfiguration#getActiveProfiles()
+ * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment()
+ */
+ ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContext.java b/spring-test/src/main/java/org/springframework/test/context/TestContext.java
new file mode 100644
index 00000000..9324340f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/TestContext.java
@@ -0,0 +1,220 @@
+/*
+ * 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.context;
+
+import java.lang.reflect.Method;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.AttributeAccessorSupport;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
+import org.springframework.util.Assert;
+
+/**
+ * {@code TestContext} encapsulates the context in which a test is executed,
+ * agnostic of the actual testing framework in use.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+public class TestContext extends AttributeAccessorSupport {
+
+ private static final long serialVersionUID = -5827157174866681233L;
+
+ private static final Log logger = LogFactory.getLog(TestContext.class);
+
+ private final ContextCache contextCache;
+
+ private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;
+
+ private final MergedContextConfiguration mergedContextConfiguration;
+
+ private final Class<?> testClass;
+
+ private Object testInstance;
+
+ private Method testMethod;
+
+ private Throwable testException;
+
+
+ /**
+ * Delegates to {@link #TestContext(Class, ContextCache, String)} with a
+ * value of {@code null} for the default {@code ContextLoader} class name.
+ */
+ TestContext(Class<?> testClass, ContextCache contextCache) {
+ this(testClass, contextCache, null);
+ }
+
+ /**
+ * Construct a new test context for the supplied {@linkplain Class test class}
+ * and {@linkplain ContextCache context cache} and parse the corresponding
+ * {@link ContextConfiguration &#064;ContextConfiguration} or
+ * {@link ContextHierarchy &#064;ContextHierarchy} annotation, if present.
+ * <p>If the supplied class name for the default {@code ContextLoader}
+ * is {@code null} or <em>empty</em> and no concrete {@code ContextLoader}
+ * class is explicitly supplied via {@code @ContextConfiguration}, a
+ * {@link org.springframework.test.context.support.DelegatingSmartContextLoader
+ * DelegatingSmartContextLoader} or
+ * {@link org.springframework.test.context.web.WebDelegatingSmartContextLoader
+ * WebDelegatingSmartContextLoader} will be used instead.
+ * @param testClass the test class for which the test context should be
+ * constructed (must not be {@code null})
+ * @param contextCache the context cache from which the constructed test
+ * context should retrieve application contexts (must not be {@code null})
+ * @param defaultContextLoaderClassName the name of the default
+ * {@code ContextLoader} class to use (may be {@code null})
+ */
+ TestContext(Class<?> testClass, ContextCache contextCache, String defaultContextLoaderClassName) {
+ Assert.notNull(testClass, "Test class must not be null");
+ Assert.notNull(contextCache, "ContextCache must not be null");
+
+ this.testClass = testClass;
+ this.contextCache = contextCache;
+ this.cacheAwareContextLoaderDelegate = new CacheAwareContextLoaderDelegate(contextCache);
+
+ MergedContextConfiguration mergedContextConfiguration;
+
+ if (testClass.isAnnotationPresent(ContextConfiguration.class) ||
+ testClass.isAnnotationPresent(ContextHierarchy.class)) {
+ mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass,
+ defaultContextLoaderClassName, cacheAwareContextLoaderDelegate);
+ }
+ else {
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format(
+ "Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]",
+ testClass.getName()));
+ }
+ mergedContextConfiguration = new MergedContextConfiguration(testClass, null, null, null, null);
+ }
+
+ this.mergedContextConfiguration = mergedContextConfiguration;
+ }
+
+
+ /**
+ * Get the {@link ApplicationContext application context} for this test
+ * context, possibly cached.
+ * @return the application context
+ * @throws IllegalStateException if an error occurs while retrieving the
+ * application context
+ */
+ public ApplicationContext getApplicationContext() {
+ return this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration);
+ }
+
+ /**
+ * Get the {@link Class test class} for this test context.
+ * @return the test class (never {@code null})
+ */
+ public Class<?> getTestClass() {
+ return this.testClass;
+ }
+
+ /**
+ * Get the current {@link Object test instance} for this test context.
+ * <p>Note: this is a mutable property.
+ * @return the current test instance (may be {@code null})
+ * @see #updateState(Object, Method, Throwable)
+ */
+ public Object getTestInstance() {
+ return this.testInstance;
+ }
+
+ /**
+ * Get the current {@link Method test method} for this test context.
+ * <p>Note: this is a mutable property.
+ * @return the current test method (may be {@code null})
+ * @see #updateState(Object, Method, Throwable)
+ */
+ public Method getTestMethod() {
+ return this.testMethod;
+ }
+
+ /**
+ * Get the {@link Throwable exception} that was thrown during execution of
+ * the {@link #getTestMethod() test method}.
+ * <p>Note: this is a mutable property.
+ * @return the exception that was thrown, or {@code null} if no
+ * exception was thrown
+ * @see #updateState(Object, Method, Throwable)
+ */
+ public Throwable getTestException() {
+ return this.testException;
+ }
+
+ /**
+ * Call this method to signal that the {@linkplain ApplicationContext application
+ * context} associated with this test context is <em>dirty</em> and should be
+ * discarded. Do this if a test has modified the context &mdash; for example,
+ * by replacing a bean definition or modifying the state of a singleton bean.
+ * @deprecated as of Spring 3.2.2; use
+ * {@link #markApplicationContextDirty(DirtiesContext.HierarchyMode)} instead.
+ */
+ @Deprecated
+ public void markApplicationContextDirty() {
+ markApplicationContextDirty((HierarchyMode) null);
+ }
+
+ /**
+ * Call this method to signal that the {@linkplain ApplicationContext application
+ * context} associated with this test context is <em>dirty</em> and should be
+ * discarded. Do this if a test has modified the context &mdash; for example,
+ * by replacing a bean definition or modifying the state of a singleton bean.
+ * @param hierarchyMode the context cache clearing mode to be applied if the
+ * context is part of a hierarchy (may be {@code null})
+ */
+ public void markApplicationContextDirty(HierarchyMode hierarchyMode) {
+ synchronized (this.contextCache) {
+ this.contextCache.remove(this.mergedContextConfiguration, hierarchyMode);
+ }
+ }
+
+ /**
+ * Update this test context to reflect the state of the currently executing test.
+ * @param testInstance the current test instance (may be {@code null})
+ * @param testMethod the current test method (may be {@code null})
+ * @param testException the exception that was thrown in the test method, or
+ * {@code null} if no exception was thrown
+ */
+ void updateState(Object testInstance, Method testMethod, Throwable testException) {
+ this.testInstance = testInstance;
+ this.testMethod = testMethod;
+ this.testException = testException;
+ }
+
+
+ /**
+ * Provide a String representation of this test context's state.
+ */
+ @Override
+ public String toString() {
+ return new ToStringCreator(this)
+ .append("testClass", this.testClass)
+ .append("testInstance", this.testInstance)
+ .append("testMethod", this.testMethod)
+ .append("testException", this.testException)
+ .append("mergedContextConfiguration", this.mergedContextConfiguration)
+ .toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java
new file mode 100644
index 00000000..4cf8b13f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java
@@ -0,0 +1,463 @@
+/*
+ * 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.context;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.BeanInstantiationException;
+import org.springframework.beans.BeanUtils;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * {@code TestContextManager} is the main entry point into the <em>Spring
+ * TestContext Framework</em>, which provides support for loading and accessing
+ * {@link org.springframework.context.ApplicationContext application contexts},
+ * dependency injection of test instances,
+ * {@link org.springframework.transaction.annotation.Transactional transactional}
+ * execution of test methods, etc.
+ *
+ * <p>Specifically, a {@code TestContextManager} is responsible for managing a
+ * single {@link TestContext} and signaling events to all registered
+ * {@link TestExecutionListener TestExecutionListeners} at well defined test
+ * execution points:
+ *
+ * <ul>
+ * <li>{@link #beforeTestClass() before test class execution}: prior to any
+ * <em>before class methods</em> of a particular testing framework (e.g., JUnit
+ * 4's {@link org.junit.BeforeClass @BeforeClass})</li>
+ * <li>{@link #prepareTestInstance(Object) test instance preparation}:
+ * immediately following instantiation of the test instance</li>
+ * <li>{@link #beforeTestMethod(Object, Method) before test method execution}:
+ * prior to any <em>before methods</em> of a particular testing framework (e.g.,
+ * JUnit 4's {@link org.junit.Before @Before})</li>
+ * <li>{@link #afterTestMethod(Object, Method, Throwable) after test method
+ * execution}: after any <em>after methods</em> of a particular testing
+ * framework (e.g., JUnit 4's {@link org.junit.After @After})</li>
+ * <li>{@link #afterTestClass() after test class execution}: after any
+ * <em>after class methods</em> of a particular testing framework (e.g., JUnit
+ * 4's {@link org.junit.AfterClass @AfterClass})</li>
+ * </ul>
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see TestContext
+ * @see TestExecutionListeners
+ * @see ContextConfiguration
+ * @see org.springframework.test.context.transaction.TransactionConfiguration
+ */
+public class TestContextManager {
+
+ private static final String[] DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES = new String[] {
+ "org.springframework.test.context.web.ServletTestExecutionListener",
+ "org.springframework.test.context.support.DependencyInjectionTestExecutionListener",
+ "org.springframework.test.context.support.DirtiesContextTestExecutionListener",
+ "org.springframework.test.context.transaction.TransactionalTestExecutionListener" };
+
+ private static final Log logger = LogFactory.getLog(TestContextManager.class);
+
+ /**
+ * Cache of Spring application contexts.
+ * <p>This needs to be static, since test instances may be destroyed and
+ * recreated between invocations of individual test methods, as is the case
+ * with JUnit.
+ */
+ static final ContextCache contextCache = new ContextCache();
+
+ private final TestContext testContext;
+
+ private final List<TestExecutionListener> testExecutionListeners = new ArrayList<TestExecutionListener>();
+
+
+ /**
+ * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class}
+ * and automatically {@link #registerTestExecutionListeners register} the
+ * {@link TestExecutionListener TestExecutionListeners} configured for the test class
+ * via the {@link TestExecutionListeners &#064;TestExecutionListeners} annotation.
+ * <p>Delegates to {@link #TestContextManager(Class, String)} with a value of
+ * {@code null} for the default {@code ContextLoader} class name.
+ */
+ public TestContextManager(Class<?> testClass) {
+ this(testClass, null);
+ }
+
+ /**
+ * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class}
+ * and automatically {@link #registerTestExecutionListeners register} the
+ * {@link TestExecutionListener TestExecutionListeners} configured for the test class
+ * via the {@link TestExecutionListeners &#064;TestExecutionListeners} annotation.
+ * @param testClass the test class to be managed
+ * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} class
+ * to use (may be {@code null})
+ * @see #registerTestExecutionListeners
+ */
+ public TestContextManager(Class<?> testClass, String defaultContextLoaderClassName) {
+ this.testContext = new TestContext(testClass, contextCache, defaultContextLoaderClassName);
+ registerTestExecutionListeners(retrieveTestExecutionListeners(testClass));
+ }
+
+
+ /**
+ * Get the {@link TestContext} managed by this {@code TestContextManager}.
+ */
+ protected final TestContext getTestContext() {
+ return this.testContext;
+ }
+
+ /**
+ * Register the supplied {@link TestExecutionListener TestExecutionListeners}
+ * by appending them to the set of listeners used by this {@code TestContextManager}.
+ */
+ public void registerTestExecutionListeners(TestExecutionListener... testExecutionListeners) {
+ for (TestExecutionListener listener : testExecutionListeners) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Registering TestExecutionListener: " + listener);
+ }
+ this.testExecutionListeners.add(listener);
+ }
+ }
+
+ /**
+ * Get the current {@link TestExecutionListener TestExecutionListeners}
+ * registered for this {@code TestContextManager}.
+ * <p>Allows for modifications, e.g. adding a listener to the beginning of the list.
+ * However, make sure to keep the list stable while actually executing tests.
+ */
+ public final List<TestExecutionListener> getTestExecutionListeners() {
+ return this.testExecutionListeners;
+ }
+
+ /**
+ * Get a copy of the {@link TestExecutionListener TestExecutionListeners}
+ * registered for this {@code TestContextManager} in reverse order.
+ */
+ private List<TestExecutionListener> getReversedTestExecutionListeners() {
+ List<TestExecutionListener> listenersReversed = new ArrayList<TestExecutionListener>(getTestExecutionListeners());
+ Collections.reverse(listenersReversed);
+ return listenersReversed;
+ }
+
+ /**
+ * Retrieve an array of newly instantiated {@link TestExecutionListener TestExecutionListeners}
+ * for the specified {@link Class class}. If {@link TestExecutionListeners @TestExecutionListeners}
+ * is not <em>present</em> on the supplied class, the default listeners will be returned.
+ * <p>Note that the {@link TestExecutionListeners#inheritListeners() inheritListeners} flag of
+ * {@link TestExecutionListeners @TestExecutionListeners} will be taken into consideration.
+ * Specifically, if the {@code inheritListeners} flag is set to {@code true}, listeners
+ * defined in the annotated class will be appended to the listeners defined in superclasses.
+ * @param clazz the test class for which the listeners should be retrieved
+ * @return an array of TestExecutionListeners for the specified class
+ */
+ private TestExecutionListener[] retrieveTestExecutionListeners(Class<?> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ Class<TestExecutionListeners> annotationType = TestExecutionListeners.class;
+ List<Class<? extends TestExecutionListener>> classesList = new ArrayList<Class<? extends TestExecutionListener>>();
+ Class<?> declaringClass = AnnotationUtils.findAnnotationDeclaringClass(annotationType, clazz);
+
+ // Use defaults?
+ if (declaringClass == null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("@TestExecutionListeners is not present for class [" + clazz.getName() + "]: using defaults.");
+ }
+ classesList.addAll(getDefaultTestExecutionListenerClasses());
+ }
+ else {
+ // Traverse the class hierarchy...
+ while (declaringClass != null) {
+ TestExecutionListeners testExecutionListeners = declaringClass.getAnnotation(annotationType);
+ if (logger.isTraceEnabled()) {
+ logger.trace("Retrieved @TestExecutionListeners [" + testExecutionListeners +
+ "] for declaring class [" + declaringClass.getName() + "].");
+ }
+
+ Class<? extends TestExecutionListener>[] valueListenerClasses = testExecutionListeners.value();
+ Class<? extends TestExecutionListener>[] listenerClasses = testExecutionListeners.listeners();
+ if (!ObjectUtils.isEmpty(valueListenerClasses) && !ObjectUtils.isEmpty(listenerClasses)) {
+ throw new IllegalStateException(String.format("Class [%s] configured with @TestExecutionListeners' " +
+ "'value' [%s] and 'listeners' [%s] attributes. Use one or the other, but not both.",
+ declaringClass.getName(), ObjectUtils.nullSafeToString(valueListenerClasses),
+ ObjectUtils.nullSafeToString(listenerClasses)));
+ }
+ else if (!ObjectUtils.isEmpty(valueListenerClasses)) {
+ listenerClasses = valueListenerClasses;
+ }
+
+ if (listenerClasses != null) {
+ classesList.addAll(0, Arrays.<Class<? extends TestExecutionListener>> asList(listenerClasses));
+ }
+ declaringClass = (testExecutionListeners.inheritListeners() ? AnnotationUtils.findAnnotationDeclaringClass(
+ annotationType, declaringClass.getSuperclass()) : null);
+ }
+ }
+
+ List<TestExecutionListener> listeners = new ArrayList<TestExecutionListener>(classesList.size());
+ for (Class<? extends TestExecutionListener> listenerClass : classesList) {
+ NoClassDefFoundError ncdfe = null;
+ try {
+ listeners.add(BeanUtils.instantiateClass(listenerClass));
+ }
+ catch (NoClassDefFoundError err) {
+ ncdfe = err;
+ }
+ catch (BeanInstantiationException ex) {
+ if (ex.getCause() instanceof NoClassDefFoundError) {
+ ncdfe = (NoClassDefFoundError) ex.getCause();
+ }
+ }
+ if (ncdfe != null) {
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format("Could not instantiate TestExecutionListener [%s]. " +
+ "Specify custom listener classes or make the default listener classes " +
+ "(and their required dependencies) available. Offending class: [%s]",
+ listenerClass.getName(), ncdfe.getMessage()));
+ }
+ }
+ }
+ return listeners.toArray(new TestExecutionListener[listeners.size()]);
+ }
+
+ /**
+ * Determine the default {@link TestExecutionListener} classes.
+ */
+ @SuppressWarnings("unchecked")
+ protected Set<Class<? extends TestExecutionListener>> getDefaultTestExecutionListenerClasses() {
+ Set<Class<? extends TestExecutionListener>> defaultListenerClasses = new LinkedHashSet<Class<? extends TestExecutionListener>>();
+ ClassLoader cl = getClass().getClassLoader();
+ for (String className : DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES) {
+ try {
+ defaultListenerClasses.add((Class<? extends TestExecutionListener>) ClassUtils.forName(className, cl));
+ }
+ catch (Throwable ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not load default TestExecutionListener class [" + className +
+ "]. Specify custom listener classes or make the default listener classes available.", ex);
+ }
+ }
+ }
+ return defaultListenerClasses;
+ }
+
+ /**
+ * Hook for pre-processing a test class <em>before</em> execution of any
+ * tests within the class. Should be called prior to any framework-specific
+ * <em>before class methods</em> (e.g., methods annotated with JUnit's
+ * {@link org.junit.BeforeClass @BeforeClass}).
+ * <p>An attempt will be made to give each registered
+ * {@link TestExecutionListener} a chance to pre-process the test class
+ * execution. If a listener throws an exception, however, the remaining
+ * registered listeners will <strong>not</strong> be called.
+ * @throws Exception if a registered TestExecutionListener throws an
+ * exception
+ * @see #getTestExecutionListeners()
+ */
+ public void beforeTestClass() throws Exception {
+ Class<?> testClass = getTestContext().getTestClass();
+ if (logger.isTraceEnabled()) {
+ logger.trace("beforeTestClass(): class [" + testClass.getName() + "]");
+ }
+ getTestContext().updateState(null, null, null);
+
+ for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
+ try {
+ testExecutionListener.beforeTestClass(getTestContext());
+ }
+ catch (Exception ex) {
+ logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
+ "] to process 'before class' callback for test class [" + testClass + "]", ex);
+ throw ex;
+ }
+ }
+ }
+
+ /**
+ * Hook for preparing a test instance prior to execution of any individual
+ * test methods, for example for injecting dependencies, etc. Should be
+ * called immediately after instantiation of the test instance.
+ * <p>The managed {@link TestContext} will be updated with the supplied
+ * {@code testInstance}.
+ * <p>An attempt will be made to give each registered
+ * {@link TestExecutionListener} a chance to prepare the test instance. If a
+ * listener throws an exception, however, the remaining registered listeners
+ * will <strong>not</strong> be called.
+ * @param testInstance the test instance to prepare (never {@code null})
+ * @throws Exception if a registered TestExecutionListener throws an exception
+ * @see #getTestExecutionListeners()
+ */
+ public void prepareTestInstance(Object testInstance) throws Exception {
+ Assert.notNull(testInstance, "testInstance must not be null");
+ if (logger.isTraceEnabled()) {
+ logger.trace("prepareTestInstance(): instance [" + testInstance + "]");
+ }
+ getTestContext().updateState(testInstance, null, null);
+
+ for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
+ try {
+ testExecutionListener.prepareTestInstance(getTestContext());
+ }
+ catch (Exception ex) {
+ logger.error("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
+ "] to prepare test instance [" + testInstance + "]", ex);
+ throw ex;
+ }
+ }
+ }
+
+ /**
+ * Hook for pre-processing a test <em>before</em> execution of the supplied
+ * {@link Method test method}, for example for setting up test fixtures,
+ * starting a transaction, etc. Should be called prior to any
+ * framework-specific <em>before methods</em> (e.g., methods annotated with
+ * JUnit's {@link org.junit.Before @Before}).
+ * <p>The managed {@link TestContext} will be updated with the supplied
+ * {@code testInstance} and {@code testMethod}.
+ * <p>An attempt will be made to give each registered
+ * {@link TestExecutionListener} a chance to pre-process the test method
+ * execution. If a listener throws an exception, however, the remaining
+ * registered listeners will <strong>not</strong> be called.
+ * @param testInstance the current test instance (never {@code null})
+ * @param testMethod the test method which is about to be executed on the
+ * test instance
+ * @throws Exception if a registered TestExecutionListener throws an exception
+ * @see #getTestExecutionListeners()
+ */
+ public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception {
+ Assert.notNull(testInstance, "Test instance must not be null");
+ if (logger.isTraceEnabled()) {
+ logger.trace("beforeTestMethod(): instance [" + testInstance + "], method [" + testMethod + "]");
+ }
+ getTestContext().updateState(testInstance, testMethod, null);
+
+ for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
+ try {
+ testExecutionListener.beforeTestMethod(getTestContext());
+ }
+ catch (Exception ex) {
+ logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
+ "] to process 'before' execution of test method [" + testMethod + "] for test instance [" +
+ testInstance + "]", ex);
+ throw ex;
+ }
+ }
+ }
+
+ /**
+ * Hook for post-processing a test <em>after</em> execution of the supplied
+ * {@link Method test method}, for example for tearing down test fixtures,
+ * ending a transaction, etc. Should be called after any framework-specific
+ * <em>after methods</em> (e.g., methods annotated with JUnit's
+ * {@link org.junit.After @After}).
+ * <p>The managed {@link TestContext} will be updated with the supplied
+ * {@code testInstance}, {@code testMethod}, and
+ * {@code exception}.
+ * <p>Each registered {@link TestExecutionListener} will be given a chance to
+ * post-process the test method execution. If a listener throws an
+ * exception, the remaining registered listeners will still be called, but
+ * the first exception thrown will be tracked and rethrown after all
+ * listeners have executed. Note that registered listeners will be executed
+ * in the opposite order in which they were registered.
+ * @param testInstance the current test instance (never {@code null})
+ * @param testMethod the test method which has just been executed on the
+ * test instance
+ * @param exception the exception that was thrown during execution of the
+ * test method or by a TestExecutionListener, or {@code null} if none
+ * was thrown
+ * @throws Exception if a registered TestExecutionListener throws an exception
+ * @see #getTestExecutionListeners()
+ */
+ public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception {
+ Assert.notNull(testInstance, "testInstance must not be null");
+ if (logger.isTraceEnabled()) {
+ logger.trace("afterTestMethod(): instance [" + testInstance + "], method [" + testMethod +
+ "], exception [" + exception + "]");
+ }
+ getTestContext().updateState(testInstance, testMethod, exception);
+
+ Exception afterTestMethodException = null;
+ // Traverse the TestExecutionListeners in reverse order to ensure proper
+ // "wrapper"-style execution of listeners.
+ for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) {
+ try {
+ testExecutionListener.afterTestMethod(getTestContext());
+ }
+ catch (Exception ex) {
+ logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
+ "] to process 'after' execution for test: method [" + testMethod + "], instance [" +
+ testInstance + "], exception [" + exception + "]", ex);
+ if (afterTestMethodException == null) {
+ afterTestMethodException = ex;
+ }
+ }
+ }
+ if (afterTestMethodException != null) {
+ throw afterTestMethodException;
+ }
+ }
+
+ /**
+ * Hook for post-processing a test class <em>after</em> execution of all
+ * tests within the class. Should be called after any framework-specific
+ * <em>after class methods</em> (e.g., methods annotated with JUnit's
+ * {@link org.junit.AfterClass @AfterClass}).
+ * <p>Each registered {@link TestExecutionListener} will be given a chance to
+ * post-process the test class. If a listener throws an exception, the
+ * remaining registered listeners will still be called, but the first
+ * exception thrown will be tracked and rethrown after all listeners have
+ * executed. Note that registered listeners will be executed in the opposite
+ * order in which they were registered.
+ * @throws Exception if a registered TestExecutionListener throws an exception
+ * @see #getTestExecutionListeners()
+ */
+ public void afterTestClass() throws Exception {
+ Class<?> testClass = getTestContext().getTestClass();
+ if (logger.isTraceEnabled()) {
+ logger.trace("afterTestClass(): class [" + testClass.getName() + "]");
+ }
+ getTestContext().updateState(null, null, null);
+
+ Exception afterTestClassException = null;
+ // Traverse the TestExecutionListeners in reverse order to ensure proper
+ // "wrapper"-style execution of listeners.
+ for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) {
+ try {
+ testExecutionListener.afterTestClass(getTestContext());
+ }
+ catch (Exception ex) {
+ logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
+ "] to process 'after class' callback for test class [" + testClass + "]", ex);
+ if (afterTestClassException == null) {
+ afterTestClassException = ex;
+ }
+ }
+ }
+ if (afterTestClassException != null) {
+ throw afterTestClassException;
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java
new file mode 100644
index 00000000..a052523f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2002-2013 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.context;
+
+/**
+ * {@code TestExecutionListener} defines a <em>listener</em> API for reacting to
+ * test execution events published by the {@link TestContextManager} with which
+ * the listener is registered.
+ * <p>
+ * Concrete implementations must provide a {@code public} no-args constructor,
+ * so that listeners can be instantiated transparently by tools and configuration
+ * mechanisms.
+ * <p>
+ * Spring provides the following out-of-the-box implementations:
+ * <ul>
+ * <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener
+ * DependencyInjectionTestExecutionListener}</li>
+ * <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener
+ * DirtiesContextTestExecutionListener}</li>
+ * <li>{@link org.springframework.test.context.transaction.TransactionalTestExecutionListener
+ * TransactionalTestExecutionListener}</li>
+ * <li>{@link org.springframework.test.context.web.ServletTestExecutionListener
+ * ServletTestExecutionListener}</li>
+ * </ul>
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+public interface TestExecutionListener {
+
+ /**
+ * Pre-processes a test class <em>before</em> execution of all tests within
+ * the class.
+ * <p>
+ * This method should be called immediately before framework-specific
+ * <em>before class</em> lifecycle callbacks.
+ * <p>
+ * If a given testing framework (e.g., JUnit 3.8) does not support
+ * <em>before class</em> lifecycle callbacks, this method will not be called
+ * for that framework.
+ *
+ * @param testContext the test context for the test; never {@code null}
+ * @throws Exception allows any exception to propagate
+ */
+ void beforeTestClass(TestContext testContext) throws Exception;
+
+ /**
+ * Prepares the {@link Object test instance} of the supplied
+ * {@link TestContext test context}, for example by injecting dependencies.
+ * <p>
+ * This method should be called immediately after instantiation of the test
+ * instance but prior to any framework-specific lifecycle callbacks.
+ *
+ * @param testContext the test context for the test; never {@code null}
+ * @throws Exception allows any exception to propagate
+ */
+ void prepareTestInstance(TestContext testContext) throws Exception;
+
+ /**
+ * Pre-processes a test <em>before</em> execution of the
+ * {@link java.lang.reflect.Method test method} in the supplied
+ * {@link TestContext test context}, for example by setting up test
+ * fixtures.
+ * <p>
+ * This method should be called immediately prior to framework-specific
+ * <em>before</em> lifecycle callbacks.
+ *
+ * @param testContext the test context in which the test method will be
+ * executed; never {@code null}
+ * @throws Exception allows any exception to propagate
+ */
+ void beforeTestMethod(TestContext testContext) throws Exception;
+
+ /**
+ * Post-processes a test <em>after</em> execution of the
+ * {@link java.lang.reflect.Method test method} in the supplied
+ * {@link TestContext test context}, for example by tearing down test
+ * fixtures.
+ * <p>
+ * This method should be called immediately after framework-specific
+ * <em>after</em> lifecycle callbacks.
+ *
+ * @param testContext the test context in which the test method was
+ * executed; never {@code null}
+ * @throws Exception allows any exception to propagate
+ */
+ void afterTestMethod(TestContext testContext) throws Exception;
+
+ /**
+ * Post-processes a test class <em>after</em> execution of all tests within
+ * the class.
+ * <p>
+ * This method should be called immediately after framework-specific
+ * <em>after class</em> lifecycle callbacks.
+ * <p>
+ * If a given testing framework (e.g., JUnit 3.8) does not support
+ * <em>after class</em> lifecycle callbacks, this method will not be called
+ * for that framework.
+ *
+ * @param testContext the test context for the test; never {@code null}
+ * @throws Exception allows any exception to propagate
+ */
+ void afterTestClass(TestContext testContext) throws Exception;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java
new file mode 100644
index 00000000..388f66b5
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2002-2013 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.context;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * {@code TestExecutionListeners} defines class-level metadata for
+ * configuring which {@link TestExecutionListener TestExecutionListeners} should
+ * be registered with a {@link TestContextManager}.
+ *
+ * <p>Typically, {@code @TestExecutionListeners} will be used in conjunction with
+ * {@link ContextConfiguration @ContextConfiguration}.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ * @see TestExecutionListener
+ * @see TestContextManager
+ * @see ContextConfiguration
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface TestExecutionListeners {
+
+ /**
+ * The {@link TestExecutionListener TestExecutionListeners} to register with
+ * a {@link TestContextManager}.
+ *
+ * @see org.springframework.test.context.web.ServletTestExecutionListener
+ * @see org.springframework.test.context.support.DependencyInjectionTestExecutionListener
+ * @see org.springframework.test.context.support.DirtiesContextTestExecutionListener
+ * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
+ */
+ Class<? extends TestExecutionListener>[] listeners() default {};
+
+ /**
+ * Alias for {@link #listeners() listeners}.
+ */
+ Class<? extends TestExecutionListener>[] value() default {};
+
+ /**
+ * Whether or not {@link #value() TestExecutionListeners} from superclasses
+ * should be <em>inherited</em>.
+ * <p>
+ * The default value is {@code true}, which means that an annotated
+ * class will <em>inherit</em> the listeners defined by an annotated
+ * superclass. Specifically, the listeners for an annotated class will be
+ * appended to the list of listeners defined by an annotated superclass.
+ * Thus, subclasses have the option of <em>extending</em> the list of
+ * listeners. In the following example, {@code AbstractBaseTest} will
+ * be configured with {@code DependencyInjectionTestExecutionListener}
+ * and {@code DirtiesContextTestExecutionListener}; whereas,
+ * {@code TransactionalTest} will be configured with
+ * {@code DependencyInjectionTestExecutionListener},
+ * {@code DirtiesContextTestExecutionListener}, <strong>and</strong>
+ * {@code TransactionalTestExecutionListener}, in that order.
+ *
+ * <pre class="code">
+ * &#064;TestExecutionListeners({
+ * DependencyInjectionTestExecutionListener.class,
+ * DirtiesContextTestExecutionListener.class
+ * })
+ * public abstract class AbstractBaseTest {
+ * // ...
+ * }
+ *
+ * &#064;TestExecutionListeners(TransactionalTestExecutionListener.class)
+ * public class TransactionalTest extends AbstractBaseTest {
+ * // ...
+ * }</pre>
+ *
+ * <p>
+ * If {@code inheritListeners} is set to {@code false}, the listeners for the
+ * annotated class will <em>shadow</em> and effectively replace any listeners
+ * defined by a superclass.
+ */
+ boolean inheritListeners() default true;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java
new file mode 100644
index 00000000..732c1e0b
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java
@@ -0,0 +1,399 @@
+/*
+ * 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.context.junit38;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.test.annotation.ExpectedException;
+import org.springframework.test.annotation.IfProfileValue;
+import org.springframework.test.annotation.ProfileValueSource;
+import org.springframework.test.annotation.ProfileValueUtils;
+import org.springframework.test.annotation.Repeat;
+import org.springframework.test.annotation.Timed;
+import org.springframework.test.context.TestContextManager;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
+import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
+
+/**
+ * <p>
+ * Abstract base {@link TestCase} which integrates the <i>Spring TestContext
+ * Framework</i> and explicit {@link ApplicationContext} testing support in a
+ * <strong>JUnit 3.8</strong> environment.
+ * </p>
+ * <p>
+ * Concrete subclasses:
+ * </p>
+ * <ul>
+ * <li>Typically declare a class-level
+ * {@link org.springframework.test.context.ContextConfiguration
+ * &#064;ContextConfiguration} annotation to configure the
+ * {@link ApplicationContext application context}
+ * {@link org.springframework.test.context.ContextConfiguration#locations()
+ * resource locations}. <i>If your test does not need to load an application
+ * context, you may choose to omit the
+ * {@link org.springframework.test.context.ContextConfiguration
+ * &#064;ContextConfiguration} declaration and configure the appropriate
+ * {@link org.springframework.test.context.TestExecutionListener
+ * TestExecutionListeners} manually.</i></li>
+ * <li>Must declare public constructors which match the signatures of
+ * {@link #AbstractJUnit38SpringContextTests()
+ * AbstractJUnit38SpringContextTests()} and
+ * {@link #AbstractJUnit38SpringContextTests(String)
+ * AbstractJUnit38SpringContextTests(String)} and delegate to
+ * {@code super();} and {@code super(name);} respectively.</li>
+ * </ul>
+ * <p>
+ * The following list constitutes all annotations currently supported directly
+ * by {@code AbstractJUnit38SpringContextTests}. <i>(Note that additional
+ * annotations may be supported by various
+ * {@link org.springframework.test.context.TestExecutionListener
+ * TestExecutionListeners})</i>
+ * </p>
+ * <ul>
+ * <li>{@link org.springframework.test.annotation.DirtiesContext
+ * &#064;DirtiesContext} (via the configured
+ * {@link DirtiesContextTestExecutionListener}; only supported on methods for
+ * JUnit 3.8)</li>
+ * <li>
+ * {@link org.springframework.test.annotation.ProfileValueSourceConfiguration
+ * &#064;ProfileValueSourceConfiguration}</li>
+ * <li>{@link IfProfileValue &#064;IfProfileValue}</li>
+ * <li>{@link ExpectedException &#064;ExpectedException}</li>
+ * <li>{@link Timed &#064;Timed}</li>
+ * <li>{@link Repeat &#064;Repeat}</li>
+ * </ul>
+ * <p>
+ * JUnit 3.8 does not support <i>before class</i> or <i>after class</i>
+ * lifecycle callbacks. The following
+ * {@link org.springframework.test.context.TestExecutionListener
+ * TestExecutionListener} methods are therefore unsupported in a JUnit 3.8
+ * environment:
+ * <ul>
+ * <li>
+ * {@link org.springframework.test.context.TestExecutionListener#beforeTestClass(org.springframework.test.context.TestContext)
+ * beforeTestClass()}</li>
+ * <li>
+ * {@link org.springframework.test.context.TestExecutionListener#afterTestClass(org.springframework.test.context.TestContext)
+ * afterTestClass()}</li>
+ * </ul>
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see org.springframework.test.context.TestContext
+ * @see org.springframework.test.context.TestContextManager
+ * @see org.springframework.test.context.TestExecutionListeners
+ * @see AbstractTransactionalJUnit38SpringContextTests
+ * @see org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests
+ * @see org.springframework.test.context.testng.AbstractTestNGSpringContextTests
+ * @deprecated as of Spring 3.1, in favor of using
+ * {@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests AbstractJUnit4SpringContextTests}
+ */
+@Deprecated
+@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class })
+public abstract class AbstractJUnit38SpringContextTests extends TestCase implements ApplicationContextAware {
+
+ private static int disabledTestCount = 0;
+
+
+ /**
+ * Return the number of tests disabled in this environment.
+ */
+ public static int getDisabledTestCount() {
+ return disabledTestCount;
+ }
+
+
+ /**
+ * Logger available to subclasses.
+ */
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ /**
+ * The {@link ApplicationContext} that was injected into this test instance
+ * via {@link #setApplicationContext(ApplicationContext)}.
+ */
+ protected ApplicationContext applicationContext;
+
+ /**
+ * {@link ProfileValueSource} available to subclasses but primarily intended
+ * for internal use to provide support for {@link IfProfileValue
+ * &#064;IfProfileValue}.
+ */
+ protected final ProfileValueSource profileValueSource;
+
+ private final TestContextManager testContextManager;
+
+
+ /**
+ * Constructs a new AbstractJUnit38SpringContextTests instance; initializes
+ * the internal {@link TestContextManager} for the current test; and
+ * retrieves the configured (or default) {@link ProfileValueSource}.
+ */
+ public AbstractJUnit38SpringContextTests() {
+ super();
+ this.testContextManager = new TestContextManager(getClass());
+ this.profileValueSource = ProfileValueUtils.retrieveProfileValueSource(getClass());
+ }
+
+ /**
+ * Constructs a new AbstractJUnit38SpringContextTests instance with the
+ * supplied {@code name}; initializes the internal
+ * {@link TestContextManager} for the current test; and retrieves the
+ * configured (or default) {@link ProfileValueSource}.
+ *
+ * @param name the name of the current test to execute
+ */
+ public AbstractJUnit38SpringContextTests(String name) {
+ super(name);
+ this.testContextManager = new TestContextManager(getClass());
+ this.profileValueSource = ProfileValueUtils.retrieveProfileValueSource(getClass());
+ }
+
+ /**
+ * Sets the {@link ApplicationContext} to be used by this test instance,
+ * provided via {@link ApplicationContextAware} semantics.
+ */
+ public final void setApplicationContext(final ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
+ }
+
+ /**
+ * Runs the <em>Spring TestContext Framework</em> test sequence.
+ * <p>
+ * In addition to standard {@link TestCase#runBare()} semantics, this
+ * implementation performs the following:
+ * <ul>
+ * <li>Calls {@link TestContextManager#prepareTestInstance(Object)
+ * prepareTestInstance()},
+ * {@link TestContextManager#beforeTestMethod(Object,Method)
+ * beforeTestMethod()}, and
+ * {@link TestContextManager#afterTestMethod(Object,Method,Throwable)
+ * afterTestMethod()} on this test's {@link TestContextManager} at the
+ * appropriate test execution points.</li>
+ * <li>Provides support for {@link IfProfileValue &#064;IfProfileValue}.</li>
+ * <li>Provides support for {@link Repeat &#064;Repeat}.</li>
+ * <li>Provides support for {@link Timed &#064;Timed}.</li>
+ * <li>Provides support for {@link ExpectedException
+ * &#064;ExpectedException}.</li>
+ * </ul>
+ *
+ * @see ProfileValueUtils#isTestEnabledInThisEnvironment
+ */
+ @Override
+ public void runBare() throws Throwable {
+ this.testContextManager.prepareTestInstance(this);
+ final Method testMethod = getTestMethod();
+
+ if (!ProfileValueUtils.isTestEnabledInThisEnvironment(this.profileValueSource, testMethod, getClass())) {
+ recordDisabled(testMethod);
+ return;
+ }
+
+ runTestTimed(new TestExecutionCallback() {
+
+ public void run() throws Throwable {
+ runManaged(testMethod);
+ }
+ }, testMethod);
+ }
+
+ /**
+ * Get the current test method.
+ */
+ private Method getTestMethod() {
+ assertNotNull("TestCase.getName() cannot be null", getName());
+ Method testMethod = null;
+ try {
+ testMethod = getClass().getMethod(getName(), (Class[]) null);
+ }
+ catch (NoSuchMethodException ex) {
+ fail("Method \"" + getName() + "\" not found");
+ }
+ if (!Modifier.isPublic(testMethod.getModifiers())) {
+ fail("Method \"" + getName() + "\" should be public");
+ }
+ return testMethod;
+ }
+
+ /**
+ * Runs a <em>timed</em> test via the supplied {@link TestExecutionCallback}
+ * , providing support for the {@link Timed &#064;Timed} annotation.
+ *
+ * @param tec the test execution callback to run
+ * @param testMethod the actual test method: used to retrieve the
+ * {@code timeout}
+ * @throws Throwable if any exception is thrown
+ * @see Timed
+ * @see #runTest
+ */
+ private void runTestTimed(TestExecutionCallback tec, Method testMethod) throws Throwable {
+ Timed timed = testMethod.getAnnotation(Timed.class);
+ if (timed == null) {
+ runTest(tec, testMethod);
+ }
+ else {
+ long startTime = System.currentTimeMillis();
+ try {
+ runTest(tec, testMethod);
+ }
+ finally {
+ long elapsed = System.currentTimeMillis() - startTime;
+ if (elapsed > timed.millis()) {
+ fail("Took " + elapsed + " ms; limit was " + timed.millis());
+ }
+ }
+ }
+ }
+
+ /**
+ * Runs a test via the supplied {@link TestExecutionCallback}, providing
+ * support for the {@link ExpectedException &#064;ExpectedException} and
+ * {@link Repeat &#064;Repeat} annotations.
+ *
+ * @param tec the test execution callback to run
+ * @param testMethod the actual test method: used to retrieve the
+ * {@link ExpectedException &#064;ExpectedException} and {@link Repeat
+ * &#064;Repeat} annotations
+ * @throws Throwable if any exception is thrown
+ * @see ExpectedException
+ * @see Repeat
+ */
+ private void runTest(TestExecutionCallback tec, Method testMethod) throws Throwable {
+ ExpectedException expectedExceptionAnnotation = testMethod.getAnnotation(ExpectedException.class);
+ boolean exceptionIsExpected = (expectedExceptionAnnotation != null && expectedExceptionAnnotation.value() != null);
+ Class<? extends Throwable> expectedException = (exceptionIsExpected ? expectedExceptionAnnotation.value()
+ : null);
+
+ Repeat repeat = testMethod.getAnnotation(Repeat.class);
+ int runs = ((repeat != null) && (repeat.value() > 1)) ? repeat.value() : 1;
+
+ for (int i = 0; i < runs; i++) {
+ try {
+ if (runs > 1 && this.logger.isInfoEnabled()) {
+ this.logger.info("Repetition " + (i + 1) + " of test " + testMethod.getName());
+ }
+ tec.run();
+ if (exceptionIsExpected) {
+ fail("Expected exception: " + expectedException.getName());
+ }
+ }
+ catch (Throwable ex) {
+ if (!exceptionIsExpected) {
+ throw ex;
+ }
+ if (!expectedException.isAssignableFrom(ex.getClass())) {
+ // Wrap the unexpected throwable with an explicit message.
+ AssertionFailedError assertionError = new AssertionFailedError("Unexpected exception, expected <"
+ + expectedException.getName() + "> but was <" + ex.getClass().getName() + ">");
+ assertionError.initCause(ex);
+ throw assertionError;
+ }
+ }
+ }
+ }
+
+ /**
+ * Calls {@link TestContextManager#beforeTestMethod(Object,Method)} and
+ * {@link TestContextManager#afterTestMethod(Object,Method,Throwable)} at
+ * the appropriate test execution points.
+ *
+ * @param testMethod the test method to run
+ * @throws Throwable if any exception is thrown
+ * @see #runBare()
+ * @see TestCase#runTest()
+ */
+ private void runManaged(Method testMethod) throws Throwable {
+ Throwable exception = null;
+ boolean reachedTest = false;
+
+ try {
+ this.testContextManager.beforeTestMethod(this, testMethod);
+ setUp();
+ reachedTest = true;
+ runTest();
+ }
+ catch (Throwable ex) {
+ exception = ex;
+ }
+ finally {
+ try {
+ if (reachedTest) {
+ tearDown();
+ }
+ }
+ catch (Throwable ex) {
+ if (exception == null) {
+ exception = ex;
+ }
+ }
+ finally {
+ try {
+ this.testContextManager.afterTestMethod(this, testMethod, exception);
+ }
+ catch (Throwable ex) {
+ if (exception == null) {
+ exception = ex;
+ }
+ }
+ }
+ }
+
+ if (exception != null) {
+ if (exception.getCause() instanceof AssertionError) {
+ exception = exception.getCause();
+ }
+ throw exception;
+ }
+ }
+
+ /**
+ * Records the supplied test method as <em>disabled</em> in the current
+ * environment by incrementing the total number of disabled tests and
+ * logging a debug message.
+ *
+ * @param testMethod the test method that is disabled.
+ * @see #getDisabledTestCount()
+ */
+ protected void recordDisabled(Method testMethod) {
+ disabledTestCount++;
+ if (this.logger.isInfoEnabled()) {
+ this.logger.info("**** " + getClass().getName() + "." + getName() + "() is disabled in this environment. "
+ + "Total disabled tests = " + getDisabledTestCount());
+ }
+ }
+
+
+ /**
+ * Private inner class that defines a callback analogous to {@link Runnable}
+ * , just declaring Throwable.
+ */
+ private static interface TestExecutionCallback {
+
+ void run() throws Throwable;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java
new file mode 100644
index 00000000..9b7fd03e
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java
@@ -0,0 +1,158 @@
+/*
+ * 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.context.junit38;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
+import org.springframework.test.jdbc.SimpleJdbcTestUtils;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * <p>
+ * Abstract {@link Transactional transactional} extension of
+ * {@link AbstractJUnit38SpringContextTests} which adds convenience
+ * functionality for JDBC access. Expects a {@link javax.sql.DataSource} bean
+ * and a {@link PlatformTransactionManager} bean to be defined in the Spring
+ * {@link ApplicationContext application context}.
+ * </p>
+ * <p>
+ * This class exposes a {@link SimpleJdbcTemplate} and provides an easy way to
+ * {@link #countRowsInTable(String) count the number of rows in a table} ,
+ * {@link #deleteFromTables(String...) delete from the database} , and
+ * {@link #executeSqlScript(String, boolean) execute SQL scripts} within a
+ * transaction.
+ * </p>
+ * <p>
+ * Concrete subclasses must fulfill the same requirements outlined in
+ * {@link AbstractJUnit38SpringContextTests}.
+ * </p>
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see AbstractJUnit38SpringContextTests
+ * @see org.springframework.test.context.ContextConfiguration
+ * @see org.springframework.test.context.TestExecutionListeners
+ * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
+ * @see org.springframework.test.context.transaction.TransactionConfiguration
+ * @see org.springframework.transaction.annotation.Transactional
+ * @see org.springframework.test.annotation.NotTransactional
+ * @see org.springframework.test.annotation.Rollback
+ * @see org.springframework.test.context.transaction.BeforeTransaction
+ * @see org.springframework.test.context.transaction.AfterTransaction
+ * @see org.springframework.test.jdbc.SimpleJdbcTestUtils
+ * @see org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
+ * @see org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
+ * @deprecated as of Spring 3.1, in favor of using
+ * {@link org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests AbstractTransactionalJUnit4SpringContextTests}
+ */
+@Deprecated
+@TestExecutionListeners(TransactionalTestExecutionListener.class)
+@Transactional
+public abstract class AbstractTransactionalJUnit38SpringContextTests extends AbstractJUnit38SpringContextTests {
+
+ /**
+ * The SimpleJdbcTemplate that this base class manages, available to subclasses.
+ */
+ protected SimpleJdbcTemplate simpleJdbcTemplate;
+
+ private String sqlScriptEncoding;
+
+
+ /**
+ * Constructs a new AbstractTransactionalJUnit38SpringContextTests instance.
+ */
+ public AbstractTransactionalJUnit38SpringContextTests() {
+ super();
+ }
+
+ /**
+ * Constructs a new AbstractTransactionalJUnit38SpringContextTests instance
+ * with the supplied {@code name}.
+ * @param name the name of the current test to execute
+ */
+ public AbstractTransactionalJUnit38SpringContextTests(String name) {
+ super(name);
+ }
+
+
+ /**
+ * Set the DataSource, typically provided via Dependency Injection.
+ * @param dataSource The DataSource to inject
+ */
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
+ }
+
+ /**
+ * Specify the encoding for SQL scripts, if different from the platform encoding.
+ * @see #executeSqlScript
+ */
+ public void setSqlScriptEncoding(String sqlScriptEncoding) {
+ this.sqlScriptEncoding = sqlScriptEncoding;
+ }
+
+
+ /**
+ * Count the rows in the given table.
+ * @param tableName table name to count rows in
+ * @return the number of rows in the table
+ */
+ protected int countRowsInTable(String tableName) {
+ return SimpleJdbcTestUtils.countRowsInTable(this.simpleJdbcTemplate, tableName);
+ }
+
+ /**
+ * Convenience method for deleting all rows from the specified tables.
+ * Use with caution outside of a transaction!
+ * @param names the names of the tables from which to delete
+ * @return the total number of rows deleted from all specified tables
+ */
+ protected int deleteFromTables(String... names) {
+ return SimpleJdbcTestUtils.deleteFromTables(this.simpleJdbcTemplate, names);
+ }
+
+ /**
+ * Execute the given SQL script. Use with caution outside of a transaction!
+ * <p>The script will normally be loaded by classpath. There should be one statement
+ * per line. Any semicolons will be removed. <b>Do not use this method to execute
+ * DDL if you expect rollback.</b>
+ * @param sqlResourcePath the Spring resource path for the SQL script
+ * @param continueOnError whether or not to continue without throwing an
+ * exception in the event of an error
+ * @throws DataAccessException if there is an error executing a statement
+ * and continueOnError was {@code false}
+ */
+ protected void executeSqlScript(String sqlResourcePath, boolean continueOnError)
+ throws DataAccessException {
+
+ Resource resource = this.applicationContext.getResource(sqlResourcePath);
+ SimpleJdbcTestUtils.executeSqlScript(
+ this.simpleJdbcTemplate, new EncodedResource(resource, this.sqlScriptEncoding), continueOnError);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit38/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit38/package-info.java
new file mode 100644
index 00000000..22ead8d5
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit38/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * <p>Support classes for ApplicationContext-based and transactional
+ * tests run with JUnit 3.8 and the <em>Spring TestContext Framework</em>.</p>
+ */
+
+package org.springframework.test.context.junit38;
+
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java
new file mode 100644
index 00000000..05d35d01
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java
@@ -0,0 +1,102 @@
+/*
+ * 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.context.junit4;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.junit.runner.RunWith;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.TestContextManager;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
+import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
+import org.springframework.test.context.web.ServletTestExecutionListener;
+
+/**
+ * Abstract base test class which integrates the <em>Spring TestContext
+ * Framework</em> with explicit {@link ApplicationContext} testing support in a
+ * <strong>JUnit 4.5+</strong> environment.
+ *
+ * <p>Concrete subclasses should typically declare a class-level
+ * {@link ContextConfiguration &#064;ContextConfiguration} annotation to
+ * configure the {@link ApplicationContext application context} {@link
+ * ContextConfiguration#locations() resource locations} or {@link
+ * ContextConfiguration#classes() annotated classes}. <em>If your test does not
+ * need to load an application context, you may choose to omit the {@link
+ * ContextConfiguration &#064;ContextConfiguration} declaration and to configure
+ * the appropriate {@link org.springframework.test.context.TestExecutionListener
+ * TestExecutionListeners} manually.</em>
+ *
+ * <p>The following {@link org.springframework.test.context.TestExecutionListener
+ * TestExecutionListeners} are configured by default:
+ *
+ * <ul>
+ * <li>{@link org.springframework.test.context.web.ServletTestExecutionListener}
+ * <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener}
+ * <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener}
+ * </ul>
+ *
+ * <p>Note: this class serves only as a convenience for extension. If you do not
+ * wish for your test classes to be tied to a Spring-specific class hierarchy,
+ * you may configure your own custom test classes by using
+ * {@link SpringJUnit4ClassRunner}, {@link ContextConfiguration
+ * &#064;ContextConfiguration}, {@link TestExecutionListeners
+ * &#064;TestExecutionListeners}, etc.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ * @see ContextConfiguration
+ * @see TestContext
+ * @see TestContextManager
+ * @see TestExecutionListeners
+ * @see ServletTestExecutionListener
+ * @see DependencyInjectionTestExecutionListener
+ * @see DirtiesContextTestExecutionListener
+ * @see AbstractTransactionalJUnit4SpringContextTests
+ * @see org.springframework.test.context.testng.AbstractTestNGSpringContextTests
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@TestExecutionListeners({ ServletTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
+ DirtiesContextTestExecutionListener.class })
+public abstract class AbstractJUnit4SpringContextTests implements ApplicationContextAware {
+
+ /**
+ * Logger available to subclasses.
+ */
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ /**
+ * The {@link ApplicationContext} that was injected into this test instance
+ * via {@link #setApplicationContext(ApplicationContext)}.
+ */
+ protected ApplicationContext applicationContext;
+
+
+ /**
+ * Set the {@link ApplicationContext} to be used by this test instance,
+ * provided via {@link ApplicationContextAware} semantics.
+ */
+ public final void setApplicationContext(final ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java
new file mode 100644
index 00000000..083b0fae
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java
@@ -0,0 +1,175 @@
+/*
+ * 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.context.junit4;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
+import org.springframework.test.jdbc.JdbcTestUtils;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Abstract {@linkplain Transactional transactional} extension of
+ * {@link AbstractJUnit4SpringContextTests} which adds convenience functionality
+ * for JDBC access. Expects a {@link DataSource} bean and a
+ * {@link PlatformTransactionManager} bean to be defined in the Spring
+ * {@linkplain ApplicationContext application context}.
+ *
+ * <p>This class exposes a {@link JdbcTemplate} and provides an easy way to
+ * {@linkplain #countRowsInTable count the number of rows in a table}
+ * (potentially {@linkplain #countRowsInTableWhere with a WHERE clause}),
+ * {@linkplain #deleteFromTables delete from tables},
+ * {@linkplain #dropTables drop tables}, and
+ * {@linkplain #executeSqlScript execute SQL scripts} within a transaction.
+ *
+ * <p>Concrete subclasses must fulfill the same requirements outlined in
+ * {@link AbstractJUnit4SpringContextTests}.
+ *
+ * <p>Note: this class serves only as a convenience for extension. If you do not
+ * wish for your test classes to be tied to a Spring-specific class hierarchy,
+ * you may configure your own custom test classes by using
+ * {@link SpringJUnit4ClassRunner}, {@link ContextConfiguration
+ * &#064;ContextConfiguration}, {@link TestExecutionListeners
+ * &#064;TestExecutionListeners}, {@link Transactional &#064;Transactional},
+ * etc.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see AbstractJUnit4SpringContextTests
+ * @see org.springframework.test.context.ContextConfiguration
+ * @see org.springframework.test.context.TestExecutionListeners
+ * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
+ * @see org.springframework.test.context.transaction.TransactionConfiguration
+ * @see org.springframework.transaction.annotation.Transactional
+ * @see org.springframework.test.annotation.NotTransactional
+ * @see org.springframework.test.annotation.Rollback
+ * @see org.springframework.test.context.transaction.BeforeTransaction
+ * @see org.springframework.test.context.transaction.AfterTransaction
+ * @see org.springframework.test.jdbc.JdbcTestUtils
+ * @see org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
+ */
+@TestExecutionListeners(TransactionalTestExecutionListener.class)
+@Transactional
+@SuppressWarnings("deprecation")
+public abstract class AbstractTransactionalJUnit4SpringContextTests extends AbstractJUnit4SpringContextTests {
+
+ /**
+ * The {@code SimpleJdbcTemplate} that this base class manages, available to subclasses.
+ * @deprecated As of Spring 3.2, use {@link #jdbcTemplate} instead.
+ */
+ @Deprecated
+ protected SimpleJdbcTemplate simpleJdbcTemplate;
+
+ /**
+ * The {@code JdbcTemplate} that this base class manages, available to subclasses.
+ * @since 3.2
+ */
+ protected JdbcTemplate jdbcTemplate;
+
+ private String sqlScriptEncoding;
+
+
+ /**
+ * Set the {@code DataSource}, typically provided via Dependency Injection.
+ * <p>This method also instantiates the {@link #simpleJdbcTemplate} and
+ * {@link #jdbcTemplate} instance variables.
+ */
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ /**
+ * Specify the encoding for SQL scripts, if different from the platform encoding.
+ * @see #executeSqlScript
+ */
+ public void setSqlScriptEncoding(String sqlScriptEncoding) {
+ this.sqlScriptEncoding = sqlScriptEncoding;
+ }
+
+ /**
+ * Count the rows in the given table.
+ * @param tableName table name to count rows in
+ * @return the number of rows in the table
+ */
+ protected int countRowsInTable(String tableName) {
+ return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
+ }
+
+ /**
+ * Count the rows in the given table, using the provided {@code WHERE} clause.
+ * <p>See the Javadoc for {@link JdbcTestUtils#countRowsInTableWhere} for details.
+ * @param tableName the name of the table to count rows in
+ * @param whereClause the {@code WHERE} clause to append to the query
+ * @return the number of rows in the table that match the provided
+ * {@code WHERE} clause
+ * @since 3.2
+ */
+ protected int countRowsInTableWhere(String tableName, String whereClause) {
+ return JdbcTestUtils.countRowsInTableWhere(this.jdbcTemplate, tableName, whereClause);
+ }
+
+ /**
+ * Convenience method for deleting all rows from the specified tables. Use
+ * with caution outside of a transaction!
+ * @param names the names of the tables from which to delete
+ * @return the total number of rows deleted from all specified tables
+ */
+ protected int deleteFromTables(String... names) {
+ return JdbcTestUtils.deleteFromTables(this.jdbcTemplate, names);
+ }
+
+ /**
+ * Convenience method for dropping all of the specified tables. Use
+ * with caution outside of a transaction!
+ * @param names the names of the tables to drop
+ * @since 3.2
+ */
+ protected void dropTables(String... names) {
+ JdbcTestUtils.dropTables(this.jdbcTemplate, names);
+ }
+
+ /**
+ * Execute the given SQL script. Use with caution outside of a transaction!
+ * <p>The script will normally be loaded by classpath. There should be one
+ * statement per line. Any semicolons will be removed. <b>Do not use this
+ * method to execute DDL if you expect rollback.</b>
+ * @param sqlResourcePath the Spring resource path for the SQL script
+ * @param continueOnError whether or not to continue without throwing an
+ * exception in the event of an error
+ * @throws DataAccessException if there is an error executing a statement
+ * and continueOnError was {@code false}
+ */
+ protected void executeSqlScript(String sqlResourcePath, boolean continueOnError) throws DataAccessException {
+ Resource resource = this.applicationContext.getResource(sqlResourcePath);
+ JdbcTestUtils.executeSqlScript(this.jdbcTemplate, new EncodedResource(resource,
+ this.sqlScriptEncoding), continueOnError);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java
new file mode 100644
index 00000000..5496b4ee
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java
@@ -0,0 +1,474 @@
+/*
+ * 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.context.junit4;
+
+import java.lang.reflect.Method;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.internal.AssumptionViolatedException;
+import org.junit.internal.runners.model.EachTestNotifier;
+import org.junit.internal.runners.model.ReflectiveCallable;
+import org.junit.internal.runners.statements.ExpectException;
+import org.junit.internal.runners.statements.Fail;
+import org.junit.internal.runners.statements.FailOnTimeout;
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+import org.springframework.test.annotation.ExpectedException;
+import org.springframework.test.annotation.ProfileValueUtils;
+import org.springframework.test.annotation.Repeat;
+import org.springframework.test.annotation.Timed;
+import org.springframework.test.context.TestContextManager;
+import org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks;
+import org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks;
+import org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks;
+import org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks;
+import org.springframework.test.context.junit4.statements.SpringFailOnTimeout;
+import org.springframework.test.context.junit4.statements.SpringRepeat;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * <p>
+ * {@code SpringJUnit4ClassRunner} is a custom extension of
+ * {@link BlockJUnit4ClassRunner} which provides functionality of the
+ * <em>Spring TestContext Framework</em> to standard JUnit 4.5+ tests by means
+ * of the {@link TestContextManager} and associated support classes and
+ * annotations.
+ * </p>
+ * <p>
+ * The following list constitutes all annotations currently supported directly
+ * by {@code SpringJUnit4ClassRunner}.
+ * <em>(Note that additional annotations may be supported by various
+ * {@link org.springframework.test.context.TestExecutionListener
+ * TestExecutionListeners})</em>
+ * </p>
+ * <ul>
+ * <li>{@link Test#expected() &#064;Test(expected=...)}</li>
+ * <li>{@link ExpectedException &#064;ExpectedException}</li>
+ * <li>{@link Test#timeout() &#064;Test(timeout=...)}</li>
+ * <li>{@link Timed &#064;Timed}</li>
+ * <li>{@link Repeat &#064;Repeat}</li>
+ * <li>{@link Ignore &#064;Ignore}</li>
+ * <li>
+ * {@link org.springframework.test.annotation.ProfileValueSourceConfiguration
+ * &#064;ProfileValueSourceConfiguration}</li>
+ * <li>{@link org.springframework.test.annotation.IfProfileValue
+ * &#064;IfProfileValue}</li>
+ * </ul>
+ * <p>
+ * <b>NOTE:</b> As of Spring 3.0, {@code SpringJUnit4ClassRunner} requires
+ * JUnit 4.5+.
+ * </p>
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see TestContextManager
+ */
+@SuppressWarnings("deprecation")
+public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner {
+
+ private static final Log logger = LogFactory.getLog(SpringJUnit4ClassRunner.class);
+
+ private final TestContextManager testContextManager;
+
+
+ /**
+ * Constructs a new {@code SpringJUnit4ClassRunner} and initializes a
+ * {@link TestContextManager} to provide Spring testing functionality to
+ * standard JUnit tests.
+ * @param clazz the test class to be run
+ * @see #createTestContextManager(Class)
+ */
+ public SpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
+ super(clazz);
+ if (logger.isDebugEnabled()) {
+ logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "].");
+ }
+ this.testContextManager = createTestContextManager(clazz);
+ }
+
+ /**
+ * Creates a new {@link TestContextManager} for the supplied test class and
+ * the configured <em>default {@code ContextLoader} class name</em>.
+ * Can be overridden by subclasses.
+ * @param clazz the test class to be managed
+ * @see #getDefaultContextLoaderClassName(Class)
+ */
+ protected TestContextManager createTestContextManager(Class<?> clazz) {
+ return new TestContextManager(clazz, getDefaultContextLoaderClassName(clazz));
+ }
+
+ /**
+ * Get the {@link TestContextManager} associated with this runner.
+ */
+ protected final TestContextManager getTestContextManager() {
+ return this.testContextManager;
+ }
+
+ /**
+ * Get the name of the default {@code ContextLoader} class to use for
+ * the supplied test class. The named class will be used if the test class
+ * does not explicitly declare a {@code ContextLoader} class via the
+ * {@code &#064;ContextConfiguration} annotation.
+ * <p>The default implementation returns {@code null}, thus implying use
+ * of the <em>standard</em> default {@code ContextLoader} class name.
+ * Can be overridden by subclasses.
+ * @param clazz the test class
+ * @return {@code null}
+ */
+ protected String getDefaultContextLoaderClassName(Class<?> clazz) {
+ return null;
+ }
+
+ /**
+ * Returns a description suitable for an ignored test class if the test is
+ * disabled via {@code &#064;IfProfileValue} at the class-level, and
+ * otherwise delegates to the parent implementation.
+ * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)
+ */
+ @Override
+ public Description getDescription() {
+ if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
+ return Description.createSuiteDescription(getTestClass().getJavaClass());
+ }
+ return super.getDescription();
+ }
+
+ /**
+ * Check whether the test is enabled in the first place. This prevents
+ * classes with a non-matching {@code &#064;IfProfileValue} annotation
+ * from running altogether, even skipping the execution of
+ * {@code prepareTestInstance()} {@code TestExecutionListener}
+ * methods.
+ * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)
+ * @see org.springframework.test.annotation.IfProfileValue
+ * @see org.springframework.test.context.TestExecutionListener
+ */
+ @Override
+ public void run(RunNotifier notifier) {
+ if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
+ notifier.fireTestIgnored(getDescription());
+ return;
+ }
+ super.run(notifier);
+ }
+
+ /**
+ * Wraps the {@link Statement} returned by the parent implementation with a
+ * {@link RunBeforeTestClassCallbacks} statement, thus preserving the
+ * default functionality but adding support for the Spring TestContext
+ * Framework.
+ * @see RunBeforeTestClassCallbacks
+ */
+ @Override
+ protected Statement withBeforeClasses(Statement statement) {
+ Statement junitBeforeClasses = super.withBeforeClasses(statement);
+ return new RunBeforeTestClassCallbacks(junitBeforeClasses, getTestContextManager());
+ }
+
+ /**
+ * Wraps the {@link Statement} returned by the parent implementation with a
+ * {@link RunAfterTestClassCallbacks} statement, thus preserving the default
+ * functionality but adding support for the Spring TestContext Framework.
+ * @see RunAfterTestClassCallbacks
+ */
+ @Override
+ protected Statement withAfterClasses(Statement statement) {
+ Statement junitAfterClasses = super.withAfterClasses(statement);
+ return new RunAfterTestClassCallbacks(junitAfterClasses, getTestContextManager());
+ }
+
+ /**
+ * Delegates to the parent implementation for creating the test instance and
+ * then allows the {@link #getTestContextManager() TestContextManager} to
+ * prepare the test instance before returning it.
+ * @see TestContextManager#prepareTestInstance(Object)
+ */
+ @Override
+ protected Object createTest() throws Exception {
+ Object testInstance = super.createTest();
+ getTestContextManager().prepareTestInstance(testInstance);
+ return testInstance;
+ }
+
+ /**
+ * Performs the same logic as
+ * {@link BlockJUnit4ClassRunner#runChild(FrameworkMethod, RunNotifier)},
+ * except that tests are determined to be <em>ignored</em> by
+ * {@link #isTestMethodIgnored(FrameworkMethod)}.
+ */
+ @Override
+ protected void runChild(FrameworkMethod frameworkMethod, RunNotifier notifier) {
+ EachTestNotifier eachNotifier = springMakeNotifier(frameworkMethod, notifier);
+ if (isTestMethodIgnored(frameworkMethod)) {
+ eachNotifier.fireTestIgnored();
+ return;
+ }
+
+ eachNotifier.fireTestStarted();
+ try {
+ methodBlock(frameworkMethod).evaluate();
+ }
+ catch (AssumptionViolatedException e) {
+ eachNotifier.addFailedAssumption(e);
+ }
+ catch (Throwable e) {
+ eachNotifier.addFailure(e);
+ }
+ finally {
+ eachNotifier.fireTestFinished();
+ }
+ }
+
+ /**
+ * {@code springMakeNotifier()} is an exact copy of
+ * {@link BlockJUnit4ClassRunner BlockJUnit4ClassRunner's}
+ * {@code makeNotifier()} method, but we have decided to prefix it with
+ * "spring" and keep it {@code private} in order to avoid the
+ * compatibility clashes that were introduced in JUnit between versions 4.5,
+ * 4.6, and 4.7.
+ */
+ private EachTestNotifier springMakeNotifier(FrameworkMethod method, RunNotifier notifier) {
+ Description description = describeChild(method);
+ return new EachTestNotifier(notifier, description);
+ }
+
+ /**
+ * Augments the default JUnit behavior
+ * {@link #withPotentialRepeat(FrameworkMethod, Object, Statement) with
+ * potential repeats} of the entire execution chain.
+ * <p>Furthermore, support for timeouts has been moved down the execution chain
+ * in order to include execution of {@link org.junit.Before &#064;Before}
+ * and {@link org.junit.After &#064;After} methods within the timed
+ * execution. Note that this differs from the default JUnit behavior of
+ * executing {@code &#064;Before} and {@code &#064;After} methods
+ * in the main thread while executing the actual test method in a separate
+ * thread. Thus, the end effect is that {@code &#064;Before} and
+ * {@code &#064;After} methods will be executed in the same thread as
+ * the test method. As a consequence, JUnit-specified timeouts will work
+ * fine in combination with Spring transactions. Note that JUnit-specific
+ * timeouts still differ from Spring-specific timeouts in that the former
+ * execute in a separate thread while the latter simply execute in the main
+ * thread (like regular tests).
+ * @see #possiblyExpectingExceptions(FrameworkMethod, Object, Statement)
+ * @see #withBefores(FrameworkMethod, Object, Statement)
+ * @see #withAfters(FrameworkMethod, Object, Statement)
+ * @see #withPotentialTimeout(FrameworkMethod, Object, Statement)
+ * @see #withPotentialRepeat(FrameworkMethod, Object, Statement)
+ */
+ @Override
+ protected Statement methodBlock(FrameworkMethod frameworkMethod) {
+ Object testInstance;
+ try {
+ testInstance = new ReflectiveCallable() {
+
+ @Override
+ protected Object runReflectiveCall() throws Throwable {
+ return createTest();
+ }
+ }.run();
+ }
+ catch (Throwable ex) {
+ return new Fail(ex);
+ }
+
+ Statement statement = methodInvoker(frameworkMethod, testInstance);
+ statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement);
+ statement = withBefores(frameworkMethod, testInstance, statement);
+ statement = withAfters(frameworkMethod, testInstance, statement);
+ statement = withRulesReflectively(frameworkMethod, testInstance, statement);
+ statement = withPotentialRepeat(frameworkMethod, testInstance, statement);
+ statement = withPotentialTimeout(frameworkMethod, testInstance, statement);
+
+ return statement;
+ }
+
+ /**
+ * Invokes JUnit 4.7's private {@code withRules()} method using
+ * reflection. This is necessary for backwards compatibility with the JUnit
+ * 4.5 and 4.6 implementations of {@link BlockJUnit4ClassRunner}.
+ */
+ private Statement withRulesReflectively(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
+ Method withRulesMethod = ReflectionUtils.findMethod(getClass(), "withRules", FrameworkMethod.class,
+ Object.class, Statement.class);
+ if (withRulesMethod != null) {
+ // Original JUnit 4.7 code:
+ // statement = withRules(frameworkMethod, testInstance, statement);
+ ReflectionUtils.makeAccessible(withRulesMethod);
+ statement = (Statement) ReflectionUtils.invokeMethod(withRulesMethod, this, frameworkMethod, testInstance,
+ statement);
+ }
+ return statement;
+ }
+
+ /**
+ * Returns {@code true} if {@link Ignore &#064;Ignore} is present for
+ * the supplied {@link FrameworkMethod test method} or if the test method is
+ * disabled via {@code &#064;IfProfileValue}.
+ * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Method, Class)
+ */
+ protected boolean isTestMethodIgnored(FrameworkMethod frameworkMethod) {
+ Method method = frameworkMethod.getMethod();
+ return (method.isAnnotationPresent(Ignore.class) || !ProfileValueUtils.isTestEnabledInThisEnvironment(method,
+ getTestClass().getJavaClass()));
+ }
+
+ /**
+ * Performs the same logic as
+ * {@link BlockJUnit4ClassRunner#possiblyExpectingExceptions(FrameworkMethod, Object, Statement)}
+ * except that the <em>expected exception</em> is retrieved using
+ * {@link #getExpectedException(FrameworkMethod)}.
+ */
+ @Override
+ protected Statement possiblyExpectingExceptions(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
+ Class<? extends Throwable> expectedException = getExpectedException(frameworkMethod);
+ return expectedException != null ? new ExpectException(next, expectedException) : next;
+ }
+
+ /**
+ * Get the {@code exception} that the supplied {@link FrameworkMethod
+ * test method} is expected to throw.
+ * <p>Supports both Spring's {@link ExpectedException @ExpectedException(...)}
+ * and JUnit's {@link Test#expected() @Test(expected=...)} annotations, but
+ * not both simultaneously.
+ * @return the expected exception, or {@code null} if none was specified
+ */
+ protected Class<? extends Throwable> getExpectedException(FrameworkMethod frameworkMethod) {
+ Test testAnnotation = frameworkMethod.getAnnotation(Test.class);
+ Class<? extends Throwable> junitExpectedException = (testAnnotation != null
+ && testAnnotation.expected() != Test.None.class ? testAnnotation.expected() : null);
+
+ ExpectedException expectedExAnn = frameworkMethod.getAnnotation(ExpectedException.class);
+ Class<? extends Throwable> springExpectedException = (expectedExAnn != null ? expectedExAnn.value() : null);
+
+ if (springExpectedException != null && junitExpectedException != null) {
+ String msg = "Test method [" + frameworkMethod.getMethod()
+ + "] has been configured with Spring's @ExpectedException(" + springExpectedException.getName()
+ + ".class) and JUnit's @Test(expected=" + junitExpectedException.getName()
+ + ".class) annotations. "
+ + "Only one declaration of an 'expected exception' is permitted per test method.";
+ logger.error(msg);
+ throw new IllegalStateException(msg);
+ }
+
+ return springExpectedException != null ? springExpectedException : junitExpectedException;
+ }
+
+ /**
+ * Supports both Spring's {@link Timed &#064;Timed} and JUnit's
+ * {@link Test#timeout() &#064;Test(timeout=...)} annotations, but not both
+ * simultaneously. Returns either a {@link SpringFailOnTimeout}, a
+ * {@link FailOnTimeout}, or the unmodified, supplied {@link Statement} as
+ * appropriate.
+ * @see #getSpringTimeout(FrameworkMethod)
+ * @see #getJUnitTimeout(FrameworkMethod)
+ */
+ @Override
+ protected Statement withPotentialTimeout(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
+ Statement statement = null;
+ long springTimeout = getSpringTimeout(frameworkMethod);
+ long junitTimeout = getJUnitTimeout(frameworkMethod);
+ if (springTimeout > 0 && junitTimeout > 0) {
+ String msg = "Test method [" + frameworkMethod.getMethod()
+ + "] has been configured with Spring's @Timed(millis=" + springTimeout
+ + ") and JUnit's @Test(timeout=" + junitTimeout
+ + ") annotations. Only one declaration of a 'timeout' is permitted per test method.";
+ logger.error(msg);
+ throw new IllegalStateException(msg);
+ }
+ else if (springTimeout > 0) {
+ statement = new SpringFailOnTimeout(next, springTimeout);
+ }
+ else if (junitTimeout > 0) {
+ statement = new FailOnTimeout(next, junitTimeout);
+ }
+ else {
+ statement = next;
+ }
+
+ return statement;
+ }
+
+ /**
+ * Retrieves the configured JUnit {@code timeout} from the {@link Test
+ * &#064;Test} annotation on the supplied {@link FrameworkMethod test method}.
+ * @return the timeout, or {@code 0} if none was specified.
+ */
+ protected long getJUnitTimeout(FrameworkMethod frameworkMethod) {
+ Test testAnnotation = frameworkMethod.getAnnotation(Test.class);
+ return (testAnnotation != null && testAnnotation.timeout() > 0 ? testAnnotation.timeout() : 0);
+ }
+
+ /**
+ * Retrieves the configured Spring-specific {@code timeout} from the
+ * {@link Timed &#064;Timed} annotation on the supplied
+ * {@link FrameworkMethod test method}.
+ * @return the timeout, or {@code 0} if none was specified.
+ */
+ protected long getSpringTimeout(FrameworkMethod frameworkMethod) {
+ Timed timedAnnotation = frameworkMethod.getAnnotation(Timed.class);
+ return (timedAnnotation != null && timedAnnotation.millis() > 0 ? timedAnnotation.millis() : 0);
+ }
+
+ /**
+ * Wraps the {@link Statement} returned by the parent implementation with a
+ * {@link RunBeforeTestMethodCallbacks} statement, thus preserving the
+ * default functionality but adding support for the Spring TestContext
+ * Framework.
+ * @see RunBeforeTestMethodCallbacks
+ */
+ @Override
+ protected Statement withBefores(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
+ Statement junitBefores = super.withBefores(frameworkMethod, testInstance, statement);
+ return new RunBeforeTestMethodCallbacks(junitBefores, testInstance, frameworkMethod.getMethod(),
+ getTestContextManager());
+ }
+
+ /**
+ * Wraps the {@link Statement} returned by the parent implementation with a
+ * {@link RunAfterTestMethodCallbacks} statement, thus preserving the
+ * default functionality but adding support for the Spring TestContext
+ * Framework.
+ * @see RunAfterTestMethodCallbacks
+ */
+ @Override
+ protected Statement withAfters(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
+ Statement junitAfters = super.withAfters(frameworkMethod, testInstance, statement);
+ return new RunAfterTestMethodCallbacks(junitAfters, testInstance, frameworkMethod.getMethod(),
+ getTestContextManager());
+ }
+
+ /**
+ * Supports Spring's {@link Repeat &#064;Repeat} annotation by returning a
+ * {@link SpringRepeat} statement initialized with the configured repeat
+ * count or {@code 1} if no repeat count is configured.
+ * @see SpringRepeat
+ */
+ protected Statement withPotentialRepeat(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
+ Repeat repeatAnnotation = frameworkMethod.getAnnotation(Repeat.class);
+ int repeat = (repeatAnnotation != null ? repeatAnnotation.value() : 1);
+ return new SpringRepeat(next, frameworkMethod.getMethod(), repeat);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java
new file mode 100644
index 00000000..64f30c8f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * <p>Support classes for ApplicationContext-based and transactional
+ * tests run with JUnit 4.5+ and the <em>Spring TestContext Framework</em>.</p>
+ */
+
+package org.springframework.test.context.junit4;
+
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java
new file mode 100644
index 00000000..c3d6c8f5
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java
@@ -0,0 +1,92 @@
+/*
+ * 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.context.junit4.statements;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.internal.runners.model.MultipleFailureException;
+import org.junit.runners.model.Statement;
+import org.springframework.test.context.TestContextManager;
+
+/**
+ * {@code RunAfterTestClassCallbacks} is a custom JUnit 4.5+
+ * {@link Statement} which allows the <em>Spring TestContext Framework</em> to
+ * be plugged into the JUnit execution chain by calling
+ * {@link TestContextManager#afterTestClass() afterTestClass()} on the supplied
+ * {@link TestContextManager}.
+ *
+ * @see #evaluate()
+ * @see RunBeforeTestMethodCallbacks
+ * @author Sam Brannen
+ * @since 3.0
+ */
+@SuppressWarnings("deprecation")
+public class RunAfterTestClassCallbacks extends Statement {
+
+ private final Statement next;
+
+ private final TestContextManager testContextManager;
+
+
+ /**
+ * Constructs a new {@code RunAfterTestClassCallbacks} statement.
+ *
+ * @param next the next {@code Statement} in the execution chain
+ * @param testContextManager the TestContextManager upon which to call
+ * {@code afterTestClass()}
+ */
+ public RunAfterTestClassCallbacks(Statement next, TestContextManager testContextManager) {
+ this.next = next;
+ this.testContextManager = testContextManager;
+ }
+
+ /**
+ * Invokes the next {@link Statement} in the execution chain (typically an
+ * instance of {@link org.junit.internal.runners.statements.RunAfters
+ * RunAfters}), catching any exceptions thrown, and then calls
+ * {@link TestContextManager#afterTestClass()}. If the call to
+ * {@code afterTestClass()} throws an exception, it will also be
+ * tracked. Multiple exceptions will be combined into a
+ * {@link MultipleFailureException}.
+ */
+ @Override
+ public void evaluate() throws Throwable {
+ List<Throwable> errors = new ArrayList<Throwable>();
+ try {
+ this.next.evaluate();
+ }
+ catch (Throwable e) {
+ errors.add(e);
+ }
+
+ try {
+ this.testContextManager.afterTestClass();
+ }
+ catch (Exception e) {
+ errors.add(e);
+ }
+
+ if (errors.isEmpty()) {
+ return;
+ }
+ if (errors.size() == 1) {
+ throw errors.get(0);
+ }
+ throw new MultipleFailureException(errors);
+ }
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java
new file mode 100644
index 00000000..0fa60566
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java
@@ -0,0 +1,105 @@
+/*
+ * 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.context.junit4.statements;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.internal.runners.model.MultipleFailureException;
+import org.junit.runners.model.Statement;
+import org.springframework.test.context.TestContextManager;
+
+/**
+ * {@code RunAfterTestMethodCallbacks} is a custom JUnit 4.5+
+ * {@link Statement} which allows the <em>Spring TestContext Framework</em> to
+ * be plugged into the JUnit execution chain by calling
+ * {@link TestContextManager#afterTestMethod(Object, Method, Throwable) afterTestMethod()}
+ * on the supplied {@link TestContextManager}.
+ *
+ * @see #evaluate()
+ * @see RunBeforeTestMethodCallbacks
+ * @author Sam Brannen
+ * @since 3.0
+ */
+@SuppressWarnings("deprecation")
+public class RunAfterTestMethodCallbacks extends Statement {
+
+ private final Statement next;
+
+ private final Object testInstance;
+
+ private final Method testMethod;
+
+ private final TestContextManager testContextManager;
+
+
+ /**
+ * Constructs a new {@code RunAfterTestMethodCallbacks} statement.
+ *
+ * @param next the next {@code Statement} in the execution chain
+ * @param testInstance the current test instance (never {@code null})
+ * @param testMethod the test method which has just been executed on the
+ * test instance
+ * @param testContextManager the TestContextManager upon which to call
+ * {@code afterTestMethod()}
+ */
+ public RunAfterTestMethodCallbacks(Statement next, Object testInstance, Method testMethod,
+ TestContextManager testContextManager) {
+ this.next = next;
+ this.testInstance = testInstance;
+ this.testMethod = testMethod;
+ this.testContextManager = testContextManager;
+ }
+
+ /**
+ * Invokes the next {@link Statement} in the execution chain (typically an
+ * instance of {@link org.junit.internal.runners.statements.RunAfters
+ * RunAfters}), catching any exceptions thrown, and then calls
+ * {@link TestContextManager#afterTestMethod(Object, Method, Throwable)} with the first
+ * caught exception (if any). If the call to {@code afterTestMethod()}
+ * throws an exception, it will also be tracked. Multiple exceptions will be
+ * combined into a {@link MultipleFailureException}.
+ */
+ @Override
+ public void evaluate() throws Throwable {
+ Throwable testException = null;
+ List<Throwable> errors = new ArrayList<Throwable>();
+ try {
+ this.next.evaluate();
+ }
+ catch (Throwable e) {
+ testException = e;
+ errors.add(e);
+ }
+
+ try {
+ this.testContextManager.afterTestMethod(this.testInstance, this.testMethod, testException);
+ }
+ catch (Exception e) {
+ errors.add(e);
+ }
+
+ if (errors.isEmpty()) {
+ return;
+ }
+ if (errors.size() == 1) {
+ throw errors.get(0);
+ }
+ throw new MultipleFailureException(errors);
+ }
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java
new file mode 100644
index 00000000..16c6f97d
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java
@@ -0,0 +1,64 @@
+/*
+ * 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.context.junit4.statements;
+
+import org.junit.runners.model.Statement;
+import org.springframework.test.context.TestContextManager;
+
+/**
+ * {@code RunBeforeTestClassCallbacks} is a custom JUnit 4.5+
+ * {@link Statement} which allows the <em>Spring TestContext Framework</em> to
+ * be plugged into the JUnit execution chain by calling
+ * {@link TestContextManager#beforeTestClass() beforeTestClass()} on the
+ * supplied {@link TestContextManager}.
+ *
+ * @see #evaluate()
+ * @see RunAfterTestMethodCallbacks
+ * @author Sam Brannen
+ * @since 3.0
+ */
+public class RunBeforeTestClassCallbacks extends Statement {
+
+ private final Statement next;
+
+ private final TestContextManager testContextManager;
+
+
+ /**
+ * Constructs a new {@code RunBeforeTestClassCallbacks} statement.
+ *
+ * @param next the next {@code Statement} in the execution chain
+ * @param testContextManager the TestContextManager upon which to call
+ * {@code beforeTestClass()}
+ */
+ public RunBeforeTestClassCallbacks(Statement next, TestContextManager testContextManager) {
+ this.next = next;
+ this.testContextManager = testContextManager;
+ }
+
+ /**
+ * Calls {@link TestContextManager#beforeTestClass()} and then invokes the
+ * next {@link Statement} in the execution chain (typically an instance of
+ * {@link org.junit.internal.runners.statements.RunBefores RunBefores}).
+ */
+ @Override
+ public void evaluate() throws Throwable {
+ this.testContextManager.beforeTestClass();
+ this.next.evaluate();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java
new file mode 100644
index 00000000..92d32ea5
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java
@@ -0,0 +1,77 @@
+/*
+ * 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.context.junit4.statements;
+
+import java.lang.reflect.Method;
+
+import org.junit.runners.model.Statement;
+import org.springframework.test.context.TestContextManager;
+
+/**
+ * {@code RunBeforeTestMethodCallbacks} is a custom JUnit 4.5+
+ * {@link Statement} which allows the <em>Spring TestContext Framework</em> to
+ * be plugged into the JUnit execution chain by calling
+ * {@link TestContextManager#beforeTestMethod(Object, Method)
+ * beforeTestMethod()} on the supplied {@link TestContextManager}.
+ *
+ * @see #evaluate()
+ * @see RunAfterTestMethodCallbacks
+ * @author Sam Brannen
+ * @since 3.0
+ */
+public class RunBeforeTestMethodCallbacks extends Statement {
+
+ private final Statement next;
+
+ private final Object testInstance;
+
+ private final Method testMethod;
+
+ private final TestContextManager testContextManager;
+
+
+ /**
+ * Constructs a new {@code RunBeforeTestMethodCallbacks} statement.
+ *
+ * @param next the next {@code Statement} in the execution chain
+ * @param testInstance the current test instance (never {@code null})
+ * @param testMethod the test method which is about to be executed on the
+ * test instance
+ * @param testContextManager the TestContextManager upon which to call
+ * {@code beforeTestMethod()}
+ */
+ public RunBeforeTestMethodCallbacks(Statement next, Object testInstance, Method testMethod,
+ TestContextManager testContextManager) {
+ this.next = next;
+ this.testInstance = testInstance;
+ this.testMethod = testMethod;
+ this.testContextManager = testContextManager;
+ }
+
+ /**
+ * Calls {@link TestContextManager#beforeTestMethod(Object, Method)} and
+ * then invokes the next {@link Statement} in the execution chain (typically
+ * an instance of {@link org.junit.internal.runners.statements.RunBefores
+ * RunBefores}).
+ */
+ @Override
+ public void evaluate() throws Throwable {
+ this.testContextManager.beforeTestMethod(this.testInstance, this.testMethod);
+ this.next.evaluate();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java
new file mode 100644
index 00000000..093ae49b
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java
@@ -0,0 +1,76 @@
+/*
+ * 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.context.junit4.statements;
+
+import java.util.concurrent.TimeoutException;
+
+import org.junit.runners.model.Statement;
+import org.springframework.test.annotation.Timed;
+
+/**
+ * {@code SpringFailOnTimeout} is a custom JUnit 4.5+ {@link Statement}
+ * which adds support for Spring's {@link Timed @Timed} annotation by throwing
+ * an exception if the next statement in the execution chain takes more than the
+ * specified number of milliseconds.
+ *
+ * @see #evaluate()
+ * @author Sam Brannen
+ * @since 3.0
+ */
+public class SpringFailOnTimeout extends Statement {
+
+ private final Statement next;
+
+ private final long timeout;
+
+
+ /**
+ * Constructs a new {@code SpringFailOnTimeout} statement.
+ *
+ * @param next the next {@code Statement} in the execution chain
+ * @param timeout the configured {@code timeout} for the current test
+ * @see Timed#millis()
+ */
+ public SpringFailOnTimeout(Statement next, long timeout) {
+ this.next = next;
+ this.timeout = timeout;
+ }
+
+ /**
+ * Invokes the next {@link Statement statement} in the execution chain
+ * (typically an instance of
+ * {@link org.junit.internal.runners.statements.InvokeMethod InvokeMethod}
+ * or {@link org.junit.internal.runners.statements.ExpectException
+ * ExpectException}) and throws an exception if the next
+ * {@code statement} takes more than the specified {@code timeout}
+ * .
+ */
+ @Override
+ public void evaluate() throws Throwable {
+ long startTime = System.currentTimeMillis();
+ try {
+ this.next.evaluate();
+ }
+ finally {
+ long elapsed = System.currentTimeMillis() - startTime;
+ if (elapsed > this.timeout) {
+ throw new TimeoutException(String.format("Test took %s ms; limit was %s ms.", elapsed, this.timeout));
+ }
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java
new file mode 100644
index 00000000..0aa21888
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java
@@ -0,0 +1,76 @@
+/*
+ * 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.context.junit4.statements;
+
+import java.lang.reflect.Method;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.runners.model.Statement;
+import org.springframework.test.annotation.Repeat;
+import org.springframework.util.ClassUtils;
+
+/**
+ * {@code SpringRepeat} is a custom JUnit 4.5+ {@link Statement} which adds
+ * support for Spring's {@link Repeat &#064;Repeat} annotation by repeating the
+ * test for the specified number of times.
+ *
+ * @see #evaluate()
+ * @author Sam Brannen
+ * @since 3.0
+ */
+public class SpringRepeat extends Statement {
+
+ protected static final Log logger = LogFactory.getLog(SpringRepeat.class);
+
+ private final Statement next;
+
+ private final Method testMethod;
+
+ private final int repeat;
+
+
+ /**
+ * Constructs a new {@code SpringRepeat} statement.
+ *
+ * @param next the next {@code Statement} in the execution chain
+ * @param testMethod the current test method
+ * @param repeat the configured repeat count for the current test method
+ * @see Repeat#value()
+ */
+ public SpringRepeat(Statement next, Method testMethod, int repeat) {
+ this.next = next;
+ this.testMethod = testMethod;
+ this.repeat = Math.max(1, repeat);
+ }
+
+ /**
+ * Invokes the next {@link Statement statement} in the execution chain for
+ * the specified repeat count.
+ */
+ @Override
+ public void evaluate() throws Throwable {
+ for (int i = 0; i < this.repeat; i++) {
+ if (this.repeat > 1 && logger.isInfoEnabled()) {
+ logger.info(String.format("Repetition %d of test %s#%s()", (i + 1),
+ ClassUtils.getShortName(this.testMethod.getDeclaringClass()), this.testMethod.getName()));
+ }
+ this.next.evaluate();
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java
new file mode 100644
index 00000000..116e5fe1
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java
@@ -0,0 +1,7 @@
+/**
+ *
+ * <p>JUnit 4.5 based {@code statements} used in the <em>Spring TestContext Framework</em>.</p>
+ *
+ */
+package org.springframework.test.context.junit4.statements;
+
diff --git a/spring-test/src/main/java/org/springframework/test/context/package-info.java b/spring-test/src/main/java/org/springframework/test/context/package-info.java
new file mode 100644
index 00000000..b02e0b44
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/package-info.java
@@ -0,0 +1,16 @@
+/**
+ * <p>This package contains the <em>Spring TestContext Framework</em> which
+ * provides annotation-driven unit and integration testing support that is
+ * agnostic of the actual testing framework in use. The same techniques and
+ * annotation-based configuration used in, for example, a JUnit 4.5+ environment
+ * can also be applied to tests written with TestNG, etc.
+ *
+ * <p>In addition to providing generic and extensible testing infrastructure,
+ * the Spring TestContext Framework provides out-of-the-box support for
+ * Spring-specific integration testing functionality such as context management
+ * and caching, dependency injection of test fixtures, and transactional test
+ * management with default rollback semantics.
+ */
+
+package org.springframework.test.context;
+
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java
new file mode 100644
index 00000000..8b497ccc
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2002-2013 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.context.support;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.GenericTypeResolver;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.support.ResourcePatternUtils;
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.ContextLoader;
+import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.test.context.SmartContextLoader;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.ResourceUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Abstract application context loader that provides a basis for all concrete
+ * implementations of the {@link ContextLoader} SPI. Provides a
+ * <em>Template Method</em> based approach for {@link #processLocations processing}
+ * resource locations.
+ *
+ * <p>As of Spring 3.1, {@code AbstractContextLoader} also provides a basis
+ * for all concrete implementations of the {@link SmartContextLoader} SPI. For
+ * backwards compatibility with the {@code ContextLoader} SPI,
+ * {@link #processContextConfiguration(ContextConfigurationAttributes)} delegates
+ * to {@link #processLocations(Class, String...)}.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see #generateDefaultLocations
+ * @see #modifyLocations
+ */
+public abstract class AbstractContextLoader implements SmartContextLoader {
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private static final String SLASH = "/";
+
+ private static final Log logger = LogFactory.getLog(AbstractContextLoader.class);
+
+
+ // --- SmartContextLoader -----------------------------------------------
+
+ /**
+ * For backwards compatibility with the {@link ContextLoader} SPI, the
+ * default implementation simply delegates to {@link #processLocations(Class, String...)},
+ * passing it the {@link ContextConfigurationAttributes#getDeclaringClass()
+ * declaring class} and {@link ContextConfigurationAttributes#getLocations()
+ * resource locations} retrieved from the supplied
+ * {@link ContextConfigurationAttributes configuration attributes}. The
+ * processed locations are then
+ * {@link ContextConfigurationAttributes#setLocations(String[]) set} in
+ * the supplied configuration attributes.
+ * <p>Can be overridden in subclasses &mdash; for example, to process
+ * annotated classes instead of resource locations.
+ * @since 3.1
+ * @see #processLocations(Class, String...)
+ */
+ public void processContextConfiguration(ContextConfigurationAttributes configAttributes) {
+ String[] processedLocations = processLocations(configAttributes.getDeclaringClass(), configAttributes.getLocations());
+ configAttributes.setLocations(processedLocations);
+ }
+
+ /**
+ * Prepare the {@link ConfigurableApplicationContext} created by this
+ * {@code SmartContextLoader} <i>before</i> bean definitions are read.
+ * <p>The default implementation:
+ * <ul>
+ * <li>Sets the <em>active bean definition profiles</em> from the supplied
+ * {@code MergedContextConfiguration} in the
+ * {@link org.springframework.core.env.Environment Environment} of the context.</li>
+ * <li>Determines what (if any) context initializer classes have been supplied
+ * via the {@code MergedContextConfiguration} and
+ * {@linkplain ApplicationContextInitializer#initialize invokes each} with the
+ * given application context.</li>
+ * </ul>
+ * <p>Any {@code ApplicationContextInitializers} implementing
+ * {@link org.springframework.core.Ordered Ordered} or marked with {@link
+ * org.springframework.core.annotation.Order @Order} will be sorted appropriately.
+ * @param context the newly created application context
+ * @param mergedConfig the merged context configuration
+ * @since 3.2
+ * @see ApplicationContextInitializer#initialize(ConfigurableApplicationContext)
+ * @see #loadContext(MergedContextConfiguration)
+ * @see ConfigurableApplicationContext#setId
+ */
+ @SuppressWarnings("unchecked")
+ protected void prepareContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
+ context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles());
+
+ Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses =
+ mergedConfig.getContextInitializerClasses();
+ if (initializerClasses.isEmpty()) {
+ // no ApplicationContextInitializers have been declared -> nothing to do
+ return;
+ }
+
+ List<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerInstances =
+ new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>();
+ Class<?> contextClass = context.getClass();
+
+ for (Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>> initializerClass : initializerClasses) {
+ Class<?> initializerContextClass = GenericTypeResolver.resolveTypeArgument(initializerClass,
+ ApplicationContextInitializer.class);
+ Assert.isAssignable(initializerContextClass, contextClass, String.format(
+ "Could not add context initializer [%s] since its generic parameter [%s] " +
+ "is not assignable from the type of application context used by this " +
+ "context loader [%s]: ", initializerClass.getName(), initializerContextClass.getName(),
+ contextClass.getName()));
+ initializerInstances.add((ApplicationContextInitializer<ConfigurableApplicationContext>) BeanUtils.instantiateClass(initializerClass));
+ }
+
+ AnnotationAwareOrderComparator.sort(initializerInstances);
+ for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : initializerInstances) {
+ initializer.initialize(context);
+ }
+ }
+
+ // --- ContextLoader -------------------------------------------------------
+
+ /**
+ * If the supplied {@code locations} are {@code null} or
+ * <em>empty</em> and {@link #isGenerateDefaultLocations()} returns
+ * {@code true}, default locations will be
+ * {@link #generateDefaultLocations(Class) generated} for the specified
+ * {@link Class class} and the configured
+ * {@link #getResourceSuffix() resource suffix}; otherwise, the supplied
+ * {@code locations} will be {@link #modifyLocations modified} if
+ * necessary and returned.
+ * @param clazz the class with which the locations are associated: to be
+ * used when generating default locations
+ * @param locations the unmodified locations to use for loading the
+ * application context (can be {@code null} or empty)
+ * @return a processed array of application context resource locations
+ * @since 2.5
+ * @see #isGenerateDefaultLocations()
+ * @see #generateDefaultLocations(Class)
+ * @see #modifyLocations(Class, String...)
+ * @see org.springframework.test.context.ContextLoader#processLocations(Class, String...)
+ * @see #processContextConfiguration(ContextConfigurationAttributes)
+ */
+ public final String[] processLocations(Class<?> clazz, String... locations) {
+ return (ObjectUtils.isEmpty(locations) && isGenerateDefaultLocations()) ?
+ generateDefaultLocations(clazz) : modifyLocations(clazz, locations);
+ }
+
+ /**
+ * Generate the default classpath resource locations array based on the
+ * supplied class.
+ * <p>For example, if the supplied class is {@code com.example.MyTest},
+ * the generated locations will contain a single string with a value of
+ * &quot;classpath:/com/example/MyTest{@code <suffix>}&quot;,
+ * where {@code <suffix>} is the value of the
+ * {@link #getResourceSuffix() resource suffix} string.
+ * <p>As of Spring 3.1, the implementation of this method adheres to the
+ * contract defined in the {@link SmartContextLoader} SPI. Specifically,
+ * this method will <em>preemptively</em> verify that the generated default
+ * location actually exists. If it does not exist, this method will log a
+ * warning and return an empty array.
+ * <p>Subclasses can override this method to implement a different
+ * <em>default location generation</em> strategy.
+ * @param clazz the class for which the default locations are to be generated
+ * @return an array of default application context resource locations
+ * @since 2.5
+ * @see #getResourceSuffix()
+ */
+ protected String[] generateDefaultLocations(Class<?> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ String suffix = getResourceSuffix();
+ Assert.hasText(suffix, "Resource suffix must not be empty");
+ String resourcePath = SLASH + ClassUtils.convertClassNameToResourcePath(clazz.getName()) + suffix;
+ String prefixedResourcePath = ResourceUtils.CLASSPATH_URL_PREFIX + resourcePath;
+ ClassPathResource classPathResource = new ClassPathResource(resourcePath, clazz);
+
+ if (classPathResource.exists()) {
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format("Detected default resource location \"%s\" for test class [%s]",
+ prefixedResourcePath, clazz.getName()));
+ }
+ return new String[] {prefixedResourcePath};
+ }
+ else {
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format("Could not detect default resource locations for test class [%s]: " +
+ "%s does not exist", clazz.getName(), classPathResource));
+ }
+ return EMPTY_STRING_ARRAY;
+ }
+ }
+
+ /**
+ * Generate a modified version of the supplied locations array and return it.
+ * <p>A plain path &mdash; for example, &quot;context.xml&quot; &mdash; will
+ * be treated as a classpath resource that is relative to the package in which
+ * the specified class is defined. A path starting with a slash is treated
+ * as an absolute classpath location, for example:
+ * &quot;/org/springframework/whatever/foo.xml&quot;. A path which
+ * references a URL (e.g., a path prefixed with
+ * {@link ResourceUtils#CLASSPATH_URL_PREFIX classpath:},
+ * {@link ResourceUtils#FILE_URL_PREFIX file:}, {@code http:},
+ * etc.) will be added to the results unchanged.
+ * <p>Subclasses can override this method to implement a different
+ * <em>location modification</em> strategy.
+ * @param clazz the class with which the locations are associated
+ * @param locations the resource locations to be modified
+ * @return an array of modified application context resource locations
+ * @since 2.5
+ */
+ protected String[] modifyLocations(Class<?> clazz, String... locations) {
+ String[] modifiedLocations = new String[locations.length];
+ for (int i = 0; i < locations.length; i++) {
+ String path = locations[i];
+ if (path.startsWith(SLASH)) {
+ modifiedLocations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + path;
+ }
+ else if (!ResourcePatternUtils.isUrl(path)) {
+ modifiedLocations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + SLASH +
+ StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(clazz) + SLASH + path);
+ }
+ else {
+ modifiedLocations[i] = StringUtils.cleanPath(path);
+ }
+ }
+ return modifiedLocations;
+ }
+
+ /**
+ * Determine whether or not <em>default</em> resource locations should be
+ * generated if the {@code locations} provided to
+ * {@link #processLocations(Class, String...)} are {@code null} or empty.
+ * <p>As of Spring 3.1, the semantics of this method have been overloaded
+ * to include detection of either default resource locations or default
+ * configuration classes. Consequently, this method can also be used to
+ * determine whether or not <em>default</em> configuration classes should be
+ * detected if the {@code classes} present in the
+ * {@link ContextConfigurationAttributes configuration attributes} supplied
+ * to {@link #processContextConfiguration(ContextConfigurationAttributes)}
+ * are {@code null} or empty.
+ * <p>Can be overridden by subclasses to change the default behavior.
+ * @return always {@code true} by default
+ * @since 2.5
+ */
+ protected boolean isGenerateDefaultLocations() {
+ return true;
+ }
+
+ /**
+ * Get the suffix to append to {@link ApplicationContext} resource locations
+ * when generating default locations.
+ * <p>Must be implemented by subclasses.
+ * @return the resource suffix; should not be {@code null} or empty
+ * @since 2.5
+ * @see #generateDefaultLocations(Class)
+ */
+ protected abstract String getResourceSuffix();
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java
new file mode 100644
index 00000000..edf34158
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java
@@ -0,0 +1,286 @@
+/*
+ * 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.context.support;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.ContextLoader;
+import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.test.context.SmartContextLoader;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * {@code AbstractDelegatingSmartContextLoader} serves as an abstract base class
+ * for implementations of the {@link SmartContextLoader} SPI that delegate to a
+ * set of <em>candidate</em> SmartContextLoaders (i.e., one that supports XML
+ * configuration files and one that supports annotated classes) to determine which
+ * context loader is appropriate for a given test class's configuration. Each
+ * candidate is given a chance to {@link #processContextConfiguration process} the
+ * {@link ContextConfigurationAttributes} for each class in the test class hierarchy
+ * that is annotated with {@link ContextConfiguration @ContextConfiguration}, and
+ * the candidate that supports the merged, processed configuration will be used to
+ * actually {@link #loadContext load} the context.
+ *
+ * <p>Placing an empty {@code @ContextConfiguration} annotation on a test class signals
+ * that default resource locations (i.e., XML configuration files) or default
+ * {@link org.springframework.context.annotation.Configuration configuration classes}
+ * should be detected. Furthermore, if a specific {@link ContextLoader} or
+ * {@link SmartContextLoader} is not explicitly declared via
+ * {@code @ContextConfiguration}, a concrete subclass of
+ * {@code AbstractDelegatingSmartContextLoader} will be used as the default loader,
+ * thus providing automatic support for either XML configuration files or annotated
+ * classes, but not both simultaneously.
+ *
+ * <p>As of Spring 3.2, a test class may optionally declare neither XML configuration
+ * files nor annotated classes and instead declare only {@linkplain
+ * ContextConfiguration#initializers application context initializers}. In such
+ * cases, an attempt will still be made to detect defaults, but their absence will
+ * not result in an exception.
+ *
+ * @author Sam Brannen
+ * @since 3.2
+ * @see SmartContextLoader
+ */
+public abstract class AbstractDelegatingSmartContextLoader implements SmartContextLoader {
+
+ private static final Log logger = LogFactory.getLog(AbstractDelegatingSmartContextLoader.class);
+
+
+ /**
+ * Get the delegate {@code SmartContextLoader} that supports XML configuration files.
+ */
+ protected abstract SmartContextLoader getXmlLoader();
+
+ /**
+ * Get the delegate {@code SmartContextLoader} that supports annotated classes.
+ */
+ protected abstract SmartContextLoader getAnnotationConfigLoader();
+
+
+ // --- SmartContextLoader --------------------------------------------------
+
+ private static String name(SmartContextLoader loader) {
+ return loader.getClass().getSimpleName();
+ }
+
+ private static void delegateProcessing(SmartContextLoader loader, ContextConfigurationAttributes configAttributes) {
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Delegating to %s to process context configuration %s.", name(loader),
+ configAttributes));
+ }
+ loader.processContextConfiguration(configAttributes);
+ }
+
+ private static ApplicationContext delegateLoading(SmartContextLoader loader, MergedContextConfiguration mergedConfig)
+ throws Exception {
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig));
+ }
+ return loader.loadContext(mergedConfig);
+ }
+
+ private boolean supports(SmartContextLoader loader, MergedContextConfiguration mergedConfig) {
+ if (loader == getAnnotationConfigLoader()) {
+ return ObjectUtils.isEmpty(mergedConfig.getLocations()) && !ObjectUtils.isEmpty(mergedConfig.getClasses());
+ }
+ else {
+ return !ObjectUtils.isEmpty(mergedConfig.getLocations()) && ObjectUtils.isEmpty(mergedConfig.getClasses());
+ }
+ }
+
+ /**
+ * Delegates to candidate {@code SmartContextLoaders} to process the supplied
+ * {@link ContextConfigurationAttributes}.
+ * <p>Delegation is based on explicit knowledge of the implementations of the
+ * default loaders for {@link #getXmlLoader() XML configuration files} and
+ * {@link #getAnnotationConfigLoader() annotated classes}. Specifically, the
+ * delegation algorithm is as follows:
+ * <ul>
+ * <li>If the resource locations or annotated classes in the supplied
+ * {@code ContextConfigurationAttributes} are not empty, the appropriate
+ * candidate loader will be allowed to process the configuration <em>as is</em>,
+ * without any checks for detection of defaults.</li>
+ * <li>Otherwise, the XML-based loader will be allowed to process
+ * the configuration in order to detect default resource locations. If
+ * the XML-based loader detects default resource locations,
+ * an {@code info} message will be logged.</li>
+ * <li>Subsequently, the annotation-based loader will be allowed to
+ * process the configuration in order to detect default configuration classes.
+ * If the annotation-based loader detects default configuration
+ * classes, an {@code info} message will be logged.</li>
+ * </ul>
+ * @param configAttributes the context configuration attributes to process
+ * @throws IllegalArgumentException if the supplied configuration attributes are
+ * {@code null}, or if the supplied configuration attributes include both
+ * resource locations and annotated classes
+ * @throws IllegalStateException if the XML-based loader detects default
+ * configuration classes; if the annotation-based loader detects default
+ * resource locations; if neither candidate loader detects defaults for the supplied
+ * context configuration; or if both candidate loaders detect defaults for the
+ * supplied context configuration
+ */
+ public void processContextConfiguration(final ContextConfigurationAttributes configAttributes) {
+
+ Assert.notNull(configAttributes, "configAttributes must not be null");
+ Assert.isTrue(!(configAttributes.hasLocations() && configAttributes.hasClasses()), String.format(
+ "Cannot process locations AND classes for context configuration %s; configure one or the other, but not both.",
+ configAttributes));
+
+ // If the original locations or classes were not empty, there's no
+ // need to bother with default detection checks; just let the
+ // appropriate loader process the configuration.
+ if (configAttributes.hasLocations()) {
+ delegateProcessing(getXmlLoader(), configAttributes);
+ }
+ else if (configAttributes.hasClasses()) {
+ delegateProcessing(getAnnotationConfigLoader(), configAttributes);
+ }
+ else {
+ // Else attempt to detect defaults...
+
+ // Let the XML loader process the configuration.
+ delegateProcessing(getXmlLoader(), configAttributes);
+ boolean xmlLoaderDetectedDefaults = configAttributes.hasLocations();
+
+ if (xmlLoaderDetectedDefaults) {
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format("%s detected default locations for context configuration %s.",
+ name(getXmlLoader()), configAttributes));
+ }
+ }
+
+ if (configAttributes.hasClasses()) {
+ throw new IllegalStateException(String.format(
+ "%s should NOT have detected default configuration classes for context configuration %s.",
+ name(getXmlLoader()), configAttributes));
+ }
+
+ // Now let the annotation config loader process the configuration.
+ delegateProcessing(getAnnotationConfigLoader(), configAttributes);
+
+ if (configAttributes.hasClasses()) {
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format(
+ "%s detected default configuration classes for context configuration %s.",
+ name(getAnnotationConfigLoader()), configAttributes));
+ }
+ }
+
+ if (!xmlLoaderDetectedDefaults && configAttributes.hasLocations()) {
+ throw new IllegalStateException(String.format(
+ "%s should NOT have detected default locations for context configuration %s.",
+ name(getAnnotationConfigLoader()), configAttributes));
+ }
+
+ // If neither loader detected defaults and no initializers were declared,
+ // throw an exception.
+ if (!configAttributes.hasResources() && ObjectUtils.isEmpty(configAttributes.getInitializers())) {
+ throw new IllegalStateException(String.format(
+ "Neither %s nor %s was able to detect defaults, and no ApplicationContextInitializers " +
+ "were declared for context configuration %s", name(getXmlLoader()),
+ name(getAnnotationConfigLoader()), configAttributes));
+ }
+
+ if (configAttributes.hasLocations() && configAttributes.hasClasses()) {
+ String message = String.format(
+ "Configuration error: both default locations AND default configuration classes " +
+ "were detected for context configuration %s; configure one or the other, but not both.",
+ configAttributes);
+ logger.error(message);
+ throw new IllegalStateException(message);
+ }
+ }
+ }
+
+ /**
+ * Delegates to an appropriate candidate {@code SmartContextLoader} to load
+ * an {@link ApplicationContext}.
+ * <p>Delegation is based on explicit knowledge of the implementations of the
+ * default loaders for {@link #getXmlLoader() XML configuration files} and
+ * {@link #getAnnotationConfigLoader() annotated classes}. Specifically, the
+ * delegation algorithm is as follows:
+ * <ul>
+ * <li>If the resource locations in the supplied {@code MergedContextConfiguration}
+ * are not empty and the annotated classes are empty,
+ * the XML-based loader will load the {@code ApplicationContext}.</li>
+ * <li>If the annotated classes in the supplied {@code MergedContextConfiguration}
+ * are not empty and the resource locations are empty,
+ * the annotation-based loader will load the {@code ApplicationContext}.</li>
+ * </ul>
+ * @param mergedConfig the merged context configuration to use to load the application context
+ * @throws IllegalArgumentException if the supplied merged configuration is {@code null}
+ * @throws IllegalStateException if neither candidate loader is capable of loading an
+ * {@code ApplicationContext} from the supplied merged context configuration
+ */
+ public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
+ Assert.notNull(mergedConfig, "mergedConfig must not be null");
+ List<SmartContextLoader> candidates = Arrays.asList(getXmlLoader(), getAnnotationConfigLoader());
+
+ for (SmartContextLoader loader : candidates) {
+ // Determine if each loader can load a context from the mergedConfig. If it
+ // can, let it; otherwise, keep iterating.
+ if (supports(loader, mergedConfig)) {
+ return delegateLoading(loader, mergedConfig);
+ }
+ }
+
+ // If neither of the candidates supports the mergedConfig based on resources but
+ // ACIs were declared, then delegate to the annotation config loader.
+ if (!mergedConfig.getContextInitializerClasses().isEmpty()) {
+ return delegateLoading(getAnnotationConfigLoader(), mergedConfig);
+ }
+
+ throw new IllegalStateException(String.format(
+ "Neither %s nor %s was able to load an ApplicationContext from %s.", name(getXmlLoader()),
+ name(getAnnotationConfigLoader()), mergedConfig));
+ }
+
+
+ // --- ContextLoader -------------------------------------------------------
+
+ /**
+ * {@code AbstractDelegatingSmartContextLoader} does not support the
+ * {@link ContextLoader#processLocations(Class, String...)} method. Call
+ * {@link #processContextConfiguration(ContextConfigurationAttributes)} instead.
+ * @throws UnsupportedOperationException
+ */
+ public final String[] processLocations(Class<?> clazz, String... locations) {
+ throw new UnsupportedOperationException("DelegatingSmartContextLoaders do not support the ContextLoader SPI. " +
+ "Call processContextConfiguration(ContextConfigurationAttributes) instead.");
+ }
+
+ /**
+ * {@code AbstractDelegatingSmartContextLoader} does not support the
+ * {@link ContextLoader#loadContext(String...) } method. Call
+ * {@link #loadContext(MergedContextConfiguration)} instead.
+ * @throws UnsupportedOperationException
+ */
+ public final ApplicationContext loadContext(String... locations) throws Exception {
+ throw new UnsupportedOperationException("DelegatingSmartContextLoaders do not support the ContextLoader SPI. " +
+ "Call loadContext(MergedContextConfiguration) instead.");
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java
new file mode 100644
index 00000000..bd84068a
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2002-2013 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.context.support;
+
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.support.BeanDefinitionReader;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigUtils;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.util.StringUtils;
+
+/**
+ * Abstract, generic extension of {@link AbstractContextLoader} that loads a
+ * {@link GenericApplicationContext}.
+ *
+ * <ul>
+ * <li>If instances of concrete subclasses are invoked via the
+ * {@link org.springframework.test.context.ContextLoader ContextLoader} SPI, the
+ * context will be loaded from the <em>locations</em> provided to
+ * {@link #loadContext(String...)}.</li>
+ * <li>If instances of concrete subclasses are invoked via the
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}
+ * SPI, the context will be loaded from the {@link MergedContextConfiguration}
+ * provided to {@link #loadContext(MergedContextConfiguration)}. In such cases, a
+ * {@code SmartContextLoader} will decide whether to load the context from
+ * <em>locations</em> or <em>annotated classes</em>.</li>
+ * </ul>
+ *
+ * <p>Concrete subclasses must provide an appropriate implementation of
+ * {@link #createBeanDefinitionReader createBeanDefinitionReader()},
+ * potentially overriding {@link #loadBeanDefinitions loadBeanDefinitions()}
+ * as well.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see #loadContext(MergedContextConfiguration)
+ * @see #loadContext(String...)
+ */
+public abstract class AbstractGenericContextLoader extends AbstractContextLoader {
+
+ protected static final Log logger = LogFactory.getLog(AbstractGenericContextLoader.class);
+
+
+ /**
+ * Load a Spring ApplicationContext from the supplied {@link MergedContextConfiguration}.
+ *
+ * <p>Implementation details:
+ *
+ * <ul>
+ * <li>Creates a {@link GenericApplicationContext} instance.</li>
+ * <li>If the supplied {@code MergedContextConfiguration} references a
+ * {@linkplain MergedContextConfiguration#getParent() parent configuration},
+ * the corresponding {@link MergedContextConfiguration#getParentApplicationContext()
+ * ApplicationContext} will be retrieved and
+ * {@linkplain GenericApplicationContext#setParent(ApplicationContext) set as the parent}
+ * for the context created by this method.</li>
+ * <li>Calls {@link #prepareContext(GenericApplicationContext)} for backwards
+ * compatibility with the {@link org.springframework.test.context.ContextLoader
+ * ContextLoader} SPI.</li>
+ * <li>Calls {@link #prepareContext(ConfigurableApplicationContext, MergedContextConfiguration)}
+ * to allow for customizing the context before bean definitions are loaded.</li>
+ * <li>Calls {@link #customizeBeanFactory(DefaultListableBeanFactory)} to allow for customizing the
+ * context's {@code DefaultListableBeanFactory}.</li>
+ * <li>Delegates to {@link #loadBeanDefinitions(GenericApplicationContext, MergedContextConfiguration)}
+ * to populate the context from the locations or classes in the supplied
+ * {@code MergedContextConfiguration}.</li>
+ * <li>Delegates to {@link AnnotationConfigUtils} for
+ * {@link AnnotationConfigUtils#registerAnnotationConfigProcessors registering}
+ * annotation configuration processors.</li>
+ * <li>Calls {@link #customizeContext(GenericApplicationContext)} to allow for customizing the context
+ * before it is refreshed.</li>
+ * <li>{@link ConfigurableApplicationContext#refresh Refreshes} the
+ * context and registers a JVM shutdown hook for it.</li>
+ * </ul>
+ *
+ * @return a new application context
+ * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration)
+ * @see GenericApplicationContext
+ * @since 3.1
+ */
+ public final ConfigurableApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Loading ApplicationContext for merged context configuration [%s].",
+ mergedConfig));
+ }
+
+ GenericApplicationContext context = new GenericApplicationContext();
+
+ ApplicationContext parent = mergedConfig.getParentApplicationContext();
+ if (parent != null) {
+ context.setParent(parent);
+ }
+ prepareContext(context);
+ prepareContext(context, mergedConfig);
+ customizeBeanFactory(context.getDefaultListableBeanFactory());
+ loadBeanDefinitions(context, mergedConfig);
+ AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
+ customizeContext(context);
+ context.refresh();
+ context.registerShutdownHook();
+ return context;
+ }
+
+ /**
+ * Load a Spring ApplicationContext from the supplied {@code locations}.
+ *
+ * <p>Implementation details:
+ *
+ * <ul>
+ * <li>Creates a {@link GenericApplicationContext} instance.</li>
+ * <li>Calls {@link #prepareContext(GenericApplicationContext)} to allow for customizing the context
+ * before bean definitions are loaded.</li>
+ * <li>Calls {@link #customizeBeanFactory(DefaultListableBeanFactory)} to allow for customizing the
+ * context's {@code DefaultListableBeanFactory}.</li>
+ * <li>Delegates to {@link #createBeanDefinitionReader(GenericApplicationContext)} to create a
+ * {@link BeanDefinitionReader} which is then used to populate the context
+ * from the specified locations.</li>
+ * <li>Delegates to {@link AnnotationConfigUtils} for
+ * {@link AnnotationConfigUtils#registerAnnotationConfigProcessors registering}
+ * annotation configuration processors.</li>
+ * <li>Calls {@link #customizeContext(GenericApplicationContext)} to allow for customizing the context
+ * before it is refreshed.</li>
+ * <li>{@link ConfigurableApplicationContext#refresh Refreshes} the
+ * context and registers a JVM shutdown hook for it.</li>
+ * </ul>
+ *
+ * <p><b>Note</b>: this method does not provide a means to set active bean definition
+ * profiles for the loaded context. See {@link #loadContext(MergedContextConfiguration)}
+ * and {@link AbstractContextLoader#prepareContext(ConfigurableApplicationContext, MergedContextConfiguration)}
+ * for an alternative.
+ *
+ * @return a new application context
+ * @see org.springframework.test.context.ContextLoader#loadContext
+ * @see GenericApplicationContext
+ * @see #loadContext(MergedContextConfiguration)
+ * @since 2.5
+ */
+ public final ConfigurableApplicationContext loadContext(String... locations) throws Exception {
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Loading ApplicationContext for locations [%s].",
+ StringUtils.arrayToCommaDelimitedString(locations)));
+ }
+ GenericApplicationContext context = new GenericApplicationContext();
+ prepareContext(context);
+ customizeBeanFactory(context.getDefaultListableBeanFactory());
+ createBeanDefinitionReader(context).loadBeanDefinitions(locations);
+ AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
+ customizeContext(context);
+ context.refresh();
+ context.registerShutdownHook();
+ return context;
+ }
+
+ /**
+ * Prepare the {@link GenericApplicationContext} created by this {@code ContextLoader}.
+ * Called <i>before</i> bean definitions are read.
+ *
+ * <p>The default implementation is empty. Can be overridden in subclasses to
+ * customize {@code GenericApplicationContext}'s standard settings.
+ *
+ * @param context the context that should be prepared
+ * @see #loadContext(MergedContextConfiguration)
+ * @see #loadContext(String...)
+ * @see GenericApplicationContext#setAllowBeanDefinitionOverriding
+ * @see GenericApplicationContext#setResourceLoader
+ * @see GenericApplicationContext#setId
+ * @since 2.5
+ */
+ protected void prepareContext(GenericApplicationContext context) {
+ }
+
+ /**
+ * Customize the internal bean factory of the ApplicationContext created by
+ * this {@code ContextLoader}.
+ *
+ * <p>The default implementation is empty but can be overridden in subclasses
+ * to customize {@code DefaultListableBeanFactory}'s standard settings.
+ *
+ * @param beanFactory the bean factory created by this {@code ContextLoader}
+ * @see #loadContext(MergedContextConfiguration)
+ * @see #loadContext(String...)
+ * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding
+ * @see DefaultListableBeanFactory#setAllowEagerClassLoading
+ * @see DefaultListableBeanFactory#setAllowCircularReferences
+ * @see DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping
+ * @since 2.5
+ */
+ protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
+ }
+
+ /**
+ * Load bean definitions into the supplied {@link GenericApplicationContext context}
+ * from the locations or classes in the supplied {@code MergedContextConfiguration}.
+ *
+ * <p>The default implementation delegates to the {@link BeanDefinitionReader}
+ * returned by {@link #createBeanDefinitionReader(GenericApplicationContext)} to
+ * {@link BeanDefinitionReader#loadBeanDefinitions(String) load} the
+ * bean definitions.
+ *
+ * <p>Subclasses must provide an appropriate implementation of
+ * {@link #createBeanDefinitionReader(GenericApplicationContext)}. Alternatively subclasses
+ * may provide a <em>no-op</em> implementation of {@code createBeanDefinitionReader()}
+ * and override this method to provide a custom strategy for loading or
+ * registering bean definitions.
+ *
+ * @param context the context into which the bean definitions should be loaded
+ * @param mergedConfig the merged context configuration
+ * @see #loadContext(MergedContextConfiguration)
+ * @since 3.1
+ */
+ protected void loadBeanDefinitions(GenericApplicationContext context, MergedContextConfiguration mergedConfig) {
+ createBeanDefinitionReader(context).loadBeanDefinitions(mergedConfig.getLocations());
+ }
+
+ /**
+ * Factory method for creating a new {@link BeanDefinitionReader} for loading
+ * bean definitions into the supplied {@link GenericApplicationContext context}.
+ *
+ * @param context the context for which the {@code BeanDefinitionReader}
+ * should be created
+ * @return a {@code BeanDefinitionReader} for the supplied context
+ * @see #loadContext(String...)
+ * @see #loadBeanDefinitions
+ * @see BeanDefinitionReader
+ * @since 2.5
+ */
+ protected abstract BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context);
+
+ /**
+ * Customize the {@link GenericApplicationContext} created by this
+ * {@code ContextLoader} <i>after</i> bean definitions have been
+ * loaded into the context but <i>before</i> the context is refreshed.
+ *
+ * <p>The default implementation is empty but can be overridden in subclasses
+ * to customize the application context.
+ *
+ * @param context the newly created application context
+ * @see #loadContext(MergedContextConfiguration)
+ * @see #loadContext(String...)
+ * @since 2.5
+ */
+ protected void customizeContext(GenericApplicationContext context) {
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java
new file mode 100644
index 00000000..34dfe47e
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java
@@ -0,0 +1,73 @@
+/*
+ * 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.context.support;
+
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.TestExecutionListener;
+
+/**
+ * Abstract implementation of the {@link TestExecutionListener} interface which
+ * provides empty method stubs. Subclasses can extend this class and override
+ * only those methods suitable for the task at hand.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+public abstract class AbstractTestExecutionListener implements TestExecutionListener {
+
+ /**
+ * The default implementation is <em>empty</em>. Can be overridden by
+ * subclasses as necessary.
+ */
+ public void beforeTestClass(TestContext testContext) throws Exception {
+ /* no-op */
+ }
+
+ /**
+ * The default implementation is <em>empty</em>. Can be overridden by
+ * subclasses as necessary.
+ */
+ public void prepareTestInstance(TestContext testContext) throws Exception {
+ /* no-op */
+ }
+
+ /**
+ * The default implementation is <em>empty</em>. Can be overridden by
+ * subclasses as necessary.
+ */
+ public void beforeTestMethod(TestContext testContext) throws Exception {
+ /* no-op */
+ }
+
+ /**
+ * The default implementation is <em>empty</em>. Can be overridden by
+ * subclasses as necessary.
+ */
+ public void afterTestMethod(TestContext testContext) throws Exception {
+ /* no-op */
+ }
+
+ /**
+ * The default implementation is <em>empty</em>. Can be overridden by
+ * subclasses as necessary.
+ */
+ public void afterTestClass(TestContext testContext) throws Exception {
+ /* no-op */
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java
new file mode 100644
index 00000000..39e76f6e
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java
@@ -0,0 +1,192 @@
+/*
+ * 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.context.support;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.support.BeanDefinitionReader;
+import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * Concrete implementation of {@link AbstractGenericContextLoader} that loads
+ * bean definitions from annotated classes.
+ *
+ * <p>See the Javadoc for
+ * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration}
+ * for a definition of <em>annotated class</em>.
+ *
+ * <p>Note: {@code AnnotationConfigContextLoader} supports <em>annotated classes</em>
+ * rather than the String-based resource locations defined by the legacy
+ * {@link org.springframework.test.context.ContextLoader ContextLoader} API. Thus,
+ * although {@code AnnotationConfigContextLoader} extends
+ * {@code AbstractGenericContextLoader}, {@code AnnotationConfigContextLoader}
+ * does <em>not</em> support any String-based methods defined by
+ * {@code AbstractContextLoader} or {@code AbstractGenericContextLoader}.
+ * Consequently, {@code AnnotationConfigContextLoader} should chiefly be
+ * considered a {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}
+ * rather than a {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ *
+ * @author Sam Brannen
+ * @since 3.1
+ * @see #processContextConfiguration(ContextConfigurationAttributes)
+ * @see #detectDefaultConfigurationClasses(Class)
+ * @see #loadBeanDefinitions(GenericApplicationContext, MergedContextConfiguration)
+ */
+public class AnnotationConfigContextLoader extends AbstractGenericContextLoader {
+
+ private static final Log logger = LogFactory.getLog(AnnotationConfigContextLoader.class);
+
+
+ // --- SmartContextLoader -----------------------------------------------
+
+ /**
+ * Process <em>annotated classes</em> in the supplied {@link ContextConfigurationAttributes}.
+ *
+ * <p>If the <em>annotated classes</em> are {@code null} or empty and
+ * {@link #isGenerateDefaultLocations()} returns {@code true}, this
+ * {@code SmartContextLoader} will attempt to {@link
+ * #detectDefaultConfigurationClasses detect default configuration classes}.
+ * If defaults are detected they will be
+ * {@link ContextConfigurationAttributes#setClasses(Class[]) set} in the
+ * supplied configuration attributes. Otherwise, properties in the supplied
+ * configuration attributes will not be modified.
+ *
+ * @param configAttributes the context configuration attributes to process
+ * @see org.springframework.test.context.SmartContextLoader#processContextConfiguration(ContextConfigurationAttributes)
+ * @see #isGenerateDefaultLocations()
+ * @see #detectDefaultConfigurationClasses(Class)
+ */
+ public void processContextConfiguration(ContextConfigurationAttributes configAttributes) {
+ if (ObjectUtils.isEmpty(configAttributes.getClasses()) && isGenerateDefaultLocations()) {
+ Class<?>[] defaultConfigClasses = detectDefaultConfigurationClasses(configAttributes.getDeclaringClass());
+ configAttributes.setClasses(defaultConfigClasses);
+ }
+ }
+
+ // --- AnnotationConfigContextLoader ---------------------------------------
+
+ /**
+ * Detect the default configuration classes for the supplied test class.
+ *
+ * <p>The default implementation simply delegates to
+ * {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses(Class)}.
+ *
+ * @param declaringClass the test class that declared {@code @ContextConfiguration}
+ * @return an array of default configuration classes, potentially empty but
+ * never {@code null}
+ * @see AnnotationConfigContextLoaderUtils
+ */
+ protected Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) {
+ return AnnotationConfigContextLoaderUtils.detectDefaultConfigurationClasses(declaringClass);
+ }
+
+ // --- AbstractContextLoader -----------------------------------------------
+
+ /**
+ * {@code AnnotationConfigContextLoader} should be used as a
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader},
+ * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ * Consequently, this method is not supported.
+ *
+ * @see AbstractContextLoader#modifyLocations
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ protected String[] modifyLocations(Class<?> clazz, String... locations) {
+ throw new UnsupportedOperationException(
+ "AnnotationConfigContextLoader does not support the modifyLocations(Class, String...) method");
+ }
+
+ /**
+ * {@code AnnotationConfigContextLoader} should be used as a
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader},
+ * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ * Consequently, this method is not supported.
+ *
+ * @see AbstractContextLoader#generateDefaultLocations
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ protected String[] generateDefaultLocations(Class<?> clazz) {
+ throw new UnsupportedOperationException(
+ "AnnotationConfigContextLoader does not support the generateDefaultLocations(Class) method");
+ }
+
+ /**
+ * {@code AnnotationConfigContextLoader} should be used as a
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader},
+ * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ * Consequently, this method is not supported.
+ *
+ * @see AbstractContextLoader#getResourceSuffix
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ protected String getResourceSuffix() {
+ throw new UnsupportedOperationException(
+ "AnnotationConfigContextLoader does not support the getResourceSuffix() method");
+ }
+
+ // --- AbstractGenericContextLoader ----------------------------------------
+
+ /**
+ * Register classes in the supplied {@link GenericApplicationContext context}
+ * from the classes in the supplied {@link MergedContextConfiguration}.
+ *
+ * <p>Each class must represent an <em>annotated class</em>. An
+ * {@link AnnotatedBeanDefinitionReader} is used to register the appropriate
+ * bean definitions.
+ *
+ * <p>Note that this method does not call {@link #createBeanDefinitionReader}
+ * since {@code AnnotatedBeanDefinitionReader} is not an instance of
+ * {@link BeanDefinitionReader}.
+ *
+ * @param context the context in which the annotated classes should be registered
+ * @param mergedConfig the merged configuration from which the classes should be retrieved
+ *
+ * @see AbstractGenericContextLoader#loadBeanDefinitions
+ */
+ @Override
+ protected void loadBeanDefinitions(GenericApplicationContext context, MergedContextConfiguration mergedConfig) {
+ Class<?>[] annotatedClasses = mergedConfig.getClasses();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Registering annotated classes: " + ObjectUtils.nullSafeToString(annotatedClasses));
+ }
+ new AnnotatedBeanDefinitionReader(context).register(annotatedClasses);
+ }
+
+ /**
+ * {@code AnnotationConfigContextLoader} should be used as a
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader},
+ * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ * Consequently, this method is not supported.
+ *
+ * @see #loadBeanDefinitions
+ * @see AbstractGenericContextLoader#createBeanDefinitionReader
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ protected BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context) {
+ throw new UnsupportedOperationException(
+ "AnnotationConfigContextLoader does not support the createBeanDefinitionReader(GenericApplicationContext) method");
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java
new file mode 100644
index 00000000..0cdbe30f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java
@@ -0,0 +1,112 @@
+/*
+ * 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.context.support;
+
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.SmartContextLoader;
+import org.springframework.util.Assert;
+
+/**
+ * Utility methods for {@link SmartContextLoader SmartContextLoaders} that deal
+ * with annotated classes (e.g., {@link Configuration @Configuration} classes).
+ *
+ * @author Sam Brannen
+ * @since 3.2
+ */
+public abstract class AnnotationConfigContextLoaderUtils {
+
+ private static final Log logger = LogFactory.getLog(AnnotationConfigContextLoaderUtils.class);
+
+
+ /**
+ * Determine if the supplied {@link Class} meets the criteria for being
+ * considered a <em>default configuration class</em> candidate.
+ * <p>Specifically, such candidates:
+ * <ul>
+ * <li>must not be {@code null}</li>
+ * <li>must not be {@code private}</li>
+ * <li>must not be {@code final}</li>
+ * <li>must be {@code static}</li>
+ * <li>must be annotated with {@code @Configuration}</li>
+ * </ul>
+ * @param clazz the class to check
+ * @return {@code true} if the supplied class meets the candidate criteria
+ */
+ private static boolean isDefaultConfigurationClassCandidate(Class<?> clazz) {
+ return clazz != null && isStaticNonPrivateAndNonFinal(clazz) && clazz.isAnnotationPresent(Configuration.class);
+ }
+
+ private static boolean isStaticNonPrivateAndNonFinal(Class<?> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ int modifiers = clazz.getModifiers();
+ return (Modifier.isStatic(modifiers) && !Modifier.isPrivate(modifiers) && !Modifier.isFinal(modifiers));
+ }
+
+ /**
+ * Detect the default configuration classes for the supplied test class.
+ * <p>The returned class array will contain all static inner classes of
+ * the supplied class that meet the requirements for {@code @Configuration}
+ * class implementations as specified in the documentation for
+ * {@link Configuration @Configuration}.
+ * <p>The implementation of this method adheres to the contract defined in the
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}
+ * SPI. Specifically, this method uses introspection to detect default
+ * configuration classes that comply with the constraints required of
+ * {@code @Configuration} class implementations. If a potential candidate
+ * configuration class does not meet these requirements, this method will log a
+ * debug message, and the potential candidate class will be ignored.
+ * @param declaringClass the test class that declared {@code @ContextConfiguration}
+ * @return an array of default configuration classes, potentially empty but
+ * never {@code null}
+ */
+ public static Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) {
+ Assert.notNull(declaringClass, "Declaring class must not be null");
+ List<Class<?>> configClasses = new ArrayList<Class<?>>();
+
+ for (Class<?> candidate : declaringClass.getDeclaredClasses()) {
+ if (isDefaultConfigurationClassCandidate(candidate)) {
+ configClasses.add(candidate);
+ }
+ else {
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format(
+ "Ignoring class [%s]; it must be static, non-private, non-final, and annotated " +
+ "with @Configuration to be considered a default configuration class.",
+ candidate.getName()));
+ }
+ }
+ }
+
+ if (configClasses.isEmpty()) {
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format("Could not detect default configuration classes for test class [%s]: " +
+ "%s does not declare any static, non-private, non-final, inner classes " +
+ "annotated with @Configuration.", declaringClass.getName(), declaringClass.getSimpleName()));
+ }
+ }
+
+ return configClasses.toArray(new Class<?>[configClasses.size()]);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java
new file mode 100644
index 00000000..32442fe3
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java
@@ -0,0 +1,47 @@
+/*
+ * 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.context.support;
+
+import org.springframework.test.context.SmartContextLoader;
+
+/**
+ * {@code DelegatingSmartContextLoader} is a concrete implementation of
+ * {@link AbstractDelegatingSmartContextLoader} that delegates to a
+ * {@link GenericXmlContextLoader} and an {@link AnnotationConfigContextLoader}.
+ *
+ * @author Sam Brannen
+ * @since 3.1
+ * @see SmartContextLoader
+ * @see AbstractDelegatingSmartContextLoader
+ * @see GenericXmlContextLoader
+ * @see AnnotationConfigContextLoader
+ */
+public class DelegatingSmartContextLoader extends AbstractDelegatingSmartContextLoader {
+
+ private final SmartContextLoader xmlLoader = new GenericXmlContextLoader();
+ private final SmartContextLoader annotationConfigLoader = new AnnotationConfigContextLoader();
+
+
+ protected SmartContextLoader getXmlLoader() {
+ return this.xmlLoader;
+ }
+
+ protected SmartContextLoader getAnnotationConfigLoader() {
+ return this.annotationConfigLoader;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java
new file mode 100644
index 00000000..cb852237
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java
@@ -0,0 +1,115 @@
+/*
+ * 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.context.support;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
+import org.springframework.core.Conventions;
+import org.springframework.test.context.TestContext;
+
+/**
+ * {@code TestExecutionListener} which provides support for dependency
+ * injection and initialization of test instances.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+public class DependencyInjectionTestExecutionListener extends AbstractTestExecutionListener {
+
+ /**
+ * Attribute name for a {@link TestContext} attribute which indicates
+ * whether or not the dependencies of a test instance should be
+ * <em>reinjected</em> in
+ * {@link #beforeTestMethod(TestContext) beforeTestMethod()}. Note that
+ * dependencies will be injected in
+ * {@link #prepareTestInstance(TestContext) prepareTestInstance()} in any
+ * case.
+ * <p>Clients of a {@link TestContext} (e.g., other
+ * {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners})
+ * may therefore choose to set this attribute to signal that dependencies
+ * should be reinjected <em>between</em> execution of individual test
+ * methods.
+ * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
+ */
+ public static final String REINJECT_DEPENDENCIES_ATTRIBUTE = Conventions.getQualifiedAttributeName(
+ DependencyInjectionTestExecutionListener.class, "reinjectDependencies");
+
+ private static final Log logger = LogFactory.getLog(DependencyInjectionTestExecutionListener.class);
+
+
+ /**
+ * Performs dependency injection on the
+ * {@link TestContext#getTestInstance() test instance} of the supplied
+ * {@link TestContext test context} by
+ * {@link AutowireCapableBeanFactory#autowireBeanProperties(Object, int, boolean) autowiring}
+ * and
+ * {@link AutowireCapableBeanFactory#initializeBean(Object, String) initializing}
+ * the test instance via its own
+ * {@link TestContext#getApplicationContext() application context} (without
+ * checking dependencies).
+ * <p>The {@link #REINJECT_DEPENDENCIES_ATTRIBUTE} will be subsequently removed
+ * from the test context, regardless of its value.
+ */
+ @Override
+ public void prepareTestInstance(final TestContext testContext) throws Exception {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Performing dependency injection for test context [" + testContext + "].");
+ }
+ injectDependencies(testContext);
+ }
+
+ /**
+ * If the {@link #REINJECT_DEPENDENCIES_ATTRIBUTE} in the supplied
+ * {@link TestContext test context} has a value of {@link Boolean#TRUE},
+ * this method will have the same effect as
+ * {@link #prepareTestInstance(TestContext) prepareTestInstance()};
+ * otherwise, this method will have no effect.
+ */
+ @Override
+ public void beforeTestMethod(final TestContext testContext) throws Exception {
+ if (Boolean.TRUE.equals(testContext.getAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE))) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Reinjecting dependencies for test context [" + testContext + "].");
+ }
+ injectDependencies(testContext);
+ }
+ }
+
+ /**
+ * Performs dependency injection and bean initialization for the supplied
+ * {@link TestContext} as described in
+ * {@link #prepareTestInstance(TestContext) prepareTestInstance()}.
+ * <p>The {@link #REINJECT_DEPENDENCIES_ATTRIBUTE} will be subsequently removed
+ * from the test context, regardless of its value.
+ * @param testContext the test context for which dependency injection should
+ * be performed (never {@code null})
+ * @throws Exception allows any exception to propagate
+ * @see #prepareTestInstance(TestContext)
+ * @see #beforeTestMethod(TestContext)
+ */
+ protected void injectDependencies(final TestContext testContext) throws Exception {
+ Object bean = testContext.getTestInstance();
+ AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
+ beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
+ beanFactory.initializeBean(bean, testContext.getTestClass().getName());
+ testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java
new file mode 100644
index 00000000..f2a45a46
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2002-2013 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.context.support;
+
+import java.lang.reflect.Method;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
+import org.springframework.test.context.TestContext;
+import org.springframework.util.Assert;
+
+/**
+ * {@code TestExecutionListener} which provides support for marking the
+ * {@code ApplicationContext} associated with a test as <em>dirty</em> for
+ * both test classes and test methods configured with the {@link DirtiesContext
+ * &#064;DirtiesContext} annotation.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see DirtiesContext
+ */
+public class DirtiesContextTestExecutionListener extends AbstractTestExecutionListener {
+
+ private static final Log logger = LogFactory.getLog(DirtiesContextTestExecutionListener.class);
+
+
+ /**
+ * Marks the {@linkplain ApplicationContext application context} of the supplied
+ * {@linkplain TestContext test context} as
+ * {@linkplain TestContext#markApplicationContextDirty() dirty}, and sets the
+ * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
+ * in the test context to {@code true}.
+ * @param testContext the test context whose application context should
+ * marked as dirty
+ * @deprecated as of Spring 3.2.2, use {@link #dirtyContext(TestContext, HierarchyMode)} instead.
+ */
+ @Deprecated
+ protected void dirtyContext(TestContext testContext) {
+ testContext.markApplicationContextDirty();
+ testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, Boolean.TRUE);
+ }
+
+ /**
+ * Marks the {@linkplain ApplicationContext application context} of the supplied
+ * {@linkplain TestContext test context} as
+ * {@linkplain TestContext#markApplicationContextDirty(DirtiesContext.HierarchyMode) dirty}
+ * and sets {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
+ * in the test context to {@code true}.
+ * @param testContext the test context whose application context should
+ * marked as dirty
+ * @param hierarchyMode the context cache clearing mode to be applied if the
+ * context is part of a hierarchy; may be {@code null}
+ * @since 3.2.2
+ */
+ protected void dirtyContext(TestContext testContext, HierarchyMode hierarchyMode) {
+ testContext.markApplicationContextDirty(hierarchyMode);
+ testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, Boolean.TRUE);
+ }
+
+ /**
+ * If the current test method of the supplied {@linkplain TestContext test
+ * context} is annotated with {@link DirtiesContext &#064;DirtiesContext},
+ * or if the test class is annotated with {@link DirtiesContext
+ * &#064;DirtiesContext} and the {@linkplain DirtiesContext#classMode() class
+ * mode} is set to {@link ClassMode#AFTER_EACH_TEST_METHOD
+ * AFTER_EACH_TEST_METHOD}, the {@linkplain ApplicationContext application
+ * context} of the test context will be
+ * {@linkplain TestContext#markApplicationContextDirty() marked as dirty} and the
+ * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
+ * in the test context will be set to {@code true}.
+ */
+ @Override
+ public void afterTestMethod(TestContext testContext) throws Exception {
+ Class<?> testClass = testContext.getTestClass();
+ Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
+ Method testMethod = testContext.getTestMethod();
+ Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
+
+ final Class<DirtiesContext> annotationType = DirtiesContext.class;
+
+ boolean methodDirtiesContext = testMethod.isAnnotationPresent(annotationType);
+ boolean classDirtiesContext = testClass.isAnnotationPresent(annotationType);
+ DirtiesContext classDirtiesContextAnnotation = testClass.getAnnotation(annotationType);
+ ClassMode classMode = classDirtiesContext ? classDirtiesContextAnnotation.classMode() : null;
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("After test method: context [" + testContext + "], class dirties context ["
+ + classDirtiesContext + "], class mode [" + classMode + "], method dirties context ["
+ + methodDirtiesContext + "].");
+ }
+
+ if (methodDirtiesContext || (classDirtiesContext && classMode == ClassMode.AFTER_EACH_TEST_METHOD)) {
+ HierarchyMode hierarchyMode = methodDirtiesContext ? testMethod.getAnnotation(annotationType).hierarchyMode()
+ : classDirtiesContextAnnotation.hierarchyMode();
+ dirtyContext(testContext, hierarchyMode);
+ }
+ }
+
+ /**
+ * If the test class of the supplied {@linkplain TestContext test context} is
+ * annotated with {@link DirtiesContext &#064;DirtiesContext}, the
+ * {@linkplain ApplicationContext application context} of the test context will
+ * be {@linkplain TestContext#markApplicationContextDirty() marked as dirty} ,
+ * and the
+ * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE
+ * REINJECT_DEPENDENCIES_ATTRIBUTE} in the test context will be set to
+ * {@code true}.
+ */
+ @Override
+ public void afterTestClass(TestContext testContext) throws Exception {
+ Class<?> testClass = testContext.getTestClass();
+ Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
+
+ final Class<DirtiesContext> annotationType = DirtiesContext.class;
+
+ boolean dirtiesContext = testClass.isAnnotationPresent(annotationType);
+ if (logger.isDebugEnabled()) {
+ logger.debug("After test class: context [" + testContext + "], dirtiesContext [" + dirtiesContext + "].");
+ }
+ if (dirtiesContext) {
+ HierarchyMode hierarchyMode = testClass.getAnnotation(annotationType).hierarchyMode();
+ dirtyContext(testContext, hierarchyMode);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java
new file mode 100644
index 00000000..82368879
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java
@@ -0,0 +1,52 @@
+/*
+ * 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.context.support;
+
+import java.util.Properties;
+
+import org.springframework.beans.factory.support.BeanDefinitionReader;
+import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader;
+import org.springframework.context.support.GenericApplicationContext;
+
+/**
+ * Concrete implementation of {@link AbstractGenericContextLoader} that reads
+ * bean definitions from Java {@link Properties} resources.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ */
+public class GenericPropertiesContextLoader extends AbstractGenericContextLoader {
+
+ /**
+ * Creates a new {@link PropertiesBeanDefinitionReader}.
+ * @return a new PropertiesBeanDefinitionReader
+ * @see PropertiesBeanDefinitionReader
+ */
+ @Override
+ protected BeanDefinitionReader createBeanDefinitionReader(final GenericApplicationContext context) {
+ return new PropertiesBeanDefinitionReader(context);
+ }
+
+ /**
+ * Returns &quot;{@code -context.properties}&quot;.
+ */
+ @Override
+ public String getResourceSuffix() {
+ return "-context.properties";
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java
new file mode 100644
index 00000000..9c636e0e
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java
@@ -0,0 +1,50 @@
+/*
+ * 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.context.support;
+
+import org.springframework.beans.factory.support.BeanDefinitionReader;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.context.support.GenericApplicationContext;
+
+/**
+ * Concrete implementation of {@link AbstractGenericContextLoader} that reads
+ * bean definitions from XML resources.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ */
+public class GenericXmlContextLoader extends AbstractGenericContextLoader {
+
+ /**
+ * Create a new {@link XmlBeanDefinitionReader}.
+ * @return a new XmlBeanDefinitionReader
+ * @see XmlBeanDefinitionReader
+ */
+ @Override
+ protected BeanDefinitionReader createBeanDefinitionReader(final GenericApplicationContext context) {
+ return new XmlBeanDefinitionReader(context);
+ }
+
+ /**
+ * Returns &quot;{@code -context.xml}&quot;.
+ */
+ @Override
+ public String getResourceSuffix() {
+ return "-context.xml";
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/package-info.java b/spring-test/src/main/java/org/springframework/test/context/support/package-info.java
new file mode 100644
index 00000000..0212c4a0
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/support/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * <p>Support classes for the <em>Spring TestContext Framework</em>.</p>
+ */
+
+package org.springframework.test.context.support;
+
diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java
new file mode 100644
index 00000000..80c74a5b
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java
@@ -0,0 +1,210 @@
+/*
+ * 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.context.testng;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.TestContextManager;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
+import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
+import org.springframework.test.context.web.ServletTestExecutionListener;
+
+import org.testng.IHookCallBack;
+import org.testng.IHookable;
+import org.testng.ITestResult;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+/**
+ * Abstract base test class which integrates the <em>Spring TestContext Framework</em>
+ * with explicit {@link ApplicationContext} testing support in a <strong>TestNG</strong>
+ * environment.
+ *
+ * <p>Concrete subclasses:
+ * <ul>
+ * <li>Typically declare a class-level {@link ContextConfiguration
+ * &#064;ContextConfiguration} annotation to configure the {@link ApplicationContext
+ * application context} {@link ContextConfiguration#locations() resource locations}
+ * or {@link ContextConfiguration#classes() annotated classes}. <em>If your test
+ * does not need to load an application context, you may choose to omit the
+ * {@link ContextConfiguration &#064;ContextConfiguration} declaration and to
+ * configure the appropriate
+ * {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners}
+ * manually.</em></li>
+ * <li>Must have constructors which either implicitly or explicitly delegate to
+ * {@code super();}.</li>
+ * </ul>
+ *
+ * <p>The following {@link org.springframework.test.context.TestExecutionListener
+ * TestExecutionListeners} are configured by default:
+ *
+ * <ul>
+ * <li>{@link org.springframework.test.context.web.ServletTestExecutionListener}
+ * <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener}
+ * <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener}
+ * </ul>
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see ContextConfiguration
+ * @see TestContext
+ * @see TestContextManager
+ * @see TestExecutionListeners
+ * @see ServletTestExecutionListener
+ * @see DependencyInjectionTestExecutionListener
+ * @see DirtiesContextTestExecutionListener
+ * @see AbstractTransactionalTestNGSpringContextTests
+ * @see org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests
+ */
+@TestExecutionListeners({ ServletTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
+ DirtiesContextTestExecutionListener.class })
+public abstract class AbstractTestNGSpringContextTests implements IHookable, ApplicationContextAware {
+
+ /** Logger available to subclasses */
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ /**
+ * The {@link ApplicationContext} that was injected into this test instance
+ * via {@link #setApplicationContext(ApplicationContext)}.
+ */
+ protected ApplicationContext applicationContext;
+
+ private final TestContextManager testContextManager;
+
+ private Throwable testException;
+
+
+ /**
+ * Construct a new AbstractTestNGSpringContextTests instance and initialize
+ * the internal {@link TestContextManager} for the current test.
+ */
+ public AbstractTestNGSpringContextTests() {
+ this.testContextManager = new TestContextManager(getClass());
+ }
+
+ /**
+ * Set the {@link ApplicationContext} to be used by this test instance,
+ * provided via {@link ApplicationContextAware} semantics.
+ *
+ * @param applicationContext the applicationContext to set
+ */
+ public final void setApplicationContext(ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
+ }
+
+ /**
+ * Delegates to the configured {@link TestContextManager} to call
+ * {@link TestContextManager#beforeTestClass() 'before test class'}
+ * callbacks.
+ *
+ * @throws Exception if a registered TestExecutionListener throws an
+ * exception
+ */
+ @BeforeClass(alwaysRun = true)
+ protected void springTestContextBeforeTestClass() throws Exception {
+ this.testContextManager.beforeTestClass();
+ }
+
+ /**
+ * Delegates to the configured {@link TestContextManager} to
+ * {@link TestContextManager#prepareTestInstance(Object) prepare} this test
+ * instance prior to execution of any individual tests, for example for
+ * injecting dependencies, etc.
+ *
+ * @throws Exception if a registered TestExecutionListener throws an
+ * exception
+ */
+ @BeforeClass(alwaysRun = true, dependsOnMethods = "springTestContextBeforeTestClass")
+ protected void springTestContextPrepareTestInstance() throws Exception {
+ this.testContextManager.prepareTestInstance(this);
+ }
+
+ /**
+ * Delegates to the configured {@link TestContextManager} to
+ * {@link TestContextManager#beforeTestMethod(Object,Method) pre-process}
+ * the test method before the actual test is executed.
+ *
+ * @param testMethod the test method which is about to be executed.
+ * @throws Exception allows all exceptions to propagate.
+ */
+ @BeforeMethod(alwaysRun = true)
+ protected void springTestContextBeforeTestMethod(Method testMethod) throws Exception {
+ this.testContextManager.beforeTestMethod(this, testMethod);
+ }
+
+ /**
+ * Delegates to the {@link IHookCallBack#runTestMethod(ITestResult) test
+ * method} in the supplied {@code callback} to execute the actual test
+ * and then tracks the exception thrown during test execution, if any.
+ *
+ * @see org.testng.IHookable#run(org.testng.IHookCallBack,
+ * org.testng.ITestResult)
+ */
+ public void run(IHookCallBack callBack, ITestResult testResult) {
+ callBack.runTestMethod(testResult);
+
+ Throwable testResultException = testResult.getThrowable();
+ if (testResultException instanceof InvocationTargetException) {
+ testResultException = ((InvocationTargetException) testResultException).getCause();
+ }
+ this.testException = testResultException;
+ }
+
+ /**
+ * Delegates to the configured {@link TestContextManager} to
+ * {@link TestContextManager#afterTestMethod(Object, Method, Throwable)
+ * post-process} the test method after the actual test has executed.
+ *
+ * @param testMethod the test method which has just been executed on the
+ * test instance
+ * @throws Exception allows all exceptions to propagate
+ */
+ @AfterMethod(alwaysRun = true)
+ protected void springTestContextAfterTestMethod(Method testMethod) throws Exception {
+ try {
+ this.testContextManager.afterTestMethod(this, testMethod, this.testException);
+ }
+ finally {
+ this.testException = null;
+ }
+ }
+
+ /**
+ * Delegates to the configured {@link TestContextManager} to call
+ * {@link TestContextManager#afterTestClass() 'after test class'} callbacks.
+ *
+ * @throws Exception if a registered TestExecutionListener throws an
+ * exception
+ */
+ @AfterClass(alwaysRun = true)
+ protected void springTestContextAfterTestClass() throws Exception {
+ this.testContextManager.afterTestClass();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java
new file mode 100644
index 00000000..41e70a70
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java
@@ -0,0 +1,166 @@
+/*
+ * 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.context.testng;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
+import org.springframework.test.jdbc.JdbcTestUtils;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Abstract {@linkplain Transactional transactional} extension of
+ * {@link AbstractTestNGSpringContextTests} which adds convenience functionality
+ * for JDBC access. Expects a {@link DataSource} bean and a
+ * {@link PlatformTransactionManager} bean to be defined in the Spring
+ * {@linkplain ApplicationContext application context}.
+ *
+ * <p>This class exposes a {@link JdbcTemplate} and provides an easy way to
+ * {@linkplain #countRowsInTable count the number of rows in a table}
+ * (potentially {@linkplain #countRowsInTableWhere with a WHERE clause}),
+ * {@linkplain #deleteFromTables delete from tables},
+ * {@linkplain #dropTables drop tables}, and
+ * {@linkplain #executeSqlScript execute SQL scripts} within a transaction.
+ *
+ * <p>Concrete subclasses must fulfill the same requirements outlined in
+ * {@link AbstractTestNGSpringContextTests}.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see AbstractTestNGSpringContextTests
+ * @see org.springframework.test.context.ContextConfiguration
+ * @see org.springframework.test.context.TestExecutionListeners
+ * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
+ * @see org.springframework.test.context.transaction.TransactionConfiguration
+ * @see org.springframework.transaction.annotation.Transactional
+ * @see org.springframework.test.annotation.NotTransactional
+ * @see org.springframework.test.annotation.Rollback
+ * @see org.springframework.test.context.transaction.BeforeTransaction
+ * @see org.springframework.test.context.transaction.AfterTransaction
+ * @see org.springframework.test.jdbc.JdbcTestUtils
+ * @see org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
+ */
+@TestExecutionListeners(TransactionalTestExecutionListener.class)
+@Transactional
+@SuppressWarnings("deprecation")
+public abstract class AbstractTransactionalTestNGSpringContextTests extends AbstractTestNGSpringContextTests {
+
+ /**
+ * The {@code SimpleJdbcTemplate} that this base class manages, available to subclasses.
+ * @deprecated As of Spring 3.2, use {@link #jdbcTemplate} instead.
+ */
+ @Deprecated
+ protected SimpleJdbcTemplate simpleJdbcTemplate;
+
+ /**
+ * The {@code JdbcTemplate} that this base class manages, available to subclasses.
+ * @since 3.2
+ */
+ protected JdbcTemplate jdbcTemplate;
+
+ private String sqlScriptEncoding;
+
+
+ /**
+ * Set the {@code DataSource}, typically provided via Dependency Injection.
+ * <p>This method also instantiates the {@link #simpleJdbcTemplate} and
+ * {@link #jdbcTemplate} instance variables.
+ */
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ /**
+ * Specify the encoding for SQL scripts, if different from the platform encoding.
+ * @see #executeSqlScript
+ */
+ public void setSqlScriptEncoding(String sqlScriptEncoding) {
+ this.sqlScriptEncoding = sqlScriptEncoding;
+ }
+
+ /**
+ * Count the rows in the given table.
+ * @param tableName table name to count rows in
+ * @return the number of rows in the table
+ */
+ protected int countRowsInTable(String tableName) {
+ return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
+ }
+
+ /**
+ * Count the rows in the given table, using the provided {@code WHERE} clause.
+ * <p>See the Javadoc for {@link JdbcTestUtils#countRowsInTableWhere} for details.
+ * @param tableName the name of the table to count rows in
+ * @param whereClause the {@code WHERE} clause to append to the query
+ * @return the number of rows in the table that match the provided
+ * {@code WHERE} clause
+ * @since 3.2
+ */
+ protected int countRowsInTableWhere(String tableName, String whereClause) {
+ return JdbcTestUtils.countRowsInTableWhere(this.jdbcTemplate, tableName, whereClause);
+ }
+
+ /**
+ * Convenience method for deleting all rows from the specified tables. Use
+ * with caution outside of a transaction!
+ * @param names the names of the tables from which to delete
+ * @return the total number of rows deleted from all specified tables
+ */
+ protected int deleteFromTables(String... names) {
+ return JdbcTestUtils.deleteFromTables(this.jdbcTemplate, names);
+ }
+
+ /**
+ * Convenience method for dropping all of the specified tables. Use
+ * with caution outside of a transaction!
+ * @param names the names of the tables to drop
+ * @since 3.2
+ */
+ protected void dropTables(String... names) {
+ JdbcTestUtils.dropTables(this.jdbcTemplate, names);
+ }
+
+ /**
+ * Execute the given SQL script. Use with caution outside of a transaction!
+ * <p>The script will normally be loaded by classpath. There should be one
+ * statement per line. Any semicolons will be removed. <b>Do not use this
+ * method to execute DDL if you expect rollback.</b>
+ * @param sqlResourcePath the Spring resource path for the SQL script
+ * @param continueOnError whether or not to continue without throwing an
+ * exception in the event of an error
+ * @throws DataAccessException if there is an error executing a statement
+ * and continueOnError was {@code false}
+ */
+ protected void executeSqlScript(String sqlResourcePath, boolean continueOnError) throws DataAccessException {
+ Resource resource = this.applicationContext.getResource(sqlResourcePath);
+ JdbcTestUtils.executeSqlScript(this.jdbcTemplate, new EncodedResource(resource,
+ this.sqlScriptEncoding), continueOnError);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java b/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java
new file mode 100644
index 00000000..3d2045a4
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * <p>Support classes for ApplicationContext-based and transactional
+ * tests run with TestNG and the <em>Spring TestContext Framework</em>.</p>
+ */
+
+package org.springframework.test.context.testng;
+
diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java
new file mode 100644
index 00000000..bb25f1f8
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java
@@ -0,0 +1,45 @@
+/*
+ * 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.context.transaction;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Test annotation to indicate that the annotated {@code public void}
+ * method should be executed <em>after</em> a transaction is ended for test
+ * methods configured to run within a transaction via the
+ * {@code &#064;Transactional} annotation.
+ * </p>
+ * <p>
+ * The {@code &#064;AfterTransaction} methods of superclasses will be
+ * executed after those of the current class.
+ * </p>
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ * @see org.springframework.transaction.annotation.Transactional
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface AfterTransaction {
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java
new file mode 100644
index 00000000..d3625009
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java
@@ -0,0 +1,45 @@
+/*
+ * 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.context.transaction;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Test annotation to indicate that the annotated {@code public void}
+ * method should be executed <em>before</em> a transaction is started for test
+ * methods configured to run within a transaction via the
+ * {@code &#064;Transactional} annotation.
+ * </p>
+ * <p>
+ * The {@code &#064;BeforeTransaction} methods of superclasses will be
+ * executed before those of the current class.
+ * </p>
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ * @see org.springframework.transaction.annotation.Transactional
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface BeforeTransaction {
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java
new file mode 100644
index 00000000..7d91bcba
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java
@@ -0,0 +1,70 @@
+/*
+ * 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.context.transaction;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * {@code TransactionConfiguration} defines class-level metadata for configuring
+ * transactional tests.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ * @see TransactionalTestExecutionListener
+ * @see org.springframework.test.context.ContextConfiguration
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface TransactionConfiguration {
+
+ /**
+ * The bean name of the {@link org.springframework.transaction.PlatformTransactionManager
+ * PlatformTransactionManager} that should be used to drive transactions.
+ *
+ * <p>This attribute is not required and only needs to be declared if there
+ * are multiple beans of type {@code PlatformTransactionManager} in the test's
+ * {@code ApplicationContext} <em>and</em> if one of the following is true.
+ * <ul>
+ * <li>the bean name of the desired {@code PlatformTransactionManager} is not
+ * "transactionManager"</li>
+ * <li>{@link org.springframework.transaction.annotation.TransactionManagementConfigurer
+ * TransactionManagementConfigurer} was not implemented to specify which
+ * {@code PlatformTransactionManager} bean should be used for annotation-driven
+ * transaction management
+ * </ul>
+ *
+ * <p><b>NOTE:</b> The XML {@code <tx:annotation-driven>} element also refers
+ * to a bean named "transactionManager" by default. If you are using both
+ * features in combination, make sure to point to the same transaction manager
+ * bean - here in {@code @TransactionConfiguration} and also in
+ * {@code <tx:annotation-driven transaction-manager="...">}.
+ */
+ String transactionManager() default "transactionManager";
+
+ /**
+ * Should transactions be rolled back by default?
+ */
+ boolean defaultRollback() default true;
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java
new file mode 100644
index 00000000..c6124703
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2008 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.context.transaction;
+
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.util.Assert;
+
+/**
+ * Configuration attributes for configuring transactional tests.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see TransactionConfiguration
+ */
+public class TransactionConfigurationAttributes {
+
+ private final String transactionManagerName;
+
+ private final boolean defaultRollback;
+
+
+ /**
+ * Construct a new TransactionConfigurationAttributes instance from the
+ * supplied arguments.
+ * @param transactionManagerName the bean name of the
+ * {@link PlatformTransactionManager} that is to be used to drive transactions
+ * @param defaultRollback whether or not transactions should be rolled back by default
+ */
+ public TransactionConfigurationAttributes(String transactionManagerName, boolean defaultRollback) {
+ Assert.notNull(transactionManagerName, "transactionManagerName can not be null");
+ this.transactionManagerName = transactionManagerName;
+ this.defaultRollback = defaultRollback;
+ }
+
+
+ /**
+ * Get the bean name of the {@link PlatformTransactionManager} that is to
+ * be used to drive transactions.
+ */
+ public final String getTransactionManagerName() {
+ return this.transactionManagerName;
+ }
+
+ /**
+ * Whether or not transactions should be rolled back by default.
+ * @return the <em>default rollback</em> flag
+ */
+ public final boolean isDefaultRollback() {
+ return this.defaultRollback;
+ }
+
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this)
+ .append("transactionManagerName", this.transactionManagerName)
+ .append("defaultRollback", this.defaultRollback)
+ .toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java
new file mode 100644
index 00000000..5535fa97
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java
@@ -0,0 +1,596 @@
+/*
+ * 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.context.transaction;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.test.annotation.NotTransactional;
+import org.springframework.test.annotation.Rollback;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.support.AbstractTestExecutionListener;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionException;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
+import org.springframework.transaction.annotation.TransactionManagementConfigurer;
+import org.springframework.transaction.interceptor.DelegatingTransactionAttribute;
+import org.springframework.transaction.interceptor.TransactionAttribute;
+import org.springframework.transaction.interceptor.TransactionAttributeSource;
+import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@code TestExecutionListener} that provides support for executing tests
+ * within transactions by honoring the
+ * {@link org.springframework.transaction.annotation.Transactional &#064;Transactional}
+ * and {@link NotTransactional &#064;NotTransactional} annotations. Expects a
+ * {@link PlatformTransactionManager} bean to be defined in the Spring
+ * {@link ApplicationContext} for the test.
+ *
+ * <p>Changes to the database during a test that is run with {@code @Transactional}
+ * will be run within a transaction that will, by default, be automatically
+ * <em>rolled back</em> after completion of the test; whereas, changes to the
+ * database during a test that is run with {@code @NotTransactional} will
+ * <strong>not</strong> be run within a transaction. Test methods that are not
+ * annotated with {@code @Transactional} (at the class or method level) will not
+ * be run within a transaction.
+ *
+ * <p>Transactional commit and rollback behavior can be configured via the
+ * class-level {@link TransactionConfiguration @TransactionConfiguration} and
+ * method-level {@link Rollback @Rollback} annotations.
+ *
+ * <p>In case there are multiple instances of {@code PlatformTransactionManager}
+ * within the test's {@code ApplicationContext}, {@code @TransactionConfiguration}
+ * supports configuring the bean name of the {@code PlatformTransactionManager}
+ * that should be used to drive transactions. Alternatively,
+ * {@link TransactionManagementConfigurer} can be implemented in an
+ * {@link org.springframework.context.annotation.Configuration @Configuration}
+ * class.
+ *
+ * <p>When executing transactional tests, it is sometimes useful to be able to
+ * execute certain <em>set up</em> or <em>tear down</em> code outside of a
+ * transaction. {@code TransactionalTestExecutionListener} provides such
+ * support for methods annotated with
+ * {@link BeforeTransaction @BeforeTransaction} and
+ * {@link AfterTransaction @AfterTransaction}.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see TransactionConfiguration
+ * @see TransactionManagementConfigurer
+ * @see org.springframework.transaction.annotation.Transactional
+ * @see org.springframework.test.annotation.NotTransactional
+ * @see org.springframework.test.annotation.Rollback
+ * @see BeforeTransaction
+ * @see AfterTransaction
+ */
+@SuppressWarnings("deprecation")
+public class TransactionalTestExecutionListener extends AbstractTestExecutionListener {
+
+ private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class);
+
+ private static final String DEFAULT_TRANSACTION_MANAGER_NAME = (String) AnnotationUtils.getDefaultValue(
+ TransactionConfiguration.class, "transactionManager");
+
+ private static final Boolean DEFAULT_DEFAULT_ROLLBACK = (Boolean) AnnotationUtils.getDefaultValue(
+ TransactionConfiguration.class, "defaultRollback");
+
+ protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource();
+
+ private final Map<Method, TransactionContext> transactionContextCache =
+ new ConcurrentHashMap<Method, TransactionContext>(8);
+
+ private TransactionConfigurationAttributes configurationAttributes;
+
+ private volatile int transactionsStarted = 0;
+
+
+ /**
+ * If the test method of the supplied {@link TestContext test context} is
+ * configured to run within a transaction, this method will run
+ * {@link BeforeTransaction &#064;BeforeTransaction methods} and start a new
+ * transaction.
+ * <p>Note that if a {@code @BeforeTransaction} method fails, any remaining
+ * {@code @BeforeTransaction} methods will not be invoked, and a transaction
+ * will not be started.
+ * @see org.springframework.transaction.annotation.Transactional
+ * @see org.springframework.test.annotation.NotTransactional
+ * @see #getTransactionManager(TestContext, String)
+ */
+ @SuppressWarnings("serial")
+ @Override
+ public void beforeTestMethod(TestContext testContext) throws Exception {
+ final Method testMethod = testContext.getTestMethod();
+ Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
+
+ if (this.transactionContextCache.remove(testMethod) != null) {
+ throw new IllegalStateException("Cannot start new transaction without ending existing transaction: "
+ + "Invoke endTransaction() before startNewTransaction().");
+ }
+
+ if (testMethod.isAnnotationPresent(NotTransactional.class)) {
+ return;
+ }
+
+ PlatformTransactionManager tm = null;
+ TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod,
+ testContext.getTestClass());
+
+ if (transactionAttribute != null) {
+ transactionAttribute = new DelegatingTransactionAttribute(transactionAttribute) {
+
+ public String getName() {
+ return testMethod.getName();
+ }
+ };
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Explicit transaction definition [" + transactionAttribute + "] found for test context "
+ + testContext);
+ }
+ tm = getTransactionManager(testContext, transactionAttribute.getQualifier());
+ }
+
+ if (tm != null) {
+ TransactionContext txContext = new TransactionContext(tm, transactionAttribute);
+ runBeforeTransactionMethods(testContext);
+ startNewTransaction(testContext, txContext);
+ this.transactionContextCache.put(testMethod, txContext);
+ }
+ }
+
+ /**
+ * If a transaction is currently active for the test method of the supplied
+ * {@link TestContext test context}, this method will end the transaction
+ * and run {@link AfterTransaction &#064;AfterTransaction methods}.
+ * <p>{@code @AfterTransaction} methods are guaranteed to be
+ * invoked even if an error occurs while ending the transaction.
+ */
+ @Override
+ public void afterTestMethod(TestContext testContext) throws Exception {
+ Method testMethod = testContext.getTestMethod();
+ Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
+
+ // If the transaction is still active...
+ TransactionContext txContext = this.transactionContextCache.remove(testMethod);
+ if (txContext != null && !txContext.transactionStatus.isCompleted()) {
+ try {
+ endTransaction(testContext, txContext);
+ }
+ finally {
+ runAfterTransactionMethods(testContext);
+ }
+ }
+ }
+
+ /**
+ * Run all {@link BeforeTransaction &#064;BeforeTransaction methods} for the
+ * specified {@link TestContext test context}. If one of the methods fails,
+ * however, the caught exception will be rethrown in a wrapped
+ * {@link RuntimeException}, and the remaining methods will <strong>not</strong>
+ * be given a chance to execute.
+ * @param testContext the current test context
+ */
+ protected void runBeforeTransactionMethods(TestContext testContext) throws Exception {
+ try {
+ List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), BeforeTransaction.class);
+ Collections.reverse(methods);
+ for (Method method : methods) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Executing @BeforeTransaction method [" + method + "] for test context " + testContext);
+ }
+ method.invoke(testContext.getTestInstance());
+ }
+ }
+ catch (InvocationTargetException ex) {
+ logger.error("Exception encountered while executing @BeforeTransaction methods for test context "
+ + testContext + ".", ex.getTargetException());
+ ReflectionUtils.rethrowException(ex.getTargetException());
+ }
+ }
+
+ /**
+ * Run all {@link AfterTransaction &#064;AfterTransaction methods} for the
+ * specified {@link TestContext test context}. If one of the methods fails,
+ * the caught exception will be logged as an error, and the remaining
+ * methods will be given a chance to execute. After all methods have
+ * executed, the first caught exception, if any, will be rethrown.
+ * @param testContext the current test context
+ */
+ protected void runAfterTransactionMethods(TestContext testContext) throws Exception {
+ Throwable afterTransactionException = null;
+
+ List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), AfterTransaction.class);
+ for (Method method : methods) {
+ try {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Executing @AfterTransaction method [" + method + "] for test context " + testContext);
+ }
+ method.invoke(testContext.getTestInstance());
+ }
+ catch (InvocationTargetException ex) {
+ Throwable targetException = ex.getTargetException();
+ if (afterTransactionException == null) {
+ afterTransactionException = targetException;
+ }
+ logger.error("Exception encountered while executing @AfterTransaction method [" + method
+ + "] for test context " + testContext, targetException);
+ }
+ catch (Exception ex) {
+ if (afterTransactionException == null) {
+ afterTransactionException = ex;
+ }
+ logger.error("Exception encountered while executing @AfterTransaction method [" + method
+ + "] for test context " + testContext, ex);
+ }
+ }
+
+ if (afterTransactionException != null) {
+ ReflectionUtils.rethrowException(afterTransactionException);
+ }
+ }
+
+ /**
+ * Start a new transaction for the supplied {@link TestContext test context}.
+ * <p>Only call this method if {@link #endTransaction} has been called or if no
+ * transaction has been previously started.
+ * @param testContext the current test context
+ * @throws TransactionException if starting the transaction fails
+ * @throws Exception if an error occurs while retrieving the transaction manager
+ */
+ private void startNewTransaction(TestContext testContext, TransactionContext txContext) throws Exception {
+ txContext.startTransaction();
+ ++this.transactionsStarted;
+ if (logger.isInfoEnabled()) {
+ logger.info("Began transaction (" + this.transactionsStarted + "): transaction manager ["
+ + txContext.transactionManager + "]; rollback [" + isRollback(testContext) + "]");
+ }
+ }
+
+ /**
+ * Immediately force a <em>commit</em> or <em>rollback</em> of the
+ * transaction for the supplied {@link TestContext test context}, according
+ * to the commit and rollback flags.
+ * @param testContext the current test context
+ * @throws Exception if an error occurs while retrieving the transaction manager
+ */
+ private void endTransaction(TestContext testContext, TransactionContext txContext) throws Exception {
+ boolean rollback = isRollback(testContext);
+ if (logger.isTraceEnabled()) {
+ logger.trace("Ending transaction for test context " + testContext + "; transaction manager ["
+ + txContext.transactionStatus + "]; rollback [" + rollback + "]");
+ }
+ txContext.endTransaction(rollback);
+ if (logger.isInfoEnabled()) {
+ logger.info((rollback ? "Rolled back" : "Committed")
+ + " transaction after test execution for test context " + testContext);
+ }
+ }
+
+ /**
+ * Get the {@link PlatformTransactionManager transaction manager} to use
+ * for the supplied {@link TestContext test context} and {@code qualifier}.
+ * <p>Delegates to {@link #getTransactionManager(TestContext)} if the
+ * supplied {@code qualifier} is {@code null} or empty.
+ * @param testContext the test context for which the transaction manager
+ * should be retrieved
+ * @param qualifier the qualifier for selecting between multiple bean matches;
+ * may be {@code null} or empty
+ * @return the transaction manager to use, or {@code null} if not found
+ * @throws BeansException if an error occurs while retrieving the transaction manager
+ * @see #getTransactionManager(TestContext)
+ */
+ protected final PlatformTransactionManager getTransactionManager(TestContext testContext, String qualifier) {
+ // look up by type and qualifier from @Transactional
+ if (StringUtils.hasText(qualifier)) {
+ try {
+ // Use autowire-capable factory in order to support extended qualifier
+ // matching (only exposed on the internal BeanFactory, not on the
+ // ApplicationContext).
+ BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory();
+
+ return BeanFactoryAnnotationUtils.qualifiedBeanOfType(bf, PlatformTransactionManager.class, qualifier);
+ } catch (RuntimeException ex) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Caught exception while retrieving transaction manager for test context " + testContext
+ + " and qualifier [" + qualifier + "]", ex);
+ }
+ throw ex;
+ }
+ }
+
+ // else
+ return getTransactionManager(testContext);
+ }
+
+ /**
+ * Get the {@link PlatformTransactionManager transaction manager} to use
+ * for the supplied {@link TestContext test context}.
+ * @param testContext the test context for which the transaction manager
+ * should be retrieved
+ * @return the transaction manager to use, or {@code null} if not found
+ * @throws BeansException if an error occurs while retrieving the transaction manager
+ * @see #getTransactionManager(TestContext, String)
+ */
+ protected final PlatformTransactionManager getTransactionManager(TestContext testContext) {
+ BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory();
+ String tmName = retrieveConfigurationAttributes(testContext).getTransactionManagerName();
+
+ try {
+ // look up by type and explicit name from @TransactionConfiguration
+ if (StringUtils.hasText(tmName) && !DEFAULT_TRANSACTION_MANAGER_NAME.equals(tmName)) {
+ return bf.getBean(tmName, PlatformTransactionManager.class);
+ }
+
+ if (bf instanceof ListableBeanFactory) {
+ ListableBeanFactory lbf = (ListableBeanFactory) bf;
+
+ // look up single bean by type
+ Map<String, PlatformTransactionManager> txMgrs = BeanFactoryUtils.beansOfTypeIncludingAncestors(
+ lbf, PlatformTransactionManager.class);
+ if (txMgrs.size() == 1) {
+ return txMgrs.values().iterator().next();
+ }
+
+ // look up single TransactionManagementConfigurer
+ Map<String, TransactionManagementConfigurer> configurers = BeanFactoryUtils.beansOfTypeIncludingAncestors(
+ lbf, TransactionManagementConfigurer.class);
+ if (configurers.size() > 1) {
+ throw new IllegalStateException(
+ "Only one TransactionManagementConfigurer may exist in the ApplicationContext");
+ }
+ if (configurers.size() == 1) {
+ return configurers.values().iterator().next().annotationDrivenTransactionManager();
+ }
+ }
+
+ // look up by type and default name from @TransactionConfiguration
+ return bf.getBean(DEFAULT_TRANSACTION_MANAGER_NAME, PlatformTransactionManager.class);
+
+ } catch (BeansException ex) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Caught exception while retrieving transaction manager for test context " + testContext, ex);
+ }
+ throw ex;
+ }
+ }
+
+ /**
+ * Determine whether or not to rollback transactions by default for the
+ * supplied {@link TestContext test context}.
+ * @param testContext the test context for which the default rollback flag
+ * should be retrieved
+ * @return the <em>default rollback</em> flag for the supplied test context
+ * @throws Exception if an error occurs while determining the default rollback flag
+ */
+ protected final boolean isDefaultRollback(TestContext testContext) throws Exception {
+ return retrieveConfigurationAttributes(testContext).isDefaultRollback();
+ }
+
+ /**
+ * Determine whether or not to rollback transactions for the supplied
+ * {@link TestContext test context} by taking into consideration the
+ * {@link #isDefaultRollback(TestContext) default rollback} flag and a
+ * possible method-level override via the {@link Rollback} annotation.
+ * @param testContext the test context for which the rollback flag
+ * should be retrieved
+ * @return the <em>rollback</em> flag for the supplied test context
+ * @throws Exception if an error occurs while determining the rollback flag
+ */
+ protected final boolean isRollback(TestContext testContext) throws Exception {
+ boolean rollback = isDefaultRollback(testContext);
+ Rollback rollbackAnnotation = testContext.getTestMethod().getAnnotation(Rollback.class);
+ if (rollbackAnnotation != null) {
+ boolean rollbackOverride = rollbackAnnotation.value();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Method-level @Rollback(" + rollbackOverride + ") overrides default rollback [" + rollback
+ + "] for test context " + testContext);
+ }
+ rollback = rollbackOverride;
+ }
+ else {
+ if (logger.isDebugEnabled()) {
+ logger.debug("No method-level @Rollback override: using default rollback [" + rollback
+ + "] for test context " + testContext);
+ }
+ }
+ return rollback;
+ }
+
+ /**
+ * Gets all superclasses of the supplied {@link Class class}, including the
+ * class itself. The ordering of the returned list will begin with the
+ * supplied class and continue up the class hierarchy.
+ * <p>Note: This code has been borrowed from
+ * {@link org.junit.internal.runners.TestClass#getSuperClasses(Class)} and
+ * adapted.
+ * @param clazz the class for which to retrieve the superclasses.
+ * @return all superclasses of the supplied class.
+ */
+ private List<Class<?>> getSuperClasses(Class<?> clazz) {
+ ArrayList<Class<?>> results = new ArrayList<Class<?>>();
+ Class<?> current = clazz;
+ while (current != null) {
+ results.add(current);
+ current = current.getSuperclass();
+ }
+ return results;
+ }
+
+ /**
+ * Gets all methods in the supplied {@link Class class} and its superclasses
+ * which are annotated with the supplied {@code annotationType} but
+ * which are not <em>shadowed</em> by methods overridden in subclasses.
+ * <p>Note: This code has been borrowed from
+ * {@link org.junit.internal.runners.TestClass#getAnnotatedMethods(Class)}
+ * and adapted.
+ * @param clazz the class for which to retrieve the annotated methods
+ * @param annotationType the annotation type for which to search
+ * @return all annotated methods in the supplied class and its superclasses
+ */
+ private List<Method> getAnnotatedMethods(Class<?> clazz, Class<? extends Annotation> annotationType) {
+ List<Method> results = new ArrayList<Method>();
+ for (Class<?> eachClass : getSuperClasses(clazz)) {
+ Method[] methods = eachClass.getDeclaredMethods();
+ for (Method eachMethod : methods) {
+ Annotation annotation = eachMethod.getAnnotation(annotationType);
+ if (annotation != null && !isShadowed(eachMethod, results)) {
+ results.add(eachMethod);
+ }
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Determines if the supplied {@link Method method} is <em>shadowed</em>
+ * by a method in supplied {@link List list} of previous methods.
+ * <p>Note: This code has been borrowed from
+ * {@link org.junit.internal.runners.TestClass#isShadowed(Method, List)}.
+ * @param method the method to check for shadowing
+ * @param previousMethods the list of methods which have previously been processed
+ * @return {@code true} if the supplied method is shadowed by a
+ * method in the {@code previousMethods} list
+ */
+ private boolean isShadowed(Method method, List<Method> previousMethods) {
+ for (Method each : previousMethods) {
+ if (isShadowed(method, each)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines if the supplied {@link Method current method} is
+ * <em>shadowed</em> by a {@link Method previous method}.
+ * <p>Note: This code has been borrowed from
+ * {@link org.junit.internal.runners.TestClass#isShadowed(Method, Method)}.
+ * @param current the current method
+ * @param previous the previous method
+ * @return {@code true} if the previous method shadows the current one
+ */
+ private boolean isShadowed(Method current, Method previous) {
+ if (!previous.getName().equals(current.getName())) {
+ return false;
+ }
+ if (previous.getParameterTypes().length != current.getParameterTypes().length) {
+ return false;
+ }
+ for (int i = 0; i < previous.getParameterTypes().length; i++) {
+ if (!previous.getParameterTypes()[i].equals(current.getParameterTypes()[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Retrieves the {@link TransactionConfigurationAttributes} for the
+ * specified {@link Class class} which may optionally declare or inherit
+ * {@link TransactionConfiguration &#064;TransactionConfiguration}. If
+ * {@code @TransactionConfiguration} is not present for the supplied
+ * class, the <em>default values</em> for attributes defined in
+ * {@code @TransactionConfiguration} will be used instead.
+ * @param testContext the test context for which the configuration
+ * attributes should be retrieved
+ * @return a new TransactionConfigurationAttributes instance
+ */
+ private TransactionConfigurationAttributes retrieveConfigurationAttributes(TestContext testContext) {
+ if (this.configurationAttributes == null) {
+ Class<?> clazz = testContext.getTestClass();
+ TransactionConfiguration config = clazz.getAnnotation(TransactionConfiguration.class);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Retrieved @TransactionConfiguration [" + config + "] for test class [" + clazz + "]");
+ }
+
+ String transactionManagerName;
+ boolean defaultRollback;
+ if (config != null) {
+ transactionManagerName = config.transactionManager();
+ defaultRollback = config.defaultRollback();
+ }
+ else {
+ transactionManagerName = DEFAULT_TRANSACTION_MANAGER_NAME;
+ defaultRollback = DEFAULT_DEFAULT_ROLLBACK;
+ }
+
+ TransactionConfigurationAttributes configAttributes = new TransactionConfigurationAttributes(
+ transactionManagerName, defaultRollback);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Retrieved TransactionConfigurationAttributes " + configAttributes + " for class ["
+ + clazz + "]");
+ }
+ this.configurationAttributes = configAttributes;
+ }
+ return this.configurationAttributes;
+ }
+
+
+ /**
+ * Internal context holder for a specific test method.
+ */
+ private static class TransactionContext {
+
+ private final PlatformTransactionManager transactionManager;
+
+ private final TransactionDefinition transactionDefinition;
+
+ private TransactionStatus transactionStatus;
+
+
+ public TransactionContext(PlatformTransactionManager transactionManager,
+ TransactionDefinition transactionDefinition) {
+ this.transactionManager = transactionManager;
+ this.transactionDefinition = transactionDefinition;
+ }
+
+ public void startTransaction() {
+ this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
+ }
+
+ public void endTransaction(boolean rollback) {
+ if (rollback) {
+ this.transactionManager.rollback(this.transactionStatus);
+ }
+ else {
+ this.transactionManager.commit(this.transactionStatus);
+ }
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java b/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java
new file mode 100644
index 00000000..3c685db1
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * <p>Transactional support classes for the <em>Spring TestContext
+ * Framework</em>.</p>
+ */
+
+package org.springframework.test.context.transaction;
+
diff --git a/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java
new file mode 100644
index 00000000..75c363dd
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2002-2013 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.context.web;
+
+import javax.servlet.ServletContext;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigUtils;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.FileSystemResourceLoader;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.mock.web.MockServletContext;
+import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.test.context.support.AbstractContextLoader;
+import org.springframework.util.Assert;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.GenericWebApplicationContext;
+
+/**
+ * Abstract, generic extension of {@link AbstractContextLoader} that loads a
+ * {@link GenericWebApplicationContext}.
+ *
+ * <p>If instances of concrete subclasses are invoked via the
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}
+ * SPI, the context will be loaded from the {@link MergedContextConfiguration}
+ * provided to {@link #loadContext(MergedContextConfiguration)}. In such cases, a
+ * {@code SmartContextLoader} will decide whether to load the context from
+ * <em>locations</em> or <em>annotated classes</em>. Note that {@code
+ * AbstractGenericWebContextLoader} does not support the {@code
+ * loadContext(String... locations)} method from the legacy
+ * {@link org.springframework.test.context.ContextLoader ContextLoader} SPI.
+ *
+ * <p>Concrete subclasses must provide an appropriate implementation of
+ * {@link #loadBeanDefinitions}.
+ *
+ * @author Sam Brannen
+ * @since 3.2
+ * @see #loadContext(MergedContextConfiguration)
+ * @see #loadContext(String...)
+ */
+public abstract class AbstractGenericWebContextLoader extends AbstractContextLoader {
+
+ private static final Log logger = LogFactory.getLog(AbstractGenericWebContextLoader.class);
+
+
+ // --- SmartContextLoader -----------------------------------------------
+
+ /**
+ * Load a Spring {@link WebApplicationContext} from the supplied
+ * {@link MergedContextConfiguration}.
+ *
+ * <p>Implementation details:
+ *
+ * <ul>
+ * <li>Creates a {@link GenericWebApplicationContext} instance.</li>
+ * <li>If the supplied {@code MergedContextConfiguration} references a
+ * {@linkplain MergedContextConfiguration#getParent() parent configuration},
+ * the corresponding {@link MergedContextConfiguration#getParentApplicationContext()
+ * ApplicationContext} will be retrieved and
+ * {@linkplain GenericWebApplicationContext#setParent(ApplicationContext) set as the parent}
+ * for the context created by this method.</li>
+ * <li>Delegates to {@link #configureWebResources} to create the
+ * {@link MockServletContext} and set it in the {@code WebApplicationContext}.</li>
+ * <li>Calls {@link #prepareContext} to allow for customizing the context
+ * before bean definitions are loaded.</li>
+ * <li>Calls {@link #customizeBeanFactory} to allow for customizing the
+ * context's {@code DefaultListableBeanFactory}.</li>
+ * <li>Delegates to {@link #loadBeanDefinitions} to populate the context
+ * from the locations or classes in the supplied {@code MergedContextConfiguration}.</li>
+ * <li>Delegates to {@link AnnotationConfigUtils} for
+ * {@linkplain AnnotationConfigUtils#registerAnnotationConfigProcessors registering}
+ * annotation configuration processors.</li>
+ * <li>Calls {@link #customizeContext} to allow for customizing the context
+ * before it is refreshed.</li>
+ * <li>{@link ConfigurableApplicationContext#refresh Refreshes} the
+ * context and registers a JVM shutdown hook for it.</li>
+ * </ul>
+ *
+ * @return a new web application context
+ * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration)
+ * @see GenericWebApplicationContext
+ */
+ public final ConfigurableApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
+
+ if (!(mergedConfig instanceof WebMergedContextConfiguration)) {
+ throw new IllegalArgumentException(String.format(
+ "Cannot load WebApplicationContext from non-web merged context configuration %s. "
+ + "Consider annotating your test class with @WebAppConfiguration.", mergedConfig));
+ }
+ WebMergedContextConfiguration webMergedConfig = (WebMergedContextConfiguration) mergedConfig;
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Loading WebApplicationContext for merged context configuration %s.",
+ webMergedConfig));
+ }
+
+ GenericWebApplicationContext context = new GenericWebApplicationContext();
+
+ ApplicationContext parent = mergedConfig.getParentApplicationContext();
+ if (parent != null) {
+ context.setParent(parent);
+ }
+ configureWebResources(context, webMergedConfig);
+ prepareContext(context, webMergedConfig);
+ customizeBeanFactory(context.getDefaultListableBeanFactory(), webMergedConfig);
+ loadBeanDefinitions(context, webMergedConfig);
+ AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
+ customizeContext(context, webMergedConfig);
+ context.refresh();
+ context.registerShutdownHook();
+ return context;
+ }
+
+ /**
+ * Configures web resources for the supplied web application context (WAC).
+ *
+ * <h4>Implementation Details</h4>
+ *
+ * <p>If the supplied WAC has no parent or its parent is not a WAC, the
+ * supplied WAC will be configured as the Root WAC (see "<em>Root WAC
+ * Configuration</em>" below).
+ *
+ * <p>Otherwise the context hierarchy of the supplied WAC will be traversed
+ * to find the top-most WAC (i.e., the root); and the {@link ServletContext}
+ * of the Root WAC will be set as the {@code ServletContext} for the supplied
+ * WAC.
+ *
+ * <h4>Root WAC Configuration</h4>
+ *
+ * <ul>
+ * <li>The resource base path is retrieved from the supplied
+ * {@code WebMergedContextConfiguration}.</li>
+ * <li>A {@link ResourceLoader} is instantiated for the {@link MockServletContext}:
+ * if the resource base path is prefixed with "{@code classpath:}", a
+ * {@link DefaultResourceLoader} will be used; otherwise, a
+ * {@link FileSystemResourceLoader} will be used.</li>
+ * <li>A {@code MockServletContext} will be created using the resource base
+ * path and resource loader.</li>
+ * <li>The supplied {@link GenericWebApplicationContext} is then stored in
+ * the {@code MockServletContext} under the
+ * {@link WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE} key.</li>
+ * <li>Finally, the {@code MockServletContext} is set in the
+ * {@code WebApplicationContext}.</li>
+ *
+ * @param context the web application context for which to configure the web
+ * resources
+ * @param webMergedConfig the merged context configuration to use to load the
+ * web application context
+ */
+ protected void configureWebResources(GenericWebApplicationContext context,
+ WebMergedContextConfiguration webMergedConfig) {
+
+ ApplicationContext parent = context.getParent();
+
+ // if the WAC has no parent or the parent is not a WAC, set the WAC as
+ // the Root WAC:
+ if (parent == null || (!(parent instanceof WebApplicationContext))) {
+ String resourceBasePath = webMergedConfig.getResourceBasePath();
+ ResourceLoader resourceLoader = resourceBasePath.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX) ? new DefaultResourceLoader()
+ : new FileSystemResourceLoader();
+
+ ServletContext servletContext = new MockServletContext(resourceBasePath, resourceLoader);
+ servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context);
+ context.setServletContext(servletContext);
+ }
+ else {
+ ServletContext servletContext = null;
+
+ // find the Root WAC
+ while (parent != null) {
+ if (parent instanceof WebApplicationContext && !(parent.getParent() instanceof WebApplicationContext)) {
+ servletContext = ((WebApplicationContext) parent).getServletContext();
+ break;
+ }
+ parent = parent.getParent();
+ }
+ Assert.state(servletContext != null, "Failed to find Root WebApplicationContext in the context hierarchy");
+ context.setServletContext(servletContext);
+ }
+ }
+
+ /**
+ * Customize the internal bean factory of the {@code WebApplicationContext}
+ * created by this context loader.
+ *
+ * <p>The default implementation is empty but can be overridden in subclasses
+ * to customize {@code DefaultListableBeanFactory}'s standard settings.
+ *
+ * @param beanFactory the bean factory created by this context loader
+ * @param webMergedConfig the merged context configuration to use to load the
+ * web application context
+ * @see #loadContext(MergedContextConfiguration)
+ * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding
+ * @see DefaultListableBeanFactory#setAllowEagerClassLoading
+ * @see DefaultListableBeanFactory#setAllowCircularReferences
+ * @see DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping
+ */
+ protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory,
+ WebMergedContextConfiguration webMergedConfig) {
+ }
+
+ /**
+ * Load bean definitions into the supplied {@link GenericWebApplicationContext context}
+ * from the locations or classes in the supplied {@code WebMergedContextConfiguration}.
+ *
+ * <p>Concrete subclasses must provide an appropriate implementation.
+ *
+ * @param context the context into which the bean definitions should be loaded
+ * @param webMergedConfig the merged context configuration to use to load the
+ * web application context
+ * @see #loadContext(MergedContextConfiguration)
+ */
+ protected abstract void loadBeanDefinitions(GenericWebApplicationContext context,
+ WebMergedContextConfiguration webMergedConfig);
+
+ /**
+ * Customize the {@link GenericWebApplicationContext} created by this context
+ * loader <i>after</i> bean definitions have been loaded into the context but
+ * <i>before</i> the context is refreshed.
+ *
+ * <p>The default implementation is empty but can be overridden in subclasses
+ * to customize the web application context.
+ *
+ * @param context the newly created web application context
+ * @param webMergedConfig the merged context configuration to use to load the
+ * web application context
+ * @see #loadContext(MergedContextConfiguration)
+ */
+ protected void customizeContext(GenericWebApplicationContext context, WebMergedContextConfiguration webMergedConfig) {
+ }
+
+ // --- ContextLoader -------------------------------------------------------
+
+ /**
+ * {@code AbstractGenericWebContextLoader} should be used as a
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader},
+ * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ * Consequently, this method is not supported.
+ *
+ * @see org.springframework.test.context.ContextLoader#loadContext(java.lang.String[])
+ * @throws UnsupportedOperationException
+ */
+ public final ApplicationContext loadContext(String... locations) throws Exception {
+ throw new UnsupportedOperationException(
+ "AbstractGenericWebContextLoader does not support the loadContext(String... locations) method");
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java
new file mode 100644
index 00000000..05acb463
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java
@@ -0,0 +1,172 @@
+/*
+ * 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.context.web;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.support.AnnotationConfigContextLoaderUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.context.support.GenericWebApplicationContext;
+
+/**
+ * Concrete implementation of {@link AbstractGenericWebContextLoader} that loads
+ * bean definitions from annotated classes.
+ *
+ * <p>See the Javadoc for
+ * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration}
+ * for a definition of <em>annotated class</em>.
+ *
+ * <p>Note: {@code AnnotationConfigWebContextLoader} supports <em>annotated classes</em>
+ * rather than the String-based resource locations defined by the legacy
+ * {@link org.springframework.test.context.ContextLoader ContextLoader} API. Thus,
+ * although {@code AnnotationConfigWebContextLoader} extends
+ * {@code AbstractGenericWebContextLoader}, {@code AnnotationConfigWebContextLoader}
+ * does <em>not</em> support any String-based methods defined by
+ * {@link org.springframework.test.context.support.AbstractContextLoader
+ * AbstractContextLoader} or {@code AbstractGenericWebContextLoader}.
+ * Consequently, {@code AnnotationConfigWebContextLoader} should chiefly be
+ * considered a {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}
+ * rather than a {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ *
+ * @author Sam Brannen
+ * @since 3.2
+ * @see #processContextConfiguration(ContextConfigurationAttributes)
+ * @see #detectDefaultConfigurationClasses(Class)
+ * @see #loadBeanDefinitions(GenericWebApplicationContext, WebMergedContextConfiguration)
+ */
+public class AnnotationConfigWebContextLoader extends AbstractGenericWebContextLoader {
+
+ private static final Log logger = LogFactory.getLog(AnnotationConfigWebContextLoader.class);
+
+
+ // --- SmartContextLoader -----------------------------------------------
+
+ /**
+ * Process <em>annotated classes</em> in the supplied {@link ContextConfigurationAttributes}.
+ *
+ * <p>If the <em>annotated classes</em> are {@code null} or empty and
+ * {@link #isGenerateDefaultLocations()} returns {@code true}, this
+ * {@code SmartContextLoader} will attempt to {@linkplain
+ * #detectDefaultConfigurationClasses detect default configuration classes}.
+ * If defaults are detected they will be
+ * {@linkplain ContextConfigurationAttributes#setClasses(Class[]) set} in the
+ * supplied configuration attributes. Otherwise, properties in the supplied
+ * configuration attributes will not be modified.
+ *
+ * @param configAttributes the context configuration attributes to process
+ * @see org.springframework.test.context.SmartContextLoader#processContextConfiguration(ContextConfigurationAttributes)
+ * @see #isGenerateDefaultLocations()
+ * @see #detectDefaultConfigurationClasses(Class)
+ */
+ public void processContextConfiguration(ContextConfigurationAttributes configAttributes) {
+ if (ObjectUtils.isEmpty(configAttributes.getClasses()) && isGenerateDefaultLocations()) {
+ Class<?>[] defaultConfigClasses = detectDefaultConfigurationClasses(configAttributes.getDeclaringClass());
+ configAttributes.setClasses(defaultConfigClasses);
+ }
+ }
+
+ /**
+ * Detect the default configuration classes for the supplied test class.
+ *
+ * <p>The default implementation simply delegates to
+ * {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses(Class)}.
+ *
+ * @param declaringClass the test class that declared {@code @ContextConfiguration}
+ * @return an array of default configuration classes, potentially empty but
+ * never {@code null}
+ * @see AnnotationConfigContextLoaderUtils
+ */
+ protected Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) {
+ return AnnotationConfigContextLoaderUtils.detectDefaultConfigurationClasses(declaringClass);
+ }
+
+ // --- AbstractContextLoader -----------------------------------------------
+
+ /**
+ * {@code AnnotationConfigWebContextLoader} should be used as a
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader},
+ * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ * Consequently, this method is not supported.
+ *
+ * @see org.springframework.test.context.support.AbstractContextLoader#modifyLocations
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ protected String[] modifyLocations(Class<?> clazz, String... locations) {
+ throw new UnsupportedOperationException(
+ "AnnotationConfigWebContextLoader does not support the modifyLocations(Class, String...) method");
+ }
+
+ /**
+ * {@code AnnotationConfigWebContextLoader} should be used as a
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader},
+ * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ * Consequently, this method is not supported.
+ *
+ * @see org.springframework.test.context.support.AbstractContextLoader#generateDefaultLocations
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ protected String[] generateDefaultLocations(Class<?> clazz) {
+ throw new UnsupportedOperationException(
+ "AnnotationConfigWebContextLoader does not support the generateDefaultLocations(Class) method");
+ }
+
+ /**
+ * {@code AnnotationConfigWebContextLoader} should be used as a
+ * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader},
+ * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}.
+ * Consequently, this method is not supported.
+ *
+ * @see org.springframework.test.context.support.AbstractContextLoader#getResourceSuffix
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ protected String getResourceSuffix() {
+ throw new UnsupportedOperationException(
+ "AnnotationConfigWebContextLoader does not support the getResourceSuffix() method");
+ }
+
+ // --- AbstractGenericWebContextLoader -------------------------------------
+
+ /**
+ * Register classes in the supplied {@linkplain GenericWebApplicationContext context}
+ * from the classes in the supplied {@link WebMergedContextConfiguration}.
+ *
+ * <p>Each class must represent an <em>annotated class</em>. An
+ * {@link AnnotatedBeanDefinitionReader} is used to register the appropriate
+ * bean definitions.
+ *
+ * @param context the context in which the annotated classes should be registered
+ * @param webMergedConfig the merged configuration from which the classes should be retrieved
+ *
+ * @see AbstractGenericWebContextLoader#loadBeanDefinitions
+ */
+ @Override
+ protected void loadBeanDefinitions(GenericWebApplicationContext context,
+ WebMergedContextConfiguration webMergedConfig) {
+ Class<?>[] annotatedClasses = webMergedConfig.getClasses();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Registering annotated classes: " + ObjectUtils.nullSafeToString(annotatedClasses));
+ }
+ new AnnotatedBeanDefinitionReader(context).register(annotatedClasses);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java
new file mode 100644
index 00000000..0bf295e0
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java
@@ -0,0 +1,48 @@
+/*
+ * 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.context.web;
+
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.web.context.support.GenericWebApplicationContext;
+
+/**
+ * Concrete implementation of {@link AbstractGenericWebContextLoader} that loads
+ * bean definitions from XML resources.
+ *
+ * @author Sam Brannen
+ * @since 3.2
+ */
+public class GenericXmlWebContextLoader extends AbstractGenericWebContextLoader {
+
+ /**
+ * Loads bean definitions using an {@link XmlBeanDefinitionReader}.
+ * @see AbstractGenericWebContextLoader#loadBeanDefinitions
+ */
+ @Override
+ protected void loadBeanDefinitions(GenericWebApplicationContext context,
+ WebMergedContextConfiguration webMergedConfig) {
+ new XmlBeanDefinitionReader(context).loadBeanDefinitions(webMergedConfig.getLocations());
+ }
+
+ /**
+ * Returns &quot;{@code -context.xml}&quot;.
+ */
+ protected String getResourceSuffix() {
+ return "-context.xml";
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java
new file mode 100644
index 00000000..5166f8ce
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java
@@ -0,0 +1,194 @@
+/*
+ * 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.context.web;
+
+import javax.servlet.ServletContext;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.Conventions;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockServletContext;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.TestExecutionListener;
+import org.springframework.test.context.support.AbstractTestExecutionListener;
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
+import org.springframework.util.Assert;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletWebRequest;
+
+/**
+ * {@code TestExecutionListener} which provides mock Servlet API support to
+ * {@link WebApplicationContext WebApplicationContexts} loaded by the <em>Spring
+ * TestContext Framework</em>.
+ *
+ * <p>Specifically, {@code ServletTestExecutionListener} sets up thread-local
+ * state via Spring Web's {@link RequestContextHolder} during {@linkplain
+ * #prepareTestInstance(TestContext) test instance preparation} and {@linkplain
+ * #beforeTestMethod(TestContext) before each test method} and creates a {@link
+ * MockHttpServletRequest}, {@link MockHttpServletResponse}, and
+ * {@link ServletWebRequest} based on the {@link MockServletContext} present in
+ * the {@code WebApplicationContext}. This listener also ensures that the
+ * {@code MockHttpServletResponse} and {@code ServletWebRequest} can be injected
+ * into the test instance, and once the test is complete this listener {@linkplain
+ * #afterTestMethod(TestContext) cleans up} thread-local state.
+ *
+ * <p>Note that {@code ServletTestExecutionListener} is enabled by default but
+ * generally takes no action if the {@linkplain TestContext#getTestClass() test
+ * class} is not annotated with {@link WebAppConfiguration @WebAppConfiguration}.
+ * See the Javadoc for individual methods in this class for details.
+ *
+ * @author Sam Brannen
+ * @since 3.2
+ */
+public class ServletTestExecutionListener extends AbstractTestExecutionListener {
+
+ /**
+ * Attribute name for a {@link TestContext} attribute which indicates
+ * whether or not the {@code ServletTestExecutionListener} should {@linkplain
+ * RequestContextHolder#resetRequestAttributes() reset} Spring Web's
+ * {@code RequestContextHolder} in {@link #afterTestMethod(TestContext)}.
+ *
+ * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
+ */
+ public static final String RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName(
+ ServletTestExecutionListener.class, "resetRequestContextHolder");
+
+ /**
+ * Attribute name for a {@link TestContext} attribute which indicates that
+ * {@code ServletTestExecutionListener} has already populated Spring Web's
+ * {@code RequestContextHolder}.
+ *
+ * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
+ */
+ public static final String POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName(
+ ServletTestExecutionListener.class, "populatedRequestContextHolder");
+
+ private static final Log logger = LogFactory.getLog(ServletTestExecutionListener.class);
+
+
+ /**
+ * Sets up thread-local state during the <em>test instance preparation</em>
+ * callback phase via Spring Web's {@link RequestContextHolder}, but only if
+ * the {@linkplain TestContext#getTestClass() test class} is annotated with
+ * {@link WebAppConfiguration @WebAppConfiguration}.
+ *
+ * @see TestExecutionListener#prepareTestInstance(TestContext)
+ * @see #setUpRequestContextIfNecessary(TestContext)
+ */
+ @SuppressWarnings("javadoc")
+ public void prepareTestInstance(TestContext testContext) throws Exception {
+ setUpRequestContextIfNecessary(testContext);
+ }
+
+ /**
+ * Sets up thread-local state before each test method via Spring Web's
+ * {@link RequestContextHolder}, but only if the
+ * {@linkplain TestContext#getTestClass() test class} is annotated with
+ * {@link WebAppConfiguration @WebAppConfiguration}.
+ *
+ * @see TestExecutionListener#beforeTestMethod(TestContext)
+ * @see #setUpRequestContextIfNecessary(TestContext)
+ */
+ @SuppressWarnings("javadoc")
+ public void beforeTestMethod(TestContext testContext) throws Exception {
+ setUpRequestContextIfNecessary(testContext);
+ }
+
+ /**
+ * If the {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} in the supplied
+ * {@code TestContext} has a value of {@link Boolean#TRUE}, this method will
+ * (1) clean up thread-local state after each test method by {@linkplain
+ * RequestContextHolder#resetRequestAttributes() resetting} Spring Web's
+ * {@code RequestContextHolder} and (2) ensure that new mocks are injected
+ * into the test instance for subsequent tests by setting the
+ * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
+ * in the test context to {@code true}.
+ *
+ * <p>The {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} and
+ * {@link #POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} will be subsequently
+ * removed from the test context, regardless of their values.
+ *
+ * @see TestExecutionListener#afterTestMethod(TestContext)
+ */
+ public void afterTestMethod(TestContext testContext) throws Exception {
+ if (Boolean.TRUE.equals(testContext.getAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE))) {
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Resetting RequestContextHolder for test context %s.", testContext));
+ }
+ RequestContextHolder.resetRequestAttributes();
+ testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE,
+ Boolean.TRUE);
+ }
+ testContext.removeAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE);
+ testContext.removeAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE);
+ }
+
+ private boolean notAnnotatedWithWebAppConfiguration(TestContext testContext) {
+ return AnnotationUtils.findAnnotation(testContext.getTestClass(), WebAppConfiguration.class) == null;
+ }
+
+ private boolean alreadyPopulatedRequestContextHolder(TestContext testContext) {
+ return Boolean.TRUE.equals(testContext.getAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE));
+ }
+
+ private void setUpRequestContextIfNecessary(TestContext testContext) {
+ if (notAnnotatedWithWebAppConfiguration(testContext) || alreadyPopulatedRequestContextHolder(testContext)) {
+ return;
+ }
+
+ ApplicationContext context = testContext.getApplicationContext();
+
+ if (context instanceof WebApplicationContext) {
+ WebApplicationContext wac = (WebApplicationContext) context;
+ ServletContext servletContext = wac.getServletContext();
+ Assert.state(servletContext instanceof MockServletContext, String.format(
+ "The WebApplicationContext for test context %s must be configured with a MockServletContext.",
+ testContext));
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format(
+ "Setting up MockHttpServletRequest, MockHttpServletResponse, ServletWebRequest, and RequestContextHolder for test context %s.",
+ testContext));
+ }
+
+ MockServletContext mockServletContext = (MockServletContext) servletContext;
+ MockHttpServletRequest request = new MockHttpServletRequest(mockServletContext);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ ServletWebRequest servletWebRequest = new ServletWebRequest(request, response);
+
+ RequestContextHolder.setRequestAttributes(servletWebRequest);
+ testContext.setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
+ testContext.setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
+
+ if (wac instanceof ConfigurableApplicationContext) {
+ @SuppressWarnings("resource")
+ ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) wac;
+ ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory();
+ bf.registerResolvableDependency(MockHttpServletResponse.class, response);
+ bf.registerResolvableDependency(ServletWebRequest.class, servletWebRequest);
+ }
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java
new file mode 100644
index 00000000..7f24c266
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java
@@ -0,0 +1,66 @@
+/*
+ * 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.context.web;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * {@code @WebAppConfiguration} is a class-level annotation that is used to
+ * declare that the {@code ApplicationContext} loaded for an integration test
+ * should be a {@link org.springframework.web.context.WebApplicationContext
+ * WebApplicationContext}.
+ *
+ * <p>The mere presence of {@code @WebAppConfiguration} on a test class ensures
+ * that a {@code WebApplicationContext} will be loaded for the test using a default
+ * for the path to the root of the web application. To override the default,
+ * specify an explicit resource path via the {@link #value} attribute.
+ *
+ * <p>Note that {@code @WebAppConfiguration} must be used in conjunction with
+ * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration},
+ * either within a single test class or within a test class hierarchy.
+ *
+ * @author Sam Brannen
+ * @since 3.2
+ * @see org.springframework.web.context.WebApplicationContext
+ * @see org.springframework.test.context.ContextConfiguration
+ * @see ServletTestExecutionListener
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface WebAppConfiguration {
+
+ /**
+ * The resource path to the root directory of the web application.
+ *
+ * <p>A path that does not include a Spring resource prefix (e.g., {@code classpath:},
+ * {@code file:}, etc.) will be interpreted as a file system resource, and a
+ * path should not end with a slash.
+ *
+ * <p>Defaults to {@code "src/main/webapp"} as a file system resource. Note
+ * that this is the standard directory for the root of a web application in
+ * a project that follows the standard Maven project layout for a WAR.
+ */
+ String value() default "src/main/webapp";
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java
new file mode 100644
index 00000000..fa561c00
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java
@@ -0,0 +1,48 @@
+/*
+ * 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.context.web;
+
+import org.springframework.test.context.SmartContextLoader;
+import org.springframework.test.context.support.AbstractDelegatingSmartContextLoader;
+
+/**
+ * {@code WebDelegatingSmartContextLoader} is a concrete implementation of
+ * {@link AbstractDelegatingSmartContextLoader} that delegates to a
+ * {@link GenericXmlWebContextLoader} and an {@link AnnotationConfigWebContextLoader}.
+ *
+ * @author Sam Brannen
+ * @since 3.2
+ * @see SmartContextLoader
+ * @see AbstractDelegatingSmartContextLoader
+ * @see GenericXmlWebContextLoader
+ * @see AnnotationConfigWebContextLoader
+ */
+public class WebDelegatingSmartContextLoader extends AbstractDelegatingSmartContextLoader {
+
+ private final SmartContextLoader xmlLoader = new GenericXmlWebContextLoader();
+ private final SmartContextLoader annotationConfigLoader = new AnnotationConfigWebContextLoader();
+
+
+ protected SmartContextLoader getXmlLoader() {
+ return this.xmlLoader;
+ }
+
+ protected SmartContextLoader getAnnotationConfigLoader() {
+ return this.annotationConfigLoader;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java
new file mode 100644
index 00000000..58e8b386
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2002-2013 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.context.web;
+
+import java.util.Set;
+
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.test.context.CacheAwareContextLoaderDelegate;
+import org.springframework.test.context.ContextLoader;
+import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@code WebMergedContextConfiguration} encapsulates the <em>merged</em>
+ * context configuration declared on a test class and all of its superclasses
+ * via {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration},
+ * {@link WebAppConfiguration @WebAppConfiguration}, and
+ * {@link org.springframework.test.context.ActiveProfiles @ActiveProfiles}.
+ *
+ * <p>{@code WebMergedContextConfiguration} extends the contract of
+ * {@link MergedContextConfiguration} by adding support for the {@link
+ * #getResourceBasePath() resource base path} configured via {@code @WebAppConfiguration}.
+ * This allows the {@link org.springframework.test.context.TestContext TestContext}
+ * to properly cache the corresponding {@link
+ * org.springframework.web.context.WebApplicationContext WebApplicationContext}
+ * that was loaded using properties of this {@code WebMergedContextConfiguration}.
+ *
+ * @author Sam Brannen
+ * @since 3.2
+ * @see WebAppConfiguration
+ * @see MergedContextConfiguration
+ * @see org.springframework.test.context.ContextConfiguration
+ * @see org.springframework.test.context.ActiveProfiles
+ * @see org.springframework.test.context.ContextConfigurationAttributes
+ * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration)
+ */
+public class WebMergedContextConfiguration extends MergedContextConfiguration {
+
+ private static final long serialVersionUID = 7323361588604247458L;
+
+ private final String resourceBasePath;
+
+
+ /**
+ * Create a new {@code WebMergedContextConfiguration} instance for the
+ * supplied test class, resource locations, annotated classes, context
+ * initializers, active profiles, resource base path, and {@code ContextLoader}.
+ *
+ * <p>If a {@code null} value is supplied for {@code locations},
+ * {@code classes}, or {@code activeProfiles} an empty array will
+ * be stored instead. If a {@code null} value is supplied for the
+ * {@code contextInitializerClasses} an empty set will be stored instead.
+ * If an <em>empty</em> value is supplied for the {@code resourceBasePath}
+ * an empty string will be used. Furthermore, active profiles will be sorted,
+ * and duplicate profiles will be removed.
+ *
+ * @param testClass the test class for which the configuration was merged
+ * @param locations the merged resource locations
+ * @param classes the merged annotated classes
+ * @param contextInitializerClasses the merged context initializer classes
+ * @param activeProfiles the merged active bean definition profiles
+ * @param resourceBasePath the resource path to the root directory of the web application
+ * @param contextLoader the resolved {@code ContextLoader}
+ * @see #WebMergedContextConfiguration(Class, String[], Class[], Set, String[], String, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)
+ * @deprecated as of Spring 3.2.2, use
+ * {@link #WebMergedContextConfiguration(Class, String[], Class[], Set, String[], String, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} instead.
+ */
+ @Deprecated
+ public WebMergedContextConfiguration(
+ Class<?> testClass,
+ String[] locations,
+ Class<?>[] classes,
+ Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
+ String[] activeProfiles, String resourceBasePath, ContextLoader contextLoader) {
+
+ this(testClass, locations, classes, contextInitializerClasses, activeProfiles, resourceBasePath, contextLoader,
+ null, null);
+ }
+
+ /**
+ * Create a new {@code WebMergedContextConfiguration} instance for the
+ * supplied test class, resource locations, annotated classes, context
+ * initializers, active profiles, resource base path, and {@code ContextLoader}.
+ *
+ * <p>If a {@code null} value is supplied for {@code locations},
+ * {@code classes}, or {@code activeProfiles} an empty array will
+ * be stored instead. If a {@code null} value is supplied for the
+ * {@code contextInitializerClasses} an empty set will be stored instead.
+ * If an <em>empty</em> value is supplied for the {@code resourceBasePath}
+ * an empty string will be used. Furthermore, active profiles will be sorted,
+ * and duplicate profiles will be removed.
+ *
+ * @param testClass the test class for which the configuration was merged
+ * @param locations the merged resource locations
+ * @param classes the merged annotated classes
+ * @param contextInitializerClasses the merged context initializer classes
+ * @param activeProfiles the merged active bean definition profiles
+ * @param resourceBasePath the resource path to the root directory of the web application
+ * @param contextLoader the resolved {@code ContextLoader}
+ * @param cacheAwareContextLoaderDelegate a cache-aware context loader
+ * delegate with which to retrieve the parent context
+ * @param parent the parent configuration or {@code null} if there is no parent
+ * @since 3.2.2
+ */
+ public WebMergedContextConfiguration(
+ Class<?> testClass,
+ String[] locations,
+ Class<?>[] classes,
+ Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
+ String[] activeProfiles, String resourceBasePath, ContextLoader contextLoader,
+ CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) {
+
+ super(testClass, locations, classes, contextInitializerClasses, activeProfiles, contextLoader,
+ cacheAwareContextLoaderDelegate, parent);
+
+ this.resourceBasePath = !StringUtils.hasText(resourceBasePath) ? "" : resourceBasePath;
+ }
+
+ /**
+ * Get the resource path to the root directory of the web application for the
+ * {@linkplain #getTestClass() test class}, configured via {@code @WebAppConfiguration}.
+ * @see WebAppConfiguration
+ */
+ public String getResourceBasePath() {
+ return this.resourceBasePath;
+ }
+
+ /**
+ * Generate a unique hash code for all properties of this
+ * {@code WebMergedContextConfiguration} excluding the
+ * {@linkplain #getTestClass() test class}.
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + resourceBasePath.hashCode();
+ return result;
+ }
+
+ /**
+ * Determine if the supplied object is equal to this {@code WebMergedContextConfiguration}
+ * instance by comparing both object's {@linkplain #getLocations() locations},
+ * {@linkplain #getClasses() annotated classes},
+ * {@linkplain #getContextInitializerClasses() context initializer classes},
+ * {@linkplain #getActiveProfiles() active profiles},
+ * {@linkplain #getResourceBasePath() resource base path},
+ * {@linkplain #getParent() parents}, and the fully qualified names of their
+ * {@link #getContextLoader() ContextLoaders}.
+ */
+ @Override
+ public boolean equals(Object obj) {
+
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof WebMergedContextConfiguration)) {
+ return false;
+ }
+
+ final WebMergedContextConfiguration that = (WebMergedContextConfiguration) obj;
+
+ return super.equals(that) && this.getResourceBasePath().equals(that.getResourceBasePath());
+ }
+
+ /**
+ * Provide a String representation of the {@linkplain #getTestClass() test class},
+ * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes},
+ * {@linkplain #getContextInitializerClasses() context initializer classes},
+ * {@linkplain #getActiveProfiles() active profiles},
+ * {@linkplain #getResourceBasePath() resource base path}, the name of the
+ * {@link #getContextLoader() ContextLoader}, and the
+ * {@linkplain #getParent() parent configuration}.
+ */
+ @Override
+ public String toString() {
+ return new ToStringCreator(this)//
+ .append("testClass", getTestClass())//
+ .append("locations", ObjectUtils.nullSafeToString(getLocations()))//
+ .append("classes", ObjectUtils.nullSafeToString(getClasses()))//
+ .append("contextInitializerClasses", ObjectUtils.nullSafeToString(getContextInitializerClasses()))//
+ .append("activeProfiles", ObjectUtils.nullSafeToString(getActiveProfiles()))//
+ .append("resourceBasePath", getResourceBasePath())//
+ .append("contextLoader", nullSafeToString(getContextLoader()))//
+ .append("parent", getParent())//
+ .toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/web/package-info.java b/spring-test/src/main/java/org/springframework/test/context/web/package-info.java
new file mode 100644
index 00000000..ebf00203
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/web/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Web support classes for the <em>Spring TestContext Framework</em>.
+ */
+
+package org.springframework.test.context.web;
+
diff --git a/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java b/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java
new file mode 100644
index 00000000..383b20df
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2002-2013 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.jdbc;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.util.LinkedList;
+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.core.io.ResourceLoader;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.SqlParameterValue;
+import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@code JdbcTestUtils} is a collection of JDBC related utility functions
+ * intended to simplify standard database testing scenarios.
+ *
+ * <p>As of Spring 3.1.3, {@code JdbcTestUtils} supersedes {@link SimpleJdbcTestUtils}.
+ *
+ * @author Thomas Risberg
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @author Phillip Webb
+ * @since 2.5.4
+ */
+public class JdbcTestUtils {
+
+ private static final Log logger = LogFactory.getLog(JdbcTestUtils.class);
+
+ private static final String DEFAULT_COMMENT_PREFIX = "--";
+
+ private static final char DEFAULT_STATEMENT_SEPARATOR = ';';
+
+
+ /**
+ * Count the rows in the given table.
+ * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations
+ * @param tableName name of the table to count rows in
+ * @return the number of rows in the table
+ */
+ public static int countRowsInTable(JdbcTemplate jdbcTemplate, String tableName) {
+ return jdbcTemplate.queryForObject("SELECT COUNT(0) FROM " + tableName, Integer.class);
+ }
+
+ /**
+ * Count the rows in the given table, using the provided {@code WHERE} clause.
+ * <p>If the provided {@code WHERE} clause contains text, it will be prefixed
+ * with {@code " WHERE "} and then appended to the generated {@code SELECT}
+ * statement. For example, if the provided table name is {@code "person"} and
+ * the provided where clause is {@code "name = 'Bob' and age > 25"}, the
+ * resulting SQL statement to execute will be
+ * {@code "SELECT COUNT(0) FROM person WHERE name = 'Bob' and age > 25"}.
+ * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations
+ * @param tableName the name of the table to count rows in
+ * @param whereClause the {@code WHERE} clause to append to the query
+ * @return the number of rows in the table that match the provided
+ * {@code WHERE} clause
+ */
+ public static int countRowsInTableWhere(JdbcTemplate jdbcTemplate, String tableName, String whereClause) {
+ String sql = "SELECT COUNT(0) FROM " + tableName;
+ if (StringUtils.hasText(whereClause)) {
+ sql += " WHERE " + whereClause;
+ }
+ return jdbcTemplate.queryForObject(sql, Integer.class);
+ }
+
+ /**
+ * Delete all rows from the specified tables.
+ * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations
+ * @param tableNames the names of the tables to delete from
+ * @return the total number of rows deleted from all specified tables
+ */
+ public static int deleteFromTables(JdbcTemplate jdbcTemplate, String... tableNames) {
+ int totalRowCount = 0;
+ for (String tableName : tableNames) {
+ int rowCount = jdbcTemplate.update("DELETE FROM " + tableName);
+ totalRowCount += rowCount;
+ if (logger.isInfoEnabled()) {
+ logger.info("Deleted " + rowCount + " rows from table " + tableName);
+ }
+ }
+ return totalRowCount;
+ }
+
+ /**
+ * Delete rows from the given table, using the provided {@code WHERE} clause.
+ * <p>If the provided {@code WHERE} clause contains text, it will be prefixed
+ * with {@code " WHERE "} and then appended to the generated {@code DELETE}
+ * statement. For example, if the provided table name is {@code "person"} and
+ * the provided where clause is {@code "name = 'Bob' and age > 25"}, the
+ * resulting SQL statement to execute will be
+ * {@code "DELETE FROM person WHERE name = 'Bob' and age > 25"}.
+ * <p>As an alternative to hard-coded values, the {@code "?"} placeholder can
+ * be used within the {@code WHERE} clause, binding to the given arguments.
+ * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations
+ * @param tableName the name of the table to delete rows from
+ * @param whereClause the {@code WHERE} clause to append to the query
+ * @param args arguments to bind to the query (leaving it to the PreparedStatement
+ * to guess the corresponding SQL type); may also contain {@link SqlParameterValue}
+ * objects which indicate not only the argument value but also the SQL type and
+ * optionally the scale.
+ * @return the number of rows deleted from the table
+ */
+ public static int deleteFromTableWhere(JdbcTemplate jdbcTemplate, String tableName,
+ String whereClause, Object... args) {
+ String sql = "DELETE FROM " + tableName;
+ if (StringUtils.hasText(whereClause)) {
+ sql += " WHERE " + whereClause;
+ }
+ int rowCount = (args != null && args.length > 0 ? jdbcTemplate.update(sql, args)
+ : jdbcTemplate.update(sql));
+ if (logger.isInfoEnabled()) {
+ logger.info("Deleted " + rowCount + " rows from table " + tableName);
+ }
+ return rowCount;
+ }
+
+ /**
+ * Drop the specified tables.
+ * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations
+ * @param tableNames the names of the tables to drop
+ */
+ public static void dropTables(JdbcTemplate jdbcTemplate, String... tableNames) {
+ for (String tableName : tableNames) {
+ jdbcTemplate.execute("DROP TABLE " + tableName);
+ if (logger.isInfoEnabled()) {
+ logger.info("Dropped table " + tableName);
+ }
+ }
+ }
+
+ /**
+ * Execute the given SQL script.
+ * <p>The script will typically be loaded from the classpath. There should
+ * be one statement per line. Any semicolons and line comments will be removed.
+ * <p><b>Do not use this method to execute DDL if you expect rollback.</b>
+ * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations
+ * @param resourceLoader the resource loader with which to load the SQL script
+ * @param sqlResourcePath the Spring resource path for the SQL script
+ * @param continueOnError whether or not to continue without throwing an
+ * exception in the event of an error
+ * @throws DataAccessException if there is an error executing a statement
+ * and {@code continueOnError} is {@code false}
+ * @see ResourceDatabasePopulator
+ * @see #executeSqlScript(JdbcTemplate, Resource, boolean)
+ */
+ public static void executeSqlScript(JdbcTemplate jdbcTemplate, ResourceLoader resourceLoader,
+ String sqlResourcePath, boolean continueOnError) throws DataAccessException {
+ Resource resource = resourceLoader.getResource(sqlResourcePath);
+ executeSqlScript(jdbcTemplate, resource, continueOnError);
+ }
+
+ /**
+ * Execute the given SQL script.
+ * <p>The script will typically be loaded from the classpath. Statements
+ * should be delimited with a semicolon. If statements are not delimited with
+ * a semicolon then there should be one statement per line. Statements are
+ * allowed to span lines only if they are delimited with a semicolon. Any
+ * line comments will be removed.
+ * <p><b>Do not use this method to execute DDL if you expect rollback.</b>
+ * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations
+ * @param resource the resource to load the SQL script from
+ * @param continueOnError whether or not to continue without throwing an
+ * exception in the event of an error
+ * @throws DataAccessException if there is an error executing a statement
+ * and {@code continueOnError} is {@code false}
+ * @see ResourceDatabasePopulator
+ * @see #executeSqlScript(JdbcTemplate, EncodedResource, boolean)
+ */
+ public static void executeSqlScript(JdbcTemplate jdbcTemplate, Resource resource, boolean continueOnError)
+ throws DataAccessException {
+ executeSqlScript(jdbcTemplate, new EncodedResource(resource), continueOnError);
+ }
+
+ /**
+ * Execute the given SQL script.
+ * <p>The script will typically be loaded from the classpath. There should
+ * be one statement per line. Any semicolons and line comments will be removed.
+ * <p><b>Do not use this method to execute DDL if you expect rollback.</b>
+ * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations
+ * @param resource the resource (potentially associated with a specific encoding)
+ * to load the SQL script from
+ * @param continueOnError whether or not to continue without throwing an
+ * exception in the event of an error
+ * @throws DataAccessException if there is an error executing a statement
+ * and {@code continueOnError} is {@code false}
+ * @see ResourceDatabasePopulator
+ */
+ public static void executeSqlScript(JdbcTemplate jdbcTemplate, EncodedResource resource, boolean continueOnError)
+ throws DataAccessException {
+
+ if (logger.isInfoEnabled()) {
+ logger.info("Executing SQL script from " + resource);
+ }
+ long startTime = System.currentTimeMillis();
+ List<String> statements = new LinkedList<String>();
+ LineNumberReader reader = null;
+ try {
+ reader = new LineNumberReader(resource.getReader());
+ String script = readScript(reader);
+ char delimiter = DEFAULT_STATEMENT_SEPARATOR;
+ if (!containsSqlScriptDelimiters(script, delimiter)) {
+ delimiter = '\n';
+ }
+ splitSqlScript(script, delimiter, statements);
+ int lineNumber = 0;
+ for (String statement : statements) {
+ lineNumber++;
+ try {
+ int rowsAffected = jdbcTemplate.update(statement);
+ if (logger.isDebugEnabled()) {
+ logger.debug(rowsAffected + " rows affected by SQL: " + statement);
+ }
+ }
+ catch (DataAccessException ex) {
+ if (continueOnError) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Failed to execute SQL script statement at line " + lineNumber
+ + " of resource " + resource + ": " + statement, ex);
+ }
+ }
+ else {
+ throw ex;
+ }
+ }
+ }
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format("Executed SQL script from %s in %s ms.", resource, elapsedTime));
+ }
+ }
+ catch (IOException ex) {
+ throw new DataAccessResourceFailureException("Failed to open SQL script from " + resource, ex);
+ }
+ finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * Read a script from the provided {@code LineNumberReader}, using
+ * "{@code --}" as the comment prefix, and build a {@code String} containing
+ * the lines.
+ * @param lineNumberReader the {@code LineNumberReader} containing the script
+ * to be processed
+ * @return a {@code String} containing the script lines
+ * @see #readScript(LineNumberReader, String)
+ */
+ public static String readScript(LineNumberReader lineNumberReader) throws IOException {
+ return readScript(lineNumberReader, DEFAULT_COMMENT_PREFIX);
+ }
+
+ /**
+ * Read a script from the provided {@code LineNumberReader}, using the supplied
+ * comment prefix, and build a {@code String} containing the lines.
+ * <p>Lines <em>beginning</em> with the comment prefix are excluded from the
+ * results; however, line comments anywhere else &mdash; for example, within
+ * a statement &mdash; will be included in the results.
+ * @param lineNumberReader the {@code LineNumberReader} containing the script
+ * to be processed
+ * @param commentPrefix the prefix that identifies comments in the SQL script &mdash; typically "--"
+ * @return a {@code String} containing the script lines
+ */
+ public static String readScript(LineNumberReader lineNumberReader, String commentPrefix) throws IOException {
+ String currentStatement = lineNumberReader.readLine();
+ StringBuilder scriptBuilder = new StringBuilder();
+ while (currentStatement != null) {
+ if (StringUtils.hasText(currentStatement)
+ && (commentPrefix != null && !currentStatement.startsWith(commentPrefix))) {
+ if (scriptBuilder.length() > 0) {
+ scriptBuilder.append('\n');
+ }
+ scriptBuilder.append(currentStatement);
+ }
+ currentStatement = lineNumberReader.readLine();
+ }
+ return scriptBuilder.toString();
+ }
+
+ /**
+ * Determine if the provided SQL script contains the specified delimiter.
+ * @param script the SQL script
+ * @param delim character delimiting each statement &mdash; typically a ';' character
+ * @return {@code true} if the script contains the delimiter; {@code false} otherwise
+ */
+ public static boolean containsSqlScriptDelimiters(String script, char delim) {
+ boolean inLiteral = false;
+ char[] content = script.toCharArray();
+ for (int i = 0; i < script.length(); i++) {
+ if (content[i] == '\'') {
+ inLiteral = !inLiteral;
+ }
+ if (content[i] == delim && !inLiteral) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Split an SQL script into separate statements delimited by the provided
+ * delimiter character. Each individual statement will be added to the
+ * provided {@code List}.
+ * <p>Within a statement, "{@code --}" will be used as the comment prefix;
+ * any text beginning with the comment prefix and extending to the end of
+ * the line will be omitted from the statement. In addition, multiple adjacent
+ * whitespace characters will be collapsed into a single space.
+ * @param script the SQL script
+ * @param delim character delimiting each statement &mdash; typically a ';' character
+ * @param statements the list that will contain the individual statements
+ */
+ public static void splitSqlScript(String script, char delim, List<String> statements) {
+ splitSqlScript(script, "" + delim, DEFAULT_COMMENT_PREFIX, statements);
+ }
+
+ /**
+ * Split an SQL script into separate statements delimited by the provided
+ * delimiter string. Each individual statement will be added to the provided
+ * {@code List}.
+ * <p>Within a statement, the provided {@code commentPrefix} will be honored;
+ * any text beginning with the comment prefix and extending to the end of the
+ * line will be omitted from the statement. In addition, multiple adjacent
+ * whitespace characters will be collapsed into a single space.
+ * @param script the SQL script
+ * @param delim character delimiting each statement &mdash; typically a ';' character
+ * @param commentPrefix the prefix that identifies line comments in the SQL script &mdash; typically "--"
+ * @param statements the List that will contain the individual statements
+ */
+ private static void splitSqlScript(String script, String delim, String commentPrefix, List<String> statements) {
+ StringBuilder sb = new StringBuilder();
+ boolean inLiteral = false;
+ boolean inEscape = false;
+ char[] content = script.toCharArray();
+ for (int i = 0; i < script.length(); i++) {
+ char c = content[i];
+ if (inEscape) {
+ inEscape = false;
+ sb.append(c);
+ continue;
+ }
+ // MySQL style escapes
+ if (c == '\\') {
+ inEscape = true;
+ sb.append(c);
+ continue;
+ }
+ if (c == '\'') {
+ inLiteral = !inLiteral;
+ }
+ if (!inLiteral) {
+ if (script.startsWith(delim, i)) {
+ // we've reached the end of the current statement
+ if (sb.length() > 0) {
+ statements.add(sb.toString());
+ sb = new StringBuilder();
+ }
+ i += delim.length() - 1;
+ continue;
+ }
+ else if (script.startsWith(commentPrefix, i)) {
+ // skip over any content from the start of the comment to the EOL
+ int indexOfNextNewline = script.indexOf("\n", i);
+ if (indexOfNextNewline > i) {
+ i = indexOfNextNewline;
+ continue;
+ }
+ else {
+ // if there's no newline after the comment, we must be at the end
+ // of the script, so stop here.
+ break;
+ }
+ }
+ else if (c == ' ' || c == '\n' || c == '\t') {
+ // avoid multiple adjacent whitespace characters
+ if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
+ c = ' ';
+ }
+ else {
+ continue;
+ }
+ }
+ }
+ sb.append(c);
+ }
+ if (StringUtils.hasText(sb)) {
+ statements.add(sb.toString());
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java b/spring-test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java
new file mode 100644
index 00000000..24cee9ad
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java
@@ -0,0 +1,187 @@
+/*
+ * 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.jdbc;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.util.LinkedList;
+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.core.io.ResourceLoader;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
+
+/**
+ * A Java-5-based collection of JDBC related utility functions intended to
+ * simplify standard database testing scenarios.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @author Thomas Risberg
+ * @since 2.5
+ * @deprecated as of Spring 3.1.3; use {@link JdbcTestUtils} instead.
+ */
+@Deprecated
+public abstract class SimpleJdbcTestUtils {
+
+ private static final Log logger = LogFactory.getLog(SimpleJdbcTestUtils.class);
+
+
+ /**
+ * Count the rows in the given table.
+ * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations
+ * @param tableName table name to count rows in
+ * @return the number of rows in the table
+ */
+ public static int countRowsInTable(SimpleJdbcTemplate simpleJdbcTemplate, String tableName) {
+ return simpleJdbcTemplate.queryForInt("SELECT COUNT(0) FROM " + tableName);
+ }
+
+ /**
+ * Delete all rows from the specified tables.
+ * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations
+ * @param tableNames the names of the tables from which to delete
+ * @return the total number of rows deleted from all specified tables
+ */
+ public static int deleteFromTables(SimpleJdbcTemplate simpleJdbcTemplate, String... tableNames) {
+ int totalRowCount = 0;
+ for (String tableName : tableNames) {
+ int rowCount = simpleJdbcTemplate.update("DELETE FROM " + tableName);
+ totalRowCount += rowCount;
+ if (logger.isInfoEnabled()) {
+ logger.info("Deleted " + rowCount + " rows from table " + tableName);
+ }
+ }
+ return totalRowCount;
+ }
+
+ /**
+ * Execute the given SQL script.
+ * <p>The script will normally be loaded by classpath. There should be one statement
+ * per line. Any semicolons will be removed. <b>Do not use this method to execute
+ * DDL if you expect rollback.</b>
+ * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations
+ * @param resourceLoader the resource loader (with which to load the SQL script
+ * @param sqlResourcePath the Spring resource path for the SQL script
+ * @param continueOnError whether or not to continue without throwing an
+ * exception in the event of an error
+ * @throws DataAccessException if there is an error executing a statement
+ * and continueOnError was {@code false}
+ */
+ public static void executeSqlScript(SimpleJdbcTemplate simpleJdbcTemplate, ResourceLoader resourceLoader,
+ String sqlResourcePath, boolean continueOnError) throws DataAccessException {
+
+ Resource resource = resourceLoader.getResource(sqlResourcePath);
+ executeSqlScript(simpleJdbcTemplate, resource, continueOnError);
+ }
+
+ /**
+ * Execute the given SQL script. The script will normally be loaded by classpath.
+ * <p>Statements should be delimited with a semicolon. If statements are not delimited with
+ * a semicolon then there should be one statement per line. Statements are allowed to span
+ * lines only if they are delimited with a semicolon.
+ * <p><b>Do not use this method to execute DDL if you expect rollback.</b>
+ * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations
+ * @param resource the resource to load the SQL script from.
+ * @param continueOnError whether or not to continue without throwing an
+ * exception in the event of an error.
+ * @throws DataAccessException if there is an error executing a statement
+ * and continueOnError was {@code false}
+ */
+ public static void executeSqlScript(SimpleJdbcTemplate simpleJdbcTemplate, Resource resource,
+ boolean continueOnError) throws DataAccessException {
+
+ executeSqlScript(simpleJdbcTemplate, new EncodedResource(resource), continueOnError);
+ }
+
+ /**
+ * Execute the given SQL script.
+ * <p>The script will normally be loaded by classpath. There should be one statement
+ * per line. Any semicolons will be removed. <b>Do not use this method to execute
+ * DDL if you expect rollback.</b>
+ * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations
+ * @param resource the resource (potentially associated with a specific encoding)
+ * to load the SQL script from.
+ * @param continueOnError whether or not to continue without throwing an
+ * exception in the event of an error.
+ * @throws DataAccessException if there is an error executing a statement
+ * and continueOnError was {@code false}
+ */
+ public static void executeSqlScript(SimpleJdbcTemplate simpleJdbcTemplate, EncodedResource resource,
+ boolean continueOnError) throws DataAccessException {
+
+ if (logger.isInfoEnabled()) {
+ logger.info("Executing SQL script from " + resource);
+ }
+
+ long startTime = System.currentTimeMillis();
+ List<String> statements = new LinkedList<String>();
+ LineNumberReader reader = null;
+ try {
+ reader = new LineNumberReader(resource.getReader());
+ String script = JdbcTestUtils.readScript(reader);
+ char delimiter = ';';
+ if (!JdbcTestUtils.containsSqlScriptDelimiters(script, delimiter)) {
+ delimiter = '\n';
+ }
+ JdbcTestUtils.splitSqlScript(script, delimiter, statements);
+ for (String statement : statements) {
+ try {
+ int rowsAffected = simpleJdbcTemplate.update(statement);
+ if (logger.isDebugEnabled()) {
+ logger.debug(rowsAffected + " rows affected by SQL: " + statement);
+ }
+ }
+ catch (DataAccessException ex) {
+ if (continueOnError) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("SQL: " + statement + " failed", ex);
+ }
+ }
+ else {
+ throw ex;
+ }
+ }
+ }
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ if (logger.isInfoEnabled()) {
+ logger.info("Done executing SQL scriptBuilder from " + resource + " in " + elapsedTime + " ms.");
+ }
+ }
+ catch (IOException ex) {
+ throw new DataAccessResourceFailureException("Failed to open SQL script from " + resource, ex);
+ }
+ finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java b/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java
new file mode 100644
index 00000000..4587aa46
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Support classes for tests based on JDBC.
+ */
+
+package org.springframework.test.jdbc;
+
diff --git a/spring-test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java b/spring-test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java
new file mode 100644
index 00000000..dd55d0e7
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java
@@ -0,0 +1,61 @@
+/*
+ * 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.jpa;
+
+import org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter;
+
+import org.springframework.instrument.classloading.ResourceOverridingShadowingClassLoader;
+
+/**
+ * Subclass of {@link AbstractJpaTests} that activates AspectJ load-time weaving and
+ * allows for specifying a custom location for AspectJ's {@code aop.xml} file.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests})
+ */
+@Deprecated
+public abstract class AbstractAspectjJpaTests extends AbstractJpaTests {
+
+ /**
+ * Default location of the {@code aop.xml} file in the class path:
+ * "META-INF/aop.xml"
+ */
+ public static final String DEFAULT_AOP_XML_LOCATION = "META-INF/aop.xml";
+
+
+ @Override
+ protected void customizeResourceOverridingShadowingClassLoader(ClassLoader shadowingClassLoader) {
+ ResourceOverridingShadowingClassLoader orxl = (ResourceOverridingShadowingClassLoader) shadowingClassLoader;
+ orxl.override(DEFAULT_AOP_XML_LOCATION, getActualAopXmlLocation());
+ orxl.addTransformer(new ClassPreProcessorAgentAdapter());
+ }
+
+ /**
+ * Return the actual location of the {@code aop.xml} file
+ * in the class path. The default is "META-INF/aop.xml".
+ * <p>Override this method to point to a specific {@code aop.xml}
+ * file within your test suite, allowing for different config files
+ * to co-exist within the same class path.
+ */
+ protected String getActualAopXmlLocation() {
+ return DEFAULT_AOP_XML_LOCATION;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java b/spring-test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java
new file mode 100644
index 00000000..005c19e3
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright 2002-2013 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.jpa;
+
+import java.lang.instrument.ClassFileTransformer;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+
+import junit.framework.TestCase;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver;
+import org.springframework.instrument.classloading.LoadTimeWeaver;
+import org.springframework.instrument.classloading.ResourceOverridingShadowingClassLoader;
+import org.springframework.instrument.classloading.ShadowingClassLoader;
+import org.springframework.orm.jpa.ExtendedEntityManagerCreator;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.SharedEntityManagerCreator;
+import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager;
+import org.springframework.test.annotation.AbstractAnnotationAwareTransactionalTests;
+import org.springframework.util.StringUtils;
+
+/**
+ * Convenient support class for JPA-related tests. Offers the same contract as
+ * AbstractTransactionalDataSourceSpringContextTests and equally good performance,
+ * even when performing the instrumentation required by the JPA specification.
+ *
+ * <p>Exposes an EntityManagerFactory and a shared EntityManager.
+ * Requires an EntityManagerFactory to be injected, plus the DataSource and
+ * JpaTransactionManager through the superclass.
+ *
+ * <p>When using Xerces, make sure a post 2.0.2 version is available on the classpath
+ * to avoid a critical
+ * <a href="http://nagoya.apache.org/bugzilla/show_bug.cgi?id=16014"/>bug</a>
+ * that leads to StackOverflow. Maven users are likely to encounter this problem since
+ * 2.0.2 is used by default.
+ *
+ * <p>A workaround is to explicitly specify the Xerces version inside the Maven POM:
+ * <pre>
+ * &lt;dependency&gt;
+ * &lt;groupId&gt;xerces&lt;/groupId&gt;
+ * &lt;artifactId&gt;xercesImpl&lt;/artifactId&gt;
+ * &lt;version&gt;2.8.1&lt;/version&gt;
+ * &lt;/dependency&gt;
+ * </pre>
+ *
+ * @author Rod Johnson
+ * @author Rob Harrop
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests})
+ */
+@Deprecated
+public abstract class AbstractJpaTests extends AbstractAnnotationAwareTransactionalTests {
+
+ private static final String DEFAULT_ORM_XML_LOCATION = "META-INF/orm.xml";
+
+ /**
+ * Map from String defining unique combination of config locations, to ApplicationContext.
+ * Values are intentionally not strongly typed, to avoid potential class cast exceptions
+ * through use between different class loaders.
+ */
+ private static Map<String, Object> contextCache = new HashMap<String, Object>();
+
+ private static Map<String, ClassLoader> classLoaderCache = new HashMap<String, ClassLoader>();
+
+ protected EntityManagerFactory entityManagerFactory;
+
+ /**
+ * If this instance is in a shadow loader, this variable
+ * will contain the parent instance of the subclass.
+ * The class will not be the same as the class of the
+ * shadow instance, as it was loaded by a different class loader,
+ * but it can be invoked reflectively. The shadowParent
+ * and the shadow loader can communicate reflectively
+ * but not through direct invocation.
+ */
+ private Object shadowParent;
+
+ /**
+ * Subclasses can use this in test cases.
+ * It will participate in any current transaction.
+ */
+ protected EntityManager sharedEntityManager;
+
+
+ public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) {
+ this.entityManagerFactory = entityManagerFactory;
+ this.sharedEntityManager = SharedEntityManagerCreator.createSharedEntityManager(this.entityManagerFactory);
+ }
+
+ /**
+ * Create an EntityManager that will always automatically enlist itself in current
+ * transactions, in contrast to an EntityManager returned by
+ * {@code EntityManagerFactory.createEntityManager()}
+ * (which requires an explicit {@code joinTransaction()} call).
+ */
+ protected EntityManager createContainerManagedEntityManager() {
+ return ExtendedEntityManagerCreator.createContainerManagedEntityManager(this.entityManagerFactory);
+ }
+
+ /**
+ * Subclasses should override this method if they wish to disable shadow class loading.
+ * <p>The default implementation deactivates shadow class loading if Spring's
+ * InstrumentationSavingAgent has been configured on VM startup.
+ */
+ protected boolean shouldUseShadowLoader() {
+ return !InstrumentationLoadTimeWeaver.isInstrumentationAvailable();
+ }
+
+ @Override
+ public void setDirty() {
+ super.setDirty();
+ contextCache.remove(cacheKeys());
+ classLoaderCache.remove(cacheKeys());
+
+ // If we are a shadow loader, we need to invoke
+ // the shadow parent to set it dirty, as
+ // it is the shadow parent that maintains the cache state,
+ // not the child
+ if (this.shadowParent != null) {
+ try {
+ Method m = shadowParent.getClass().getMethod("setDirty", (Class[]) null);
+ m.invoke(shadowParent, (Object[]) null);
+ }
+ catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ }
+
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public void runBare() throws Throwable {
+
+ // getName will return the name of the method being run.
+ if (isDisabledInThisEnvironment(getName())) {
+ // Let superclass log that we didn't run the test.
+ super.runBare();
+ return;
+ }
+
+ final Method testMethod = getTestMethod();
+
+ if (isDisabledInThisEnvironment(testMethod)) {
+ recordDisabled();
+ this.logger.info("**** " + getClass().getName() + "." + getName() + " is disabled in this environment: "
+ + "Total disabled tests=" + getDisabledTestCount());
+ return;
+ }
+
+ if (!shouldUseShadowLoader()) {
+ super.runBare();
+ return;
+ }
+
+ String combinationOfContextLocationsForThisTestClass = cacheKeys();
+ ClassLoader classLoaderForThisTestClass = getClass().getClassLoader();
+ // save the TCCL
+ ClassLoader initialClassLoader = Thread.currentThread().getContextClassLoader();
+
+ if (this.shadowParent != null) {
+ Thread.currentThread().setContextClassLoader(classLoaderForThisTestClass);
+ super.runBare();
+ }
+
+ else {
+ ShadowingClassLoader shadowingClassLoader = (ShadowingClassLoader) classLoaderCache.get(combinationOfContextLocationsForThisTestClass);
+
+ if (shadowingClassLoader == null) {
+ shadowingClassLoader = (ShadowingClassLoader) createShadowingClassLoader(classLoaderForThisTestClass);
+ classLoaderCache.put(combinationOfContextLocationsForThisTestClass, shadowingClassLoader);
+ }
+ try {
+ Thread.currentThread().setContextClassLoader(shadowingClassLoader);
+ String[] configLocations = getConfigLocations();
+
+ // Do not strongly type, to avoid ClassCastException.
+ Object cachedContext = contextCache.get(combinationOfContextLocationsForThisTestClass);
+
+ if (cachedContext == null) {
+
+ // Create the LoadTimeWeaver.
+ Class shadowingLoadTimeWeaverClass = shadowingClassLoader.loadClass(ShadowingLoadTimeWeaver.class.getName());
+ Constructor constructor = shadowingLoadTimeWeaverClass.getConstructor(ClassLoader.class);
+ constructor.setAccessible(true);
+ Object ltw = constructor.newInstance(shadowingClassLoader);
+
+ // Create the BeanFactory.
+ Class beanFactoryClass = shadowingClassLoader.loadClass(DefaultListableBeanFactory.class.getName());
+ Object beanFactory = BeanUtils.instantiateClass(beanFactoryClass);
+
+ // Create the BeanDefinitionReader.
+ Class beanDefinitionReaderClass = shadowingClassLoader.loadClass(XmlBeanDefinitionReader.class.getName());
+ Class beanDefinitionRegistryClass = shadowingClassLoader.loadClass(BeanDefinitionRegistry.class.getName());
+ Object reader = beanDefinitionReaderClass.getConstructor(beanDefinitionRegistryClass).newInstance(beanFactory);
+
+ // Load the bean definitions into the BeanFactory.
+ Method loadBeanDefinitions = beanDefinitionReaderClass.getMethod("loadBeanDefinitions", String[].class);
+ loadBeanDefinitions.invoke(reader, new Object[] {configLocations});
+
+ // Create LoadTimeWeaver-injecting BeanPostProcessor.
+ Class loadTimeWeaverInjectingBeanPostProcessorClass = shadowingClassLoader.loadClass(LoadTimeWeaverInjectingBeanPostProcessor.class.getName());
+ Class loadTimeWeaverClass = shadowingClassLoader.loadClass(LoadTimeWeaver.class.getName());
+ Constructor bppConstructor = loadTimeWeaverInjectingBeanPostProcessorClass.getConstructor(loadTimeWeaverClass);
+ bppConstructor.setAccessible(true);
+ Object beanPostProcessor = bppConstructor.newInstance(ltw);
+
+ // Add LoadTimeWeaver-injecting BeanPostProcessor.
+ Class beanPostProcessorClass = shadowingClassLoader.loadClass(BeanPostProcessor.class.getName());
+ Method addBeanPostProcessor = beanFactoryClass.getMethod("addBeanPostProcessor", beanPostProcessorClass);
+ addBeanPostProcessor.invoke(beanFactory, beanPostProcessor);
+
+ // Create the GenericApplicationContext.
+ Class genericApplicationContextClass = shadowingClassLoader.loadClass(GenericApplicationContext.class.getName());
+ Class defaultListableBeanFactoryClass = shadowingClassLoader.loadClass(DefaultListableBeanFactory.class.getName());
+ cachedContext = genericApplicationContextClass.getConstructor(defaultListableBeanFactoryClass).newInstance(beanFactory);
+
+ // Invoke the context's "refresh" method.
+ genericApplicationContextClass.getMethod("refresh").invoke(cachedContext);
+
+ // Store the context reference in the cache.
+ contextCache.put(combinationOfContextLocationsForThisTestClass, cachedContext);
+ }
+ // create the shadowed test
+ Class shadowedTestClass = shadowingClassLoader.loadClass(getClass().getName());
+
+ // So long as JUnit is excluded from shadowing we
+ // can minimize reflective invocation here
+ TestCase shadowedTestCase = (TestCase) BeanUtils.instantiateClass(shadowedTestClass);
+
+ /* shadowParent = this */
+ Class thisShadowedClass = shadowingClassLoader.loadClass(AbstractJpaTests.class.getName());
+ Field shadowed = thisShadowedClass.getDeclaredField("shadowParent");
+ shadowed.setAccessible(true);
+ shadowed.set(shadowedTestCase, this);
+
+ /* AbstractSpringContextTests.addContext(Object, ApplicationContext) */
+ Class applicationContextClass = shadowingClassLoader.loadClass(ConfigurableApplicationContext.class.getName());
+ Method addContextMethod = shadowedTestClass.getMethod("addContext", Object.class, applicationContextClass);
+ addContextMethod.invoke(shadowedTestCase, configLocations, cachedContext);
+
+ // Invoke tests on shadowed test case
+ shadowedTestCase.setName(getName());
+ shadowedTestCase.runBare();
+ }
+ catch (InvocationTargetException ex) {
+ // Unwrap this for better exception reporting
+ // when running tests
+ throw ex.getTargetException();
+ }
+ finally {
+ Thread.currentThread().setContextClassLoader(initialClassLoader);
+ }
+ }
+ }
+
+ protected String cacheKeys() {
+ return StringUtils.arrayToCommaDelimitedString(getConfigLocations());
+ }
+
+ /**
+ * NB: This method must <b>not</b> have a return type of ShadowingClassLoader as that would cause that
+ * class to be loaded eagerly when this test case loads, creating verify errors at runtime.
+ */
+ protected ClassLoader createShadowingClassLoader(ClassLoader classLoader) {
+ OrmXmlOverridingShadowingClassLoader orxl = new OrmXmlOverridingShadowingClassLoader(classLoader,
+ getActualOrmXmlLocation());
+ customizeResourceOverridingShadowingClassLoader(orxl);
+ return orxl;
+ }
+
+ /**
+ * Customize the shadowing class loader.
+ * @param shadowingClassLoader this parameter is actually of type
+ * ResourceOverridingShadowingClassLoader, and can safely to be cast to
+ * that type. However, the signature must not be of that type as that
+ * would cause the present class loader to load that type.
+ */
+ protected void customizeResourceOverridingShadowingClassLoader(ClassLoader shadowingClassLoader) {
+ // empty
+ }
+
+ /**
+ * Subclasses can override this to return the real location path for
+ * orm.xml or null if they do not wish to find any orm.xml
+ * @return orm.xml path or null to hide any such file
+ */
+ protected String getActualOrmXmlLocation() {
+ return DEFAULT_ORM_XML_LOCATION;
+ }
+
+
+ private static class LoadTimeWeaverInjectingBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {
+
+ private final LoadTimeWeaver ltw;
+
+ @SuppressWarnings("unused")
+ public LoadTimeWeaverInjectingBeanPostProcessor(LoadTimeWeaver ltw) {
+ this.ltw = ltw;
+ }
+
+ public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+ if (bean instanceof LocalContainerEntityManagerFactoryBean) {
+ ((LocalContainerEntityManagerFactoryBean) bean).setLoadTimeWeaver(this.ltw);
+ }
+ if (bean instanceof DefaultPersistenceUnitManager) {
+ ((DefaultPersistenceUnitManager) bean).setLoadTimeWeaver(this.ltw);
+ }
+ return bean;
+ }
+ }
+
+
+ private static class ShadowingLoadTimeWeaver implements LoadTimeWeaver {
+
+ private final ClassLoader shadowingClassLoader;
+
+ @SuppressWarnings("unused")
+ public ShadowingLoadTimeWeaver(ClassLoader shadowingClassLoader) {
+ this.shadowingClassLoader = shadowingClassLoader;
+ }
+
+ public void addTransformer(ClassFileTransformer transformer) {
+ try {
+ Method addClassFileTransformer =
+ this.shadowingClassLoader.getClass().getMethod("addTransformer", ClassFileTransformer.class);
+ addClassFileTransformer.setAccessible(true);
+ addClassFileTransformer.invoke(this.shadowingClassLoader, transformer);
+ }
+ catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public ClassLoader getInstrumentableClassLoader() {
+ return this.shadowingClassLoader;
+ }
+
+ public ClassLoader getThrowawayClassLoader() {
+ // Be sure to copy the same resource overrides and same class file transformers:
+ // We want the throwaway class loader to behave like the instrumentable class loader.
+ ResourceOverridingShadowingClassLoader roscl =
+ new ResourceOverridingShadowingClassLoader(getClass().getClassLoader());
+ if (this.shadowingClassLoader instanceof ShadowingClassLoader) {
+ roscl.copyTransformers((ShadowingClassLoader) this.shadowingClassLoader);
+ }
+ if (this.shadowingClassLoader instanceof ResourceOverridingShadowingClassLoader) {
+ roscl.copyOverrides((ResourceOverridingShadowingClassLoader) this.shadowingClassLoader);
+ }
+ return roscl;
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java b/spring-test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java
new file mode 100644
index 00000000..9cf1ded1
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java
@@ -0,0 +1,55 @@
+/*
+ * 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.jpa;
+
+import org.springframework.instrument.classloading.ResourceOverridingShadowingClassLoader;
+
+/**
+ * Subclass of ShadowingClassLoader that overrides attempts to
+ * locate {@code orm.xml}.
+ *
+ * <p>This class must <b>not</b> be an inner class of AbstractJpaTests
+ * to avoid it being loaded until first used.
+ *
+ * @author Rod Johnson
+ * @author Adrian Colyer
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+class OrmXmlOverridingShadowingClassLoader extends ResourceOverridingShadowingClassLoader {
+
+ /**
+ * Default location of the {@code orm.xml} file in the class path:
+ * "META-INF/orm.xml"
+ */
+ public static final String DEFAULT_ORM_XML_LOCATION = "META-INF/orm.xml";
+
+
+ public OrmXmlOverridingShadowingClassLoader(ClassLoader loader, String realOrmXmlLocation) {
+ super(loader);
+
+ // Automatically exclude classes from these well-known persistence providers.
+ // Do NOT exclude Hibernate classes --
+ // this causes class casts due to use of CGLIB by Hibernate.
+ // Same goes for OpenJPA which will not enhance the domain classes.
+ excludePackage("oracle.toplink.essentials");
+ excludePackage("junit");
+
+ override(DEFAULT_ORM_XML_LOCATION, realOrmXmlLocation);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/jpa/package-info.java b/spring-test/src/main/java/org/springframework/test/jpa/package-info.java
new file mode 100644
index 00000000..58ef717a
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/jpa/package-info.java
@@ -0,0 +1,9 @@
+
+/**
+ *
+ * As of Spring 3.0, this package has been deprecated in favor of using the listener-based
+ * <em>Spring TestContext Framework</em>.
+ *
+ */
+package org.springframework.test.jpa;
+
diff --git a/spring-test/src/main/java/org/springframework/test/package-info.java b/spring-test/src/main/java/org/springframework/test/package-info.java
new file mode 100644
index 00000000..9ec02f96
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/package-info.java
@@ -0,0 +1,9 @@
+
+/**
+ *
+ * This package contains the legacy JUnit 3.8 class hierarchy, which as of Spring 3.0
+ * has been deprecated in favor of using the listener-based <em>Spring TestContext Framework</em>.
+ *
+ */
+package org.springframework.test;
+
diff --git a/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java
new file mode 100644
index 00000000..4f2dbab7
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java
@@ -0,0 +1,93 @@
+/*
+ * 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.util;
+
+import org.springframework.util.ObjectUtils;
+
+/**
+ * JUnit independent assertion class.
+ *
+ * @author Lukas Krecan
+ * @author Arjen Poutsma
+ * @since 3.2
+ */
+public abstract class AssertionErrors {
+
+
+ private AssertionErrors() {
+ }
+
+ /**
+ * Fails a test with the given message.
+ *
+ * @param message describes the reason for the failure
+ */
+ public static void fail(String message) {
+ throw new AssertionError(message);
+ }
+
+ /**
+ * Fails a test with the given message passing along expected and actual
+ * values to be added to the message.
+ *
+ * <p>For example given:
+ * <pre>
+ * assertEquals("Response header [" + name + "]", actual, expected);
+ * </pre>
+ * <p>The resulting message is:
+ * <pre>
+ * Response header [Accept] expected:&lt;application/json&gt; but was:&lt;text/plain&gt;
+ * </pre>
+ *
+ * @param message describes the value that failed the match
+ * @param expected expected value
+ * @param actual actual value
+ */
+ public static void fail(String message, Object expected, Object actual) {
+ throw new AssertionError(message + " expected:<" + expected + "> but was:<" + actual + ">");
+ }
+
+ /**
+ * Assert the given condition is {@code true} and raise an
+ * {@link AssertionError} if it is not.
+ *
+ * @param message the message
+ * @param condition the condition to test for
+ */
+ public static void assertTrue(String message, boolean condition) {
+ if (!condition) {
+ fail(message);
+ }
+ }
+
+ /**
+ * Assert two objects are equal raise an {@link AssertionError} if not.
+ * <p>For example:
+ * <pre>
+ * assertEquals("Response header [" + name + "]", actual, expected);
+ * </pre>
+ *
+ * @param message describes the value being checked
+ * @param expected the expected value
+ * @param actual the actual value
+ */
+ public static void assertEquals(String message, Object expected, Object actual) {
+ if (!ObjectUtils.nullSafeEquals(expected, actual)) {
+ fail(message, expected, actual);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java
new file mode 100644
index 00000000..6b54d547
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java
@@ -0,0 +1,315 @@
+/*
+ * 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.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.util.Assert;
+import org.springframework.util.MethodInvoker;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@code ReflectionTestUtils} is a collection of reflection-based utility
+ * methods for use in unit and integration testing scenarios.
+ *
+ * <p>There are often times when it would be beneficial to be able to set a
+ * non-{@code public} field, invoke a non-{@code public} setter method, or
+ * invoke a non-{@code public} <em>configuration</em> or <em>lifecycle</em>
+ * callback method when testing code involving, for example:
+ *
+ * <ul>
+ * <li>ORM frameworks such as JPA and Hibernate which condone the usage of
+ * {@code private} or {@code protected} field access as opposed to
+ * {@code public} setter methods for properties in a domain entity.</li>
+ * <li>Spring's support for annotations such as
+ * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} and
+ * {@link javax.annotation.Resource @Resource} which provides dependency
+ * injection for {@code private} or {@code protected} fields, setter methods,
+ * and configuration methods.</li>
+ * <li>Use of annotations such as {@link javax.annotation.PostConstruct @PostConstruct}
+ * and {@link javax.annotation.PreDestroy @PreDestroy} for lifecycle callback
+ * methods.</li>
+ * </ul>
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see ReflectionUtils
+ */
+public class ReflectionTestUtils {
+
+ private static final String SETTER_PREFIX = "set";
+
+ private static final String GETTER_PREFIX = "get";
+
+ private static final Log logger = LogFactory.getLog(ReflectionTestUtils.class);
+
+
+ /**
+ * Set the {@link Field field} with the given {@code name} on the provided
+ * {@link Object target object} to the supplied {@code value}.
+ *
+ * <p>This method traverses the class hierarchy in search of the desired field.
+ * In addition, an attempt will be made to make non-{@code public} fields
+ * <em>accessible</em>, thus allowing one to set {@code protected},
+ * {@code private}, and <em>package-private</em> fields.
+ *
+ * @param target the target object on which to set the field
+ * @param name the name of the field to set
+ * @param value the value to set
+ * @see ReflectionUtils#findField(Class, String, Class)
+ * @see ReflectionUtils#makeAccessible(Field)
+ * @see ReflectionUtils#setField(Field, Object, Object)
+ */
+ public static void setField(Object target, String name, Object value) {
+ setField(target, name, value, null);
+ }
+
+ /**
+ * Set the {@link Field field} with the given {@code name} on the provided
+ * {@link Object target object} to the supplied {@code value}.
+ *
+ * <p>This method traverses the class hierarchy in search of the desired
+ * field. In addition, an attempt will be made to make non-{@code public}
+ * fields <em>accessible</em>, thus allowing one to set {@code protected},
+ * {@code private}, and <em>package-private</em> fields.
+ *
+ * @param target the target object on which to set the field
+ * @param name the name of the field to set
+ * @param value the value to set
+ * @param type the type of the field (may be {@code null})
+ * @see ReflectionUtils#findField(Class, String, Class)
+ * @see ReflectionUtils#makeAccessible(Field)
+ * @see ReflectionUtils#setField(Field, Object, Object)
+ */
+ public static void setField(Object target, String name, Object value, Class<?> type) {
+ Assert.notNull(target, "Target object must not be null");
+ Field field = ReflectionUtils.findField(target.getClass(), name, type);
+
+ // SPR-9571: inline Assert.notNull() in order to avoid accidentally invoking
+ // toString() on a non-null target.
+ if (field == null) {
+ throw new IllegalArgumentException(String.format("Could not find field [%s] of type [%s] on target [%s]",
+ name, type, target));
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Setting field [%s] of type [%s] on target [%s] to value [%s]", name, type,
+ target,
+ value));
+ }
+ ReflectionUtils.makeAccessible(field);
+ ReflectionUtils.setField(field, target, value);
+ }
+
+ /**
+ * Get the field with the given {@code name} from the provided target object.
+ *
+ * <p>This method traverses the class hierarchy in search of the desired
+ * field. In addition, an attempt will be made to make non-{@code public}
+ * fields <em>accessible</em>, thus allowing one to get {@code protected},
+ * {@code private}, and <em>package-private</em> fields.
+ *
+ * @param target the target object on which to set the field
+ * @param name the name of the field to get
+ * @return the field's current value
+ * @see ReflectionUtils#findField(Class, String, Class)
+ * @see ReflectionUtils#makeAccessible(Field)
+ * @see ReflectionUtils#setField(Field, Object, Object)
+ */
+ public static Object getField(Object target, String name) {
+ Assert.notNull(target, "Target object must not be null");
+ Field field = ReflectionUtils.findField(target.getClass(), name);
+ Assert.notNull(field, "Could not find field [" + name + "] on target [" + target + "]");
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Getting field [" + name + "] from target [" + target + "]");
+ }
+ ReflectionUtils.makeAccessible(field);
+ return ReflectionUtils.getField(field, target);
+ }
+
+ /**
+ * Invoke the setter method with the given {@code name} on the supplied
+ * target object with the supplied {@code value}.
+ *
+ * <p>This method traverses the class hierarchy in search of the desired
+ * method. In addition, an attempt will be made to make non-{@code public}
+ * methods <em>accessible</em>, thus allowing one to invoke {@code protected},
+ * {@code private}, and <em>package-private</em> setter methods.
+ *
+ * <p>In addition, this method supports JavaBean-style <em>property</em>
+ * names. For example, if you wish to set the {@code name} property on the
+ * target object, you may pass either &quot;name&quot; or
+ * &quot;setName&quot; as the method name.
+ *
+ * @param target the target object on which to invoke the specified setter
+ * method
+ * @param name the name of the setter method to invoke or the corresponding
+ * property name
+ * @param value the value to provide to the setter method
+ * @see ReflectionUtils#findMethod(Class, String, Class[])
+ * @see ReflectionUtils#makeAccessible(Method)
+ * @see ReflectionUtils#invokeMethod(Method, Object, Object[])
+ */
+ public static void invokeSetterMethod(Object target, String name, Object value) {
+ invokeSetterMethod(target, name, value, null);
+ }
+
+ /**
+ * Invoke the setter method with the given {@code name} on the supplied
+ * target object with the supplied {@code value}.
+ *
+ * <p>This method traverses the class hierarchy in search of the desired
+ * method. In addition, an attempt will be made to make non-{@code public}
+ * methods <em>accessible</em>, thus allowing one to invoke {@code protected},
+ * {@code private}, and <em>package-private</em> setter methods.
+ *
+ * <p>In addition, this method supports JavaBean-style <em>property</em>
+ * names. For example, if you wish to set the {@code name} property on the
+ * target object, you may pass either &quot;name&quot; or
+ * &quot;setName&quot; as the method name.
+ *
+ * @param target the target object on which to invoke the specified setter
+ * method
+ * @param name the name of the setter method to invoke or the corresponding
+ * property name
+ * @param value the value to provide to the setter method
+ * @param type the formal parameter type declared by the setter method
+ * @see ReflectionUtils#findMethod(Class, String, Class[])
+ * @see ReflectionUtils#makeAccessible(Method)
+ * @see ReflectionUtils#invokeMethod(Method, Object, Object[])
+ */
+ public static void invokeSetterMethod(Object target, String name, Object value, Class<?> type) {
+ Assert.notNull(target, "Target object must not be null");
+ Assert.hasText(name, "Method name must not be empty");
+ Class<?>[] paramTypes = (type != null ? new Class<?>[] { type } : null);
+
+ String setterMethodName = name;
+ if (!name.startsWith(SETTER_PREFIX)) {
+ setterMethodName = SETTER_PREFIX + StringUtils.capitalize(name);
+ }
+ Method method = ReflectionUtils.findMethod(target.getClass(), setterMethodName, paramTypes);
+ if (method == null && !setterMethodName.equals(name)) {
+ setterMethodName = name;
+ method = ReflectionUtils.findMethod(target.getClass(), setterMethodName, paramTypes);
+ }
+ Assert.notNull(method, "Could not find setter method [" + setterMethodName + "] on target [" + target
+ + "] with parameter type [" + type + "]");
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Invoking setter method [" + setterMethodName + "] on target [" + target + "]");
+ }
+ ReflectionUtils.makeAccessible(method);
+ ReflectionUtils.invokeMethod(method, target, new Object[] { value });
+ }
+
+ /**
+ * Invoke the getter method with the given {@code name} on the supplied
+ * target object with the supplied {@code value}.
+ *
+ * <p>This method traverses the class hierarchy in search of the desired
+ * method. In addition, an attempt will be made to make non-{@code public}
+ * methods <em>accessible</em>, thus allowing one to invoke {@code protected},
+ * {@code private}, and <em>package-private</em> getter methods.
+ *
+ * <p>In addition, this method supports JavaBean-style <em>property</em>
+ * names. For example, if you wish to get the {@code name} property on the
+ * target object, you may pass either &quot;name&quot; or
+ * &quot;getName&quot; as the method name.
+ *
+ * @param target the target object on which to invoke the specified getter
+ * method
+ * @param name the name of the getter method to invoke or the corresponding
+ * property name
+ * @return the value returned from the invocation
+ * @see ReflectionUtils#findMethod(Class, String, Class[])
+ * @see ReflectionUtils#makeAccessible(Method)
+ * @see ReflectionUtils#invokeMethod(Method, Object, Object[])
+ */
+ public static Object invokeGetterMethod(Object target, String name) {
+ Assert.notNull(target, "Target object must not be null");
+ Assert.hasText(name, "Method name must not be empty");
+
+ String getterMethodName = name;
+ if (!name.startsWith(GETTER_PREFIX)) {
+ getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
+ }
+ Method method = ReflectionUtils.findMethod(target.getClass(), getterMethodName);
+ if (method == null && !getterMethodName.equals(name)) {
+ getterMethodName = name;
+ method = ReflectionUtils.findMethod(target.getClass(), getterMethodName);
+ }
+ Assert.notNull(method, "Could not find getter method [" + getterMethodName + "] on target [" + target + "]");
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Invoking getter method [" + getterMethodName + "] on target [" + target + "]");
+ }
+ ReflectionUtils.makeAccessible(method);
+ return ReflectionUtils.invokeMethod(method, target);
+ }
+
+ /**
+ * Invoke the method with the given {@code name} on the supplied target
+ * object with the supplied arguments.
+ *
+ * <p>This method traverses the class hierarchy in search of the desired
+ * method. In addition, an attempt will be made to make non-{@code public}
+ * methods <em>accessible</em>, thus allowing one to invoke {@code protected},
+ * {@code private}, and <em>package-private</em> methods.
+ *
+ * @param target the target object on which to invoke the specified method
+ * @param name the name of the method to invoke
+ * @param args the arguments to provide to the method
+ * @return the invocation result, if any
+ * @see MethodInvoker
+ * @see ReflectionUtils#makeAccessible(Method)
+ * @see ReflectionUtils#invokeMethod(Method, Object, Object[])
+ * @see ReflectionUtils#handleReflectionException(Exception)
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T invokeMethod(Object target, String name, Object... args) {
+ Assert.notNull(target, "Target object must not be null");
+ Assert.hasText(name, "Method name must not be empty");
+
+ try {
+ MethodInvoker methodInvoker = new MethodInvoker();
+ methodInvoker.setTargetObject(target);
+ methodInvoker.setTargetMethod(name);
+ methodInvoker.setArguments(args);
+ methodInvoker.prepare();
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Invoking method [" + name + "] on target [" + target + "] with arguments ["
+ + ObjectUtils.nullSafeToString(args) + "]");
+ }
+
+ return (T) methodInvoker.invoke();
+ }
+ catch (Exception e) {
+ ReflectionUtils.handleReflectionException(e);
+ }
+
+ throw new IllegalStateException("Should never get here");
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/util/package-info.java b/spring-test/src/main/java/org/springframework/test/util/package-info.java
new file mode 100644
index 00000000..7d9e7496
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/util/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * General utility classes for use in unit and integration tests.
+ */
+
+package org.springframework.test.util; \ No newline at end of file
diff --git a/spring-test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java b/spring-test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java
new file mode 100644
index 00000000..fefc31e1
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java
@@ -0,0 +1,169 @@
+/*
+ * 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;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Convenient JUnit 3.8 base class for tests dealing with Spring Web MVC
+ * {@link org.springframework.web.servlet.ModelAndView ModelAndView} objects.
+ *
+ * <p>All {@code assert*()} methods throw {@link AssertionFailedError}s.
+ *
+ * <p>Consider the use of {@link ModelAndViewAssert} with JUnit 4 and TestNG.
+ *
+ * @author Alef Arendsen
+ * @author Bram Smeets
+ * @author Sam Brannen
+ * @since 2.0
+ * @see org.springframework.web.servlet.ModelAndView
+ * @see ModelAndViewAssert
+ * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
+ * ({@link org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests})
+ * or {@link ModelAndViewAssert} with JUnit 4 and TestNG.
+ */
+@Deprecated
+public abstract class AbstractModelAndViewTests extends TestCase {
+
+ /**
+ * Checks whether the model value under the given {@code modelName}
+ * exists and checks it type, based on the {@code expectedType}. If
+ * the model entry exists and the type matches, the model value is returned.
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ * @param expectedType expected type of the model value
+ * @return the model value
+ */
+ protected <T> T assertAndReturnModelAttributeOfType(ModelAndView mav, String modelName, Class<T> expectedType) {
+ try {
+ return ModelAndViewAssert.assertAndReturnModelAttributeOfType(mav, modelName, expectedType);
+ }
+ catch (AssertionError e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ }
+
+ /**
+ * Compare each individual entry in a list, without first sorting the lists.
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ * @param expectedList the expected list
+ */
+ @SuppressWarnings("rawtypes")
+ protected void assertCompareListModelAttribute(ModelAndView mav, String modelName, List expectedList) {
+ try {
+ ModelAndViewAssert.assertCompareListModelAttribute(mav, modelName, expectedList);
+ }
+ catch (AssertionError e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ }
+
+ /**
+ * Assert whether or not a model attribute is available.
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ */
+ protected void assertModelAttributeAvailable(ModelAndView mav, String modelName) {
+ try {
+ ModelAndViewAssert.assertModelAttributeAvailable(mav, modelName);
+ }
+ catch (AssertionError e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ }
+
+ /**
+ * Compare a given {@code expectedValue} to the value from the model
+ * bound under the given {@code modelName}.
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ * @param expectedValue the model value
+ */
+ protected void assertModelAttributeValue(ModelAndView mav, String modelName, Object expectedValue) {
+ try {
+ ModelAndViewAssert.assertModelAttributeValue(mav, modelName, expectedValue);
+ }
+ catch (AssertionError e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ }
+
+ /**
+ * Inspect the {@code expectedModel} to see if all elements in the
+ * model appear and are equal.
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param expectedModel the expected model
+ */
+ protected void assertModelAttributeValues(ModelAndView mav, Map<String, Object> expectedModel) {
+ try {
+ ModelAndViewAssert.assertModelAttributeValues(mav, expectedModel);
+ }
+ catch (AssertionError e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ }
+
+ /**
+ * Compare each individual entry in a list after having sorted both lists
+ * (optionally using a comparator).
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ * @param expectedList the expected list
+ * @param comparator the comparator to use (may be {@code null}). If
+ * not specifying the comparator, both lists will be sorted not using
+ * any comparator.
+ */
+ @SuppressWarnings("rawtypes")
+ protected void assertSortAndCompareListModelAttribute(
+ ModelAndView mav, String modelName, List expectedList, Comparator comparator) {
+ try {
+ ModelAndViewAssert.assertSortAndCompareListModelAttribute(mav, modelName, expectedList, comparator);
+ }
+ catch (AssertionError e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ }
+
+ /**
+ * Check to see if the view name in the ModelAndView matches the given
+ * {@code expectedName}.
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param expectedName the name of the model value
+ */
+ protected void assertViewName(ModelAndView mav, String expectedName) {
+ try {
+ ModelAndViewAssert.assertViewName(mav, expectedName);
+ }
+ catch (AssertionError e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java b/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java
new file mode 100644
index 00000000..62677fdb
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java
@@ -0,0 +1,230 @@
+/*
+ * 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;
+
+import static org.springframework.test.util.AssertionErrors.*;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * A collection of assertions intended to simplify testing scenarios dealing
+ * with Spring Web MVC {@link org.springframework.web.servlet.ModelAndView
+ * ModelAndView} objects.
+ * <p>
+ * Intended for use with JUnit 4 and TestNG. All {@code assert*()} methods
+ * throw {@link AssertionError}s.
+ *
+ * @author Sam Brannen
+ * @author Alef Arendsen
+ * @author Bram Smeets
+ * @since 2.5
+ * @see org.springframework.web.servlet.ModelAndView
+ */
+public abstract class ModelAndViewAssert {
+
+ /**
+ * Checks whether the model value under the given {@code modelName}
+ * exists and checks it type, based on the {@code expectedType}. If the
+ * model entry exists and the type matches, the model value is returned.
+ *
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ * @param expectedType expected type of the model value
+ * @return the model value
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T assertAndReturnModelAttributeOfType(ModelAndView mav, String modelName, Class<T> expectedType) {
+ assertTrue("ModelAndView is null", mav != null);
+ assertTrue("Model is null", mav.getModel() != null);
+ Object obj = mav.getModel().get(modelName);
+ assertTrue("Model attribute with name '" + modelName + "' is null", obj != null);
+ assertTrue("Model attribute is not of expected type '" + expectedType.getName() + "' but rather of type '"
+ + obj.getClass().getName() + "'", expectedType.isAssignableFrom(obj.getClass()));
+ return (T) obj;
+ }
+
+ /**
+ * Compare each individual entry in a list, without first sorting the lists.
+ *
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ * @param expectedList the expected list
+ */
+ @SuppressWarnings("rawtypes")
+ public static void assertCompareListModelAttribute(ModelAndView mav, String modelName, List expectedList) {
+ assertTrue("ModelAndView is null", mav != null);
+ List modelList = assertAndReturnModelAttributeOfType(mav, modelName, List.class);
+ assertTrue(
+ "Size of model list is '" + modelList.size() + "' while size of expected list is '" + expectedList.size()
+ + "'", expectedList.size() == modelList.size());
+ assertTrue("List in model under name '" + modelName + "' is not equal to the expected list.",
+ expectedList.equals(modelList));
+ }
+
+ /**
+ * Assert whether or not a model attribute is available.
+ *
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ */
+ public static void assertModelAttributeAvailable(ModelAndView mav, String modelName) {
+ assertTrue("ModelAndView is null", mav != null);
+ assertTrue("Model is null", mav.getModel() != null);
+ assertTrue("Model attribute with name '" + modelName + "' is not available",
+ mav.getModel().containsKey(modelName));
+ }
+
+ /**
+ * Compare a given {@code expectedValue} to the value from the model
+ * bound under the given {@code modelName}.
+ *
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ * @param expectedValue the model value
+ */
+ public static void assertModelAttributeValue(ModelAndView mav, String modelName, Object expectedValue) {
+ assertTrue("ModelAndView is null", mav != null);
+ Object modelValue = assertAndReturnModelAttributeOfType(mav, modelName, Object.class);
+ assertTrue("Model value with name '" + modelName + "' is not the same as the expected value which was '"
+ + expectedValue + "'", modelValue.equals(expectedValue));
+ }
+
+ /**
+ * Inspect the {@code expectedModel} to see if all elements in the
+ * model appear and are equal.
+ *
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param expectedModel the expected model
+ */
+ public static void assertModelAttributeValues(ModelAndView mav, Map<String, Object> expectedModel) {
+ assertTrue("ModelAndView is null", mav != null);
+ assertTrue("Model is null", mav.getModel() != null);
+
+ if (!mav.getModel().keySet().equals(expectedModel.keySet())) {
+ StringBuilder sb = new StringBuilder("Keyset of expected model does not match.\n");
+ appendNonMatchingSetsErrorMessage(expectedModel.keySet(), mav.getModel().keySet(), sb);
+ fail(sb.toString());
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (String modelName : mav.getModel().keySet()) {
+ Object assertionValue = expectedModel.get(modelName);
+ Object mavValue = mav.getModel().get(modelName);
+ if (!assertionValue.equals(mavValue)) {
+ sb.append("Value under name '").append(modelName).append("' differs, should have been '").append(
+ assertionValue).append("' but was '").append(mavValue).append("'\n");
+ }
+ }
+
+ if (sb.length() != 0) {
+ sb.insert(0, "Values of expected model do not match.\n");
+ fail(sb.toString());
+ }
+ }
+
+ /**
+ * Compare each individual entry in a list after having sorted both lists
+ * (optionally using a comparator).
+ *
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param modelName name of the object to add to the model (never
+ * {@code null})
+ * @param expectedList the expected list
+ * @param comparator the comparator to use (may be {@code null}). If
+ * not specifying the comparator, both lists will be sorted not using any
+ * comparator.
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public static void assertSortAndCompareListModelAttribute(ModelAndView mav, String modelName, List expectedList,
+ Comparator comparator) {
+
+ assertTrue("ModelAndView is null", mav != null);
+ List modelList = assertAndReturnModelAttributeOfType(mav, modelName, List.class);
+
+ assertTrue(
+ "Size of model list is '" + modelList.size() + "' while size of expected list is '" + expectedList.size()
+ + "'", expectedList.size() == modelList.size());
+
+ if (comparator != null) {
+ Collections.sort(modelList, comparator);
+ Collections.sort(expectedList, comparator);
+ }
+ else {
+ Collections.sort(modelList);
+ Collections.sort(expectedList);
+ }
+
+ assertTrue("List in model under name '" + modelName + "' is not equal to the expected list.",
+ expectedList.equals(modelList));
+ }
+
+ /**
+ * Check to see if the view name in the ModelAndView matches the given
+ * {@code expectedName}.
+ *
+ * @param mav ModelAndView to test against (never {@code null})
+ * @param expectedName the name of the model value
+ */
+ public static void assertViewName(ModelAndView mav, String expectedName) {
+ assertTrue("ModelAndView is null", mav != null);
+ assertTrue("View name is not equal to '" + expectedName + "' but was '" + mav.getViewName() + "'",
+ ObjectUtils.nullSafeEquals(expectedName, mav.getViewName()));
+ }
+
+ private static void appendNonMatchingSetsErrorMessage(Set<String> assertionSet, Set<String> incorrectSet,
+ StringBuilder sb) {
+
+ Set<String> tempSet = new HashSet<String>();
+ tempSet.addAll(incorrectSet);
+ tempSet.removeAll(assertionSet);
+
+ if (tempSet.size() > 0) {
+ sb.append("Set has too many elements:\n");
+ for (Object element : tempSet) {
+ sb.append('-');
+ sb.append(element);
+ sb.append('\n');
+ }
+ }
+
+ tempSet = new HashSet<String>();
+ tempSet.addAll(assertionSet);
+ tempSet.removeAll(incorrectSet);
+
+ if (tempSet.size() > 0) {
+ sb.append("Set is missing elements:\n");
+ for (Object element : tempSet) {
+ sb.append('-');
+ sb.append(element);
+ sb.append('\n');
+ }
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/package-info.java b/spring-test/src/main/java/org/springframework/test/web/package-info.java
new file mode 100644
index 00000000..00853208
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Helper classes for unit tests based on Spring's web support.
+ */
+
+package org.springframework.test.web;
+
diff --git a/spring-test/src/main/java/overview.html b/spring-test/src/main/java/overview.html
new file mode 100644
index 00000000..96bde489
--- /dev/null
+++ b/spring-test/src/main/java/overview.html
@@ -0,0 +1,7 @@
+<html>
+<body>
+<p>
+Spring's TestContext framework. Also includes common Servlet and Portlet API mocks.
+</p>
+</body>
+</html>