1 /*
2 * Copyright 2006,2007 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.springunit.framework;
18
19 import java.lang.reflect.Method;
20 import java.util.ArrayList;
21 import java.util.List;
22
23 import org.springframework.core.io.DefaultResourceLoader;
24 import org.springframework.core.io.Resource;
25 import org.springframework.core.io.ResourceLoader;
26 import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
27 import org.springframework.test.AbstractTransactionalSpringContextTests;
28 import org.springunit.framework.junit4.SpringUnit4Test;
29 import org.springunit.framework.junit4.SpringUnit4TransactionalTest;
30
31 /**
32 * Manages the contexts of a SpringUnit test.
33 * Stores an ordered list of the classes that comprise
34 * the class hierarchy for the test (<code>testClasses</code>).
35 * The first element in the list is the class of the test,
36 * the last element is the descendent of the descendent
37 * of AbstractDependencyInjectionSpringContextTests
38 * or AbstractTransactionalSpringContextTests
39 * that is an ancestor of the test class.<br/>
40 * Using the list of classes in the hierarchy,
41 * creates and provides a corresponding list of
42 * resources that describe configuration data used
43 * to populate the corresponding contexts.<br/>
44 * Given a name for a data value, a name of a test
45 * and the test object itself, traverses the
46 * hierarchy of contexts to return the requested
47 * data value, if it exists.<br/>
48 *
49 * @author <a href="mailto:ted@velkoff.com">Ted Velkoff</a>
50 *
51 */
52 public class HierarchicalSpringUnitContext<S extends AbstractDependencyInjectionSpringContextTests> {
53
54 /**
55 * Given the class of the current test (<code>testClass</code>),
56 * construct an ordered list of classes, where the first class is
57 * <code>testClass</code>, the second class is the superclass of
58 * <code>testClass</code> (if any), the third class is the superclass
59 * of the second (if any), etc., and the last class is
60 * the immediate descendent of the immediate descendent of
61 * AbstractDependencyInjectionSpringContextTests
62 * or AbstractTransactionalSpringContextTests.
63 * @param testClass Class of test whose class hierarchy is to be constructed.
64 */
65 protected HierarchicalSpringUnitContext(Class<? extends S> testClass) {
66 this.resourceLoader = new DefaultResourceLoader();
67 this.testClasses = new ArrayList<Class<? super S>>();
68 buildTestClasses(this.testClasses, (Class<? super S>)testClass);
69 }
70
71 /**
72 * Given the list of test classes in the hierarchy,
73 * return the list of resource names that satisfy the naming convention
74 * "<i>Classname</i>.xml".<br/>
75 * Caches the list of locations so this need only be computed once.
76 * @return Array of string filenames
77 */
78 protected String[] getConfigLocations() {
79 if (this.configLocations == null) {
80 this.configLocations = createDataFileNames(this.testClasses);
81 }
82 return this.configLocations;
83 }
84
85 /**
86 * Search for object identified by <code>key</code>
87 * in the hierarchy of classes descending from
88 * the descendent of AbstractDependencyInjectionSpringContextTests
89 * or AbstractTransactionalSpringContextTests.
90 * This hierarchy is expressed as a list (<code>testClasses</code>)
91 * with lowest first and highest last.
92 * Iterates through this list,
93 * calling <code>getObject(key, getName(), c)</code>
94 * until a non-null result is returned or
95 * every member of the list has been visited.
96 * @param key Identifier of data value to find
97 * @param fName Name of test
98 * @param test SpringUnit test whose context will be searched
99 * @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, Object test) throws Exception {
105 for (Class<? super S> c : this.testClasses) {
106 T obj = (T)getObject(key, fName, c, test);
107 if (obj != null) {
108 return obj;
109 }
110 }
111 return null;
112 }
113
114 /**
115 * Using reflection to obtain the SpringUnitContext associated
116 * with class <code>c</code>, return the result of calling
117 * <code>getObject</code> on that context for
118 * identifier <code>key</code> and test method
119 * <code>fName</code>.
120 * @param key Identifier of data value
121 * @param fName Name of test in whose scope to begin search
122 * @param c Class whose SpringUnitContext to be searched
123 * @param test SpringUnit test whose context will be searched
124 * @return Object of type T if found, or null
125 * @throws Exception if errors occur when using reflection
126 * to access the SpringUnitContext for class <code>c</code>
127 */
128 private <T extends Object> T getObject(String key, String fName, Class<? super S> c, Object test) throws Exception {
129 assert c != null : "c != null";
130 String methodName = "get" + c.getSimpleName();
131 Method method = c.getMethod(methodName, new Class[0]);
132 SpringUnitContext<T> dataContext = (SpringUnitContext<T>)method.invoke(test, new Object[0]);
133 return dataContext.getObject(key, fName);
134 }
135
136 /**
137 * Generate an array of resource names, given a list of <code>classes</code>.
138 * For each class <code>c</code> in <code>classes</code>,
139 * form a resource name by converting its fully qualified name
140 * to a path and appending the file extension ".xml".
141 * For backward compatibility, resource names derived from
142 * the simple class name are generated when a resource
143 * identified by the fully qualified name cannot be found.
144 * By convention, each class descending from a SpringUnit test has
145 * an associated file named <code><i>Classname</i>.xml</code>.
146 * @param classes List of classes that are ancestors of the
147 * test class and proper descendents of a SpringUnit test
148 * @return Array of string resource names
149 */
150 private String[] createDataFileNames(List<Class<? super S>> classes) {
151 assert classes != null : "classes != null";
152 String[] result = new String[classes.size()];
153 int i = 0;
154 for (Class<? super S> c : classes) {
155 result[i++] = createDataFileName(c);
156 }
157 return result;
158 }
159
160 /**
161 * Derive the resource name of the XML bean configuration file
162 * from the name of the class <code>c</code>.<br/>
163 * @param c Class whose bean configuration resource name
164 * will be derived
165 * @return Name of resource containing the corresponding
166 * XML bean configuration file
167 */
168 private String createDataFileName(Class c) {
169 assert c != null : "c != null";
170 StringBuilder result = new StringBuilder();
171 result.append("classpath:");
172 String name = c.getName();
173 int index = 0;
174 int dot = name.indexOf('.', index);
175 while (dot > -1) {
176 result.append(name.substring(index, dot));
177 result.append('/');
178 index = dot + 1;
179 dot = name.indexOf('.', index);
180 }
181 result.append(name.substring(index));
182 result.append(".xml");
183 return simpleOrFullyQualifiedName(result.toString());
184 }
185
186 /**
187 * Return <code>resourceName</code> if its corresponding resource exists.
188 * Otherwise, derive a name for the resource if it were held in the
189 * root directory of the classpath and return that name.
190 * <br/>
191 * This provides backward-compatibility with earlier versions of
192 * the framework, which required that the XML data files be
193 * kept in the root directory.
194 * @param resourceName Name of the resource to locate
195 * @return resourceName if resource exists, otherwise
196 * a derived name of a resource found in the root of the classpath
197 */
198 private String simpleOrFullyQualifiedName(String resourceName) {
199 Resource resource = this.resourceLoader.getResource(resourceName);
200 if (resource.exists()) {
201 return resourceName;
202 }
203 // Continue for backward compatibility
204 StringBuilder result = new StringBuilder();
205 result.append("classpath:");
206 String name = resourceName.substring("classpath:".length());
207 int slash = name.lastIndexOf('/');
208 if (slash < 0) {
209 result.append(name);
210 }
211 else {
212 result.append(name.substring(slash + 1));
213 }
214 return result.toString();
215 }
216
217 /**
218 * Build the list of ancestors of class <code>c</code>
219 * that are proper descendents of the immediate
220 * descendent of AbstractDependencyInjectionSpringContextTests.<br/>
221 * @param c Class to be added to list of <code>classes</code>
222 * unless <code>c.getSuperClass()</code> is
223 * <code>org.springframework.test.AbstractDependencyInjectionSpringContextTests</code> or
224 * <code>org.springframework.test.AbstractTransactionalSpringContextTests</code>,
225 * in which case the recursion stops.
226 * @param classes List that accumulates the ordered list of
227 * ancestors of <code>c</code>
228 */
229 private void buildTestClasses(List<Class<? super S>> classes, Class<? super S> c) {
230 assert c != null : "c != null";
231 assert classes != null : "classes != null";
232 Class<? super S> superclass = c.getSuperclass();
233 if (!AbstractDependencyInjectionSpringContextTests.class.equals(superclass) &&
234 !AbstractTransactionalSpringContextTests.class.equals(superclass) &&
235 !SpringUnit4Test.class.equals(c) &&
236 !SpringUnit4TransactionalTest.class.equals(c)) {
237 classes.add(c);
238 buildTestClasses(classes, superclass);
239 }
240 }
241
242 /**
243 * List of classes that comprise the class hierarchy
244 * of the test. The class of the test is first
245 * in the list.<br/>
246 */
247 private List<Class<? super S>> testClasses;
248
249 /**
250 * List of names of resources that hold test data.
251 * This caches the list of locations so it need only
252 * be generated once.
253 */
254 private String[] configLocations;
255
256 /**
257 * ResourceLoader is used to check for existence of data
258 * configuration files that correspond to the classes of the
259 * test hierarchy. This check provides backward compatibility
260 * for data files stored in the root directory of the classpath.
261 */
262 private ResourceLoader resourceLoader;
263
264 }