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.container;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.net.HttpURLConnection;
26  import java.net.URL;
27  
28  import org.apache.cactus.integration.api.exceptions.CactusRuntimeException;
29  import org.codehaus.cargo.util.log.Logger;
30  
31  /**
32   * Support class that handles the lifecycle of a container, which basically
33   * consists of startup and shutdown.
34   * 
35   * @version $Id: ContainerRunner.java 239130 2005-01-29 15:49:18Z vmassol $
36   */
37  public final class ContainerRunner
38  {
39      // Instance Variables ------------------------------------------------------
40  
41      /**
42       * The container to run.
43       */
44      //private org.codehaus.cargo.container.Container container;
45      
46      private ContainerWrapper containerWrapper = null;
47  
48      /**
49       * The URL that is continuously pinged to check if the container is running.
50       */
51      private URL testURL;
52  
53      /**
54       * Timeout in milliseconds after which to give up connecting to the
55       * container.
56       */
57      private long timeout = 180000;
58  
59      /**
60       * The time interval in milliseconds to sleep between polling the container.
61       */
62      private long checkInterval = 500;
63  
64      /**
65       * The time to sleep after the container has shut down. 
66       */
67      private long shutDownWait = 2000;
68  
69      /**
70       * Whether the container had already been running before.
71       */
72      private boolean alreadyRunning;
73  
74      /**
75       * The server name as returned in the 'Server' header of the server's
76       * HTTP response.
77       */
78      private String serverName;
79  
80      /**
81       * The logger to use.
82       */
83      private transient Logger logger;
84  
85      // Constructors ------------------------------------------------------------
86  
87      /**
88       * Constructor.
89       * 
90       * @param theContainerWrapper The container to run
91       */
92      public ContainerRunner(ContainerWrapper theContainerWrapper)
93      {
94          //this.container = theContainer;
95          this.containerWrapper = theContainerWrapper;
96      }
97  
98      // Public Methods ----------------------------------------------------------
99  
100     /**
101      * Returns the server name as reported in the 'Server' header of HTTP 
102      * responses from the server.
103      * 
104      * @return The server name
105      */
106     public String getServerName()
107     {
108         return this.serverName;
109     }
110 
111     /**
112      * Method called by the task to perform the startup of the container. This
113      * method takes care of starting the container in another thread, and
114      * polling the test URL to check whether startup has completed. As soon as
115      * the URL is available (or the timeout is exceeded), control is returned to
116      * the caller.
117      * 
118      * @throws IllegalStateException If the 'url' property is <code>null</code>
119      */
120     public void startUpContainer() throws IllegalStateException
121     {
122         if (this.testURL == null)
123         {
124             throw new IllegalStateException("Property [url] must be set");
125         }
126 
127         // Try connecting in case the server is already running. If so, does
128         // nothing
129         this.alreadyRunning = isAvailable(testConnectivity(this.testURL));
130         if (this.alreadyRunning)
131         {
132             // Server is already running. Record this information so that we
133             // don't stop it afterwards.
134             this.logger.debug("Server is already running",
135                 getClass().toString());
136             return;
137         }
138 
139         // Now start the server in another thread
140         Thread thread = new Thread(new Runnable()
141         {
142             public void run()
143             {
144                 containerWrapper.startUp();
145             }
146         });
147         thread.start();
148 
149         // Continuously try calling the test URL until it succeeds or
150         // until a timeout is reached (we then throw a build exception).
151         long startTime = System.currentTimeMillis();
152         int responseCode = -1;
153         do
154         {
155             if ((System.currentTimeMillis() - startTime) > this.timeout)
156             {
157                 throw new CactusRuntimeException("Failed to start the container after "
158                     + "more than [" + this.timeout + "] ms. Trying to connect "
159                     + "to the [" + this.testURL + "] test URL yielded a ["
160                     + responseCode + "] error code. Please run in debug mode "
161                     + "for more details about the error.");
162             }
163             sleep(this.checkInterval);
164             this.logger.debug("Checking if server is up ...",
165                 getClass().toString());
166             responseCode = testConnectivity(this.testURL);
167         } while (!isAvailable(responseCode));
168 
169         // Wait a few ms more (just to be sure !)
170         sleep(this.containerWrapper.getStartUpWait());
171 
172         this.serverName = retrieveServerName(this.testURL);
173         this.logger.info("Server [" + this.serverName + "] started",
174             getClass().toString());
175     }
176 
177     /**
178      * Method called by the task to perform the stopping of the container. This
179      * method takes care of stopping the container in another thread, and
180      * polling the test URL to check whether shutdown has completed. As soon as
181      * the URL stops responding, control is returned to the caller.
182      * 
183      * @throws IllegalStateException If the 'url' property is <code>null</code>
184      */
185     public void shutDownContainer() throws IllegalStateException
186     {
187         if (this.testURL == null)
188         {
189             throw new IllegalStateException("Property [url] must be set");
190         }
191 
192         // Don't shut down a container that has not been started by us
193         if (this.alreadyRunning)
194         {
195             return;
196         }
197         
198         if (!isAvailable(testConnectivity(this.testURL)))
199         {
200             this.logger.debug("Server isn't running!", getClass().toString());
201             return;
202         }
203 
204         // Call the target that stops the server, in another thread. The called
205         // target must be blocking.
206         Thread thread = new Thread(new Runnable()
207         {
208             public void run()
209             {
210                 containerWrapper.shutDown();
211             }
212         });
213         thread.start();
214 
215         // Continuously try calling the test URL until it fails
216         do 
217         {
218             sleep(this.checkInterval);
219         } while (isAvailable(testConnectivity(this.testURL)));
220 
221         // sleep a bit longer to be sure the container has terminated
222         sleep(this.shutDownWait);
223         
224         this.logger.debug("Server stopped!", getClass().toString());
225     }
226 
227     /**
228      * Sets the time interval to sleep between polling the container. 
229      * 
230      * The default interval is 500 milliseconds.
231      * 
232      * @param theCheckInterval The interval in milliseconds
233      */
234     public void setCheckInterval(long theCheckInterval)
235     {
236         this.checkInterval = theCheckInterval;
237     }
238 
239     /**
240      * Sets the log to write to.
241      *  
242      * @param theLogger The log to set
243      */
244     public void setLogger(Logger theLogger)
245     {
246         this.logger = theLogger;
247     }
248 
249     /**
250      * Sets the time to wait after the container has been shut down.
251      * 
252      * The default time is 2 seconds.
253      * 
254      * @param theShutDownWait The time to wait in milliseconds
255      */
256     public void setShutDownWait(long theShutDownWait)
257     {
258         this.shutDownWait = theShutDownWait;
259     }
260 
261     /**
262      * Sets the timeout after which to stop trying to call the container.
263      * 
264      * The default timeout is 3 minutes.
265      * 
266      * @param theTimeout The timeout in milliseconds
267      */
268     public void setTimeout(long theTimeout)
269     {
270         this.timeout = theTimeout;
271     }
272 
273     /**
274      * Sets the HTTP/HTTPS URL that will be continuously pinged to check if the
275      * container is running.
276      * 
277      * @param theTestURL The URL to set
278      */
279     public void setURL(URL theTestURL)
280     {
281         if (!(theTestURL.getProtocol().equalsIgnoreCase("http") 
282             || theTestURL.getProtocol().equalsIgnoreCase("https")))
283         {
284             throw new IllegalArgumentException("Not a HTTP or HTTPS URL");
285         } 
286         this.testURL = theTestURL;
287     }
288 
289     // Private Methods ---------------------------------------------------------
290 
291     /**
292      * Tests whether we are able to connect to the HTTP server identified by the
293      * specified URL.
294      * 
295      * @param theUrl The URL to check
296      * @return the HTTP response code or -1 if no connection could be 
297      *         established
298      */
299     public int testConnectivity(URL theUrl)
300     {
301         int code;
302         try
303         {
304             HttpURLConnection connection = 
305                 (HttpURLConnection) theUrl.openConnection();
306             connection.setRequestProperty("Connection", "close");
307             connection.connect();
308             readFully(connection);
309             connection.disconnect();
310             code = connection.getResponseCode();
311         }
312         catch (IOException e)
313         {
314             this.logger.debug("Failed to connect to [" + theUrl + "]",
315                  e.getMessage());
316             code = -1;
317         }
318         return code;
319     }
320 
321 
322     /**
323      * Tests whether an HTTP return code corresponds to a valid connection
324      * to the test URL or not. Success is 200 up to but excluding 300.
325      * 
326      * @param theCode the HTTP response code to verify
327      * @return <code>true</code> if the test URL could be called without error,
328      *         <code>false</code> otherwise
329      */
330     private boolean isAvailable(int theCode)
331     {
332         boolean result;
333         if ((theCode != -1) && (theCode < 300)) 
334         {
335             result = true;            
336         }
337         else
338         {
339             result = false;
340         }
341         return result;
342     }
343 
344     /**
345      * Retrieves the server name of the container.
346      * 
347      * @param theUrl The URL to retrieve
348      * @return The server name, or <code>null</code> if the server name could 
349      *         not be retrieved
350      */
351     private String retrieveServerName(URL theUrl)
352     {
353         String retVal = null;
354         try
355         {
356             HttpURLConnection connection = 
357                 (HttpURLConnection) theUrl.openConnection();
358             connection.connect();
359             retVal = connection.getHeaderField("Server");
360             connection.disconnect();
361         }
362         catch (IOException e)
363         {
364             this.logger.debug("Could not get server name from [" 
365                 + theUrl + "]", e.getMessage());
366         }
367         return retVal;
368     }
369 
370     /**
371      * Fully reads the input stream from the passed HTTP URL connection to
372      * prevent (harmless) server-side exception.
373      *
374      * @param theConnection the HTTP URL connection to read from
375      * @exception IOException if an error happens during the read
376      */
377     static void readFully(HttpURLConnection theConnection)
378                    throws IOException
379     {
380         // Only read if there is data to read ... The problem is that not
381         // all servers return a content-length header. If there is no header
382         // getContentLength() returns -1. It seems to work and it seems
383         // that all servers that return no content-length header also do
384         // not block on read() operations!
385         if (theConnection.getContentLength() != 0)
386         {
387             byte[] buf = new byte[256];
388             InputStream in = theConnection.getInputStream();
389             while (in.read(buf) != -1)
390             {
391                 // Make sure we read all the data in the stream
392             }
393         }
394     }
395 
396     /**
397      * Pauses the current thread for the specified amount.
398      *
399      * @param theMs The time to sleep in milliseconds
400      * @throws CactusRuntimeException If the sleeping thread is interrupted
401      */
402     private void sleep(long theMs) throws CactusRuntimeException
403     {
404         try
405         {
406             Thread.sleep(theMs);
407         }
408         catch (InterruptedException e)
409         {
410             throw new CactusRuntimeException("Interruption during sleep", e);
411         }
412     }
413 }