001: /*
002: * Copyright 2006,2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springunit.framework;
018:
019: import java.lang.reflect.Method;
020: import java.util.ArrayList;
021: import java.util.List;
022:
023: import org.springframework.core.io.DefaultResourceLoader;
024: import org.springframework.core.io.Resource;
025: import org.springframework.core.io.ResourceLoader;
026: import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
027: import org.springframework.test.AbstractTransactionalSpringContextTests;
028: import org.springunit.framework.junit4.SpringUnit4Test;
029: import org.springunit.framework.junit4.SpringUnit4TransactionalTest;
030:
031: /**
032: * Manages the contexts of a SpringUnit test.
033: * Stores an ordered list of the classes that comprise
034: * the class hierarchy for the test (<code>testClasses</code>).
035: * The first element in the list is the class of the test,
036: * the last element is the descendent of the descendent
037: * of AbstractDependencyInjectionSpringContextTests
038: * or AbstractTransactionalSpringContextTests
039: * that is an ancestor of the test class.<br/>
040: * Using the list of classes in the hierarchy,
041: * creates and provides a corresponding list of
042: * resources that describe configuration data used
043: * to populate the corresponding contexts.<br/>
044: * Given a name for a data value, a name of a test
045: * and the test object itself, traverses the
046: * hierarchy of contexts to return the requested
047: * data value, if it exists.<br/>
048: *
049: * @author <a href="mailto:ted@velkoff.com">Ted Velkoff</a>
050: *
051: */
052: public class HierarchicalSpringUnitContext<S extends AbstractDependencyInjectionSpringContextTests> {
053:
054: /**
055: * Given the class of the current test (<code>testClass</code>),
056: * construct an ordered list of classes, where the first class is
057: * <code>testClass</code>, the second class is the superclass of
058: * <code>testClass</code> (if any), the third class is the superclass
059: * of the second (if any), etc., and the last class is
060: * the immediate descendent of the immediate descendent of
061: * AbstractDependencyInjectionSpringContextTests
062: * or AbstractTransactionalSpringContextTests.
063: * @param testClass Class of test whose class hierarchy is to be constructed.
064: */
065: protected HierarchicalSpringUnitContext(Class<? extends S> testClass) {
066: this .resourceLoader = new DefaultResourceLoader();
067: this .testClasses = new ArrayList<Class<? super S>>();
068: buildTestClasses(this .testClasses, (Class<? super S>) testClass);
069: }
070:
071: /**
072: * Given the list of test classes in the hierarchy,
073: * return the list of resource names that satisfy the naming convention
074: * "<i>Classname</i>.xml".<br/>
075: * Caches the list of locations so this need only be computed once.
076: * @return Array of string filenames
077: */
078: protected String[] getConfigLocations() {
079: if (this .configLocations == null) {
080: this .configLocations = createDataFileNames(this .testClasses);
081: }
082: return this .configLocations;
083: }
084:
085: /**
086: * Search for object identified by <code>key</code>
087: * in the hierarchy of classes descending from
088: * the descendent of AbstractDependencyInjectionSpringContextTests
089: * or AbstractTransactionalSpringContextTests.
090: * This hierarchy is expressed as a list (<code>testClasses</code>)
091: * with lowest first and highest last.
092: * Iterates through this list,
093: * calling <code>getObject(key, getName(), c)</code>
094: * until a non-null result is returned or
095: * every member of the list has been visited.
096: * @param key Identifier of data value to find
097: * @param fName Name of test
098: * @param test SpringUnit test whose context will be searched
099: * @return Object of type T if found or null
100: * @throws Exception if errors occur when using reflection
101: * to access the SpringUnitContext for any
102: * class in the list
103: */
104: protected <T extends Object> T getObject(String key, String fName,
105: Object test) throws Exception {
106: for (Class<? super S> c : this .testClasses) {
107: T obj = (T) getObject(key, fName, c, test);
108: if (obj != null) {
109: return obj;
110: }
111: }
112: return null;
113: }
114:
115: /**
116: * Using reflection to obtain the SpringUnitContext associated
117: * with class <code>c</code>, return the result of calling
118: * <code>getObject</code> on that context for
119: * identifier <code>key</code> and test method
120: * <code>fName</code>.
121: * @param key Identifier of data value
122: * @param fName Name of test in whose scope to begin search
123: * @param c Class whose SpringUnitContext to be searched
124: * @param test SpringUnit test whose context will be searched
125: * @return Object of type T if found, or null
126: * @throws Exception if errors occur when using reflection
127: * to access the SpringUnitContext for class <code>c</code>
128: */
129: private <T extends Object> T getObject(String key, String fName,
130: Class<? super S> c, Object test) throws Exception {
131: assert c != null : "c != null";
132: String methodName = "get" + c.getSimpleName();
133: Method method = c.getMethod(methodName, new Class[0]);
134: SpringUnitContext<T> dataContext = (SpringUnitContext<T>) method
135: .invoke(test, new Object[0]);
136: return dataContext.getObject(key, fName);
137: }
138:
139: /**
140: * Generate an array of resource names, given a list of <code>classes</code>.
141: * For each class <code>c</code> in <code>classes</code>,
142: * form a resource name by converting its fully qualified name
143: * to a path and appending the file extension ".xml".
144: * For backward compatibility, resource names derived from
145: * the simple class name are generated when a resource
146: * identified by the fully qualified name cannot be found.
147: * By convention, each class descending from a SpringUnit test has
148: * an associated file named <code><i>Classname</i>.xml</code>.
149: * @param classes List of classes that are ancestors of the
150: * test class and proper descendents of a SpringUnit test
151: * @return Array of string resource names
152: */
153: private String[] createDataFileNames(List<Class<? super S>> classes) {
154: assert classes != null : "classes != null";
155: String[] result = new String[classes.size()];
156: int i = 0;
157: for (Class<? super S> c : classes) {
158: result[i++] = createDataFileName(c);
159: }
160: return result;
161: }
162:
163: /**
164: * Derive the resource name of the XML bean configuration file
165: * from the name of the class <code>c</code>.<br/>
166: * @param c Class whose bean configuration resource name
167: * will be derived
168: * @return Name of resource containing the corresponding
169: * XML bean configuration file
170: */
171: private String createDataFileName(Class c) {
172: assert c != null : "c != null";
173: StringBuilder result = new StringBuilder();
174: result.append("classpath:");
175: String name = c.getName();
176: int index = 0;
177: int dot = name.indexOf('.', index);
178: while (dot > -1) {
179: result.append(name.substring(index, dot));
180: result.append('/');
181: index = dot + 1;
182: dot = name.indexOf('.', index);
183: }
184: result.append(name.substring(index));
185: result.append(".xml");
186: return simpleOrFullyQualifiedName(result.toString());
187: }
188:
189: /**
190: * Return <code>resourceName</code> if its corresponding resource exists.
191: * Otherwise, derive a name for the resource if it were held in the
192: * root directory of the classpath and return that name.
193: * <br/>
194: * This provides backward-compatibility with earlier versions of
195: * the framework, which required that the XML data files be
196: * kept in the root directory.
197: * @param resourceName Name of the resource to locate
198: * @return resourceName if resource exists, otherwise
199: * a derived name of a resource found in the root of the classpath
200: */
201: private String simpleOrFullyQualifiedName(String resourceName) {
202: Resource resource = this .resourceLoader
203: .getResource(resourceName);
204: if (resource.exists()) {
205: return resourceName;
206: }
207: // Continue for backward compatibility
208: StringBuilder result = new StringBuilder();
209: result.append("classpath:");
210: String name = resourceName.substring("classpath:".length());
211: int slash = name.lastIndexOf('/');
212: if (slash < 0) {
213: result.append(name);
214: } else {
215: result.append(name.substring(slash + 1));
216: }
217: return result.toString();
218: }
219:
220: /**
221: * Build the list of ancestors of class <code>c</code>
222: * that are proper descendents of the immediate
223: * descendent of AbstractDependencyInjectionSpringContextTests.<br/>
224: * @param c Class to be added to list of <code>classes</code>
225: * unless <code>c.getSuperClass()</code> is
226: * <code>org.springframework.test.AbstractDependencyInjectionSpringContextTests</code> or
227: * <code>org.springframework.test.AbstractTransactionalSpringContextTests</code>,
228: * in which case the recursion stops.
229: * @param classes List that accumulates the ordered list of
230: * ancestors of <code>c</code>
231: */
232: private void buildTestClasses(List<Class<? super S>> classes,
233: Class<? super S> c) {
234: assert c != null : "c != null";
235: assert classes != null : "classes != null";
236: Class<? super S> super class = c.getSuperclass();
237: if (!AbstractDependencyInjectionSpringContextTests.class
238: .equals(super class)
239: && !AbstractTransactionalSpringContextTests.class
240: .equals(super class)
241: && !SpringUnit4Test.class.equals(c)
242: && !SpringUnit4TransactionalTest.class.equals(c)) {
243: classes.add(c);
244: buildTestClasses(classes, super class);
245: }
246: }
247:
248: /**
249: * List of classes that comprise the class hierarchy
250: * of the test. The class of the test is first
251: * in the list.<br/>
252: */
253: private List<Class<? super S>> testClasses;
254:
255: /**
256: * List of names of resources that hold test data.
257: * This caches the list of locations so it need only
258: * be generated once.
259: */
260: private String[] configLocations;
261:
262: /**
263: * ResourceLoader is used to check for existence of data
264: * configuration files that correspond to the classes of the
265: * test hierarchy. This check provides backward compatibility
266: * for data files stored in the root directory of the classpath.
267: */
268: private ResourceLoader resourceLoader;
269:
270: }
|