001: /*
002: * ========================================================================
003: *
004: * Copyright 2001-2004 The Apache Software Foundation.
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: *
018: * ========================================================================
019: */
020: package org.apache.cactus.extension.jetty;
021:
022: import java.io.File;
023: import java.io.IOException;
024: import java.io.InputStream;
025: import java.net.HttpURLConnection;
026: import java.net.URL;
027:
028: import junit.extensions.TestSetup;
029: import junit.framework.Protectable;
030: import junit.framework.Test;
031: import junit.framework.TestResult;
032:
033: import org.apache.cactus.internal.configuration.BaseConfiguration;
034: import org.apache.cactus.internal.configuration.Configuration;
035: import org.apache.cactus.internal.configuration.DefaultFilterConfiguration;
036: import org.apache.cactus.internal.configuration.DefaultServletConfiguration;
037: import org.apache.cactus.internal.configuration.FilterConfiguration;
038: import org.apache.cactus.internal.configuration.ServletConfiguration;
039: import org.apache.cactus.internal.util.ClassLoaderUtils;
040: import org.apache.cactus.server.FilterTestRedirector;
041: import org.apache.cactus.server.ServletTestRedirector;
042:
043: /**
044: * Custom JUnit test setup to use to automatically start Jetty. Example:<br/>
045: * <code><pre>
046: * public static Test suite()
047: * {
048: * TestSuite suite = new TestSuite(Myclass.class);
049: * return new JettyTestSetup(suite);
050: * }
051: * </pre></code>
052: *
053: * @version $Id: JettyTestSetup.java 239036 2004-08-17 10:35:57Z vmassol $
054: */
055: public class JettyTestSetup extends TestSetup {
056: /**
057: * Name of optional system property that points to a Jetty XML
058: * configuration file.
059: */
060: private static final String CACTUS_JETTY_CONFIG_PROPERTY = "cactus.jetty.config";
061:
062: /**
063: * Name of optional system property that gives the directory
064: * where JSPs and other resources are located.
065: */
066: private static final String CACTUS_JETTY_RESOURCE_DIR_PROPERTY = "cactus.jetty.resourceDir";
067:
068: /**
069: * The configuration file to be used for initializing Jetty.
070: */
071: private File configFile;
072:
073: /**
074: * The directory containing the resources of the web-application.
075: */
076: private File resourceDir;
077:
078: /**
079: * The Jetty server object representing the running instance. It is
080: * used to stop Jetty in {@link #tearDown()}.
081: */
082: private Object server;
083:
084: /**
085: * Whether the container had already been running before.
086: */
087: private boolean alreadyRunning;
088:
089: /**
090: * Whether the container is running or not.
091: */
092: private boolean isRunning = false;
093:
094: /**
095: * Whether the container should be stopped on tearDown even though
096: * it was not started by us.
097: */
098: private boolean forceShutdown = false;
099:
100: /**
101: * The Servlet configuration object used to configure Jetty.
102: */
103: private ServletConfiguration servletConfiguration;
104:
105: /**
106: * The Filter configuration object used to configure Jetty.
107: */
108: private FilterConfiguration filterConfiguration;
109:
110: /**
111: * The base configuration object used to configure Jetty.
112: */
113: private Configuration baseConfiguration;
114:
115: /**
116: * @param theTest the test we are decorating (usually a test suite)
117: */
118: public JettyTestSetup(Test theTest) {
119: super (theTest);
120: this .baseConfiguration = new BaseConfiguration();
121: this .servletConfiguration = new DefaultServletConfiguration();
122: this .filterConfiguration = new DefaultFilterConfiguration();
123: }
124:
125: /**
126: * @param theTest the test we are decorating (usually a test suite)
127: * @param theBaseConfiguration the base configuration object used to
128: * configure Jetty
129: * @param theServletConfiguration the servlet configuration object used
130: * to configure Jetty
131: * @param theFilterConfiguration the filter configuration object used
132: * to configure Jetty
133: */
134: public JettyTestSetup(Test theTest,
135: Configuration theBaseConfiguration,
136: ServletConfiguration theServletConfiguration,
137: FilterConfiguration theFilterConfiguration) {
138: this (theTest);
139: this .baseConfiguration = theBaseConfiguration;
140: this .servletConfiguration = theServletConfiguration;
141: this .filterConfiguration = theFilterConfiguration;
142: }
143:
144: /**
145: * Make sure that {@link #tearDown} is called if {@link #setUp} fails
146: * to start the container properly. The default
147: * {@link TestSetup#run(TestResult)} method does not provide this feature
148: * unfortunately.
149: *
150: * @see TestSetup#run(TestResult)
151: */
152: public void run(final TestResult theResult) {
153: Protectable p = new Protectable() {
154: public void protect() throws Exception {
155: try {
156: setUp();
157: basicRun(theResult);
158: } finally {
159: tearDown();
160: }
161: }
162: };
163: theResult.runProtected(this , p);
164: }
165:
166: /**
167: * Start an embedded Jetty server. It is allowed to pass a Jetty XML as
168: * a system property (<code>cactus.jetty.config</code>) to further
169: * configure Jetty. Example:
170: * <code>-Dcactus.jetty.config=./jetty.xml</code>.
171: *
172: * @exception Exception if an error happens during initialization
173: */
174: protected void setUp() throws Exception {
175: // Try connecting in case the server is already running. If so, does
176: // nothing
177: URL contextURL = new URL(this .baseConfiguration.getContextURL()
178: + "/"
179: + this .servletConfiguration.getDefaultRedirectorName()
180: + "?Cactus_Service=RUN_TEST");
181: this .alreadyRunning = isAvailable(testConnectivity(contextURL));
182: if (this .alreadyRunning) {
183: // Server is already running. Record this information so that we
184: // don't stop it afterwards.
185: this .isRunning = true;
186: return;
187: }
188:
189: // Note: We are currently using reflection in order not to need Jetty
190: // to compile Cactus. If the code becomes more complex or we need to
191: // add other initializer, it will be worth considering moving them
192: // to a separate "extension" subproject which will need additional jars
193: // in its classpath (using the same mechanism as the Ant project is
194: // using to conditionally compile tasks).
195:
196: // Create a Jetty Server object and configure a listener
197: this .server = createServer(this .baseConfiguration);
198:
199: // Create a Jetty context.
200: Object context = createContext(this .server,
201: this .baseConfiguration);
202:
203: // Add the Cactus Servlet redirector
204: addServletRedirector(context, this .servletConfiguration);
205:
206: // Add the Cactus Jsp redirector
207: addJspRedirector(context);
208:
209: // Add the Cactus Filter redirector
210: addFilterRedirector(context, this .filterConfiguration);
211:
212: // Configure Jetty with an XML file if one has been specified on the
213: // command line.
214: if (getConfigFile() != null) {
215: this .server.getClass().getMethod("configure",
216: new Class[] { String.class }).invoke(this .server,
217: new Object[] { getConfigFile().toString() });
218: }
219:
220: // Start the Jetty server
221: this .server.getClass().getMethod("start", null).invoke(
222: this .server, null);
223:
224: this .isRunning = true;
225: }
226:
227: /**
228: * Stop the running Jetty server.
229: *
230: * @exception Exception if an error happens during the shutdown
231: */
232: protected void tearDown() throws Exception {
233: // Don't shut down a container that has not been started by us
234: if (!this .forceShutdown && this .alreadyRunning) {
235: return;
236: }
237:
238: if (this .server != null) {
239: // First, verify if the server is running
240: boolean started = ((Boolean) this .server.getClass()
241: .getMethod("isStarted", null).invoke(this .server,
242: null)).booleanValue();
243:
244: // Stop and destroy the Jetty server, if started
245: if (started) {
246: // Stop all listener and contexts
247: this .server.getClass().getMethod("stop", null).invoke(
248: this .server, null);
249:
250: // Destroy a stopped server. Remove all components and send
251: // notifications to all event listeners.
252: this .server.getClass().getMethod("destroy", null)
253: .invoke(this .server, null);
254: }
255: }
256:
257: this .isRunning = false;
258: }
259:
260: /**
261: * Sets the configuration file to use for initializing Jetty.
262: *
263: * @param theConfigFile The configuration file to set
264: */
265: public final void setConfigFile(File theConfigFile) {
266: this .configFile = theConfigFile;
267: }
268:
269: /**
270: * Sets the directory in which Jetty will look for the web-application
271: * resources.
272: *
273: * @param theResourceDir The resource directory to set
274: */
275: public final void setResourceDir(File theResourceDir) {
276: this .resourceDir = theResourceDir;
277: }
278:
279: /**
280: * @param isForcedShutdown if true the container will be stopped even
281: * if it has not been started by us
282: */
283: public final void setForceShutdown(boolean isForcedShutdown) {
284: this .forceShutdown = isForcedShutdown;
285: }
286:
287: /**
288: * @return The resource directory, or <code>null</code> if it has not been
289: * set
290: */
291: protected final File getConfigFile() {
292: if (this .configFile == null) {
293: String configFileProperty = System
294: .getProperty(CACTUS_JETTY_CONFIG_PROPERTY);
295: if (configFileProperty != null) {
296: this .configFile = new File(configFileProperty);
297: }
298: }
299: return this .configFile;
300: }
301:
302: /**
303: * @return The resource directory, or <code>null</code> if it has not been
304: * set
305: */
306: protected final File getResourceDir() {
307: if (this .resourceDir == null) {
308: String resourceDirProperty = System
309: .getProperty(CACTUS_JETTY_RESOURCE_DIR_PROPERTY);
310: if (resourceDirProperty != null) {
311: this .resourceDir = new File(resourceDirProperty);
312: }
313: }
314: return this .resourceDir;
315: }
316:
317: /**
318: * Create a Jetty server object and configures a listener on the
319: * port defined in the Cactus context URL property.
320: *
321: * @param theConfiguration the base Cactus configuration
322: * @return the Jetty <code>Server</code> object
323: *
324: * @exception Exception if an error happens during initialization
325: */
326: private Object createServer(Configuration theConfiguration)
327: throws Exception {
328: // Create Jetty Server object
329: Class serverClass = ClassLoaderUtils.loadClass(
330: "org.mortbay.jetty.Server", this .getClass());
331: Object server = serverClass.newInstance();
332:
333: URL contextURL = new URL(theConfiguration.getContextURL());
334:
335: // Add a listener on the port defined in the Cactus configuration
336: server.getClass().getMethod("addListener",
337: new Class[] { String.class }).invoke(server,
338: new Object[] { "" + contextURL.getPort() });
339:
340: return server;
341: }
342:
343: /**
344: * Create a Jetty Context. We use a <code>WebApplicationContext</code>
345: * because we need to use Servlet Filters.
346: *
347: * @param theServer the Jetty Server object
348: * @param theConfiguration the base Cactus configuration
349: * @return Object the <code>WebApplicationContext</code> object
350: *
351: * @exception Exception if an error happens during initialization
352: */
353: private Object createContext(Object theServer,
354: Configuration theConfiguration) throws Exception {
355: // Add a web application. This creates a WebApplicationContext.
356: // Note: We do not put any WEB-INF/, lib/ nor classes/ directory
357: // in the webapp.
358: URL contextURL = new URL(theConfiguration.getContextURL());
359:
360: if (getResourceDir() != null) {
361: theServer.getClass().getMethod("addWebApplication",
362: new Class[] { String.class, String.class }).invoke(
363: theServer,
364: new Object[] { contextURL.getPath(),
365: getResourceDir().toString() });
366: }
367:
368: // Retrieves the WebApplication context created by the
369: // "addWebApplication". We need it to be able to manually configure
370: // other items in the context.
371: Object context = theServer.getClass().getMethod("getContext",
372: new Class[] { String.class }).invoke(theServer,
373: new Object[] { contextURL.getPath() });
374:
375: return context;
376: }
377:
378: /**
379: * Adds the Cactus Servlet redirector configuration
380: *
381: * @param theContext the Jetty context under which to add the configuration
382: * @param theConfiguration the Cactus Servlet configuration
383: *
384: * @exception Exception if an error happens during initialization
385: */
386: private void addServletRedirector(Object theContext,
387: ServletConfiguration theConfiguration) throws Exception {
388: theContext
389: .getClass()
390: .getMethod(
391: "addServlet",
392: new Class[] { String.class, String.class,
393: String.class })
394: .invoke(
395: theContext,
396: new Object[] {
397: theConfiguration
398: .getDefaultRedirectorName(),
399: "/"
400: + theConfiguration
401: .getDefaultRedirectorName(),
402: ServletTestRedirector.class.getName() });
403: }
404:
405: /**
406: * Adds the Cactus Jsp redirector configuration. We only add it if the
407: * CACTUS_JETTY_RESOURCE_DIR_PROPERTY has been provided by the user. This
408: * is because JSPs need to be attached to a WebApplicationHandler in Jetty.
409: *
410: * @param theContext the Jetty context under which to add the configuration
411: *
412: * @exception Exception if an error happens during initialization
413: */
414: private void addJspRedirector(Object theContext) throws Exception {
415: if (getResourceDir() != null) {
416: theContext.getClass().getMethod("addServlet",
417: new Class[] { String.class, String.class }).invoke(
418: theContext,
419: new Object[] { "*.jsp",
420: "org.apache.jasper.servlet.JspServlet" });
421:
422: // Get the WebApplicationHandler object in order to be able to
423: // call the addServlet() method that accpets a forced path.
424: Object handler = theContext.getClass().getMethod(
425: "getWebApplicationHandler", new Class[] {}).invoke(
426: theContext, new Object[] {});
427:
428: handler.getClass().getMethod(
429: "addServlet",
430: new Class[] { String.class, String.class,
431: String.class, String.class }).invoke(
432: handler,
433: new Object[] { "JspRedirector", "/JspRedirector",
434: "org.apache.jasper.servlet.JspServlet",
435: "/jspRedirector.jsp" });
436: }
437: }
438:
439: /**
440: * Adds the Cactus Filter redirector configuration. We only add it if the
441: * CACTUS_JETTY_RESOURCE_DIR_PROPERTY has been provided by the user. This
442: * is because Filters need to be attached to a WebApplicationHandler in
443: * Jetty.
444: *
445: * @param theContext the Jetty context under which to add the configuration
446: * @param theConfiguration the Cactus Filter configuration
447: *
448: * @exception Exception if an error happens during initialization
449: */
450: private void addFilterRedirector(Object theContext,
451: FilterConfiguration theConfiguration) throws Exception {
452: if (getResourceDir() != null) {
453: // Get the WebApplicationHandler object in order to be able to add
454: // the Cactus Filter redirector
455: Object handler = theContext.getClass().getMethod(
456: "getWebApplicationHandler", new Class[] {}).invoke(
457: theContext, new Object[] {});
458:
459: Object filterHolder = handler
460: .getClass()
461: .getMethod("defineFilter",
462: new Class[] { String.class, String.class })
463: .invoke(
464: handler,
465: new Object[] {
466: theConfiguration
467: .getDefaultRedirectorName(),
468: FilterTestRedirector.class
469: .getName() });
470:
471: filterHolder.getClass().getMethod("addAppliesTo",
472: new Class[] { String.class }).invoke(filterHolder,
473: new Object[] { "REQUEST" });
474:
475: // Map the Cactus Filter redirector to a path
476: handler
477: .getClass()
478: .getMethod("mapPathToFilter",
479: new Class[] { String.class, String.class })
480: .invoke(
481: handler,
482: new Object[] {
483: "/"
484: + theConfiguration
485: .getDefaultRedirectorName(),
486: theConfiguration
487: .getDefaultRedirectorName() });
488: }
489: }
490:
491: /**
492: * Tests whether we are able to connect to the HTTP server identified by the
493: * specified URL.
494: *
495: * @param theUrl The URL to check
496: * @return the HTTP response code or -1 if no connection could be
497: * established
498: */
499: protected int testConnectivity(URL theUrl) {
500: int code;
501: try {
502: HttpURLConnection connection = (HttpURLConnection) theUrl
503: .openConnection();
504: connection.setRequestProperty("Connection", "close");
505: connection.connect();
506: readFully(connection);
507: connection.disconnect();
508: code = connection.getResponseCode();
509: } catch (IOException e) {
510: code = -1;
511: }
512: return code;
513: }
514:
515: /**
516: * Tests whether an HTTP return code corresponds to a valid connection
517: * to the test URL or not. Success is 200 up to but excluding 300.
518: *
519: * @param theCode the HTTP response code to verify
520: * @return <code>true</code> if the test URL could be called without error,
521: * <code>false</code> otherwise
522: */
523: protected boolean isAvailable(int theCode) {
524: boolean result;
525: if ((theCode != -1) && (theCode < 300)) {
526: result = true;
527: } else {
528: result = false;
529: }
530: return result;
531: }
532:
533: /**
534: * Fully reads the input stream from the passed HTTP URL connection to
535: * prevent (harmless) server-side exception.
536: *
537: * @param theConnection the HTTP URL connection to read from
538: * @exception IOException if an error happens during the read
539: */
540: protected void readFully(HttpURLConnection theConnection)
541: throws IOException {
542: // Only read if there is data to read ... The problem is that not
543: // all servers return a content-length header. If there is no header
544: // getContentLength() returns -1. It seems to work and it seems
545: // that all servers that return no content-length header also do
546: // not block on read() operations!
547: if (theConnection.getContentLength() != 0) {
548: byte[] buf = new byte[256];
549: InputStream in = theConnection.getInputStream();
550: while (in.read(buf) != -1) {
551: // Make sure we read all the data in the stream
552: }
553: }
554: }
555:
556: /**
557: * @return true if the server is running or false otherwise
558: */
559: protected boolean isRunning() {
560: return this.isRunning;
561: }
562: }
|