diff --git a/org.springframework.test/build.xml b/org.springframework.test/build.xml
new file mode 100644
index 00000000000..37bc26f00fc
--- /dev/null
+++ b/org.springframework.test/build.xml
@@ -0,0 +1,6 @@
+
+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/org.springframework.test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java b/org.springframework.test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java
new file mode 100644
index 00000000000..75347b38f0b
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java
@@ -0,0 +1,349 @@
+/*
+ * 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.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.
+ *
+ *
This class is not intended for direct usage by applications, although it
+ * can be used for example to override JndiTemplate's 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 org.springframework.mock.jndi.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 boundObjects;
+
+ private final Hashtable environment = new Hashtable();
+
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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 boundObjects, Hashtable environment) {
+ this.root = root;
+ this.boundObjects = boundObjects;
+ if (environment != null) {
+ this.environment.putAll(environment);
+ }
+ }
+
+
+ // Actual implementations of Context methods follow
+
+ public NamingEnumeration list(String root) throws NamingException {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Listing name/class pairs under [" + root + "]");
+ }
+ return new NameClassPairEnumeration(this, root);
+ }
+
+ public NamingEnumeration 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.
+ *
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 (Iterator it = this.boundObjects.keySet().iterator(); it.hasNext();) { + String boundName = (String) it.next(); + 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 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 list(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public NamingEnumeration 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 implements NamingEnumeration { + + private Iterator iterator; + + private AbstractNamingEnumeration(SimpleNamingContext context, String proot) throws NamingException { + if (!"".equals(proot) && !proot.endsWith("/")) { + proot = proot + "/"; + } + String root = context.root + proot; + Map contents = new HashMap(); + Iterator it = context.boundObjects.keySet().iterator(); + while (it.hasNext()) { + String boundName = (String) it.next(); + 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 Object createObject(String strippedName, Object obj); + + public boolean hasMore() { + return this.iterator.hasNext(); + } + + public Object next() { + return this.iterator.next(); + } + + public boolean hasMoreElements() { + return this.iterator.hasNext(); + } + + public Object nextElement() { + return this.iterator.next(); + } + + public void close() { + } + } + + + private static class NameClassPairEnumeration extends AbstractNamingEnumeration { + + private NameClassPairEnumeration(SimpleNamingContext context, String root) throws NamingException { + super(context, root); + } + + protected Object createObject(String strippedName, Object obj) { + return new NameClassPair(strippedName, obj.getClass().getName()); + } + } + + + private static class BindingEnumeration extends AbstractNamingEnumeration { + + private BindingEnumeration(SimpleNamingContext context, String root) throws NamingException { + super(context, root); + } + + protected Object createObject(String strippedName, Object obj) { + return new Binding(strippedName, obj); + } + } + +} diff --git a/org.springframework.test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java b/org.springframework.test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java new file mode 100644 index 00000000000..130de79de5e --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java @@ -0,0 +1,234 @@ +/* + * 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.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. + * + *
Mainly targeted at test environments, where each test case can
+ * configure JNDI appropriately, so that 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.
+ *
+ *
There are various choices for DataSource implementations: + *
Typical usage in bootstrap code: + * + *
+ * SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
+ * DataSource ds = new DriverManagerDataSource(...);
+ * builder.bind("java:comp/env/jdbc/myds", ds);
+ * builder.activate();
+ *
+ * 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:
+ *
+ *
+ * SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.emptyActivatedContextBuilder();
+ * DataSource ds = new DriverManagerDataSource(...);
+ * builder.bind("java:comp/env/jdbc/myds", ds);
+ *
+ * Note that you should not call activate() on a builder from
+ * this factory method, as there will already be an activated one in any case.
+ *
+ * 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 org.springframework.mock.jndi.SimpleNamingContext
+ * @see org.springframework.jdbc.datasource.SingleConnectionDataSource
+ * @see org.springframework.jdbc.datasource.DriverManagerDataSource
+ * @see org.apache.commons.dbcp.BasicDataSource
+ */
+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 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.
+ *
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 assigment to the activated variable.
+ builder.activate();
+ }
+ return activated;
+ }
+
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final Hashtable boundObjects = new Hashtable();
+
+
+ /**
+ * Register the context builder by registering it with the JNDI NamingManager.
+ * Note that once this has been done, new InitialContext() will always
+ * return a context from this factory. Use the 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.
+ *
Call activate() again in order to expose this contexz 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 = null;
+ 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) {
+ IllegalStateException ise =
+ new IllegalStateException("Cannot instantiate specified InitialContextFactory: " + icf);
+ ise.initCause(ex);
+ throw ise;
+ }
+ }
+ }
+
+ // Default case...
+ return new InitialContextFactory() {
+ public Context getInitialContext(Hashtable environment) {
+ return new SimpleNamingContext("", boundObjects, environment);
+ }
+ };
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/jndi/package.html b/org.springframework.test/src/main/java/org/springframework/mock/jndi/package.html
new file mode 100644
index 00000000000..141ee8db0fa
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/jndi/package.html
@@ -0,0 +1,12 @@
+
+
Useful for setting up a simple JNDI environment for test suites +or standalone applications. If e.g. JDBC DataSources get bound to the +same JNDI names as within a J2EE container, both application code and +configuration can me reused without changes. + + + diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java b/org.springframework.test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java new file mode 100644 index 00000000000..04b7320b100 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java @@ -0,0 +1,67 @@ +/* + * 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.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}. + * + *
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 null)
+ */
+ public DelegatingServletInputStream(InputStream sourceStream) {
+ Assert.notNull(sourceStream, "Source InputStream must not be null");
+ this.sourceStream = sourceStream;
+ }
+
+ /**
+ * Return the underlying source stream (never 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/org.springframework.test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java b/org.springframework.test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java
new file mode 100644
index 00000000000..e1625775850
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java
@@ -0,0 +1,72 @@
+/*
+ * 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.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}.
+ *
+ *
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 null)
+ */
+ public DelegatingServletOutputStream(OutputStream targetStream) {
+ Assert.notNull(targetStream, "Target OutputStream must not be null");
+ this.targetStream = targetStream;
+ }
+
+ /**
+ * Return the underlying target stream (never 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/org.springframework.test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java b/org.springframework.test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java
new file mode 100644
index 00000000000..89dba8837a9
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java
@@ -0,0 +1,85 @@
+/*
+ * 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.mock.web;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+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 values = new LinkedList();
+
+
+ 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 getValues() {
+ return Collections.unmodifiableList(this.values);
+ }
+
+ public Object getValue() {
+ return (!this.values.isEmpty() ? this.values.get(0) : 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 null if none found
+ */
+ public static HeaderValueHolder getByName(Map headers, String name) {
+ Assert.notNull(name, "Header name must not be null");
+ for (Iterator it = headers.keySet().iterator(); it.hasNext();) {
+ String headerName = (String) it.next();
+ if (headerName.equalsIgnoreCase(name)) {
+ return (HeaderValueHolder) headers.get(headerName);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/MockBodyContent.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockBodyContent.java
new file mode 100644
index 00000000000..829d109cdae
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockBodyContent.java
@@ -0,0 +1,198 @@
+/*
+ * 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.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.
+ *
+ *
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/org.springframework.test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java new file mode 100644 index 00000000000..fd87175939c --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2006 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. + * + *
Used for testing the web framework; only necessary for testing + * applications when testing custom JSP tags. + * + *
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 + */ +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; + } + + 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); + } + }; + } + + 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); + } + + 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/org.springframework.test/src/main/java/org/springframework/mock/web/MockFilterChain.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockFilterChain.java new file mode 100644 index 00000000000..b4e53bdd438 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockFilterChain.java @@ -0,0 +1,70 @@ +/* + * 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.mock.web; + +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.servlet.FilterConfig} interface. + * + *
Used for testing the web framework; also usefol for testing + * custom {@link javax.servlet.Filter} implementations. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see MockFilterConfig + * @see PassThroughFilterChain + */ +public class MockFilterChain implements FilterChain { + + private ServletRequest request; + + private ServletResponse response; + + + /** + * Records the request and response. + */ + public void doFilter(ServletRequest request, ServletResponse response) { + 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!"); + } + this.request = request; + this.response = response; + } + + /** + * 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; + } + +} diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/MockFilterConfig.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockFilterConfig.java new file mode 100644 index 00000000000..dd77753dd39 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockFilterConfig.java @@ -0,0 +1,103 @@ +/* + * 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.mock.web; + +import java.util.Enumeration; +import java.util.Properties; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.servlet.FilterConfig} interface. + * + *
Used for testing the web framework; also usefol 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 Properties initParameters = new Properties(); + + + /** + * 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.setProperty(name, value); + } + + public String getInitParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.initParameters.getProperty(name); + } + + public Enumeration getInitParameterNames() { + return this.initParameters.keys(); + } + +} diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java new file mode 100644 index 00000000000..7586fa32f13 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -0,0 +1,858 @@ +/* + * 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.mock.web; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +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.Hashtable; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletInputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.servlet.http.HttpServletRequest} + * interface. Supports the Servlet 2.4 API level. + * + *
Used for testing the web framework; also useful for testing
+ * application controllers.
+ *
+ * @author Juergen Hoeller
+ * @author Rod Johnson
+ * @author Rick Evans
+ * @author Mark Fisher
+ * @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 boolean active = true;
+
+
+ //---------------------------------------------------------------------
+ // ServletRequest properties
+ //---------------------------------------------------------------------
+
+ private final Hashtable attributes = new Hashtable();
+
+ private String characterEncoding;
+
+ private byte[] content;
+
+ private String contentType;
+
+ private final Map parameters = new LinkedHashMap(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 Vector locales = new Vector();
+
+ 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;
+
+ /**
+ * The key is the lowercase header name; the value is a {@link HeaderValueHolder} object.
+ */
+ private final Hashtable headers = new Hashtable();
+
+ private String method;
+
+ private String pathInfo;
+
+ private String contextPath = "";
+
+ private String queryString;
+
+ private String remoteUser;
+
+ private final Set userRoles = new HashSet();
+
+ private Principal userPrincipal;
+
+ 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 MockHttpServletRequest with a default
+ * {@link MockServletContext}.
+ * @see MockServletContext
+ */
+ public MockHttpServletRequest() {
+ this(null, "", "");
+ }
+
+ /**
+ * Create a new MockHttpServletRequest with a default
+ * {@link MockServletContext}.
+ * @param method the request method (may be null)
+ * @param requestURI the request URI (may be null)
+ * @see #setMethod
+ * @see #setRequestURI
+ * @see MockServletContext
+ */
+ public MockHttpServletRequest(String method, String requestURI) {
+ this(null, method, requestURI);
+ }
+
+ /**
+ * Create a new MockHttpServletRequest.
+ * @param servletContext the ServletContext that the request runs in
+ * (may be null to use a default MockServletContext)
+ * @see MockServletContext
+ */
+ public MockHttpServletRequest(ServletContext servletContext) {
+ this(servletContext, "", "");
+ }
+
+ /**
+ * Create a new MockHttpServletRequest.
+ * @param servletContext the ServletContext that the request runs in
+ * (may be null to use a default MockServletContext)
+ * @param method the request method (may be null)
+ * @param requestURI the request URI (may be null)
+ * @see #setMethod
+ * @see #setRequestURI
+ * @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 getAttributeNames() {
+ checkActive();
+ return this.attributes.keys();
+ }
+
+ public String getCharacterEncoding() {
+ return this.characterEncoding;
+ }
+
+ public void setCharacterEncoding(String characterEncoding) {
+ this.characterEncoding = characterEncoding;
+ }
+
+ 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;
+ }
+
+ 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.
+ *
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. + *
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
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. + *
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 = (String[]) 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
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. + *
Multiple values can only be stored as list of Strings,
+ * following the Servlet spec (see getHeaders accessor).
+ * As alternative to repeated 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) {
+ 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 (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.getValue().toString() : null);
+ }
+
+ public Enumeration getHeaders(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ return Collections.enumeration(header != null ? header.getValues() : Collections.EMPTY_LIST);
+ }
+
+ public Enumeration getHeaderNames() {
+ return this.headers.keys();
+ }
+
+ 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;
+ }
+
+ /**
+ * @deprecated in favor of addUserRole
+ * @see #addUserRole
+ */
+ public void addRole(String role) {
+ addUserRole(role);
+ }
+
+ public void addUserRole(String role) {
+ this.userRoles.add(role);
+ }
+
+ public boolean isUserInRole(String role) {
+ return this.userRoles.contains(role);
+ }
+
+ public void setUserPrincipal(Principal userPrincipal) {
+ this.userPrincipal = userPrincipal;
+ }
+
+ public Principal getUserPrincipal() {
+ return this.userPrincipal;
+ }
+
+ public String getRequestedSessionId() {
+ HttpSession session = getSession();
+ return (session != null ? session.getId() : null);
+ }
+
+ 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();
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java
new file mode 100644
index 00000000000..986864076d6
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java
@@ -0,0 +1,517 @@
+/*
+ * 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.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.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.util.Assert;
+import org.springframework.web.util.WebUtils;
+
+/**
+ * Mock implementation of the {@link javax.servlet.http.HttpServletResponse}
+ * interface. Supports the Servlet 2.4 API level.
+ *
+ *
Used for testing the web framework; also useful for testing + * application controllers. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 1.0.2 + */ +public class MockHttpServletResponse implements HttpServletResponse { + + public static final int DEFAULT_SERVER_PORT = 80; + + private static final String CHARSET_PREFIX = "charset="; + + + //--------------------------------------------------------------------- + // ServletResponse properties + //--------------------------------------------------------------------- + + private boolean outputStreamAccessAllowed = true; + + private boolean writerAccessAllowed = true; + + private String characterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING; + + private final ByteArrayOutputStream content = new ByteArrayOutputStream(); + + 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 cookies = new ArrayList(); + + /** + * The key is the lowercase header name; the value is a {@link HeaderValueHolder} object. + */ + private final Map headers = new HashMap(); + + private int status = HttpServletResponse.SC_OK; + + private String errorMessage; + + private String redirectedUrl; + + private String forwardedUrl; + + private String includedUrl; + + + //--------------------------------------------------------------------- + // ServletResponse interface + //--------------------------------------------------------------------- + + /** + * Set whether {@link #getOutputStream()} access is allowed. + *
Default is 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.
+ *
Default is 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;
+ }
+
+ 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;
+ }
+
+ 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) {
+ String encoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length());
+ setCharacterEncoding(encoding);
+ }
+ }
+ }
+
+ 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 (Cookie[]) this.cookies.toArray(new Cookie[this.cookies.size()]);
+ }
+
+ public Cookie getCookie(String name) {
+ Assert.notNull(name, "Cookie name must not be null");
+ for (Iterator it = this.cookies.iterator(); it.hasNext();) {
+ Cookie cookie = (Cookie) it.next();
+ 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.
+ * @return the Set of header name Strings, or an empty Set if none
+ */
+ public Set getHeaderNames() {
+ return this.headers.keySet();
+ }
+
+ /**
+ * Return the primary value for the given header, if any.
+ *
Will return the first value in case of multiple values.
+ * @param name the name of the header
+ * @return the associated header value, or 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.
+ * 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, appyling
+ * 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");
+ this.redirectedUrl = url;
+ setCommitted(true);
+ }
+
+ public String getRedirectedUrl() {
+ return this.redirectedUrl;
+ }
+
+ public void setDateHeader(String name, long value) {
+ setHeaderValue(name, new Long(value));
+ }
+
+ public void addDateHeader(String name, long value) {
+ addHeaderValue(name, new Long(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, new Integer(value));
+ }
+
+ public void addIntHeader(String name, int value) {
+ addHeaderValue(name, new Integer(value));
+ }
+
+ private void setHeaderValue(String name, Object value) {
+ doAddHeaderValue(name, value, true);
+ }
+
+ private void addHeaderValue(String name, Object value) {
+ doAddHeaderValue(name, value, 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.includedUrl = includedUrl;
+ }
+
+ public String getIncludedUrl() {
+ return this.includedUrl;
+ }
+
+
+ /**
+ * 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/org.springframework.test/src/main/java/org/springframework/mock/web/MockHttpSession.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockHttpSession.java
new file mode 100644
index 00000000000..0698a7b156b
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockHttpSession.java
@@ -0,0 +1,245 @@
+/*
+ * 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.mock.web;
+
+import java.io.Serializable;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+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.
+ * Supports the Servlet 2.4 API level.
+ *
+ * Used for testing the web framework; also useful for testing
+ * application controllers.
+ *
+ * @author Juergen Hoeller
+ * @author Rod Johnson
+ * @author Mark Fisher
+ * @since 1.0.2
+ */
+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 Hashtable attributes = new Hashtable();
+
+ 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 getAttributeNames() {
+ return this.attributes.keys();
+ }
+
+ public String[] getValueNames() {
+ return (String[]) 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 it = this.attributes.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ String name = (String) entry.getKey();
+ Object value = entry.getValue();
+ it.remove();
+ if (value instanceof HttpSessionBindingListener) {
+ ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, 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;
+ }
+
+
+ /**
+ * 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 state = new HashMap();
+ for (Iterator it = this.attributes.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ String name = (String) entry.getKey();
+ Object value = entry.getValue();
+ it.remove();
+ if (value instanceof Serializable) {
+ state.put(name, 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
+ */
+ public void deserializeState(Serializable state) {
+ Assert.isTrue(state instanceof Map, "Serialized state needs to be of type [java.util.Map]");
+ this.attributes.putAll((Map) state);
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/MockJspWriter.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockJspWriter.java
new file mode 100644
index 00000000000..f9f65de2c2e
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockJspWriter.java
@@ -0,0 +1,192 @@
+/*
+ * 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.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.
+ *
+ * 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/org.springframework.test/src/main/java/org/springframework/mock/web/MockMultipartFile.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockMultipartFile.java
new file mode 100644
index 00000000000..8e12da1ac5a
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockMultipartFile.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2002-2006 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.
+ *
+ * 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/org.springframework.test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java
new file mode 100644
index 00000000000..04f3e278b4c
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.mock.web;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.springframework.util.Assert;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.MultipartHttpServletRequest;
+
+/**
+ * Mock implementation of the
+ * {@link org.springframework.web.multipart.MultipartHttpServletRequest} interface.
+ *
+ * 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
+ * @since 2.0
+ * @see MockMultipartFile
+ */
+public class MockMultipartHttpServletRequest extends MockHttpServletRequest implements MultipartHttpServletRequest {
+
+ private final Map multipartFiles = new LinkedHashMap(4);
+
+
+ /**
+ * 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.put(file.getName(), file);
+ }
+
+ public Iterator getFileNames() {
+ return getFileMap().keySet().iterator();
+ }
+
+ public MultipartFile getFile(String name) {
+ return (MultipartFile) this.multipartFiles.get(name);
+ }
+
+ public Map getFileMap() {
+ return Collections.unmodifiableMap(this.multipartFiles);
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/MockPageContext.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockPageContext.java
new file mode 100644
index 00000000000..1b70bcf586b
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockPageContext.java
@@ -0,0 +1,330 @@
+/*
+ * 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.mock.web;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+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.
+ *
+ * Used for testing the web framework; only necessary for testing
+ * applications when testing custom JSP tags.
+ *
+ * Note: Expects initialization via the constructor rather than via the
+ * 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 MockRequestDispatcher implements RequestDispatcher {
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final String url;
+
+
+ /**
+ * Create a new MockRequestDispatcher for the given URL.
+ * @param url the URL to dispatch to.
+ */
+ public MockRequestDispatcher(String url) {
+ Assert.notNull(url, "URL must not be null");
+ this.url = url;
+ }
+
+
+ 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.url);
+ if (logger.isDebugEnabled()) {
+ logger.debug("MockRequestDispatcher: forwarding to URL [" + this.url + "]");
+ }
+ }
+
+ 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).setIncludedUrl(this.url);
+ if (logger.isDebugEnabled()) {
+ logger.debug("MockRequestDispatcher: including URL [" + this.url + "]");
+ }
+ }
+
+ /**
+ * Obtain the underlying 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/org.springframework.test/src/main/java/org/springframework/mock/web/MockServletConfig.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockServletConfig.java
new file mode 100644
index 00000000000..1647ebff5d4
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockServletConfig.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2002-2006 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.Enumeration;
+import java.util.Properties;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+
+import org.springframework.util.Assert;
+
+/**
+ * Mock implementation of the {@link javax.servlet.ServletConfig} interface.
+ *
+ * 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 Properties initParameters = new Properties();
+
+
+ /**
+ * 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 servletName;
+ }
+
+ public ServletContext getServletContext() {
+ return servletContext;
+ }
+
+ public void addInitParameter(String name, String value) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.initParameters.setProperty(name, value);
+ }
+
+ public String getInitParameter(String name) {
+ Assert.notNull(name, "Parameter name must not be null");
+ return this.initParameters.getProperty(name);
+ }
+
+ public Enumeration getInitParameterNames() {
+ return this.initParameters.keys();
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/MockServletContext.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockServletContext.java
new file mode 100644
index 00000000000..4eebe54bef3
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockServletContext.java
@@ -0,0 +1,356 @@
+/*
+ * 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.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.Hashtable;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Properties;
+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.ObjectUtils;
+import org.springframework.web.util.WebUtils;
+
+/**
+ * Mock implementation of the {@link javax.servlet.ServletContext} interface.
+ *
+ * Used for testing the Spring web framework; only rarely necessary for testing
+ * application controllers. As long as application components don't explicitly
+ * access the ServletContext, ClassPathXmlApplicationContext or
+ * FileSystemXmlApplicationContext can be used to load the context files for testing,
+ * even for DispatcherServlet context definitions.
+ *
+ * For setting up a full WebApplicationContext in a test environment, you can
+ * use XmlWebApplicationContext (or GenericWebApplicationContext), passing in an
+ * appropriate MockServletContext instance. You might want to configure your
+ * MockServletContext with a FileSystemResourceLoader in that case, to make your
+ * resource paths interpreted as relative file system locations.
+ *
+ * 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
+ * FileSystemXmlApplicationContext (which will load straight from the file system)
+ * and XmlWebApplicationContext with an underlying MockServletContext (as long as
+ * the MockServletContext has been configured with a FileSystemResourceLoader).
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @since 1.0.2
+ * @see #MockServletContext(org.springframework.core.io.ResourceLoader)
+ * @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 {
+
+ private static final String TEMP_DIR_SYSTEM_PROPERTY = "java.io.tmpdir";
+
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final ResourceLoader resourceLoader;
+
+ private final String resourceBasePath;
+
+ private String contextPath = "";
+
+ private final Map contexts = new HashMap();
+
+ private final Properties initParameters = new Properties();
+
+ private final Hashtable attributes = new Hashtable();
+
+ private String servletContextName = "MockServletContext";
+
+
+ /**
+ * 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 WAR root directory (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.
+ * @param resourceBasePath the WAR root directory (should not end with a slash)
+ * @param resourceLoader the ResourceLoader to use (or null for the default)
+ */
+ 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));
+ }
+ }
+
+
+ /**
+ * 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 (ServletContext) this.contexts.get(contextPath);
+ }
+
+ public int getMajorVersion() {
+ return 2;
+ }
+
+ public int getMinorVersion() {
+ return 5;
+ }
+
+ public String getMimeType(String filePath) {
+ return MimeTypeResolver.getMimeType(filePath);
+ }
+
+ public Set 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 resourcePaths = new LinkedHashSet(fileList.length);
+ for (int i = 0; i < fileList.length; i++) {
+ String resultPath = actualPath + fileList[i];
+ if (resource.createRelative(fileList[i]).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 null;
+ }
+
+ public Servlet getServlet(String name) {
+ return null;
+ }
+
+ public Enumeration getServlets() {
+ return Collections.enumeration(Collections.EMPTY_SET);
+ }
+
+ public Enumeration getServletNames() {
+ return Collections.enumeration(Collections.EMPTY_SET);
+ }
+
+ 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.getProperty(name);
+ }
+
+ public void addInitParameter(String name, String value) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.initParameters.setProperty(name, value);
+ }
+
+ public Enumeration getInitParameterNames() {
+ return this.initParameters.keys();
+ }
+
+ public Object getAttribute(String name) {
+ Assert.notNull(name, "Attribute name must not be null");
+ return this.attributes.get(name);
+ }
+
+ public Enumeration getAttributeNames() {
+ return this.attributes.keys();
+ }
+
+ 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;
+ }
+
+
+ /**
+ * 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/org.springframework.test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java b/org.springframework.test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java
new file mode 100644
index 00000000000..9fe173b8ad3
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java
@@ -0,0 +1,86 @@
+/*
+ * 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.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 combo
+ * (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/org.springframework.test/src/main/java/org/springframework/mock/web/package.html b/org.springframework.test/src/main/java/org/springframework/mock/web/package.html
new file mode 100644
index 00000000000..1bff67eed1f
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/package.html
@@ -0,0 +1,14 @@
+
+ More convenient to use than dynamic mock objects
+(EasyMock) or
+existing Servlet API mock objects
+(MockObjects).
+
+
+
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java
new file mode 100644
index 00000000000..5ea5cf8b894
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.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.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 MockPortletRequest implements ActionRequest {
+
+ private String characterEncoding;
+
+ private byte[] content;
+
+ private String contentType;
+
+
+ /**
+ * Create a new MockActionRequest with a default {@link MockPortalContext}
+ * and a default {@link MockPortletContext}.
+ * @see MockPortalContext
+ * @see MockPortletContext
+ */
+ public MockActionRequest() {
+ super();
+ }
+
+ /**
+ * 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);
+ }
+
+
+ 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 characterEncoding;
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public int getContentLength() {
+ return (this.content != null ? content.length : -1);
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java
new file mode 100644
index 00000000000..58c65df5219
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java
@@ -0,0 +1,164 @@
+/*
+ * 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.mock.web.portlet;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.LinkedHashMap;
+
+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.core.CollectionFactory;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Mock implementation of the {@link javax.portlet.ActionResponse} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockActionResponse extends MockPortletResponse implements ActionResponse {
+
+ private WindowState windowState;
+
+ private PortletMode portletMode;
+
+ private String redirectedUrl;
+
+ private final Map renderParameters = new LinkedHashMap(16);
+
+
+ /**
+ * 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");
+ }
+ if (!CollectionUtils.contains(getPortalContext().getSupportedWindowStates(), windowState)) {
+ throw new WindowStateException("WindowState not supported", windowState);
+ }
+ this.windowState = windowState;
+ }
+
+ public WindowState getWindowState() {
+ return windowState;
+ }
+
+ public void setPortletMode(PortletMode portletMode) throws PortletModeException {
+ if (this.redirectedUrl != null) {
+ throw new IllegalStateException("Cannot set PortletMode after sendRedirect has been called");
+ }
+ if (!CollectionUtils.contains(getPortalContext().getSupportedPortletModes(), portletMode)) {
+ throw new PortletModeException("PortletMode not supported", portletMode);
+ }
+ this.portletMode = portletMode;
+ }
+
+ public PortletMode getPortletMode() {
+ return portletMode;
+ }
+
+ public void sendRedirect(String url) throws IOException {
+ if (this.windowState != null || this.portletMode != null || !this.renderParameters.isEmpty()) {
+ throw new IllegalStateException(
+ "Cannot call sendRedirect after windowState, portletMode, or renderParameters have been set");
+ }
+ Assert.notNull(url, "Redirect URL must not be null");
+ this.redirectedUrl = url;
+ }
+
+ public String getRedirectedUrl() {
+ return redirectedUrl;
+ }
+
+ public void setRenderParameters(Map parameters) {
+ if (this.redirectedUrl != null) {
+ throw new IllegalStateException("Cannot set render parameters after sendRedirect has been called");
+ }
+ Assert.notNull(parameters, "Parameters Map must not be null");
+ this.renderParameters.clear();
+ for (Iterator it = parameters.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ Assert.isTrue(entry.getKey() instanceof String, "Key must be of type String");
+ Assert.isTrue(entry.getValue() instanceof String[], "Value must be of type String[]");
+ this.renderParameters.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ public void setRenderParameter(String key, String value) {
+ if (this.redirectedUrl != null) {
+ throw new IllegalStateException("Cannot set render parameters after sendRedirect has been called");
+ }
+ 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 String getRenderParameter(String name) {
+ String[] arr = (String[]) this.renderParameters.get(name);
+ return (arr != null && arr.length > 0 ? arr[0] : null);
+ }
+
+ public void setRenderParameter(String key, String[] values) {
+ if (this.redirectedUrl != null) {
+ throw new IllegalStateException("Cannot set render parameters after sendRedirect has been called");
+ }
+ 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[] getRenderParameterValues(String key) {
+ Assert.notNull(key, "Parameter key must not be null");
+ return (String[]) this.renderParameters.get(key);
+ }
+
+ public Iterator getRenderParameterNames() {
+ return this.renderParameters.keySet().iterator();
+ }
+
+ public Map getRenderParameterMap() {
+ return Collections.unmodifiableMap(this.renderParameters);
+ }
+
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java
new file mode 100644
index 00000000000..7505eafa227
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.mock.web.portlet;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.springframework.util.Assert;
+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.
+ *
+ * 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
+ * @since 2.0
+ * @see org.springframework.mock.web.MockMultipartFile
+ */
+public class MockMultipartActionRequest extends MockActionRequest implements MultipartActionRequest {
+
+ private final Map multipartFiles = new LinkedHashMap(4);
+
+
+ /**
+ * 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.put(file.getName(), file);
+ }
+
+ public Iterator getFileNames() {
+ return getFileMap().keySet().iterator();
+ }
+
+ public MultipartFile getFile(String name) {
+ return (MultipartFile) this.multipartFiles.get(name);
+ }
+
+ public Map getFileMap() {
+ return Collections.unmodifiableMap(this.multipartFiles);
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java
new file mode 100644
index 00000000000..ccf411a78e7
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2002-2006 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.Enumeration;
+import java.util.List;
+import java.util.Properties;
+import java.util.Vector;
+
+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 Properties properties = new Properties();
+
+ private final Vector portletModes;
+
+ private final Vector 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 Vector(3);
+ this.portletModes.add(PortletMode.VIEW);
+ this.portletModes.add(PortletMode.EDIT);
+ this.portletModes.add(PortletMode.HELP);
+
+ this.windowStates = new Vector(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 supportedPortletModes, List supportedWindowStates) {
+ this.portletModes = new Vector(supportedPortletModes);
+ this.windowStates = new Vector(supportedWindowStates);
+ }
+
+
+ public String getPortalInfo() {
+ return "MockPortal/1.0";
+ }
+
+ public String getProperty(String name) {
+ return this.properties.getProperty(name);
+ }
+
+ public Enumeration getPropertyNames() {
+ return this.properties.propertyNames();
+ }
+
+ public Enumeration getSupportedPortletModes() {
+ return this.portletModes.elements();
+ }
+
+ public Enumeration getSupportedWindowStates() {
+ return this.windowStates.elements();
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java
new file mode 100644
index 00000000000..a5c0e0452f7
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java
@@ -0,0 +1,114 @@
+/*
+ * 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.mock.web.portlet;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.ResourceBundle;
+
+import javax.portlet.PortletConfig;
+import javax.portlet.PortletContext;
+
+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 HashMap resourceBundles = new HashMap();
+
+ private final Properties initParameters = new Properties();
+
+
+ /**
+ * 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 (ResourceBundle) this.resourceBundles.get(locale);
+ }
+
+ public void addInitParameter(String name, String value) {
+ Assert.notNull(name, "Parameter name must not be null");
+ this.initParameters.setProperty(name, value);
+ }
+
+ public String getInitParameter(String name) {
+ Assert.notNull(name, "Parameter name must not be null");
+ return this.initParameters.getProperty(name);
+ }
+
+ public Enumeration getInitParameterNames() {
+ return this.initParameters.keys();
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java
new file mode 100644
index 00000000000..b823ad1d0e3
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2002-2006 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.Enumeration;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Properties;
+import java.util.Set;
+
+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 Hashtable attributes = new Hashtable();
+
+ private final Properties initParameters = new Properties();
+
+ private String portletContextName = "MockPortletContext";
+
+
+ /**
+ * 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 1;
+ }
+
+ public int getMinorVersion() {
+ return 0;
+ }
+
+ public String getMimeType(String filePath) {
+ return null;
+ }
+
+ 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 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 resourcePaths = new HashSet(fileList.length);
+ for (int i = 0; i < fileList.length; i++) {
+ resourcePaths.add(prefix + fileList[i]);
+ }
+ 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 getAttributeNames() {
+ return this.attributes.keys();
+ }
+
+ 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.setProperty(name, value);
+ }
+
+ public String getInitParameter(String name) {
+ Assert.notNull(name, "Parameter name must not be null");
+ return this.initParameters.getProperty(name);
+ }
+
+ public Enumeration getInitParameterNames() {
+ return this.initParameters.keys();
+ }
+
+ 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 portletContextName;
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java
new file mode 100644
index 00000000000..4d3c7a2d927
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java
@@ -0,0 +1,115 @@
+/*
+ * 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.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 preferences = new LinkedHashMap(16);
+
+ private final Set readOnly = new HashSet();
+
+
+ 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 = (String[]) 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 = (String[]) 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 getNames() {
+ return Collections.enumeration(this.preferences.keySet());
+ }
+
+ public Map 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/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java
new file mode 100644
index 00000000000..8c62d7b0afa
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java
@@ -0,0 +1,471 @@
+/*
+ * 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.mock.web.portlet;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.Vector;
+
+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 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 properties = new LinkedHashMap(16);
+
+ private final Hashtable attributes = new Hashtable();
+
+ private final Map parameters = new LinkedHashMap(16);
+
+ private String authType = null;
+
+ private String contextPath = "";
+
+ private String remoteUser = null;
+
+ private Principal userPrincipal = null;
+
+ private final Set userRoles = new HashSet();
+
+ private boolean secure = false;
+
+ private boolean requestedSessionIdValid = true;
+
+ private final Vector responseContentTypes = new Vector();
+
+ private final Vector locales = new Vector();
+
+ private String scheme = "http";
+
+ private String serverName = "localhost";
+
+ private int serverPort = 80;
+
+
+ /**
+ * 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);
+ }
+
+
+ //---------------------------------------------------------------------
+ // Lifecycle methods
+ //---------------------------------------------------------------------
+
+ /**
+ * 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.
+ * 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 list = new LinkedList();
+ list.add(value);
+ this.properties.put(key, list);
+ }
+
+ /**
+ * Add a single value for the specified property.
+ * 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 oldList = (List) this.properties.get(key);
+ if (oldList != null) {
+ oldList.add(value);
+ }
+ else {
+ List list = new LinkedList();
+ list.add(value);
+ this.properties.put(key, list);
+ }
+ }
+
+ public String getProperty(String key) {
+ Assert.notNull(key, "Property key must not be null");
+ List list = (List) this.properties.get(key);
+ return (list != null && list.size() > 0 ? (String) list.get(0) : null);
+ }
+
+ public Enumeration getProperties(String key) {
+ Assert.notNull(key, "property key must not be null");
+ return Collections.enumeration((List) this.properties.get(key));
+ }
+
+ public Enumeration 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 getAttributeNames() {
+ checkActive();
+ return this.attributes.keys();
+ }
+
+ public void setParameters(Map parameters) {
+ Assert.notNull(parameters, "Parameters Map must not be null");
+ this.parameters.clear();
+ for (Iterator it = parameters.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ Assert.isTrue(entry.getKey() instanceof String, "Key must be of type String");
+ Assert.isTrue(entry.getValue() instanceof String[], "Value must be of type String[]");
+ this.parameters.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ 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 = (String[]) 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 = (String[]) this.parameters.get(name);
+ return (arr != null && arr.length > 0 ? arr[0] : null);
+ }
+
+ public Enumeration getParameterNames() {
+ return Collections.enumeration(this.parameters.keySet());
+ }
+
+ public String[] getParameterValues(String name) {
+ return (String[]) this.parameters.get(name);
+ }
+
+ public Map 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 (String) this.responseContentTypes.get(0);
+ }
+
+ public Enumeration getResponseContentTypes() {
+ return this.responseContentTypes.elements();
+ }
+
+ public void addLocale(Locale locale) {
+ this.locales.add(locale);
+ }
+
+ public void addPreferredLocale(Locale locale) {
+ this.locales.add(0, locale);
+ }
+
+ public Locale getLocale() {
+ return (Locale) this.locales.get(0);
+ }
+
+ public Enumeration getLocales() {
+ return this.locales.elements();
+ }
+
+ public void setScheme(String scheme) {
+ this.scheme = scheme;
+ }
+
+ public String getScheme() {
+ return scheme;
+ }
+
+ public void setServerName(String serverName) {
+ this.serverName = serverName;
+ }
+
+ public String getServerName() {
+ return serverName;
+ }
+
+ public void setServerPort(int serverPort) {
+ this.serverPort = serverPort;
+ }
+
+ public int getServerPort() {
+ return serverPort;
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java
new file mode 100644
index 00000000000..071206fad82
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2006 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.PortletRequestDispatcher;
+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 {
+ Assert.notNull(request, "Request must not be null");
+ Assert.notNull(response, "Response must not be null");
+ if (!(response instanceof MockRenderResponse)) {
+ throw new IllegalArgumentException("MockPortletRequestDispatcher requires MockRenderResponse");
+ }
+ ((MockRenderResponse) response).setIncludedUrl(this.url);
+ if (logger.isDebugEnabled()) {
+ logger.debug("MockPortletRequestDispatcher: including URL [" + this.url + "]");
+ }
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java
new file mode 100644
index 00000000000..d047c05af21
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java
@@ -0,0 +1,110 @@
+/*
+ * 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.mock.web.portlet;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.portlet.PortalContext;
+import javax.portlet.PortletResponse;
+
+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 properties = new LinkedHashMap(16);
+
+
+ /**
+ * 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 portalContext;
+ }
+
+
+ //---------------------------------------------------------------------
+ // PortletResponse methods
+ //---------------------------------------------------------------------
+
+ public void addProperty(String key, String value) {
+ Assert.notNull(key, "Property key must not be null");
+ String[] oldArr = (String[]) 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 getPropertyNames() {
+ return this.properties.keySet();
+ }
+
+ public String getProperty(String key) {
+ Assert.notNull(key, "Property key must not be null");
+ String[] arr = (String[]) 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 (String[]) this.properties.get(key);
+ }
+
+ public String encodeURL(String path) {
+ return path;
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java
new file mode 100644
index 00000000000..6888af250f5
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2002-2006 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.Enumeration;
+import java.util.Hashtable;
+
+import javax.portlet.PortletContext;
+import javax.portlet.PortletSession;
+
+/**
+ * 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 Hashtable portletAttributes = new Hashtable();
+
+ private final Hashtable applicationAttributes = new Hashtable();
+
+ 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 getAttributeNames() {
+ return this.portletAttributes.keys();
+ }
+
+ public Enumeration getAttributeNames(int scope) {
+ if (scope == PortletSession.PORTLET_SCOPE) {
+ return this.portletAttributes.keys();
+ }
+ else if (scope == PortletSession.APPLICATION_SCOPE) {
+ return this.applicationAttributes.keys();
+ }
+ 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;
+ }
+
+ public void invalidate() {
+ this.invalid = true;
+ this.portletAttributes.clear();
+ this.applicationAttributes.clear();
+ }
+
+ public boolean isInvalid() {
+ return 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 portletContext;
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java
new file mode 100644
index 00000000000..2e60b5fbea8
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java
@@ -0,0 +1,193 @@
+/*
+ * 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.mock.web.portlet;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.portlet.PortalContext;
+import javax.portlet.PortletMode;
+import javax.portlet.PortletModeException;
+import javax.portlet.PortletSecurityException;
+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 implements PortletURL {
+
+ public static final String URL_TYPE_RENDER = "render";
+
+ public static final String URL_TYPE_ACTION = "action";
+
+ private static final String ENCODING = "UTF-8";
+
+
+ private final PortalContext portalContext;
+
+ private final String urlType;
+
+ private WindowState windowState;
+
+ private PortletMode portletMode;
+
+ private final Map parameters = new LinkedHashMap(16);
+
+ private boolean secure = false;
+
+
+ /**
+ * 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 void setPortletMode(PortletMode portletMode) throws PortletModeException {
+ if (!CollectionUtils.contains(this.portalContext.getSupportedPortletModes(), portletMode)) {
+ throw new PortletModeException("PortletMode not supported", portletMode);
+ }
+ this.portletMode = portletMode;
+ }
+
+ 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 parameters) {
+ Assert.notNull(parameters, "Parameters Map must not be null");
+ this.parameters.clear();
+ for (Iterator it = parameters.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ Assert.isTrue(entry.getKey() instanceof String, "Key must be of type String");
+ Assert.isTrue(entry.getValue() instanceof String[], "Value must be of type String[]");
+ this.parameters.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ public Set getParameterNames() {
+ return this.parameters.keySet();
+ }
+
+ public String getParameter(String name) {
+ String[] arr = (String[]) this.parameters.get(name);
+ return (arr != null && arr.length > 0 ? arr[0] : null);
+ }
+
+ public String[] getParameterValues(String name) {
+ return (String[]) this.parameters.get(name);
+ }
+
+ public Map getParameterMap() {
+ return Collections.unmodifiableMap(this.parameters);
+ }
+
+ public void setSecure(boolean secure) throws PortletSecurityException {
+ this.secure = secure;
+ }
+
+ public boolean isSecure() {
+ return secure;
+ }
+
+ public String toString() {
+ StringBuffer query = new StringBuffer();
+ query.append(encodeParameter("urlType", this.urlType));
+ if (this.windowState != null) {
+ query.append(";" + encodeParameter("windowState", this.windowState.toString()));
+ }
+ if (this.portletMode != null) {
+ query.append(";" + encodeParameter("portletMode", this.portletMode.toString()));
+ }
+ for (Iterator it = this.parameters.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ String name = (String) entry.getKey();
+ String[] values = (String[]) entry.getValue();
+ query.append(";" + encodeParameter("param_" + name, values));
+ }
+ return (this.secure ? "https:" : "http:") +
+ "//localhost/mockportlet?" + query.toString();
+ }
+
+
+ private String encodeParameter(String name, String value) {
+ try {
+ return URLEncoder.encode(name, ENCODING) + "=" +
+ URLEncoder.encode(value, ENCODING);
+ }
+ catch (UnsupportedEncodingException ex) {
+ return null;
+ }
+ }
+
+ private String encodeParameter(String name, String[] values) {
+ try {
+ StringBuffer buf = new StringBuffer();
+ for (int i = 0, n = values.length; i < n; i++) {
+ buf.append((i > 0 ? ";" : "") +
+ URLEncoder.encode(name, ENCODING) + "=" +
+ URLEncoder.encode(values[i], ENCODING));
+ }
+ return buf.toString();
+ }
+ catch (UnsupportedEncodingException ex) {
+ return null;
+ }
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java
new file mode 100644
index 00000000000..5bb8c54e42d
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.mock.web.portlet;
+
+import javax.portlet.PortalContext;
+import javax.portlet.PortletContext;
+import javax.portlet.PortletMode;
+import javax.portlet.RenderRequest;
+
+/**
+ * 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}.
+ * @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);
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java
new file mode 100644
index 00000000000..c63e9a44599
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2002-2006 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.Locale;
+
+import javax.portlet.PortalContext;
+import javax.portlet.PortletURL;
+import javax.portlet.RenderResponse;
+
+import org.springframework.web.util.WebUtils;
+
+/**
+ * Mock implementation of the {@link javax.portlet.RenderResponse} interface.
+ *
+ * @author John A. Lewis
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class MockRenderResponse extends MockPortletResponse implements RenderResponse {
+
+ private String contentType;
+
+ private String namespace = "MockPortlet";
+
+ private String title;
+
+ 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();
+
+ private boolean committed;
+
+ private String includedUrl;
+
+
+ /**
+ * 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);
+ }
+
+
+ //---------------------------------------------------------------------
+ // RenderResponse methods
+ //---------------------------------------------------------------------
+
+ public String getContentType() {
+ return this.contentType;
+ }
+
+ public PortletURL createRenderURL() {
+ PortletURL url = new MockPortletURL(getPortalContext(), MockPortletURL.URL_TYPE_RENDER);
+ return url;
+ }
+
+ public PortletURL createActionURL() {
+ PortletURL url = new MockPortletURL(getPortalContext(), MockPortletURL.URL_TYPE_ACTION);
+ return url;
+ }
+
+ public String getNamespace() {
+ return this.namespace;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = 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();
+ }
+ if (this.outputStream != null) {
+ 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;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Methods for MockPortletRequestDispatcher
+ //---------------------------------------------------------------------
+
+ public void setIncludedUrl(String includedUrl) {
+ this.includedUrl = includedUrl;
+ }
+
+ public String getIncludedUrl() {
+ return includedUrl;
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/package.html b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/package.html
new file mode 100644
index 00000000000..3fa19b640c7
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/portlet/package.html
@@ -0,0 +1,13 @@
+
+ More convenient to use than dynamic mock objects
+(EasyMock) or
+existing Portlet API mock objects.
+
+
+
diff --git a/org.springframework.test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java
new file mode 100644
index 00000000000..e4dd510458e
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java
@@ -0,0 +1,289 @@
+/*
+ * 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;
+
+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;
+
+/**
+ *
+ * Convenient superclass for JUnit 3.8 based tests depending on a Spring
+ * context. The test instance itself is populated by Dependency Injection.
+ *
+ * Really for integration testing, not unit testing. You should not
+ * normally use the Spring container for unit tests: simply populate your POJOs
+ * in plain JUnit tests!
+ *
+ * This supports two modes of populating the test:
+ * 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.
+ * The default is Note: if the {@link ApplicationContext} for this test instance has not
+ * been configured (e.g., is 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).
+ * 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()
+ */
+ 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/org.springframework.test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java
new file mode 100644
index 00000000000..7ed2ff71b06
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java
@@ -0,0 +1,359 @@
+/*
+ * 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;
+
+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;
+
+/**
+ *
+ * Abstract JUnit 3.8 test class that holds and exposes a single Spring
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}.
+ *
+ * This class will cache contexts based on a context key: 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.
+ *
+ * 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()}.
+ *
+ * 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.
+ *
+ * WARNING: When doing integration tests from within Eclipse, only use
+ * classpath resource URLs. Else, you may see misleading failures when changing
+ * context locations.
+ * 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 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 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.
+ * If you override {@link #contextKey()}, you will typically have to
+ * override this method as well, being able to handle the key type that
+ * 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.
+ * 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 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.
+ * 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}.
+ * 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.
+ * 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.
+ * 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.
+ * 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".
+ * 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.
+ * 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".
+ * The default implementation simply returns
+ * Superclass for JUnit 3.8 test cases using Spring
+ * {@link org.springframework.context.ApplicationContext ApplicationContexts}.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * Normally you won't extend this class directly but rather one of its
+ * subclasses.
+ *
+ * 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));
+ }
+
+ /**
+ *
+ * Determines if the supplied context
+ * By default,
+ * 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 = (ConfigurableApplicationContext) 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.
+ *
+ * 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/org.springframework.test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java
new file mode 100644
index 00000000000..f74753df41c
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java
@@ -0,0 +1,196 @@
+/*
+ * 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;
+
+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;
+import org.springframework.util.StringUtils;
+
+/**
+ * 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.
+ *
+ * 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()
+ */
+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
+ * 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.
+ * Do not use this method to execute DDL if you expect rollback.
+ * @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/org.springframework.test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java
new file mode 100644
index 00000000000..db302973e28
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java
@@ -0,0 +1,397 @@
+/*
+ * 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 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.
+ *
+ * This is useful in a range of circumstances, allowing the following benefits:
+ *
+ * This class is typically very fast, compared to traditional setup/teardown
+ * scripts.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * This base class can also be used without transaction management, if no
+ * PlatformTransactionManager bean is found in the context provided. 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
+ */
+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(final 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.
+ *
+ * This mode works only if dependency checking is turned off in the
+ * {@link AbstractDependencyInjectionSpringContextTests} superclass.
+ */
+ public void setTransactionManager(final PlatformTransactionManager transactionManager) {
+
+ this.transactionManager = transactionManager;
+ }
+
+ /**
+ * Get the default rollback flag for this test.
+ *
+ * @see #setDefaultRollback(boolean)
+ * @return The default rollback flag.
+ */
+ protected boolean isDefaultRollback() {
+
+ return this.defaultRollback;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ *
+ * Determines whether or not to rollback transactions for the current test.
+ *
+ * The default implementation delegates to {@link #isDefaultRollback()}.
+ * Subclasses can override as necessary.
+ *
+ * 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
+ * NB: Not called if there is no transaction management, due to no
+ * transaction manager being provided in the context.
+ *
+ * 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.
+ *
+ * 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
+ * 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 not 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 still active 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.
+ *
+ * NB: 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 not active anymore.
+ *
+ * @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
+ * 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/org.springframework.test/src/main/java/org/springframework/test/AssertThrows.java b/org.springframework.test/src/main/java/org/springframework/test/AssertThrows.java
new file mode 100644
index 00000000000..45b9185bdbc
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/AssertThrows.java
@@ -0,0 +1,256 @@
+/*
+ * 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.Assert;
+import junit.framework.AssertionFailedError;
+
+/**
+ * Simple method object encapsulation of the 'test-for-Exception' scenario (for JUnit).
+ *
+ * Used like so:
+ *
+ * Note: This class requires JDK 1.4 or higher.
+ *
+ * @author Rick Evans
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+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 java.lang.Exception} expected to be
+ * thrown during the execution of the surrounding test
+ * @throws IllegalArgumentException if the supplied The default implementation simply fails the test via a call to
+ * {@link junit.framework.Assert#fail(String)}.
+ * 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() {
+ Assert.fail(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() {
+ StringBuffer sb = new StringBuffer();
+ 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 java.lang.Exception} that was thrown in the body of a test is
+ * an instance of the {@link #getExpectedException()} class (or an
+ * instance of a subclass).
+ * If you want to customise the failure message, consider overriding
+ * {@link #createMessageForWrongThrownExceptionType(Exception)}.
+ * @param actualException the {@link java.lang.Exception} that has been thrown
+ * in the body of a test method (will never be
+ * Java 5 specific subclass of
+ * {@link AbstractTransactionalDataSourceSpringContextTests}, exposing a
+ * {@link SimpleJdbcTemplate} and obeying annotations for transaction control.
+ *
+ * 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.
+ *
+ * The following list constitutes all annotations currently supported by
+ * AbstractAnnotationAwareTransactionalTests:
+ * 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 The default implementation is based on
+ * {@link IfProfileValue @IfProfileValue} semantics.
+ * @param testMethod the test method
+ * @return
+ * Test annotation to indicate that a test method dirties the context
+ * for the current test.
+ *
+ * Using this annotation in conjunction with
+ * {@link AbstractAnnotationAwareTransactionalTests} is less error-prone than
+ * calling
+ * {@link org.springframework.test.AbstractSingleSpringContextTests#setDirty() setDirty()}
+ * explicitly because the call to
+ * 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.
+ *
+ * Note: {@link IfProfileValue @IfProfileValue} can be applied at either the
+ * class or method level.
+ *
+ * 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:
+ *
+ * You can alternatively configure {@link IfProfileValue @IfProfileValue} with
+ * OR semantics for multiple {@link #values() values} as follows
+ * (assuming a {@link ProfileValueSource} has been appropriately configured for
+ * the "test-groups" name):
+ * Note: Assigning values to both {@link #value()} and {@link #values()}
+ * will lead to a configuration conflict.
+ */
+ String value() default "";
+
+ /**
+ * A list of all permissible Note: Assigning values to both {@link #value()} and {@link #values()}
+ * will lead to a configuration conflict.
+ */
+ String[] values() default {};
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/NotTransactional.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/NotTransactional.java
new file mode 100644
index 00000000000..9b3df718069
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/NotTransactional.java
@@ -0,0 +1,37 @@
+/*
+ * 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 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 not transactional.
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ */
+@Target( { ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface NotTransactional {
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java
new file mode 100644
index 00000000000..186e90321e9
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java
@@ -0,0 +1,52 @@
+/*
+ * 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.annotation;
+
+/**
+ *
+ * Strategy interface for retrieving profile values for a given
+ * testing environment.
+ *
+ * Concrete implementations must provide a
+ * Spring provides the following out-of-the-box implementations:
+ *
+ * ProfileValueSourceConfiguration is a class-level annotation which is used to
+ * specify what type of {@link ProfileValueSource} to use when retrieving
+ * profile values configured via the
+ * {@link IfProfileValue @IfProfileValue} annotation.
+ *
+ * The type of {@link ProfileValueSource} to use when retrieving
+ * profile values.
+ *
+ * If
+ * {@link ProfileValueSourceConfiguration @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
+ * Defaults to
+ * Defaults to
+ * Defaults to
+ * Whether or not the transaction for the annotated method should be rolled
+ * back after the method has completed.
+ *
+ * Test-specific annotation to indicate that a test method has to finish
+ * execution in a {@link #millis() specified time period}.
+ *
+ * If the text execution takes longer than the specified time period, then the
+ * test is to be considered failed.
+ *
+ * Note that the time period includes execution of the test method itself, any
+ * {@link Repeat repetitions} of the test, and any set up or
+ * tear down of the test fixture.
+ * Maintains a cache of {@link ApplicationContext contexts} by
+ * {@link Serializable serializable} key. 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
+ * {@link org.springframework.orm.hibernate3.LocalSessionFactoryBean LocalSessionFactoryBean}
+ * for working with Hibernate, may take some time to initialize. Hence it often
+ * makes sense to perform that initialization once.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+class ContextCache {
+
+ /**
+ * Map of context keys to Spring ApplicationContext instances.
+ */
+ private final Map The {@link #getHitCount() hit} and {@link #getMissCount() miss}
+ * counts will be updated accordingly.
+ * @param key the context key (never Generally speaking, you would only call this method only if you change
+ * the state of a singleton bean, potentially affecting future interaction
+ * with the context.
+ * @param key the context key (never The default value is 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()}.
+ *
+ * Concrete implementations must provide a Spring provides the following out-of-the-box implementations:
+ * 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 Configuration locations are generally considered to be classpath
+ * resources by default.
+ * 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}
+ * and {@link javax.annotation.Resource @Resource}.
+ * Any ApplicationContext loaded by a ContextLoader must
+ * 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/org.springframework.test/src/main/java/org/springframework/test/context/TestContext.java b/org.springframework.test/src/main/java/org/springframework/test/context/TestContext.java
new file mode 100644
index 00000000000..eb7e71b56ac
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/TestContext.java
@@ -0,0 +1,319 @@
+/*
+ * 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;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+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.core.AttributeAccessorSupport;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * 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 String DEFAULT_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.support.GenericXmlContextLoader";
+
+ private static final long serialVersionUID = -5827157174866681233L;
+
+ private static final Log logger = LogFactory.getLog(TestContext.class);
+
+ private final ContextCache contextCache;
+
+ private final ContextLoader contextLoader;
+
+ private final String[] locations;
+
+ private final Class> testClass;
+
+ private Object testInstance;
+
+ private Method testMethod;
+
+ private Throwable testException;
+
+
+ /**
+ * Construct a new test context for the supplied {@link Class test class}
+ * and {@link ContextCache context cache} and parses the corresponding
+ * {@link ContextConfiguration @ContextConfiguration} annotation, if present.
+ * @param testClass the {@link Class} object corresponding to the test class
+ * for which the test context should be constructed (must not be Note that the
+ * {@link ContextConfiguration#inheritLocations() inheritLocations} flag of
+ * {@link ContextConfiguration @ContextConfiguration} will be taken into
+ * consideration. Specifically, if the Note: this is a mutable property.
+ * @return the current test instance (may be Note: this is a mutable property.
+ * @return the current test method (may be Note: this is a mutable property.
+ * @return the exception that was thrown, or
+ *
+ * Specifically, a Note that the
+ * {@link TestExecutionListeners#inheritListeners() inheritListeners} flag
+ * of {@link TestExecutionListeners @TestExecutionListeners} will be taken
+ * into consideration. Specifically, if the The managed {@link TestContext} will be updated with the supplied
+ * 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 not be called.
+ * @param testInstance the test instance to prepare (never The managed {@link TestContext} will be updated with the supplied
+ * 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 not be called.
+ * @param testInstance the current test instance (never The managed {@link TestContext} will be updated with the supplied
+ * 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
+ *
+ * Concrete implementations must provide a
+ * Spring provides the following out-of-the-box implementations:
+ * This method should be called immediately after instantiation but prior to
+ * any framework-specific lifecycle callbacks.
+ * @param testContext the test context for the test (never
+ * The {@link TestExecutionListener TestExecutionListeners} to register with
+ * a {@link TestContextManager}.
+ *
+ * Whether or not {@link #value() TestExecutionListeners} from superclasses
+ * should be inherited.
+ *
+ * The default value is
+ * If
+ * Abstract base {@link TestCase} which integrates the
+ * Spring TestContext Framework with explicit
+ * {@link ApplicationContext} testing support in a JUnit 3.8
+ * environment.
+ *
+ * Concrete subclasses:
+ *
+ * The following list constitutes all annotations currently supported directly
+ * by In addition to standard {@link TestCase#runBare()} semantics, this
+ * implementation performs the following:
+ *
+ * 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}.
+ *
+ * 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.
+ *
+ * Concrete subclasses must fulfill the same requirements outlined in
+ * {@link AbstractJUnit38SpringContextTests}.
+ * The script will normally be loaded by classpath. There should be one statement
+ * per line. Any semicolons will be removed. Do not use this method to execute
+ * DDL if you expect rollback.
+ * @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 Support classes for ApplicationContext-based and transactional
+tests run with JUnit 3.8 and the Spring TestContext Framework.
+ * Abstract base test class which integrates the
+ * Spring TestContext Framework with explicit
+ * {@link ApplicationContext} testing support in a JUnit 4.4
+ * environment.
+ *
+ * Concrete subclasses should typically declare a class-level
+ * {@link ContextConfiguration @ContextConfiguration} annotation to configure
+ * the {@link ApplicationContext application context}
+ * {@link ContextConfiguration#locations() resource locations}.
+ * If your test does not need to load an application context, you may choose
+ * to omit the {@link ContextConfiguration @ContextConfiguration} declaration
+ * and to configure the appropriate
+ * {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners}
+ * manually.
+ *
+ * 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 @ContextConfiguration},
+ * {@link TestExecutionListeners @TestExecutionListeners}, etc.
+ *
+ * Abstract {@link 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
+ * {@link ApplicationContext application context}.
+ *
+ * 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.
+ *
+ * Concrete subclasses must fulfill the same requirements outlined in
+ * {@link AbstractJUnit4SpringContextTests}.
+ *
+ * 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 @ContextConfiguration},
+ * {@link TestExecutionListeners @TestExecutionListeners},
+ * {@link Transactional @Transactional}, etc.
+ * The script will normally be loaded by classpath. There should be one statement
+ * per line. Any semicolons will be removed. Do not use this method to execute
+ * DDL if you expect rollback.
+ * @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
+ * SpringJUnit4ClassRunner is a custom extension of {@link JUnit4ClassRunner}
+ * which provides functionality of the Spring TestContext Framework
+ * to standard JUnit 4.4+ tests by means of the {@link TestContextManager} and
+ * associated support classes and annotations.
+ *
+ * The following list constitutes all annotations currently supported directly
+ * by SpringJUnit4ClassRunner.
+ * (Note that additional annotations may be supported by various
+ * {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners})
+ *
+ *
+ * Due to method and field visibility constraints, the code of
+ * SpringTestMethod also provides support for
+ * {@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}
+ * and {@link ExpectedException @ExpectedException}. See {@link #isIgnored()}
+ * and {@link #getExpectedException()} for further details.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ */
+class SpringTestMethod {
+
+ private static final Log logger = LogFactory.getLog(SpringTestMethod.class);
+
+ private final Method method;
+
+ private final TestClass testClass;
+
+
+ /**
+ * Constructs a test method for the supplied {@link Method method} and
+ * {@link TestClass test class}; and retrieves the configured (or default)
+ * {@link ProfileValueSource}.
+ * @param method The test method
+ * @param testClass the test class
+ */
+ public SpringTestMethod(Method method, TestClass testClass) {
+ this.method = method;
+ this.testClass = testClass;
+ }
+
+
+ /**
+ * Determine if this test method is {@link Test#expected() expected} to
+ * throw an exception.
+ */
+ public boolean expectsException() {
+ return (getExpectedException() != null);
+ }
+
+ /**
+ * Get the {@link After @After} methods for this test method.
+ */
+ public List 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 Supports JUnit's {@link Test#timeout() @Test(timeout=...)} annotation.
+ * @return the timeout, or Support classes for ApplicationContext-based and transactional
+tests run with JUnit 4.4 and the Spring TestContext Framework. This package contains the Spring TestContext Framework
+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 3.8 environment can also be applied to tests written with JUnit
+4.4, TestNG, etc. 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. For example, if the supplied class is Subclasses can override this method to implement a different
+ * default location generation strategy.
+ * @param clazz the class for which the default locations are to be generated
+ * @return an array of default application context resource locations
+ * @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");
+ return new String[] { ResourceUtils.CLASSPATH_URL_PREFIX + "/" +
+ ClassUtils.convertClassNameToResourcePath(clazz.getName()) + suffix };
+ }
+
+ /**
+ * Generate a modified version of the supplied locations array and returns it.
+ * A plain path, e.g. "context.xml", will be treated as a
+ * classpath resource from the same package in which the specified class is
+ * defined. A path starting with a slash is treated as a fully qualified
+ * class path location, e.g.:
+ * "/org/springframework/whatever/foo.xml". A path which
+ * references a URL (e.g., a path prefixed with
+ * {@link ResourceUtils#CLASSPATH_URL_PREFIX classpath:},
+ * {@link ResourceUtils#FILE_URL_PREFIX file:}, Subclasses can override this method to implement a different
+ * location modification 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
+ */
+ 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("/")) {
+ modifiedLocations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + path;
+ }
+ else if (!ResourcePatternUtils.isUrl(path)) {
+ modifiedLocations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + "/"
+ + StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(clazz) + "/" + path);
+ }
+ else {
+ modifiedLocations[i] = StringUtils.cleanPath(path);
+ }
+ }
+ return modifiedLocations;
+ }
+
+
+ /**
+ * Determine whether or not default resource locations should be
+ * generated if the Can be overridden by subclasses to change the default behavior.
+ * @return always Must be implemented by subclasses.
+ * @return the resource suffix; should not be Concrete subclasses must provide an appropriate
+ * {@link #createBeanDefinitionReader(GenericApplicationContext) BeanDefinitionReader}.
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see #loadContext(String...)
+ */
+public abstract class AbstractGenericContextLoader extends AbstractContextLoader {
+
+ protected static final Log logger = LogFactory.getLog(AbstractGenericContextLoader.class);
+
+
+ /**
+ * Loads a Spring ApplicationContext from the supplied Implementation details:
+ * Subclasses must provide an appropriate implementation of
+ * {@link #createBeanDefinitionReader(GenericApplicationContext)}.
+ * @return a new application context
+ * @see org.springframework.test.context.ContextLoader#loadContext
+ * @see GenericApplicationContext
+ * @see #customizeBeanFactory(DefaultListableBeanFactory)
+ * @see #createBeanDefinitionReader(GenericApplicationContext)
+ * @see BeanDefinitionReader
+ */
+ public final ConfigurableApplicationContext loadContext(String... locations) throws Exception {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Loading ApplicationContext for locations [" +
+ 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 ContextLoader.
+ * Called before> bean definitions are read.
+ * 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 #loadContext
+ * @see org.springframework.context.support.GenericApplicationContext#setResourceLoader
+ * @see org.springframework.context.support.GenericApplicationContext#setId
+ */
+ protected void prepareContext(GenericApplicationContext context) {
+ }
+
+ /**
+ * Customize the internal bean factory of the ApplicationContext created by
+ * this ContextLoader.
+ * The default implementation is empty but can be overridden in subclasses
+ * to customize DefaultListableBeanFactory's standard settings.
+ * @param beanFactory the bean factory created by this ContextLoader
+ * @see #loadContext
+ * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowBeanDefinitionOverriding(boolean)
+ * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowEagerClassLoading(boolean)
+ * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowCircularReferences(boolean)
+ * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping(boolean)
+ */
+ protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
+ }
+
+ /**
+ * Factory method for creating new {@link BeanDefinitionReader}s for
+ * loading bean definitions into the supplied
+ * {@link GenericApplicationContext context}.
+ * @param context the context for which the BeanDefinitionReader should be created
+ * @return a BeanDefinitionReader for the supplied context
+ * @see #loadContext
+ * @see BeanDefinitionReader
+ */
+ protected abstract BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context);
+
+ /**
+ * Customize the {@link GenericApplicationContext} created by this ContextLoader
+ * after bean definitions have been loaded into the context but
+ * before the context is refreshed.
+ * 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(String...)
+ */
+ protected void customizeContext(GenericApplicationContext context) {
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java b/org.springframework.test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java
new file mode 100644
index 00000000000..4b2ac9223d4
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java
@@ -0,0 +1,57 @@
+/*
+ * 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.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 empty. Can be overridden by
+ * subclasses as necessary.
+ */
+ public void prepareTestInstance(TestContext testContext) throws Exception {
+ /* no-op */
+ }
+
+ /**
+ * The default implementation is empty. Can be overridden by
+ * subclasses as necessary.
+ */
+ public void beforeTestMethod(TestContext testContext) throws Exception {
+ /* no-op */
+ }
+
+ /**
+ * The default implementation is empty. Can be overridden by
+ * subclasses as necessary.
+ */
+ public void afterTestMethod(TestContext testContext) throws Exception {
+ /* no-op */
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java b/org.springframework.test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java
new file mode 100644
index 00000000000..36a96743220
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java
@@ -0,0 +1,115 @@
+/*
+ * 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.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;
+
+/**
+ * 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 between execution of individual test
+ * methods.
+ * 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).
+ * 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()}.
+ * 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
+ * Concrete implementation of {@link AbstractGenericContextLoader} which reads
+ * bean definitions from Java {@link Properties} resources.
+ *
+ * Creates a new {@link PropertiesBeanDefinitionReader}.
+ *
+ * Concrete implementation of {@link AbstractGenericContextLoader} which reads
+ * bean definitions from XML resources.
+ *
+ * Creates a new {@link XmlBeanDefinitionReader}.
+ * Support classes for the Spring TestContext Framework.
+ * Abstract base test class which integrates the
+ * Spring TestContext Framework with explicit
+ * {@link ApplicationContext} testing support in a TestNG
+ * environment.
+ *
+ * Concrete subclasses:
+ *
+ * Abstract {@link 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
+ * {@link ApplicationContext application context}.
+ *
+ * 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.
+ *
+ * Concrete subclasses must fulfill the same requirements outlined in
+ * {@link AbstractTestNGSpringContextTests}.
+ * The script will normally be loaded by classpath. There should be one statement
+ * per line. Any semicolons will be removed. Do not use this method to execute
+ * DDL if you expect rollback.
+ * @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 Support classes for ApplicationContext-based and transactional
+tests run with TestNG and the Spring TestContext Framework.
+ * Test annotation to indicate that the annotated
+ * The
+ * Test annotation to indicate that the annotated
+ * The
+ *
+ * Changes to the database during a test run with @Transactional will be
+ * run within a transaction that will, by default, be automatically
+ * rolled back after completion of the test; whereas, changes to the
+ * database during a test run with @NotTransactional will not
+ * be run within a transaction. Similarly, test methods that are not annotated
+ * with either @Transactional (at the class or method level) or
+ * @NotTransactional will not be run within a transaction.
+ *
+ * Transactional commit and rollback behavior can be configured via the
+ * class-level {@link TransactionConfiguration @TransactionConfiguration} and
+ * method-level {@link Rollback @Rollback} annotations.
+ * {@link TransactionConfiguration @TransactionConfiguration} also provides
+ * configuration of the bean name of the {@link PlatformTransactionManager} that
+ * is to be used to drive transactions.
+ *
+ * When executing transactional tests, it is sometimes useful to be able execute
+ * certain set up or tear down code outside of a
+ * transaction. Note that if a {@link BeforeTransaction @BeforeTransaction method} fails,
+ * remaining {@link BeforeTransaction @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
+ */
+ @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;
+ }
+
+ TransactionAttribute transactionAttribute =
+ this.attributeSource.getTransactionAttribute(testMethod, testContext.getTestClass());
+ TransactionDefinition transactionDefinition = null;
+ if (transactionAttribute != null) {
+ transactionDefinition = new DelegatingTransactionAttribute(transactionAttribute) {
+ public String getName() {
+ return testMethod.getName();
+ }
+ };
+ }
+
+ if (transactionDefinition != null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Explicit transaction definition [" + transactionDefinition +
+ "] found for test context [" + testContext + "]");
+ }
+ TransactionContext txContext =
+ new TransactionContext(getTransactionManager(testContext), transactionDefinition);
+ 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 @AfterTransaction methods}.
+ * {@link AfterTransaction @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 @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 not
+ * be given a chance to execute.
+ * @param testContext the current test context
+ */
+ protected void runBeforeTransactionMethods(TestContext testContext) throws Exception {
+ try {
+ List 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 commit or rollback 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}.
+ * @param testContext the test context for which the transaction manager
+ * should be retrieved
+ * @return the transaction manager to use, or 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 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 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 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
+ * Retrieves the {@link TransactionConfigurationAttributes} for the
+ * specified {@link Class class} which may optionally declare or inherit a
+ * {@link TransactionConfiguration @TransactionConfiguration}. If a
+ * {@link TransactionConfiguration} annotation is not present for the
+ * supplied class, the default values for attributes defined in
+ * {@link TransactionConfiguration} will be used instead.
+ * @param clazz the Class object corresponding to the test class for which
+ * the configuration attributes should be retrieved
+ * @return a new TransactionConfigurationAttributes instance
+ */
+ private TransactionConfigurationAttributes retrieveTransactionConfigurationAttributes(Class> clazz) {
+ Class Transactional support classes for the Spring TestContext
+Framework. The script will normally be loaded by classpath. There should be one statement
+ * per line. Any semicolons will be removed. Do not use this method to execute
+ * DDL if you expect rollback.
+ * @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 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.
+ * Do not use this method to execute DDL if you expect rollback.
+ * @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 The script will normally be loaded by classpath. There should be one statement
+ * per line. Any semicolons will be removed. Do not use this method to execute
+ * DDL if you expect rollback.
+ * @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 Override this method to point to a specific Exposes an EntityManagerFactory and a shared EntityManager.
+ * Requires an EntityManagerFactory to be injected, plus the DataSource and
+ * JpaTransactionManager through the superclass.
+ *
+ * When using Xerces, make sure a post 2.0.2 version is available on the classpath
+ * to avoid a critical
+ * bug
+ * that leads to StackOverflow. Maven users are likely to encounter this problem since
+ * 2.0.2 is used by default.
+ *
+ * A workaround is to explicitly specify the Xerces version inside the Maven POM:
+ * 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
+ public void runBare() throws Throwable {
+ 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 not 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;
+
+ 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;
+
+ 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/org.springframework.test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java b/org.springframework.test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java
new file mode 100644
index 00000000000..a6baeaffcd2
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java
@@ -0,0 +1,55 @@
+/*
+ * 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.jpa;
+
+import org.springframework.instrument.classloading.ResourceOverridingShadowingClassLoader;
+
+/**
+ * Subclass of ShadowingClassLoader that overrides attempts to
+ * locate This class must not 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 The superclasses in this package are ideal for integration testing.
+Unit testing should not normally involve the Spring container,
+but should test classes in isolation.
+
+
+
diff --git a/org.springframework.test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java b/org.springframework.test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java
new file mode 100644
index 00000000000..149e44a38a7
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java
@@ -0,0 +1,251 @@
+/*
+ * 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.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.ReflectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ *
+ * ReflectionTestUtils is a collection of reflection-based utility methods for
+ * use in unit and integration testing scenarios.
+ *
+ * There are often situations in which it would be beneficial to be able to set
+ * a non- This method traverses the class hierarchy in search of the desired field.
+ * In addition, an attempt will be made to make non- This method traverses the class hierarchy in search of the desired field.
+ * In addition, an attempt will be made to make non- This method traverses the class hierarchy in search of the desired field.
+ * In addition, an attempt will be made to make non- This method traverses the class hierarchy in search of the desired
+ * method. In addition, an attempt will be made to make non- In addition, this method supports JavaBean-style property
+ * names. For example, if you wish to set the This method traverses the class hierarchy in search of the desired
+ * method. In addition, an attempt will be made to make non- In addition, this method supports JavaBean-style property
+ * names. For example, if you wish to set the This method traverses the class hierarchy in search of the desired
+ * method. In addition, an attempt will be made to make non- In addition, this method supports JavaBean-style property
+ * names. For example, if you wish to get the
+ * Convenient JUnit 3.8 base class for tests dealing with Spring Web MVC
+ * {@link org.springframework.web.servlet.ModelAndView ModelAndView} objects.
+ *
+ * All
+ * Consider the use of {@link ModelAndViewAssert} with JUnit 4 and TestNG.
+ *
+ * A collection of assertions intended to simplify testing scenarios
+ * dealing with Spring Web MVC
+ * {@link org.springframework.web.servlet.ModelAndView ModelAndView} objects.
+ * Intended for use with JUnit 4 and TestNG.
+ *
+ * All
+The Spring Data Binding framework, an internal library used by Spring Web Flow.
+null if none
+ */
+ public Object getHeader(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 getHeaders(String name) {
+ HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
+ return (header != null ? header.getValues() : Collections.EMPTY_LIST);
+ }
+
+ /**
+ * The default implementation returns the given URL String as-is.
+ * PageContext.initialize method. Does not support writing to
+ * a JspWriter, request dispatching, and handlePageException calls.
+ *
+ * @author Juergen Hoeller
+ * @since 1.0.2
+ */
+public class MockPageContext extends PageContext {
+
+ private final ServletContext servletContext;
+
+ private final HttpServletRequest request;
+
+ private final HttpServletResponse response;
+
+ private final ServletConfig servletConfig;
+
+ private final Hashtable attributes = new Hashtable();
+
+ 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 getAttributeNames() {
+ return this.attributes.keys();
+ }
+
+ public Enumeration 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 VariableResolver getVariableResolver() {
+ return null;
+ }
+
+ public HttpSession getSession() {
+ return this.request.getSession();
+ }
+
+ public Object getPage() {
+ throw new UnsupportedOperationException("getPage");
+ }
+
+ public ServletRequest getRequest() {
+ return this.request;
+ }
+
+ public ServletResponse getResponse() {
+ return this.response;
+ }
+
+ public Exception getException() {
+ throw new UnsupportedOperationException("getException");
+ }
+
+ public ServletConfig getServletConfig() {
+ return this.servletConfig;
+ }
+
+ public ServletContext getServletContext() {
+ return this.servletContext;
+ }
+
+ public void forward(String url) throws ServletException, IOException {
+ throw new UnsupportedOperationException("forward");
+ }
+
+ public void include(String url) throws ServletException, IOException {
+ throw new UnsupportedOperationException("include");
+ }
+
+ public void include(String url, boolean flush) throws ServletException, IOException {
+ throw new UnsupportedOperationException("include");
+ }
+
+ public void handlePageException(Exception ex) throws ServletException, IOException {
+ throw new UnsupportedOperationException("handlePageException");
+ }
+
+ public void handlePageException(Throwable ex) throws ServletException, IOException {
+ throw new UnsupportedOperationException("handlePageException");
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java b/org.springframework.test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java
new file mode 100644
index 00000000000..c3ed4d54bb7
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java
@@ -0,0 +1,91 @@
+/*
+ * 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.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.
+ *
+ *
+ *
+ *
+ * @author Rod Johnson
+ * @author Rob Harrop
+ * @author Rick Evans
+ * @author Sam Brannen
+ * @since 1.1.1
+ * @see #setDirty
+ * @see #contextKey
+ * @see #getContext
+ * @see #getConfigLocations
+ */
+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
+ * populateProtectedVariables property to true in
+ * the constructor to switch on Field Injection.
+ * 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.
+ * 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.
+ * null), dependency injection
+ * will naturally not 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).
+ * 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.
+ * setUp()
+ * method, which is final in this class.
+ * 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()}.
+ * contextKey() returns.
+ * @see #getConfigLocations()
+ */
+ protected ConfigurableApplicationContext loadContext(Object key) throws Exception {
+ return loadContextLocations((String[]) key);
+ }
+
+ /**
+ * Load a Spring ApplicationContext from the given config locations.
+ * 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.
+ * null.
+ * @return an array of config locations
+ * @see #getConfigPath()
+ * @see java.lang.Class#getResource(String)
+ */
+ protected String getConfigPath() {
+ return null;
+ }
+
+ /**
+ * Return the ApplicationContext that this base class manages; may be
+ * 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/org.springframework.test/src/main/java/org/springframework/test/AbstractSpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/AbstractSpringContextTests.java
new file mode 100644
index 00000000000..37982f95a2e
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/AbstractSpringContextTests.java
@@ -0,0 +1,182 @@
+/*
+ * 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 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;
+
+/**
+ * key is empty.
+ * null values, empty strings, and zero-length
+ * arrays are considered empty.
+ * 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 null.
+ * @return the corresponding ApplicationContext instance (potentially
+ * cached), or null if the provided key
+ * is empty.
+ */
+ protected final ConfigurableApplicationContext getContext(Object key) throws Exception {
+
+ if (isContextKeyEmpty(key)) {
+ return null;
+ }
+
+ String keyString = contextKeyString(key);
+ ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) 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.
+ * 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 permanent 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.
+ *
+ *
+ * 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, before the transaction created by
+ * this class. Only invoked if there is 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, within the transaction created by
+ * this class.
+ * super.onTearDown() as
+ * part of your method implementation.
+ * complete and {@link #isRollback() rollback} flags.
+ *
+ * // 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...
+ * }
+ * }
+ *
+ * The test for the above bad argument path can be expressed using the
+ * {@link AssertThrows} class like so:
+ *
+ *
+ * public class FooTest {
+ * public void testSomeBusinessLogicBadArgumentPath() {
+ * new AssertThrows(IllegalArgumentException.class) {
+ * public void test() {
+ * new Foo().someBusinessLogic(null);
+ * }
+ * }.runTest();
+ * }
+ * }
+ *
+ * This will result in the test passing if the Foo.someBusinessLogic(..)
+ * method threw an {@link java.lang.IllegalArgumentException}; if it did not, the
+ * test would fail with the following message:
+ *
+ *
+ * "Must have thrown a [class java.lang.IllegalArgumentException]"
+ *
+ * If the wrong type of {@link java.lang.Exception} was thrown, the
+ * test will also fail, this time with a message similar to the following:
+ *
+ *
+ * "junit.framework.AssertionFailedError: Was expecting a [class java.lang.UnsupportedOperationException] to be thrown, but instead a [class java.lang.IllegalArgumentException] was thrown"
+ *
+ * The test for the correct {@link java.lang.Exception} respects polymorphism,
+ * so you can test that any old {@link java.lang.Exception} is thrown like so:
+ *
+ *
+ * public class FooTest {
+ * public void testSomeBusinessLogicBadArgumentPath() {
+ * // any Exception will do...
+ * new AssertThrows(Exception.class) {
+ * public void test() {
+ * new Foo().someBusinessLogic(null);
+ * }
+ * }.runTest();
+ * }
+ * }
+ *
+ * You might want to compare this class with the
+ * {@link junit.extensions.ExceptionTestCase} class.
+ *
+ * expectedException is
+ * null; or if said argument is not an {@link java.lang.Exception}-derived class
+ */
+ public AssertThrows(Class expectedException) {
+ this(expectedException, null);
+ }
+
+ /**
+ * Create a new instance of the {@link AssertThrows} class.
+ * @param expectedException the {@link java.lang.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 null)
+ * @throws IllegalArgumentException if the supplied expectedException is
+ * null; or if said argument is not an {@link java.lang.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 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 not thrown.
+ * null)
+ */
+ protected void checkExceptionExpectations(Exception actualException) {
+ if (!getExpectedException().isAssignableFrom(actualException.getClass())) {
+ AssertionFailedError error =
+ new AssertionFailedError(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) {
+ StringBuffer sb = new StringBuffer();
+ 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 null if none
+ */
+ public final Exception getActualException() {
+ return this.actualException;
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/ConditionalTestCase.java b/org.springframework.test/src/main/java/org/springframework/test/ConditionalTestCase.java
new file mode 100644
index 00000000000..6ea36a8626a
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/ConditionalTestCase.java
@@ -0,0 +1,98 @@
+/*
+ * 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
+ */
+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() + " 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/org.springframework.test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java
new file mode 100644
index 00000000000..c7c0e3df790
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java
@@ -0,0 +1,309 @@
+/*
+ * 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.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;
+
+/**
+ *
+ *
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+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)}.
+ * 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
+ * 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 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() + " 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 testMethod should
+ * run in the current environment.
+ * true if the test is disabled 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 rollback 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/org.springframework.test/src/main/java/org/springframework/test/annotation/DirtiesContext.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/DirtiesContext.java
new file mode 100644
index 00000000000..288c5b0d42f
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/DirtiesContext.java
@@ -0,0 +1,51 @@
+/*
+ * 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 java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * setDirty() is guaranteed to
+ * occur, even if the test failed. If only a particular code path in the test
+ * dirties the context, prefer calling setDirty() explicitly --
+ * and take care!
+ *
+ * {@link IfProfileValue @IfProfileValue}(name="java.vendor", value="Sun Microsystems Inc.")
+ * testSomething() {
+ * // ...
+ * }
+ *
+ *
+ *
+ * {@link IfProfileValue @IfProfileValue}(name="test-groups", values={"unit-tests", "integration-tests"})
+ * public void testWhichRunsForUnitOrIntegrationTestGroups() {
+ * // ...
+ * }
+ *
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ * @see ProfileValueSource
+ * @see ProfileValueSourceConfiguration
+ * @see ProfileValueUtils
+ * @see AbstractAnnotationAwareTransactionalTests
+ * @see org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests
+ * @see org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests
+ * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+public @interface IfProfileValue {
+
+ /**
+ * The name of the profile value against which to test.
+ */
+ String name();
+
+ /**
+ * A single, permissible value of the profile value
+ * for the given {@link #name() name}.
+ * values of the
+ * profile value for the given {@link #name() name}.
+ * public no-args
+ * constructor.
+ *
+ *
+ *
+ * @author Rod Johnson
+ * @author Sam Brannen
+ * @since 2.0
+ * @see ProfileValueSourceConfiguration
+ * @see IfProfileValue
+ * @see ProfileValueUtils
+ */
+public interface ProfileValueSource {
+
+ /**
+ * Get the profile value indicated by the specified key.
+ * @param key the name of the profile value
+ * @return the String value of the profile value, or null
+ * if there is no profile value with that key
+ */
+ String get(String key);
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java
new file mode 100644
index 00000000000..dafbe4f1a16
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java
@@ -0,0 +1,56 @@
+/*
+ * 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 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;
+
+/**
+ * testClass is enabled
+ * in the current environment, as specified by the
+ * {@link IfProfileValue @IfProfileValue} annotation at the class level.
+ * true if no
+ * {@link IfProfileValue @IfProfileValue} annotation is declared.
+ *
+ * @param testClass the test class
+ * @return true if the test is enabled in the
+ * current environment
+ */
+ public static boolean isTestEnabledInThisEnvironment(Class> testClass) {
+ IfProfileValue ifProfileValue = testClass.getAnnotation(IfProfileValue.class);
+ if (ifProfileValue == null) {
+ return true;
+ }
+ ProfileValueSource profileValueSource = retrieveProfileValueSource(testClass);
+ return isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue);
+ }
+
+ /**
+ * Determine if the supplied testMethod is enabled
+ * in the current environment, as specified by the
+ * {@link IfProfileValue @IfProfileValue} annotation, which may be declared
+ * on the test method itself or at the class level.
+ * true if no
+ * {@link IfProfileValue @IfProfileValue} annotation is declared.
+ *
+ * @param testMethod the test method
+ * @param testClass the test class
+ * @return true if the test is enabled in the
+ * current environment
+ */
+ public static boolean isTestEnabledInThisEnvironment(Method testMethod, Class> testClass) {
+ IfProfileValue ifProfileValue = testMethod.getAnnotation(IfProfileValue.class);
+ if (ifProfileValue == null) {
+ ifProfileValue = testClass.getAnnotation(IfProfileValue.class);
+ if (ifProfileValue == null) {
+ return true;
+ }
+ }
+ ProfileValueSource profileValueSource = retrieveProfileValueSource(testClass);
+ return isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue);
+ }
+
+ /**
+ * Determine if the supplied testMethod is enabled
+ * in the current environment, as specified by the
+ * {@link IfProfileValue @IfProfileValue} annotation, which may be declared
+ * on the test method itself or at the class level.
+ * true if no
+ * {@link IfProfileValue @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 true if the test is enabled in the
+ * current environment
+ */
+ public static boolean isTestEnabledInThisEnvironment(ProfileValueSource profileValueSource, Method testMethod,
+ Class> testClass) {
+
+ IfProfileValue ifProfileValue = testMethod.getAnnotation(IfProfileValue.class);
+ if (ifProfileValue == null) {
+ ifProfileValue = testClass.getAnnotation(IfProfileValue.class);
+ if (ifProfileValue == null) {
+ return true;
+ }
+ }
+ return isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue);
+ }
+
+ /**
+ * Determine if the value (or one of the values)
+ * in the supplied {@link IfProfileValue @IfProfileValue} annotation is
+ * enabled in the current environment.
+ *
+ * @param profileValueSource the ProfileValueSource to use to determine if
+ * the test is enabled
+ * @param ifProfileValue the annotation to introspect
+ * @return true if the test is enabled in the
+ * current environment
+ */
+ private static boolean isTestEnabledInThisEnvironment(ProfileValueSource profileValueSource,
+ IfProfileValue ifProfileValue) {
+
+ 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/org.springframework.test/src/main/java/org/springframework/test/annotation/Repeat.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/Repeat.java
new file mode 100644
index 00000000000..b2e832fac3f
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/Repeat.java
@@ -0,0 +1,43 @@
+/*
+ * 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.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.
+ * true, the transaction will be rolled back;
+ * otherwise, the transaction will be committed.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ */
+@Target( { ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Rollback {
+
+ /**
+ * null)
+ */
+ boolean contains(String key) {
+ Assert.notNull(key, "Key must not be null");
+ return this.contextKeyToContextMap.containsKey(key);
+ }
+
+ /**
+ * Obtain a cached ApplicationContext for the given key.
+ * null)
+ * @return the corresponding ApplicationContext instance,
+ * or null if not found in the cache.
+ * @see #remove
+ */
+ ApplicationContext get(String key) {
+ Assert.notNull(key, "Key must not be null");
+ ApplicationContext context = this.contextKeyToContextMap.get(key);
+ if (context == null) {
+ incrementMissCount();
+ }
+ else {
+ incrementHitCount();
+ }
+ return context;
+ }
+
+ /**
+ * Increment the hit count by one. A hit is an access to the
+ * cache, which returned a non-null context for a queried key.
+ */
+ private void incrementHitCount() {
+ this.hitCount++;
+ }
+
+ /**
+ * Increment the miss count by one. A miss is an access to the
+ * cache, which returned a null context for a queried key.
+ */
+ private void incrementMissCount() {
+ this.missCount++;
+ }
+
+ /**
+ * Get the overall hit count for this cache. A hit is an access
+ * to the cache, which returned a non-null context for a queried key.
+ */
+ int getHitCount() {
+ return this.hitCount;
+ }
+
+ /**
+ * Get the overall miss count for this cache. A miss is an
+ * access to the cache, which returned a null context for a
+ * queried key.
+ */
+ int getMissCount() {
+ return this.missCount;
+ }
+
+ /**
+ * Explicitly add a ApplicationContext instance to the cache under the given key.
+ * @param key the context key (never null)
+ * @param context the ApplicationContext instance (never null)
+ */
+ void put(String key, ApplicationContext context) {
+ Assert.notNull(key, "Key must not be null");
+ Assert.notNull(context, "ApplicationContext must not be null");
+ this.contextKeyToContextMap.put(key, context);
+ }
+
+ /**
+ * Remove the context with the given key.
+ * @param key the context key (never null)
+ * @return the corresponding ApplicationContext instance,
+ * or null if not found in the cache.
+ * @see #setDirty
+ */
+ ApplicationContext remove(String key) {
+ return this.contextKeyToContextMap.remove(key);
+ }
+
+ /**
+ * Mark the context with the given key as dirty, effectively
+ * {@link #remove removing} the context from the cache and explicitly
+ * {@link ConfigurableApplicationContext#close() closing} it if
+ * it is an instance of {@link ConfigurableApplicationContext}.
+ * null)
+ * @see #remove
+ */
+ void setDirty(String key) {
+ Assert.notNull(key, "Key must not be null");
+ ApplicationContext context = remove(key);
+ if (context instanceof ConfigurableApplicationContext) {
+ ((ConfigurableApplicationContext) context).close();
+ }
+ }
+
+ /**
+ * Determine the number of contexts currently stored in the cache. If the
+ * cache contains more than Integer.MAX_VALUE elements, returns
+ * Integer.MAX_VALUE.
+ */
+ int size() {
+ return this.contextKeyToContextMap.size();
+ }
+
+ /**
+ * Generates a text string, which contains the {@link #size() size} as well
+ * as the {@link #hitCount hit} and {@link #missCount miss} counts.
+ */
+ public String toString() {
+ return new ToStringCreator(this)
+ .append("size", size())
+ .append("hitCount", getHitCount())
+ .append("missCount",getMissCount())
+ .toString();
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/org.springframework.test/src/main/java/org/springframework/test/context/ContextConfiguration.java
new file mode 100644
index 00000000000..377cbc910cb
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/ContextConfiguration.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+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;
+
+/**
+ * ContextConfiguration defines class-level metadata which can be used to
+ * instruct client code with regard to how to load and configure an
+ * {@link org.springframework.context.ApplicationContext}.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ * @see ContextLoader
+ * @see org.springframework.context.ApplicationContext
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+@Documented
+public @interface ContextConfiguration {
+
+ /**
+ * The resource locations to use for loading an
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}.
+ */
+ String[] locations() default {};
+
+ /**
+ * Whether or not {@link #locations() resource locations} from superclasses
+ * should be inherited.
+ * true, which means that an annotated
+ * class will inherit the resource locations defined by an
+ * annotated superclass. Specifically, the resource locations for an
+ * annotated class will be appended to the list of resource locations
+ * defined by an annotated superclass. Thus, subclasses have the option of
+ * extending the list of resource locations. In the following
+ * example, the {@link org.springframework.context.ApplicationContext}
+ * for ExtendedTest will be loaded from
+ * "base-context.xml" and
+ * "extended-context.xml", in that order. Beans defined in
+ * "extended-context.xml" may therefore override those defined in
+ * "base-context.xml".
+ *
+ * {@link ContextConfiguration @ContextConfiguration}(locations={"base-context.xml"})
+ * public class BaseTest {
+ * // ...
+ * }
+ * {@link ContextConfiguration @ContextConfiguration}(locations={"extended-context.xml"})
+ * public class ExtendedTest extends BaseTest {
+ * // ...
+ * }
+ *
+ * If inheritLocations is set to false, the
+ * resource locations for the annotated class will shadow and
+ * effectively replace any resource locations defined by a superclass.
+ */
+ boolean inheritLocations() default true;
+
+ /**
+ * The type of {@link ContextLoader} to use for loading an
+ * {@link org.springframework.context.ApplicationContext}.
+ */
+ Class extends ContextLoader> loader() default ContextLoader.class;
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/ContextLoader.java b/org.springframework.test/src/main/java/org/springframework/test/context/ContextLoader.java
new file mode 100644
index 00000000000..5c125c717d3
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/ContextLoader.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+import org.springframework.context.ApplicationContext;
+
+/**
+ * Strategy interface for loading an
+ * {@link ApplicationContext application context}.
+ *
+ * public no-args
+ * constructor.
+ *
+ *
+ *
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+public interface ContextLoader {
+
+ /**
+ * Processes application context resource locations for a specified class.
+ * 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
+ * locations, configures the context, and finally returns
+ * the context in fully refreshed state.
+ * null)
+ * @param contextCache the context cache from which the constructed test context
+ * should retrieve application contexts (must not be null)
+ */
+ @SuppressWarnings("unchecked")
+ TestContext(Class> testClass, ContextCache contextCache) {
+ Assert.notNull(testClass, "Test class must not be null");
+ Assert.notNull(contextCache, "ContextCache must not be null");
+
+ ContextConfiguration contextConfiguration = testClass.getAnnotation(ContextConfiguration.class);
+ String[] locations = null;
+ ContextLoader contextLoader = null;
+
+ if (contextConfiguration == null) {
+ if (logger.isInfoEnabled()) {
+ logger.info("@ContextConfiguration not found for class [" + testClass + "]");
+ }
+ }
+ else {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Retrieved @ContextConfiguration [" + contextConfiguration + "] for class [" + testClass + "]");
+ }
+
+ Class extends ContextLoader> contextLoaderClass = contextConfiguration.loader();
+ if (ContextLoader.class.equals(contextLoaderClass)) {
+ try {
+ contextLoaderClass = (Class extends ContextLoader>) getClass().getClassLoader().loadClass(
+ DEFAULT_CONTEXT_LOADER_CLASS_NAME);
+ }
+ catch (ClassNotFoundException ex) {
+ throw new IllegalStateException("Could not load default ContextLoader class ["
+ + DEFAULT_CONTEXT_LOADER_CLASS_NAME + "]. Specify @ContextConfiguration's 'loader' "
+ + "attribute or make the default loader class available.");
+ }
+ }
+ contextLoader = (ContextLoader) BeanUtils.instantiateClass(contextLoaderClass);
+ locations = retrieveContextLocations(contextLoader, testClass);
+ }
+
+ this.testClass = testClass;
+ this.contextCache = contextCache;
+ this.contextLoader = contextLoader;
+ this.locations = locations;
+ }
+
+ /**
+ * Retrieve {@link ApplicationContext} resource locations for the supplied
+ * {@link Class class}, using the supplied {@link ContextLoader} to
+ * {@link ContextLoader#processLocations(Class, String...) process} the
+ * locations.
+ * inheritLocations flag
+ * is set to true, locations defined in the annotated class
+ * will be appended to the locations defined in superclasses.
+ * @param contextLoader the ContextLoader to use for processing the locations
+ * (must not be null)
+ * @param clazz the class for which to retrieve the resource locations
+ * (must not be null)
+ * @return the list of ApplicationContext resource locations for the specified
+ * class, including locations from superclasses if appropriate
+ * @throws IllegalArgumentException if {@link ContextConfiguration @ContextConfiguration}
+ * is not present on the supplied class
+ */
+ private String[] retrieveContextLocations(ContextLoader contextLoader, Class> clazz) {
+ Assert.notNull(contextLoader, "ContextLoader must not be null");
+ Assert.notNull(clazz, "Class must not be null");
+
+ Listkey to a String
+ * representation for use in caching, logging, etc.
+ * @param key the context key to convert to a String
+ */
+ private String contextKeyString(Serializable key) {
+ return ObjectUtils.nullSafeToString(key);
+ }
+
+ /**
+ * Get the {@link ApplicationContext application context} for this test
+ * context, possibly cached.
+ * @return the application context; may be null if the
+ * current test context is not configured to use an application context
+ * @throws IllegalStateException if an error occurs while retrieving the application context
+ */
+ public ApplicationContext getApplicationContext() {
+ ApplicationContext context = null;
+ ContextCache cache = getContextCache();
+ synchronized (cache) {
+ context = cache.get(contextKeyString(getLocations()));
+ if (context == null) {
+ try {
+ context = loadApplicationContext();
+ cache.put(contextKeyString(getLocations()), context);
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException("Failed to load ApplicationContext", ex);
+ }
+ }
+ }
+ return context;
+ }
+
+ /**
+ * Get the {@link ContextCache context cache} for this test context.
+ * @return the context cache (never null)
+ */
+ ContextCache getContextCache() {
+ return this.contextCache;
+ }
+
+ /**
+ * Get the {@link ContextLoader} to use for loading the
+ * {@link ApplicationContext} for this test context.
+ * @return the context loader. May be null if the current
+ * test context is not configured to use an application context.
+ */
+ ContextLoader getContextLoader() {
+ return this.contextLoader;
+ }
+
+ /**
+ * Get the resource locations to use for loading the
+ * {@link ApplicationContext} for this test context.
+ * @return the application context resource locations.
+ * May be null if the current test context is
+ * not configured to use an application context.
+ */
+ String[] getLocations() {
+ return this.locations;
+ }
+
+ /**
+ * Get the {@link Class test class} for this test context.
+ * @return the test class (never null)
+ */
+ public final Class> getTestClass() {
+ return this.testClass;
+ }
+
+ /**
+ * Gets the current {@link Object test instance} for this test context.
+ * null)
+ * @see #updateState(Object,Method,Throwable)
+ */
+ public final Object getTestInstance() {
+ return this.testInstance;
+ }
+
+ /**
+ * Gets the current {@link Method test method} for this test context.
+ * null)
+ * @see #updateState(Object, Method, Throwable)
+ */
+ public final Method getTestMethod() {
+ return this.testMethod;
+ }
+
+ /**
+ * Gets the {@link Throwable exception} that was thrown during execution of
+ * the {@link #getTestMethod() test method}.
+ * null if no
+ * exception was thrown
+ * @see #updateState(Object, Method, Throwable)
+ */
+ public final Throwable getTestException() {
+ return this.testException;
+ }
+
+ /**
+ * Call this method to signal that the
+ * {@link ApplicationContext application context} associated with this test
+ * context is dirty and should be reloaded. Do this if a test has
+ * modified the context (for example, by replacing a bean definition).
+ */
+ public void markApplicationContextDirty() {
+ getContextCache().setDirty(contextKeyString(getLocations()));
+ }
+
+ /**
+ * Updates this test context to reflect the state of the currently executing test.
+ * @param testInstance the current test instance (may be null)
+ * @param testMethod the current test method (may be null)
+ * @param testException the exception that was thrown in the test method,
+ * or null if no exception was thrown
+ */
+ synchronized void updateState(Object testInstance, Method testMethod, Throwable testException) {
+ this.testInstance = testInstance;
+ this.testMethod = testMethod;
+ this.testException = testException;
+ }
+
+ /**
+ * Provides a string representation of this test context's
+ * {@link #getTestClass() test class},
+ * {@link #getLocations() application context resource locations},
+ * {@link #getTestInstance() test instance},
+ * {@link #getTestMethod() test method}, and
+ * {@link #getTestException() test exception}.
+ */
+ @Override
+ public String toString() {
+ return new ToStringCreator(this).
+ append("testClass", getTestClass()).
+ append("locations", getLocations()).append("testInstance", getTestInstance()).
+ append("testMethod", getTestMethod()).append("testException", getTestException()).
+ toString();
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/TestContextManager.java b/org.springframework.test/src/main/java/org/springframework/test/context/TestContextManager.java
new file mode 100644
index 00000000000..4dc654de1a7
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/TestContextManager.java
@@ -0,0 +1,360 @@
+/*
+ * 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;
+
+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.BeanUtils;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.util.Assert;
+
+/**
+ * TestContextManager is the main entry point into the
+ * Spring TestContext Framework, which provides support for loading
+ * and accessing {@link ApplicationContext application contexts}, dependency
+ * injection of test instances,
+ * {@link org.springframework.transaction.annotation.Transactional transactional}
+ * execution of test methods, etc.
+ * TestContextManager is responsible for managing
+ * a single {@link TestContext} and signaling events to all registered
+ * {@link TestExecutionListener TestExecutionListeners} at well defined test
+ * execution points:
+ *
+ *
+ *
+ * @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.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. This needs to be static, as tests
+ * may be destroyed and recreated between running individual test methods,
+ * for example with JUnit.
+ */
+ static final ContextCache contextCache = new ContextCache();
+
+
+ private final TestContext testContext;
+
+ private final ListTestContextManager for the specified
+ * {@link Class test class} and automatically
+ * {@link #registerTestExecutionListeners(TestExecutionListener...) registers}
+ * the {@link TestExecutionListener TestExecutionListeners} configured for
+ * the test class via the
+ * {@link TestExecutionListeners @TestExecutionListeners} annotation.
+ * @param testClass the Class object corresponding to the test class to be managed
+ * @see #registerTestExecutionListeners(TestExecutionListener...)
+ * @see #retrieveTestExecutionListeners(Class)
+ */
+ public TestContextManager(Class> testClass) {
+ this.testContext = new TestContext(testClass, contextCache);
+ registerTestExecutionListeners(retrieveTestExecutionListeners(testClass));
+ }
+
+
+ /**
+ * Returns the {@link TestContext} managed by this 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 TestContextManager.
+ */
+ public void registerTestExecutionListeners(TestExecutionListener... testExecutionListeners) {
+ for (TestExecutionListener listener : testExecutionListeners) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Registering TestExecutionListener [" + listener + "]");
+ }
+ this.testExecutionListeners.add(listener);
+ }
+ }
+
+ /**
+ * Gets an {@link Collections#unmodifiableList(List) unmodifiable} copy of
+ * the {@link TestExecutionListener TestExecutionListeners} registered for
+ * this TestContextManager.
+ */
+ public final ListinheritListeners
+ * flag is set to true, listeners defined in the annotated
+ * class will be appended to the listeners defined in superclasses.
+ * @param clazz the Class object corresponding to 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");
+ ClasstestInstance.
+ * 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 before 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 before methods (e.g., methods annotated
+ * with JUnit's {@link org.junit.Before @Before} ).
+ * testInstance and testMethod.
+ * 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 after 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 after methods (e.g., methods annotated with
+ * JUnit's {@link org.junit.After @After}).
+ * testInstance, testMethod, and
+ * exception.
+ * 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 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);
+
+ // Traverse the TestExecutionListeners in reverse order to ensure proper
+ // "wrapper"-style execution of listeners.
+ ListTestExecutionListener defines a listener API for
+ * reacting to test execution events published by the {@link TestContextManager}
+ * with which the listener is registered.
+ * public no-args
+ * constructor, so that listeners can be instantiated transparently by tools and
+ * configuration mechanisms.
+ *
+ *
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ */
+public interface TestExecutionListener {
+
+ /**
+ * Prepares the {@link Object test instance} of the supplied
+ * {@link TestContext test context}, for example for injecting
+ * dependencies.
+ * null)
+ * @throws Exception allows any exception to propagate
+ */
+ void prepareTestInstance(TestContext testContext) throws Exception;
+
+ /**
+ * Pre-processes a test just before execution of the
+ * {@link java.lang.reflect.Method test method} in the supplied
+ * {@link TestContext test context}, for example for setting up test
+ * fixtures.
+ * @param testContext the test context in which the test method will be
+ * executed (never null)
+ * @throws Exception allows any exception to propagate
+ */
+ void beforeTestMethod(TestContext testContext) throws Exception;
+
+ /**
+ * Post-processes a test just after execution of the
+ * {@link java.lang.reflect.Method test method} in the supplied
+ * {@link TestContext test context}, for example for tearing down test
+ * fixtures.
+ * @param testContext the test context in which the test method was
+ * executed (never null)
+ * @throws Exception allows any exception to propagate
+ */
+ void afterTestMethod(TestContext testContext) throws Exception;
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/org.springframework.test/src/main/java/org/springframework/test/context/TestExecutionListeners.java
new file mode 100644
index 00000000000..2e739cf3b0d
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/TestExecutionListeners.java
@@ -0,0 +1,98 @@
+/*
+ * 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.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;
+
+/**
+ * TestExecutionListeners defines class-level metadata for configuring which
+ * {@link TestExecutionListener TestExecutionListeners} should be registered
+ * with a {@link TestContextManager}. Typically,
+ * {@link TestExecutionListeners @TestExecutionListeners} will be used in
+ * conjunction with {@link ContextConfiguration @ContextConfiguration}.
+ *
+ * @author Sam Brannen
+ * @since 2.5
+ * @see TestExecutionListener
+ * @see TestContextManager
+ * @see ContextConfiguration
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+@Documented
+public @interface TestExecutionListeners {
+
+ /**
+ * true, which means that an annotated
+ * class will inherit 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 extending the list of
+ * listeners. In the following example, AbstractBaseTest will
+ * be configured with DependencyInjectionTestExecutionListener
+ * and DirtiesContextTestExecutionListener; whereas,
+ * TransactionalTest will be configured with
+ * DependencyInjectionTestExecutionListener,
+ * DirtiesContextTestExecutionListener, and
+ * TransactionalTestExecutionListener, in that order.
+ *
+ * {@link TestExecutionListeners @TestExecutionListeners}({ DependencyInjectionTestExecutionListener.class,
+ * DirtiesContextTestExecutionListener.class })
+ * public abstract class AbstractBaseTest {
+ * // ...
+ * }
+ *
+ * {@link TestExecutionListeners @TestExecutionListeners}({ TransactionalTestExecutionListener.class })
+ * public class TransactionalTest extends BaseTest {
+ * // ...
+ * }
+ *
+ *
+ * inheritListeners is set to false, the
+ * listeners for the annotated class will shadow and effectively
+ * replace any listeners defined by a superclass.
+ *
+ *
+ * super(); and super(name);
+ * respectively.AbstractJUnit38SpringContextTests.
+ * (Note that additional annotations may be supported by various
+ * {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners})
+ *
+ *
+ *
+ * @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
+ */
+@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 @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 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 Spring TestContext Framework test sequence.
+ *
+ *
+ * @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 timed test via the supplied {@link TestExecutionCallback},
+ * providing support for the {@link Timed @Timed} annotation.
+ * @param tec the test execution callback to run
+ * @param testMethod the actual test method: used to retrieve the 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 @ExpectedException} and
+ * {@link Repeat @Repeat} annotations.
+ * @param tec the test execution callback to run
+ * @param testMethod the actual test method: used to retrieve the
+ * {@link ExpectedException @ExpectedException} and {@link Repeat @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 disabled 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/org.springframework.test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java
new file mode 100644
index 00000000000..03785791527
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java
@@ -0,0 +1,155 @@
+/*
+ * 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.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;
+
+/**
+ * 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!
+ * 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/org.springframework.test/src/main/java/org/springframework/test/context/junit38/package.html b/org.springframework.test/src/main/java/org/springframework/test/context/junit38/package.html
new file mode 100644
index 00000000000..df20e03f8a5
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/junit38/package.html
@@ -0,0 +1,8 @@
+
+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/org.springframework.test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java
new file mode 100644
index 00000000000..6092ab6ce46
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java
@@ -0,0 +1,163 @@
+/*
+ * 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.junit4;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.internal.runners.InitializationError;
+import org.junit.internal.runners.JUnit4ClassRunner;
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunNotifier;
+
+import org.springframework.test.annotation.ProfileValueUtils;
+import org.springframework.test.context.TestContextManager;
+
+/**
+ *
+ *
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see TestContextManager
+ */
+public class SpringJUnit4ClassRunner extends JUnit4ClassRunner {
+
+ private static final Log logger = LogFactory.getLog(SpringJUnit4ClassRunner.class);
+
+ private final TestContextManager testContextManager;
+
+
+ /**
+ * Constructs a new SpringJUnit4ClassRunner and initializes a
+ * {@link TestContextManager} to provide Spring testing functionality to
+ * standard JUnit tests.
+ * @param clazz the Class object corresponding to 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);
+ }
+
+
+ @Override
+ /**
+ * Check whether the test is enabled in the first place. This prevents classes with
+ * a non-matching @IfProfileValue annotation from running altogether,
+ * even skipping the execution of prepareTestInstance listener methods.
+ * @see org.springframework.test.annotation.IfProfileValue
+ * @see org.springframework.test.context.TestExecutionListener
+ */
+ public void run(RunNotifier notifier) {
+ if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
+ notifier.fireTestIgnored(getDescription());
+ return;
+ }
+ super.run(notifier);
+ }
+
+ /**
+ * Delegates to {@link JUnit4ClassRunner#createTest()} to create the test
+ * instance and then to a {@link TestContextManager} to
+ * {@link TestContextManager#prepareTestInstance(Object) prepare} the test
+ * instance for Spring testing functionality.
+ * @see JUnit4ClassRunner#createTest()
+ * @see TestContextManager#prepareTestInstance(Object)
+ */
+ @Override
+ protected Object createTest() throws Exception {
+ Object testInstance = super.createTest();
+ getTestContextManager().prepareTestInstance(testInstance);
+ return testInstance;
+ }
+
+ /**
+ * Creates a new {@link TestContextManager}. Can be overridden by subclasses.
+ * @param clazz the Class object corresponding to the test class to be managed
+ */
+ protected TestContextManager createTestContextManager(Class> clazz) {
+ return new TestContextManager(clazz);
+ }
+
+ /**
+ * Get the {@link TestContextManager} associated with this runner.
+ */
+ protected final TestContextManager getTestContextManager() {
+ return this.testContextManager;
+ }
+
+ /**
+ * Invokes the supplied {@link Method test method} and notifies the supplied
+ * {@link RunNotifier} of the appropriate events.
+ * @see #createTest()
+ * @see JUnit4ClassRunner#invokeTestMethod(Method,RunNotifier)
+ */
+ @Override
+ protected void invokeTestMethod(Method method, RunNotifier notifier) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Invoking test method [" + method.toGenericString() + "]");
+ }
+
+ // The following is a 1-to-1 copy of the original JUnit 4.4 code, except
+ // that we use custom implementations for TestMethod and MethodRoadie.
+
+ Description description = methodDescription(method);
+ Object testInstance;
+ try {
+ testInstance = createTest();
+ }
+ catch (InvocationTargetException ex) {
+ notifier.testAborted(description, ex.getCause());
+ return;
+ }
+ catch (Exception ex) {
+ notifier.testAborted(description, ex);
+ return;
+ }
+
+ SpringTestMethod testMethod = new SpringTestMethod(method, getTestClass());
+ new SpringMethodRoadie(getTestContextManager(), testInstance, testMethod, notifier, description).run();
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/junit4/SpringMethodRoadie.java b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/SpringMethodRoadie.java
new file mode 100644
index 00000000000..1dc1eb46e11
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/SpringMethodRoadie.java
@@ -0,0 +1,352 @@
+/*
+ * 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.junit4;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.Assume.AssumptionViolatedException;
+import org.junit.runner.Description;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunNotifier;
+
+import org.springframework.test.annotation.Repeat;
+import org.springframework.test.annotation.Timed;
+import org.springframework.test.context.TestContextManager;
+
+/**
+ * SpringMethodRoadie is a custom implementation of JUnit 4.4's
+ * {@link org.junit.internal.runners.MethodRoadie MethodRoadie}, which provides
+ * the following enhancements:
+ *
+ *
+ * MethodRoadie has been duplicated here instead of subclassing
+ * MethodRoadie directly.
+ * SpringMethodRoadie.
+ * @param testContextManager the TestContextManager to notify
+ * @param testInstance the test instance upon which to invoke the test method
+ * @param testMethod the test method to invoke
+ * @param notifier the RunNotifier to notify
+ * @param description the test description
+ */
+ public SpringMethodRoadie(TestContextManager testContextManager, Object testInstance,
+ SpringTestMethod testMethod, RunNotifier notifier, Description description) {
+
+ this.testContextManager = testContextManager;
+ this.testInstance = testInstance;
+ this.testMethod = testMethod;
+ this.notifier = notifier;
+ this.description = description;
+ }
+
+ /**
+ * Runs the test, including notification of events to the
+ * {@link RunNotifier} and {@link TestContextManager} as well as proper
+ * handling of {@link org.junit.Ignore @Ignore},
+ * {@link org.junit.Test#expected() expected exceptions},
+ * {@link org.junit.Test#timeout() test timeouts}, and
+ * {@link org.junit.Assume.AssumptionViolatedException assumptions}.
+ */
+ public void run() {
+ if (this.testMethod.isIgnored()) {
+ this.notifier.fireTestIgnored(this.description);
+ return;
+ }
+
+ this.notifier.fireTestStarted(this.description);
+ try {
+ Timed timedAnnotation = this.testMethod.getMethod().getAnnotation(Timed.class);
+ long springTimeout = (timedAnnotation != null && timedAnnotation.millis() > 0 ?
+ timedAnnotation.millis() : 0);
+ long junitTimeout = this.testMethod.getTimeout();
+ if (springTimeout > 0 && junitTimeout > 0) {
+ throw new IllegalStateException("Test method [" + this.testMethod.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.");
+ }
+ else if (springTimeout > 0) {
+ long startTime = System.currentTimeMillis();
+ try {
+ runTest();
+ }
+ finally {
+ long elapsed = System.currentTimeMillis() - startTime;
+ if (elapsed > springTimeout) {
+ addFailure(new TimeoutException("Took " + elapsed + " ms; limit was " + springTimeout));
+ }
+ }
+ }
+ else if (junitTimeout > 0) {
+ runWithTimeout(junitTimeout);
+ }
+ else {
+ runTest();
+ }
+ }
+ finally {
+ this.notifier.fireTestFinished(this.description);
+ }
+ }
+
+ /**
+ * Runs the test method on the test instance with the specified
+ * timeout.
+ * @param timeout the timeout in milliseconds
+ * @see #runWithRepetitions(Runnable)
+ * @see #runTestMethod()
+ */
+ protected void runWithTimeout(final long timeout) throws CancellationException {
+ runWithRepetitions(new Runnable() {
+ public void run() {
+ ExecutorService service = Executors.newSingleThreadExecutor();
+ Future result = service.submit(new RunBeforesThenTestThenAfters());
+ service.shutdown();
+ try {
+ boolean terminated = service.awaitTermination(timeout, TimeUnit.MILLISECONDS);
+ if (!terminated) {
+ service.shutdownNow();
+ }
+ // Throws the exception if one occurred during the invocation.
+ result.get(0, TimeUnit.MILLISECONDS);
+ }
+ catch (TimeoutException ex) {
+ String message = "Test timed out after " + timeout + " milliseconds";
+ addFailure(new TimeoutException(message));
+ // We're cancelling repetitions here since we don't want
+ // the abandoned test method execution to conflict with
+ // further execution attempts of the same test method.
+ throw new CancellationException(message);
+ }
+ catch (ExecutionException ex) {
+ addFailure(ex.getCause());
+ }
+ catch (Exception ex) {
+ addFailure(ex);
+ }
+ }
+ });
+ }
+
+ /**
+ * Runs the test, including {@link #runBefores() @Before} and
+ * {@link #runAfters() @After} methods.
+ * @see #runWithRepetitions(Runnable)
+ * @see #runTestMethod()
+ */
+ protected void runTest() {
+ runWithRepetitions(new RunBeforesThenTestThenAfters());
+ }
+
+ /**
+ * Runs the supplied test with repetitions. Checks for the
+ * presence of {@link Repeat @Repeat} to determine if the test should be run
+ * more than once. The test will be run at least once.
+ * @param test the runnable test
+ * @see Repeat
+ */
+ protected void runWithRepetitions(Runnable test) {
+ Method method = this.testMethod.getMethod();
+ Repeat repeat = method.getAnnotation(Repeat.class);
+ int runs = (repeat != null && repeat.value() > 1 ? repeat.value() : 1);
+
+ for (int i = 0; i < runs; i++) {
+ if (runs > 1 && logger.isInfoEnabled()) {
+ logger.info("Repetition " + (i + 1) + " of test " + method.getName());
+ }
+ try {
+ test.run();
+ }
+ catch (CancellationException ex) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Runs the test method on the test instance, processing exceptions
+ * (both expected and unexpected), assumptions, and registering
+ * failures as necessary.
+ */
+ protected void runTestMethod() {
+ this.testException = null;
+ try {
+ this.testMethod.invoke(this.testInstance);
+ if (this.testMethod.expectsException()) {
+ addFailure(new AssertionError("Expected exception: " + this.testMethod.getExpectedException().getName()));
+ }
+ }
+ catch (InvocationTargetException ex) {
+ this.testException = ex.getTargetException();
+ if (!(this.testException instanceof AssumptionViolatedException)) {
+ if (!this.testMethod.expectsException()) {
+ addFailure(this.testException);
+ }
+ else if (this.testMethod.isUnexpected(this.testException)) {
+ addFailure(new Exception("Unexpected exception, expected <" +
+ this.testMethod.getExpectedException().getName() + "> but was <" +
+ this.testException.getClass().getName() + ">", this.testException));
+ }
+ }
+ }
+ catch (Throwable ex) {
+ addFailure(ex);
+ }
+ finally {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Test method [" + this.testMethod.getMethod() + "] threw exception: " +
+ this.testException);
+ }
+ }
+ }
+
+ /**
+ * Calls {@link TestContextManager#beforeTestMethod} and then runs
+ * {@link org.junit.Before @Before methods}, registering failures
+ * and throwing {@link FailedBefore} exceptions as necessary.
+ * @throws FailedBefore if an error occurs while executing a before method
+ */
+ protected void runBefores() throws FailedBefore {
+ try {
+ this.testContextManager.beforeTestMethod(this.testInstance, this.testMethod.getMethod());
+ Listexception with the
+ * {@link RunNotifier}.
+ * @param exception the exception upon which to base the failure
+ */
+ protected void addFailure(Throwable exception) {
+ this.notifier.fireTestFailure(new Failure(this.description, exception));
+ }
+
+
+ /**
+ * Runs the test method, executing @Before and @After
+ * methods accordingly.
+ */
+ private class RunBeforesThenTestThenAfters implements Runnable {
+
+ public void run() {
+ try {
+ runBefores();
+ runTestMethod();
+ }
+ catch (FailedBefore ex) {
+ }
+ finally {
+ runAfters();
+ }
+ }
+ }
+
+
+ /**
+ * Marker exception to signal that an exception was encountered while
+ * executing an {@link org.junit.Before @Before} method.
+ */
+ private static class FailedBefore extends Exception {
+
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/junit4/SpringTestMethod.java b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/SpringTestMethod.java
new file mode 100644
index 00000000000..12f58277978
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/SpringTestMethod.java
@@ -0,0 +1,183 @@
+/*
+ * 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.junit4;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.Test.None;
+import org.junit.internal.runners.TestClass;
+
+import org.springframework.test.annotation.ExpectedException;
+import org.springframework.test.annotation.ProfileValueSource;
+import org.springframework.test.annotation.ProfileValueUtils;
+
+/**
+ * SpringTestMethod is a custom implementation of JUnit 4.4's
+ * {@link org.junit.internal.runners.TestMethod TestMethod}. Due to method and
+ * field visibility constraints, the code of TestMethod has been duplicated here
+ * instead of subclassing TestMethod directly.
+ *
+ * exception that this test method is expected to throw.
+ * null if none was specified
+ */
+ public Class extends Throwable> getExpectedException() throws IllegalStateException {
+ ExpectedException expectedExAnn = getMethod().getAnnotation(ExpectedException.class);
+ Test testAnnotation = getMethod().getAnnotation(Test.class);
+
+ Class extends Throwable> expectedException = null;
+ Class extends Throwable> springExpectedException =
+ (expectedExAnn != null && expectedExAnn.value() != null ? expectedExAnn.value() : null);
+ Class extends Throwable> junitExpectedException =
+ (testAnnotation != null && testAnnotation.expected() != None.class ? testAnnotation.expected() : null);
+
+ if (springExpectedException != null && junitExpectedException != null) {
+ String msg = "Test method [" + 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);
+ }
+ else if (springExpectedException != null) {
+ expectedException = springExpectedException;
+ }
+ else if (junitExpectedException != null) {
+ expectedException = junitExpectedException;
+ }
+
+ return expectedException;
+ }
+
+ /**
+ * Get the actual {@link Method method} referenced by this test method.
+ */
+ public final Method getMethod() {
+ return this.method;
+ }
+
+ /**
+ * Get the {@link TestClass test class} for this test method.
+ */
+ public final TestClass getTestClass() {
+ return this.testClass;
+ }
+
+ /**
+ * Get the configured timeout for this test method.
+ * 0 if none was specified
+ */
+ public long getTimeout() {
+ Test testAnnotation = getMethod().getAnnotation(Test.class);
+ return (testAnnotation != null && testAnnotation.timeout() > 0 ? testAnnotation.timeout() : 0);
+ }
+
+ /**
+ * Convenience method for {@link Method#invoke(Object,Object...) invoking}
+ * the method associated with this test method. Throws exceptions consistent
+ * with {@link Method#invoke(Object,Object...) Method.invoke()}.
+ * @param testInstance the test instance upon which to invoke the method
+ */
+ public void invoke(Object testInstance) throws IllegalAccessException, InvocationTargetException {
+ getMethod().invoke(testInstance);
+ }
+
+ /**
+ * Determine if this test method should be ignored.
+ * @return true if this test method should be ignored
+ * @see ProfileValueUtils#isTestEnabledInThisEnvironment
+ */
+ public boolean isIgnored() {
+ return (getMethod().isAnnotationPresent(Ignore.class) ||
+ !ProfileValueUtils.isTestEnabledInThisEnvironment(this.method, this.testClass.getJavaClass()));
+ }
+
+ /**
+ * Determine if this test method {@link Test#expected() expects} exceptions
+ * of the type of the supplied exception to be thrown.
+ * @param exception the thrown exception
+ * @return true if the supplied exception was of an expected type
+ */
+ public boolean isUnexpected(Throwable exception) {
+ return !getExpectedException().isAssignableFrom(exception.getClass());
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/junit4/package.html b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/package.html
new file mode 100644
index 00000000000..3b0b16fb86f
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/package.html
@@ -0,0 +1,8 @@
+
+locations are null or
+ * empty and {@link #isGenerateDefaultLocations()} is
+ * 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
+ * locations will be
+ * {@link #modifyLocations(Class,String...) 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 null or empty)
+ * @return an array of application context resource locations
+ * @see #generateDefaultLocations
+ * @see #modifyLocations
+ * @see org.springframework.test.context.ContextLoader#processLocations
+ */
+ public final String[] processLocations(Class> clazz, String... locations) {
+ return (ObjectUtils.isEmpty(locations) && isGenerateDefaultLocations()) ?
+ generateDefaultLocations(clazz) : modifyLocations(clazz, locations);
+ }
+
+ /**
+ * Generates the default classpath resource locations array based on the
+ * supplied class.
+ * com.example.MyTest,
+ * the generated locations will contain a single string with a value of
+ * "classpath:/com/example/MyTest<suffix>",
+ * where <suffix> is the value of the
+ * {@link #getResourceSuffix() resource suffix} string.
+ * http:,
+ * etc.) will be added to the results unchanged.
+ * locations provided to
+ * {@link #processLocations(Class,String...) processLocations()} are
+ * null or empty.
+ * true by default
+ */
+ protected boolean isGenerateDefaultLocations() {
+ return true;
+ }
+
+ /**
+ * Get the suffix to append to {@link ApplicationContext} resource
+ * locations when generating default locations.
+ * null or empty
+ * @see #generateDefaultLocations(Class)
+ */
+ protected abstract String getResourceSuffix();
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java b/org.springframework.test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java
new file mode 100644
index 00000000000..eba8207c8a2
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java
@@ -0,0 +1,140 @@
+/*
+ * 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.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.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigUtils;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.util.StringUtils;
+
+/**
+ * Abstract, generic extension of {@link AbstractContextLoader} which loads a
+ * {@link GenericApplicationContext} from the locations provided to
+ * {@link #loadContext(String...)}.
+ *
+ * locations.
+ *
+ *
+ * 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
+ * reinjected in
+ * {@link #beforeTestMethod(TestContext) beforeTestMethod()}. Note that
+ * dependencies will be injected in
+ * {@link #prepareTestInstance(TestContext) prepareTestInstance()} in any
+ * case.
+ * 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/org.springframework.test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java b/org.springframework.test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java
new file mode 100644
index 00000000000..e3805e335db
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java
@@ -0,0 +1,70 @@
+/*
+ * 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.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.context.TestContext;
+import org.springframework.util.Assert;
+
+/**
+ * TestExecutionListener which processes test methods configured
+ * with the {@link DirtiesContext @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);
+
+
+ /**
+ * If the current test method of the supplied
+ * {@link TestContext test context} has been annotated with
+ * {@link DirtiesContext @DirtiesContext}, the
+ * {@link ApplicationContext application context} of the test context will
+ * be {@link TestContext#markApplicationContextDirty() marked as dirty},
+ * and the
+ * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
+ * will be set to true in the test context.
+ */
+ @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");
+
+ boolean dirtiesContext = testMethod.isAnnotationPresent(DirtiesContext.class);
+ if (logger.isDebugEnabled()) {
+ logger.debug("After test method: context [" + testContext + "], dirtiesContext [" + dirtiesContext + "].");
+ }
+
+ if (dirtiesContext) {
+ testContext.markApplicationContextDirty();
+ testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE,
+ Boolean.TRUE);
+ }
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java b/org.springframework.test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java
new file mode 100644
index 00000000000..9d49ee33279
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java
@@ -0,0 +1,60 @@
+/*
+ * 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.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;
+
+/**
+ * -context.properties".
+ *
+ * @see org.springframework.test.context.support.AbstractContextLoader#getResourceSuffix()
+ */
+ @Override
+ public String getResourceSuffix() {
+ return "-context.properties";
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java b/org.springframework.test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java
new file mode 100644
index 00000000000..78a5df22817
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.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.context.support;
+
+import org.springframework.beans.factory.support.BeanDefinitionReader;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.context.support.GenericApplicationContext;
+
+/**
+ * -context.xml".
+ *
+ * @see org.springframework.test.context.support.AbstractContextLoader#getResourceSuffix()
+ */
+ @Override
+ public String getResourceSuffix() {
+ return "-context.xml";
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/support/package.html b/org.springframework.test/src/main/java/org/springframework/test/context/support/package.html
new file mode 100644
index 00000000000..a4b44508373
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/support/package.html
@@ -0,0 +1,7 @@
+
+
+
+
+ *
+ *
+ * @author Sam Brannen
+ * @author Juergen Hoeller
+ * @since 2.5
+ * @see TestContext
+ * @see TestContextManager
+ * @see TestExecutionListeners
+ * @see AbstractTransactionalTestNGSpringContextTests
+ * @see org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests
+ * @see org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests
+ */
+@TestExecutionListeners({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
+ * initializes 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
+ * {@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)
+ 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 super();.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);
+ this.testException = testResult.getThrowable();
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java
new file mode 100644
index 00000000000..14320b8cd59
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java
@@ -0,0 +1,136 @@
+/*
+ * 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.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.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;
+
+/**
+ * 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/org.springframework.test/src/main/java/org/springframework/test/context/testng/package.html b/org.springframework.test/src/main/java/org/springframework/test/context/testng/package.html
new file mode 100644
index 00000000000..6ed3013c360
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/context/testng/package.html
@@ -0,0 +1,8 @@
+
+public void
+ * method should be executed after a transaction is ended for test
+ * methods configured to run within a transaction via the
+ * @Transactional annotation.
+ * @AfterTransaction methods of superclasses will be
+ * executed after those of the current class.
+ * public void
+ * method should be executed before a transaction is started for test
+ * methods configured to run within a transaction via the
+ * @Transactional annotation.
+ * @BeforeTransaction methods of superclasses will be
+ * executed before those of the current class.
+ * TestExecutionListener which provides support for executing
+ * tests within transactions by using
+ * {@link org.springframework.transaction.annotation.Transactional @Transactional}
+ * and {@link NotTransactional @NotTransactional} annotations.
+ * TransactionalTestExecutionListener provides such
+ * support for methods annotated with
+ * {@link BeforeTransaction @BeforeTransaction} and
+ * {@link AfterTransaction @AfterTransaction}.
+ * null if not found
+ * @throws BeansException if an error occurs while retrieving the transaction manager
+ */
+ protected final PlatformTransactionManager getTransactionManager(TestContext testContext) {
+ if (this.configAttributes == null) {
+ this.configAttributes = retrieveTransactionConfigurationAttributes(testContext.getTestClass());
+ }
+ String transactionManagerName = this.configAttributes.getTransactionManagerName();
+ try {
+ return (PlatformTransactionManager) testContext.getApplicationContext().getBean(
+ transactionManagerName, PlatformTransactionManager.class);
+ }
+ catch (BeansException ex) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Caught exception while retrieving transaction manager with bean name [" +
+ transactionManagerName + "] 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 default rollback 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 retrieveTransactionConfigurationAttributes(testContext.getTestClass()).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 rollback 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.
+ * annotationType but
+ * which are not shadowed by methods overridden in subclasses.
+ * true if the supplied method is shadowed by a
+ * method in the previousMethods list
+ */
+ private boolean isShadowed(Method method, Listtrue 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;
+ }
+
+ /**
+ * LineNumberReader> containing the script to be processed
+ * @return String containing the script lines
+ * @throws IOException
+ */
+ public static String readScript(LineNumberReader lineNumberReader) throws IOException {
+ String currentStatement = lineNumberReader.readLine();
+ StringBuffer scriptBuilder = new StringBuffer();
+ while (currentStatement != null) {
+ if (StringUtils.hasText(currentStatement)) {
+ if (scriptBuilder.length() > 0) {
+ scriptBuilder.append('\n');
+ }
+ scriptBuilder.append(currentStatement);
+ }
+ currentStatement = lineNumberReader.readLine();
+ }
+ return scriptBuilder.toString();
+ }
+
+ /**
+ * Does the provided SQL script contain the specified delimiter?
+ *
+ * @param script the SQL script
+ * @param delim charecter delimiting each statement - typically a ';' character
+ */
+ 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 ? false : true;
+ }
+ if (content[i] == delim && !inLiteral) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Split an SQL script into separate statements delimited with the provided delimiter character. Each
+ * individual statement will be added to the provided List.
+ * @param script the SQL script
+ * @param delim charecter delimiting each statement - typically a ';' character
+ * @param statements the List that will contain the individual statements
+ */
+ public static void splitSqlScript(String script, char delim, List statements) {
+ StringBuffer sb = new StringBuffer();
+ boolean inLiteral = false;
+ char[] content = script.toCharArray();
+
+ for (int i = 0; i < script.length(); i++) {
+ if (content[i] == '\'') {
+ inLiteral = inLiteral ? false : true;
+ }
+ if (content[i] == delim && !inLiteral) {
+ if (sb.length() > 0) {
+ statements.add(sb.toString());
+ sb = new StringBuffer();
+ }
+ }
+ else {
+ sb.append(content[i]);
+ }
+ }
+ if (sb.length() > 0) {
+ statements.add(sb.toString());
+ }
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java b/org.springframework.test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java
new file mode 100644
index 00000000000..3a3466eb0c3
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java
@@ -0,0 +1,177 @@
+/*
+ * 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.jdbc;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.util.Iterator;
+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;
+import org.springframework.util.StringUtils;
+
+/**
+ * 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
+ */
+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 (int i = 0; i < tableNames.length; i++) {
+ int rowCount = simpleJdbcTemplate.update("DELETE FROM " + tableNames[i]);
+ totalRowCount += rowCount;
+ if (logger.isInfoEnabled()) {
+ logger.info("Deleted " + rowCount + " rows from table " + tableNames[i]);
+ }
+ }
+ return totalRowCount;
+ }
+
+ /**
+ * Execute the given SQL script.
+ * 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.
+ * false
+ */
+ public static void executeSqlScript(SimpleJdbcTemplate simpleJdbcTemplate,
+ Resource resource, boolean continueOnError) throws DataAccessException {
+
+ executeSqlScript(simpleJdbcTemplate, new EncodedResource(resource), continueOnError);
+ }
+
+ /**
+ * Execute the given SQL script.
+ * 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();
+ Listaop.xml file.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public abstract class AbstractAspectjJpaTests extends AbstractJpaTests {
+
+ /**
+ * Default location of the 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 aop.xml file
+ * in the class path. The default is "META-INF/aop.xml".
+ * 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/org.springframework.test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java b/org.springframework.test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java
new file mode 100644
index 00000000000..821539121f3
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java
@@ -0,0 +1,363 @@
+/*
+ * 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.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.
+ *
+ *
+ * <dependency>
+ * <groupId>xerces</groupId>
+ * <artifactId>xercesImpl</artifactId>
+ * <version>2.8.1</version>
+ * </dependency>
+ *
+ *
+ * @author Rod Johnson
+ * @author Rob Harrop
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+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 MapEntityManagerFactory.createEntityManager()
+ * (which requires an explicit joinTransaction() call).
+ */
+ protected EntityManager createContainerManagedEntityManager() {
+ return ExtendedEntityManagerCreator.createContainerManagedEntityManager(this.entityManagerFactory);
+ }
+
+ /**
+ * Subclasses should override this method if they wish to disable shadow class loading.
+ * orm.xml.
+ *
+ * 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/org.springframework.test/src/main/java/org/springframework/test/jpa/package.html b/org.springframework.test/src/main/java/org/springframework/test/jpa/package.html
new file mode 100644
index 00000000000..9bd4752d7e8
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/jpa/package.html
@@ -0,0 +1,7 @@
+
+public field or invoke a non-public
+ * setter method when testing code involving, for example:
+ *
+ *
+ *
+ * @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 private or protected field access as opposed to
+ * public setter methods for properties in a domain entity.private or protected fields,
+ * setter methods, and configuration methods.name on the
+ * provided {@link Object target object} to the supplied value.
+ * public
+ * fields accessible, thus allowing one to set
+ * protected, private, and
+ * package-private 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 name on the
+ * provided {@link Object target object} to the supplied value.
+ * public
+ * fields accessible, thus allowing one to set
+ * protected, private, and
+ * package-private 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 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);
+ if (field == null) {
+ throw new IllegalArgumentException("Could not find field [" + name + "] on target [" + target + "]");
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Setting field [" + name + "] on target [" + target + "]");
+ }
+ ReflectionUtils.makeAccessible(field);
+ ReflectionUtils.setField(field, target, value);
+ }
+
+ /**
+ * Get the field with the given name from the provided
+ * target object.
+ * public fields
+ * accessible, thus allowing one to get protected,
+ * private, and package-private 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);
+ if (field == null) {
+ throw new IllegalArgumentException("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 name on the supplied
+ * target object with the supplied value.
+ * public
+ * methods accessible, thus allowing one to invoke protected,
+ * private, and package-private setter methods.
+ * name property
+ * on the target object, you may pass either "name" or
+ * "setName" 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 name on the supplied
+ * target object with the supplied value.
+ * public
+ * methods accessible, thus allowing one to invoke protected,
+ * private, and package-private setter methods.
+ * name property
+ * on the target object, you may pass either "name" or
+ * "setName" 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.notNull(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);
+ }
+ if (method == null) {
+ throw new IllegalArgumentException("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 name on the supplied
+ * target object with the supplied value.
+ * public
+ * methods accessible, thus allowing one to invoke protected,
+ * private, and package-private getter methods.
+ * name property
+ * on the target object, you may pass either "name" or
+ * "getName" 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.notNull(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);
+ }
+ if (method == null) {
+ throw new IllegalArgumentException("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);
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/util/package.html b/org.springframework.test/src/main/java/org/springframework/test/util/package.html
new file mode 100644
index 00000000000..ee8a63c825a
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/util/package.html
@@ -0,0 +1,7 @@
+
+assert*() methods throw {@link AssertionFailedError}s.
+ * modelName
+ * exists and checks it type, based on the expectedType. If
+ * the model entry exists and the type matches, the model value is returned.
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ * @param expectedType expected type of the model value
+ * @return the model value
+ */
+ protected Object assertAndReturnModelAttributeOfType(ModelAndView mav, Object modelName, Class 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 null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ * @param expectedList the expected list
+ */
+ protected void assertCompareListModelAttribute(ModelAndView mav, Object 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 null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ */
+ protected void assertModelAttributeAvailable(ModelAndView mav, Object modelName) {
+
+ try {
+ ModelAndViewAssert.assertModelAttributeAvailable(mav, modelName);
+ }
+ catch (AssertionError e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ }
+
+ /**
+ * Compare a given expectedValue to the value from the model
+ * bound under the given modelName.
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ * @param expectedValue the model value
+ */
+ protected void assertModelAttributeValue(ModelAndView mav, Object modelName, Object expectedValue) {
+
+ try {
+ ModelAndViewAssert.assertModelAttributeValue(mav, modelName, expectedValue);
+ }
+ catch (AssertionError e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ }
+
+ /**
+ * Inspect the expectedModel to see if all elements in the
+ * model appear and are equal.
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param expectedModel the expected model
+ */
+ protected void assertModelAttributeValues(ModelAndView mav, Map 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 null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ * @param expectedList the expected list
+ * @param comparator the comparator to use (may be null). If
+ * not specifying the comparator, both lists will be sorted not using
+ * any comparator.
+ */
+ protected void assertSortAndCompareListModelAttribute(
+ ModelAndView mav, Object 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
+ * expectedName.
+ *
+ * @param mav ModelAndView to test against (never 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/org.springframework.test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java b/org.springframework.test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java
new file mode 100644
index 00000000000..3a9b547eb6b
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java
@@ -0,0 +1,271 @@
+/*
+ * 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.web;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * assert*() methods throw {@link AssertionError}s.
+ * modelName
+ * exists and checks it type, based on the expectedType. If
+ * the model entry exists and the type matches, the model value is returned.
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ * @param expectedType expected type of the model value
+ * @return the model value
+ */
+ public static Object assertAndReturnModelAttributeOfType(ModelAndView mav, Object modelName, Class expectedType)
+ throws AssertionError {
+
+ assertCondition(mav != null, "ModelAndView is null");
+ assertCondition(mav.getModel() != null, "Model is null");
+ final Object obj = mav.getModel().get(modelName);
+ assertCondition(obj != null, "Model attribute with name '" + modelName + "' is null");
+
+ assertCondition(expectedType.isAssignableFrom(obj.getClass()), "Model attribute is not of expected type '"
+ + expectedType.getName() + "' but rather of type '" + obj.getClass().getName() + "'");
+ return obj;
+ }
+
+ /**
+ * Compare each individual entry in a list, without first sorting the lists.
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ * @param expectedList the expected list
+ */
+ public static void assertCompareListModelAttribute(ModelAndView mav, Object modelName, List expectedList)
+ throws AssertionError {
+
+ assertCondition(mav != null, "ModelAndView is null");
+ List modelList = (List) assertAndReturnModelAttributeOfType(mav, modelName, List.class);
+ assertCondition(expectedList.size() == modelList.size(), "Size of model list is '" + modelList.size()
+ + "' while size of expected list is '" + expectedList.size() + "'");
+ assertCondition(expectedList.equals(modelList), "List in model under name '" + modelName
+ + "' is not equal to the expected list.");
+ }
+
+ /**
+ * Assert whether or not a model attribute is available.
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ */
+ public static void assertModelAttributeAvailable(ModelAndView mav, Object modelName) throws AssertionError {
+
+ assertCondition(mav != null, "ModelAndView is null");
+ assertCondition(mav.getModel() != null, "Model is null");
+ assertCondition(mav.getModel().containsKey(modelName), "Model attribute with name '" + modelName
+ + "' is not available");
+ }
+
+ /**
+ * Compare a given expectedValue to the value from the model
+ * bound under the given modelName.
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ * @param expectedValue the model value
+ */
+ public static void assertModelAttributeValue(ModelAndView mav, Object modelName, Object expectedValue)
+ throws AssertionError {
+
+ assertCondition(mav != null, "ModelAndView is null");
+ Object modelValue = assertAndReturnModelAttributeOfType(mav, modelName, Object.class);
+ assertCondition(modelValue.equals(expectedValue), "Model value with name '" + modelName
+ + "' is not the same as the expected value which was '" + expectedValue + "'");
+ }
+
+ /**
+ * Inspect the expectedModel to see if all elements in the
+ * model appear and are equal.
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param expectedModel the expected model
+ */
+ public static void assertModelAttributeValues(ModelAndView mav, Map expectedModel) throws AssertionError {
+
+ assertCondition(mav != null, "ModelAndView is null");
+ assertCondition(mav.getModel() != null, "Model is null");
+
+ if (!mav.getModel().keySet().equals(expectedModel.keySet())) {
+ StringBuffer buf = new StringBuffer("Keyset of expected model does not match.\n");
+ appendNonMatchingSetsErrorMessage(expectedModel.keySet(), mav.getModel().keySet(), buf);
+ fail(buf.toString());
+ }
+
+ StringBuffer buf = new StringBuffer();
+ Iterator it = mav.getModel().keySet().iterator();
+ while (it.hasNext()) {
+ Object modelName = it.next();
+ Object assertionValue = expectedModel.get(modelName);
+ Object mavValue = mav.getModel().get(modelName);
+ if (!assertionValue.equals(mavValue)) {
+ buf.append("Value under name '" + modelName + "' differs, should have been '" + assertionValue
+ + "' but was '" + mavValue + "'\n");
+ }
+ }
+
+ if (buf.length() != 0) {
+ buf.insert(0, "Values of expected model do not match.\n");
+ fail(buf.toString());
+ }
+ }
+
+ /**
+ * Compare each individual entry in a list after having sorted both lists
+ * (optionally using a comparator).
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param modelName name of the object to add to the model (never
+ * null)
+ * @param expectedList the expected list
+ * @param comparator the comparator to use (may be null). If
+ * not specifying the comparator, both lists will be sorted not using
+ * any comparator.
+ */
+ public static void assertSortAndCompareListModelAttribute(
+ ModelAndView mav, Object modelName, List expectedList, Comparator comparator) throws AssertionError {
+
+ assertCondition(mav != null, "ModelAndView is null");
+ List modelList = (List) assertAndReturnModelAttributeOfType(mav, modelName, List.class);
+
+ assertCondition(expectedList.size() == modelList.size(), "Size of model list is '" + modelList.size()
+ + "' while size of expected list is '" + expectedList.size() + "'");
+
+ if (comparator != null) {
+ Collections.sort(modelList, comparator);
+ Collections.sort(expectedList, comparator);
+ }
+ else {
+ Collections.sort(modelList);
+ Collections.sort(expectedList);
+ }
+
+ assertCondition(expectedList.equals(modelList), "List in model under name '" + modelName
+ + "' is not equal to the expected list.");
+ }
+
+ /**
+ * Check to see if the view name in the ModelAndView matches the given
+ * expectedName.
+ *
+ * @param mav ModelAndView to test against (never null)
+ * @param expectedName the name of the model value
+ */
+ public static void assertViewName(ModelAndView mav, String expectedName) throws AssertionError {
+
+ assertCondition(mav != null, "ModelAndView is null");
+ assertCondition(expectedName.equals(mav.getViewName()), "View name is not equal to '" + expectedName
+ + "' but was '" + mav.getViewName() + "'");
+ }
+
+
+ /**
+ * Fails by throwing an AssertionError with the supplied
+ * message.
+ *
+ * @param message the exception message to use
+ * @see #assertCondition(boolean,String)
+ */
+ private static void fail(String message) throws AssertionError {
+
+ throw new AssertionError(message);
+ }
+
+ /**
+ * Assert the provided boolean condition, throwing
+ * AssertionError with the supplied message if
+ * the test result is false.
+ *
+ * @param condition a boolean expression
+ * @param message the exception message to use if the assertion fails
+ * @throws AssertionError if condition is false
+ * @see #fail(String)
+ */
+ private static void assertCondition(boolean condition, String message) throws AssertionError {
+
+ if (!condition) {
+ fail(message);
+ }
+ }
+
+ private static void appendNonMatchingSetsErrorMessage(Set assertionSet, Set incorrectSet, StringBuffer buf) {
+
+ Set tempSet = new HashSet();
+ tempSet.addAll(incorrectSet);
+ tempSet.removeAll(assertionSet);
+
+ if (tempSet.size() > 0) {
+ buf.append("Set has too many elements:\n");
+ Iterator it = tempSet.iterator();
+ while (it.hasNext()) {
+ Object o = it.next();
+ buf.append('-');
+ buf.append(o.toString());
+ buf.append('\n');
+ }
+ }
+
+ tempSet = new HashSet();
+ tempSet.addAll(assertionSet);
+ tempSet.removeAll(incorrectSet);
+
+ if (tempSet.size() > 0) {
+ buf.append("Set is missing elements:\n");
+ Iterator it = tempSet.iterator();
+ while (it.hasNext()) {
+ Object o = it.next();
+ buf.append('-');
+ buf.append(o.toString());
+ buf.append('\n');
+ }
+ }
+ }
+
+}
diff --git a/org.springframework.test/src/main/java/org/springframework/test/web/package.html b/org.springframework.test/src/main/java/org/springframework/test/web/package.html
new file mode 100644
index 00000000000..d27a1696833
--- /dev/null
+++ b/org.springframework.test/src/main/java/org/springframework/test/web/package.html
@@ -0,0 +1,7 @@
+
+
+
+Helper classes for unit tests based on Spring's web support.
+
+
+
diff --git a/org.springframework.test/src/main/java/overview.html b/org.springframework.test/src/main/java/overview.html
new file mode 100644
index 00000000000..1eb7a2e8c19
--- /dev/null
+++ b/org.springframework.test/src/main/java/overview.html
@@ -0,0 +1,7 @@
+
+
+