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 @@ + + + + + + diff --git a/org.springframework.test/ivy.xml b/org.springframework.test/ivy.xml new file mode 100644 index 00000000000..663231a5992 --- /dev/null +++ b/org.springframework.test/ivy.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.test/pom.xml b/org.springframework.test/pom.xml new file mode 100644 index 00000000000..9fdf36c5c88 --- /dev/null +++ b/org.springframework.test/pom.xml @@ -0,0 +1,40 @@ + + + + org.springframework + org.springframework.parent + 3.0-M1-SNAPSHOT + + 4.0.0 + org.springframework.core + jar + Spring Framework: Core + + + org.apache.log4j + com.springsource.org.apache.log4j + compile + true + + + org.apache.commons + com.springsource.org.apache.commons.collections + true + + + org.aspectj + com.springsource.org.aspectj.weaver + true + + + org.objectweb.asm + com.springsource.org.objectweb.asm + true + + + org.objectweb.asm + com.springsource.org.objectweb.asm.commons + true + + + \ No newline at end of file diff --git a/org.springframework.test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java b/org.springframework.test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java new file mode 100644 index 00000000000..909f8984650 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java @@ -0,0 +1,81 @@ +/* + * 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.Map; + +import javax.naming.NamingException; + +import org.springframework.core.CollectionFactory; +import org.springframework.jndi.JndiTemplate; + +/** + * Simple extension of the JndiTemplate class that always returns + * a given object. Very useful for testing. Effectively a mock object. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class ExpectedLookupTemplate extends JndiTemplate { + + private final Map jndiObjects = CollectionFactory.createConcurrentMapIfPossible(16); + + + /** + * Construct a new JndiTemplate that will always return given objects + * for given names. To be populated through 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 @@ + + + +The simplest implementation of the JNDI SPI that could possibly work. + +

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 replacing any + * existing values for the provided parameter names. To add without + * replacing existing values, use {@link #addParameters(Map)}. + */ + public void setParameters(Map params) { + Assert.notNull(params, "Parameter map must not be null"); + for (Iterator it = params.keySet().iterator(); it.hasNext();) { + Object key = it.next(); + Assert.isInstanceOf(String.class, key, + "Parameter map key must be of type [" + String.class.getName() + "]"); + Object value = params.get(key); + if (value instanceof String) { + this.setParameter((String) key, (String) value); + } + else if (value instanceof String[]) { + this.setParameter((String) key, (String[]) value); + } + else { + throw new IllegalArgumentException("Parameter map value must be single value " + + " or array of type [" + String.class.getName() + "]"); + } + } + } + + /** + * Add a single value for the specified HTTP parameter. + *

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 without replacing + * any existing values. To replace existing values, use + * {@link #setParameters(Map)}. + */ + public void addParameters(Map params) { + Assert.notNull(params, "Parameter map must not be null"); + for (Iterator it = params.keySet().iterator(); it.hasNext();) { + Object key = it.next(); + Assert.isInstanceOf(String.class, key, + "Parameter map key must be of type [" + String.class.getName() + "]"); + Object value = params.get(key); + if (value instanceof String) { + this.addParameter((String) key, (String) value); + } + else if (value instanceof String[]) { + this.addParameter((String) key, (String[]) value); + } + else { + throw new IllegalArgumentException("Parameter map value must be single value " + + " or array of type [" + String.class.getName() + "]"); + } + } + } + + /** + * Remove already registered values for the specified HTTP parameter, if any. + */ + public void removeParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + this.parameters.remove(name); + } + + /** + * Removes all existing parameters. + */ + public void removeAllParameters() { + this.parameters.clear(); + } + + public String getParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + 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) { + Assert.notNull(name, "Parameter name must not be null"); + return (String[]) this.parameters.get(name); + } + + public Map getParameterMap() { + return Collections.unmodifiableMap(this.parameters); + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getProtocol() { + return this.protocol; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public String getScheme() { + return this.scheme; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public String getServerName() { + return this.serverName; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + public int getServerPort() { + return this.serverPort; + } + + public BufferedReader getReader() throws UnsupportedEncodingException { + if (this.content != null) { + InputStream sourceStream = new ByteArrayInputStream(this.content); + Reader sourceReader = (this.characterEncoding != null) ? + new InputStreamReader(sourceStream, this.characterEncoding) : new InputStreamReader(sourceStream); + return new BufferedReader(sourceReader); + } + else { + return null; + } + } + + public void setRemoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + public String getRemoteAddr() { + return this.remoteAddr; + } + + public void setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + } + + public String getRemoteHost() { + return this.remoteHost; + } + + public void setAttribute(String name, Object value) { + checkActive(); + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + public void removeAttribute(String name) { + checkActive(); + Assert.notNull(name, "Attribute name must not be null"); + this.attributes.remove(name); + } + + /** + * Clear all of this request's attributes. + */ + public void clearAttributes() { + this.attributes.clear(); + } + + /** + * Add a new preferred locale, before any existing locales. + */ + public void addPreferredLocale(Locale locale) { + Assert.notNull(locale, "Locale must not be null"); + this.locales.add(0, locale); + } + + public Locale getLocale() { + return (Locale) this.locales.get(0); + } + + public Enumeration getLocales() { + return this.locales.elements(); + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public boolean isSecure() { + return this.secure; + } + + public RequestDispatcher getRequestDispatcher(String path) { + return new MockRequestDispatcher(path); + } + + public String getRealPath(String path) { + return this.servletContext.getRealPath(path); + } + + public void setRemotePort(int remotePort) { + this.remotePort = remotePort; + } + + public int getRemotePort() { + return this.remotePort; + } + + public void setLocalName(String localName) { + this.localName = localName; + } + + public String getLocalName() { + return this.localName; + } + + public void setLocalAddr(String localAddr) { + this.localAddr = localAddr; + } + + public String getLocalAddr() { + return this.localAddr; + } + + public void setLocalPort(int localPort) { + this.localPort = localPort; + } + + public int getLocalPort() { + return this.localPort; + } + + + //--------------------------------------------------------------------- + // HttpServletRequest interface + //--------------------------------------------------------------------- + + public void setAuthType(String authType) { + this.authType = authType; + } + + public String getAuthType() { + return this.authType; + } + + public void setCookies(Cookie[] cookies) { + this.cookies = cookies; + } + + public Cookie[] getCookies() { + return this.cookies; + } + + /** + * Add a header entry for the given name. + *

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 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. + *

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 + * 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. + * + *

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 @@ + + + +A comprehensive set of Servlet API mock objects, +targeted at usage with Spring's web MVC framework. +Useful for testing web contexts and controllers. + +

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 @@ + + + +A comprehensive set of Portlet API mock objects, +targeted at usage with Spring's web MVC framework. +Useful for testing web contexts and controllers. + +

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: + *

+ * + * + * @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 + * 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. + *

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 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. + *

Note: if the {@link ApplicationContext} for this test instance has not + * been configured (e.g., is 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). + *

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. + *

+ * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + * @see #getConfigLocations() + * @see #contextKey() + * @see #loadContext(Object) + * @see #getApplicationContext() + */ +public abstract class AbstractSingleSpringContextTests extends AbstractSpringContextTests { + + /** Application context this test will run against */ + protected ConfigurableApplicationContext applicationContext; + + private int loadCount = 0; + + + /** + * Default constructor for AbstractSingleSpringContextTests. + */ + public AbstractSingleSpringContextTests() { + } + + /** + * Constructor for AbstractSingleSpringContextTests with a JUnit name. + * @param name the name of this text fixture + */ + public AbstractSingleSpringContextTests(String name) { + super(name); + } + + /** + * This implementation is final. Override 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. + *

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 setUp() + * method, which is final in this class. + *

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 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()}. + *

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 + * contextKey() returns. + * @see #getConfigLocations() + */ + protected ConfigurableApplicationContext loadContext(Object key) throws Exception { + return loadContextLocations((String[]) key); + } + + /** + * Load a Spring ApplicationContext from the given config locations. + *

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 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. + *

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 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; + +/** + *

+ * 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. + *

+ * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.1.1 + * @see AbstractSingleSpringContextTests + * @see AbstractDependencyInjectionSpringContextTests + * @see AbstractTransactionalSpringContextTests + * @see AbstractTransactionalDataSourceSpringContextTests + */ +public abstract class AbstractSpringContextTests extends ConditionalTestCase { + + /** + * Map of context keys returned by subclasses of this class, to Spring + * contexts. This needs to be static, as JUnit tests are destroyed and + * recreated between running individual test methods. + */ + private static Map contextKeyToContextMap = new HashMap(); + + + /** + * Default constructor for AbstractSpringContextTests. + */ + public AbstractSpringContextTests() { + } + + /** + * Constructor for AbstractSpringContextTests with a JUnit name. + */ + public AbstractSpringContextTests(String name) { + super(name); + } + + /** + * Explicitly add an ApplicationContext instance under a given key. + *

+ * 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 key is empty. + *

+ *

+ * By default, null values, empty strings, and zero-length + * arrays are considered empty. + *

+ * + * @param key the context key to check + * @return 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. + *

+ * 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 + * 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. + *

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. + *

+ * + * @return The rollback flag for the current test. + */ + protected boolean isRollback() { + + return isDefaultRollback(); + } + + /** + * Call this method in an overridden {@link #runBare()} method to prevent + * transactional execution. + */ + protected void preventTransaction() { + + this.transactionDefinition = null; + } + + /** + * Call this method in an overridden {@link #runBare()} method to override + * the transaction attributes that will be used, so that {@link #setUp()} + * and {@link #tearDown()} behavior is modified. + * + * @param customDefinition the custom transaction definition + */ + protected void setTransactionDefinition(final TransactionDefinition customDefinition) { + + this.transactionDefinition = customDefinition; + } + + /** + * This implementation creates a transaction before test execution. + *

+ * 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 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. + *

+ * 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 super.onTearDown() as + * part of your method implementation. + *

+ * 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 complete and {@link #isRollback() rollback} flags. + *

+ * 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: + * + *

+ * // 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. + * + *

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 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. + *

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 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; + +/** + *

+ * 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: + *

+ * + * + * @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)}. + *

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 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. + *

The default implementation is based on + * {@link IfProfileValue @IfProfileValue} semantics. + * @param testMethod the test method + * @return 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 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; + +/** + *

+ * 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 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! + *

+ * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + * @see org.springframework.test.AbstractSingleSpringContextTests + */ +@Target( { ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DirtiesContext { + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/ExpectedException.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/ExpectedException.java new file mode 100644 index 00000000000..22faabb2b44 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/ExpectedException.java @@ -0,0 +1,40 @@ +/* + * 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 test method is required to throw the + * specified exception. + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + */ +@Target( { ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ExpectedException { + + Class value(); + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/IfProfileValue.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/IfProfileValue.java new file mode 100644 index 00000000000..fc9050b2554 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/IfProfileValue.java @@ -0,0 +1,102 @@ +/* + * 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.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

+ * 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: + *

+ * + *
+ * {@link IfProfileValue @IfProfileValue}(name="java.vendor", value="Sun Microsystems Inc.")
+ * testSomething() {
+ *     // ...
+ * }
+ * 
+ * + *

+ * 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): + *

+ * + *
+ * {@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}. + *

Note: Assigning values to both {@link #value()} and {@link #values()} + * will lead to a configuration conflict. + */ + String value() default ""; + + /** + * A list of all permissible values of the + * profile value for the given {@link #name() name}. + *

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 public no-args + * constructor. + *

+ *

+ * Spring provides the following out-of-the-box implementations: + *

+ * + * + * @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; + +/** + *

+ * 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. + *

+ * + * @author Sam Brannen + * @since 2.5 + * @see ProfileValueSource + * @see IfProfileValue + * @see ProfileValueUtils + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +@Documented +public @interface ProfileValueSourceConfiguration { + + /** + *

+ * The type of {@link ProfileValueSource} to use when retrieving + * profile values. + *

+ * + * @see SystemProfileValueSource + */ + Class value() default SystemProfileValueSource.class; + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java new file mode 100644 index 00000000000..ea1a6797af5 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java @@ -0,0 +1,213 @@ +/* + * 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * General utility methods for working with profile values. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see ProfileValueSource + * @see ProfileValueSourceConfiguration + * @see IfProfileValue + */ +public abstract class ProfileValueUtils { + + private static final Log logger = LogFactory.getLog(ProfileValueUtils.class); + + + /** + * Retrieves the {@link ProfileValueSource} type for the specified + * {@link Class test class} as configured via the + * {@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration} + * annotation and instantiates a new instance of that type. + *

+ * 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 annotationType = ProfileValueSourceConfiguration.class; + ProfileValueSourceConfiguration config = testClass.getAnnotation(annotationType); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved @ProfileValueSourceConfiguration [" + config + "] for test class [" + + testClass.getName() + "]"); + } + + Class profileValueSourceType; + if (config != null) { + profileValueSourceType = config.value(); + } + else { + profileValueSourceType = (Class) AnnotationUtils.getDefaultValue(annotationType); + } + if (logger.isDebugEnabled()) { + logger.debug("Retrieved ProfileValueSource type [" + profileValueSourceType + "] for class [" + + testClass.getName() + "]"); + } + + ProfileValueSource profileValueSource; + if (SystemProfileValueSource.class.equals(profileValueSourceType)) { + profileValueSource = SystemProfileValueSource.getInstance(); + } + else { + try { + profileValueSource = profileValueSourceType.newInstance(); + } + catch (Exception e) { + if (logger.isWarnEnabled()) { + logger.warn("Could not instantiate a ProfileValueSource of type [" + profileValueSourceType + + "] for class [" + testClass.getName() + "]: using default.", e); + } + profileValueSource = SystemProfileValueSource.getInstance(); + } + } + + return profileValueSource; + } + + /** + * Determine if the supplied testClass is enabled + * in the current environment, as specified by the + * {@link IfProfileValue @IfProfileValue} annotation at the class level. + *

+ * Defaults to 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. + *

+ * Defaults to 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. + *

+ * Defaults to 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. + *

+ * Note that the scope of execution to be repeated includes execution of the + * test method itself as well as any set up or tear down + * of the test fixture. + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + */ +@Target( { ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Repeat { + + int value() default 1; + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/Rollback.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/Rollback.java new file mode 100644 index 00000000000..e4620f9a2e6 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/Rollback.java @@ -0,0 +1,47 @@ +/* + * 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 whether or not the transaction for the annotated + * test method should be rolled back after the test method has + * completed. If 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 { + + /** + *

+ * Whether or not the transaction for the annotated method should be rolled + * back after the method has completed. + *

+ */ + boolean value() default true; + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java new file mode 100644 index 00000000000..f806acc1ff6 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.annotation; + +import org.springframework.util.Assert; + +/** + * Implementation of {@link ProfileValueSource} which uses system properties as + * the underlying source. + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + */ +public class SystemProfileValueSource implements ProfileValueSource { + + private static final SystemProfileValueSource INSTANCE = new SystemProfileValueSource(); + + + /** + * Obtain the canonical instance of this ProfileValueSource. + */ + public static final SystemProfileValueSource getInstance() { + return INSTANCE; + } + + + /** + * Private constructor, enforcing the singleton pattern. + */ + private SystemProfileValueSource() { + } + + /** + * Get the profile value indicated by the specified key from the + * system properties. + * @see System#getProperty(String) + */ + public String get(String key) { + Assert.hasText(key, "'key' must not be empty"); + return System.getProperty(key); + } + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/Timed.java b/org.springframework.test/src/main/java/org/springframework/test/annotation/Timed.java new file mode 100644 index 00000000000..aaba1d92cd5 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/Timed.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.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-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. + *

+ * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + * @see Repeat + * @see AbstractAnnotationAwareTransactionalTests + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Timed { + + /** + * The maximum amount of time (in milliseconds) that a test execution can + * take without being marked as failed due to taking too long. + */ + long millis(); + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/annotation/package.html b/org.springframework.test/src/main/java/org/springframework/test/annotation/package.html new file mode 100644 index 00000000000..9aa32de9ea9 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/annotation/package.html @@ -0,0 +1,7 @@ + + + +Support classes for annotation-driven tests. + + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/ContextCache.java b/org.springframework.test/src/main/java/org/springframework/test/context/ContextCache.java new file mode 100644 index 00000000000..e63969f7f71 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/ContextCache.java @@ -0,0 +1,198 @@ +/* + * 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.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; + +/** + * Cache for Spring {@link ApplicationContext ApplicationContexts} + * in a test environment. + * + *

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 contextKeyToContextMap = + new ConcurrentHashMap(); + + private int hitCount; + + private int missCount; + + + /** + * Clears all contexts from the cache. + */ + void clear() { + this.contextKeyToContextMap.clear(); + } + + /** + * Clears hit and miss count statistics for the cache (i.e., resets counters + * to zero). + */ + void clearStatistics() { + this.hitCount = 0; + this.missCount = 0; + } + + /** + * Return whether there is a cached context for the given key. + * @param key the context key (never 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. + *

The {@link #getHitCount() hit} and {@link #getMissCount() miss} + * counts will be updated accordingly. + * @param key the context key (never 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}. + *

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 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. + *

The default value is 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 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}. + * + *

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 public no-args + * constructor. + * + *

Spring provides the following out-of-the-box implementations: + *

+ * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + */ +public interface ContextLoader { + + /** + * Processes application context resource locations for a specified class. + *

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 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. + *

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 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 contextLoaderClass = contextConfiguration.loader(); + if (ContextLoader.class.equals(contextLoaderClass)) { + try { + contextLoaderClass = (Class) 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. + *

Note that the + * {@link ContextConfiguration#inheritLocations() inheritLocations} flag of + * {@link ContextConfiguration @ContextConfiguration} will be taken into + * consideration. Specifically, if the 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"); + + List locationsList = new ArrayList(); + Class annotationType = ContextConfiguration.class; + Class declaringClass = AnnotationUtils.findAnnotationDeclaringClass(annotationType, clazz); + Assert.notNull(declaringClass, "Could not find an 'annotation declaring class' for annotation type [" + + annotationType + "] and class [" + clazz + "]"); + + while (declaringClass != null) { + ContextConfiguration contextConfiguration = declaringClass.getAnnotation(annotationType); + if (logger.isTraceEnabled()) { + logger.trace("Retrieved @ContextConfiguration [" + contextConfiguration + "] for declaring class [" + + declaringClass + "]"); + } + String[] locations = contextLoader.processLocations(declaringClass, contextConfiguration.locations()); + locationsList.addAll(0, Arrays. asList(locations)); + declaringClass = contextConfiguration.inheritLocations() ? AnnotationUtils.findAnnotationDeclaringClass( + annotationType, declaringClass.getSuperclass()) : null; + } + + return locationsList.toArray(new String[locationsList.size()]); + } + + /** + * Build an {@link ApplicationContext} for this test context using the + * configured {@link #getContextLoader() ContextLoader} and + * {@link #getLocations() resource locations}. + * @throws Exception if an error occurs while building the application context + */ + private ApplicationContext loadApplicationContext() throws Exception { + Assert.notNull(getContextLoader(), + "Can not build an ApplicationContext with a NULL 'contextLoader'. Consider annotating your test class with @ContextConfiguration."); + Assert.notNull(getLocations(), + "Can not build an ApplicationContext with a NULL 'locations' array. Consider annotating your test class with @ContextConfiguration."); + return getContextLoader().loadContext(getLocations()); + } + + /** + * Convert the supplied context key 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. + *

Note: this is a mutable property. + * @return the current test instance (may be null) + * @see #updateState(Object,Method,Throwable) + */ + public final Object getTestInstance() { + return this.testInstance; + } + + /** + * Gets the current {@link Method test method} for this test context. + *

Note: this is a mutable property. + * @return the current test method (may be 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}. + *

Note: this is a mutable property. + * @return the exception that was thrown, or 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. + *

+ *

+ * Specifically, a 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 List testExecutionListeners = new ArrayList(); + + + /** + * Constructs a new TestContextManager 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 List getTestExecutionListeners() { + return Collections.unmodifiableList(this.testExecutionListeners); + } + + /** + * Retrieves an array of newly instantiated + * {@link TestExecutionListener TestExecutionListeners} for the specified + * {@link Class class}. If + * {@link TestExecutionListeners @TestExecutionListeners} is not + * present on the supplied class, the default listeners will be + * returned. + *

Note that the + * {@link TestExecutionListeners#inheritListeners() inheritListeners} flag + * of {@link TestExecutionListeners @TestExecutionListeners} will be taken + * into consideration. Specifically, if the inheritListeners + * 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"); + Class annotationType = TestExecutionListeners.class; + List> classesList = + new ArrayList>(); + Class declaringClass = AnnotationUtils.findAnnotationDeclaringClass(annotationType, clazz); + boolean defaultListeners = false; + + // Use defaults? + if (declaringClass == null) { + if (logger.isInfoEnabled()) { + logger.info("@TestExecutionListeners is not present for class [" + clazz + "]: using defaults."); + } + classesList.addAll(getDefaultTestExecutionListenerClasses()); + defaultListeners = true; + } + else { + // Traverse the class hierarchy... + while (declaringClass != null) { + TestExecutionListeners testExecutionListeners = declaringClass.getAnnotation(annotationType); + if (logger.isTraceEnabled()) { + logger.trace("Retrieved @TestExecutionListeners [" + testExecutionListeners + + "] for declaring class [" + declaringClass + "]."); + } + Class[] classes = testExecutionListeners.value(); + if (classes != null) { + classesList.addAll(0, Arrays.> asList(classes)); + } + declaringClass = (testExecutionListeners.inheritListeners() ? + AnnotationUtils.findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass()) : null); + } + } + + List listeners = new ArrayList(classesList.size()); + for (Class listenerClass : classesList) { + try { + listeners.add((TestExecutionListener) BeanUtils.instantiateClass(listenerClass)); + } + catch (NoClassDefFoundError err) { + if (defaultListeners) { + if (logger.isDebugEnabled()) { + logger.debug("Could not instantiate default TestExecutionListener class [" + listenerClass.getName() + + "]. Specify custom listener classes or make the default listener classes available."); + } + } + else { + throw err; + } + } + } + return listeners.toArray(new TestExecutionListener[listeners.size()]); + } + + /** + * Determine the default {@link TestExecutionListener} classes. + */ + @SuppressWarnings("unchecked") + protected Set> getDefaultTestExecutionListenerClasses() { + Set> defaultListenerClasses = + new LinkedHashSet>(); + for (String className : DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES) { + try { + defaultListenerClasses.add( + (Class) getClass().getClassLoader().loadClass(className)); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not load default TestExecutionListener class [" + className + + "]. Specify custom listener classes or make the default listener classes available."); + } + } + } + return defaultListenerClasses; + } + + + /** + * Hook for preparing a test instance prior to execution of any individual + * test methods, for example for injecting dependencies, etc. Should be + * called immediately after instantiation of the test instance. + *

The managed {@link TestContext} will be updated with the supplied + * testInstance. + *

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 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} ). + *

The managed {@link TestContext} will be updated with the supplied + * testInstance and testMethod. + *

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 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}). + *

The managed {@link TestContext} will be updated with the supplied + * testInstance, testMethod, and + * exception. + *

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 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. + List listenersReversed = + new ArrayList(getTestExecutionListeners()); + Collections.reverse(listenersReversed); + + Exception afterTestMethodException = null; + for (TestExecutionListener testExecutionListener : listenersReversed) { + try { + testExecutionListener.afterTestMethod(getTestContext()); + } + catch (Exception ex) { + logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to process 'after' execution for test: method [" + testMethod + "], instance [" + + testInstance + "], exception [" + exception + "]", ex); + if (afterTestMethodException == null) { + afterTestMethodException = ex; + } + } + } + if (afterTestMethodException != null) { + throw afterTestMethodException; + } + } + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/org.springframework.test/src/main/java/org/springframework/test/context/TestExecutionListener.java new file mode 100644 index 00000000000..9dbee95214a --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/TestExecutionListener.java @@ -0,0 +1,78 @@ +/* + * 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; + +/** + *

+ * TestExecutionListener defines a listener API for + * reacting to test execution events published by the {@link TestContextManager} + * with which the listener is registered. + *

+ *

+ * Concrete implementations must provide a public no-args + * constructor, so that listeners can be instantiated transparently by tools and + * configuration mechanisms. + *

+ *

+ * Spring provides the following out-of-the-box implementations: + *

+ *
    + *
  • {@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener DependencyInjectionTestExecutionListener}
  • + *
  • {@link org.springframework.test.context.support.DirtiesContextTestExecutionListener DirtiesContextTestExecutionListener}
  • + *
  • {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener TransactionalTestExecutionListener}
  • + *
+ * + * @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. + *

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 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 { + + /** + *

+ * The {@link TestExecutionListener TestExecutionListeners} to register with + * a {@link TestContextManager}. + *

+ * + * @see org.springframework.test.context.support.DependencyInjectionTestExecutionListener + * @see org.springframework.test.context.support.DirtiesContextTestExecutionListener + * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener + */ + Class[] value(); + + /** + *

+ * Whether or not {@link #value() TestExecutionListeners} from superclasses + * should be inherited. + *

+ *

+ * The default value is 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 {
+	 *     // ...
+	 * }
+	 * 
+ * + *

+ * If inheritListeners is set to false, the + * listeners for the annotated class will shadow and effectively + * replace any listeners defined by a superclass. + *

+ */ + boolean inheritListeners() default true; + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java new file mode 100644 index 00000000000..c841db88547 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java @@ -0,0 +1,368 @@ +/* + * 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 java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.test.annotation.ExpectedException; +import org.springframework.test.annotation.IfProfileValue; +import org.springframework.test.annotation.ProfileValueSource; +import org.springframework.test.annotation.ProfileValueUtils; +import org.springframework.test.annotation.Repeat; +import org.springframework.test.annotation.Timed; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; + +/** + *

+ * Abstract base {@link TestCase} which integrates the + * Spring TestContext Framework with explicit + * {@link ApplicationContext} testing support in a JUnit 3.8 + * environment. + *

+ *

+ * Concrete subclasses: + *

+ *
    + *
  • Typically declare a class-level + * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration} + * annotation to configure the {@link ApplicationContext application context} + * {@link org.springframework.test.context.ContextConfiguration#locations() resource locations}. + * If your test does not need to load an + * application context, you may choose to omit the + * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration} declaration and configure + * the appropriate {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners} + * manually.
  • + *
  • Must declare public constructors which match the signatures of + * {@link #AbstractJUnit38SpringContextTests() AbstractJUnit38SpringContextTests()} + * and + * {@link #AbstractJUnit38SpringContextTests(String) AbstractJUnit38SpringContextTests(String)} + * and delegate to super(); and super(name); + * respectively.
  • + *
+ *

+ * The following list constitutes all annotations currently supported directly + * by AbstractJUnit38SpringContextTests. + * (Note that additional annotations may be supported by various + * {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners}) + *

+ *
    + *
  • {@link org.springframework.test.annotation.DirtiesContext @DirtiesContext} + * (via the configured {@link DirtiesContextTestExecutionListener})
  • + *
  • {@link org.springframework.test.annotation.ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}
  • + *
  • {@link IfProfileValue @IfProfileValue}
  • + *
  • {@link ExpectedException @ExpectedException}
  • + *
  • {@link Timed @Timed}
  • + *
  • {@link Repeat @Repeat}
  • + *
+ * + * @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. + *

In addition to standard {@link TestCase#runBare()} semantics, this + * implementation performs the following: + *

    + *
  • Calls + * {@link TestContextManager#prepareTestInstance(Object) prepareTestInstance()}, + * {@link TestContextManager#beforeTestMethod(Object,Method) beforeTestMethod()}, + * and + * {@link TestContextManager#afterTestMethod(Object,Method,Throwable) afterTestMethod()} + * on this test's {@link TestContextManager} at the appropriate test + * execution points.
  • + *
  • Provides support for {@link IfProfileValue @IfProfileValue}.
  • + *
  • Provides support for {@link Repeat @Repeat}.
  • + *
  • Provides support for {@link Timed @Timed}.
  • + *
  • Provides support for {@link ExpectedException @ExpectedException}.
  • + *
+ * @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 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; + +/** + *

+ * 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}. + *

+ * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see AbstractJUnit38SpringContextTests + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.TestExecutionListeners + * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener + * @see org.springframework.test.context.transaction.TransactionConfiguration + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.annotation.NotTransactional + * @see org.springframework.test.annotation.Rollback + * @see org.springframework.test.context.transaction.BeforeTransaction + * @see org.springframework.test.context.transaction.AfterTransaction + * @see org.springframework.test.jdbc.SimpleJdbcTestUtils + * @see org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests + * @see org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests + */ +@TestExecutionListeners({TransactionalTestExecutionListener.class}) +@Transactional +public abstract class AbstractTransactionalJUnit38SpringContextTests extends AbstractJUnit38SpringContextTests { + + /** + * The SimpleJdbcTemplate that this base class manages, available to subclasses. + */ + protected SimpleJdbcTemplate simpleJdbcTemplate; + + private String sqlScriptEncoding; + + + /** + * Constructs a new AbstractTransactionalJUnit38SpringContextTests instance. + */ + public AbstractTransactionalJUnit38SpringContextTests() { + super(); + } + + /** + * Constructs a new AbstractTransactionalJUnit38SpringContextTests instance + * with the supplied 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! + *

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 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 @@ + + + +

Support classes for ApplicationContext-based and transactional +tests run with JUnit 3.8 and the Spring TestContext Framework.

+ + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java new file mode 100644 index 00000000000..39fc1d79056 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java @@ -0,0 +1,92 @@ +/* + * 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.runner.RunWith; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; + +/** + *

+ * 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. + *

+ * + * @author Sam Brannen + * @since 2.5 + * @see ContextConfiguration + * @see TestContext + * @see TestContextManager + * @see AbstractTransactionalJUnit4SpringContextTests + * @see org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests + * @see org.springframework.test.context.testng.AbstractTestNGSpringContextTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class}) +public abstract class AbstractJUnit4SpringContextTests implements ApplicationContextAware { + + /** + * Logger available to subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The {@link ApplicationContext} that was injected into this test instance + * via {@link #setApplicationContext(ApplicationContext)}. + */ + protected ApplicationContext applicationContext; + + + /** + * Set the {@link ApplicationContext} to be used by this test instance, + * provided via {@link ApplicationContextAware} semantics. + */ + public final void setApplicationContext(final ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java new file mode 100644 index 00000000000..44703fe713a --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java @@ -0,0 +1,147 @@ +/* + * 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 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.ContextConfiguration; +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; + +/** + *

+ * 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. + *

+ * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see AbstractJUnit4SpringContextTests + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.TestExecutionListeners + * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener + * @see org.springframework.test.context.transaction.TransactionConfiguration + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.annotation.NotTransactional + * @see org.springframework.test.annotation.Rollback + * @see org.springframework.test.context.transaction.BeforeTransaction + * @see org.springframework.test.context.transaction.AfterTransaction + * @see org.springframework.test.jdbc.SimpleJdbcTestUtils + * @see org.springframework.test.context.junit38.AbstractTransactionalJUnit38SpringContextTests + * @see org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests + */ +@TestExecutionListeners({TransactionalTestExecutionListener.class}) +@Transactional +public abstract class AbstractTransactionalJUnit4SpringContextTests extends AbstractJUnit4SpringContextTests { + + /** + * The SimpleJdbcTemplate that this base class manages, available to subclasses. + */ + protected SimpleJdbcTemplate simpleJdbcTemplate; + + private String sqlScriptEncoding; + + + /** + * Set the DataSource, typically provided via Dependency Injection. + */ + @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! + *

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 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; + +/** + *

+ * 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}) + *

+ *
    + *
  • {@link org.junit.Test#expected() @Test(expected=...)}
  • + *
  • {@link org.springframework.test.annotation.ExpectedException @ExpectedException}
  • + *
  • {@link org.junit.Test#timeout() @Test(timeout=...)}
  • + *
  • {@link org.springframework.test.annotation.Timed @Timed}
  • + *
  • {@link org.springframework.test.annotation.Repeat @Repeat}
  • + *
  • {@link org.junit.Ignore @Ignore}
  • + *
  • {@link org.springframework.test.annotation.ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}
  • + *
  • {@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}
  • + *
+ * + * @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: + *

+ *
    + *
  • Notifies a {@link TestContextManager} of + * {@link TestContextManager#beforeTestMethod(Object,Method) before} and + * {@link TestContextManager#afterTestMethod(Object,Method,Throwable) after} + * events.
  • + *
  • Uses a {@link SpringTestMethod} instead of JUnit 4.4's + * {@link org.junit.internal.runners.TestMethod TestMethod}.
  • + *
  • Tracks the exception thrown during execution of the test method.
  • + *
+ *

+ * Due to method and field visibility constraints, the code of + * MethodRoadie has been duplicated here instead of subclassing + * MethodRoadie directly. + *

+ * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + */ +class SpringMethodRoadie { + + protected static final Log logger = LogFactory.getLog(SpringMethodRoadie.class); + + private final TestContextManager testContextManager; + + private final Object testInstance; + + private final SpringTestMethod testMethod; + + private final RunNotifier notifier; + + private final Description description; + + private Throwable testException; + + + /** + * Constructs a new 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()); + List befores = this.testMethod.getBefores(); + for (Method before : befores) { + before.invoke(this.testInstance); + } + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (!(targetEx instanceof AssumptionViolatedException)) { + addFailure(targetEx); + } + throw new FailedBefore(); + } + catch (Throwable ex) { + addFailure(ex); + throw new FailedBefore(); + } + } + + /** + * Runs {@link org.junit.After @After methods}, registering failures as + * necessary, and then calls {@link TestContextManager#afterTestMethod}. + */ + protected void runAfters() { + List afters = this.testMethod.getAfters(); + for (Method after : afters) { + try { + after.invoke(this.testInstance); + } + catch (InvocationTargetException ex) { + addFailure(ex.getTargetException()); + } + catch (Throwable ex) { + addFailure(ex); + } + } + try { + this.testContextManager.afterTestMethod(this.testInstance, this.testMethod.getMethod(), this.testException); + } + catch (Throwable ex) { + addFailure(ex); + } + } + + /** + * Fire a failure for the supplied exception 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. + * + *

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 getAfters() { + return getTestClass().getAnnotatedMethods(After.class); + } + + /** + * Get the {@link Before @Before} methods for this test method. + */ + public List getBefores() { + return getTestClass().getAnnotatedMethods(Before.class); + } + + /** + * Get the exception that this test method is expected to throw. + *

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 null if none was specified + */ + public Class getExpectedException() throws IllegalStateException { + ExpectedException expectedExAnn = getMethod().getAnnotation(ExpectedException.class); + Test testAnnotation = getMethod().getAnnotation(Test.class); + + Class expectedException = null; + Class springExpectedException = + (expectedExAnn != null && expectedExAnn.value() != null ? expectedExAnn.value() : null); + Class 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. + *

Supports JUnit's {@link Test#timeout() @Test(timeout=...)} annotation. + * @return the timeout, or 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 @@ + + + +

Support classes for ApplicationContext-based and transactional +tests run with JUnit 4.4 and the Spring TestContext Framework.

+ + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/package.html b/org.springframework.test/src/main/java/org/springframework/test/context/package.html new file mode 100644 index 00000000000..cca11921a6b --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/package.html @@ -0,0 +1,18 @@ + + + +

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.

+ + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java b/org.springframework.test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java new file mode 100644 index 00000000000..bc7af11b9b9 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java @@ -0,0 +1,145 @@ +/* + * 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.context.ApplicationContext; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.test.context.ContextLoader; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * Abstract application context loader, which provides a basis for all concrete + * implementations of the {@link ContextLoader} strategy. Provides a + * Template Method based approach for + * {@link #processLocations(Class,String...) processing} locations. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see #generateDefaultLocations + * @see #modifyLocations + */ +public abstract class AbstractContextLoader implements ContextLoader { + + /** + * If the supplied 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. + *

For example, if the supplied class is 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. + *

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:}, http:, + * etc.) will be added to the results unchanged. + *

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 locations provided to + * {@link #processLocations(Class,String...) processLocations()} are + * null or empty. + *

Can be overridden by subclasses to change the default behavior. + * @return always true by default + */ + protected boolean isGenerateDefaultLocations() { + return true; + } + + /** + * Get the suffix to append to {@link ApplicationContext} resource + * locations when generating default locations. + *

Must be implemented by subclasses. + * @return the resource suffix; should not be 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...)}. + * + *

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 locations. + *

Implementation details: + *

    + *
  • Creates a standard {@link GenericApplicationContext} instance.
  • + *
  • Populates it from the specified config locations through a + * {@link #createBeanDefinitionReader(GenericApplicationContext) BeanDefinitionReader}.
  • + *
  • Calls {@link #customizeBeanFactory(DefaultListableBeanFactory)} to + * allow for customizing the context's DefaultListableBeanFactory.
  • + *
  • Delegates to {@link AnnotationConfigUtils} for + * {@link AnnotationConfigUtils#registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry) registering} + * annotation configuration processors.
  • + *
  • Calls {@link #customizeContext(GenericApplicationContext)} to allow + * for customizing the context before it is refreshed.
  • + *
  • {@link ConfigurableApplicationContext#refresh() Refreshes} the + * context and registers a JVM shutdown hook for it.
  • + *
+ *

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; + +/** + * 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. + *

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 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; + +/** + *

+ * Concrete implementation of {@link AbstractGenericContextLoader} which reads + * bean definitions from Java {@link Properties} resources. + *

+ * + * @author Sam Brannen + * @since 2.5 + */ +public class GenericPropertiesContextLoader extends AbstractGenericContextLoader { + + /** + *

+ * Creates a new {@link PropertiesBeanDefinitionReader}. + *

+ * + * @return a new PropertiesBeanDefinitionReader. + * @see AbstractGenericContextLoader#createBeanDefinitionReader(GenericApplicationContext) + * @see PropertiesBeanDefinitionReader + */ + @Override + protected BeanDefinitionReader createBeanDefinitionReader(final GenericApplicationContext context) { + return new PropertiesBeanDefinitionReader(context); + } + + /** + * Returns "-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; + +/** + *

+ * Concrete implementation of {@link AbstractGenericContextLoader} which reads + * bean definitions from XML resources. + *

+ * + * @author Sam Brannen + * @since 2.5 + */ +public class GenericXmlContextLoader extends AbstractGenericContextLoader { + + /** + *

+ * Creates a new {@link XmlBeanDefinitionReader}. + *

+ * + * @return a new XmlBeanDefinitionReader. + * @see AbstractGenericContextLoader#createBeanDefinitionReader(GenericApplicationContext) + * @see XmlBeanDefinitionReader + */ + @Override + protected BeanDefinitionReader createBeanDefinitionReader(final GenericApplicationContext context) { + return new XmlBeanDefinitionReader(context); + } + + /** + * Returns "-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 @@ + + + +

Support classes for the Spring TestContext Framework.

+ + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java b/org.springframework.test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java new file mode 100644 index 00000000000..92bec927612 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java @@ -0,0 +1,161 @@ +/* + * 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 java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.testng.IHookCallBack; +import org.testng.IHookable; +import org.testng.ITestResult; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; + +/** + *

+ * Abstract base test class which integrates the + * Spring TestContext Framework with explicit + * {@link ApplicationContext} testing support in a TestNG + * environment. + *

+ *

+ * Concrete subclasses: + *

+ *
    + *
  • 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.
  • + *
  • Must have constructors which either implicitly or explicitly delegate to + * super();.
  • + *
+ * + * @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 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; + +/** + *

+ * 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}. + *

+ * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see AbstractTestNGSpringContextTests + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.TestExecutionListeners + * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener + * @see org.springframework.test.context.transaction.TransactionConfiguration + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.annotation.NotTransactional + * @see org.springframework.test.annotation.Rollback + * @see org.springframework.test.jdbc.SimpleJdbcTestUtils + * @see org.springframework.test.context.junit38.AbstractTransactionalJUnit38SpringContextTests + * @see org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests + */ +@TestExecutionListeners({TransactionalTestExecutionListener.class}) +@Transactional +public abstract class AbstractTransactionalTestNGSpringContextTests extends AbstractTestNGSpringContextTests { + + /** + * The SimpleJdbcTemplate that this base class manages, available to subclasses. + */ + protected SimpleJdbcTemplate simpleJdbcTemplate; + + private String sqlScriptEncoding; + + + /** + * Set the DataSource, typically provided via Dependency Injection. + * @param dataSource the DataSource to inject + */ + @Autowired + public void setDataSource(final 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! + *

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 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 @@ + + + +

Support classes for ApplicationContext-based and transactional +tests run with TestNG and the Spring TestContext Framework.

+ + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java new file mode 100644 index 00000000000..26a7a44c424 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java @@ -0,0 +1,46 @@ +/* + * 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.transaction; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

+ * Test annotation to indicate that the annotated public void + * method should be executed after a transaction is ended for test + * methods configured to run within a transaction via the + * @Transactional annotation. + *

+ *

+ * The @AfterTransaction methods of superclasses will be + * executed after those of the current class. + *

+ * + * @author Sam Brannen + * @since 2.5 + * @see org.springframework.transaction.annotation.Transactional + */ +@Target( { ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AfterTransaction { + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java new file mode 100644 index 00000000000..488eaa5d1c8 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java @@ -0,0 +1,46 @@ +/* + * 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.transaction; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

+ * Test annotation to indicate that the annotated public void + * method should be executed before a transaction is started for test + * methods configured to run within a transaction via the + * @Transactional annotation. + *

+ *

+ * The @BeforeTransaction methods of superclasses will be + * executed before those of the current class. + *

+ * + * @author Sam Brannen + * @since 2.5 + * @see org.springframework.transaction.annotation.Transactional + */ +@Target( { ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface BeforeTransaction { + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java new file mode 100644 index 00000000000..420f35d7a68 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.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.transaction; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * TransactionConfiguration defines class-level metadata for configuring + * transactional tests. + * + * @author Sam Brannen + * @since 2.5 + * @see ContextConfiguration + * @see TransactionalTestExecutionListener + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +@Documented +public @interface TransactionConfiguration { + + /** + * The bean name of the {@link PlatformTransactionManager} that is to be + * used to drive transactions. This attribute is not required and only needs + * to be specified explicitly if the bean name of the desired + * PlatformTransactionManager is not "transactionManager". + */ + String transactionManager() default "transactionManager"; + + /** + * Should transactions be rolled back by default? + */ + boolean defaultRollback() default true; + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java new file mode 100644 index 00000000000..c6124703b87 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.transaction; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.util.Assert; + +/** + * Configuration attributes for configuring transactional tests. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see TransactionConfiguration + */ +public class TransactionConfigurationAttributes { + + private final String transactionManagerName; + + private final boolean defaultRollback; + + + /** + * Construct a new TransactionConfigurationAttributes instance from the + * supplied arguments. + * @param transactionManagerName the bean name of the + * {@link PlatformTransactionManager} that is to be used to drive transactions + * @param defaultRollback whether or not transactions should be rolled back by default + */ + public TransactionConfigurationAttributes(String transactionManagerName, boolean defaultRollback) { + Assert.notNull(transactionManagerName, "transactionManagerName can not be null"); + this.transactionManagerName = transactionManagerName; + this.defaultRollback = defaultRollback; + } + + + /** + * Get the bean name of the {@link PlatformTransactionManager} that is to + * be used to drive transactions. + */ + public final String getTransactionManagerName() { + return this.transactionManagerName; + } + + /** + * Whether or not transactions should be rolled back by default. + * @return the default rollback flag + */ + public final boolean isDefaultRollback() { + return this.defaultRollback; + } + + + @Override + public String toString() { + return new ToStringCreator(this) + .append("transactionManagerName", this.transactionManagerName) + .append("defaultRollback", this.defaultRollback) + .toString(); + } + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java new file mode 100644 index 00000000000..0ca7c294688 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -0,0 +1,509 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.transaction; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.annotation.NotTransactional; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.DelegatingTransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + *

+ * TestExecutionListener which provides support for executing + * tests within transactions by using + * {@link org.springframework.transaction.annotation.Transactional @Transactional} + * and {@link NotTransactional @NotTransactional} annotations. + *

+ *

+ * 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. TransactionalTestExecutionListener provides such + * support for methods annotated with + * {@link BeforeTransaction @BeforeTransaction} and + * {@link AfterTransaction @AfterTransaction}. + *

+ * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see TransactionConfiguration + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.annotation.NotTransactional + * @see org.springframework.test.annotation.Rollback + * @see BeforeTransaction + * @see AfterTransaction + */ +public class TransactionalTestExecutionListener extends AbstractTestExecutionListener { + + private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class); + + protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(); + + private TransactionConfigurationAttributes configAttributes; + + private volatile int transactionsStarted = 0; + + private final Map transactionContextCache = + Collections.synchronizedMap(new IdentityHashMap()); + + + /** + * If the test method of the supplied {@link TestContext test context} is + * configured to run within a transaction, this method will run + * {@link BeforeTransaction @BeforeTransaction methods} and start a new + * 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 methods = getAnnotatedMethods(testContext.getTestClass(), BeforeTransaction.class); + Collections.reverse(methods); + for (Method method : methods) { + if (logger.isDebugEnabled()) { + logger.debug("Executing @BeforeTransaction method [" + method + "] for test context [" + + testContext + "]"); + } + method.invoke(testContext.getTestInstance()); + } + } + catch (InvocationTargetException ex) { + logger.error("Exception encountered while executing @BeforeTransaction methods for test context [" + + testContext + "]", ex.getTargetException()); + ReflectionUtils.rethrowException(ex.getTargetException()); + } + } + + /** + * Run all {@link AfterTransaction @AfterTransaction methods} for the + * specified {@link TestContext test context}. If one of the methods fails, + * the caught exception will be logged as an error, and the remaining + * methods will be given a chance to execute. After all methods have + * executed, the first caught exception, if any, will be rethrown. + * @param testContext the current test context + */ + protected void runAfterTransactionMethods(TestContext testContext) throws Exception { + Throwable afterTransactionException = null; + + List methods = getAnnotatedMethods(testContext.getTestClass(), AfterTransaction.class); + for (Method method : methods) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Executing @AfterTransaction method [" + method + "] for test context [" + + testContext + "]"); + } + method.invoke(testContext.getTestInstance()); + } + catch (InvocationTargetException ex) { + Throwable targetException = ex.getTargetException(); + if (afterTransactionException == null) { + afterTransactionException = targetException; + } + logger.error("Exception encountered while executing @AfterTransaction method [" + method + + "] for test context [" + testContext + "]", targetException); + } + catch (Exception ex) { + if (afterTransactionException == null) { + afterTransactionException = ex; + } + logger.error("Exception encountered while executing @AfterTransaction method [" + method + + "] for test context [" + testContext + "]", ex); + } + } + + if (afterTransactionException != null) { + ReflectionUtils.rethrowException(afterTransactionException); + } + } + + /** + * Start a new transaction for the supplied {@link TestContext test context}. + *

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 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. + *

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> getSuperClasses(Class clazz) { + ArrayList> results = new ArrayList>(); + Class current = clazz; + while (current != null) { + results.add(current); + current = current.getSuperclass(); + } + return results; + } + + /** + * Gets all methods in the supplied {@link Class class} and its superclasses + * which are annotated with the supplied annotationType but + * which are not shadowed by methods overridden in subclasses. + *

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 getAnnotatedMethods(Class clazz, Class annotationType) { + List results = new ArrayList(); + for (Class eachClass : getSuperClasses(clazz)) { + Method[] methods = eachClass.getDeclaredMethods(); + for (Method eachMethod : methods) { + Annotation annotation = eachMethod.getAnnotation(annotationType); + if (annotation != null && !isShadowed(eachMethod, results)) { + results.add(eachMethod); + } + } + } + return results; + } + + /** + * Determines if the supplied {@link Method method} is shadowed + * by a method in supplied {@link List list} of previous methods. + *

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 true if the supplied method is shadowed by a + * method in the previousMethods list + */ + private boolean isShadowed(Method method, List previousMethods) { + for (Method each : previousMethods) { + if (isShadowed(method, each)) { + return true; + } + } + return false; + } + + /** + * Determines if the supplied {@link Method current method} is + * shadowed by a {@link Method previous method}. + *

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 true if the previous method shadows the current one + */ + private boolean isShadowed(Method current, Method previous) { + if (!previous.getName().equals(current.getName())) { + return false; + } + if (previous.getParameterTypes().length != current.getParameterTypes().length) { + return false; + } + for (int i = 0; i < previous.getParameterTypes().length; i++) { + if (!previous.getParameterTypes()[i].equals(current.getParameterTypes()[i])) { + return false; + } + } + return true; + } + + /** + *

+ * Retrieves the {@link TransactionConfigurationAttributes} for the + * specified {@link Class class} which may optionally declare or inherit 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 annotationType = TransactionConfiguration.class; + TransactionConfiguration config = clazz.getAnnotation(annotationType); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved @TransactionConfiguration [" + config + "] for test class [" + clazz + "]"); + } + + String transactionManagerName; + boolean defaultRollback; + if (config != null) { + transactionManagerName = config.transactionManager(); + defaultRollback = config.defaultRollback(); + } + else { + transactionManagerName = (String) AnnotationUtils.getDefaultValue(annotationType, "transactionManager"); + defaultRollback = (Boolean) AnnotationUtils.getDefaultValue(annotationType, "defaultRollback"); + } + + TransactionConfigurationAttributes configAttributes = + new TransactionConfigurationAttributes(transactionManagerName, defaultRollback); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved TransactionConfigurationAttributes [" + configAttributes + "] for class [" + clazz + "]"); + } + return configAttributes; + } + + + /** + * Internal context holder for a specific test method. + */ + private static class TransactionContext { + + private final PlatformTransactionManager transactionManager; + + private final TransactionDefinition transactionDefinition; + + private TransactionStatus transactionStatus; + + public TransactionContext(PlatformTransactionManager transactionManager, TransactionDefinition transactionDefinition) { + this.transactionManager = transactionManager; + this.transactionDefinition = transactionDefinition; + } + + public void startTransaction() { + this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition); + } + + public void endTransaction(boolean rollback) { + if (rollback) { + this.transactionManager.rollback(this.transactionStatus); + } + else { + this.transactionManager.commit(this.transactionStatus); + } + } + } + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/context/transaction/package.html b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/package.html new file mode 100644 index 00000000000..81f183bfb76 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/context/transaction/package.html @@ -0,0 +1,8 @@ + + + +

Transactional support classes for the Spring TestContext +Framework.

+ + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java b/org.springframework.test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java new file mode 100644 index 00000000000..457ea21b58d --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java @@ -0,0 +1,108 @@ +/* + * Copyright 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.jdbc; + +import org.springframework.util.StringUtils; + +import java.util.List; +import java.io.LineNumberReader; +import java.io.IOException; + +/** + * JdbcTestUtils is a collection of JDBC related utility methods for + * use in unit and integration testing scenarios. + * + * @author Thomas Risberg + * @since 2.5.4 + */ +public class JdbcTestUtils { + + /** + * Read a script from the LineNumberReaded and build a String containing the lines. + * + * @param lineNumberReader the 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. + *

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 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. + *

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 false + */ + public static void executeSqlScript(SimpleJdbcTemplate simpleJdbcTemplate, + Resource resource, boolean continueOnError) throws DataAccessException { + + executeSqlScript(simpleJdbcTemplate, new EncodedResource(resource), continueOnError); + } + + /** + * Execute the given SQL script. + *

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 false + */ + public static void executeSqlScript(SimpleJdbcTemplate simpleJdbcTemplate, + EncodedResource resource, boolean continueOnError) throws DataAccessException { + + if (logger.isInfoEnabled()) { + logger.info("Executing SQL script from " + resource); + } + + long startTime = System.currentTimeMillis(); + List 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 = itr.next(); + try { + int rowsAffected = simpleJdbcTemplate.update(statement); + if (logger.isDebugEnabled()) { + logger.debug(rowsAffected + " rows affected by SQL: " + statement); + } + } + catch (DataAccessException ex) { + if (continueOnError) { + if (logger.isWarnEnabled()) { + logger.warn("SQL: " + statement + " failed", ex); + } + } + else { + throw ex; + } + } + } + long elapsedTime = System.currentTimeMillis() - startTime; + if (logger.isInfoEnabled()) { + logger.info("Done executing SQL scriptBuilder from " + resource + " in " + elapsedTime + " ms."); + } + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Failed to open SQL script from " + resource, ex); + } + } + +} diff --git a/org.springframework.test/src/main/java/org/springframework/test/jdbc/package.html b/org.springframework.test/src/main/java/org/springframework/test/jdbc/package.html new file mode 100644 index 00000000000..28a0768b8f1 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/jdbc/package.html @@ -0,0 +1,7 @@ + + + +Support classes for tests based on JDBC. + + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java b/org.springframework.test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java new file mode 100644 index 00000000000..70478983bec --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java @@ -0,0 +1,59 @@ +/* + * 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.test.jpa; + +import org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter; + +import org.springframework.instrument.classloading.ResourceOverridingShadowingClassLoader; + +/** + * Subclass of AbstractJpaTests that activates AspectJ load-time weaving + * and allows the ability to specify a custom location for AspectJ's + * aop.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". + *

Override this method to point to a specific 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. + * + *

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: + *

+ * <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 Map contextCache = new HashMap(); + + private static Map classLoaderCache = new HashMap(); + + protected EntityManagerFactory entityManagerFactory; + + /** + * If this instance is in a shadow loader, this variable + * will contain the parent instance of the subclass. + * The class will not be the same as the class of the + * shadow instance, as it was loaded by a different class loader, + * but it can be invoked reflectively. The shadowParent + * and the shadow loader can communicate reflectively + * but not through direct invocation. + */ + private Object shadowParent; + + /** + * Subclasses can use this in test cases. + * It will participate in any current transaction. + */ + protected EntityManager sharedEntityManager; + + + public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + this.sharedEntityManager = SharedEntityManagerCreator.createSharedEntityManager(this.entityManagerFactory); + } + + /** + * Create an EntityManager that will always automatically enlist itself in current + * transactions, in contrast to an EntityManager returned by + * EntityManagerFactory.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. + *

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 orm.xml. + * + *

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 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 @@ + + + +Support classes for tests based on the Java Persistence API. + + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/package.html b/org.springframework.test/src/main/java/org/springframework/test/package.html new file mode 100644 index 00000000000..2d1899fc3ca --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/package.html @@ -0,0 +1,14 @@ + + + +Superclasses for tests requiring Spring application contexts, +including support for transactional execution of test cases, +with automatic rollback on completion. +Useful as base classes for application-specific tests. + +

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-public field or invoke a non-public + * setter method when testing code involving, for example: + *

+ *
    + *
  • ORM frameworks such as JPA and Hibernate which condone the usage of + * private or protected field access as opposed to + * public setter methods for properties in a domain entity.
  • + *
  • Spring's support for annotations such as + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} and + * {@link javax.annotation.Resource @Resource} which provides dependency + * injection for private or protected fields, + * setter methods, and configuration methods.
  • + *
+ * + * @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 name on the + * provided {@link Object target object} to the supplied value. + *

This method traverses the class hierarchy in search of the desired field. + * In addition, an attempt will be made to make non-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. + *

This method traverses the class hierarchy in search of the desired field. + * In addition, an attempt will be made to make non-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. + *

This method traverses the class hierarchy in search of the desired field. + * In addition, an attempt will be made to make non-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. + *

This method traverses the class hierarchy in search of the desired + * method. In addition, an attempt will be made to make non-public + * methods accessible, thus allowing one to invoke protected, + * private, and package-private setter methods. + *

In addition, this method supports JavaBean-style property + * names. For example, if you wish to set the 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. + *

This method traverses the class hierarchy in search of the desired + * method. In addition, an attempt will be made to make non-public + * methods accessible, thus allowing one to invoke protected, + * private, and package-private setter methods. + *

In addition, this method supports JavaBean-style property + * names. For example, if you wish to set the 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. + *

This method traverses the class hierarchy in search of the desired + * method. In addition, an attempt will be made to make non-public + * methods accessible, thus allowing one to invoke protected, + * private, and package-private getter methods. + *

In addition, this method supports JavaBean-style property + * names. For example, if you wish to get the 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 @@ + + + +Helper classes for unit tests with reflective needs. + + + diff --git a/org.springframework.test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java b/org.springframework.test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java new file mode 100644 index 00000000000..25805627137 --- /dev/null +++ b/org.springframework.test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java @@ -0,0 +1,181 @@ +/* + * 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.Comparator; +import java.util.List; +import java.util.Map; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.springframework.web.servlet.ModelAndView; + +/** + *

+ * Convenient JUnit 3.8 base class for tests dealing with Spring Web MVC + * {@link org.springframework.web.servlet.ModelAndView ModelAndView} objects. + *

+ *

+ * All assert*() methods throw {@link AssertionFailedError}s. + *

+ *

+ * Consider the use of {@link ModelAndViewAssert} with JUnit 4 and TestNG. + *

+ * + * @author Alef Arendsen + * @author Bram Smeets + * @author Sam Brannen + * @since 2.0 + * @see org.springframework.web.servlet.ModelAndView + * @see ModelAndViewAssert + */ +public abstract class AbstractModelAndViewTests extends TestCase { + + /** + * Checks whether the model value under the given 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; + +/** + *

+ * 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 assert*() methods throw {@link AssertionError}s. + *

+ * + * @author Sam Brannen + * @author Alef Arendsen + * @author Bram Smeets + * @since 2.5 + * @see org.springframework.web.servlet.ModelAndView + */ +public abstract class ModelAndViewAssert { + + /** + * Checks whether the model value under the given 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 @@ + + +

+The Spring Data Binding framework, an internal library used by Spring Web Flow. +

+ + \ No newline at end of file diff --git a/org.springframework.test/src/test/resources/log4j.xml b/org.springframework.test/src/test/resources/log4j.xml new file mode 100644 index 00000000000..767b96d6206 --- /dev/null +++ b/org.springframework.test/src/test/resources/log4j.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.springframework.test/template.mf b/org.springframework.test/template.mf new file mode 100644 index 00000000000..5d8aed02085 --- /dev/null +++ b/org.springframework.test/template.mf @@ -0,0 +1,26 @@ +Bundle-SymbolicName: org.springframework.test +Bundle-Name: Spring Test +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Template: + javax.activation.*;version="[1.1.0, 2.0.0)";resolution:=optional, + javax.persistence.*;version="[1.0.0, 2.0.0)";resolution:=optional, + javax.portlet.*;version="[1.0.0, 2.0.0)";resolution:=optional, + javax.servlet;version="[2.4.0, 3.0.0)";resolution:=optional, + javax.servlet.http;version="[2.4.0, 3.0.0)";resolution:=optional, + javax.servlet.jsp.*;version="[2.1.0, 3.0.0)";resolution:=optional, + junit.framework.*;version="[3.8.2, 4.0.0)";resolution:=optional, + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.apache.taglibs.standard.*;version="[1.1.2, 2.0.0)";resolution:=optional, + org.aspectj.weaver.*;version="[1.5.4, 2.0.0)";resolution:=optional, + org.junit.*;version="[4.4.0, 5.0.0)";resolution:=optional, + org.springframework.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.testng.*;version="[5.8.0, 6.0.0)";resolution:=optional +Unversioned-Imports: + javax.naming.*, + javax.sql +Ignored-Existing-Headers: + Bnd-LastModified, + Import-Package, + Export-Package, + Tool