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