One more week of (almost 20%) hacking on tomcat-lite:
authorcostin <costin@13f79535-47bb-0310-9956-ffa450edef68>
Mon, 14 Dec 2009 07:35:57 +0000 (07:35 +0000)
committercostin <costin@13f79535-47bb-0310-9956-ffa450edef68>
Mon, 14 Dec 2009 07:35:57 +0000 (07:35 +0000)
- protocol handler is almost useable with both tomcat7 and 6.x. It's missing most options of the other
connectors - will need to select what fits with 'lite', SSL is not hooked
- run the old watchdog tests ( in junit form ) - they seem to catch few problems, improved a bit the
runner to make eclipse happy.
- few load tests - and associated fixes.

If anyone tries it out with maven - one test will look for examples webapp, the one in tomcat7 has
a RequestDumperFilter that needs to be commented out. Ant runs more tests - some are failing. For
either - you need a build of the watchdog.

git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@890206 13f79535-47bb-0310-9956-ffa450edef68

21 files changed:
modules/tomcat-lite/.classpath
modules/tomcat-lite/java/org/apache/coyote/lite/LiteProtocolHandler.java
modules/tomcat-lite/java/org/apache/tomcat/lite/http/Http11Connection.java
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpChannel.java
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpConnector.java
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpMessage.java
modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOConnector.java
modules/tomcat-lite/java/org/apache/tomcat/lite/io/MemoryIOConnector.java
modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioThread.java
modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketConnector.java
modules/tomcat-lite/java/org/apache/tomcat/lite/service/IOStatus.java
modules/tomcat-lite/pom.xml
modules/tomcat-lite/test/org/apache/coyote/lite/ServletTests.java [new file with mode: 0644]
modules/tomcat-lite/test/org/apache/coyote/lite/Tomcat.java [new file with mode: 0644]
modules/tomcat-lite/test/org/apache/coyote/lite/TomcatLiteCoyoteTest.java
modules/tomcat-lite/test/org/apache/tomcat/lite/load/LiveHttpThreadedTest.java
modules/tomcat-lite/test/org/apache/tomcat/lite/load/ThreadRunner.java [new file with mode: 0644]
modules/tomcat-lite/test/org/apache/tomcat/lite/servlet/TomcatLiteWatchdog.java
modules/tomcat-lite/test/org/apache/tomcat/test/watchdog/WatchdogClient.java
modules/tomcat-lite/test/org/apache/tomcat/test/watchdog/WatchdogHttpClient.java
modules/tomcat-lite/test/org/apache/tomcat/test/watchdog/WatchdogTestCase.java

index b9d529b..fe5e2df 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
        <classpathentry excluding="org/apache/tomcat/lite/servlet/ServletApi30.java|org/apache/tomcat/lite/BodyReader.java|org/apache/tomcat/lite/BodyWriter.java|org/apache/tomcat/lite/ClientAbortException.java|org/apache/tomcat/lite/Connector.java|org/apache/tomcat/lite/ContextPreinitListener.java|org/apache/tomcat/lite/FilterChainImpl.java|org/apache/tomcat/lite/FilterConfigImpl.java|org/apache/tomcat/lite/Locale2Charset.java|org/apache/tomcat/lite/ParameterMap.java|org/apache/tomcat/lite/RequestDispatcherImpl.java|org/apache/tomcat/lite/ServletConfigImpl.java|org/apache/tomcat/lite/ServletContextImpl.java|org/apache/tomcat/lite/ServletInputStreamImpl.java|org/apache/tomcat/lite/ServletOutputStreamImpl.java|org/apache/tomcat/lite/ServletReaderImpl.java|org/apache/tomcat/lite/ServletRequestImpl.java|org/apache/tomcat/lite/ServletRequestWrapperImpl.java|org/apache/tomcat/lite/ServletResponseImpl.java|org/apache/tomcat/lite/ServletResponseIncludeWrapper.java|org/apache/tomcat/lite/ServletWriterImpl.java|org/apache/tomcat/lite/TomcatLite.java|org/apache/tomcat/lite/WebappContextMapper.java|org/apache/tomcat/lite/WebappFilterMapper.java|org/apache/tomcat/lite/WebappServletMapper.java|org/apache/tomcat/lite/webxml/|org/apache/tomcat/lite/coyote/CoyoteConnector.java|org/apache/coyote/servlet/" kind="src" path="java"/>
-       <classpathentry excluding="org/apache/coyote/lite/TomcatLiteCoyoteTest.java|org/apache/coyote/servlet/" kind="src" path="test"/>
+       <classpathentry excluding="org/apache/coyote/servlet/" kind="src" path="test"/>
        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
        <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
        <classpathentry kind="con" path="org.apache.ivyde.eclipse.cpcontainer.IVYDE_CONTAINER/?ivyXmlPath=pom.xml&amp;confs=compile"/>
index 1c6da20..3c35c22 100644 (file)
@@ -5,6 +5,8 @@ package org.apache.coyote.lite;
 import java.io.IOException;
 import java.util.Iterator;
 
+import org.apache.coyote.ActionCode;
+import org.apache.coyote.ActionHook;
 import org.apache.coyote.Adapter;
 import org.apache.coyote.InputBuffer;
 import org.apache.coyote.OutputBuffer;
@@ -14,11 +16,14 @@ import org.apache.coyote.Response;
 import org.apache.tomcat.lite.http.HttpConnector;
 import org.apache.tomcat.lite.http.HttpRequest;
 import org.apache.tomcat.lite.http.HttpResponse;
+import org.apache.tomcat.lite.http.MultiMap;
 import org.apache.tomcat.lite.http.HttpChannel.HttpService;
+import org.apache.tomcat.lite.http.MultiMap.Entry;
 import org.apache.tomcat.lite.io.CBuffer;
 import org.apache.tomcat.lite.io.SocketConnector;
 import org.apache.tomcat.util.buf.ByteChunk;
 import org.apache.tomcat.util.buf.MessageBytes;
+import org.apache.tomcat.util.http.MimeHeaders;
 
 /**
  * Work in progress - use the refactored http as a coyote connector.
@@ -104,19 +109,23 @@ public class LiteProtocolHandler implements ProtocolHandler {
                 buffer.length());
     }
 
-    private void coyoteService(HttpRequest httpReq, final HttpResponse httpRes) {
-        Request req = new Request();
-        req.setInputBuffer(new InputBuffer() {
-            @Override
-            public int doRead(ByteChunk chunk, Request request)
-                    throws IOException {
-                // TODO
-                return 0;
-            }
-        });
-        Response res = new Response();
-        res.setOutputBuffer(new OutputBuffer() {
-
+    private void coyoteService(final HttpRequest httpReq, final HttpResponse httpRes) {
+        RequestData rc = new RequestData();
+        rc.init(httpReq, httpRes);
+        
+        try {
+            adapter.service(rc.req, rc.res);
+        } catch (Exception e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+    }
+    
+    /** 
+     * Per request data.
+     */
+    public class RequestData implements ActionHook {
+        private final class LiteOutputBuffer implements OutputBuffer {
             @Override
             public int doWrite(org.apache.tomcat.util.buf.ByteChunk chunk,
                     Response response) throws IOException {
@@ -124,28 +133,263 @@ public class LiteProtocolHandler implements ProtocolHandler {
                         chunk.getLength());
                 return chunk.getLength();
             }
-            
-        });
-        
-        // TODO: turn http request into a coyote request - copy all fields, 
-        // add hooks where needed.
+        }
+
+        OutputBuffer outputBuffer = new LiteOutputBuffer();
+        // TODO: recycle, etc.
+        Request req = new Request();
+
+        Response res = new Response();
+        HttpResponse httpRes;
+        HttpRequest httpReq;
+        InputBuffer inputBuffer = new InputBuffer() {
+            @Override
+            public int doRead(ByteChunk bchunk, Request request)
+                    throws IOException {
+                httpReq.getBody().waitData(httpReq.getHttpChannel().getIOTimeout());
+                int rd = 
+                    httpReq.getBody().read(bchunk.getBytes(), 
+                        bchunk.getStart(), bchunk.getBytes().length);
+                if (rd > 0) {
+                    bchunk.setEnd(bchunk.getEnd() + rd);
+                }
+                return rd;
+            }
+        };
         
-        wrap(req.decodedURI(), httpReq.decodedURI());
-        wrap(req.method(), httpReq.method());
-        wrap(req.protocol(), httpReq.protocol());
-        wrap(req.requestURI(), httpReq.requestURI());
-        // Same for response.
+        public RequestData() {
+            req.setInputBuffer(inputBuffer);
+            res.setOutputBuffer(outputBuffer);
+            req.setResponse(res);
+            res.setRequest(req);
+            res.setHook(this);
+        }
         
-        try {
+        public void init(HttpRequest httpReq, HttpResponse httpRes) {
+            this.httpRes = httpRes;
+            this.httpReq = httpReq;
+            // TODO: turn http request into a coyote request - copy all fields, 
+            // add hooks where needed.
             
-            adapter.service(req, res);
+            wrap(req.decodedURI(), httpReq.decodedURI());
+            wrap(req.method(), httpReq.method());
+            wrap(req.protocol(), httpReq.protocol());
+            wrap(req.requestURI(), httpReq.requestURI());
+            wrap(req.queryString(), httpReq.queryString());
+           
+            req.setServerPort(httpReq.getServerPort());
+            req.serverName().setString(req.localName().toString());
             
-        } catch (Exception e) {
-            // TODO Auto-generated catch block
-            e.printStackTrace();
+            MultiMap mimeHeaders = httpReq.getMimeHeaders();
+            MimeHeaders coyoteHeaders = req.getMimeHeaders();
+            for (int i = 0; i < mimeHeaders.size(); i++ ) {
+                Entry entry = mimeHeaders.getEntry(i);
+                MessageBytes val = 
+                    coyoteHeaders.addValue(entry.getName().toString());
+                val.setString(entry.getValue().toString());
+            }
         }
+
+        /**
+         * Send an action to the connector.
+         *
+         * @param actionCode Type of the action
+         * @param param Action parameter
+         */
+        public void action(ActionCode actionCode, Object param) {
+
+            if (actionCode == ActionCode.ACTION_COMMIT) {
+                if (res.isCommitted())
+                    return;
+                
+                // TODO: copy headers, fields
+                httpRes.setStatus(res.getStatus());
+                httpRes.setMessage(res.getMessage());
+                MultiMap mimeHeaders = httpRes.getMimeHeaders();
+                MimeHeaders coyoteHeaders = res.getMimeHeaders();
+                for (int i = 0; i < coyoteHeaders.size(); i++ ) {
+                    MessageBytes name = coyoteHeaders.getName(i);
+                    MessageBytes val = coyoteHeaders.getValue(i);
+                    Entry entry = mimeHeaders.addEntry(name.toString());
+                    entry.getValue().set(val.toString());
+                }
+                String contentType = res.getContentType();
+                if (contentType != null) {
+                    mimeHeaders.addEntry("Content-Type").getValue().set(contentType);
+                }
+                String contentLang = res.getContentType();
+                if (contentLang != null) {
+                    mimeHeaders.addEntry("Content-Language").getValue().set(contentLang);
+                }
+                long contentLength = res.getContentLengthLong();
+                if (contentLength != -1) {
+                    httpRes.setContentLength(contentLength);
+                }
+                String lang = res.getContentLanguage();
+                if (lang != null) {
+                    httpRes.setHeader("Content-Language", lang);
+                }
+                
+                try {
+                    httpReq.send();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            } else if (actionCode == ActionCode.ACTION_ACK) {
+                // Done automatically by http connector
+            } else if (actionCode == ActionCode.ACTION_CLIENT_FLUSH) {
+                try {
+                    httpReq.send();
+                } catch (IOException e) {
+                    httpReq.getHttpChannel().abort(e);
+                    res.setErrorException(e);
+                }
+
+            } else if (actionCode == ActionCode.ACTION_CLOSE) {
+                // Close
+
+                // End the processing of the current request, and stop any further
+                // transactions with the client
+
+//                comet = false;
+//                try {
+//                    outputBuffer.endRequest();
+//                } catch (IOException e) {
+//                    // Set error flag
+//                    error = true;
+//                }
+
+            } else if (actionCode == ActionCode.ACTION_RESET) {
+                // Reset response
+                // Note: This must be called before the response is committed
+                httpRes.getBody().clear();
+
+            } else if (actionCode == ActionCode.ACTION_CUSTOM) {
+
+                // Do nothing
+
+            } else if (actionCode == ActionCode.ACTION_REQ_HOST_ADDR_ATTRIBUTE) {
+                req.remoteAddr().setString(httpReq.remoteAddr().toString());
+            } else if (actionCode == ActionCode.ACTION_REQ_LOCAL_NAME_ATTRIBUTE) {
+                req.localName().setString(httpReq.localName().toString());
+            } else if (actionCode == ActionCode.ACTION_REQ_HOST_ATTRIBUTE) {
+                req.remoteHost().setString(httpReq.remoteHost().toString());
+            } else if (actionCode == ActionCode.ACTION_REQ_LOCAL_ADDR_ATTRIBUTE) {
+                req.localAddr().setString(httpReq.localAddr().toString());
+            } else if (actionCode == ActionCode.ACTION_REQ_REMOTEPORT_ATTRIBUTE) {
+                req.setRemotePort(httpReq.getRemotePort());
+            } else if (actionCode == ActionCode.ACTION_REQ_LOCALPORT_ATTRIBUTE) {
+                req.setLocalPort(httpReq.getLocalPort());
+            } else if (actionCode == ActionCode.ACTION_REQ_SSL_ATTRIBUTE ) {
+
+//                if (ssl && (socket != 0)) {
+//                    try {
+//                        // Cipher suite
+//                        Object sslO = SSLSocket.getInfoS(socket, SSL.SSL_INFO_CIPHER);
+//                        if (sslO != null) {
+//                            request.setAttribute(AprEndpoint.CIPHER_SUITE_KEY, sslO);
+//                        }
+//                        // Get client certificate and the certificate chain if present
+//                        // certLength == -1 indicates an error
+//                        int certLength = SSLSocket.getInfoI(socket, SSL.SSL_INFO_CLIENT_CERT_CHAIN);
+//                        byte[] clientCert = SSLSocket.getInfoB(socket, SSL.SSL_INFO_CLIENT_CERT);
+//                        X509Certificate[] certs = null;
+//                        if (clientCert != null  && certLength > -1) {
+//                            certs = new X509Certificate[certLength + 1];
+//                            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+//                            certs[0] = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(clientCert));
+//                            for (int i = 0; i < certLength; i++) {
+//                                byte[] data = SSLSocket.getInfoB(socket, SSL.SSL_INFO_CLIENT_CERT_CHAIN + i);
+//                                certs[i+1] = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(data));
+//                            }
+//                        }
+//                        if (certs != null) {
+//                            request.setAttribute(AprEndpoint.CERTIFICATE_KEY, certs);
+//                        }
+//                        // User key size
+//                        sslO = new Integer(SSLSocket.getInfoI(socket, SSL.SSL_INFO_CIPHER_USEKEYSIZE));
+//                        request.setAttribute(AprEndpoint.KEY_SIZE_KEY, sslO);
+//
+//                        // SSL session ID
+//                        sslO = SSLSocket.getInfoS(socket, SSL.SSL_INFO_SESSION_ID);
+//                        if (sslO != null) {
+//                            request.setAttribute(AprEndpoint.SESSION_ID_KEY, sslO);
+//                        }
+//                        //TODO provide a hook to enable the SSL session to be
+//                        // invalidated. Set AprEndpoint.SESSION_MGR req attr
+//                    } catch (Exception e) {
+//                        log.warn(sm.getString("http11processor.socket.ssl"), e);
+//                    }
+//                }
+
+            } else if (actionCode == ActionCode.ACTION_REQ_SSL_CERTIFICATE) {
+
+//                if (ssl && (socket != 0)) {
+//                    // Consume and buffer the request body, so that it does not
+//                    // interfere with the client's handshake messages
+//                    InputFilter[] inputFilters = inputBuffer.getFilters();
+//                    ((BufferedInputFilter) inputFilters[Constants.BUFFERED_FILTER]).setLimit(maxSavePostSize);
+//                    inputBuffer.addActiveFilter(inputFilters[Constants.BUFFERED_FILTER]);
+//                    try {
+//                        // Configure connection to require a certificate
+//                        SSLSocket.setVerify(socket, SSL.SSL_CVERIFY_REQUIRE,
+//                                endpoint.getSSLVerifyDepth());
+//                        // Renegotiate certificates
+//                        if (SSLSocket.renegotiate(socket) == 0) {
+//                            // Don't look for certs unless we know renegotiation worked.
+//                            // Get client certificate and the certificate chain if present
+//                            // certLength == -1 indicates an error 
+//                            int certLength = SSLSocket.getInfoI(socket,SSL.SSL_INFO_CLIENT_CERT_CHAIN);
+//                            byte[] clientCert = SSLSocket.getInfoB(socket, SSL.SSL_INFO_CLIENT_CERT);
+//                            X509Certificate[] certs = null;
+//                            if (clientCert != null && certLength > -1) {
+//                                certs = new X509Certificate[certLength + 1];
+//                                CertificateFactory cf = CertificateFactory.getInstance("X.509");
+//                                certs[0] = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(clientCert));
+//                                for (int i = 0; i < certLength; i++) {
+//                                    byte[] data = SSLSocket.getInfoB(socket, SSL.SSL_INFO_CLIENT_CERT_CHAIN + i);
+//                                    certs[i+1] = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(data));
+//                                }
+//                            }
+//                            if (certs != null) {
+//                                request.setAttribute(AprEndpoint.CERTIFICATE_KEY, certs);
+//                            }
+//                        }
+//                    } catch (Exception e) {
+//                        log.warn(sm.getString("http11processor.socket.ssl"), e);
+//                    }
+//                }
+
+            } else if (actionCode == ActionCode.ACTION_REQ_SET_BODY_REPLAY) {
+//                ByteChunk body = (ByteChunk) param;
+//
+//                InputFilter savedBody = new SavedRequestInputFilter(body);
+//                savedBody.setRequest(request);
+//
+//                InternalAprInputBuffer internalBuffer = (InternalAprInputBuffer)
+//                request.getInputBuffer();
+//                internalBuffer.addActiveFilter(savedBody);
+
+            } else if (actionCode == ActionCode.ACTION_AVAILABLE) {
+                req.setAvailable(httpReq.getBody().available());
+            } else if (actionCode == ActionCode.ACTION_COMET_BEGIN) {
+//                comet = true;
+            } else if (actionCode == ActionCode.ACTION_COMET_END) {
+//                comet = false;
+            } else if (actionCode == ActionCode.ACTION_COMET_CLOSE) {
+                //no op
+            } else if (actionCode == ActionCode.ACTION_COMET_SETTIMEOUT) {
+                //no op
+//            } else if (actionCode == ActionCode.ACTION_ASYNC_START) {
+//                //TODO SERVLET3 - async
+//            } else if (actionCode == ActionCode.ACTION_ASYNC_COMPLETE) {
+//                //TODO SERVLET3 - async
+//            } else if (actionCode == ActionCode.ACTION_ASYNC_SETTIMEOUT) {
+//                //TODO SERVLET3 - async
+            }
+
+
+        }
+
     }
-    
-    
-    
 }
index f1bbba6..220e453 100644 (file)
@@ -16,6 +16,9 @@ import org.apache.tomcat.lite.io.FastHttpDateFormat;
 import org.apache.tomcat.lite.io.Hex;
 import org.apache.tomcat.lite.io.IOBuffer;
 import org.apache.tomcat.lite.io.IOChannel;
+import org.apache.tomcat.lite.io.IOConnector;
+import org.apache.tomcat.lite.io.NioThread;
+import org.apache.tomcat.lite.io.SocketIOChannel;
 
 public class Http11Connection extends HttpConnection {
     public static final String CHUNKED = "chunked";
@@ -61,6 +64,10 @@ public class Http11Connection extends HttpConnection {
     protected boolean http09 = false;
 
     HttpConnection switchedProtocol = null;
+
+    private int requestCount = 0;
+
+    private Object readLock = new Object();
     
     public Http11Connection(HttpConnector httpConnector) {
         this.httpConnector = httpConnector;
@@ -91,7 +98,7 @@ public class Http11Connection extends HttpConnection {
     private boolean readHead() throws IOException {
         while (true) {
             int read;
-            if (headRecvBuf.remaining() < 4) {
+            if (requestCount == 0 && headRecvBuf.remaining() < 4) {
                 // requests have at least 4 bytes - detect protocol
                 read = net.getIn().read(headRecvBuf, 4);
                 if (read < 0) {
@@ -168,63 +175,70 @@ public class Http11Connection extends HttpConnection {
             switchedProtocol.handleReceived(netx);
             return;
         }
-
+        //trace("handleReceived " + headersReceived);
         if (!checkKeepAliveClient()) {
             return; // we were in client keep alive mode
         }
-
-        if (!headersReceived) {
-            if (!readHead()) {
-                return;
+        // endSendReceived uses same lock - it will call this 
+        // to check outstanding bytes
+        synchronized (readLock) {
+            if (bodyReceived) {
+                return; // leave data in net buffer, for next req
             }
-        }
-        
-        // We have a header
-        if (activeHttp == null) {
-            if (checkHttpChannel() == null) {
-                return;
+            
+            if (!headersReceived) {
+                if (!readHead()) {
+                    return;
+                }
             }
-        }
-
-        IOBuffer receiveBody = activeHttp.receiveBody;
 
-        if (!headersReceived) {
-            headRecvBuf.wrapTo(headW);
-            parseMessage(activeHttp, headW);
-            if (serverMode && activeHttp.httpReq.decodedUri.remaining() == 0) {
-                abort(activeHttp, "Invalid url");
+            // We have a header
+            if (activeHttp == null) {
+                if (checkHttpChannel() == null) {
+                    return;
+                }
             }
 
-            headersReceived = true;
-            // Send header callbacks - we process any incoming data 
-            // first, so callbacks have more info
-            activeHttp.handleHeadersReceived(activeHttp.inMessage);
-        }
-        
-        // any remaining data will be processed as part of the 
-        // body - or left in the channel until endSendReceive()
-        
-        if (!bodyReceived) {
-            // Will close receiveBody when it consummed enough
-            rawDataReceived(activeHttp, receiveBody, net.getIn());
-            // Did we process anything ?
-            if (receiveBody.getBufferCount() > 0) {
-                activeHttp.sendHandleReceivedCallback(); // callback
+            IOBuffer receiveBody = activeHttp.receiveBody;
+
+            if (!headersReceived) {
+                headRecvBuf.wrapTo(headW);
+                parseMessage(activeHttp, headW);
+                if (serverMode && activeHttp.httpReq.decodedUri.remaining() == 0) {
+                    abort(activeHttp, "Invalid url");
+                }
+
+                headersReceived = true;
+                // Send header callbacks - we process any incoming data 
+                // first, so callbacks have more info
+                trace("Send headers received callback " + activeHttp.httpService);
+                activeHttp.handleHeadersReceived(activeHttp.inMessage);
             }
 
+            // any remaining data will be processed as part of the 
+            // body - or left in the channel until endSendReceive()
+
+            if (!bodyReceived) {
+                // Will close receiveBody when it consummed enough
+                rawDataReceived(activeHttp, receiveBody, net.getIn());
+                // Did we process anything ?
+                if (receiveBody.getBufferCount() > 0) {
+                    activeHttp.sendHandleReceivedCallback(); // callback
+                }
+            }
             // Receive has marked the body as closed
             if (receiveBody.isAppendClosed()) {
-                activeHttp.handleEndReceive();
                 bodyReceived = true;
+                activeHttp.handleEndReceive();
             }
-        }
 
 
-        if (net.getIn().isClosedAndEmpty()) {
-            // If not already closed.
-            closeStreamOnEnd("closed after body");
-        }
+            if (net.getIn().isClosedAndEmpty()) {
+                // If not already closed.
+                closeStreamOnEnd("closed after body");
+            }
         
+        }
     }
 
     /**
@@ -300,7 +314,6 @@ public class Http11Connection extends HttpConnection {
             switchedProtocol.endSendReceive(http);
             return;
         }
-
         activeHttp = null; 
         if (!keepAlive()) {
             if (debug) {
@@ -308,31 +321,30 @@ public class Http11Connection extends HttpConnection {
             }
             if (net != null) {
                 net.close();
-//                net.getOut().close(); // shutdown output if not done
-//                net.getIn().close(); // this should close the socket
                 net.startSending();
-                
+
             }
             beforeRequest(); 
             return;
         }
-        
-        beforeRequest(); // will clear head buffer
-        
-        if (serverMode) {
-            handleReceived(net); // will attempt to read next req
-            if (debug) {
-                log.info(">>> server socket KEEP_ALIVE " + net.getTarget() + 
-                        " " + net);
-            }
-            
-        } else {
-            if (debug) {
-                log.info(">>> client socket KEEP_ALIVE " + net.getTarget() + 
-                        " " + net);
+        synchronized (readLock) {
+            beforeRequest(); // will clear head buffer
+            requestCount++;
+            if (serverMode) {
+                if (debug) {
+                    log.info(">>> server socket KEEP_ALIVE " + net.getTarget() + 
+                            " " + net + " " + net.getIn().available());
+                }
+                handleReceived(net); // will attempt to read next req
+            } else {
+                if (debug) {
+                    log.info(">>> client socket KEEP_ALIVE " + net.getTarget() + 
+                            " " + net);
+                }
+                httpConnector.cpool.returnChannel(this);
             }
-            httpConnector.cpool.returnChannel(this);
         }
+
     }
     
     private void trace(String s) {
@@ -1384,8 +1396,11 @@ public class Http11Connection extends HttpConnection {
             return switchedProtocol.toString();
         }
 
-        return (serverMode ? "S11 " : "C11 ") +
-        (keepAlive() ? " KA " : "");  
+        return (serverMode ? "SR " : "CL ") +
+        (keepAlive() ? " KA " : "") +
+        (headersReceived ? " HEAD " : "") +
+        (bodyReceived ? " BODY " : "")
+        ;  
     }
     
 }
index 170dbaf..25815cc 100644 (file)
@@ -166,29 +166,32 @@ public class HttpChannel extends IOChannel {
      * 
      * @throws IOException
      */
-    public void abort(Throwable t) throws IOException {
+    public void abort(Throwable t) {
         abort(t.toString());
     }
     
-    public void abort(String t) throws IOException {
+    public void abort(String t)  {
         synchronized (this) {
             if (abortDone) {
                 return;
             }
             abortDone = true;
         }
-
-        checkRelease();
-        trace("abort " + t);
-        log.info("Abort connection " + t);
-        if (conn != null) {
-            conn.abort(this, t);
+        try {
+            checkRelease();
+            trace("abort " + t);
+            log.info("Abort connection " + t);
+            if (conn != null) {
+                conn.abort(this, t);
+            }
+            inMessage.state = HttpMessage.State.DONE;
+            outMessage.state = HttpMessage.State.DONE;
+            sendReceiveDone = true;
+            error = true;
+            handleEndSendReceive();
+        } catch (Throwable ex) {
+            log.severe("Exception in abort " + ex);
         }
-        inMessage.state = HttpMessage.State.DONE;
-        outMessage.state = HttpMessage.State.DONE;
-        sendReceiveDone = true;
-        error = true;
-        handleEndSendReceive();
     }
     
     /**
index 37279e3..e53b544 100644 (file)
@@ -6,11 +6,10 @@ import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.Queue;
 import java.util.Set;
 import java.util.Timer;
-import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.logging.Logger;
 
@@ -46,12 +45,22 @@ public class HttpConnector {
     HttpConnectionManager conManager = new HttpConnectionManager();
     
     private static Logger log = Logger.getLogger("HttpConnector");
-    private int maxHttpPoolSize = 20;
     
-    private int maxSocketPoolSize = 100;
-    private int keepAliveTimeMs = 30000;
+    /** 
+     * Cache HttpChannel/request/buffers
+     */
+    private int maxHttpPoolSize = 50;
+    
+    /** 
+     * Max number of connections to keep alive. 
+     * Each connection holds a header buffer and the socket.
+     * ( we could skip the header buffer )
+     */
+    private int maxSocketPoolSize = 500; // 10000;
+    
+    private int keepAliveTimeMs = 300000;
     
-    private Queue<HttpChannel> httpChannelPool = new ConcurrentLinkedQueue<HttpChannel>();
+    private List<HttpChannel> httpChannelPool = new ArrayList<HttpChannel>();
 
     protected IOConnector ioConnector;
     
@@ -76,12 +85,21 @@ public class HttpConnector {
     Dispatcher dispatcher;
     protected HttpService defaultService;
     int port = 8080;
-    
+
+    private Timer timer;
+
+    private static Timer defaultTimer = new Timer(true);
     
     public HttpConnector(IOConnector ioConnector) {
         this.ioConnector = ioConnector;
         dispatcher = new Dispatcher();
         defaultService = dispatcher;
+        if (ioConnector != null) {
+            timer = ioConnector.getTimer();
+        } else {
+            // tests
+            timer = defaultTimer;
+        }
     }
 
     protected HttpConnector() {
@@ -214,10 +232,16 @@ public class HttpConnector {
     protected HttpChannel get(boolean server) throws IOException {
         HttpChannel processor = null;
         synchronized (httpChannelPool) {
-            processor = httpChannelPool.poll();
+            int cnt = httpChannelPool.size();
+            if (cnt > 0) {
+                processor = httpChannelPool.remove(cnt - 1);
+            }
         }
         boolean reuse = false;
         totalHttpChannel.incrementAndGet();
+        if (!server) {
+            totalClientHttpChannel.incrementAndGet();
+        }
         if (processor == null) {
             processor = create();
         } else {
@@ -225,9 +249,6 @@ public class HttpConnector {
             reusedChannels.incrementAndGet();
             processor.release = false;
         }
-        if (!server) {
-            totalClientHttpChannel.incrementAndGet();
-        }
         processor.serverMode(server);
         if (debug) {
             log.info((reuse ? "REUSE ": "Create ") +
@@ -242,50 +263,35 @@ public class HttpConnector {
         return processor;
     }
 
-
-    /**
-     * Called by HttpChannel when the HTTP request is done, i.e. all 
-     * sending/receiving is complete. The service may still use the 
-     * HttpChannel object.
-     * 
-     * If keepOpen: clients will wait in the pool, detecting server close.
-     * For server: will wait for new requests.
-     * 
-     * TODO: timeouts, better pool management
-     */
-    protected void returnSocket(IOChannel ch, boolean serverMode, 
-                                boolean keepOpen) 
-            throws IOException {
-        // Now handle net - note that we could have reused the async object
-        
-
-    }
-    
     protected void returnToPool(HttpChannel http) throws IOException {
         inUse.decrementAndGet();
         recycledChannels.incrementAndGet();
-        if (debug) {
-            log.info("Return " + http.getTarget() + " obj=" +
-                http + " size=" + httpChannelPool.size());
-        }
+        int size = 0;
+        boolean pool = false;
         
         http.recycle();
+        http.setConnection(null);
+        http.setConnector(null);
         
         // No more data - release the object
         synchronized (httpChannelPool) {
-            http.setConnection(null);
-            http.setConnector(null);
+            size = httpChannelPool.size();
             if (httpChannelPool.contains(http)) {
-                System.err.println("dup ? ");                
-            }
-            if (httpChannelPool.size() >= maxHttpPoolSize) {
-                if (httpEvents != null) {
-                    httpEvents.onDestroy(http, this);
-                }
-            } else {
+                log.severe("Duplicate element in pool !");
+            } else if (size < maxHttpPoolSize) {
                 httpChannelPool.add(http);
+                pool = true;
             }
         }
+        
+        if (!pool && httpEvents != null) {
+            httpEvents.onDestroy(http, this);
+        }
+        if (debug) {
+            log.info((pool ? "Return " : "Destroy ")
+                    + http.getTarget() + " obj=" +
+                    http + " size=" + size);
+        }
     }
     
     
@@ -438,7 +444,7 @@ public class HttpConnector {
         implements DataReceivedCallback
     {
         protected HttpConnector httpConnector;
-        protected boolean serverMode;
+        protected boolean serverMode = false;
 
         protected BBuffer headRecvBuf = BBuffer.allocate(8192);
         
@@ -543,7 +549,6 @@ public class HttpConnector {
          */
         public Map<CharSequence, RemoteServer> hosts = new HashMap<CharSequence, 
             RemoteServer>();
-        boolean keepOpen = true;
 
         // Statistics
         public AtomicInteger waitingSockets = new AtomicInteger();
@@ -552,8 +557,6 @@ public class HttpConnector {
         public AtomicInteger hits = new AtomicInteger();
         public AtomicInteger misses = new AtomicInteger();
 
-        Timer timer;
-        
         public int getTargetCount() {
             return hosts.size();
         }
@@ -633,12 +636,6 @@ public class HttpConnector {
                 return;
             }
             
-            if (!keepOpen) {
-                ch.close();
-                return;
-            }
-            
-//            SocketIOChannel sdata = (SocketIOChannel) ch;
             if (!ch.isOpen()) {
                 ch.close(); // make sure all closed
                 if (debug) {
index 5daddfe..867ca2e 100644 (file)
@@ -79,8 +79,11 @@ public abstract class HttpMessage {
         }
         
         public int addHeader() {
-            headerNames.add(BBuffer.wrapper());
-            headerValues.add(BBuffer.wrapper());
+            if (headerCount >= headerNames.size()) {
+                // make space for the new header.
+                headerNames.add(BBuffer.wrapper());
+                headerValues.add(BBuffer.wrapper());                
+            }
             return headerCount++;
         }
         
index 1505cc3..62eb0da 100644 (file)
@@ -3,6 +3,7 @@
 package org.apache.tomcat.lite.io;
 
 import java.io.IOException;
+import java.util.Timer;
 
 
 /**
@@ -27,17 +28,25 @@ public abstract class IOConnector {
     public static interface DataFlushedCallback {
         public void handleFlushed(IOChannel ch) throws IOException;
     }
+
+    protected Timer timer;
+    
+    public Timer getTimer() {
+        return timer;
+    }
     
     public abstract void acceptor(IOConnector.ConnectedCallback sc, 
                          CharSequence port, Object extra)
         throws IOException; 
-
+    
     // TODO: failures ? 
     // TODO: use String target or url
     public abstract void connect(String host, int port, 
             IOConnector.ConnectedCallback sc) throws IOException;
     
     public void stop() {
-        
+        if (timer != null) {
+            timer.cancel();
+        }
     }
 }
index 0b9f56b..5f6b50c 100644 (file)
@@ -3,6 +3,7 @@
 package org.apache.tomcat.lite.io;
 
 import java.io.IOException;
+import java.util.Timer;
 
 public class MemoryIOConnector extends IOConnector {
 
@@ -57,6 +58,10 @@ public class MemoryIOConnector extends IOConnector {
     ConnectedCallback acceptor;
     MemoryIOConnector server;
     
+    public MemoryIOConnector() {
+        timer = new Timer(true);
+    }
+    
     public MemoryIOConnector withServer(MemoryIOConnector server) {
         this.server = server;
         return server;
index 2391246..d782ae3 100644 (file)
@@ -229,10 +229,13 @@ public class NioThread implements Runnable {
               // handle events for existing req first.
               if (selected != 0) {
                   sloops = 0;
+                  int callbackCnt = 0;
                   Set<SelectionKey> sel = selector.selectedKeys();
                   Iterator<SelectionKey> i = sel.iterator();
 
                   while (i.hasNext()) {
+                      callbackCnt++;
+                      long beforeCallback = System.currentTimeMillis();
                       SelectionKey sk = i.next();
                       i.remove();
                       
@@ -295,11 +298,12 @@ public class NioThread implements Runnable {
                           }
                           
                           long callbackTime = 
-                              System.currentTimeMillis() - lastWakeup;
+                              System.currentTimeMillis() - beforeCallback;
                           
                           if (callbackTime > 250) { 
                               log.warning("Callback too long ! ops=" + ready + 
-                                      " time=" + callbackTime + " ch=" + ch);
+                                      " time=" + callbackTime + " ch=" + ch + 
+                                      " " + callbackCnt);
                           }
                           if (callbackTime > maxCallbackTime) {
                               maxCallbackTime = callbackTime;
@@ -349,6 +353,9 @@ public class NioThread implements Runnable {
       selector = Selector.open();
       for (int i = 0; i < oldCh.size(); i++) {
           NioChannel selectorData = oldCh.get(i);
+          if (selectorData == null) {
+              continue;
+          }
           int interest = interests.get(i);
           if (selectorData.channel instanceof ServerSocketChannel) {
               ServerSocketChannel socketChannel = 
@@ -1148,4 +1155,9 @@ public class NioThread implements Runnable {
       return Thread.currentThread() == selectorThread;
   }
   
+  public static boolean isSelectorThread(IOChannel ch) {
+      SocketIOChannel sc = (SocketIOChannel) ch.getFirst();
+      return Thread.currentThread() == sc.ch.sel.selectorThread;
+  }
+  
 }
\ No newline at end of file
index 8cd33ee..9b50f49 100644 (file)
@@ -4,6 +4,7 @@ package org.apache.tomcat.lite.io;
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.util.Timer;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.logging.Logger;
@@ -36,6 +37,7 @@ public class SocketConnector extends IOConnector {
     Executor threadPool = Executors.newCachedThreadPool();
     
     public SocketConnector() {
+        timer = new Timer(true);
     }
 
     /**
index fee4d18..705df98 100644 (file)
@@ -20,10 +20,6 @@ public class IOStatus implements HttpService {
 
     private ConnectionPool pool;
 
-    public IOStatus() {
-        
-    }
-    
     public IOStatus(ConnectionPool pool) {
         this.pool = pool;
     }
@@ -31,9 +27,7 @@ public class IOStatus implements HttpService {
     @Override
     public void service(HttpRequest httpReq, HttpResponse httpRes)
             throws IOException {
-        ConnectionPool sc = pool == null ? 
-                httpReq.getHttpChannel().getConnector().cpool :
-                    pool;
+        ConnectionPool sc = pool;
         HttpWriter out = httpRes.getBodyWriter();
         
         httpRes.setContentType("text/plain");
index e3de603..d44cd77 100644 (file)
@@ -99,7 +99,6 @@
           </excludes>
           <testExcludes>
             <exclude>org/apache/coyote/servlet/**</exclude>
-            <exclude>org/apache/coyote/lite/**</exclude>
             <exclude>**/ServletApi30.java</exclude>
           </testExcludes>
         </configuration>
diff --git a/modules/tomcat-lite/test/org/apache/coyote/lite/ServletTests.java b/modules/tomcat-lite/test/org/apache/coyote/lite/ServletTests.java
new file mode 100644 (file)
index 0000000..4681406
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ */
+package org.apache.coyote.lite;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.catalina.LifecycleException;
+import org.apache.tomcat.lite.servlet.TomcatLite;
+import org.apache.tomcat.lite.servlet.TomcatLiteWatchdog;
+
+import junit.framework.Test;
+
+public class ServletTests extends TomcatLiteWatchdog {
+    
+    public ServletTests() {
+        super();
+        port = 7075;
+        exclude = new String[] {
+                "ServletToJSPErrorPageTest",
+                "ServletToJSPError502PageTest",
+        };
+    }
+    
+    public ServletTests(String name) {
+       super(name);
+       port = 7075;
+    }
+    
+    protected void addConnector(TomcatLite connector) {
+       
+    }
+    
+    public void initServerWithWatchdog(String wdDir) throws ServletException, 
+            IOException {
+        Tomcat tomcat = new Tomcat();
+        
+        File f = new File(wdDir + "/build/webapps");
+        tomcat.setPort(port);
+        tomcat.setBaseDir(f.getCanonicalPath());
+        
+        TomcatLiteCoyoteTest.setUp(tomcat, port);
+        
+        for (String s : new String[] {      
+                "servlet-compat", 
+                "servlet-tests",
+                "jsp-tests"} ) {
+            tomcat.addWebapp("/" + s, f.getCanonicalPath() + "/" + s);
+        }
+
+        try {
+            tomcat.start();
+        } catch (LifecycleException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+    }
+
+    /** 
+     * Magic JUnit method 
+     */
+    public static Test suite() {
+        return new ServletTests().getSuite();
+    }
+}
diff --git a/modules/tomcat-lite/test/org/apache/coyote/lite/Tomcat.java b/modules/tomcat-lite/test/org/apache/coyote/lite/Tomcat.java
new file mode 100644 (file)
index 0000000..887c654
--- /dev/null
@@ -0,0 +1,912 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.lite;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.Realm;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.core.StandardContext;
+import org.apache.catalina.core.StandardEngine;
+import org.apache.catalina.core.StandardHost;
+import org.apache.catalina.core.StandardServer;
+import org.apache.catalina.core.StandardService;
+import org.apache.catalina.core.StandardWrapper;
+import org.apache.catalina.realm.GenericPrincipal;
+import org.apache.catalina.realm.RealmBase;
+import org.apache.catalina.session.StandardManager;
+import org.apache.catalina.startup.ContextConfig;
+
+// TODO: lazy init for the temp dir - only when a JSP is compiled or 
+// get temp dir is called we need to create it. This will avoid the 
+// need for the baseDir
+
+// TODO: allow contexts without a base dir - i.e. 
+// only programmatic. This would disable the default servlet.
+
+/**
+ * Minimal tomcat starter for embedding/unit tests.
+ * 
+ * Tomcat supports multiple styles of configuration and 
+ * startup - the most common and stable is server.xml-based,
+ * implemented in org.apache.catalina.startup.Bootstrap.
+ *
+ * This class is for use in apps that embed tomcat. 
+ * Requirements:
+ * 
+ * - all tomcat classes and possibly servlets are in the classpath.
+ * ( for example all is in one big jar, or in eclipse CP, or in any other
+ * combination )
+ * 
+ * - we need one temporary directory for work files
+ * 
+ * - no config file is required. This class provides methods to 
+ * use if you have a webapp with a web.xml file, but it is 
+ * optional - you can use your own servlets.
+ * 
+ * This class provides a main() and few simple CLI arguments,
+ * see setters for doc. It can be used for simple tests and
+ * demo.
+ * 
+ * @see TestTomcat for examples on how to use this
+ * @author Costin Manolache
+ */
+public class Tomcat {
+    // Single engine, service, server, connector - few cases need more,
+    // they can use server.xml
+    protected StandardServer server;
+    protected StandardService service;
+    protected StandardEngine engine;
+    protected Connector connector; // for more - customize the classes
+    
+    // To make it a bit easier to config for the common case
+    // ( one host, one context ). 
+    protected StandardHost host;
+
+    // TODO: it's easy to add support for more hosts - but is it 
+    // really needed ?
+
+    // TODO: allow use of in-memory connector
+    
+    protected int port = 8080;
+    protected String hostname = "localhost";
+    protected String basedir;
+    
+    // Default in-memory realm, will be set by default on 
+    // created contexts. Can be replaced with setRealm() on 
+    // the context.
+    protected Realm defaultRealm;
+    private Map<String, String> userPass = new HashMap<String, String>();
+    private Map<String, List<String>> userRoles = 
+            new HashMap<String, List<String>>();
+    private Map<String, Principal> userPrincipals = new HashMap<String, Principal>();
+    
+    public Tomcat() {
+        // NOOP
+    }
+    
+    /**
+     * Tomcat needs a directory for temp files. This should be the 
+     * first method called. 
+     * 
+     * By default, if this method is not called, we use:
+     *  - system properties - catalina.base, catalina.home 
+     *  - $HOME/tomcat.$PORT
+     * ( /tmp doesn't seem a good choice for security ).
+     *   
+     *
+     * TODO: better default ? Maybe current dir ? 
+     * TODO: disable work dir if not needed ( no jsp, etc ).
+     */
+    public void setBaseDir(String basedir) {
+        this.basedir = basedir;
+    }
+
+    /** 
+     * Set the port for the default connector. Must 
+     * be called before start().
+     */
+    public void setPort(int port) {
+        this.port = port;
+    }
+    
+    /** 
+     * The the hostname of the default host, default is 
+     * 'localhost'.
+     */
+    public void setHostname(String s) {
+        hostname = s;
+    }
+
+    /** 
+     * Add a webapp using normal WEB-INF/web.xml if found.
+     * 
+     * @param contextPath
+     * @param baseDir
+     * @return new StandardContext
+     * @throws ServletException 
+     */
+    public StandardContext addWebapp(String contextPath, 
+                                     String baseDir) throws ServletException {
+        
+        return addWebapp(getHost(), contextPath, baseDir);    
+    }
+    
+    
+    /** 
+     * Add a context - programmatic mode, no web.xml used.
+     *
+     * API calls equivalent with web.xml:
+     * 
+     * context-param
+     *  ctx.addParameter("name", "value");
+     *     
+     *
+     * error-page
+     *    ErrorPage ep = new ErrorPage();
+     *    ep.setErrorCode(500);
+     *    ep.setLocation("/error.html");
+     *    ctx.addErrorPage(ep);
+     *   
+     * ctx.addMimeMapping("ext", "type");
+     * 
+     * Note: If you reload the Context, all your configuration will be lost. If
+     * you need reload support, consider using a LifecycleListener to provide
+     * your configuration.
+     *  
+     * TODO: add the rest
+     *
+     *  @param contextPath "/" for root context.
+     *  @param baseDir base dir for the context, for static files. Must exist, 
+     *  relative to the server home
+     */
+    public StandardContext addContext(String contextPath, 
+                                      String baseDir) {
+        return addContext(getHost(), contextPath, baseDir);
+    }
+
+    /**
+     * Equivalent with 
+     *  <servlet><servlet-name><servlet-class>.
+     *  
+     * In general it is better/faster to use the method that takes a 
+     * Servlet as param - this one can be used if the servlet is not 
+     * commonly used, and want to avoid loading all deps.
+     * ( for example: jsp servlet )
+     * 
+     * You can customize the returned servlet, ex:
+     * 
+     *    wrapper.addInitParameter("name", "value");
+     *    
+     * @param contextPath   Context to add Servlet to
+     * @param servletName   Servlet name (used in mappings)
+     * @param servletClass  The class to be used for the Servlet
+     * @return The wrapper for the servlet
+     */
+    public StandardWrapper addServlet(String contextPath, 
+            String servletName, 
+            String servletClass) {
+        Container ctx = getHost().findChild(contextPath);
+        return addServlet((StandardContext) ctx, 
+                servletName, servletClass);
+    }
+
+    /**
+     * Static version of {@link #addServlet(String, String, String)}
+     * @param ctx           Context to add Servlet to
+     * @param servletName   Servlet name (used in mappings)
+     * @param servletClass  The class to be used for the Servlet
+     * @return The wrapper for the servlet
+     */
+    public static StandardWrapper addServlet(StandardContext ctx, 
+                                      String servletName, 
+                                      String servletClass) {
+        // will do class for name and set init params
+        StandardWrapper sw = (StandardWrapper)ctx.createWrapper();
+        sw.setServletClass(servletClass);
+        sw.setName(servletName);
+        ctx.addChild(sw);
+        
+        return sw;
+    }
+
+    /**
+     * Add an existing Servlet to the context with no class.forName or
+     * initialisation.
+     * @param contextPath   Context to add Servlet to
+     * @param servletName   Servlet name (used in mappings)
+     * @param servlet       The Servlet to add
+     * @return The wrapper for the servlet
+     */
+    public StandardWrapper addServlet(String contextPath, 
+            String servletName, 
+            Servlet servlet) {
+        Container ctx = getHost().findChild(contextPath);
+        return addServlet((StandardContext) ctx, 
+                servletName, servlet);
+    }
+
+    /**
+     * Static version of {@link #addServlet(String, String, Servlet)}.
+     * @param ctx           Context to add Servlet to
+     * @param servletName   Servlet name (used in mappings)
+     * @param servlet       The Servlet to add
+     * @return The wrapper for the servlet
+     */
+    public static StandardWrapper addServlet(StandardContext ctx,
+                                      String servletName, 
+                                      Servlet servlet) {
+        // will do class for name and set init params
+        StandardWrapper sw = new ExistingStandardWrapper(servlet);
+        sw.setName(servletName);
+        ctx.addChild(sw);
+        
+        return sw;
+    }
+    
+    
+    /**
+     * Initialize and start the server.
+     * @throws LifecycleException 
+     */
+    public void start() throws LifecycleException {
+        getServer();
+        getConnector();
+        server.initialize();
+        server.start();
+    }
+
+    /** 
+     * Stop the server.
+     * @throws LifecycleException 
+     */
+    public void stop() throws LifecycleException {
+        getServer().stop();
+    }
+
+
+    /** 
+     * Add a user for the in-memory realm. All created apps use this 
+     * by default, can be replaced using setRealm().
+     *  
+     */
+    public void addUser(String user, String pass) {
+        userPass.put(user, pass);
+    }
+    
+    /**
+     * @see #addUser(String, String) 
+     */
+    public void addRole(String user, String role) {
+        List<String> roles = userRoles.get(user);
+        if (roles == null) {
+            roles = new ArrayList<String>();
+            userRoles.put(user, roles);
+        }
+        roles.add(role);
+    }
+
+    // ------- Extra customization -------
+    // You can tune individual tomcat objects, using internal APIs
+    
+    /** 
+     * Get the default http connector. You can set more 
+     * parameters - the port is already initialized.
+     * 
+     * Alternatively, you can construct a Connector and set any params,
+     * then call addConnector(Connector)
+     * 
+     * @return A connector object that can be customized
+     */
+    public Connector getConnector() {
+        getServer();
+        if (connector != null) {
+            return connector;
+        }
+        // This will load Apr connector if available,
+        // default to nio. I'm having strange problems with apr
+        // and for the use case the speed benefit wouldn't matter.
+        
+        //connector = new Connector("HTTP/1.1");
+        try {
+            connector = new Connector("org.apache.coyote.http11.Http11Protocol");
+            connector.setPort(port);
+            service.addConnector( connector );
+        } catch (Exception e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } 
+        return connector;
+    }
+    
+    public void setConnector(Connector connector) {
+        this.connector = connector;
+    }
+    
+    /** 
+     * Get the service object. Can be used to add more 
+     * connectors and few other global settings.
+     */
+    public StandardService getService() {
+        getServer();
+        return service;
+    }
+
+    /** 
+     * Sets the current host - all future webapps will
+     * be added to this host. When tomcat starts, the 
+     * host will be the default host.
+     * 
+     * @param host
+     */
+    public void setHost(StandardHost host) {
+        this.host = host;
+    }
+    
+    public StandardHost getHost() {
+        if (host == null) {
+            host = new StandardHost();
+            host.setName(hostname);
+
+            getEngine().addChild( host );
+        }
+        return host;
+    }
+    
+    /** 
+     * Set a custom realm for auth. If not called, a simple
+     * default will be used, using an internal map.
+     * 
+     * Must be called before adding a context.
+     */
+    public void setDefaultRealm(Realm realm) {
+        defaultRealm = realm;
+    }
+    
+
+    /** 
+     * Access to the engine, for further customization.
+     */
+    public StandardEngine getEngine() {
+        if(engine == null ) {
+            getServer();
+            engine = new StandardEngine();
+            engine.setName( "Tomcat" );
+            engine.setDefaultHost(hostname);
+            service.setContainer(engine);
+        }
+        return engine;
+    }
+
+    /**
+     * Get the server object. You can add listeners and few more
+     * customizations. JNDI is disabled by default.  
+     */
+    public StandardServer getServer() {
+        
+        if (server != null) {
+            return server;
+        }
+        
+        initBaseDir(); 
+        
+        System.setProperty("catalina.useNaming", "false");
+        
+        server = new StandardServer();
+        server.setPort( -1 );
+        
+        service = new StandardService();
+        service.setName("Tomcat");
+        server.addService( service );
+        return server;
+    }
+
+    public StandardContext addContext(StandardHost host, 
+                                      String contextPath, 
+                                      String dir) {
+        silence(contextPath);
+        StandardContext ctx = new StandardContext();
+        ctx.setPath( contextPath );
+        ctx.setDocBase(dir);
+        ctx.addLifecycleListener(new FixContextListener());
+        
+        if (host == null) {
+            getHost().addChild(ctx);
+        } else {
+            host.addChild(ctx);
+        }
+        return ctx;
+    }
+    
+    public StandardContext addWebapp(StandardHost host, 
+                                     String url, String path) {
+        silence(url);
+
+        StandardContext ctx = new StandardContext();
+        ctx.setPath( url );
+        ctx.setDocBase(path);
+        if (defaultRealm == null) {
+            initSimpleAuth();
+        }
+        ctx.setRealm(defaultRealm);
+        ctx.addLifecycleListener(new DefaultWebXmlListener());
+        
+        ContextConfig ctxCfg = new ContextConfig();
+        ctx.addLifecycleListener( ctxCfg );
+        // prevent it from looking ( if it finds one - it'll have dup error )
+        ctxCfg.setDefaultWebXml("org/apache/catalin/startup/NO_DEFAULT_XML");
+        
+        if (host == null) {
+            getHost().addChild(ctx);
+        } else {
+            host.addChild(ctx);
+        }
+
+        return ctx;
+    }
+
+    
+    
+
+    
+    // ---------- Helper methods and classes -------------------
+    
+    /** 
+     * Initialize an in-memory realm. You can replace it 
+     * for contexts with a real one. 
+     */
+    protected void initSimpleAuth() {
+        defaultRealm = new RealmBase() {
+            @Override
+            protected String getName() {
+                return "Simple";
+            }
+
+            @Override
+            protected String getPassword(String username) {
+                return userPass.get(username);
+            }
+
+            @Override
+            protected Principal getPrincipal(final String username) {
+                Principal p = userPrincipals.get(username);
+                if (p == null) {
+                    String pass = userPass.get(username);
+                    if (pass != null) {
+                        p = new Principal() {
+
+                            @Override
+                            public String getName() {
+                                return username;
+                            }
+                            
+                        };
+                    }
+                }
+                return p;
+            }
+            
+        };        
+    }
+    
+    protected void initBaseDir() {
+        if (basedir == null) {
+            basedir = System.getProperty("catalina.base");
+        }
+        if (basedir == null) {
+            basedir = System.getProperty("catalina.home");
+        }
+        if (basedir == null) {
+            // Create a temp dir.
+            basedir = System.getProperty("user.dir") + 
+                "/tomcat." + port;
+            File home = new File(basedir);
+            home.mkdir();
+            if (!home.isAbsolute()) {
+                try {
+                    basedir = home.getCanonicalPath();
+                } catch (IOException e) {
+                    basedir = home.getAbsolutePath();
+                }
+            }
+        }
+        System.setProperty("catalina.home", basedir);
+        System.setProperty("catalina.base", basedir);
+    }
+
+    static String[] silences = new String[] {
+        "org.apache.coyote.http11.Http11Protocol",
+        "org.apache.catalina.core.StandardService",
+        "org.apache.catalina.core.StandardEngine",
+        "org.apache.catalina.startup.ContextConfig",
+        "org.apache.catalina.core.ApplicationContext",
+        "org.apache.catalina.core.AprLifecycleListener"
+    };
+    
+    /**
+     * Controls if the loggers will be silenced or not.
+     * @param silent    <code>true</code> sets the log level to WARN for the
+     *                  loggers that log information on Tomcat start up. This
+     *                  prevents the usual startup information being logged.
+     *                  <code>false</code> sets the log level to the default
+     *                  level of INFO.
+     */
+    public void setSilent(boolean silent) {
+        for (String s : silences) {
+            if (silent) {
+                Logger.getLogger(s).setLevel(Level.WARNING);
+            } else {
+                Logger.getLogger(s).setLevel(Level.INFO);
+            }
+        }
+    }
+    
+    private void silence(String ctx) {
+        String base = "org.apache.catalina.core.ContainerBase.[default].[";
+        base += getHost().getName();
+        base += "].[";
+        base += ctx; 
+        base += "]";
+        Logger.getLogger(base).setLevel(Level.WARNING);
+    }
+    
+    /**
+     * Enables JNDI naming which is disabled by default.
+     */
+    public void enableNaming() {
+        // Make sure getServer() has been called as that is where naming is
+        // disabled
+        getServer();
+        
+        System.setProperty("catalina.useNaming", "true");
+        String value = "org.apache.naming";
+        String oldValue =
+            System.getProperty(javax.naming.Context.URL_PKG_PREFIXES);
+        if (oldValue != null) {
+            value = value + ":" + oldValue;
+        }
+        System.setProperty(javax.naming.Context.URL_PKG_PREFIXES, value);
+        value = System.getProperty
+            (javax.naming.Context.INITIAL_CONTEXT_FACTORY);
+        if (value == null) {
+            System.setProperty
+                (javax.naming.Context.INITIAL_CONTEXT_FACTORY,
+                 "org.apache.naming.java.javaURLContextFactory");
+        }
+    }
+
+    /**
+     * Provide default configuration for a context. This is the programmatic
+     * equivalent of the default web.xml. 
+     * 
+     *  TODO: in normal tomcat, if default-web.xml is not found, use this 
+     *  method
+     *  
+     * @param contextPath   The context to set the defaults for
+     */
+    public void initWebappDefaults(String contextPath) {
+        Container ctx = getHost().findChild(contextPath);
+        initWebappDefaults((StandardContext) ctx);
+    }
+    
+    /**
+     * Static version of {@link #initWebappDefaults(String)}
+     * @param ctx   The context to set the defaults for
+     */
+    public static void initWebappDefaults(StandardContext ctx) {
+        // Default servlet 
+        StandardWrapper servlet = addServlet(
+                ctx, "default", "org.apache.catalina.servlets.DefaultServlet");
+        servlet.setLoadOnStartup(1);
+
+        // JSP servlet (by class name - to avoid loading all deps)
+        servlet = addServlet(
+                ctx, "jsp", "org.apache.jasper.servlet.JspServlet");
+        servlet.addInitParameter("fork", "false");
+        servlet.setLoadOnStartup(3);
+        
+        // Servlet mappings 
+        ctx.addServletMapping("/", "default");
+        ctx.addServletMapping("*.jsp", "jsp");
+        ctx.addServletMapping("*.jspx", "jsp");
+
+        // Sessions
+        ctx.setManager( new StandardManager());
+        ctx.setSessionTimeout(30);
+        
+        // MIME mappings
+        for (int i = 0; i < DEFAULT_MIME_MAPPINGS.length; ) {
+            ctx.addMimeMapping(DEFAULT_MIME_MAPPINGS[i++],
+                    DEFAULT_MIME_MAPPINGS[i++]);
+        }
+        
+        // Welcome files
+        ctx.addWelcomeFile("index.html");
+        ctx.addWelcomeFile("index.htm");
+        ctx.addWelcomeFile("index.jsp");
+    }
+
+    
+    /**
+     * Fix startup sequence - required if you don't use web.xml.
+     * 
+     * The start() method in context will set 'configured' to false - and
+     * expects a listener to set it back to true.
+     */
+    public static class FixContextListener implements LifecycleListener {
+
+        public void lifecycleEvent(LifecycleEvent event) {
+            try {
+                Context context = (Context) event.getLifecycle();
+                if (event.getType().equals(Lifecycle.START_EVENT)) {
+                    context.setConfigured(true);
+                }
+            } catch (ClassCastException e) {
+                return;
+            }
+        }
+        
+    }
+
+
+    /**
+     * Fix reload - required if reloading and using programmatic configuration.
+     * When a context is reloaded, any programmatic configuration is lost. This
+     * listener sets the equivalent of conf/web.xml when the context starts. The
+     * context needs to be an instance of StandardContext for this listener to
+     * have any effect.
+     */
+    public static class DefaultWebXmlListener implements LifecycleListener {
+        public void lifecycleEvent(LifecycleEvent event) {
+            if (Lifecycle.BEFORE_START_EVENT.equals(event.getType()) &&
+                    event.getLifecycle() instanceof StandardContext) {
+                initWebappDefaults((StandardContext) event.getLifecycle());
+            }
+        }
+    }
+
+
+    /**
+     * Helper class for wrapping existing servlets. This disables servlet 
+     * lifecycle and normal reloading, but also reduces overhead and provide
+     * more direct control over the servlet.  
+     */
+    public static class ExistingStandardWrapper extends StandardWrapper {
+        private Servlet existing;
+        boolean init = false;
+        
+        public ExistingStandardWrapper( Servlet existing ) {
+            this.existing = existing;
+        }
+        @Override
+        public synchronized Servlet loadServlet() throws ServletException {
+            if (!init) {
+                existing.init(facade);
+                init = true;
+            }
+            return existing;
+
+        }
+        @Override
+        public long getAvailable() {
+            return 0;
+        }
+        @Override
+        public boolean isUnavailable() {
+            return false;       
+        }
+    }
+    
+    /**
+     * TODO: would a properties resource be better ? Or just parsing
+     * /etc/mime.types ?
+     * This is needed because we don't use the default web.xml, where this 
+     * is encoded.
+     */
+    public static final String[] DEFAULT_MIME_MAPPINGS = {
+        "abs", "audio/x-mpeg", 
+        "ai", "application/postscript", 
+        "aif", "audio/x-aiff", 
+        "aifc", "audio/x-aiff", 
+        "aiff", "audio/x-aiff", 
+        "aim", "application/x-aim", 
+        "art", "image/x-jg", 
+        "asf", "video/x-ms-asf", 
+        "asx", "video/x-ms-asf", 
+        "au", "audio/basic", 
+        "avi", "video/x-msvideo", 
+        "avx", "video/x-rad-screenplay", 
+        "bcpio", "application/x-bcpio", 
+        "bin", "application/octet-stream", 
+        "bmp", "image/bmp", 
+        "body", "text/html", 
+        "cdf", "application/x-cdf", 
+        "cer", "application/x-x509-ca-cert", 
+        "class", "application/java", 
+        "cpio", "application/x-cpio", 
+        "csh", "application/x-csh", 
+        "css", "text/css", 
+        "dib", "image/bmp", 
+        "doc", "application/msword", 
+        "dtd", "application/xml-dtd", 
+        "dv", "video/x-dv", 
+        "dvi", "application/x-dvi", 
+        "eps", "application/postscript", 
+        "etx", "text/x-setext", 
+        "exe", "application/octet-stream", 
+        "gif", "image/gif", 
+        "gtar", "application/x-gtar", 
+        "gz", "application/x-gzip", 
+        "hdf", "application/x-hdf", 
+        "hqx", "application/mac-binhex40", 
+        "htc", "text/x-component", 
+        "htm", "text/html", 
+        "html", "text/html", 
+        "hqx", "application/mac-binhex40", 
+        "ief", "image/ief", 
+        "jad", "text/vnd.sun.j2me.app-descriptor", 
+        "jar", "application/java-archive", 
+        "java", "text/plain", 
+        "jnlp", "application/x-java-jnlp-file", 
+        "jpe", "image/jpeg", 
+        "jpeg", "image/jpeg", 
+        "jpg", "image/jpeg", 
+        "js", "text/javascript", 
+        "jsf", "text/plain", 
+        "jspf", "text/plain", 
+        "kar", "audio/x-midi", 
+        "latex", "application/x-latex", 
+        "m3u", "audio/x-mpegurl", 
+        "mac", "image/x-macpaint", 
+        "man", "application/x-troff-man", 
+        "mathml", "application/mathml+xml", 
+        "me", "application/x-troff-me", 
+        "mid", "audio/x-midi", 
+        "midi", "audio/x-midi", 
+        "mif", "application/x-mif", 
+        "mov", "video/quicktime", 
+        "movie", "video/x-sgi-movie", 
+        "mp1", "audio/x-mpeg", 
+        "mp2", "audio/x-mpeg", 
+        "mp3", "audio/x-mpeg", 
+        "mp4", "video/mp4", 
+        "mpa", "audio/x-mpeg", 
+        "mpe", "video/mpeg", 
+        "mpeg", "video/mpeg", 
+        "mpega", "audio/x-mpeg", 
+        "mpg", "video/mpeg", 
+        "mpv2", "video/mpeg2", 
+        "ms", "application/x-wais-source", 
+        "nc", "application/x-netcdf", 
+        "oda", "application/oda", 
+        "odb", "application/vnd.oasis.opendocument.database", 
+        "odc", "application/vnd.oasis.opendocument.chart", 
+        "odf", "application/vnd.oasis.opendocument.formula", 
+        "odg", "application/vnd.oasis.opendocument.graphics", 
+        "odi", "application/vnd.oasis.opendocument.image", 
+        "odm", "application/vnd.oasis.opendocument.text-master", 
+        "odp", "application/vnd.oasis.opendocument.presentation", 
+        "ods", "application/vnd.oasis.opendocument.spreadsheet", 
+        "odt", "application/vnd.oasis.opendocument.text", 
+        "otg", "application/vnd.oasis.opendocument.graphics-template", 
+        "oth", "application/vnd.oasis.opendocument.text-web", 
+        "otp", "application/vnd.oasis.opendocument.presentation-template", 
+        "ots", "application/vnd.oasis.opendocument.spreadsheet-template ", 
+        "ott", "application/vnd.oasis.opendocument.text-template",
+        "ogx", "application/ogg",
+        "ogv", "video/ogg",
+        "oga", "audio/ogg",
+        "ogg", "audio/ogg",
+        "spx", "audio/ogg",
+        "faca", "audio/flac",
+        "anx", "application/annodex",
+        "axa", "audio/annodex",
+        "axv", "video/annodex",
+        "xspf", "application/xspf+xml",
+        "pbm", "image/x-portable-bitmap", 
+        "pct", "image/pict", 
+        "pdf", "application/pdf", 
+        "pgm", "image/x-portable-graymap", 
+        "pic", "image/pict", 
+        "pict", "image/pict", 
+        "pls", "audio/x-scpls", 
+        "png", "image/png", 
+        "pnm", "image/x-portable-anymap", 
+        "pnt", "image/x-macpaint", 
+        "ppm", "image/x-portable-pixmap", 
+        "ppt", "application/vnd.ms-powerpoint",
+        "pps", "application/vnd.ms-powerpoint",
+        "ps", "application/postscript", 
+        "psd", "image/x-photoshop", 
+        "qt", "video/quicktime", 
+        "qti", "image/x-quicktime", 
+        "qtif", "image/x-quicktime", 
+        "ras", "image/x-cmu-raster", 
+        "rdf", "application/rdf+xml", 
+        "rgb", "image/x-rgb", 
+        "rm", "application/vnd.rn-realmedia", 
+        "roff", "application/x-troff", 
+        "rtf", "application/rtf", 
+        "rtx", "text/richtext", 
+        "sh", "application/x-sh", 
+        "shar", "application/x-shar", 
+        /*"shtml", "text/x-server-parsed-html",*/
+        "smf", "audio/x-midi", 
+        "sit", "application/x-stuffit", 
+        "snd", "audio/basic", 
+        "src", "application/x-wais-source", 
+        "sv4cpio", "application/x-sv4cpio", 
+        "sv4crc", "application/x-sv4crc", 
+        "svg", "image/svg+xml", 
+        "svgz", "image/svg+xml", 
+        "swf", "application/x-shockwave-flash", 
+        "t", "application/x-troff", 
+        "tar", "application/x-tar", 
+        "tcl", "application/x-tcl", 
+        "tex", "application/x-tex", 
+        "texi", "application/x-texinfo", 
+        "texinfo", "application/x-texinfo", 
+        "tif", "image/tiff", 
+        "tiff", "image/tiff", 
+        "tr", "application/x-troff", 
+        "tsv", "text/tab-separated-values", 
+        "txt", "text/plain", 
+        "ulw", "audio/basic", 
+        "ustar", "application/x-ustar", 
+        "vxml", "application/voicexml+xml", 
+        "xbm", "image/x-xbitmap", 
+        "xht", "application/xhtml+xml", 
+        "xhtml", "application/xhtml+xml", 
+        "xls", "application/vnd.ms-excel", 
+        "xml", "application/xml", 
+        "xpm", "image/x-xpixmap", 
+        "xsl", "application/xml", 
+        "xslt", "application/xslt+xml", 
+        "xul", "application/vnd.mozilla.xul+xml", 
+        "xwd", "image/x-xwindowdump", 
+        "vsd", "application/x-visio", 
+        "wav", "audio/x-wav", 
+        "wbmp", "image/vnd.wap.wbmp", 
+        "wml", "text/vnd.wap.wml", 
+        "wmlc", "application/vnd.wap.wmlc", 
+        "wmls", "text/vnd.wap.wmlscript", 
+        "wmlscriptc", "application/vnd.wap.wmlscriptc", 
+        "wmv", "video/x-ms-wmv", 
+        "wrl", "x-world/x-vrml", 
+        "wspolicy", "application/wspolicy+xml", 
+        "Z", "application/x-compress", 
+        "z", "application/x-compress", 
+        "zip", "application/zip" 
+    };
+}
index f79c275..e165383 100644 (file)
@@ -10,8 +10,6 @@ import junit.framework.TestCase;
 
 import org.apache.catalina.LifecycleException;
 import org.apache.catalina.connector.Connector;
-import org.apache.catalina.startup.Tomcat;
-import org.apache.coyote.lite.LiteProtocolHandler;
 import org.apache.tomcat.lite.http.DefaultHttpConnector;
 import org.apache.tomcat.lite.http.HttpChannel;
 import org.apache.tomcat.lite.http.HttpConnector;
@@ -26,8 +24,9 @@ public class TomcatLiteCoyoteTest extends TestCase {
         if (tomcat == null) {
             try {
                 tomcat = new Tomcat();
+                
                 tomcat.setPort(8885);
-                tomcat.setBaseDir("output/build");
+                tomcat.setBaseDir("../../output/build/webapps");
                 
                 tomcat.addWebapp("/examples", "examples");
                 tomcat.addWebapp("/", "ROOT");
@@ -36,7 +35,7 @@ public class TomcatLiteCoyoteTest extends TestCase {
                 //tomcat.addServlet(ctx, "name", "class");
                 // ctx.addServletMapping("/foo/*", "name");
                 
-                litePH = setUp(tomcat);
+                litePH = setUp(tomcat, 8885);
                 
                 tomcat.start();
             } catch (LifecycleException e) {
@@ -49,14 +48,19 @@ public class TomcatLiteCoyoteTest extends TestCase {
         }
     }
     
-    public static LiteProtocolHandler setUp(Tomcat tomcat) {
-        Connector connector = new Connector(LiteProtocolHandler.class.getName());
-        tomcat.getService().addConnector(connector);
-        connector.setPort(8885);
-        tomcat.setConnector(connector);
-        LiteProtocolHandler ph = 
-            (LiteProtocolHandler) connector.getProtocolHandler();
-        return ph;
+    public static LiteProtocolHandler setUp(Tomcat tomcat, int port) {
+        Connector connector;
+        try {
+            connector = new Connector(LiteProtocolHandler.class.getName());
+            tomcat.getService().addConnector(connector);
+            connector.setPort(port);
+            tomcat.setConnector(connector);
+            LiteProtocolHandler ph = 
+                (LiteProtocolHandler) connector.getProtocolHandler();
+            return ph;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
     }
 
     
index fc3108d..035879c 100644 (file)
 package org.apache.tomcat.lite.load;
 
 
+import java.io.File;
 import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.management.InstanceNotFoundException;
+import javax.management.MBeanException;
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+import javax.management.ReflectionException;
 
 import junit.framework.TestCase;
 
 import org.apache.tomcat.lite.TestMain;
-import org.apache.tomcat.lite.http.DefaultHttpConnector;
 import org.apache.tomcat.lite.http.HttpChannel;
 import org.apache.tomcat.lite.http.HttpConnector;
 import org.apache.tomcat.lite.http.HttpRequest;
-import org.apache.tomcat.lite.http.HttpChannel.HttpService;
 import org.apache.tomcat.lite.http.HttpChannel.RequestCompleted;
+import org.apache.tomcat.lite.io.BBuffer;
+import org.apache.tomcat.lite.io.IOBuffer;
+
+/*
+  Notes on memory use ( from heap dumps ): 
+    - buffers are not yet recycled ( the BBuffers used in channels )
+  
+    - each active connection consumes at least 26k - 2 buffers + head buffer
+     ( 8k each )
+     TODO: could 'peak' in the In buffer and move headRecv to HttpChannel
+     
+     
+    - HttpChannel keeps about 64K ( for the hello world ).
+    -- res is 25k
+    -- req is 32k, BufferedIOReader 16k,
+    
+   TODO:
+    - leak in NioThread.active - closed sockets not removed
+    - need to rate-limit and queue requests - OOM
+    - timeouts
+    - seems few responses missing on large async requests (URL works)     
+ */
 
+/**
+ * Long running test - async tests are failing since rate control
+ * is not implemented ( too many outstanding requests - OOM ), 
+ * it seems there is a bug as well.
+ */
 public class LiveHttpThreadedTest extends TestCase {
-  HttpConnector staticMain = TestMain.initTestEnv();
+  HttpConnector clientCon = TestMain.getClientAndInit();
+  ThreadRunner tr;
+  static MBeanServer server;
   
+  AtomicInteger ok = new AtomicInteger();
+  Object lock = new Object();
+  int reqCnt;
+
+  Map<HttpRequest, HttpRequest> active = new HashMap();
   
-  int tCount = 1;
-  Thread[] threads = new Thread[tCount];
-  int[] ok = new int[tCount];
-  private int rCount = 100;
+  public void xtest1000Async() throws Exception {
+      try {
+          asyncRequest(10, 100);
+      } finally {
+          dumpHeap("heapAsync.bin");
+      }
+      
+  }
+
+  public void xtest10000Async() throws Exception {
+      try {
+          asyncRequest(20, 500);
+      } finally {
+          dumpHeap("heapAsync.bin");
+      }
+  }
   
-  public void xtestSimpleRequest() throws Exception {
-    long t0 = System.currentTimeMillis();
-    for (int i = 0; i < tCount; i++) {
-      final int j = i;
-      threads[i] = new Thread(new Runnable() {
-        public void run() {
-          makeRequests(j, true);
-        }
-      });
-      threads[i].start();
-    }
-    
-    int res = 0;
-    for (int i = 0; i < tCount; i++) {
-      threads[i].join();
-      res += ok[i];
-    }
-    long t1 = System.currentTimeMillis();
-    System.err.println("Time: " + (t1 - t0) + " " + res);
+  public void asyncRequest(int thr, int perthr) throws Exception {
+      reqCnt = thr * perthr;
+      long t0 = System.currentTimeMillis();
+      tr = new ThreadRunner(thr, perthr) {
+          public void makeRequest(int i) throws Exception {
+              HttpRequest cstate = clientCon.request("localhost", 8802);
+              synchronized (active) {
+                  active.put(cstate, cstate);
+              }
+              
+              cstate.requestURI().set("/hello");
+              cstate.setCompletedCallback(reqCallback);
+              
+              // Send the request, wait response
+              Thread.currentThread().sleep(20);
+              cstate.send();
+            }
+      };
+      tr.run();
+      assertEquals(0, tr.errors.get());
+      synchronized (lock) {
+          lock.wait(reqCnt * 100);
+      }
+      assertEquals(reqCnt, ok.get());
+      System.err.println(reqCnt + " Async requests: " + (System.currentTimeMillis() - t0));
+  }
+  
+  public void xtestURLRequest() throws Exception {
+      urlRequest(10, 200);
   }
 
-  public void testSimpleRequestNB() throws Exception {
-    long t0 = System.currentTimeMillis();
-    for (int i = 0; i < tCount; i++) {
-      final int j = i;
-      threads[i] = new Thread(new Runnable() {
-        public void run() {
-          makeRequests(j, false);
-        }
-      });
-      threads[i].start();
-    }
-    
-    int res = 0;
-    for (int i = 0; i < tCount; i++) {
-      threads[i].join();
-      res += ok[i];
-    }
-    long t1 = System.currentTimeMillis();
-    System.err.println("TimeNB: " + (t1 - t0) + " " + res);
+  public void testURLRequest2() throws Exception {
+      urlRequest(40, 500);
   }
   
-  void makeRequests(int t, boolean b) {
-    for (int i = 0; i < rCount ; i++) {
+  public void urlRequest(int thr, int cnt) throws Exception {
+          
+      
       try {
-        //System.err.println("MakeReq " + t + " " + i);
-        makeRequest(t, b);
-      } catch (Exception e) {
-        e.printStackTrace();
+          HttpConnector testServer = TestMain.testServer;
+          
+          tr = new ThreadRunner(thr, cnt) {
+
+              public void makeRequest(int i) throws Exception {
+                  try {
+                      URL url = new URL("http://localhost:8802/hello");
+                      HttpURLConnection con = (HttpURLConnection) url.openConnection();
+                      con.setReadTimeout(4000000);
+                      con.connect();
+                      if (con.getResponseCode() != 200) {
+                          errors.incrementAndGet();
+                      }
+                      BBuffer out = new IOBuffer().append(con.getInputStream()).copyAll(null);
+                      if (!"Hello world".equals(out.toString())) {
+                          errors.incrementAndGet();
+                          System.err.println("bad result " + out);
+                      }
+                  } catch(Throwable t) {
+                      t.printStackTrace();
+                      errors.incrementAndGet();
+                  }
+              }
+          };
+          tr.run();
+          assertEquals(0, tr.errors.get());
+          
+          //assertEquals(testServer., actual)
+      } finally {
+          dumpHeap("heapURLReq.bin");
       }
-    }
   }
+  
+  // TODO: move to a servlet
+  private void dumpHeap(String file) throws InstanceNotFoundException,
+      MBeanException, ReflectionException, MalformedObjectNameException {
+
+      if (server == null) {
+          server = ManagementFactory.getPlatformMBeanServer();
+          
+      }
+      File f1 = new java.io.File(file);
+      if (f1.exists()) {
+          f1.delete();
+      }
+      server.invoke(new ObjectName("com.sun.management:type=HotSpotDiagnostic"),
+              "dumpHeap",
+              new Object[] {file, Boolean.FALSE /* live */}, 
+              new String[] {String.class.getName(), "boolean"});
+  }
+
 
-  static RequestCompleted reqCallback = new RequestCompleted() {
+  RequestCompleted reqCallback = new RequestCompleted() {
     @Override
     public void handle(HttpChannel data, Object extraData) 
         throws IOException {
-        //dumpHead(cstate);  
-        //System.err.println("DATA\n" + cstate.output.toString() + "\n----");
-        //assertTrue(cstate.bodyRecvBuffer.toString().indexOf("AAA") >= 0);
-    
+        String out = data.getIn().copyAll(null).toString();
+        if (200 != data.getResponse().getStatus()) {
+            System.err.println("Wrong status");
+            tr.errors.incrementAndGet();            
+        }
+        if (!"Hello world".equals(out)) {
+            tr.errors.incrementAndGet();
+            System.err.println("bad result " + out);
+        }        
+        synchronized (active) {
+            active.remove(data.getRequest());
+        }
         data.release();
+        int okres = ok.incrementAndGet();
+        if (okres >= reqCnt) {
+            synchronized (lock) {
+                lock.notify();
+            }
+        }
     }
-      
   };
   
-  void makeRequest(int i, boolean block) throws Exception {
-    HttpRequest cstate = DefaultHttpConnector.get().request("localhost", 8802);
-    
-    cstate.requestURI().set("/hello");
-    cstate.setCompletedCallback(reqCallback);
-    
-    // Send the request, wait response
-    cstate.send();
-  }
   
 }
diff --git a/modules/tomcat-lite/test/org/apache/tomcat/lite/load/ThreadRunner.java b/modules/tomcat-lite/test/org/apache/tomcat/lite/load/ThreadRunner.java
new file mode 100644 (file)
index 0000000..8f61ead
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ */
+package org.apache.tomcat.lite.load;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class ThreadRunner {
+    int tCount = 10;
+    int rCount = 100;
+    Thread[] threads;
+    int[] ok;
+    
+    int sleepTime = 0;
+    
+    long time;
+    protected AtomicInteger errors = new AtomicInteger();
+    
+    public ThreadRunner(int threads, int count) {
+        tCount = threads;
+        rCount = count;
+        this.threads = new Thread[tCount];
+        ok = new int[tCount];
+    }
+
+    public void run() {
+        long t0 = System.currentTimeMillis();
+        for (int i = 0; i < tCount; i++) {
+          final int j = i;
+          threads[i] = new Thread(new Runnable() {
+            public void run() {
+              makeRequests(j);
+            }
+          });
+          threads[i].start();
+        }
+        
+        int res = 0;
+        for (int i = 0; i < tCount; i++) {
+          try {
+            threads[i].join();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+          res += ok[i];
+        }
+        long t1 = System.currentTimeMillis();
+        time = t1 - t0;
+        System.err.println("TimeNB: " + (t1 - t0) + " " + res);
+    }
+    
+    public void makeRequests(int cnt) {
+        for (int i = 0; i < rCount ; i++) {
+            try {
+              //System.err.println("MakeReq " + t + " " + i);
+              makeRequest(cnt);
+            } catch (Exception e) {
+              e.printStackTrace();
+            }
+          }
+    }
+    
+    public void makeRequest(int i) throws Exception {
+        
+    }
+}
\ No newline at end of file
index 4b75826..a306b3c 100644 (file)
@@ -30,6 +30,7 @@ import org.apache.tomcat.test.watchdog.WatchdogClient;
 public abstract class TomcatLiteWatchdog extends WatchdogClient {
   
   public TomcatLiteWatchdog() {
+      super();
       goldenDir = getWatchdogdir() + "/src/clients/org/apache/jcheck/servlet/client/";
       testMatch = 
           //"HttpServletResponseWrapperSetStatusMsgTest";
@@ -40,6 +41,11 @@ public abstract class TomcatLiteWatchdog extends WatchdogClient {
       targetMatch = "gtestservlet-test";
   }
   
+  public TomcatLiteWatchdog(String s) {
+      this();
+      super.single = s;
+  }
+  
   protected void beforeSuite() {
       // required for the tests
       System.setProperty("org.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER",
@@ -58,10 +64,10 @@ public abstract class TomcatLiteWatchdog extends WatchdogClient {
   
   protected abstract void addConnector(TomcatLite liteServer);
   
-  TomcatLite tomcatForWatchdog;
-  
   public void initServerWithWatchdog(String wdDir) throws ServletException, 
           IOException {
+      TomcatLite tomcatForWatchdog;
+      
       File f = new File(wdDir + "/build/webapps");
       
       tomcatForWatchdog = new TomcatLite();
index ac10f84..8f39d35 100644 (file)
@@ -33,7 +33,7 @@ import org.w3c.dom.Element;
 import org.w3c.dom.NodeList;
 import org.xml.sax.SAXException;
 
-public class WatchdogClient {
+public class WatchdogClient implements Test {
         
   protected String goldenDir; 
   protected String testMatch;
@@ -45,8 +45,9 @@ public class WatchdogClient {
   };  
 
   protected String targetMatch;
-    
-
+  
+  protected int port;
+  
   Properties props = new Properties();
   
   protected void beforeSuite() {
@@ -56,7 +57,7 @@ public class WatchdogClient {
   }
   
   public Test getSuite() {
-      return getSuite(8080);
+      return getSuite(port);
   }
   
   /** 
@@ -95,6 +96,9 @@ public class WatchdogClient {
         for (int j = 0; j < watchDogL.getLength(); j++) {
           Element watchE = (Element) watchDogL.item(j);
           String testName = watchE.getAttribute("testName");
+          if (single != null && !testName.equals(single)) {
+              continue;
+          }
           if (testMatch != null) {
               if (!testName.startsWith(testMatch)) {
                   continue;
@@ -112,9 +116,13 @@ public class WatchdogClient {
                   continue;
               }
           }
-          testName = testName + ";" + this.getClass().getName();
+          testName = testName + "(" + this.getClass().getName() + ")";
           WatchdogTestCase test = new WatchdogTestCase(watchE, props, testName);
           tests.addTest(test);
+          if (single != null) {
+              singleTest = test;
+              break;
+          }
         }
       }
       
@@ -154,4 +162,24 @@ public class WatchdogClient {
       }
   }
 
+  // Support for running a single test in the suite
+  
+  protected String single;
+  WatchdogTestCase singleTest;
+  
+  @Override
+  public int countTestCases() {
+      return 1;
+  }
+
+  @Override
+  public void run(TestResult result) {
+      getSuite();
+      if (singleTest != null) {
+          beforeSuite();
+          singleTest.run(result);
+          afterSuite(result);
+      }
+  }
+
 }
index 3100737..f4b71d0 100644 (file)
@@ -52,7 +52,7 @@ public class WatchdogHttpClient {
             System.out.println( " Socket Exception: " + ex );
             return;
         }
-        //socket.setSoTimeout(2000);
+        socket.setSoTimeout(10000);
         
         //socket obtained, rebuild the request.
         rebuildRequest(client, client.request, socket);
index 3486a80..e5cf561 100644 (file)
@@ -7,7 +7,6 @@ import java.util.Properties;
 import junit.framework.AssertionFailedError;
 import junit.framework.Test;
 import junit.framework.TestResult;
-import junit.framework.TestSuite;
 
 import org.apache.tomcat.integration.DynamicObject;
 import org.apache.tomcat.integration.simple.AntProperties;
@@ -22,30 +21,12 @@ public class WatchdogTestCase implements Test {
 
     private Properties props;
 
-    private WatchdogTestCase delegate;
     private WatchdogClient wc;
     
-    public WatchdogTestCase(String s) throws Throwable {
-        String[] comp = s.split(";");
-        if (comp.length < 2) {
-            return;
-        }
-        Class c = Class.forName(comp[1]);
-        wc = (WatchdogClient) c.newInstance();
-        TestSuite suite = (TestSuite) wc.getSuite();
-        // need to encode the base, file, etc in the test name
-
-        System.err.println(s);
-
-        for (int i = 0; i < suite.testCount(); i++) {
-            WatchdogTestCase t = (WatchdogTestCase) suite.testAt(i);
-            if (s.equals(t.getName())) {
-                delegate = t;
-                return;
-            }
-        }
+    public WatchdogTestCase() {
+        
     }
-
+    
     public WatchdogTestCase(Element watchE, Properties props, String testName) {
         this.testName = testName;
         this.watchE = watchE;
@@ -57,20 +38,17 @@ public class WatchdogTestCase implements Test {
     }
 
     public String getName() {
-        return testName;
+        return testName == null ? "WatchdogTest" : testName;
     }
 
+    public String toString() {
+        return getName();
+    }
+    
     public void testDummy() {
     }
     
     public void run(TestResult res) {
-        if (delegate != null) {
-            // Single method run
-            wc.beforeSuite();
-            delegate.run(res);
-            wc.afterSuite(res);
-            return;
-        }
         if (watchE == null) {
             res.endTest(this);
             return;