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.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 }