2011/08/05 - Jakarta Cactus has been retired.

For more information, please explore the Attic.

View Javadoc

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 }