001: /*
002: * $Id: HTTPQueryHandler.java,v 1.4 2007/09/18 08:45:08 agoubard Exp $
003: *
004: * Copyright 2003-2007 Orange Nederland Breedband B.V.
005: * See the COPYRIGHT file for redistribution and use restrictions.
006: */
007: package org.xins.common.servlet.container;
008:
009: import java.io.BufferedReader;
010: import java.io.ByteArrayOutputStream;
011: import java.io.IOException;
012: import java.io.InputStream;
013: import java.io.InputStreamReader;
014: import java.io.OutputStream;
015: import java.net.FileNameMap;
016: import java.net.Socket;
017: import java.net.URLConnection;
018: import java.util.HashMap;
019: import java.util.Iterator;
020: import java.util.Map;
021:
022: import org.apache.commons.httpclient.HttpStatus;
023:
024: import org.xins.common.MandatoryArgumentChecker;
025: import org.xins.common.Utils;
026: import org.xins.common.collections.PropertyReader;
027: import org.xins.common.text.ParseException;
028:
029: /**
030: * HTTP query received to be handled by the servlet.
031: *
032: * @version $Revision: 1.4 $ $Date: 2007/09/18 08:45:08 $
033: * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
034: * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
035: */
036: class HTTPQueryHandler extends Thread {
037:
038: /**
039: * The encoding of the request.
040: */
041: private static final String REQUEST_ENCODING = "ISO-8859-1";
042:
043: /**
044: * The map containing the MIME type information. Never <code>null</code>
045: */
046: private static final FileNameMap MIME_TYPES_MAP = URLConnection
047: .getFileNameMap();
048:
049: /**
050: * The line separator used by the HTTP protocol.
051: */
052: private static final String CRLF = "\r\n";
053:
054: /**
055: * The instance number for this created query.
056: */
057: private static int _instanceNumber = 0;
058:
059: /**
060: * The socket of the HTTP query.
061: */
062: private final Socket _client;
063:
064: /**
065: * Mapping between the path and the servlet.
066: */
067: private final Map _servlets;
068:
069: /**
070: * Creates a new HTTPQueryHandler to handle the HTTP query sent by the client.
071: *
072: * @param client
073: * the connection with the client, cannot be <code>null</code>.
074: *
075: * @param servlets
076: * the mapping between the path and the servlets, cannot be <code>null</code>.
077: *
078: * @throws IllegalArgumentException
079: * if <code>client == null || servlets == null</code>.
080: */
081: public HTTPQueryHandler(Socket client, Map servlets)
082: throws IllegalArgumentException {
083:
084: // Check argument
085: MandatoryArgumentChecker.check("client", client, "servlets",
086: servlets);
087:
088: _client = client;
089: _servlets = servlets;
090: synchronized (servlets) {
091: _instanceNumber++;
092: }
093: setName("XINS Query handler #" + _instanceNumber);
094: }
095:
096: public void run() {
097: try {
098: serviceClient(_client);
099: } catch (Exception ex) {
100:
101: // If anything goes wrong still continue accepting clients
102: Utils.logIgnoredException(ex);
103: } finally {
104: try {
105: _client.close();
106: } catch (Throwable exception) {
107: // ignore
108: }
109: }
110: }
111:
112: /**
113: * This method is invoked when a client connects to the server.
114: *
115: * @param client
116: * the connection with the client, cannot be <code>null</code>.
117: *
118: * @throws IllegalArgumentException
119: * if <code>client == null</code>.
120: *
121: * @throws IOException
122: * if the query is not handled correctly.
123: */
124: public void serviceClient(Socket client)
125: throws IllegalArgumentException, IOException {
126:
127: // Check argument
128: MandatoryArgumentChecker.check("client", client);
129:
130: InputStream inbound = client.getInputStream();
131: OutputStream outbound = client.getOutputStream();
132:
133: // Delegate to httpQuery in a way it does not have to bother with
134: // closing the streams
135: try {
136: httpQuery(inbound, outbound);
137:
138: // Clean up for httpQuery, if necessary
139: } finally {
140: if (inbound != null) {
141: try {
142: inbound.close();
143: } catch (Throwable exception) {
144: Utils.logIgnoredException(exception);
145: }
146: }
147: if (outbound != null) {
148: try {
149: outbound.close();
150: } catch (Throwable exception) {
151: Utils.logIgnoredException(exception);
152: }
153: }
154: }
155: }
156:
157: /**
158: * This method parses the data sent from the client to get the input
159: * parameters and format the result as a compatible HTTP result.
160: * This method will used the servlet associated with the passed virtual
161: * path. If no servlet is associated with the virtual path, the servlet with
162: * the virtual path "/" is used as default. If there is no servlet then with
163: * the virtual path "/" is found then HTTP 404 is returned.
164: *
165: * @param in
166: * the input byte stream that contains the request sent by the client.
167: *
168: * @param out
169: * the output byte stream that must be fed the response towards the
170: * client.
171: *
172: * @throws IOException
173: * if the query is not handled correctly.
174: *
175: * @since XINS 1.5.0.
176: */
177: public void httpQuery(InputStream in, OutputStream out)
178: throws IOException {
179:
180: // Read the input
181: // XXX: Buffer size determines maximum request size
182: char[] buffer = new char[16384];
183: BufferedReader inReader = new BufferedReader(
184: new InputStreamReader(in, REQUEST_ENCODING));
185: int lengthRead = inReader.read(buffer);
186: if (lengthRead < 0) {
187: sendBadRequest(out);
188: return;
189: }
190: String request = new String(buffer, 0, lengthRead);
191: //byte[] requestBytes = IOReader.readFullyAsBytes(in);
192: //String request = new String(requestBytes, 0, requestBytes.length, REQUEST_ENCODING);
193:
194: // Read the first line
195: int eolIndex = request.indexOf(CRLF);
196: if (eolIndex < 0) {
197: sendBadRequest(out);
198: return;
199: }
200:
201: // The first line must end with "HTTP/1.0" or "HTTP/1.1"
202: String line = request.substring(0, eolIndex);
203: request = request.substring(eolIndex + 2);
204: if (!(line.endsWith(" HTTP/1.1") || line.endsWith(" HTTP/1.0"))) {
205: sendBadRequest(out);
206: return;
207: }
208:
209: // Cut off the last part
210: line = line.substring(0, line.length() - 9);
211:
212: // Find the space
213: int spaceIndex = line.indexOf(' ');
214: if (spaceIndex < 1) {
215: sendBadRequest(out);
216: return;
217: }
218:
219: // Determine the method
220: String method = line.substring(0, spaceIndex);
221:
222: // Determine the query string
223: String url = line.substring(spaceIndex + 1);
224: if ("".equals(url)) {
225: sendBadRequest(out);
226: return;
227: } else if ("GET".equals(method) || "HEAD".equals(method)
228: || "OPTIONS".equals(method)) {
229: url = url.replace(',', '&');
230: }
231:
232: // Normalize the query string
233: if ("GET".equals(method) && url.endsWith("/")
234: && getClass().getResource(url + "index.html") != null) {
235: url += "index.html";
236: }
237:
238: // Read the headers
239: HashMap inHeaders = new HashMap();
240: boolean done = false;
241: while (!done) {
242: int nextEOL = request.indexOf(CRLF);
243: if (nextEOL <= 0) {
244: done = true;
245: } else {
246: try {
247: parseHeader(inHeaders, request
248: .substring(0, nextEOL));
249: } catch (ParseException exception) {
250: sendBadRequest(out);
251: return;
252: }
253: request = request.substring(nextEOL + 2);
254: }
255: }
256:
257: // Determine the body contents
258: String body = "".equals(request) ? "" : request.substring(2);
259:
260: // Response encoding defaults to request encoding
261: String responseEncoding = REQUEST_ENCODING;
262:
263: // Handle the case that a web page is requested
264: boolean getMethod = method.equals("GET")
265: || method.equals("HEAD");
266: String httpResult;
267: if (getMethod && url.indexOf('?') == -1 && !url.endsWith("/")
268: && !"*".equals(url)) {
269: httpResult = readWebPage(url);
270:
271: // No web page requested
272: } else {
273:
274: // Determine the content type
275: String inContentType = getHeader(inHeaders, "Content-Type");
276:
277: // If www-form encoded, then append the body to the query string
278: if ((inContentType == null || inContentType
279: .startsWith("application/x-www-form-urlencoded"))
280: && body.length() > 0) {
281: // XXX: What if the URL already contains a question mark?
282: url += '?' + body;
283: body = null;
284: }
285:
286: // Locate the path of the URL
287: String virtualPath = url;
288: if (virtualPath.indexOf('?') != -1) {
289: virtualPath = virtualPath
290: .substring(0, url.indexOf('?'));
291: }
292: if (virtualPath.endsWith("/") && virtualPath.length() > 1) {
293: virtualPath = virtualPath.substring(0, virtualPath
294: .length() - 1);
295: }
296:
297: // Get the Servlet according to the path
298: LocalServletHandler servlet = findServlet(virtualPath);
299:
300: // If no servlet is found return 404
301: if (servlet == null) {
302: sendError(out, "404 Not Found");
303: return;
304: } else {
305:
306: // Query the Servlet
307: XINSServletResponse response = servlet.query(method,
308: url, body, inHeaders);
309:
310: // Create the HTTP answer
311: StringBuffer sbHttpResult = new StringBuffer();
312: sbHttpResult.append("HTTP/1.1 "
313: + response.getStatus()
314: + " "
315: + HttpStatus
316: .getStatusText(response.getStatus())
317: + CRLF);
318: PropertyReader outHeaders = response.getHeaders();
319: Iterator itHeaderNames = outHeaders.getNames();
320: while (itHeaderNames.hasNext()) {
321: String nextHeader = (String) itHeaderNames.next();
322: String headerValue = outHeaders.get(nextHeader);
323: if (headerValue != null) {
324: sbHttpResult.append(nextHeader + ": "
325: + headerValue + "\r\n");
326: }
327: }
328:
329: String result = response.getResult();
330: if (result != null) {
331: responseEncoding = response.getCharacterEncoding();
332: int length = response.getContentLength();
333: if (length < 0) {
334: length = result.getBytes(responseEncoding).length;
335: }
336: sbHttpResult.append("Content-Length: " + length
337: + "\r\n");
338: sbHttpResult.append("Connection: close\r\n");
339: sbHttpResult.append("\r\n");
340: sbHttpResult.append(result);
341: }
342: httpResult = sbHttpResult.toString();
343: }
344: }
345:
346: byte[] bytes = httpResult.getBytes(responseEncoding);
347: out.write(bytes, 0, bytes.length);
348: out.flush();
349: }
350:
351: /**
352: * Finds the servlet that should handle a request at the specified virtual
353: * path.
354: *
355: * @param path
356: * the virtual path, cannot be <code>null</code>.
357: *
358: * @return
359: * the servlet that was found, or <code>null</code> if none was found.
360: *
361: * @throws NullPointerException
362: * if <code>path == null</code>.
363: */
364: private LocalServletHandler findServlet(String path)
365: throws NullPointerException {
366:
367: // Special case is path "*"
368: if ("*".equals(path)) {
369: path = "/";
370: }
371:
372: // If the path does not end with a slash, then add one,
373: // to avoid checking that option
374: if (path.charAt(path.length() - 1) != '/') {
375: path += '/';
376: }
377:
378: LocalServletHandler servlet;
379: do {
380:
381: // Find a servlet at this path
382: servlet = (LocalServletHandler) _servlets.get(path);
383:
384: // If not found, then strip off the last part of the path
385: // E.g. "/objects/boats/Cherry" becomes "/objects/boats/"
386: // and "/objects/boats/Cherry/" becomes "/objects/boats/"
387: if (servlet == null) {
388:
389: // Remove the trailing slash, if any
390: int lastPos = path.length() - 1;
391: if (path.charAt(lastPos) == '/') {
392: path = path.substring(0, lastPos);
393: }
394:
395: // Cut up until and including the last slash, if appropriate
396: if (path.length() > 0) {
397: int i = path.lastIndexOf('/');
398: path = path.substring(0, i + 1);
399: }
400: }
401:
402: } while (servlet == null && path.length() > 0);
403:
404: return servlet;
405: }
406:
407: /**
408: * Sends an HTTP error back to the client.
409: *
410: * @param out
411: * the output stream to contact the client.
412: *
413: * @param status
414: * the HTTP error code status.
415: *
416: * @throws IOException
417: * if the error cannot be sent.
418: */
419: private void sendError(OutputStream out, String status)
420: throws IOException {
421: String httpResult = "HTTP/1.1 " + status + CRLF + CRLF;
422: byte[] bytes = httpResult.getBytes(REQUEST_ENCODING);
423: out.write(bytes, 0, bytes.length);
424: out.flush();
425: }
426:
427: /**
428: * Sends an HTTP bad request back to the client.
429: *
430: * @param out
431: * the output stream to contact the client.
432: *
433: * @throws IOException
434: * if the error cannot be sent.
435: */
436: private void sendBadRequest(OutputStream out) throws IOException {
437: sendError(out, "400 Bad Request");
438: }
439:
440: /**
441: * Parses an HTTP header.
442: *
443: * @param headers
444: * the headers already collected.
445: *
446: * @param header
447: * the line of the header to be parsed.
448: *
449: * @throws ParseException
450: * if the header is incorrect
451: */
452: private static void parseHeader(HashMap headers, String header)
453: throws ParseException {
454: int index = header.indexOf(':');
455: if (index < 1) {
456: throw new ParseException();
457: }
458:
459: // Get key and value
460: String key = header.substring(0, index);
461: String value = header.substring(index + 1);
462:
463: // Always convert the key to upper case
464: key = key.toUpperCase();
465:
466: // Always trim the value
467: value = value.trim();
468:
469: // XXX: Only one header supported
470: if (headers.get(key) != null) {
471: throw new ParseException();
472: }
473:
474: // Store the key-value combo
475: headers.put(key, value);
476: }
477:
478: /**
479: * Gets a HTTP header from the request.
480: *
481: * @param headers
482: * the list of the headers.
483: *
484: * @param key
485: * the name of the header.
486: *
487: * @return
488: * the header value for the specified key or <code>null</code> if the
489: * key is not in the haeders.
490: */
491: String getHeader(HashMap headers, String key) {
492: return (String) headers.get(key.toUpperCase());
493: }
494:
495: /**
496: * Reads the content of a web page.
497: *
498: * @param url
499: * the location of the content, cannot be <code>null</code>.
500: *
501: * @return
502: * the HTTP response to return, never <code>null</code>.
503: *
504: * @throws IOException
505: * if an error occcurs when reading the URL.
506: */
507: private String readWebPage(String url) throws IOException {
508: String httpResult;
509: if (getClass().getResource(url) != null) {
510: InputStream urlInputStream = getClass()
511: .getResourceAsStream(url);
512: ByteArrayOutputStream contentOutputStream = new ByteArrayOutputStream();
513: byte[] buf = new byte[8192];
514: int len;
515: while ((len = urlInputStream.read(buf)) > 0) {
516: contentOutputStream.write(buf, 0, len);
517: }
518: contentOutputStream.close();
519: urlInputStream.close();
520: String content = contentOutputStream.toString("ISO-8859-1");
521:
522: httpResult = "HTTP/1.1 200 OK\r\n";
523: String fileName = url.substring(url.lastIndexOf('/') + 1);
524: httpResult += "Content-Type: "
525: + MIME_TYPES_MAP.getContentTypeFor(fileName)
526: + "\r\n";
527: int length = content.getBytes("ISO-8859-1").length;
528: httpResult += "Content-Length: " + length + "\r\n";
529: httpResult += "Connection: close\r\n";
530: httpResult += "\r\n";
531: httpResult += content;
532: } else {
533: httpResult = "HTTP/1.1 404 Not Found\r\n";
534: }
535: return httpResult;
536: }
537: }
|