1 /*
2 * ========================================================================
3 *
4 * Licensed to the Apache Software Foundation (ASF) under one or more
5 * contributor license agreements. See the NOTICE file distributed with
6 * this work for additional information regarding copyright ownership.
7 * The ASF licenses this file to You under the Apache License, Version 2.0
8 * (the "License"); you may not use this file except in compliance with
9 * the License. You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 * ========================================================================
20 */
21 package org.apache.cactus.internal.server;
22
23 import java.io.IOException;
24 import java.io.Writer;
25
26 import java.lang.reflect.Constructor;
27
28 import javax.servlet.ServletException;
29
30 import junit.framework.Test;
31 import junit.framework.TestCase;
32
33 import org.apache.cactus.internal.CactusTestCase;
34 import org.apache.cactus.internal.HttpServiceDefinition;
35 import org.apache.cactus.internal.ServiceEnumeration;
36 import org.apache.cactus.internal.WebTestResult;
37 import org.apache.cactus.internal.configuration.Version;
38 import org.apache.cactus.internal.util.ClassLoaderUtils;
39 import org.apache.commons.logging.Log;
40 import org.apache.commons.logging.LogFactory;
41
42 /**
43 * Responsible for instanciating the <code>TestCase</code> class on the server
44 * side, set up the implicit objects and call the test method. This class
45 * provides a common abstraction for all test web requests.
46 *
47 * @version $Id: AbstractWebTestCaller.java 238991 2004-05-22 11:34:50Z vmassol $
48 */
49 public abstract class AbstractWebTestCaller
50 {
51 /**
52 * Name of the attribute in the <code>application</code> scope that will
53 * hold the results of the test.
54 */
55 protected static final String TEST_RESULTS =
56 "ServletTestRedirector_TestResults";
57
58 /**
59 * The logger.
60 */
61 private static final Log LOGGER =
62 LogFactory.getLog(AbstractWebTestCaller.class);
63
64 /**
65 * The implicit objects (which will be used to set the test case fields
66 * in the <code>setTesCaseFields</code> method.
67 */
68 protected WebImplicitObjects webImplicitObjects;
69
70 /**
71 * @param theObjects the implicit objects coming from the redirector
72 */
73 public AbstractWebTestCaller(WebImplicitObjects theObjects)
74 {
75 this.webImplicitObjects = theObjects;
76 }
77
78 /**
79 * Sets the implicit object in the test case class.
80 *
81 * @param theTestCase the instance of the test case class on which the
82 * class variable (implicit objects) should be set
83 * @exception Exception if an errors occurs when setting the implicit
84 * objects
85 */
86 protected abstract void setTestCaseFields(TestCase theTestCase)
87 throws Exception;
88
89 /**
90 * @return a <code>Writer</code> object that will be used to return the
91 * test result to the client side.
92 * @exception IOException if an error occurs when retrieving the writer
93 */
94 protected abstract Writer getResponseWriter() throws IOException;
95
96 /**
97 * Calls a test method. The parameters needed to call this method are found
98 * in the HTTP request. Save the results in the <code>application</code>
99 * scope so that the Get Test Result service can find them.
100 *
101 * @exception ServletException if an unexpected error occurred
102 */
103 public void doTest() throws ServletException
104 {
105 WebTestResult result = null;
106
107 try
108 {
109 // Create an instance of the test class
110 TestCase testInstance = getTestClassInstance(
111 getTestClassName(), getWrappedTestClassName(),
112 getTestMethodName());
113
114 // Set its fields (implicit objects)
115 setTestCaseFields(testInstance);
116
117 // Call it's method corresponding to the current test case
118 if (testInstance instanceof CactusTestCase)
119 {
120 ((CactusTestCase) testInstance).runBareServer();
121
122 }
123 else
124 {
125 testInstance.runBare();
126 }
127
128 // Return an instance of <code>WebTestResult</code> with a
129 // positive result.
130 result = new WebTestResult();
131 }
132 catch (Throwable e)
133 {
134 // An error occurred, return an instance of
135 // <code>WebTestResult</code> with an exception.
136 result = new WebTestResult(e);
137 }
138
139 LOGGER.debug("Test result : [" + result + "]");
140
141
142 // Set the test result.
143 this.webImplicitObjects.getServletContext()
144 .setAttribute(TEST_RESULTS, result);
145
146 LOGGER.debug("Result saved in context scope");
147 }
148
149 /**
150 * Return the last test results in the HTTP response.
151 *
152 * @exception ServletException if an unexpected error occurred
153 */
154 public void doGetResults() throws ServletException
155 {
156 // One could think there is a potential risk that the client side of
157 // Cactus will request the result before it has been written to the
158 // context scope as the HTTP request will not block in some containers.
159 // However this will not happen because on the client side, once the
160 // first request is done to execute the test, all the result is read
161 // by the AutoReadHttpURLConnection class, thus ensuring that the
162 // request is fully finished and the result has been committed ...
163 WebTestResult result = (WebTestResult) (this.webImplicitObjects
164 .getServletContext().getAttribute(TEST_RESULTS));
165
166 // It can happen that the result has not been written in the Servlet
167 // context. This could happen for example when using a load-balancer
168 // which would direct the second Cactus HTTP connection to another
169 // instance. In that case, we throw an error.
170 if (result == null)
171 {
172 String message = "Error getting test result. This could happen "
173 + "for example if you're using a load-balancer. Please disable "
174 + "it before running Cactus tests.";
175
176 LOGGER.error(message);
177 throw new ServletException(message);
178 }
179
180 LOGGER.debug("Test Result = [" + result + "]");
181
182 // Write back the results to the outgoing stream as an XML string.
183
184 // Use UTF-8 to transfer the result back
185 webImplicitObjects.getHttpServletResponse().setContentType(
186 "text/xml; charset=UTF-8");
187
188 try
189 {
190 Writer writer = getResponseWriter();
191
192 writer.write(result.toXml());
193 writer.close();
194 }
195 catch (IOException e)
196 {
197 String message = "Error writing WebTestResult instance to output "
198 + "stream";
199
200 LOGGER.error(message, e);
201 throw new ServletException(message, e);
202 }
203 }
204
205 /**
206 * Run the connection test between client and server. This is just to
207 * ensure that configuration is set up correctly.
208 *
209 * @exception ServletException if an unexpected error occurred
210 */
211 public void doRunTest() throws ServletException
212 {
213 // Do not return any http response (not needed). It is enough to
214 // know this point has been reached ... it means the connection has
215 // been established !
216 }
217
218 /**
219 * Return the cactus version. This is to make sure both the client side
220 * and server side are using the same version.
221 *
222 * @exception ServletException if an unexpected error occurred
223 */
224 public void doGetVersion() throws ServletException
225 {
226 try
227 {
228 Writer writer = getResponseWriter();
229 writer.write(Version.VERSION);
230 writer.close();
231 }
232 catch (IOException e)
233 {
234 String message = "Error writing HTTP response back to client "
235 + "for service [" + ServiceEnumeration.GET_VERSION_SERVICE
236 + "]";
237
238 LOGGER.error(message, e);
239 throw new ServletException(message, e);
240 }
241 }
242
243 /**
244 * Create an HTTP Session and returns the response that contains the
245 * HTTP session as a cookie (unless URL rewriting is used in which
246 * case the jsesssionid cookie is not returned).
247 *
248 * @exception ServletException if an unexpected error occurred
249 */
250 public void doCreateSession() throws ServletException
251 {
252 // Create an HTTP session
253 this.webImplicitObjects.getHttpServletRequest().getSession(true);
254
255 try
256 {
257 Writer writer = getResponseWriter();
258 writer.close();
259 }
260 catch (IOException e)
261 {
262 String message = "Error writing HTTP response back to client "
263 + "for service [" + ServiceEnumeration.CREATE_SESSION_SERVICE
264 + "]";
265
266 LOGGER.error(message, e);
267 throw new ServletException(message, e);
268 }
269 }
270
271 /**
272 * @return the class to test class name, extracted from the HTTP request
273 * @exception ServletException if the class name of the test case is missing
274 * from the HTTP request
275 */
276 protected String getTestClassName() throws ServletException
277 {
278 String queryString = this.webImplicitObjects.getHttpServletRequest()
279 .getQueryString();
280 String className = ServletUtil.getQueryStringParameter(queryString,
281 HttpServiceDefinition.CLASS_NAME_PARAM);
282
283 if (className == null)
284 {
285 String message = "Missing class name parameter ["
286 + HttpServiceDefinition.CLASS_NAME_PARAM
287 + "] in HTTP request.";
288
289 LOGGER.error(message);
290 throw new ServletException(message);
291 }
292
293 LOGGER.debug("Class to call = [" + className + "]");
294
295 return className;
296 }
297
298 /**
299 * @return the optional test class that is wrapped by a Cactus test case,
300 * extracted from the HTTP request
301 * @exception ServletException if the wrapped class name is missing from
302 * the HTTP request
303 */
304 protected String getWrappedTestClassName() throws ServletException
305 {
306 String queryString = this.webImplicitObjects.getHttpServletRequest()
307 .getQueryString();
308 String className = ServletUtil.getQueryStringParameter(queryString,
309 HttpServiceDefinition.WRAPPED_CLASS_NAME_PARAM);
310
311 if (className == null)
312 {
313 LOGGER.debug("No wrapped test class");
314 }
315 else
316 {
317 LOGGER.debug("Wrapped test class = [" + className + "]");
318 }
319
320 return className;
321 }
322
323 /**
324 * @return the class method to call for the current test case, extracted
325 * from the HTTP request
326 * @exception ServletException if the method name of the test case is
327 * missing from the HTTP request
328 */
329 protected String getTestMethodName() throws ServletException
330 {
331 String queryString = this.webImplicitObjects.getHttpServletRequest()
332 .getQueryString();
333 String methodName = ServletUtil.getQueryStringParameter(queryString,
334 HttpServiceDefinition.METHOD_NAME_PARAM);
335
336 if (methodName == null)
337 {
338 String message = "Missing method name parameter ["
339 + HttpServiceDefinition.METHOD_NAME_PARAM
340 + "] in HTTP request.";
341
342 LOGGER.error(message);
343 throw new ServletException(message);
344 }
345
346 LOGGER.debug("Method to call = " + methodName);
347
348 return methodName;
349 }
350
351 /**
352 * @return true if the auto session flag for the Session can be found in
353 * the HTTP request
354 */
355 protected boolean isAutoSession()
356 {
357 String queryString = this.webImplicitObjects.getHttpServletRequest()
358 .getQueryString();
359 String autoSession = ServletUtil.getQueryStringParameter(queryString,
360 HttpServiceDefinition.AUTOSESSION_NAME_PARAM);
361
362 boolean isAutomaticSession =
363 Boolean.valueOf(autoSession).booleanValue();
364
365 LOGGER.debug("Auto session is " + isAutomaticSession);
366
367 return isAutomaticSession;
368 }
369
370 /**
371 * @param theClassName the name of the test class
372 * @param theWrappedClassName the name of the wrapped test class. Can be
373 * null if there is none
374 * @param theTestCaseName the name of the current test case
375 * @return an instance of the test class to call
376 * @exception ServletException if the test case instance for the current
377 * test fails to be instanciated (for example if some
378 * information is missing from the HTTP request)
379 */
380 protected TestCase getTestClassInstance(
381 String theClassName, String theWrappedClassName,
382 String theTestCaseName) throws ServletException
383 {
384 // Get the class to call and build an instance of it.
385 Class testClass = getTestClassClass(theClassName);
386 TestCase testInstance = null;
387 Constructor constructor;
388
389 try
390 {
391 if (theWrappedClassName == null)
392 {
393 constructor = getTestClassConstructor(testClass);
394
395 if (constructor.getParameterTypes().length == 0)
396 {
397 testInstance = (TestCase) constructor.newInstance(
398 new Object[0]);
399 ((TestCase) testInstance).setName(theTestCaseName);
400 }
401 else
402 {
403 testInstance = (TestCase) constructor.newInstance(
404 new Object[] {theTestCaseName});
405 }
406 }
407 else
408 {
409 Class wrappedTestClass =
410 getTestClassClass(theWrappedClassName);
411 Constructor wrappedConstructor =
412 getTestClassConstructor(wrappedTestClass);
413
414 TestCase wrappedTestInstance;
415 if (wrappedConstructor.getParameterTypes().length == 0)
416 {
417 wrappedTestInstance =
418 (TestCase) wrappedConstructor.newInstance(
419 new Object[0]);
420 wrappedTestInstance.setName(theTestCaseName);
421 }
422 else
423 {
424 wrappedTestInstance =
425 (TestCase) wrappedConstructor.newInstance(
426 new Object[] {theTestCaseName});
427 }
428
429 constructor = testClass.getConstructor(
430 new Class[] {String.class, Test.class});
431
432 testInstance =
433 (TestCase) constructor.newInstance(
434 new Object[] {theTestCaseName, wrappedTestInstance});
435 }
436 }
437 catch (Exception e)
438 {
439 String message = "Error instantiating class [" + theClassName + "(["
440 + theTestCaseName + "], [" + theWrappedClassName + "])]";
441
442 LOGGER.error(message, e);
443 throw new ServletException(message, e);
444 }
445
446 return testInstance;
447 }
448
449 /**
450 * @param theTestClass the test class for which we want to find the
451 * constructor
452 * @return the availble constructor for the test class
453 * @throws NoSuchMethodException if no suitable constructor is found
454 */
455 private Constructor getTestClassConstructor(Class theTestClass)
456 throws NoSuchMethodException
457 {
458 Constructor constructor;
459 try
460 {
461 constructor = theTestClass.getConstructor(
462 new Class[] {String.class});
463 }
464 catch (NoSuchMethodException e)
465 {
466 constructor = theTestClass.getConstructor(new Class[0]);
467 }
468 return constructor;
469 }
470
471 /**
472 * @param theClassName the name of the test class
473 * @return the class object the test class to call
474 * @exception ServletException if the class of the current test case
475 * cannot be loaded in memory (i.e. it is not in the
476 * classpath)
477 */
478 protected Class getTestClassClass(String theClassName)
479 throws ServletException
480 {
481 // Get the class to call and build an instance of it.
482 Class testClass = null;
483
484 try
485 {
486 testClass = ClassLoaderUtils.loadClass(theClassName,
487 this.getClass());
488 }
489 catch (Exception e)
490 {
491 String message = "Error finding class [" + theClassName
492 + "] using both the Context classloader and the webapp "
493 + "classloader. Possible causes include:\r\n";
494
495 message += ("\t- Your webapp does not include your test "
496 + "classes,\r\n");
497 message += ("\t- The cactus.jar is not located in your "
498 + "WEB-INF/lib directory and your Container has not set the "
499 + "Context classloader to point to the webapp one");
500
501 LOGGER.error(message, e);
502 throw new ServletException(message, e);
503 }
504
505 return testClass;
506 }
507
508 }