001: package com.meterware.pseudoserver;
002:
003: /********************************************************************************************************************
004: * $Id: PseudoServer.java,v 1.16 2004/04/20 16:35:21 russgold Exp $
005: *
006: * Copyright (c) 2000-2003, Russell Gold
007: *
008: * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
009: * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
010: * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
011: * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
012: *
013: * The above copyright notice and this permission notice shall be included in all copies or substantial portions
014: * of the Software.
015: *
016: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
017: * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
018: * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
019: * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
020: * DEALINGS IN THE SOFTWARE.
021: *
022: *******************************************************************************************************************/
023: import java.io.*;
024:
025: import java.net.HttpURLConnection;
026: import java.net.ServerSocket;
027: import java.net.Socket;
028:
029: import java.util.*;
030:
031: /**
032: * A basic simulated web-server for testing user agents without a web server.
033: **/
034: public class PseudoServer {
035:
036: private static final int DEFAULT_SOCKET_TIMEOUT = 1000;
037:
038: private static final int INPUT_POLL_INTERVAL = 10;
039:
040: /** Time in msec to wait for an outstanding server socket to be released before creating a new one. **/
041: private static int _socketReleaseWaitTime = 50;
042:
043: /** Number of outstanding server sockets that must be present before trying to wait for one to be released. **/
044: private static int _waitThreshhold = 10;
045:
046: private static int _numServers = 0;
047:
048: private int _serverNum = 0;
049:
050: private int _connectionNum = 0;
051:
052: private ArrayList _classpathDirs = new ArrayList();
053:
054: private String _maxProtocolLevel = "1.1";
055:
056: private final int _socketTimeout;
057:
058: /**
059: * Returns the amount of time the pseudo server will wait for a server socket to be released (in msec)
060: * before allocating a new one. See also {@link #getWaitThreshhold getWaitThreshhold}.
061: */
062: public static int getSocketReleaseWaitTime() {
063: return _socketReleaseWaitTime;
064: }
065:
066: /**
067: * Returns the amount of time the pseudo server will wait for a server socket to be released (in msec)
068: * before allocating a new one. See also {@link #getWaitThreshhold getWaitThreshhold}.
069: */
070: public static void setSocketReleaseWaitTime(
071: int socketReleaseWaitTime) {
072: _socketReleaseWaitTime = socketReleaseWaitTime;
073: }
074:
075: /**
076: * Returns the number of server sockets that must have been allocated and not returned before waiting for one
077: * to be returned.
078: */
079: public static int getWaitThreshhold() {
080: return _waitThreshhold;
081: }
082:
083: /**
084: * Specifies the number of server sockets that must have been allocated and not returned before waiting for one
085: * to be returned.
086: */
087: public static void setWaitThreshhold(int waitThreshhold) {
088: _waitThreshhold = waitThreshhold;
089: }
090:
091: public PseudoServer() {
092: this (DEFAULT_SOCKET_TIMEOUT);
093: }
094:
095: public PseudoServer(int socketTimeout) {
096: _socketTimeout = socketTimeout;
097: _serverNum = ++_numServers;
098: Thread t = new Thread("PseudoServer " + _serverNum) {
099: public void run() {
100: while (_active) {
101: try {
102: handleNewConnection(getServerSocket().accept());
103: Thread.sleep(20);
104: } catch (InterruptedIOException e) {
105: } catch (IOException e) {
106: System.out.println("Error in pseudo server: "
107: + e);
108: e.printStackTrace();
109: } catch (InterruptedException e) {
110: System.out
111: .println("Interrupted. Shutting down");
112: _active = false;
113: }
114: }
115: try {
116: if (_serverSocket != null)
117: ServerSocketFactory.release(_serverSocket);
118: _serverSocket = null;
119: } catch (IOException e) {
120: }
121: }
122: };
123: t.start();
124: }
125:
126: public void shutDown() {
127: if (_debug)
128: System.out
129: .println("** Requested shutdown of pseudoserver: "
130: + hashCode());
131: _active = false;
132: }
133:
134: public void setMaxProtocolLevel(int majorLevel, int minorLevel) {
135: _maxProtocolLevel = majorLevel + "." + minorLevel;
136: }
137:
138: /**
139: * Returns the port on which this server is listening.
140: **/
141: public int getConnectedPort() throws IOException {
142: return getServerSocket().getLocalPort();
143: }
144:
145: /**
146: * Defines the contents of an expected resource.
147: **/
148: public void setResource(String name, String value) {
149: setResource(name, value, "text/html");
150: }
151:
152: /**
153: * Defines the contents of an expected resource.
154: **/
155: public void setResource(String name, PseudoServlet servlet) {
156: _resources.put(asResourceName(name), servlet);
157: }
158:
159: /**
160: * Defines the contents of an expected resource.
161: **/
162: public void setResource(String name, String value,
163: String contentType) {
164: _resources.put(asResourceName(name), new WebResource(value,
165: contentType));
166: }
167:
168: /**
169: * Defines the contents of an expected resource.
170: **/
171: public void setResource(String name, byte[] value,
172: String contentType) {
173: _resources.put(asResourceName(name), new WebResource(value,
174: contentType));
175: }
176:
177: /**
178: * Defines a resource which will result in an error message.
179: **/
180: public void setErrorResource(String name, int errorCode,
181: String errorMessage) {
182: _resources.put(asResourceName(name), new WebResource(
183: errorMessage, errorCode));
184: }
185:
186: /**
187: * Enables the sending of the character set in the content-type header.
188: **/
189: public void setSendCharacterSet(String name, boolean enabled) {
190: WebResource resource = (WebResource) _resources
191: .get(asResourceName(name));
192: if (resource == null)
193: throw new IllegalArgumentException("No defined resource "
194: + name);
195: resource.setSendCharacterSet(enabled);
196: }
197:
198: /**
199: * Specifies the character set encoding for a resource.
200: **/
201: public void setCharacterSet(String name, String characterSet) {
202: WebResource resource = (WebResource) _resources
203: .get(asResourceName(name));
204: if (resource == null) {
205: resource = new WebResource("");
206: _resources.put(asResourceName(name), resource);
207: }
208: resource.setCharacterSet(characterSet);
209: }
210:
211: /**
212: * Adds a header to a defined resource.
213: **/
214: public void addResourceHeader(String name, String header) {
215: WebResource resource = (WebResource) _resources
216: .get(asResourceName(name));
217: if (resource == null) {
218: resource = new WebResource("");
219: _resources.put(asResourceName(name), resource);
220: }
221: resource.addHeader(header);
222: }
223:
224: public void mapToClasspath(String directory) {
225: _classpathDirs.add(directory);
226: }
227:
228: public void setDebug(boolean debug) {
229: _debug = debug;
230: }
231:
232: //------------------------------------- private members ---------------------------------------
233:
234: private Hashtable _resources = new Hashtable();
235:
236: private boolean _active = true;
237:
238: private boolean _debug;
239:
240: private String asResourceName(String rawName) {
241: if (rawName.startsWith("http:") || rawName.startsWith("/")) {
242: return escape(rawName);
243: } else {
244: return escape("/" + rawName);
245: }
246: }
247:
248: private static String escape(String urlString) {
249: if (urlString.indexOf(' ') < 0)
250: return urlString;
251: StringBuffer sb = new StringBuffer();
252:
253: int start = 0;
254: do {
255: int index = urlString.indexOf(' ', start);
256: if (index < 0) {
257: sb.append(urlString.substring(start));
258: break;
259: } else {
260: sb.append(urlString.substring(start, index)).append(
261: "%20");
262: start = index + 1;
263: }
264: } while (true);
265: return sb.toString();
266: }
267:
268: private void handleNewConnection(final Socket socket) {
269: Thread t = new Thread("PseudoServer " + _serverNum
270: + " connection " + (++_connectionNum)) {
271: public void run() {
272: try {
273: serveRequests(socket);
274: } catch (IOException e) {
275: e.printStackTrace(); //To change body of catch statement use Options | File Templates.
276: }
277: }
278: };
279: t.start();
280: }
281:
282: private void serveRequests(Socket socket) throws IOException {
283: socket.setSoTimeout(_socketTimeout);
284: socket.setTcpNoDelay(true);
285:
286: if (_debug)
287: System.out.println("** Created server thread: "
288: + hashCode());
289: final BufferedInputStream inputStream = new BufferedInputStream(
290: socket.getInputStream());
291: final HttpResponseStream outputStream = new HttpResponseStream(
292: socket.getOutputStream());
293:
294: while (_active) {
295: HttpRequest request = new HttpRequest(inputStream);
296: boolean keepAlive = respondToRequest(request, outputStream);
297: if (!keepAlive)
298: break;
299: while (_active && 0 == inputStream.available()) {
300: try {
301: Thread.sleep(INPUT_POLL_INTERVAL);
302: } catch (InterruptedException e) {
303: }
304: }
305: }
306: if (_debug)
307: System.out.println("** Closing server thread: "
308: + hashCode());
309: outputStream.close();
310: socket.close();
311: }
312:
313: private boolean respondToRequest(HttpRequest request,
314: HttpResponseStream response) {
315: if (_debug)
316: System.out.println("** Server thread " + hashCode()
317: + " handling request: " + request);
318: boolean keepAlive = isKeepAlive(request);
319: WebResource resource = null;
320: try {
321: response.restart();
322: response.setProtocol(getResponseProtocol(request));
323: resource = getResource(request);
324: if (resource == null) {
325: response.setResponse(HttpURLConnection.HTTP_NOT_FOUND,
326: "unable to find " + request.getURI());
327: } else {
328: if (resource.closesConnection())
329: keepAlive = false;
330: if (resource.getResponseCode() != HttpURLConnection.HTTP_OK) {
331: response
332: .setResponse(resource.getResponseCode(), "");
333: }
334: String[] headers = resource.getHeaders();
335: for (int i = 0; i < headers.length; i++) {
336: if (_debug)
337: System.out.println("** Server thread "
338: + hashCode() + " sending header: "
339: + headers[i]);
340: response.addHeader(headers[i]);
341: }
342: }
343: } catch (UnknownMethodException e) {
344: response.setResponse(HttpURLConnection.HTTP_BAD_METHOD,
345: "unsupported method: " + e.getMethod());
346: } catch (Throwable t) {
347: t.printStackTrace();
348: response.setResponse(HttpURLConnection.HTTP_INTERNAL_ERROR,
349: t.toString());
350: }
351: try {
352: response.write(resource);
353: } catch (IOException e) {
354: System.out.println("*** Failed to send reply: " + e);
355: }
356: return keepAlive;
357: }
358:
359: private boolean isKeepAlive(HttpRequest request) {
360: return request.wantsKeepAlive()
361: && _maxProtocolLevel.equals("1.1");
362: }
363:
364: private String getResponseProtocol(HttpRequest request) {
365: return _maxProtocolLevel.equalsIgnoreCase("1.1") ? request
366: .getProtocol() : "HTTP/1.0";
367: }
368:
369: private WebResource getResource(HttpRequest request)
370: throws IOException {
371: Object resource = _resources.get(request.getURI());
372: if (resource == null)
373: resource = _resources.get(withoutParameters(request
374: .getURI()));
375:
376: if (request.getCommand().equals("GET")
377: && resource instanceof WebResource) {
378: return (WebResource) resource;
379: } else if (resource instanceof PseudoServlet) {
380: return getResource((PseudoServlet) resource, request);
381: } else if (request.getURI().endsWith(".class")) {
382: for (Iterator iterator = _classpathDirs.iterator(); iterator
383: .hasNext();) {
384: String directory = (String) iterator.next();
385: if (request.getURI().startsWith(directory)) {
386: String resourceName = request.getURI().substring(
387: directory.length() + 1);
388: return new WebResource(getClass().getClassLoader()
389: .getResourceAsStream(resourceName),
390: "application/class", 200);
391: }
392: }
393: return null;
394: } else if (request.getURI().endsWith(".zip")
395: || request.getURI().endsWith(".jar")) {
396: for (Iterator iterator = _classpathDirs.iterator(); iterator
397: .hasNext();) {
398: String directory = (String) iterator.next();
399: if (request.getURI().startsWith(directory)) {
400: String resourceName = request.getURI().substring(
401: directory.length() + 1);
402: String classPath = System
403: .getProperty("java.class.path");
404: StringTokenizer st = new StringTokenizer(classPath,
405: ":;,");
406: while (st.hasMoreTokens()) {
407: String file = st.nextToken();
408: if (file.endsWith(resourceName)) {
409: File f = new File(file);
410: return new WebResource(new FileInputStream(
411: f), "application/zip", 200);
412: }
413: }
414: }
415: }
416: return null;
417: } else {
418: return null;
419: }
420: }
421:
422: private String withoutParameters(String uri) {
423: return uri.indexOf('?') < 0 ? uri : uri.substring(0, uri
424: .indexOf('?'));
425: }
426:
427: private WebResource getResource(PseudoServlet servlet,
428: HttpRequest request) throws IOException {
429: servlet.init(request);
430: return servlet.getResponse(request.getCommand());
431: }
432:
433: private ServerSocket getServerSocket() throws IOException {
434: synchronized (this ) {
435: if (_serverSocket == null)
436: _serverSocket = ServerSocketFactory.newServerSocket();
437: }
438: return _serverSocket;
439: }
440:
441: private ServerSocket _serverSocket;
442:
443: }
444:
445: class HttpResponseStream {
446:
447: final private static String CRLF = "\r\n";
448:
449: void restart() {
450: _headersWritten = false;
451: _headers.clear();
452: _responseCode = HttpURLConnection.HTTP_OK;
453: _responseText = "OK";
454: }
455:
456: void close() throws IOException {
457: flushHeaders();
458: _pw.close();
459: }
460:
461: HttpResponseStream(OutputStream stream) {
462: _stream = stream;
463: try {
464: setCharacterSet("us-ascii");
465: } catch (UnsupportedEncodingException e) {
466: _pw = new PrintWriter(new OutputStreamWriter(_stream));
467: }
468: }
469:
470: void setProtocol(String protocol) {
471: _protocol = protocol;
472: }
473:
474: void setResponse(int responseCode, String responseText) {
475: _responseCode = responseCode;
476: _responseText = responseText;
477: }
478:
479: void addHeader(String header) {
480: _headers.addElement(header);
481: }
482:
483: void write(String contents, String charset) throws IOException {
484: flushHeaders();
485: setCharacterSet(charset);
486: sendText(contents);
487: }
488:
489: void write(WebResource resource) throws IOException {
490: flushHeaders();
491: if (resource != null)
492: resource.writeTo(_stream);
493: _stream.flush();
494: }
495:
496: private void setCharacterSet(String characterSet)
497: throws UnsupportedEncodingException {
498: if (_pw != null)
499: _pw.flush();
500: _pw = new PrintWriter(new OutputStreamWriter(_stream,
501: characterSet));
502: }
503:
504: private void flushHeaders() {
505: if (!_headersWritten) {
506: sendResponse(_responseCode, _responseText);
507: for (Enumeration e = _headers.elements(); e
508: .hasMoreElements();) {
509: sendLine((String) e.nextElement());
510: }
511: sendText(CRLF);
512: _headersWritten = true;
513: _pw.flush();
514: }
515: }
516:
517: private void sendResponse(int responseCode, String responseText) {
518: sendLine(_protocol + ' ' + responseCode + ' ' + responseText);
519: }
520:
521: private void sendLine(String text) {
522: sendText(text);
523: sendText(CRLF);
524: }
525:
526: private void sendText(String text) {
527: _pw.write(text);
528: }
529:
530: private OutputStream _stream;
531: private PrintWriter _pw;
532:
533: private Vector _headers = new Vector();
534: private String _protocol = "HTTP/1.0";
535: private int _responseCode = HttpURLConnection.HTTP_OK;
536: private String _responseText = "OK";
537:
538: private boolean _headersWritten;
539:
540: }
541:
542: class ServerSocketFactory {
543:
544: static private ArrayList _sockets = new ArrayList();
545:
546: static private int _outstandingSockets;
547:
548: static private Object _releaseSemaphore = new Object();
549:
550: static synchronized ServerSocket newServerSocket()
551: throws IOException {
552: if (_sockets.isEmpty()
553: && _outstandingSockets > PseudoServer
554: .getWaitThreshhold()) {
555: try {
556: synchronized (_releaseSemaphore) {
557: _releaseSemaphore.wait(PseudoServer
558: .getSocketReleaseWaitTime());
559: }
560: } catch (InterruptedException e) {
561: }
562: ;
563: }
564: _outstandingSockets++;
565: if (!_sockets.isEmpty()) {
566: return (ServerSocket) _sockets.remove(0);
567: } else {
568: ServerSocket serverSocket = new ServerSocket(0);
569: serverSocket.setSoTimeout(1000);
570: return serverSocket;
571: }
572: }
573:
574: static synchronized void release(ServerSocket serverSocket)
575: throws IOException {
576: if (_sockets.size() >= 2 * PseudoServer.getWaitThreshhold()) {
577: serverSocket.close();
578: } else {
579: _sockets.add(serverSocket);
580: _outstandingSockets--;
581: synchronized (_releaseSemaphore) {
582: _releaseSemaphore.notify();
583: }
584: }
585: }
586: }
587:
588: class RecordingOutputStream extends OutputStream {
589:
590: private OutputStream _nestedStream;
591: private PrintStream _log;
592:
593: public RecordingOutputStream(OutputStream nestedStream,
594: PrintStream log) {
595: _nestedStream = nestedStream;
596: _log = log;
597: }
598:
599: public void write(int b) throws IOException {
600: _nestedStream.write(b);
601: _log.println("sending " + Integer.toHexString(b));
602: }
603:
604: public void write(byte b[], int offset, int len) throws IOException {
605: _nestedStream.write(b, offset, len);
606: _log.print("sending");
607: for (int i = offset; i < offset + len; i++) {
608: _log.print(' ' + Integer.toHexString(b[i]));
609: }
610: _log.println();
611: }
612: }
613:
614: class RecordingInputStream extends InputStream {
615:
616: private InputStream _nestedStream;
617: private PrintStream _log;
618:
619: public RecordingInputStream(InputStream nestedStream,
620: PrintStream log) {
621: _nestedStream = nestedStream;
622: _log = log;
623: }
624:
625: public int read() throws IOException {
626: int value = _nestedStream.read();
627: if (value != -1)
628: _log.print(' ' + Integer.toHexString(value));
629: return value;
630: }
631: }
|