View Javadoc

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 }