001: /*
002: * ========================================================================
003: *
004: * Copyright 2003-2005 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.integration.ant.container;
021:
022: import java.io.IOException;
023: import java.io.InputStream;
024: import java.net.HttpURLConnection;
025: import java.net.URL;
026:
027: import org.apache.cactus.integration.ant.util.AntLog;
028: import org.apache.commons.logging.Log;
029: import org.apache.tools.ant.BuildException;
030:
031: /**
032: * Support class that handles the lifecycle of a container, which basically
033: * consists of startup and shutdown.
034: *
035: * @version $Id: ContainerRunner.java 239130 2005-01-29 15:49:18Z vmassol $
036: */
037: public final class ContainerRunner {
038: // Instance Variables ------------------------------------------------------
039:
040: /**
041: * The container to run.
042: */
043: private Container container;
044:
045: /**
046: * The URL that is continuously pinged to check if the container is running.
047: */
048: private URL testURL;
049:
050: /**
051: * Timeout in milliseconds after which to give up connecting to the
052: * container.
053: */
054: private long timeout = 180000;
055:
056: /**
057: * The time interval in milliseconds to sleep between polling the container.
058: */
059: private long checkInterval = 500;
060:
061: /**
062: * The time to sleep after the container has shut down.
063: */
064: private long shutDownWait = 2000;
065:
066: /**
067: * Whether the container had already been running before.
068: */
069: private boolean alreadyRunning;
070:
071: /**
072: * The server name as returned in the 'Server' header of the server's
073: * HTTP response.
074: */
075: private String serverName;
076:
077: /**
078: * The log to use.
079: */
080: private transient Log log = AntLog.NULL;
081:
082: // Constructors ------------------------------------------------------------
083:
084: /**
085: * Constructor.
086: *
087: * @param theContainer The container to run
088: */
089: public ContainerRunner(Container theContainer) {
090: this .container = theContainer;
091: }
092:
093: // Public Methods ----------------------------------------------------------
094:
095: /**
096: * Returns the server name as reported in the 'Server' header of HTTP
097: * responses from the server.
098: *
099: * @return The server name
100: */
101: public String getServerName() {
102: return this .serverName;
103: }
104:
105: /**
106: * Method called by the task to perform the startup of the container. This
107: * method takes care of starting the container in another thread, and
108: * polling the test URL to check whether startup has completed. As soon as
109: * the URL is available (or the timeout is exceeded), control is returned to
110: * the caller.
111: *
112: * @throws IllegalStateException If the 'url' property is <code>null</code>
113: */
114: public void startUpContainer() throws IllegalStateException {
115: if (this .testURL == null) {
116: throw new IllegalStateException(
117: "Property [url] must be set");
118: }
119:
120: // Try connecting in case the server is already running. If so, does
121: // nothing
122: this .alreadyRunning = isAvailable(testConnectivity(this .testURL));
123: if (this .alreadyRunning) {
124: // Server is already running. Record this information so that we
125: // don't stop it afterwards.
126: this .log.debug("Server is already running");
127: return;
128: }
129:
130: // Now start the server in another thread
131: Thread thread = new Thread(new Runnable() {
132: public void run() {
133: container.startUp();
134: }
135: });
136: thread.start();
137:
138: // Continuously try calling the test URL until it succeeds or
139: // until a timeout is reached (we then throw a build exception).
140: long startTime = System.currentTimeMillis();
141: int responseCode = -1;
142: do {
143: if ((System.currentTimeMillis() - startTime) > this .timeout) {
144: throw new BuildException(
145: "Failed to start the container after "
146: + "more than ["
147: + this .timeout
148: + "] ms. Trying to connect "
149: + "to the ["
150: + this .testURL
151: + "] test URL yielded a ["
152: + responseCode
153: + "] error code. Please run in debug mode "
154: + "for more details about the error.");
155: }
156: sleep(this .checkInterval);
157: this .log.debug("Checking if server is up ...");
158: responseCode = testConnectivity(this .testURL);
159: } while (!isAvailable(responseCode));
160:
161: // Wait a few ms more (just to be sure !)
162: sleep(this .container.getStartUpWait());
163:
164: this .serverName = retrieveServerName(this .testURL);
165: this .log.trace("Server [" + this .serverName + "] started");
166: }
167:
168: /**
169: * Method called by the task to perform the stopping of the container. This
170: * method takes care of stopping the container in another thread, and
171: * polling the test URL to check whether shutdown has completed. As soon as
172: * the URL stops responding, control is returned to the caller.
173: *
174: * @throws IllegalStateException If the 'url' property is <code>null</code>
175: */
176: public void shutDownContainer() throws IllegalStateException {
177: if (this .testURL == null) {
178: throw new IllegalStateException(
179: "Property [url] must be set");
180: }
181:
182: // Don't shut down a container that has not been started by us
183: if (this .alreadyRunning) {
184: return;
185: }
186:
187: if (!isAvailable(testConnectivity(this .testURL))) {
188: this .log.debug("Server isn't running!");
189: return;
190: }
191:
192: // Call the target that stops the server, in another thread. The called
193: // target must be blocking.
194: Thread thread = new Thread(new Runnable() {
195: public void run() {
196: container.shutDown();
197: }
198: });
199: thread.start();
200:
201: // Continuously try calling the test URL until it fails
202: do {
203: sleep(this .checkInterval);
204: } while (isAvailable(testConnectivity(this .testURL)));
205:
206: // sleep a bit longer to be sure the container has terminated
207: sleep(this .shutDownWait);
208:
209: this .log.debug("Server stopped!");
210: }
211:
212: /**
213: * Sets the time interval to sleep between polling the container.
214: *
215: * The default interval is 500 milliseconds.
216: *
217: * @param theCheckInterval The interval in milliseconds
218: */
219: public void setCheckInterval(long theCheckInterval) {
220: this .checkInterval = theCheckInterval;
221: }
222:
223: /**
224: * Sets the log to write to.
225: *
226: * @param theLog The log to set
227: */
228: public void setLog(Log theLog) {
229: this .log = theLog;
230: }
231:
232: /**
233: * Sets the time to wait after the container has been shut down.
234: *
235: * The default time is 2 seconds.
236: *
237: * @param theShutDownWait The time to wait in milliseconds
238: */
239: public void setShutDownWait(long theShutDownWait) {
240: this .shutDownWait = theShutDownWait;
241: }
242:
243: /**
244: * Sets the timeout after which to stop trying to call the container.
245: *
246: * The default timeout is 3 minutes.
247: *
248: * @param theTimeout The timeout in milliseconds
249: */
250: public void setTimeout(long theTimeout) {
251: this .timeout = theTimeout;
252: }
253:
254: /**
255: * Sets the HTTP/HTTPS URL that will be continuously pinged to check if the
256: * container is running.
257: *
258: * @param theTestURL The URL to set
259: */
260: public void setURL(URL theTestURL) {
261: if (!(theTestURL.getProtocol().equalsIgnoreCase("http") || theTestURL
262: .getProtocol().equalsIgnoreCase("https"))) {
263: throw new IllegalArgumentException(
264: "Not a HTTP or HTTPS URL");
265: }
266: this .testURL = theTestURL;
267: }
268:
269: // Private Methods ---------------------------------------------------------
270:
271: /**
272: * Tests whether we are able to connect to the HTTP server identified by the
273: * specified URL.
274: *
275: * @param theUrl The URL to check
276: * @return the HTTP response code or -1 if no connection could be
277: * established
278: */
279: private int testConnectivity(URL theUrl) {
280: int code;
281: try {
282: HttpURLConnection connection = (HttpURLConnection) theUrl
283: .openConnection();
284: connection.setRequestProperty("Connection", "close");
285: connection.connect();
286: readFully(connection);
287: connection.disconnect();
288: code = connection.getResponseCode();
289: } catch (IOException e) {
290: this .log.debug("Failed to connect to [" + theUrl + "]", e);
291: code = -1;
292: }
293: return code;
294: }
295:
296: /**
297: * Tests whether an HTTP return code corresponds to a valid connection
298: * to the test URL or not. Success is 200 up to but excluding 300.
299: *
300: * @param theCode the HTTP response code to verify
301: * @return <code>true</code> if the test URL could be called without error,
302: * <code>false</code> otherwise
303: */
304: private boolean isAvailable(int theCode) {
305: boolean result;
306: if ((theCode != -1) && (theCode < 300)) {
307: result = true;
308: } else {
309: result = false;
310: }
311: return result;
312: }
313:
314: /**
315: * Retrieves the server name of the container.
316: *
317: * @param theUrl The URL to retrieve
318: * @return The server name, or <code>null</code> if the server name could
319: * not be retrieved
320: */
321: private String retrieveServerName(URL theUrl) {
322: String retVal = null;
323: try {
324: HttpURLConnection connection = (HttpURLConnection) theUrl
325: .openConnection();
326: connection.connect();
327: retVal = connection.getHeaderField("Server");
328: connection.disconnect();
329: } catch (IOException e) {
330: this .log.debug("Could not get server name from [" + theUrl
331: + "]", e);
332: }
333: return retVal;
334: }
335:
336: /**
337: * Fully reads the input stream from the passed HTTP URL connection to
338: * prevent (harmless) server-side exception.
339: *
340: * @param theConnection the HTTP URL connection to read from
341: * @exception IOException if an error happens during the read
342: */
343: static void readFully(HttpURLConnection theConnection)
344: throws IOException {
345: // Only read if there is data to read ... The problem is that not
346: // all servers return a content-length header. If there is no header
347: // getContentLength() returns -1. It seems to work and it seems
348: // that all servers that return no content-length header also do
349: // not block on read() operations!
350: if (theConnection.getContentLength() != 0) {
351: byte[] buf = new byte[256];
352: InputStream in = theConnection.getInputStream();
353: while (in.read(buf) != -1) {
354: // Make sure we read all the data in the stream
355: }
356: }
357: }
358:
359: /**
360: * Pauses the current thread for the specified amount.
361: *
362: * @param theMs The time to sleep in milliseconds
363: * @throws BuildException If the sleeping thread is interrupted
364: */
365: private void sleep(long theMs) throws BuildException {
366: try {
367: Thread.sleep(theMs);
368: } catch (InterruptedException e) {
369: throw new BuildException("Interruption during sleep", e);
370: }
371: }
372: }
|