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.extension.jetty;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.net.HttpURLConnection;
27 import java.net.URL;
28
29 import junit.extensions.TestSetup;
30 import junit.framework.Protectable;
31 import junit.framework.Test;
32 import junit.framework.TestResult;
33
34 import org.apache.cactus.internal.configuration.BaseConfiguration;
35 import org.apache.cactus.internal.configuration.Configuration;
36 import org.apache.cactus.internal.configuration.DefaultFilterConfiguration;
37 import org.apache.cactus.internal.configuration.DefaultServletConfiguration;
38 import org.apache.cactus.internal.configuration.FilterConfiguration;
39 import org.apache.cactus.internal.configuration.ServletConfiguration;
40 import org.apache.cactus.internal.util.ClassLoaderUtils;
41 import org.apache.cactus.server.FilterTestRedirector;
42 import org.apache.cactus.server.ServletTestRedirector;
43
44 /**
45 * Custom JUnit test setup to use to automatically start Jetty. Example:<br/>
46 * <code><pre>
47 * public static Test suite()
48 * {
49 * TestSuite suite = new TestSuite(Myclass.class);
50 * return new JettyTestSetup(suite);
51 * }
52 * </pre></code>
53 *
54 * @version $Id: JettyTestSetup.java 239036 2004-08-17 10:35:57Z vmassol $
55 */
56 public class Jetty5xTestSetup extends TestSetup
57 {
58 /**
59 * Name of optional system property that points to a Jetty XML
60 * configuration file.
61 */
62 private static final String CACTUS_JETTY_CONFIG_PROPERTY =
63 "cactus.jetty.config";
64
65 /**
66 * Name of optional system property that gives the directory
67 * where JSPs and other resources are located.
68 */
69 private static final String CACTUS_JETTY_RESOURCE_DIR_PROPERTY =
70 "cactus.jetty.resourceDir";
71
72 /**
73 * The configuration file to be used for initializing Jetty.
74 */
75 private File configFile;
76
77 /**
78 * The directory containing the resources of the web-application.
79 */
80 private File resourceDir;
81
82 /**
83 * The Jetty server object representing the running instance. It is
84 * used to stop Jetty in {@link #tearDown()}.
85 */
86 private Object server;
87
88 /**
89 * Whether the container had already been running before.
90 */
91 private boolean alreadyRunning;
92
93 /**
94 * Whether the container is running or not.
95 */
96 private boolean isRunning = false;
97
98 /**
99 * Whether the container should be stopped on tearDown even though
100 * it was not started by us.
101 */
102 private boolean forceShutdown = false;
103
104 /**
105 * The Servlet configuration object used to configure Jetty.
106 */
107 private ServletConfiguration servletConfiguration;
108
109 /**
110 * The Filter configuration object used to configure Jetty.
111 */
112 private FilterConfiguration filterConfiguration;
113
114 /**
115 * The base configuration object used to configure Jetty.
116 */
117 private Configuration baseConfiguration;
118
119 /**
120 * @param theTest the test we are decorating (usually a test suite)
121 */
122 public Jetty5xTestSetup(Test theTest)
123 {
124 super(theTest);
125 this.baseConfiguration = new BaseConfiguration();
126 this.servletConfiguration = new DefaultServletConfiguration();
127 this.filterConfiguration = new DefaultFilterConfiguration();
128 }
129
130 /**
131 * @param theTest the test we are decorating (usually a test suite)
132 * @param theBaseConfiguration the base configuration object used to
133 * configure Jetty
134 * @param theServletConfiguration the servlet configuration object used
135 * to configure Jetty
136 * @param theFilterConfiguration the filter configuration object used
137 * to configure Jetty
138 */
139 public Jetty5xTestSetup(Test theTest,
140 Configuration theBaseConfiguration,
141 ServletConfiguration theServletConfiguration,
142 FilterConfiguration theFilterConfiguration)
143 {
144 this(theTest);
145 this.baseConfiguration = theBaseConfiguration;
146 this.servletConfiguration = theServletConfiguration;
147 this.filterConfiguration = theFilterConfiguration;
148 }
149
150 /**
151 * Make sure that {@link #tearDown} is called if {@link #setUp} fails
152 * to start the container properly. The default
153 * {@link TestSetup#run(TestResult)} method does not provide this feature
154 * unfortunately.
155 *
156 * {@inheritDoc}
157 * @see TestSetup#run(TestResult)
158 */
159 public void run(final TestResult theResult)
160 {
161 Protectable p = new Protectable()
162 {
163 public void protect() throws Exception
164 {
165 try
166 {
167 setUp();
168 basicRun(theResult);
169 }
170 finally
171 {
172 tearDown();
173 }
174 }
175 };
176 theResult.runProtected(this, p);
177 }
178
179 /**
180 * Start an embedded Jetty server. It is allowed to pass a Jetty XML as
181 * a system property (<code>cactus.jetty.config</code>) to further
182 * configure Jetty. Example:
183 * <code>-Dcactus.jetty.config=./jetty.xml</code>.
184 *
185 * @exception Exception if an error happens during initialization
186 */
187 protected void setUp() throws Exception
188 {
189 // Try connecting in case the server is already running. If so, does
190 // nothing
191 URL contextURL = new URL(this.baseConfiguration.getContextURL()
192 + "/" + this.servletConfiguration.getDefaultRedirectorName()
193 + "?Cactus_Service=RUN_TEST");
194 this.alreadyRunning = isAvailable(testConnectivity(contextURL));
195 if (this.alreadyRunning)
196 {
197 // Server is already running. Record this information so that we
198 // don't stop it afterwards.
199 this.isRunning = true;
200 return;
201 }
202
203 // Note: We are currently using reflection in order not to need Jetty
204 // to compile Cactus. If the code becomes more complex or we need to
205 // add other initializer, it will be worth considering moving them
206 // to a separate "extension" subproject which will need additional jars
207 // in its classpath (using the same mechanism as the Ant project is
208 // using to conditionally compile tasks).
209
210 // Create a Jetty Server object and configure a listener
211 this.server = createServer(this.baseConfiguration);
212
213 // Create a Jetty context.
214 Object context = createContext(this.server, this.baseConfiguration);
215
216 // Add the Cactus Servlet redirector
217 addServletRedirector(context, this.servletConfiguration);
218
219 // Add the Cactus Jsp redirector
220 addJspRedirector(context);
221
222 // Add the Cactus Filter redirector
223 addFilterRedirector(context, this.filterConfiguration);
224
225 // Configure Jetty with an XML file if one has been specified on the
226 // command line.
227 if (getConfigFile() != null)
228 {
229 this.server.getClass().getMethod("configure",
230 new Class[] {String.class}).invoke(
231 this.server, new Object[] {getConfigFile().toString()});
232 }
233
234 // Start the Jetty server
235 this.server.getClass().getMethod("start", null).invoke(
236 this.server, null);
237
238 this.isRunning = true;
239 }
240
241 /**
242 * Stop the running Jetty server.
243 *
244 * @exception Exception if an error happens during the shutdown
245 */
246 protected void tearDown() throws Exception
247 {
248 // Don't shut down a container that has not been started by us
249 if (!this.forceShutdown && this.alreadyRunning)
250 {
251 return;
252 }
253
254 if (this.server != null)
255 {
256 // First, verify if the server is running
257 boolean started = ((Boolean) this.server.getClass().getMethod(
258 "isStarted", null).invoke(this.server, null)).booleanValue();
259
260 // Stop and destroy the Jetty server, if started
261 if (started)
262 {
263 // Stop all listener and contexts
264 this.server.getClass().getMethod("stop", null).invoke(
265 this.server, null);
266
267 // Destroy a stopped server. Remove all components and send
268 // notifications to all event listeners.
269 this.server.getClass().getMethod("destroy", null).invoke(
270 this.server, null);
271 }
272 }
273
274 this.isRunning = false;
275 }
276
277 /**
278 * Sets the configuration file to use for initializing Jetty.
279 *
280 * @param theConfigFile The configuration file to set
281 */
282 public final void setConfigFile(File theConfigFile)
283 {
284 this.configFile = theConfigFile;
285 }
286
287 /**
288 * Sets the directory in which Jetty will look for the web-application
289 * resources.
290 *
291 * @param theResourceDir The resource directory to set
292 */
293 public final void setResourceDir(File theResourceDir)
294 {
295 this.resourceDir = theResourceDir;
296 }
297
298 /**
299 * @param isForcedShutdown if true the container will be stopped even
300 * if it has not been started by us
301 */
302 public final void setForceShutdown(boolean isForcedShutdown)
303 {
304 this.forceShutdown = isForcedShutdown;
305 }
306
307 /**
308 * @return The resource directory, or <code>null</code> if it has not been
309 * set
310 */
311 protected final File getConfigFile()
312 {
313 if (this.configFile == null)
314 {
315 String configFileProperty = System.getProperty(
316 CACTUS_JETTY_CONFIG_PROPERTY);
317 if (configFileProperty != null)
318 {
319 this.configFile = new File(configFileProperty);
320 }
321 }
322 return this.configFile;
323 }
324
325 /**
326 * @return The resource directory, or <code>null</code> if it has not been
327 * set
328 */
329 protected final File getResourceDir()
330 {
331 if (this.resourceDir == null)
332 {
333 String resourceDirProperty = System.getProperty(
334 CACTUS_JETTY_RESOURCE_DIR_PROPERTY);
335 if (resourceDirProperty != null)
336 {
337 this.resourceDir = new File(resourceDirProperty);
338 }
339 }
340 return this.resourceDir;
341 }
342
343 /**
344 * Create a Jetty server object and configures a listener on the
345 * port defined in the Cactus context URL property.
346 *
347 * @param theConfiguration the base Cactus configuration
348 * @return the Jetty <code>Server</code> object
349 *
350 * @exception Exception if an error happens during initialization
351 */
352 private Object createServer(Configuration theConfiguration)
353 throws Exception
354 {
355 // Create Jetty Server object
356 Class serverClass = ClassLoaderUtils.loadClass(
357 "org.mortbay.jetty.Server", this.getClass());
358 Object server = serverClass.newInstance();
359
360 URL contextURL = new URL(theConfiguration.getContextURL());
361
362 // Add a listener on the port defined in the Cactus configuration
363 server.getClass().getMethod("addListener",
364 new Class[] {String.class})
365 .invoke(server, new Object[] {"" + contextURL.getPort()});
366
367 return server;
368 }
369
370 /**
371 * Create a Jetty Context. We use a <code>WebApplicationContext</code>
372 * because we need to use Servlet Filters.
373 *
374 * @param theServer the Jetty Server object
375 * @param theConfiguration the base Cactus configuration
376 * @return Object the <code>WebApplicationContext</code> object
377 *
378 * @exception Exception if an error happens during initialization
379 */
380 private Object createContext(Object theServer,
381 Configuration theConfiguration) throws Exception
382 {
383 // Add a web application. This creates a WebApplicationContext.
384 // Note: We do not put any WEB-INF/, lib/ nor classes/ directory
385 // in the webapp.
386 URL contextURL = new URL(theConfiguration.getContextURL());
387
388 if (getResourceDir() != null)
389 {
390 theServer.getClass().getMethod("addWebApplication",
391 new Class[] {String.class, String.class})
392 .invoke(theServer, new Object[] {contextURL.getPath(),
393 getResourceDir().toString()});
394 }
395
396 // Retrieves the WebApplication context created by the
397 // "addWebApplication". We need it to be able to manually configure
398 // other items in the context.
399 Object context = theServer.getClass().getMethod(
400 "getContext", new Class[] {String.class})
401 .invoke(theServer, new Object[] {contextURL.getPath()});
402
403 return context;
404 }
405
406 /**
407 * Adds the Cactus Servlet redirector configuration.
408 *
409 * @param theContext the Jetty context under which to add the configuration
410 * @param theConfiguration the Cactus Servlet configuration
411 *
412 * @exception Exception if an error happens during initialization
413 */
414 private void addServletRedirector(Object theContext,
415 ServletConfiguration theConfiguration) throws Exception
416 {
417 theContext.getClass().getMethod("addServlet",
418 new Class[] {String.class, String.class, String.class})
419 .invoke(theContext,
420 new Object[] {theConfiguration.getDefaultRedirectorName(),
421 "/" + theConfiguration.getDefaultRedirectorName(),
422 ServletTestRedirector.class.getName()});
423 }
424
425 /**
426 * Adds the Cactus Jsp redirector configuration. We only add it if the
427 * CACTUS_JETTY_RESOURCE_DIR_PROPERTY has been provided by the user. This
428 * is because JSPs need to be attached to a WebApplicationHandler in Jetty.
429 *
430 * @param theContext the Jetty context under which to add the configuration
431 *
432 * @exception Exception if an error happens during initialization
433 */
434 private void addJspRedirector(Object theContext) throws Exception
435 {
436 if (getResourceDir() != null)
437 {
438 theContext.getClass().getMethod("addServlet",
439 new Class[] {String.class, String.class})
440 .invoke(theContext,
441 new Object[] {"*.jsp",
442 "org.apache.jasper.servlet.JspServlet"});
443
444 // Get the WebApplicationHandler object in order to be able to
445 // call the addServlet() method that accpets a forced path.
446 Object handler = theContext.getClass().getMethod(
447 "getWebApplicationHandler",
448 new Class[] {}).invoke(theContext, new Object[] {});
449
450 handler.getClass().getMethod("addServlet",
451 new Class[] {String.class, String.class, String.class,
452 String.class})
453 .invoke(handler,
454 new Object[] {
455 "JspRedirector",
456 "/JspRedirector",
457 "org.apache.jasper.servlet.JspServlet",
458 "/jspRedirector.jsp"});
459 }
460 }
461
462 /**
463 * Adds the Cactus Filter redirector configuration. We only add it if the
464 * CACTUS_JETTY_RESOURCE_DIR_PROPERTY has been provided by the user. This
465 * is because Filters need to be attached to a WebApplicationHandler in
466 * Jetty.
467 *
468 * @param theContext the Jetty context under which to add the configuration
469 * @param theConfiguration the Cactus Filter configuration
470 *
471 * @exception Exception if an error happens during initialization
472 */
473 private void addFilterRedirector(Object theContext,
474 FilterConfiguration theConfiguration) throws Exception
475 {
476 if (getResourceDir() != null)
477 {
478 // Get the WebApplicationHandler object in order to be able to add
479 // the Cactus Filter redirector
480 Object handler = theContext.getClass().getMethod(
481 "getWebApplicationHandler",
482 new Class[] {}).invoke(theContext, new Object[] {});
483
484 Object filterHolder = handler.getClass().getMethod("defineFilter",
485 new Class[] {String.class, String.class})
486 .invoke(handler,
487 new Object[] {theConfiguration.getDefaultRedirectorName(),
488 FilterTestRedirector.class.getName()});
489
490 // filterHolder.getClass().getMethod("addAppliesTo",
491 // new Class[] {String.class})
492 // .invoke(filterHolder, new Object[] {"REQUEST"});
493
494
495 // Map the Cactus Filter redirector to a path
496 handler.getClass().getMethod("addFilterPathMapping",
497 new Class[] {String.class, String.class, Integer.TYPE})
498 .invoke(handler,
499 new Object[] {"/"
500 + theConfiguration.getDefaultRedirectorName(),
501 theConfiguration.getDefaultRedirectorName(), new Integer(0)});
502 }
503 }
504
505 /**
506 * Tests whether we are able to connect to the HTTP server identified by the
507 * specified URL.
508 *
509 * @param theUrl The URL to check
510 * @return the HTTP response code or -1 if no connection could be
511 * established
512 */
513 protected int testConnectivity(URL theUrl)
514 {
515 int code;
516 try
517 {
518 HttpURLConnection connection =
519 (HttpURLConnection) theUrl.openConnection();
520 connection.setRequestProperty("Connection", "close");
521 connection.connect();
522 readFully(connection);
523 connection.disconnect();
524 code = connection.getResponseCode();
525 }
526 catch (IOException e)
527 {
528 code = -1;
529 }
530 return code;
531 }
532
533 /**
534 * Tests whether an HTTP return code corresponds to a valid connection
535 * to the test URL or not. Success is 200 up to but excluding 300.
536 *
537 * @param theCode the HTTP response code to verify
538 * @return <code>true</code> if the test URL could be called without error,
539 * <code>false</code> otherwise
540 */
541 protected boolean isAvailable(int theCode)
542 {
543 boolean result;
544 if ((theCode != -1) && (theCode < 300))
545 {
546 result = true;
547 }
548 else
549 {
550 result = false;
551 }
552 return result;
553 }
554
555 /**
556 * Fully reads the input stream from the passed HTTP URL connection to
557 * prevent (harmless) server-side exception.
558 *
559 * @param theConnection the HTTP URL connection to read from
560 * @exception IOException if an error happens during the read
561 */
562 protected void readFully(HttpURLConnection theConnection)
563 throws IOException
564 {
565 // Only read if there is data to read ... The problem is that not
566 // all servers return a content-length header. If there is no header
567 // getContentLength() returns -1. It seems to work and it seems
568 // that all servers that return no content-length header also do
569 // not block on read() operations!
570 if (theConnection.getContentLength() != 0)
571 {
572 byte[] buf = new byte[256];
573 InputStream in = theConnection.getInputStream();
574 while (in.read(buf) != -1)
575 {
576 // Make sure we read all the data in the stream
577 }
578 }
579 }
580
581 /**
582 * @return true if the server is running or false otherwise
583 */
584 protected boolean isRunning()
585 {
586 return this.isRunning;
587 }
588 }