The http implementation - it may be hard to recognize the original connector code...
authorcostin <costin@13f79535-47bb-0310-9956-ffa450edef68>
Thu, 26 Nov 2009 06:41:00 +0000 (06:41 +0000)
committercostin <costin@13f79535-47bb-0310-9956-ffa450edef68>
Thu, 26 Nov 2009 06:41:00 +0000 (06:41 +0000)
Changes compared with coyote:
- both server and client mode
- HttpRequest/HttpResponse implement most of methods in the HttpServletRequest - with the addition of setters, for use
in client mode. They don't implement the interfaces - or 'servlet framework' specific methods - but should look
familiar to people using this as a library
- mapping is moved in this package, also support running HttpServices in the selector thread (proxy will run this way)
- MimeHeaders are gone, so are the parameters - replaced with the MultiMap, which is based on MimeHeaders but adds a HashMap
instead of linear scanning
See tests for examples.

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

16 files changed:
modules/tomcat-lite/java/org/apache/tomcat/lite/http/BaseMapper.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/ContentType.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/DefaultHttpConnector.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/Dispatcher.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/FutureCallbacks.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpBody.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpChannel.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpConnector.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpMessage.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpRequest.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpResponse.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpWriter.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/MappingData.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/MultiMap.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/ServerCookie.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/http/package.html [new file with mode: 0644]

diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/BaseMapper.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/BaseMapper.java
new file mode 100644 (file)
index 0000000..7e0e392
--- /dev/null
@@ -0,0 +1,1092 @@
+/*
+ *  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.tomcat.lite.http;
+
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import org.apache.tomcat.lite.io.CBuffer;
+import org.apache.tomcat.lite.io.FileConnector;
+import org.apache.tomcat.lite.io.BBucket;
+
+/**
+ * Mapper, which implements the servlet API mapping rules (which are derived
+ * from the HTTP rules).
+ * 
+ * This class doesn't use JNDI.
+ */
+public class BaseMapper {
+
+    private static Logger logger =
+        Logger.getLogger(BaseMapper.class.getName());
+
+    // TODO:
+    /**
+     * Mapping should be done on bytes - as received from net, before 
+     * translation to chars. This would allow setting the default charset
+     * for the context - or even executing the servlet and letting it specify 
+     * the charset to use for further decoding.
+     * 
+     */
+    public static interface Mapper {
+        public void map(BBucket host, BBucket url, MappingData md);
+    }
+
+    
+    /** 
+     * Like BaseMapper, for a Context.
+     */
+    public static class ServiceMapper extends BaseMapper {
+        /**
+         * Context associated with this wrapper, used for wrapper mapping.
+         */
+        public BaseMapper.ContextMapping contextMapElement = new BaseMapper.ContextMapping();
+
+        /**
+         * Set context, used for wrapper mapping (request dispatcher).
+         *
+         * @param welcomeResources Welcome files defined for this context
+         */
+        public void setContext(String path, String[] welcomeResources) {
+            contextMapElement.name = path;
+            contextMapElement.welcomeResources = welcomeResources;
+        }
+
+
+        /**
+         * Add a wrapper to the context associated with this wrapper.
+         *
+         * @param path Wrapper mapping
+         * @param wrapper The Wrapper object
+         */
+        public void addWrapper(String path, Object wrapper) {
+            addWrapper(contextMapElement, path, wrapper);
+        }
+
+
+        public void addWrapper(String path, Object wrapper, boolean jspWildCard) {
+            addWrapper(contextMapElement, path, wrapper, jspWildCard);
+        }
+
+        
+
+        /**
+         * Remove a wrapper from the context associated with this wrapper.
+         *
+         * @param path Wrapper mapping
+         */
+        public void removeWrapper(String path) {
+            removeWrapper(contextMapElement, path);
+        }
+
+
+//        /**
+//         * Map the specified URI relative to the context,
+//         * mutating the given mapping data.
+//         *
+//         * @param uri URI
+//         * @param mappingData This structure will contain the result of the mapping
+//         *                    operation
+//         */
+//        public void map(CBuffer uri, MappingData mappingData)
+//            throws Exception {
+//
+//           CBuffer uricc = uri.getCharBuffer();
+//           internalMapWrapper(contextMapElement, uricc, mappingData);
+//
+//        }
+    }
+
+    /**
+     * Array containing the virtual hosts definitions.
+     */
+    Host[] hosts = new Host[0];
+    
+    /** 
+     * If no other host is found. 
+     * For single-host servers ( most common ) this is the only one 
+     * used.
+     */
+    Host defaultHost = new Host();
+
+    public BaseMapper() {
+        defaultHost.contextList = new ContextList();
+    }
+
+    // --------------------------------------------------------- Public Methods
+
+    public synchronized Host addHost(String name) {
+        if (name == null) {
+            name = "localhost";
+        }
+        Host[] newHosts = new Host[hosts.length + 1];
+        Host newHost = new Host();
+        newHost.name = name;
+        newHost.contextList = new ContextList();
+        
+        if (insertMap(hosts, newHosts, newHost)) {
+            hosts = newHosts;
+        }
+        return newHost;
+    }
+
+
+    /**
+     * Remove a host from the mapper.
+     *
+     * @param name Virtual host name
+     */
+    public synchronized void removeHost(String name) {
+        // Find and remove the old host
+        int pos = find(hosts, name);
+        if (pos < 0) {
+            return;
+        }
+        Object host = hosts[pos].object;
+        Host[] newHosts = new Host[hosts.length - 1];
+        if (removeMap(hosts, newHosts, name)) {
+            hosts = newHosts;
+        }
+        // Remove all aliases (they will map to the same host object)
+        for (int i = 0; i < newHosts.length; i++) {
+            if (newHosts[i].object == host) {
+                Host[] newHosts2 = new Host[hosts.length - 1];
+                if (removeMap(hosts, newHosts2, newHosts[i].name)) {
+                    hosts = newHosts2;
+                }
+            }
+        }
+    }
+
+    /**
+     * Add an alias to an existing host.
+     * @param name  The name of the host
+     * @param alias The alias to add
+     */
+    public synchronized void addHostAlias(String name, String alias) {
+        int pos = find(hosts, name);
+        if (pos < 0) {
+            // Should not be adding an alias for a host that doesn't exist but
+            // just in case...
+            return;
+        }
+        Host realHost = hosts[pos];
+        
+        Host[] newHosts = new Host[hosts.length + 1];
+        Host newHost = new Host();
+        newHost.name = alias;
+        newHost.contextList = realHost.contextList;
+        newHost.object = realHost;
+        if (insertMap(hosts, newHosts, newHost)) {
+            hosts = newHosts;
+        }
+    }
+
+    private Host getHost(String host) {
+        return getHost(CBuffer.newInstance().append(host));
+    }
+    
+    private Host getHost(CBuffer host) {
+        if (hosts == null || hosts.length <= 1 || host == null 
+                || host.length() == 0 || host.equals("")) {
+            return defaultHost;
+        } else {
+            Host[] hosts = this.hosts;
+            // TODO: if hosts.length == 1 or defaultHost ? 
+            int pos = findIgnoreCase(hosts, host);
+            if ((pos != -1) && (host.equalsIgnoreCase(hosts[pos].name))) {
+                return hosts[pos];
+            } else {
+                return defaultHost;
+            }
+        }
+    }
+
+    private Host getOrCreateHost(String hostName) {
+        Host host = getHost(CBuffer.newInstance().append(hostName));
+        if (host == null) {
+            host = addHost(hostName);
+        }
+        return host;
+    }
+
+    // Contexts
+
+    /**
+     * Add a new Context to an existing Host.
+     *
+     * @param hostName Virtual host name this context belongs to
+     * @param path Context path
+     * @param context Context object
+     * @param welcomeResources Welcome files defined for this context
+     * @param resources Static resources of the context
+     * @param ctxService 
+     */
+    public BaseMapper.ContextMapping addContext(String hostName, String path, Object context,
+            String[] welcomeResources, FileConnector resources, 
+            HttpChannel.HttpService ctxService) {
+
+        if (path == null) {
+            path = "/";
+        }
+        
+        Host host = getOrCreateHost(hostName);
+        
+        int slashCount = slashCount(path);
+        synchronized (host) {
+            BaseMapper.ContextMapping[] contexts = host.contextList.contexts;
+            // Update nesting
+            if (slashCount > host.contextList.nesting) {
+                host.contextList.nesting = slashCount;
+            }
+            BaseMapper.ContextMapping[] newContexts = new BaseMapper.ContextMapping[contexts.length + 1];
+            BaseMapper.ContextMapping newContext = new BaseMapper.ContextMapping();
+            newContext.name = path;
+            newContext.object = context;
+            if (welcomeResources != null) {
+                newContext.welcomeResources = welcomeResources;
+            }
+            newContext.resources = resources;
+            if (ctxService != null) {
+                newContext.defaultWrapper = new BaseMapper.ServiceMapping();
+                newContext.defaultWrapper.object = ctxService;
+            }
+
+            if (insertMap(contexts, newContexts, newContext)) {
+                host.contextList.contexts = newContexts;
+            }
+            return newContext;
+        }
+
+    }
+
+
+    /**
+     * Remove a context from an existing host.
+     *
+     * @param hostName Virtual host name this context belongs to
+     * @param path Context path
+     */
+    public void removeContext(String hostName, String path) {
+        Host host = getHost(hostName);
+        synchronized (host) {
+            BaseMapper.ContextMapping[] contexts = host.contextList.contexts;
+            if( contexts.length == 0 ){
+                return;
+            }
+            BaseMapper.ContextMapping[] newContexts = new BaseMapper.ContextMapping[contexts.length - 1];
+            if (removeMap(contexts, newContexts, path)) {
+                host.contextList.contexts = newContexts;
+                // Recalculate nesting
+                host.contextList.nesting = 0;
+                for (int i = 0; i < newContexts.length; i++) {
+                    int slashCount = slashCount(newContexts[i].name);
+                    if (slashCount > host.contextList.nesting) {
+                        host.contextList.nesting = slashCount;
+                    }
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Add a new Wrapper to an existing Context.
+     *
+     * @param hostName Virtual host name this wrapper belongs to
+     * @param contextPath Context path this wrapper belongs to
+     * @param path Wrapper mapping
+     * @param wrapper Wrapper object
+     */
+    public void addWrapper(String hostName, String contextPath, String path,
+                           Object wrapper) {
+        addWrapper(hostName, contextPath, path, wrapper, false);
+    }
+
+
+    public void addWrapper(String hostName, String contextPath, String path,
+                           Object wrapper, boolean jspWildCard) {
+        Host host = getHost(hostName);
+        BaseMapper.ContextMapping[] contexts = host.contextList.contexts;
+        int pos2 = find(contexts, contextPath);
+        if( pos2<0 ) {
+            logger.severe("No context found: " + contextPath );
+            return;
+        }
+        BaseMapper.ContextMapping context = contexts[pos2];
+        if (context.name.equals(contextPath)) {
+            addWrapper(context, path, wrapper, jspWildCard);
+        }
+    }
+
+
+    public void addWrapper(BaseMapper.ContextMapping context, String path, Object wrapper) {
+        addWrapper(context, path, wrapper, false);
+    }
+
+
+    /**
+     * Adds a wrapper to the given context.
+     *
+     * @param context The context to which to add the wrapper
+     * @param path Wrapper mapping
+     * @param wrapper The Wrapper object
+     * @param jspWildCard true if the wrapper corresponds to the JspServlet
+     * and the mapping path contains a wildcard; false otherwise
+     */
+    protected void addWrapper(BaseMapper.ContextMapping context, String path, Object wrapper,
+                              boolean jspWildCard) {
+
+        synchronized (context) {
+            BaseMapper.ServiceMapping newWrapper = new BaseMapper.ServiceMapping();
+            newWrapper.object = wrapper;
+            newWrapper.jspWildCard = jspWildCard;
+            if (path.endsWith("/*")) {
+                // Wildcard wrapper
+                newWrapper.name = path.substring(0, path.length() - 2);
+                BaseMapper.ServiceMapping[] oldWrappers = context.wildcardWrappers;
+                BaseMapper.ServiceMapping[] newWrappers =
+                    new BaseMapper.ServiceMapping[oldWrappers.length + 1];
+                if (insertMap(oldWrappers, newWrappers, newWrapper)) {
+                    context.wildcardWrappers = newWrappers;
+                    int slashCount = slashCount(newWrapper.name);
+                    if (slashCount > context.nesting) {
+                        context.nesting = slashCount;
+                    }
+                }
+            } else if (path.startsWith("*.")) {
+                // Extension wrapper
+                newWrapper.name = path.substring(2);
+                BaseMapper.ServiceMapping[] oldWrappers = context.extensionWrappers;
+                BaseMapper.ServiceMapping[] newWrappers =
+                    new BaseMapper.ServiceMapping[oldWrappers.length + 1];
+                if (insertMap(oldWrappers, newWrappers, newWrapper)) {
+                    context.extensionWrappers = newWrappers;
+                }
+            } else if (path.equals("/")) {
+                // Default wrapper
+                newWrapper.name = "";
+                context.defaultWrapper = newWrapper;
+            } else {
+                // Exact wrapper
+                newWrapper.name = path;
+                BaseMapper.ServiceMapping[] oldWrappers = context.exactWrappers;
+                BaseMapper.ServiceMapping[] newWrappers =
+                    new BaseMapper.ServiceMapping[oldWrappers.length + 1];
+                if (insertMap(oldWrappers, newWrappers, newWrapper)) {
+                    context.exactWrappers = newWrappers;
+                }
+            }
+        }
+    }
+
+    /**
+     * Remove a wrapper from an existing context.
+     *
+     * @param hostName Virtual host name this wrapper belongs to
+     * @param contextPath Context path this wrapper belongs to
+     * @param path Wrapper mapping
+     */
+    public void removeWrapper(String hostName, String contextPath, 
+                              String path) {
+        Host host = getHost(hostName);
+        BaseMapper.ContextMapping[] contexts = host.contextList.contexts;
+        int pos2 = find(contexts, contextPath);
+        if (pos2 < 0) {
+            return;
+        }
+        BaseMapper.ContextMapping context = contexts[pos2];
+        if (context.name.equals(contextPath)) {
+            removeWrapper(context, path);
+        }
+    }
+
+    protected void removeWrapper(BaseMapper.ContextMapping context, String path) {
+        synchronized (context) {
+            if (path.endsWith("/*")) {
+                // Wildcard wrapper
+                String name = path.substring(0, path.length() - 2);
+                BaseMapper.ServiceMapping[] oldWrappers = context.wildcardWrappers;
+                BaseMapper.ServiceMapping[] newWrappers =
+                    new BaseMapper.ServiceMapping[oldWrappers.length - 1];
+                if (removeMap(oldWrappers, newWrappers, name)) {
+                    // Recalculate nesting
+                    context.nesting = 0;
+                    for (int i = 0; i < newWrappers.length; i++) {
+                        int slashCount = slashCount(newWrappers[i].name);
+                        if (slashCount > context.nesting) {
+                            context.nesting = slashCount;
+                        }
+                    }
+                    context.wildcardWrappers = newWrappers;
+                }
+            } else if (path.startsWith("*.")) {
+                // Extension wrapper
+                String name = path.substring(2);
+                BaseMapper.ServiceMapping[] oldWrappers = context.extensionWrappers;
+                BaseMapper.ServiceMapping[] newWrappers =
+                    new BaseMapper.ServiceMapping[oldWrappers.length - 1];
+                if (removeMap(oldWrappers, newWrappers, name)) {
+                    context.extensionWrappers = newWrappers;
+                }
+            } else if (path.equals("/")) {
+                // Default wrapper
+                context.defaultWrapper = null;
+            } else {
+                // Exact wrapper
+                String name = path;
+                BaseMapper.ServiceMapping[] oldWrappers = context.exactWrappers;
+                BaseMapper.ServiceMapping[] newWrappers =
+                    new BaseMapper.ServiceMapping[oldWrappers.length - 1];
+                if (removeMap(oldWrappers, newWrappers, name)) {
+                    context.exactWrappers = newWrappers;
+                }
+            }
+        }
+    }
+
+    /**
+     * Map the specified host name and URI, mutating the given mapping data.
+     *
+     * @param host Virtual host name
+     * @param uri URI
+     * @param mappingData This structure will contain the result of the mapping
+     *                    operation
+     */
+    public void map(CBuffer host, CBuffer uri,
+                    MappingData mappingData)
+        throws Exception {
+
+        internalMap(host.length() == 0 ? null : 
+            host, uri, mappingData);
+    }
+
+
+    // -------------------------------------------------------- Private Methods
+
+    // public Context mapContext(CBuffer host, CBuffer url);
+
+    /**
+     * Map the specified URI.
+     */
+    private final void internalMap(CBuffer host, CBuffer uri,
+                                   MappingData mappingData)
+        throws Exception {
+        BaseMapper.ContextMapping[] contexts = null;
+        BaseMapper.ContextMapping context = null;
+        int nesting = 0;
+
+        // Virtual host mapping
+        Host mappedHost = getHost(host); 
+        contexts = mappedHost.contextList.contexts;
+        nesting = mappedHost.contextList.nesting;
+
+        // Context mapping
+        if (contexts.length == 0) {
+            return;
+        }
+        
+        if (mappingData.context == null) {
+            if (nesting < 1 || contexts.length == 1 && "".equals(contexts[0].name)) {
+                // if 1 context (default) -> fast return
+                context = contexts[0];
+            } else if (nesting == 1) {
+                // if all contexts are 1-component-only
+                int nextSlash = uri.indexOf('/', 1);
+                if (nextSlash == -1) {
+                  nextSlash = uri.length();
+                }
+                mappingData.contextPath.set(uri, 0, nextSlash);
+                int pos = find(contexts, uri);
+                if (pos == -1) {
+                        pos = find(contexts, "/");
+                }
+                if (pos >= 0) {
+                    context = contexts[pos];
+                }
+            } else {
+                int pos = find(contexts, uri);
+                if (pos >= 0) {
+                    int lastSlash = -1;
+                    int length = -1;
+                    boolean found = false;
+                    CBuffer tmp = mappingData.tmpPrefix;
+                    tmp.wrap(uri, 0, uri.length());
+                    
+                    while (pos >= 0) {
+                        if (tmp.startsWith(contexts[pos].name)) {
+                            length = contexts[pos].name.length();
+                            if (tmp.length() == length) {
+                                found = true;
+                                break;
+                            } else if (tmp.startsWithIgnoreCase("/", length)) {
+                                found = true;
+                                break;
+                            }
+                        }
+                        if (lastSlash == -1) {
+                            lastSlash = tmp.nthSlash(nesting + 1);
+                        } else {
+                            lastSlash = tmp.lastIndexOf('/');
+                        }
+                        tmp.delete(lastSlash);
+                        pos = find(contexts, tmp);
+                    }
+
+                    if (!found) {
+                        if (contexts[0].name.equals("")) {
+                            context = contexts[0];
+                        }
+                    } else {
+                        context = contexts[pos];
+                    }
+                }
+            }
+            
+            if (context != null) {
+                mappingData.context = context.object;
+                mappingData.contextPath.set(context.name);
+            }
+        }
+
+        // Wrapper mapping
+        if ((context != null) && (mappingData.getServiceObject() == null)) {
+            internalMapWrapper(context, uri, mappingData);
+        }
+
+    }
+
+
+    /**
+     * Wrapper mapping, using servlet rules.
+     */
+    protected final void internalMapWrapper(
+            BaseMapper.ContextMapping context, 
+            CBuffer url,
+            MappingData mappingData)
+        throws Exception {
+
+        boolean noServletPath = false;
+        if (url.length() < context.name.length()) {
+            throw new IOException("Invalid mapping " + context.name + " " + 
+                    url);
+        }
+
+        try {
+            
+        mappingData.tmpServletPath.set(url, 
+                context.name.length(),
+                url.length() - context.name.length());
+        if (mappingData.tmpServletPath.length() == 0) {
+            mappingData.tmpServletPath.append('/');
+            noServletPath = true;
+        }
+
+        mapAfterContext(context, url, mappingData.tmpServletPath, mappingData, 
+                noServletPath);
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            System.err.println(1);
+        }
+    }
+
+    void mapAfterContext(BaseMapper.ContextMapping context, 
+            CBuffer url, CBuffer urlNoContext, 
+            MappingData mappingData, boolean noServletPath) 
+        throws Exception {
+
+
+        // Rule 1 -- Exact Match
+        BaseMapper.ServiceMapping[] exactWrappers = context.exactWrappers;
+        internalMapExactWrapper(exactWrappers, urlNoContext, mappingData);
+
+        // Rule 2 -- Prefix Match
+        boolean checkJspWelcomeFiles = false;
+        BaseMapper.ServiceMapping[] wildcardWrappers = context.wildcardWrappers;
+        if (mappingData.getServiceObject() == null) {
+
+            internalMapWildcardWrapper(wildcardWrappers, context.nesting, 
+                                       urlNoContext, mappingData);
+            
+            if (mappingData.getServiceObject() != null 
+                    && mappingData.service.jspWildCard) {
+                if (urlNoContext.lastChar() == '/') {
+                    /*
+                     * Path ending in '/' was mapped to JSP servlet based on
+                     * wildcard match (e.g., as specified in url-pattern of a
+                     * jsp-property-group.
+                     * Force the context's welcome files, which are interpreted
+                     * as JSP files (since they match the url-pattern), to be
+                     * considered. See Bugzilla 27664.
+                     */ 
+                    mappingData.service = null;
+                    checkJspWelcomeFiles = true;
+                } else {
+                    // See Bugzilla 27704
+                    mappingData.wrapperPath.set(urlNoContext);
+                    mappingData.pathInfo.recycle();
+                }
+            }
+        }
+
+        if(mappingData.getServiceObject() == null && noServletPath) {
+            // The path is empty, redirect to "/"
+            mappingData.redirectPath.set(context.name);
+            mappingData.redirectPath.append("/");
+            return;
+        }
+
+        // Rule 3 -- Extension Match
+        BaseMapper.ServiceMapping[] extensionWrappers = context.extensionWrappers;
+        if (mappingData.getServiceObject() == null && !checkJspWelcomeFiles) {
+            internalMapExtensionWrapper(extensionWrappers, urlNoContext, mappingData);
+        }
+
+        // Rule 4 -- Welcome resources processing for servlets
+        if (mappingData.getServiceObject() == null) {
+            boolean checkWelcomeFiles = checkJspWelcomeFiles;
+            if (!checkWelcomeFiles) {
+                checkWelcomeFiles = (urlNoContext.lastChar() == '/');
+            }
+            if (checkWelcomeFiles) {
+                for (int i = 0; (i < context.welcomeResources.length)
+                         && (mappingData.getServiceObject() == null); i++) {
+                    
+                    CBuffer wpath = mappingData.tmpWelcome;
+                    wpath.set(urlNoContext);
+                    wpath.append(context.welcomeResources[i]);
+
+                    // Rule 4a -- Welcome resources processing for exact macth
+                    internalMapExactWrapper(exactWrappers, urlNoContext, mappingData);
+
+                    // Rule 4b -- Welcome resources processing for prefix match
+                    if (mappingData.getServiceObject() == null) {
+                        internalMapWildcardWrapper
+                            (wildcardWrappers, context.nesting, 
+                             urlNoContext, mappingData);
+                    }
+
+                    // Rule 4c -- Welcome resources processing
+                    //            for physical folder
+                    if (mappingData.getServiceObject() == null
+                        && context.resources != null) {
+                        String pathStr = urlNoContext.toString();
+                        
+                        mapWelcomResource(context, urlNoContext, mappingData,
+                                extensionWrappers, pathStr);
+                        
+                    }
+                }
+            }
+                                        
+        }
+
+
+        // Rule 7 -- Default servlet
+        if (mappingData.getServiceObject() == null && !checkJspWelcomeFiles) {
+            if (context.defaultWrapper != null) {
+                mappingData.service = context.defaultWrapper;
+                mappingData.requestPath.set(urlNoContext);
+                mappingData.wrapperPath.set(urlNoContext);
+            }
+            // Redirection to a folder
+            if (context.resources != null && urlNoContext.lastChar() != '/') {
+                String pathStr = urlNoContext.toString();
+                mapDefaultServlet(context, urlNoContext, mappingData, 
+                        url,
+                        pathStr);
+            }
+        }
+    }
+
+    /** 
+     * Filesystem-dependent method:
+     *  if pathStr corresponds to a directory, we'll need to redirect with / 
+     *  at end. 
+     */
+    protected void mapDefaultServlet(BaseMapper.ContextMapping context, 
+            CBuffer path,
+            MappingData mappingData, 
+            CBuffer url,
+            String pathStr) throws IOException {
+        
+        if (context.resources != null 
+                && context.resources.isDirectory(pathStr)) {
+            mappingData.redirectPath.set(url);
+            mappingData.redirectPath.append("/");
+        } else {
+            mappingData.requestPath.set(pathStr);
+            mappingData.wrapperPath.set(pathStr);
+        }
+    }
+
+
+    /**
+     * Filesystem dependent method: 
+     *  check if a resource exists in filesystem. 
+     */
+    protected void mapWelcomResource(BaseMapper.ContextMapping context, CBuffer path,
+                               MappingData mappingData,
+                               BaseMapper.ServiceMapping[] extensionWrappers, String pathStr) {
+        
+        if (context.resources != null &&
+                context.resources.isFile(pathStr)) {
+            internalMapExtensionWrapper(extensionWrappers,
+                                        path, mappingData);
+            if (mappingData.getServiceObject() == null
+                && context.defaultWrapper != null) {
+                mappingData.service = context.defaultWrapper;
+                mappingData.requestPath.set(path);
+                mappingData.wrapperPath.set(path);
+                mappingData.requestPath.set(pathStr);
+                mappingData.wrapperPath.set(pathStr);
+            }
+        }
+    }
+
+    /**
+     * Exact mapping.
+     */
+    private final void internalMapExactWrapper
+        (BaseMapper.ServiceMapping[] wrappers, CBuffer path, MappingData mappingData) {
+        int pos = find(wrappers, path);
+        if ((pos != -1) && (path.equals(wrappers[pos].name))) {
+            mappingData.requestPath.set(wrappers[pos].name);
+            mappingData.wrapperPath.set(wrappers[pos].name);
+            mappingData.service = wrappers[pos];
+        }
+    }
+
+
+    /**
+     * Prefix mapping. ( /foo/* )
+     */
+    private final void internalMapWildcardWrapper
+        (BaseMapper.ServiceMapping[] wrappers, int nesting, CBuffer path, 
+         MappingData mappingData) {
+
+        int lastSlash = -1;
+        int length = -1;
+        
+        CBuffer tmp = mappingData.tmpPrefix;
+        tmp.wrap(path, 0, path.length());
+        
+        int pos = find(wrappers, tmp);
+        if (pos != -1) {
+            boolean found = false;
+            while (pos >= 0) {
+                if (tmp.startsWith(wrappers[pos].name)) {
+                    length = wrappers[pos].name.length();
+                    if (tmp.length() == length) {
+                        found = true;
+                        break;
+                    } else if (tmp.startsWithIgnoreCase("/", length)) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (lastSlash == -1) {
+                    lastSlash = tmp.nthSlash(nesting + 1); 
+                } else {
+                    lastSlash = tmp.lastIndexOf('/');
+                }
+                tmp.delete(lastSlash);
+                pos = find(wrappers, tmp);
+            }
+            if (found) {
+                mappingData.wrapperPath.set(wrappers[pos].name);
+                
+                if (path.length() > length) {
+                    mappingData.pathInfo.set
+                        (path, length, path.length() - length);
+                }
+                mappingData.requestPath.set(path);
+
+                mappingData.service = wrappers[pos];
+            }
+        }
+    }
+    
+
+    /**
+     * Extension mappings.
+     */
+    protected final void internalMapExtensionWrapper
+        (BaseMapper.ServiceMapping[] wrappers, CBuffer path, MappingData mappingData) {
+        
+        int dot = path.getExtension(mappingData.ext, '/', '.');
+        if (dot >= 0) {
+            int pos = find(wrappers, mappingData.ext);
+                
+            if ((pos != -1)
+                    && (mappingData.ext.equals(wrappers[pos].name))) {
+                    
+                mappingData.wrapperPath.set(path);
+                mappingData.requestPath.set(path);
+
+                mappingData.service = wrappers[pos];
+            }
+        }
+    }
+
+
+    /**
+     * Find a map elemnt given its name in a sorted array of map elements.
+     * This will return the index for the closest inferior or equal item in the
+     * given array.
+     */
+    private static final int find(BaseMapper.Mapping[] map, CBuffer name) {
+
+        int a = 0;
+        int b = map.length - 1;
+
+        // Special cases: -1 and 0
+        if (b == -1) {
+            return -1;
+        }
+        
+        if (name.compare(map[0].name) < 0 ) {
+            return -1;
+        }         
+        if (b == 0) {
+            return 0;
+        }
+
+        int i = 0;
+        while (true) {
+            i = (b + a) / 2;
+            int result = name.compare(map[i].name);
+            if (result == 1) {
+                a = i;
+            } else if (result == 0) {
+                return i;
+            } else {
+                b = i;
+            }
+            if ((b - a) == 1) {
+                int result2 = name.compare(map[b].name);
+                if (result2 < 0) {
+                    return a;
+                } else {
+                    return b;
+                }
+            }
+        }
+
+    }
+
+    /**
+     * Find a map elemnt given its name in a sorted array of map elements.
+     * This will return the index for the closest inferior or equal item in the
+     * given array.
+     */
+    private static final int findIgnoreCase(BaseMapper.Mapping[] map, 
+            CBuffer name) {
+        int a = 0;
+        int b = map.length - 1;
+
+        // Special cases: -1 and 0
+        if (b == -1) {
+            return -1;
+        }
+        if (name.compareIgnoreCase(map[0].name) < 0 ) {
+            return -1;
+        }         
+        if (b == 0) {
+            return 0;
+        }
+
+        int i = 0;
+        while (true) {
+            i = (b + a) / 2;
+            int result = name.compareIgnoreCase(map[i].name);
+            if (result == 1) {
+                a = i;
+            } else if (result == 0) {
+                return i;
+            } else {
+                b = i;
+            }
+            if ((b - a) == 1) {
+                int result2 = name.compareIgnoreCase(map[b].name);
+                if (result2 < 0) {
+                    return a;
+                } else {
+                    return b;
+                }
+            }
+        }
+
+    }
+
+
+    /**
+     * Find a map element given its name in a sorted array of map elements.
+     * This will return the index for the closest inferior or equal item in the
+     * given array.
+     */
+    private static final int find(BaseMapper.Mapping[] map, String name) {
+
+        int a = 0;
+        int b = map.length - 1;
+
+        // Special cases: -1 and 0
+        if (b == -1) {
+            return -1;
+        }
+        
+        if (name.compareTo(map[0].name) < 0) {
+            return -1;
+        } 
+        if (b == 0) {
+            return 0;
+        }
+
+        int i = 0;
+        while (true) {
+            i = (b + a) / 2;
+            int result = name.compareTo(map[i].name);
+            if (result > 0) {
+                a = i;
+            } else if (result == 0) {
+                return i;
+            } else {
+                b = i;
+            }
+            if ((b - a) == 1) {
+                int result2 = name.compareTo(map[b].name);
+                if (result2 < 0) {
+                    return a;
+                } else {
+                    return b;
+                }
+            }
+        }
+
+    }
+
+
+    /**
+     * Return the slash count in a given string.
+     */
+    private static final int slashCount(String name) {
+        int pos = -1;
+        int count = 0;
+        while ((pos = name.indexOf('/', pos + 1)) != -1) {
+            count++;
+        }
+        return count;
+    }
+
+
+    /**
+     * Insert into the right place in a sorted MapElement array, and prevent
+     * duplicates.
+     */
+    private static final boolean insertMap
+        (BaseMapper.Mapping[] oldMap, BaseMapper.Mapping[] newMap, BaseMapper.Mapping newElement) {
+        int pos = find(oldMap, newElement.name);
+        if ((pos != -1) && (newElement.name.equals(oldMap[pos].name))) {
+            return false;
+        }
+        System.arraycopy(oldMap, 0, newMap, 0, pos + 1);
+        newMap[pos + 1] = newElement;
+        System.arraycopy
+            (oldMap, pos + 1, newMap, pos + 2, oldMap.length - pos - 1);
+        return true;
+    }
+
+
+    /**
+     * Insert into the right place in a sorted MapElement array.
+     */
+    private static final boolean removeMap
+        (BaseMapper.Mapping[] oldMap, BaseMapper.Mapping[] newMap, String name) {
+        int pos = find(oldMap, name);
+        if ((pos != -1) && (name.equals(oldMap[pos].name))) {
+            System.arraycopy(oldMap, 0, newMap, 0, pos);
+            System.arraycopy(oldMap, pos + 1, newMap, pos,
+                             oldMap.length - pos - 1);
+            return true;
+        }
+        return false;
+    }
+
+
+    // ------------------------------------------------- MapElement Inner Class
+
+
+    protected static final class Host
+        extends BaseMapper.Mapping {
+        //Map<String, Context> contexts = new HashMap();
+        //Context rootContext;
+
+        public ContextList contextList = null;
+
+    }
+
+
+    // ------------------------------------------------ ContextList Inner Class
+
+    // Shared among host aliases.
+    protected static final class ContextList {
+
+        public BaseMapper.ContextMapping[] contexts = new BaseMapper.ContextMapping[0];
+        public int nesting = 0;
+
+    }
+
+
+    public static final class ContextMapping extends BaseMapper.Mapping {
+    
+        public String[] welcomeResources = new String[0];
+        public FileConnector resources = null;
+        
+        public BaseMapper.ServiceMapping defaultWrapper = null;
+        
+        public BaseMapper.ServiceMapping[] exactWrappers = new BaseMapper.ServiceMapping[0];
+        public BaseMapper.ServiceMapping[] wildcardWrappers = new BaseMapper.ServiceMapping[0];
+        public BaseMapper.ServiceMapping[] extensionWrappers = new BaseMapper.ServiceMapping[0];
+        public int nesting = 0;
+    
+    }
+
+
+    public static class ServiceMapping extends BaseMapper.Mapping {
+        public boolean jspWildCard = false;
+        // If set, the service will run in the selector thread ( should 
+        // be non-blocking )
+        public boolean selectorThread = false;
+        
+    }
+
+
+    protected static abstract class Mapping {    
+        public String name = null;
+        public Object object = null;
+    
+        public String toString() {
+            return name;
+        }
+    }
+
+
+    // ---------------------------------------------------- Context Inner Class
+
+
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ContentType.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ContentType.java
new file mode 100644 (file)
index 0000000..993566c
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ *  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.tomcat.lite.http;
+
+
+/**
+ * Usefull methods for Content-Type processing
+ * 
+ * @author James Duncan Davidson [duncan@eng.sun.com]
+ * @author James Todd [gonzo@eng.sun.com]
+ * @author Jason Hunter [jch@eng.sun.com]
+ * @author Harish Prabandham
+ * @author costin@eng.sun.com
+ */
+public class ContentType {
+
+    /**
+     * Parse the character encoding from the specified content type header.
+     * If the content type is null, or there is no explicit character encoding,
+     * <code>null</code> is returned.
+     *
+     * @param contentType a content type header
+     */
+    public static String getCharsetFromContentType(String contentType) {
+
+        if (contentType == null)
+            return (null);
+        int start = contentType.indexOf("charset=");
+        if (start < 0)
+            return (null);
+        String encoding = contentType.substring(start + 8);
+        int end = encoding.indexOf(';');
+        if (end >= 0)
+            encoding = encoding.substring(0, end);
+        encoding = encoding.trim();
+        if ((encoding.length() > 2) && (encoding.startsWith("\""))
+            && (encoding.endsWith("\"")))
+            encoding = encoding.substring(1, encoding.length() - 1);
+        return (encoding.trim());
+
+    }
+
+
+    /**
+     * Returns true if the given content type contains a charset component,
+     * false otherwise.
+     *
+     * @param type Content type
+     * @return true if the given content type contains a charset component,
+     * false otherwise
+     */
+    public static boolean hasCharset(String type) {
+
+        boolean hasCharset = false;
+
+        int len = type.length();
+        int index = type.indexOf(';');
+        while (index != -1) {
+            index++;
+            while (index < len && Character.isSpace(type.charAt(index))) {
+                index++;
+            }
+            if (index+8 < len
+                    && type.charAt(index) == 'c'
+                    && type.charAt(index+1) == 'h'
+                    && type.charAt(index+2) == 'a'
+                    && type.charAt(index+3) == 'r'
+                    && type.charAt(index+4) == 's'
+                    && type.charAt(index+5) == 'e'
+                    && type.charAt(index+6) == 't'
+                    && type.charAt(index+7) == '=') {
+                hasCharset = true;
+                break;
+            }
+            index = type.indexOf(';', index);
+        }
+
+        return hasCharset;
+    }
+
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/DefaultHttpConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/DefaultHttpConnector.java
new file mode 100644 (file)
index 0000000..a20aa41
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ */
+package org.apache.tomcat.lite.http;
+
+import org.apache.tomcat.lite.io.SocketConnector;
+
+public class DefaultHttpConnector {
+
+    public synchronized static HttpConnector getNew() {
+        return new HttpConnector(new SocketConnector());
+    }
+
+    public synchronized static HttpConnector get() {
+        if (DefaultHttpConnector.defaultHttpConnector == null) {
+            DefaultHttpConnector.defaultHttpConnector = 
+                new HttpConnector(new SocketConnector());
+        }
+        return DefaultHttpConnector.defaultHttpConnector;
+    }
+    
+    private static HttpConnector defaultHttpConnector;
+
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/Dispatcher.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/Dispatcher.java
new file mode 100644 (file)
index 0000000..59a3c7c
--- /dev/null
@@ -0,0 +1,180 @@
+/*  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.tomcat.lite.http;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+import org.apache.tomcat.lite.http.BaseMapper.ContextMapping;
+import org.apache.tomcat.lite.http.HttpChannel.HttpService;
+import org.apache.tomcat.lite.http.HttpChannel.RequestCompleted;
+import org.apache.tomcat.lite.io.CBuffer;
+import org.apache.tomcat.lite.io.FileConnector;
+import org.apache.tomcat.lite.io.CBuffer;
+import org.apache.tomcat.lite.io.UrlEncoding;
+
+/**
+ * This class has several functions:
+ * - maps the request to another HttpService
+ * - decide if the request should be run in the selector thread
+ * or in a thread pool
+ * - finalizes the request ( close / flush )
+ * - detectsif the request is complete or set callbacks 
+ * for receive/flush/done.
+ *  
+ */
+public class Dispatcher implements HttpService {
+
+    private BaseMapper mapper;
+    static boolean debug = false;
+    static Logger log = Logger.getLogger("Mapper");
+    Executor tp = Executors.newCachedThreadPool();
+
+    public Dispatcher() {
+        init();
+    }
+    
+    protected void init() {
+        mapper = new BaseMapper();        
+    }
+
+    public void runService(HttpChannel ch) {
+        MappingData mapRes = ch.getRequest().getMappingData();
+        HttpService h = (HttpService) mapRes.getServiceObject();
+        try {
+            //log.info("Service ");
+            h.service(ch.getRequest(), ch.getResponse());
+            if (!ch.getRequest().isAsyncStarted()) {
+                ch.complete();
+                ch.release(); // recycle objects.
+            } else {
+                // Nothing - complete must be called when done.
+            }
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch( Throwable t ) {
+            t.printStackTrace();
+        }
+    }
+    
+    @Override
+    public void service(HttpRequest httpReq, HttpResponse httpRes) throws IOException {
+        service(httpReq, httpRes, false);
+    }
+
+    
+    public void service(HttpRequest httpReq, HttpResponse httpRes, boolean noThread) 
+            throws IOException {
+        long t0 = System.currentTimeMillis();
+        HttpChannel http = httpReq.getHttpChannel();
+        
+        http.setCompletedCallback(doneCallback);
+        
+        try {
+          // compute decodedURI - not done by connector
+            MappingData mapRes = httpReq.getMappingData();
+            mapRes.recycle();
+          
+            mapper.map(httpReq.serverName(),
+                  httpReq.decodedURI(), mapRes);
+
+          HttpService h = (HttpService) mapRes.getServiceObject();
+
+          if (h != null) {
+              if (debug) {
+                  log.info(">>>>>>>> START: " + http.getRequest().method() + " " + 
+                      http.getRequest().decodedURI() + " " + 
+                      h.getClass().getSimpleName());
+              }
+              
+              if (mapRes.service.selectorThread || noThread) {
+                  runService(http);
+              } else {
+                  tp.execute(httpReq.getHttpChannel().dispatcherRunnable);
+              }
+              
+          } else {
+              httpRes.setStatus(404);
+              http.complete();
+          }
+          
+        } catch (IOException ex) {
+            if ("Broken pipe".equals(ex.getMessage())) { 
+                log.warning("Connection interrupted while writting");
+            }
+            throw ex;
+        } catch( Throwable t ) {
+            t.printStackTrace();
+            httpRes.setStatus(500);
+            http.abort(t);
+        }
+    }
+
+    private RequestCompleted doneCallback = new RequestCompleted() {
+        @Override
+        public void handle(HttpChannel client, Object extraData) throws IOException {
+            if (debug) {
+                log.info("<<<<<<<< DONE: " + client.getRequest().method() + " " + 
+                        client.getRequest().decodedURI() + " " + 
+                        client.getResponse().getStatus() + " "  
+                        );
+            }
+        }
+    };
+    
+    public BaseMapper.ContextMapping addContext(String hostname, String ctxPath,
+            Object ctx, String[] welcomeResources, FileConnector resources,
+            HttpService ctxService) {
+        return mapper.addContext(hostname, ctxPath, ctx, welcomeResources, resources, 
+                ctxService);
+    }
+    
+    public void map(CBuffer hostMB, CBuffer urlMB, MappingData md) {
+        try {
+            mapper.map(hostMB, urlMB, md);
+        } catch (Exception e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+    }
+
+    public void map(BaseMapper.ContextMapping ctx,
+            CBuffer uri, MappingData md) {
+        try {
+            mapper.internalMapWrapper(ctx, uri, md);
+        } catch (Exception e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+    }
+
+    public void addWrapper(BaseMapper.ContextMapping ctx, String path,
+            HttpService service) {
+        mapper.addWrapper(ctx, path, service);
+    }
+    
+    
+    public void setDefaultService(HttpService service) {
+        BaseMapper.ContextMapping mCtx = 
+            mapper.addContext(null, "/", null, null, null, null);
+        mapper.addWrapper(mCtx, "/", service);
+    }
+
+
+}
\ No newline at end of file
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/FutureCallbacks.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/FutureCallbacks.java
new file mode 100644 (file)
index 0000000..0b8324d
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ */
+package org.apache.tomcat.lite.http;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.AbstractQueuedSynchronizer;
+
+
+/**
+ * Support for blocking calls and callbacks.
+ * 
+ * Unlike FutureTask, it is possible to reuse this and hopefully 
+ * easier to extends. Also has callbacks.  
+ * 
+ * @author Costin Manolache
+ */
+public class FutureCallbacks<V> implements Future<V> {
+
+    // Other options: ReentrantLock uses AbstractQueueSynchronizer, 
+    // more complex. Same for CountDownLatch
+    // FutureTask - uses Sync as well, ugly interface with
+    // Callable, can't be recycled.
+    // Mina: simple object lock, doesn't extend java.util.concurent.Future
+    
+    private Sync sync = new Sync(); 
+
+    private V value;
+
+    public static interface Callback<V> {
+        public void run(V param);
+    }
+    
+    private List<Callback<V>> callbacks = new ArrayList();
+    
+    public FutureCallbacks() {
+    }
+
+    /** 
+     * Unlocks the object if it was locked. Should be called
+     * when the object is reused.
+     * 
+     * Callbacks will not be invoked.
+     */
+    public void reset() {
+        sync.releaseShared(0);
+        sync.reset();
+    }
+
+    public void recycle() {
+        callbacks.clear();
+        sync.releaseShared(0);
+        sync.reset();
+    }
+    
+    /**
+     * Unlocks object and calls the callbacks.
+     * @param v 
+     * 
+     * @throws IOException
+     */
+    public void signal(V v) throws IOException {
+        sync.releaseShared(0);
+        onSignal(v);
+    }
+    
+    protected boolean isSignaled() {
+        return true;
+    }
+    
+    /** 
+     * Override to call specific callbacks
+     */
+    protected void onSignal(V v) {
+        for (Callback<V> cb: callbacks) {
+            if (cb != null) {
+                cb.run(v);
+            }
+        }
+    }
+
+    /**
+     * Set the response. Will cause the callback to be called and lock to be
+     * released.
+     * 
+     * @param value
+     * @throws IOException 
+     */
+    public void setValue(V value) throws IOException {
+        synchronized (this) {
+            this.value = value;
+            signal(value);
+        }
+    }
+
+    public void waitSignal(long to) throws IOException {
+        try {
+            get(to, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e1) {
+            throw new IOException(e1.getMessage());
+        } catch (TimeoutException e1) {
+            throw new IOException(e1.getMessage());
+        } catch (ExecutionException e) {
+            throw new IOException(e.getMessage());
+        }                
+    }
+
+    @Override
+    public V get() throws InterruptedException, ExecutionException {
+        sync.acquireSharedInterruptibly(0);
+        return value;
+    }
+
+    @Override
+    public V get(long timeout, TimeUnit unit) throws InterruptedException,
+            ExecutionException, TimeoutException {
+        if (!sync.tryAcquireSharedNanos(0, unit.toNanos(timeout))) {
+            throw new TimeoutException();
+        }
+        return value;
+    }
+
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        return false;
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return false;
+    }
+
+    @Override
+    public boolean isDone() {
+        return sync.isSignaled();
+    }
+
+    private class Sync extends AbstractQueuedSynchronizer {
+        
+        static final int DONE = 1;
+        static final int BLOCKED = 0;
+        Object result;
+        Throwable t;
+        
+        @Override
+        protected int tryAcquireShared(int ignore) {
+            return getState() == DONE ? 1 : -1;
+        }
+
+        @Override
+        protected boolean tryReleaseShared(int ignore) {
+            setState(DONE);
+            return true; 
+        }
+
+        public void reset() {
+            setState(BLOCKED);
+        }
+        
+        boolean isSignaled() {
+            return getState() == DONE;
+        }
+    }    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpBody.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpBody.java
new file mode 100644 (file)
index 0000000..11e84df
--- /dev/null
@@ -0,0 +1,589 @@
+/*
+ */
+package org.apache.tomcat.lite.http;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.logging.Logger;
+
+import org.apache.tomcat.lite.io.BBucket;
+import org.apache.tomcat.lite.io.BBuffer;
+import org.apache.tomcat.lite.io.Hex;
+import org.apache.tomcat.lite.io.IOBuffer;
+import org.apache.tomcat.lite.io.IOChannel;
+
+/**
+ * Transport decoded buffer, representing the body 
+ * of HTTP messages.
+ * 
+ * Supports:
+ *  - Chunked and Content-Length delimitation
+ *  - "Close" delimitation ( no content delimitation - TCP close )
+ *  
+ * TODO: continue support 
+ * TODO: gzip encoding
+ * 
+ * For sending, data is kept in this buffer until flush() is called.
+ */
+class HttpBody extends IOBuffer {
+    protected static Logger log = Logger.getLogger("HttpBody");
+
+    static int DEFAULT_CHUNK_SIZE = 4096;
+
+    private HttpChannel http;
+    
+    protected boolean chunked = false;
+    protected long contentLength = -1; // C-L header
+        
+    /** True: Http/1.x + chunked || C-L - 
+     *  False: for http/0.9, connection:close, errors. -> 
+     *     close delimited 
+     */    
+    boolean frameError = false;
+
+
+    /** Bytes remaining in the current chunk or body ( if CL ) */
+    protected long remaining = 0; // both chunked and C-L
+    
+    // used for chunk parsing
+    ChunkState chunk = new ChunkState();
+    
+    boolean noBody;
+
+    boolean endSent = false;
+    boolean sendBody = false;
+
+    private HttpMessage httpMsg;
+    
+    HttpBody(HttpChannel asyncHttp, boolean sendBody) {
+        this.http = asyncHttp;
+        if (sendBody) {
+            this.sendBody = true;
+            // For flush and close to work - need to fix
+            //this.ch = http;
+        } else {
+            this.ch = null;
+        }
+    }
+    
+    public String toString() {
+        return "{" + super.toString() + " " + 
+        (chunked ? "CNK/" + remaining : "") 
+        + (contentLength >= 0 ? "C-L/" + contentLength + "/" + remaining : "")
+        + (isAppendClosed() ? ", C" : "") 
+        + "}";
+    }
+    
+    public void recycle() {
+        chunked = false;
+        remaining = 0; 
+        contentLength = -1;
+        chunk.recycle();
+        chunk.recycle();
+        super.recycle();
+        frameError = false;
+        noBody = false;
+        endSent = false;
+    }
+    
+    public boolean isContentDelimited() {
+        return chunked || contentLength >= 0;
+    }
+
+    
+
+    /**
+     * Updates chunked, contentLength, remaining
+     */
+    protected void processContentDelimitation() {
+
+        contentLength = httpMsg.getContentLength();
+        if (contentLength >= 0) {
+            remaining = contentLength;
+        }        
+        
+        // TODO: multiple transfer encoding headers, only process the last
+        String transferEncodingValue = httpMsg.getHeader(HttpChannel.TRANSFERENCODING);
+        if (transferEncodingValue != null) {
+            int startPos = 0;
+            int commaPos = transferEncodingValue.indexOf(',');
+            String encodingName = null;
+            while (commaPos != -1) {
+                encodingName = transferEncodingValue.substring
+                (startPos, commaPos).toLowerCase().trim();
+                if ("chunked".equalsIgnoreCase(encodingName)) {
+                    chunked = true;
+                }
+                startPos = commaPos + 1;
+                commaPos = transferEncodingValue.indexOf(',', startPos);
+            }
+            encodingName = transferEncodingValue.substring(startPos)
+                .toLowerCase().trim();
+            if ("chunked".equals(encodingName)) {
+                chunked = true;
+                httpMsg.chunked = true;
+            } else {
+                System.err.println("TODO: ABORT 501");
+                //return 501; // Currently only chunked is supported for 
+                // transfer encoding.
+            }
+        }
+
+        if (chunked) {
+            remaining = 0;
+        }
+    }    
+        
+    void updateCloseOnEnd() {
+        if (!isContentDelimited() && !noBody) {
+            http.closeStreamOnEnd("not content delimited");
+        }
+    }
+    
+    void processContentEncoding() {
+        // Content encoding - set it on the buffer, will be processed in blocking
+        // mode, after transfer encoding.
+//        MessageBytes contentEncodingValueMB =
+//            headers.getValue("content-encoding");
+
+//        if (contentEncodingValueMB != null) {
+//            if (contentEncodingValueMB.equals("gzip")) {
+//                buffer.addActiveFilter(gzipIF);
+//            }
+//            // TODO: other encoding filters
+//            // TODO: this should be separate layer
+//        }    
+    }
+
+    /**
+     * Determine if we must drop the connection because of the HTTP status
+     * code.  Use the same list of codes as Apache/httpd.
+     */
+    protected boolean statusDropsConnection(int status) {
+        return status == 400 /* SC_BAD_REQUEST */ ||
+        status == 408 /* SC_REQUEST_TIMEOUT */ ||
+        status == 411 /* SC_LENGTH_REQUIRED */ ||
+        status == 413 /* SC_REQUEST_ENTITY_TOO_LARGE */ ||
+        status == 414 /* SC_REQUEST_URI_TOO_LARGE */ ||
+        status == 500 /* SC_INTERNAL_SERVER_ERROR */ ||
+        status == 503 /* SC_SERVICE_UNAVAILABLE */ ||
+        status == 501 /* SC_NOT_IMPLEMENTED */;
+    }
+    
+    static final int NEED_MORE = -1;
+    static final int ERROR = -4;
+    static final int DONE = -5;
+    
+    class ChunkState {
+        int partialChunkLen;
+        boolean readDigit = false;
+        boolean trailer = false;
+        protected boolean needChunkCrlf = false;
+        
+        // Buffer used for chunk length conversion.
+        protected byte[] sendChunkLength = new byte[10];
+
+        /** End chunk marker - will include chunked end or empty */
+        protected BBuffer endSendBuffer = BBuffer.wrapper();
+
+        public ChunkState() {
+            sendChunkLength[8] = (byte) '\r';
+            sendChunkLength[9] = (byte) '\n';            
+        }
+        
+        void recycle() {
+            partialChunkLen = 0;
+            readDigit = false;
+            trailer = false;
+            needChunkCrlf = false;
+            endSendBuffer.recycle();
+        }
+        
+        /**
+         * Parse the header of a chunk.
+         * A chunk header can look like 
+         * A10CRLF
+         * F23;chunk-extension to be ignoredCRLF
+         * The letters before CRLF but after the trailer mark, must be valid hex digits, 
+         * we should not parse F23IAMGONNAMESSTHISUP34CRLF as a valid header
+         * according to spec
+         */
+        int parseChunkHeader(IOBuffer buffer) throws IOException {
+            if (buffer.peekFirst() == null) {
+                return NEED_MORE;
+            }
+            if (needChunkCrlf) {
+                // TODO: Trailing headers
+                int c = buffer.read();
+                if (c == BBuffer.CR) {
+                    if (buffer.peekFirst() == null) {
+                        return NEED_MORE;
+                    }
+                    c = buffer.read();
+                }
+                if (c == BBuffer.LF) {
+                    needChunkCrlf = false;
+                } else {
+                    System.err.println("Bad CRLF " + c);
+                    return ERROR;
+                }
+            }
+
+            while (true) {
+                if (buffer.peekFirst() == null) {
+                    return NEED_MORE;
+                }
+                int c = buffer.read();
+
+                if (c == BBuffer.CR) {
+                    continue;
+                } else if (c == BBuffer.LF) {
+                    break;
+                } else if (c == HttpChannel.SEMI_COLON) {
+                    trailer = true;
+                } else if (c == BBuffer.SP) {
+                    // ignore
+                } else if (trailer) {
+                    // ignore
+                } else {
+                    //don't read data after the trailer
+                    if (Hex.DEC[c] != -1) {
+                        readDigit = true;
+                        partialChunkLen *= 16;
+                        partialChunkLen += Hex.DEC[c];
+                    } else {
+                        //we shouldn't allow invalid, non hex characters
+                        //in the chunked header
+                        log.info("Chunk parsing error1 " + c + " " + buffer);
+                        http.abort("Chunk error");
+                        return ERROR;
+                    }
+                }
+            }
+
+            if (!readDigit) {
+                log.info("Chunk parsing error2 " + buffer);
+                return ERROR;
+            }
+
+            needChunkCrlf = true;  // next time I need to parse CRLF
+            int result = partialChunkLen;
+            partialChunkLen = 0;
+            trailer = false;
+            readDigit = false;
+            return result;
+        }
+        
+
+        ByteBuffer prepareChunkHeader(int current) {
+            int pos = 7; // 8, 9 are CRLF
+            while (current > 0) {
+                int digit = current % 16;
+                current = current / 16;
+                sendChunkLength[pos--] = Hex.HEX[digit];
+            }
+            if (needChunkCrlf) {
+                sendChunkLength[pos--] = (byte) '\n';
+                sendChunkLength[pos--] = (byte) '\r';
+            } else {
+                needChunkCrlf = true;
+            }
+            // TODO: pool - this may stay in the queue while we flush more
+            ByteBuffer chunkBB = ByteBuffer.allocate(16);
+            chunkBB.put(sendChunkLength, pos + 1, 9 - pos);
+            chunkBB.flip();
+            return chunkBB;
+        }
+
+        public BBuffer endChunk() {
+            if (! needChunkCrlf) { 
+                endSendBuffer.setBytes(HttpChannel.END_CHUNK_BYTES, 2, 
+                        HttpChannel.END_CHUNK_BYTES.length - 2); // CRLF
+            } else { // 0
+                endSendBuffer.setBytes(HttpChannel.END_CHUNK_BYTES, 0, 
+                        HttpChannel.END_CHUNK_BYTES.length);                
+            }
+            return endSendBuffer;
+        }
+    }
+    
+    private int receiveDone(boolean frameError) throws IOException {
+        // Content-length case, we're done reading
+        close();
+        
+        this.frameError = frameError;
+        if (frameError) {
+            http.closeStreamOnEnd("frame error");
+        }
+
+        return DONE;        
+    }
+    
+    /** 
+     * Called when raw body data is received.
+     * Callback should not consume past the end of the body.
+     * @param rawReceiveBuffers 
+     *  
+     */
+    void rawDataReceived(IOBuffer rawReceiveBuffers) throws IOException {
+        // TODO: Make sure we don't process more than we need ( eat next req ).
+        // If we read too much: leave it in readBuf, the finalzation code
+        // should skip KeepAlive and start processing it.
+        // we need to read at least something - to detect -1 ( we could 
+        // suspend right away, but seems safer.
+        while (http.inMessage.state == HttpMessage.State.BODY_DATA) {
+            //log.info("RAW DATA: " + this + " RAW: " + rawReceiveBuffers);
+            if (noBody) {
+                receiveDone(false);
+                return;
+            }
+            if (rawReceiveBuffers.isClosedAndEmpty()) {
+                if (isContentDelimited()) {
+                    if (contentLength >= 0 && remaining == 0) {
+                        receiveDone(false);
+                    } else {
+                        // End of input - other side closed, no more data
+                        //log.info("CLOSE while reading " + this);    
+                        // they're not supposed to close !
+                        receiveDone(true);
+                    }
+                } else {
+                    receiveDone(false); // ok
+                }
+                // input connection closed ? 
+                http.closeStreamOnEnd("Closed input");
+                return;
+            }
+            BBucket rawBuf = rawReceiveBuffers.peekFirst();
+            if (rawBuf == null) {
+                return;  // need more data                 
+            }
+
+            if (!isContentDelimited()) {
+                while (true) {
+                    BBucket first = rawReceiveBuffers.popFirst();
+                    if (first == null) {
+                        break; // will go back to check if done.
+                    } else {
+                        super.queue(first);
+                    }
+                }
+            } else {
+                
+                if (contentLength >= 0 && remaining == 0) {
+                    receiveDone(false);
+                    return;
+                }
+
+                if (chunked && remaining == 0) {
+                    int rc = NEED_MORE;
+                    while (rc == NEED_MORE) {
+                        rc = chunk.parseChunkHeader(rawReceiveBuffers);
+                        if (rc == ERROR) {
+                            receiveDone(true);
+                            return;
+                        } else if (rc == NEED_MORE) {
+                            return;
+                        }
+                    }
+                    if (rc == 0) { // last chunk
+                        receiveDone(false);
+                        return;
+                    } else {
+                        remaining = rc;
+                    }
+                }
+
+                rawBuf = (BBucket) rawReceiveBuffers.peekFirst();
+                if (rawBuf == null) {
+                    return;  // need more data                 
+                }
+                
+
+                if (remaining < rawBuf.remaining()) {
+                    // To buffer has more data than we need.
+                    int lenToConsume = (int) remaining;
+                    BBucket sb = rawReceiveBuffers.popLen(lenToConsume);
+                    super.queue(sb);
+                    //log.info("Queue received buffer " + this + " " + lenToConsume);
+                    remaining = 0;
+                } else {
+                    BBucket first = rawReceiveBuffers.popFirst();
+                    remaining -= first.remaining();
+                    super.queue(first);
+                    //log.info("Queue full received buffer " + this + " RAW: " + rawReceiveBuffers);
+                }
+                if (contentLength >= 0 && remaining == 0) {
+                    // Content-Length, all done
+                    super.close();
+                    receiveDone(false);
+                }
+            }
+        }
+    }
+    
+    void flushToNext() throws IOException {
+        http.sendHeaders(); // if needed
+
+        if (getNet() == null) {
+            return; // not connected yet.
+        }
+        
+        synchronized (this) {
+            if (noBody) {
+                for (int i = 0; i < super.getBufferCount(); i++) {
+                    Object bc = super.peekBucket(i);
+                    if (bc instanceof BBucket) {
+                        ((BBucket) bc).release();
+                    }
+                }                    
+                super.clear();
+                return;
+            }
+            // TODO: only send < remainingWrite, if buffer
+            // keeps changing after startWrite() is called (shouldn't)
+            boolean done = false;
+            
+            if (chunked) {
+                done = sendChunked();
+            } else if (contentLength >= 0) {
+                // content-length based
+                done = sendContentLen();
+            } else {
+                // Close delimitation
+                while (true) {
+                    Object bc = popFirst();
+                    if (bc == null) {
+                        break;
+                    }
+                    getNet().getOut().queue(bc);
+                }
+                if (super.isClosedAndEmpty()) {
+                    done = true;
+                    if (getNet() != null) {
+                        getNet().getOut().close(); // no content-delimitation
+                    }
+                }
+            }
+        }
+    }
+
+    private boolean sendContentLen() throws IOException {
+        while (true) {
+            BBucket bucket = super.peekFirst();
+            if (bucket == null) {
+                break;
+            }
+            int len = bucket.remaining();
+            if (len <= remaining) {
+                remaining -= len;
+                bucket = super.popFirst();
+                getNet().getOut().queue(bucket);
+            } else {
+                // Write over the end of the buffer !
+                log.severe(http.dbgName + 
+                        ": write more than Content-Length");
+                len = (int) remaining;
+                // data between position and limit
+                bucket = popLen((int) remaining); 
+                getNet().getOut().queue(bucket);
+                while (bucket != null) {
+                    bucket = super.popFirst();
+                    if (bucket != null) {
+                        bucket.release();
+                    }
+                }
+                
+                // forced close
+                //close();
+                remaining = 0;
+                return true;
+            }
+        }
+        if (super.isClosedAndEmpty()) {
+            //http.rawSendBuffers.queue(IOBrigade.MARK);
+            if (remaining > 0) {
+                http.closeStreamOnEnd("sent more than content-length");
+                log.severe("Content-Length > body");
+            }
+            return true;
+        }
+        return false;
+    }
+    
+    public void close() throws IOException {
+        if (sendBody && !http.error) {
+            flushToNext(); // will send any remaining data.
+        }
+
+        if (isContentDelimited() && !http.error) {
+            if (!chunked && remaining > 0) {
+                log.severe("CLOSE CALLED WITHOUT FULL LEN");
+                // TODO: abort ?
+            } else {
+                super.close();
+                if (sendBody) {
+                    flushToNext(); // will send '0'
+                }
+            }
+        } else {
+            super.close();
+            if (sendBody) {
+                flushToNext();
+            }
+        }
+    }
+    
+    private boolean sendChunked() throws IOException {
+        int len = 0;
+        int cnt = super.getBufferCount();
+        for (int i = 0; i < cnt; i++) {
+            BBucket iob = super.peekBucket(i);
+            len += iob.remaining();
+        }
+
+        if (len > 0) {
+            ByteBuffer sendChunkBuffer = chunk.prepareChunkHeader(len); 
+            remaining = len;
+            getNet().getOut().queue(sendChunkBuffer);
+            while (cnt > 0) {
+                Object bc = super.popFirst();
+                getNet().getOut().queue(bc);
+                cnt --;
+            }
+        }
+
+        if (super.isClosedAndEmpty()) {
+            if (!endSent) {
+                getNet().getOut().append(chunk.endChunk());
+                endSent = true;
+            }
+            //http.rawSendBuffers.queue(IOBrigade.MARK);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    boolean isDone() {
+        if (noBody) {
+            return true;
+        }
+        if (isContentDelimited()) {
+            if (!chunked && remaining == 0) {
+                return true;
+            } else if (chunked && super.isAppendClosed()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    IOChannel getNet() {
+        return http.getNet();
+    }
+
+    public void setMessage(HttpMessage httpMsg) {
+        this.httpMsg = httpMsg;
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpChannel.java
new file mode 100644 (file)
index 0000000..df8878c
--- /dev/null
@@ -0,0 +1,1443 @@
+/*  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.tomcat.lite.http;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.tomcat.lite.http.HttpMessage.HttpMessageBytes;
+import org.apache.tomcat.lite.io.BBuffer;
+import org.apache.tomcat.lite.io.CBuffer;
+import org.apache.tomcat.lite.io.DumpChannel;
+import org.apache.tomcat.lite.io.FastHttpDateFormat;
+import org.apache.tomcat.lite.io.BBucket;
+import org.apache.tomcat.lite.io.IOBuffer;
+import org.apache.tomcat.lite.io.IOChannel;
+import org.apache.tomcat.lite.io.IOConnector;
+
+/**
+ * HTTP async client and server, based on tomcat NIO/APR connectors
+ * 
+ * 'Input', 'read', 'Recv' refers to information we get from the remote side - 
+ * the request body for server-mode or response body for client.
+ * 
+ * 'Output', 'write', 'Send' is for info we send - the post in client mode 
+ * and the response body for server mode.
+ * 
+ * @author Costin Manolache
+ */
+public class HttpChannel extends IOChannel {
+
+    static final int HEADER_SIZE = 8192;
+
+    static AtomicInteger serCnt = new AtomicInteger();
+
+    protected static Logger log = Logger.getLogger("HttpCh");
+
+    boolean debug = false;
+    // Used to receive an entity - headers + maybe some body
+    // read() must first consume extra data from this buffer.
+    // Next reads will be direct from socket.
+    protected BBuffer headRecvBuf = BBuffer.allocate(HEADER_SIZE);
+    BBuffer line = BBuffer.wrapper(); 
+    
+    // ---- Buffers owned by the AsyncHttp object ----
+    
+    BBuffer headB = BBuffer.wrapper();
+    FutureCallbacks<HttpChannel> doneLock = new FutureCallbacks<HttpChannel>();
+    ArrayList<IOChannel> filters = new ArrayList<IOChannel>();
+
+    // ---------- Body read side ------------
+
+    // Set if Exect: 100-continue was set on reqest.
+    // If this is the case - body won't be sent until
+    // server responds ( client ) and server will only
+    // read body after ack() - or skip to next request 
+    // without swallowing the body.
+    protected boolean expectation = false;
+    
+    /** Ready for recycle, if send/receive are done */
+    protected boolean release = false;
+    
+    protected boolean sendReceiveDone = false; 
+    
+    
+    // ----------- Body write side ------------
+    
+    
+    // TODO: setters
+    /**
+     * Called when the incoming headers have been received.
+     * ( response for client mode, request for server mode )
+     * @throws IOException 
+     */
+    HttpService httpService;
+    /** 
+     * Called when:
+     *  - body sent
+     *  - body received
+     *  - release() called - either service() done or client done with the 
+     *  buffers. 
+     *  
+     *  After this callback:
+     *  - socket closed if closeOnEndSend, or put in keep-alive
+     *  - AsyncHttp.recycle()
+     *  - returned to the pool.
+     */
+    private RequestCompleted doneAllCallback;
+    
+    
+    HttpMessage inMessage;
+    
+    
+    HttpMessage outMessage;
+    
+    // receive can be for request ( server mode ) or response ( client )
+    HttpBody receiveBody = new HttpBody(this, false);
+    HttpBody sendBody = new HttpBody(this, true);
+    
+    private HttpRequest httpReq;
+    private HttpResponse httpRes;
+
+    boolean headersDone = false;
+    protected boolean serverMode = false;
+    // ---------- Client only ------------
+    
+    //protected HttpParser parser = new HttpParser();
+    
+    protected String dbgName = this.getClass().getSimpleName();
+    // ----- Pools - sockets, heavy objects -------------
+    // If client mode - what host we are connected to.
+    protected String host;
+
+    protected int port; 
+    
+    private HttpConnector httpConnector;
+    // ------ JMX 
+    protected int ser; // id - for jmx registration and logs
+    // Server side only 
+    protected String serverHeader = "TomcatLite";
+    
+    protected boolean http11 = false;
+    
+    protected boolean http09 = false;
+    protected boolean error = false;
+    protected boolean abortDone = false;
+    FutureCallbacks<HttpChannel> doneFuture;
+    boolean doneCallbackCalled = false;
+
+    /** 
+     * Close connection when done writting, no content-length/chunked, 
+     * or no keep-alive ( http/1.0 ) or error.
+     * 
+     * ServerMode: set if HTTP/0.9 &1.0 || !keep-alive
+     * ClientMode: not currently used
+     */    
+    boolean keepAlive = true;
+
+    // Will be signalled (open) when the buffer is empty. 
+    private FutureCallbacks<IOChannel> flushLock = new FutureCallbacks<IOChannel>();
+
+    // -- Lifecycle --
+    
+    Runnable dispatcherRunnable = new Runnable() {
+        @Override
+        public void run() {
+            getConnector().getDispatcher().runService(HttpChannel.this);
+        }
+    };
+    
+    long ioTimeout = 30 * 60000; // 30 min seems high enough 
+        
+    public static final String CONTENT_LENGTH= "Content-Length";
+
+    /**
+     * HTTP/1.0.
+     */
+    public static final String HTTP_10 = "HTTP/1.0";
+    
+    public static final String HTTP_11 = "HTTP/1.1";
+    
+    public static final String CHUNKED = "chunked";
+    
+    public static final String CLOSE = "close"; 
+    
+    public static final String KEEPALIVE_S = "keep-alive";
+
+
+    public static final String CONNECTION = "Connection";
+    
+    public static final String TRANSFERENCODING = "Transfer-Encoding";
+    
+    /**
+     * SEMI_COLON.
+     */
+    public static final byte SEMI_COLON = (byte) ';';
+    
+    static byte[] END_CHUNK_BYTES = {
+    (byte) '\r', (byte) '\n', 
+    (byte) '0', 
+    (byte) '\r', (byte) '\n', 
+    (byte) '\r', (byte) '\n'};
+    
+    public static final byte QUESTION = (byte) '?';
+    
+    static final byte COLON = (byte) ':';
+    public HttpChannel() {
+        ser = serCnt.incrementAndGet();
+        httpReq = new HttpRequest(this);
+        httpRes = new HttpResponse(this);
+        init();
+        serverMode(false);
+    }
+    
+    /** 
+     * Close the connection, return to pool. Called if a 
+     * framing error happens, or if we want to force the connection
+     * to close, without waiting for all data to be sent/received.
+     * @param t 
+     * 
+     * @throws IOException
+     */
+    public void abort(Throwable t) throws IOException {
+        abort(t.toString());
+    }
+    
+    public void abort(String t) throws IOException {
+        synchronized (this) {
+            if (abortDone) {
+                return;
+            }
+            abortDone = true;
+        }
+
+        checkRelease();
+        trace("abort " + t);
+        log.info("Abort connection " + t);
+        if (net != null ) {
+            if (net.isOpen()) {
+                net.close();
+                net.startSending();
+            }
+        }
+        inMessage.state = HttpMessage.State.DONE;
+        outMessage.state = HttpMessage.State.DONE;
+        sendReceiveDone = true;
+        error = true;
+        close();
+        handleEndSendReceive();
+    }
+    
+    public HttpChannel addFilterAfter(IOChannel filter) {
+        filters.add(filter);
+        return this;
+    }
+
+    
+    private void checkRelease() throws IOException {
+        if (release && sendReceiveDone) {
+            throw new IOException("Object released");
+        }        
+    }
+    
+    void closeStreamOnEnd(String cause) {
+        if (debug) 
+            log.info("Not reusing connection because: " + cause);
+        keepAlive = false;
+    }
+
+    public void complete() throws IOException {
+        checkRelease();
+        if (!getOut().isAppendClosed()) {
+            getOut().close();            
+        }
+        if (!getIn().isAppendClosed()) {
+            getIn().close();
+        }
+
+        startSending();
+   }
+    
+    public int doRead(BBuffer chunk)
+            throws IOException {
+        checkRelease();
+        BBucket next = null;
+        while (true) {
+            getIn().waitData(0);
+            next = (BBucket) getIn().popFirst();
+            if (next != null) {
+                break;
+            } else if (getIn().isAppendClosed()) {
+                return -1;
+            } else {
+                System.err.println("Spurious waitData signal, no data");
+            }
+        } 
+        chunk.append(next.array(), next.position(), next.remaining());
+        int read =  next.remaining();
+        next.release();
+        return read;
+    }
+    
+    public void flushNet() throws IOException {
+        checkRelease();
+        if (net != null) {
+            net.startSending();
+        }
+    }
+    
+    public HttpConnector getConnector() {
+        return httpConnector;
+    }
+
+    public FutureCallbacks<HttpChannel> getDoneFuture() {
+        return doneLock;
+    }
+
+    public boolean getError() {
+        return error;
+    }
+    
+    // ---------------- Writting ------------------------------- 
+    
+    public String getId() {
+        return Integer.toString(ser);
+    }
+
+    public IOBuffer getIn() {
+        return receiveBody;
+    }
+    
+    
+    public long getIOTimeout() {
+        return ioTimeout;
+    }
+
+    public IOChannel getNet() {
+        return net;
+    }
+    
+    
+    public IOBuffer getOut() {
+        return sendBody;
+    }
+
+    public HttpRequest getRequest() {
+        return httpReq;
+    }
+    
+    
+    public HttpResponse getResponse() {
+        return httpRes;
+    }
+    
+      
+    public String getState() {
+        return
+            (serverMode ? "SRV:" : "") + 
+            (keepAlive() ? " KA " : "")  
+            + "RCV=[" + inMessage.state.toString() + " " + 
+            receiveBody.toString()  
+            + "] SND=[" + outMessage.state.toString() 
+            + " " + sendBody.toString() + "]";
+    }
+
+    
+    public String getStatus() {
+        return getResponse().getStatus() + " " + getResponse().getMessage();
+    }
+    
+    
+    public String getTarget() {
+        if (host == null) {
+            return ":" + port;
+        }
+        return host + ":" + port;
+    }
+
+
+    /**
+     * Called from IO thread, after the request body 
+     * is completed ( or if there is no req body )
+     * @throws IOException 
+     */
+    protected void handleEndReceive(boolean frameError) throws IOException {
+        if (inMessage.state == HttpMessage.State.DONE) {
+            return;
+        }
+        if (debug) {
+            trace("END_RECV " + ((frameError) ? " FRAME_ERROR" : ""));
+        }
+        if (frameError) {
+            closeStreamOnEnd("frame error");
+            // TODO: next read() should throw exception !!
+            error = true;
+        }
+
+        getIn().close();
+
+        inMessage.state = HttpMessage.State.DONE;
+        handleEndSendReceive();
+    }
+
+    /*
+     * Called when sending, receiving and processing is done.
+     * Can be called:
+     *  - from IO thread, if this is a result of a read/write event that 
+     *  finished the send/recev pair.
+     *  - from an arbitrary thread, if read was complete and the last write
+     *  was a success and done in that thread ( write is not bound to IO thr)
+     * 
+     */
+    protected void handleEndSendReceive() throws IOException {
+        this.doneLock.signal(this);
+        synchronized (this) {
+            if (doneCallbackCalled) {
+                return;
+            }
+            if (outMessage.state != HttpMessage.State.DONE || 
+                    inMessage.state != HttpMessage.State.DONE) {
+                return;
+            }
+            doneCallbackCalled = true;
+        }
+        
+        if (!keepAlive() && net != null) {
+            net.getOut().close(); // shutdown output if not done
+            net.getIn().close(); // this should close the socket
+            net.startSending();
+        }
+
+        if (doneAllCallback != null) {
+            doneAllCallback.handle(this, error ? new Throwable() : null);
+        }
+        
+        // Remove the net object - will be pooled separtely
+        IOChannel ch = this.net;
+        if (ch != null && keepAlive()) {
+            
+            boolean keepOpen = ch.isOpen(); 
+        
+            resetBuffers(); // net is now NULL - can't send anything more
+            if (getConnector() != null) {
+                getConnector().returnSocket(ch, serverMode, keepOpen);
+            }
+        }
+        
+        if (debug) {
+            trace("END_SEND_RECEIVE" 
+                    + (!keepAlive() ? " CLOSE_ON_END " : "")
+                    + (release ? " REL" : ""));
+        }
+            
+        synchronized(this) {
+            sendReceiveDone = true;
+            maybeRelease();
+        }
+    }
+    
+    /** 
+     * called from IO thread OR servlet thread when last block has been sent.
+     * If not using the socket ( net.getOut().flushCallback ) - this must 
+     * be called explicitely after flushing the body.
+     */
+    void handleEndSent() throws IOException {
+        if (outMessage.state == HttpMessage.State.DONE) {
+            // Only once.
+            if (debug) {
+                trace("Duplicate END SEND");
+            }
+            return;
+        }
+        outMessage.state = HttpMessage.State.DONE;
+
+        getOut().close();
+        
+        // Make sure the send/receive callback is called once
+        if (debug) {
+            trace("END_SEND");
+        }
+        handleEndSendReceive();
+    }
+    
+    // ----- End Selector thread callbacks ----
+    public void handleError(String type) {
+        System.err.println("Error " + type + " " + outMessage.state);
+    }
+
+    @Override
+    public void handleFlushed(IOChannel net) throws IOException {
+        flushLock.signal(this);
+        super.handleFlushed(this);
+        if (sendBody.isClosedAndEmpty()) {
+            handleEndSent();
+        }
+    }
+    
+    /**
+     * Called when the net has readable data.
+     */
+    @Override
+    public void handleReceived(IOChannel net) throws IOException {
+        try {
+            if (getConnector() == null) {
+                throw new IOException("Data received after release");
+            }
+            if (net == null) {
+                return; // connection released
+            }
+            if (net.getIn().isClosedAndEmpty()) {
+                // Close received
+                closeStreamOnEnd("close on input 2");
+                if (inMessage.state == HttpMessage.State.HEAD) {
+                    trace("NET CLOSE WHILE READING HEAD");
+                    abort(new IOException("Connection closed"));
+                    return;
+                } else if (inMessage.state == HttpMessage.State.DONE) {
+                    // Close received - make sure we close out
+                    if (sendBody.isClosedAndEmpty()) {
+                        net.getOut().close();
+                    }
+                    return;
+                }
+            }
+            if (debug) {
+                trace("Http data received " + inMessage.state + " " + 
+                        net.getIn() + " headerDone=" + headersDone);
+            }
+
+            if (inMessage.state == HttpMessage.State.HEAD) {
+                headDataReceived();
+                if (inMessage.state == HttpMessage.State.HEAD) {
+                    return; // still parsing head
+                }
+                if (serverMode && httpReq.decodedUri.remaining() == 0) {
+                    abort("Invalid url");
+                }
+            } 
+
+            if (inMessage.state == HttpMessage.State.BODY_DATA) {
+                if (net != null) {
+                    receiveBody.rawDataReceived(net.getIn());
+                }
+            }
+            
+            // Send header callbacks - we process any incoming data 
+            // first, so callbacks have more info
+            if (httpService != null && !headersDone) {
+                headersDone = true;
+                try {
+                    httpService.service(getRequest(), getResponse());
+                } catch (Throwable t) {
+                    t.printStackTrace();
+                    abort(t);
+                }
+            }
+
+            // If header callback or previous dataReceived() hasn't consumed all 
+            if (receiveBody.getBufferCount() > 0) {
+                // Has data 
+                super.sendHandleReceivedCallback(); // callback
+            }
+
+            // Receive has marked the body as closed
+            if (receiveBody.isAppendClosed() 
+                    && inMessage.state != HttpMessage.State.DONE) {
+                if (net != null && net.getIn().getBufferCount() > 0) {
+                    if (debug) {
+                        trace("Pipelined requests"); // may be a crlf
+                    }
+                }                
+                handleEndReceive(receiveBody.frameError);
+            }
+
+            if (inMessage.state == HttpMessage.State.DONE) {
+                // TCP end ? 
+                if (net == null || net.getIn() == null) {
+                    trace("NO NET");
+                    return;
+                }
+                if (net.getIn().isClosedAndEmpty()) {
+                    // If not already closed.
+                    closeStreamOnEnd("closed on input 3");
+                    if (outMessage.state == HttpMessage.State.DONE) {
+                        // need to flush out.
+                        net.getOut().close();
+                        flushNet();
+                    }
+                } else {
+                    // Next request, ignore it.
+                }
+
+            }
+        } catch (IOException ex) {
+            ex.printStackTrace();
+            abort(ex);
+        }
+    }
+
+    /** 
+     * Read and process a chunk of head, called from dataReceived() if 
+     * in HEAD mode.
+     * 
+     * @return <= 0 - still in head mode. > 0 moved to body mode, some 
+     * body chunk may have been received. 
+     */
+    protected void headDataReceived() throws IOException {
+        while (true) {
+            // we know we have one
+            int read = net.getIn().readLine(headRecvBuf);
+            if (read < 0) {
+                if (debug) {
+                    trace("CLOSE while reading HEAD");    
+                }
+                // too early - we don't have the head
+                abort("Close in head");
+                return;
+            }
+            // Remove starting empty lines.
+            headRecvBuf.skipEmptyLines();
+
+            // Do we have another full line in the input ?
+            if (BBuffer.hasLFLF(headRecvBuf)) {
+                break;
+            }
+            if (read == 0) { // no more data
+                return;
+            }
+        }
+        headRecvBuf.wrapTo(headB);
+
+        
+        parseMessage(headB);
+
+        
+        if (debug) {
+            trace("HEAD_RECV " + getRequest().requestURI() + " " + 
+                    getResponse().getMsgBytes().status() + " " + net.getIn());
+        }
+        
+    }
+    
+    public void parseMessage(BBuffer headB) throws IOException {
+        //Parse the response
+        headB.readLine(line);
+        
+        HttpMessageBytes msgBytes;
+
+        if (serverMode) {
+            msgBytes = httpReq.getMsgBytes();
+            parseRequestLine(line, msgBytes.method(),
+                    msgBytes.url(),
+                    msgBytes.query(),
+                    msgBytes.protocol());
+        } else {
+            msgBytes = httpRes.getMsgBytes();
+            parseResponseLine(line, msgBytes.protocol(), 
+                    msgBytes.status(), msgBytes.message());
+        }
+        
+        parseHeaders(msgBytes, headB);
+
+        inMessage.state = HttpMessage.State.BODY_DATA;
+        
+        // TODO: hook to allow specific charsets ( can be done later )
+
+        inMessage.processReceivedHeaders();
+    }
+
+    private void init() {
+        headRecvBuf.recycle();
+        headersDone = false;
+        sendReceiveDone = false;
+        
+        receiveBody.recycle();
+        sendBody.recycle();
+        expectation = false;
+        
+        http11 = false;
+        http09 = false;
+        error = false;
+        abortDone = false;
+       
+        
+        getRequest().recycle();
+        getResponse().recycle();
+        host = null;
+        filters.clear();
+        
+        line.recycle();
+        headB.recycle();
+        
+        doneLock.recycle();
+        flushLock.recycle();
+        
+        doneCallbackCalled = false;
+        keepAlive = true;
+        // Will be set again after pool
+        setHttpService(null);
+        doneAllCallback = null;
+        release = false;
+    }
+    
+    public boolean isDone() {
+        return outMessage.state == HttpMessage.State.DONE && inMessage.state == HttpMessage.State.DONE;
+    }
+    
+    public boolean keepAlive() {
+        if (http09) {
+            return false;
+        }
+        return keepAlive;
+    }
+
+    /**
+     * Called when all done:
+     *  - service finished ( endService was called )
+     *  - output written
+     *  - input read
+     *  
+     * or by abort(). 
+     *  
+     * @throws IOException 
+     */
+    private void maybeRelease() throws IOException {
+        synchronized (this) {
+            if (release && sendReceiveDone) {
+                if (debug) {
+                    trace("RELEASE");
+                }
+                if (getConnector() != null) {
+                    getConnector().returnToPool(this);
+                } else {
+                    log.severe("Attempt to release with no pool");
+                }
+            }
+        }
+    }
+
+
+    
+    /*
+    The field-content does not include any leading or trailing LWS: 
+    linear white space occurring before the first non-whitespace 
+    character of the field-value or after the last non-whitespace
+     character of the field-value. Such leading or trailing LWS MAY 
+     be removed without changing the semantics of the field value. 
+     Any LWS that occurs between field-content MAY be replaced with 
+     a single Http11Parser.SP before interpreting the field value or forwarding 
+     the message downstream.
+     */
+    int normalizeHeader(BBuffer value) {
+        byte[] buf = value.array();
+        int cstart = value.position();
+        int end = value.limit();
+        
+        int realPos = cstart;
+        int lastChar = cstart;
+        byte chr = 0;
+        boolean gotSpace = true;
+
+        for (int i = cstart; i < end; i++) {
+            chr = buf[i];
+            if (chr == BBuffer.CR) {
+                // skip
+            } else if(chr == BBuffer.LF) {
+                // skip
+            } else if (chr == BBuffer.SP || chr == BBuffer.HT) {
+                if (gotSpace) {
+                    // skip
+                } else {
+                    buf[realPos++] = BBuffer.SP;
+                    gotSpace = true;
+                }
+            } else {
+                buf[realPos++] = chr;
+                lastChar = realPos; // to skip trailing spaces
+                gotSpace = false;
+            }
+        }
+        realPos = lastChar;
+        
+        // so buffer is clean
+        for (int i = realPos; i < end; i++) {
+            buf[i] = BBuffer.SP;
+        }
+        value.setEnd(realPos);
+        return realPos;
+    }  
+    
+    
+    /**
+     * Parse one header. 
+     * Line must be populated. On return line will be populated
+     * with the next header:
+     * 
+     * @param line current header line, not empty.
+     */
+    public int parseHeader(BBuffer head, 
+            BBuffer line, BBuffer name, BBuffer value)
+          throws IOException {
+        
+        int newPos = line.readToDelimOrSpace(COLON, name);
+        line.skipSpace();
+        if (line.readByte() != COLON) {
+            throw new IOException("Missing ':' in header name " + line);
+        }
+        line.skipSpace();
+        line.read(value); // remaining of the line
+        
+        while (true) {
+            head.readLine(line);
+            if (line.remaining() == 0) {
+                break;
+            }
+            byte first = line.get(0);
+            if (first != BBuffer.SP && first != BBuffer.HT) {
+                break;
+            }
+            // continuation line - append it to value
+            value.setEnd(line.getEnd());
+            line.position(line.limit());
+        }
+
+        // We may want to keep the original and use separate buffer ?
+        normalizeHeader(value);
+        return 1;
+    }
+    
+    public void parseHeaders(HttpMessageBytes msgBytes,
+            BBuffer head) 
+                throws IOException {
+        
+        head.readLine(line);
+        
+        int idx = 0;
+        while(line.remaining() > 0) {
+            // not empty..
+            idx = msgBytes.addHeader();
+            BBuffer nameBuf = msgBytes.getHeaderName(idx);
+            BBuffer valBuf = msgBytes.getHeaderValue(idx);
+            parseHeader(head, line, nameBuf, valBuf);
+            
+            // TODO: process 'interesting' headers here.
+        }
+    }
+
+    /**
+     * Read the request line. This function is meant to be used during the 
+     * HTTP request header parsing. Do NOT attempt to read the request body 
+     * using it.
+     *
+     * @throws IOException If an exception occurs during the underlying socket
+     * read operations, or if the given buffer is not big enough to accomodate
+     * the whole line.
+     */
+    public boolean parseRequestLine(BBuffer line, 
+            BBuffer methodMB, BBuffer requestURIMB,
+            BBuffer queryMB,
+            BBuffer protoMB)
+        throws IOException {
+
+        line.readToSpace(methodMB);
+        line.skipSpace();
+        
+        line.readToDelimOrSpace(QUESTION, requestURIMB);
+        if (line.remaining() > 0 && line.get(0) == QUESTION) {
+            // Has query
+            line.readToSpace(queryMB);
+            // don't include '?'
+            queryMB.position(queryMB.position() + 1);
+        } else {
+            queryMB.setBytes(line.array(), line.position(), 0);
+        }
+        line.skipSpace();
+
+        line.readToSpace(protoMB);
+        
+        // proto is optional ( for 0.9 )
+        return requestURIMB.remaining() > 0;
+    }
+
+    public boolean parseResponseLine(BBuffer line,
+            BBuffer protoMB, BBuffer statusCode, BBuffer status)
+            throws IOException {
+        line.skipEmptyLines();
+
+        line.readToSpace(protoMB);
+        line.skipSpace();
+        line.readToSpace(statusCode);
+        line.skipSpace();
+        line.wrapTo(status);
+        
+        // message may be empty
+        return statusCode.remaining() > 0;
+    }
+
+    /**
+     * Update keepAlive based on Connection header and protocol.
+     */
+    void processConnectionHeader(MultiMap headers) {
+        if (http09) {
+            return;
+        }
+
+        CBuffer value = headers.getHeader(HttpChannel.CONNECTION);
+        String conHeader = (value == null) ? null : value.toString();
+        if (conHeader != null) {
+            if (HttpChannel.CLOSE.equalsIgnoreCase(conHeader)) {
+                closeStreamOnEnd("connection close");
+            }
+            if (!HttpChannel.KEEPALIVE_S.equalsIgnoreCase(conHeader)) {
+                closeStreamOnEnd("connection != keep alive");
+            }
+        } else {
+            // no connection header
+            if (!http11) {
+                closeStreamOnEnd("http1.0 no connection header");
+            }
+        }
+    }
+
+    void processExpectation() throws IOException {
+        expectation = false;
+        MultiMap headers = getRequest().getMimeHeaders();
+
+        CBuffer expect = headers.getHeader("expect");
+        if ((expect != null)
+                && (expect.indexOf("100-continue") != -1)) {
+            expectation = true;
+
+            // TODO: configure, use the callback or the servlet 'read'. 
+            net.getOut().append("HTTP/1.1 100 Continue\r\n\r\n");
+            net.startSending();
+        }
+    }
+
+    void processProtocol() throws IOException {
+        http11 = true;
+        http09 = false;
+        
+        CBuffer protocolMB = getRequest().protocol();
+        if (protocolMB.equals(HttpChannel.HTTP_11)) {
+            http11 = true;
+        } else if (protocolMB.equals(HttpChannel.HTTP_10)) {
+            http11 = false;
+        } else if (protocolMB.equals("")) {
+            http09 = true;
+            http11 = false;
+        } else {
+            // Unsupported protocol
+            http11 = false;
+            error = true;
+            // Send 505; Unsupported HTTP version
+            getResponse().setStatus(505);
+            abort("Invalid protocol");
+        }
+    }
+
+    protected void recycle() {
+        if (debug) { 
+            trace("RECYCLE");
+        }
+        init();
+    }
+
+    /** 
+     * Finalize sending and receiving. 
+     * Indicates client is no longer interested, some IO may still be in flight.
+     * If in a POST and you're not interested in the body - it may be
+     * better to call abort().
+     *  
+     * MUST be called to allow connection reuse and pooling.
+     * 
+     * @throws IOException 
+     */
+    public void release() throws IOException {
+        synchronized(this) {
+            if (release) {
+                return;
+            }
+            trace("RELEASE");
+            release = true;
+            // If send/receive is done - we can reuse this object
+            maybeRelease();
+        }
+    }
+
+    public void resetBuffers() {
+        if (net != null) {
+            net.setDataFlushedCallback(null);
+            net.setDataReceivedCallback(null);
+            setSink(null);
+        }
+    }
+
+    public void sendHeaders() throws IOException {
+        checkRelease();
+        if (serverMode) {
+            sendResponseHeaders();
+        } else {
+            sendRequest();
+        }
+    }
+
+    /** 
+     * Can be called from any thread.
+     * 
+     * @param host
+     * @param port
+     * @throws IOException
+     */
+    public void sendRequest() throws IOException {
+        if (getRequest().isCommitted()) {
+            return;
+        }
+        getRequest().setCommitted(true);
+
+        String target = host + ":" + port;
+        
+        if (getRequest().getMimeHeaders().getHeader("Host") == null
+                && host != null) {
+            CBuffer hostH = getRequest().getMimeHeaders().addValue("Host");
+            hostH.set(host); // TODO: port
+        }
+        
+        outMessage.state = HttpMessage.State.HEAD;
+
+        IOChannel ch = getConnector().cpool.getChannel(target);
+
+        if (ch == null) {
+            if (debug) {
+                trace("HTTP_CONNECT: New connection " + target);
+            }
+            IOConnector.ConnectedCallback connected = new IOConnector.ConnectedCallback() {
+                @Override
+                public void handleConnected(IOChannel ch) throws IOException {
+                    if (httpConnector.debugHttp) {
+                        IOChannel ch1 = new DumpChannel("");
+                        ch.addFilterAfter(ch1);
+                        ch = ch1;                        
+                    }
+                    
+                    sendRequestHeaders(ch);
+                }
+            };
+            getConnector().getIOConnector().connect(host, port, connected);
+        } else {
+            if (debug) {
+                trace("HTTP_CONNECT: Reuse connection " + target + " " + this);
+            }
+            // TODO retry if closed
+            sendRequestHeaders(ch);
+        }
+    }
+
+    /** 
+     * Used in request mode.  
+     * 
+     * @throws IOException
+     */
+    void sendRequestHeaders(IOChannel ch) throws IOException {
+        if (getConnector() == null) {
+            throw new IOException("after release");
+        }
+        if (!ch.isOpen()) {
+            abort("Closed channel");
+            return;
+        }
+        setChannel(ch); // register read/write callbacks
+        
+        // Update transfer fields based on headers.
+        processProtocol();
+        
+        processConnectionHeader(getRequest().getMimeHeaders());
+
+
+        // 1.0: The presence of an entity body in a request is signaled by 
+        // the inclusion of a Content-Length header field in the request 
+        // message headers. HTTP/1.0 requests containing an entity body 
+        // must include a valid Content-Length header field.
+
+        if (!sendBody.isContentDelimited()) {
+            // Will not close connection - just flush and mark the body 
+            // as sent
+            sendBody.noBody = true;
+            getOut().close();
+        }
+
+        if (sendBody.noBody) {
+            getRequest().getMimeHeaders().remove(HttpChannel.CONTENT_LENGTH);
+            getRequest().getMimeHeaders().remove(HttpChannel.TRANSFERENCODING);
+        } else {
+            long contentLength = 
+                getRequest().getContentLength();
+            if (contentLength < 0) {
+                getRequest().getMimeHeaders().addValue(HttpChannel.TRANSFERENCODING).
+                    set(HttpChannel.CHUNKED);
+            }
+            sendBody.processContentDelimitation();
+        }
+
+        sendBody.updateCloseOnEnd();
+
+        try {
+            getRequest().serialize(net.getOut());
+            if (debug) {
+                trace("S: \n" + net.getOut());
+            }
+
+        } catch (Throwable t) {
+            log.log(Level.SEVERE, "Error sending request", t);
+        }
+
+        if (outMessage.state == HttpMessage.State.HEAD) {
+            outMessage.state = HttpMessage.State.BODY_DATA;
+        }
+
+        // TODO: add any body and flush. More body can be added later - 
+        // including 'end'.
+
+        startSending();
+        
+    }
+
+    /**
+     * When committing the response, we have to validate the set of headers, as
+     * well as setup the response filters.
+     * Only in server mode.
+     */
+    void sendResponseHeaders() throws IOException {
+        checkRelease();
+        if (!serverMode) {
+            throw new IOException("Only in server mode");
+        }
+
+        if (getResponse().isCommitted()) {
+            return; 
+        }
+        getResponse().setCommitted(true);
+        
+        sendBody.noBody = !getResponse().hasBody();
+
+        if (sendBody.statusDropsConnection(getResponse().getStatus())) {
+            closeStreamOnEnd("status drops connection");
+        }
+        if (error) {
+            closeStreamOnEnd("error");
+        }
+
+        // A header explicitely set.
+        CBuffer transferEncHeader = 
+            getResponse().getMimeHeaders().getHeader(HttpChannel.TRANSFERENCODING);
+        if (!sendBody.noBody 
+                && keepAlive()) {
+            if (getResponse().getContentLength() < 0) {
+                // Use chunked by default, if no c-l
+                if (transferEncHeader == null) {
+                    getResponse().getMimeHeaders().addValue(HttpChannel.TRANSFERENCODING).set(HttpChannel.CHUNKED);
+                } else {
+                    transferEncHeader.set(HttpChannel.CHUNKED);                    
+                }
+            }
+        }
+        
+        sendBody.processContentDelimitation();
+        
+        sendBody.updateCloseOnEnd();
+        
+        MultiMap headers = getResponse().getMimeHeaders();
+
+        // Add date header
+        if (headers.getHeader("Date") == null) {
+            headers.setValue("Date").set(FastHttpDateFormat.getCurrentDate());
+        }
+
+        // Add server header
+        if (serverHeader.length() > 0) {
+            headers.setValue("Server").set(serverHeader);
+        }
+
+        // did the user set a connection header that may override what we have ?
+        processConnectionHeader(headers);
+        
+        if (!keepAlive()) {
+            headers.setValue(HttpChannel.CONNECTION).set(HttpChannel.CLOSE);
+        } else {
+            if (!http11 && !http09) {
+                headers.setValue(HttpChannel.CONNECTION).set(HttpChannel.KEEPALIVE_S);                
+            }
+        }
+    
+        if (debug) {
+            trace("Send response headers " + net);
+        }
+        if (net != null) {
+            getResponse().serialize(net.getOut());
+        }
+        
+        if (outMessage.state == HttpMessage.State.HEAD) {
+            outMessage.state = HttpMessage.State.BODY_DATA;
+        }
+        
+        if (sendBody.isDone()) {
+            getOut().close();
+        }
+
+        if (net != null) {
+            net.startSending();
+        }
+    }
+
+    public HttpChannel serverMode(boolean enabled) {
+        if (enabled) {
+            serverMode = true;
+            dbgName = "AsyncHttpServer";
+            httpReq.setBody(receiveBody);
+            httpRes.setBody(sendBody);
+            sendBody.setMessage(httpRes);
+            receiveBody.setMessage(httpReq);
+            inMessage = httpReq;
+            outMessage = httpRes;
+        } else {
+            serverMode = false;
+            dbgName = "AsyncHttp";         
+            httpReq.setBody(sendBody);
+            httpRes.setBody(receiveBody);
+            sendBody.setMessage(httpReq);
+            receiveBody.setMessage(httpRes);
+            inMessage = httpRes;
+            outMessage = httpReq;
+        }
+        if (debug) {
+            log = Logger.getLogger(dbgName);
+        }
+        return this;
+    }
+    
+    public void setChannel(IOChannel ch) throws IOException {
+        for (IOChannel filter: filters) {
+            ch.addFilterAfter(filter);
+            ch = filter;
+        }
+        
+        withBuffers(ch);
+    }
+    
+    public void setCompletedCallback(RequestCompleted doneAllCallback) 
+            throws IOException {
+        this.doneAllCallback = doneAllCallback;
+        synchronized (this) {
+            if (doneCallbackCalled) {
+                return;
+            }
+            if (outMessage.state != HttpMessage.State.DONE || inMessage.state != HttpMessage.State.DONE) {
+                return;
+            }
+        }
+        doneCallbackCalled = true;
+        if (doneAllCallback != null) {
+            doneAllCallback.handle(this, error ? new Throwable() : null);
+        }
+    }
+
+    public void setConnector(HttpConnector pool) {
+        this.httpConnector = pool;
+    }
+
+    public void setHttpService(HttpService headersReceivedCallback) {
+        this.httpService = headersReceivedCallback;
+    }
+
+    public void setIOTimeout(long timeout) {
+        ioTimeout = timeout;
+    }
+
+    public void setTarget(String host, int port) {
+        this.host = host;
+        this.port = port;
+    }
+    
+    public void startSending() throws IOException {
+        checkRelease();
+        
+        sendBody.flushToNext();
+        flushNet();
+    }
+    
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append("id=").append(ser)
+            .append(",rs=").append(getState())
+            .append(")");
+        return sb.toString();
+    }
+    
+
+    void trace(String msg) {
+        if(debug) {
+            log.info(this.toString() + " " + msg + " done=" + doneCallbackCalled);
+        }
+    }
+    public void waitFlush(long timeMs) throws IOException {
+        if (getOut().getBufferCount() == 0) {
+            return;
+        }
+        flushLock.waitSignal(timeMs);
+    }
+    public HttpChannel withBuffers(IOChannel net) {
+        setSink(net);
+        net.setDataFlushedCallback(this);
+        net.setDataReceivedCallback(this);
+        return this;
+    }
+    /**
+     * Normalize URI.
+     * <p>
+     * This method normalizes "\", "//", "/./" and "/../". This method will
+     * return false when trying to go above the root, or if the URI contains
+     * a null byte.
+     * 
+     * @param uriMB URI to be normalized, will be modified
+     */
+    public static boolean normalize(BBuffer uriBC) {
+
+        byte[] b = uriBC.array();
+        int start = uriBC.getStart();
+        int end = uriBC.getEnd();
+
+        // URL * is acceptable
+        if ((end - start == 1) && b[start] == (byte) '*')
+            return true;
+
+        if (b[start] != '/') {
+            // TODO: http://.... URLs
+            return true;
+        }
+        
+        int pos = 0;
+        int index = 0;
+
+        // Replace '\' with '/'
+        // Check for null byte
+        for (pos = start; pos < end; pos++) {
+            if (b[pos] == (byte) '\\')
+                b[pos] = (byte) '/';
+            if (b[pos] == (byte) 0)
+                return false;
+        }
+
+        // The URL must start with '/'
+        if (b[start] != (byte) '/') {
+            return false;
+        }
+
+        // Replace "//" with "/"
+        for (pos = start; pos < (end - 1); pos++) {
+            if (b[pos] == (byte) '/') {
+                while ((pos + 1 < end) && (b[pos + 1] == (byte) '/')) {
+                    copyBytes(b, pos, pos + 1, end - pos - 1);
+                    end--;
+                }
+            }
+        }
+
+        // If the URI ends with "/." or "/..", then we append an extra "/"
+        // Note: It is possible to extend the URI by 1 without any side effect
+        // as the next character is a non-significant WS.
+        if (((end - start) >= 2) && (b[end - 1] == (byte) '.')) {
+            if ((b[end - 2] == (byte) '/') 
+                    || ((b[end - 2] == (byte) '.') 
+                            && (b[end - 3] == (byte) '/'))) {
+                b[end] = (byte) '/';
+                end++;
+            }
+        }
+
+        uriBC.setEnd(end);
+
+        index = 0;
+
+        // Resolve occurrences of "/./" in the normalized path
+        while (true) {
+            index = uriBC.indexOf("/./", 0, 3, index);
+            if (index < 0)
+                break;
+            copyBytes(b, start + index, start + index + 2, 
+                    end - start - index - 2);
+            end = end - 2;
+            uriBC.setEnd(end);
+        }
+
+        index = 0;
+
+        // Resolve occurrences of "/../" in the normalized path
+        while (true) {
+            index = uriBC.indexOf("/../", 0, 4, index);
+            if (index < 0)
+                break;
+            // Prevent from going outside our context
+            if (index == 0)
+                return false;
+            int index2 = -1;
+            for (pos = start + index - 1; (pos >= 0) && (index2 < 0); pos --) {
+                if (b[pos] == (byte) '/') {
+                    index2 = pos;
+                }
+            }
+            copyBytes(b, start + index2, start + index + 3,
+                    end - start - index - 3);
+            end = end + index2 - index - 3;
+            uriBC.setEnd(end);
+            index = index2;
+        }
+
+        //uriBC.setBytes(b, start, end);
+        uriBC.setEnd(end);
+        return true;
+
+    }
+
+    /**
+     * Copy an array of bytes to a different position. Used during 
+     * normalization.
+     */
+    private static void copyBytes(byte[] b, int dest, int src, int len) {
+        for (int pos = 0; pos < len; pos++) {
+            b[pos + dest] = b[pos + src];
+        }
+    }
+    
+    
+    
+    public static interface HttpService {
+        void service(HttpRequest httpReq, HttpResponse httpRes) throws IOException;
+    }
+    
+    public static interface RequestCompleted {
+        void handle(HttpChannel data, Object extraData) throws IOException;
+    }
+
+    
+}
\ No newline at end of file
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpConnector.java
new file mode 100644 (file)
index 0000000..3bad3b0
--- /dev/null
@@ -0,0 +1,625 @@
+/*
+ */
+package org.apache.tomcat.lite.http;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+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;
+
+import org.apache.tomcat.lite.http.HttpChannel.HttpService;
+import org.apache.tomcat.lite.io.DumpChannel;
+import org.apache.tomcat.lite.io.BBucket;
+import org.apache.tomcat.lite.io.IOChannel;
+import org.apache.tomcat.lite.io.IOConnector;
+
+/**
+ * Manages HttpChannels and associated socket pool.
+ * 
+ * 
+ * @author Costin Manolache
+ */
+public class HttpConnector {
+    
+    public static interface HttpChannelEvents {
+        /** HttpChannel object created. It'll be used many times.
+         * @throws IOException 
+         */
+        public void onCreate(HttpChannel ch, HttpConnector con) throws IOException;
+        
+        /** 
+         * HttpChannel object no longer needed, out of pool.
+         * @throws IOException 
+         */
+        public void onDestroy(HttpChannel ch, HttpConnector con) throws IOException;
+    }
+    
+    private static Logger log = Logger.getLogger("HttpConnector");
+    private int maxHttpPoolSize = 20;
+    
+    private int maxSocketPoolSize = 100;
+    private int keepAliveTimeMs = 30000;
+    
+    private Queue<HttpChannel> httpChannelPool = new ConcurrentLinkedQueue<HttpChannel>();
+
+    private IOConnector ioConnector;
+    
+    boolean debugHttp = false;
+    boolean debug = false;
+    
+    boolean clientKeepAlive = true;
+    boolean serverKeepAlive = true;
+
+    HttpChannelEvents httpEvents;
+    
+    public AtomicInteger inUse = new AtomicInteger();
+    public AtomicInteger newHttpChannel = new AtomicInteger();
+    public AtomicInteger totalHttpChannel = new AtomicInteger();
+    public AtomicInteger totalClientHttpChannel = new AtomicInteger();
+    public AtomicInteger recycledChannels = new AtomicInteger();
+    public AtomicInteger reusedChannels = new AtomicInteger();
+
+    public ConnectionPool cpool = new ConnectionPool();
+    
+        
+    public HttpConnector(IOConnector ioConnector) {
+        this.ioConnector = ioConnector;
+        dispatcher = new Dispatcher();
+        defaultService = dispatcher;
+    }
+
+    protected HttpConnector() {
+        this(null);
+    }
+    
+    public Dispatcher getDispatcher() {
+        return dispatcher;
+    }
+    
+    public HttpConnector withIOConnector(IOConnector selectors) {
+        ioConnector = selectors;
+        return this;
+    }
+    
+    public void setDebug(boolean b) {
+        this.debug = b;
+    }
+
+    public void setDebugHttp(boolean b) {
+        this.debugHttp  = b;
+    }
+
+    public void setClientKeepAlive(boolean b) {
+        this.clientKeepAlive = b;
+    }
+
+    public void setServerKeepAlive(boolean b) {
+        this.serverKeepAlive = b;
+    }
+    
+    public boolean isDebug() {
+        return debug;
+    }
+
+    public boolean isClientKeepAlive() {
+        return clientKeepAlive;
+    }
+
+    public boolean isServerKeepAlive() {
+        return serverKeepAlive;
+    }
+
+    public int getInUse() {
+        return inUse.get();
+    }
+
+    public int getMaxHttpPoolSize() {
+        return maxHttpPoolSize;
+    }
+    
+    public void setMaxHttpPoolSize(int maxHttpPoolSize) {
+        this.maxHttpPoolSize = maxHttpPoolSize;
+    }
+
+    public void setOnCreate(HttpChannelEvents callback) {
+        httpEvents = callback;
+    }
+    
+    /**
+     *  Override to create customized client/server connections.
+     * 
+     * @return
+     * @throws IOException 
+     */
+    protected HttpChannel create() throws IOException {
+        HttpChannel res = new HttpChannel();
+        newHttpChannel.incrementAndGet();
+        res.setConnector(this);
+        if (httpEvents != null) {
+            httpEvents.onCreate(res, this);
+        }
+        if (debugHttp) {
+            res.debug = debugHttp;
+        }
+        return res;
+    }
+
+    public HttpChannel get(String host, int port) throws IOException {
+        HttpChannel http = get(false, host, port);
+        http.setTarget(host, port);
+        return http;
+    }
+    
+    public HttpChannel getServer() {
+        try {
+            return get(true, null, 0);
+        } catch (IOException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+            return null;
+        }
+    }
+    
+    /**
+     * Get an existing AsyncHttp object. Since it uses many buffers and 
+     * objects - it's more efficient to pool it. 
+     * 
+     * release will return the object to the pool.
+     * @throws IOException 
+     */
+    public HttpChannel get(CharSequence urlString) throws IOException {
+        URL url = new URL(urlString.toString());
+        String host = url.getHost();
+        int port = url.getPort();
+        boolean secure = "http".equals(url.getAuthority()); 
+        if (port == -1) {
+            port = secure ? 443: 80;
+        }
+        // TODO: insert SSL filter
+        HttpChannel http = get(false, host, port);
+        http.setTarget(host, port);
+        String path = url.getFile(); // path + qry
+        // TODO: query string
+        http.getRequest().requestURI().set(path);
+        return http;
+    }
+    
+    protected HttpChannel get(boolean server, CharSequence host, int port) throws IOException {
+        HttpChannel processor = null;
+        synchronized (httpChannelPool) {
+            processor = httpChannelPool.poll();
+        }
+        boolean reuse = false;
+        totalHttpChannel.incrementAndGet();
+        if (processor == null) {
+            processor = create();
+        } else {
+            reuse = true;
+            reusedChannels.incrementAndGet();
+            processor.release = false;
+        }
+        if (!server) {
+            totalClientHttpChannel.incrementAndGet();
+        }
+        processor.serverMode(server);
+        if (debug) {
+            log.info((reuse ? "REUSE ": "Create ")
+                    + host + ":" + port + 
+                    (server? " S" : "")
+                    + " id=" + processor.ser + 
+                    " " + processor +
+                    " size=" + httpChannelPool.size());
+        }
+        
+        processor.setConnector(this);
+        inUse.incrementAndGet();
+        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
+        if (serverMode) {
+            BBucket first = ch.getIn().peekFirst();
+            if (first != null) {
+                HttpChannel http = getServer();
+                if (debug) {
+                    http.trace("PIPELINED request " + first + " " + http.httpService); 
+                }
+                http.setChannel(ch);
+                http.setHttpService(defaultService);
+                
+                // In case it was disabled
+                if (ch != null) {
+                    if (ch.isOpen()) {
+                        ch.readInterest(true);
+                    }
+                    // Notify that data was received. The callback must deal with
+                    // pre-existing data.
+                    ch.sendHandleReceivedCallback();
+                }
+                http.handleReceived(http.getSink());
+                return;
+            }
+        }
+        if (serverMode && !serverKeepAlive) {
+            keepOpen = false;
+        }
+        if (!serverMode && !clientKeepAlive) {
+            keepOpen = false;
+        }
+        
+
+        if (keepOpen) {
+            // reuse the socket
+            if (serverMode) {
+                if (debug) {
+                    log.info(">>> server socket KEEP_ALIVE " + ch.getTarget() + 
+                            " " + ch);
+                }
+                ch.readInterest(true);
+                ch.setDataReceivedCallback(receiveCallback);
+                ch.setDataFlushedCallback(null);
+                
+                cpool.returnChannel(ch);
+                // TODO: timeout event to close it
+                //                ch.setTimer(10000, new Runnable() {
+                //                    @Override
+                //                    public void run() {
+                //                        System.err.println("Keep alive timeout");
+                //                    }
+                //                });
+            } else {
+                if (debug) {
+                    log.info(">>> client socket KEEP_ALIVE " + ch.getTarget() + 
+                            " " + ch);
+                }
+                ch.readInterest(true);
+                ch.setDataReceivedCallback(clientReceiveCallback);
+                ch.setDataFlushedCallback(null);
+                
+                cpool.returnChannel(ch);
+            }
+        } else { 
+            if (debug) {
+                log.info("--- Close socket, no keepalive " + ch);
+            }
+            if (ch != null) {
+                ch.close();
+            }
+        }
+    }
+    
+    protected void returnToPool(HttpChannel http) throws IOException {
+        inUse.decrementAndGet();
+        recycledChannels.incrementAndGet();
+        if (debug) {
+            log.info("Return " + http.getTarget() + " obj=" +
+                http + " size=" + httpChannelPool.size());
+        }
+        
+        http.recycle();
+        
+        // No more data - release the object
+        synchronized (httpChannelPool) {
+            http.resetBuffers();
+            http.setConnector(null);
+            if (httpChannelPool.contains(http)) {
+                System.err.println("dup ? ");                
+            }
+            if (httpChannelPool.size() >= maxHttpPoolSize) {
+                if (httpEvents != null) {
+                    httpEvents.onDestroy(http, this);
+                }
+            } else {
+                httpChannelPool.add(http);
+            }
+        }
+    }
+    
+    
+    public IOConnector getIOConnector() {
+        return ioConnector;
+    }
+    
+    // Host + context mapper.
+    Dispatcher dispatcher;
+    HttpService defaultService;
+    int port = 8080;
+    
+    
+    public void setHttpService(HttpService s) {
+        defaultService = s;
+    }
+    
+    public void start() throws IOException {
+        if (ioConnector != null) {
+            ioConnector.acceptor(new AcceptorCallback(this, defaultService), 
+                    Integer.toString(port), null);
+        }
+    }
+    
+    /** 
+     * 
+     * TODO: only clean our state and sockets we listen on. 
+     *  
+     */
+    public void stop() {
+        if (ioConnector != null) {
+            ioConnector.stop();
+        }
+    }
+    
+    private static class AcceptorCallback implements IOConnector.ConnectedCallback {
+        HttpConnector httpCon;
+        HttpService callback;
+        
+        public AcceptorCallback(HttpConnector asyncHttpConnector,
+                HttpService headersReceived) {
+            this.httpCon = asyncHttpConnector;
+            this.callback = headersReceived;
+        }
+
+        @Override
+        public void handleConnected(IOChannel accepted) throws IOException {
+            HttpChannel shttp = httpCon.getServer();
+            if (callback != null) {
+                shttp.setHttpService(callback);
+            }
+            if (httpCon.debugHttp) {
+                IOChannel ch = new DumpChannel("");
+                accepted.addFilterAfter(ch);
+                shttp.setChannel(ch);
+            } else {
+                shttp.setChannel(accepted);
+            }
+            // TODO: JSSE filter
+            
+
+            // Will read any data in the channel.
+            
+            accepted.handleReceived(accepted);
+        }
+
+    }
+
+
+    private IOConnector.DataReceivedCallback receiveCallback = 
+        new IOConnector.DataReceivedCallback() {
+        /** For keepalive - for server
+         * 
+         * @param peer
+         * @throws IOException
+         */
+        @Override
+        public void handleReceived(IOChannel net) throws IOException {
+            cpool.stopKeepAlive(net);
+            if (!net.isOpen()) {
+                return;
+            }
+            HttpChannel shttp = getServer();
+            shttp.setChannel(net);
+            shttp.setHttpService(defaultService);
+            net.sendHandleReceivedCallback();
+        }
+    };
+
+
+    // Sate-less, just closes the net.
+    private IOConnector.DataReceivedCallback clientReceiveCallback = 
+        new IOConnector.DataReceivedCallback() {
+        
+        @Override
+        public void handleReceived(IOChannel net) throws IOException {
+            if (!net.isOpen()) {
+                cpool.stopKeepAlive(net);
+                return;
+            }
+            log.warning("Unexpected message from server in client keep alive " 
+                    + net.getIn());
+            if (net.isOpen()) {
+                net.close();
+            }
+        }
+        
+    };
+
+    public HttpConnector setPort(int port2) {
+        this.port = port2;
+        return this;
+    }
+    
+    /** 
+     * Connections for one remote host.
+     * This should't be restricted by IP:port or even hostname,
+     * for example if a server has multiple IPs or LB replicas - any would work.   
+     */
+    public static class RemoteServer {
+        public ConnectionPool pool;
+        public ArrayList<IOChannel> connections = new ArrayList<IOChannel>();
+    }
+
+    
+    // TODO: add timeouts, limits per host/total, expire old entries 
+    // TODO: discover apr and use it
+    
+    public class ConnectionPool {
+        // visible for debugging - will be made private, with accessor 
+        /**
+         * Map from client names to socket pools.
+         */
+        public Map<CharSequence, RemoteServer> hosts = new HashMap<CharSequence, 
+            RemoteServer>();
+        boolean keepOpen = true;
+
+        // Statistics
+        public AtomicInteger waitingSockets = new AtomicInteger();
+        public AtomicInteger closedSockets = new AtomicInteger();
+
+        public AtomicInteger hits = new AtomicInteger();
+        public AtomicInteger misses = new AtomicInteger();
+
+        Timer timer;
+        
+        public int getTargetCount() {
+            return hosts.size();
+        }
+
+        public int getSocketCount() {
+            return waitingSockets.get();
+        }
+
+        public int getClosedSockets() {
+            return closedSockets.get();
+        }
+
+        public String dumpSockets() {
+            StringBuffer sb = new StringBuffer();
+            for (CharSequence k: hosts.keySet()) {
+                RemoteServer t = hosts.get(k);
+                sb.append(k).append("=").append(t.connections.size()).append("\n");
+            }
+            return sb.toString();
+        }
+
+        public Set<CharSequence> getKeepAliveTargets() {
+            return hosts.keySet();
+        }
+
+        /** 
+         * @param key host:port, or some other key if multiple hosts:ips
+         * are connected to equivalent servers ( LB ) 
+         * @throws IOException 
+         */
+        public IOChannel getChannel(CharSequence key) throws IOException {
+            RemoteServer t = null;
+            synchronized (hosts) {
+                t = hosts.get(key);
+                if (t == null) {
+                    misses.incrementAndGet();
+                    return null;
+                }
+            }
+            IOChannel res = null;
+            synchronized (t) {
+                if (t.connections.size() == 0) {
+                    misses.incrementAndGet();
+                    hosts.remove(key); 
+                    return null;
+                } // one may be added - no harm.
+                res = t.connections.remove(t.connections.size() - 1);
+
+                if (t.connections.size() == 0) {
+                    hosts.remove(key); 
+                } 
+                if (res == null) {
+                    log.fine("Null connection ?");
+                    misses.incrementAndGet();
+                    return null;
+                }
+                
+                if (!res.isOpen()) {
+                    res.setDataReceivedCallback(null);
+                    res.close();
+                    log.fine("Already closed " + res);
+                    //res.keepAliveServer = null;
+                    res = null;
+                }
+                
+                waitingSockets.decrementAndGet();
+            }
+            hits.incrementAndGet();
+            if (debug) {
+                log.info("REUSE channel ..." + key + " " + res);
+            }
+            return res;      
+        }
+
+        /**
+         * Must be called in IOThread for the channel
+         */
+        public void returnChannel(IOChannel ch) 
+                throws IOException {
+            CharSequence key = ch.getTarget(); 
+            if (key == null) {
+                ch.close();
+                if (debug) {
+                    log.info("Return channel, no target ..." + key + " " + ch);
+                }
+                return;
+            }
+            
+            if (!keepOpen) {
+                ch.close();
+                return;
+            }
+            
+//            SocketIOChannel sdata = (SocketIOChannel) ch;
+            if (!ch.isOpen()) {
+                ch.close(); // make sure all closed
+                if (debug) {
+                    log.info("Return closed channel ..." + key + " " + ch);
+                }
+                return;
+            }
+            
+            RemoteServer t = null;
+            synchronized (hosts) {
+                t = hosts.get(key);
+                if (t == null) {
+                    t = new RemoteServer();
+                    t.pool = this;
+                    hosts.put(key, t);
+                }
+            }
+            waitingSockets.incrementAndGet();
+            
+            ch.ts = System.currentTimeMillis();
+            synchronized (t) {
+                // sdata.keepAliveServer = t;
+                t.connections.add(ch);      
+                //sdata.ch.setDataCallbacks(readable, null, cc);
+                ch.readInterest(true);
+            }
+        }
+        
+        // Called by handleClosed
+        void stopKeepAlive(IOChannel schannel) {
+            CharSequence target = schannel.getTarget();
+            RemoteServer t = null;
+            synchronized (hosts) {
+                t = hosts.get(target);
+                if (t == null) {
+                    return;
+                }
+            }
+            synchronized (t) {
+                if (t.connections.remove(schannel)) {      
+                    waitingSockets.decrementAndGet();
+                    if (t.connections.size() == 0) {
+                        hosts.remove(target);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpMessage.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpMessage.java
new file mode 100644 (file)
index 0000000..eb81f07
--- /dev/null
@@ -0,0 +1,439 @@
+/*
+ */
+package org.apache.tomcat.lite.http;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.tomcat.lite.io.BBuffer;
+import org.apache.tomcat.lite.io.BufferedIOReader;
+import org.apache.tomcat.lite.io.CBuffer;
+import org.apache.tomcat.lite.io.IOBuffer;
+import org.apache.tomcat.lite.io.IOInputStream;
+import org.apache.tomcat.lite.io.IOOutputStream;
+import org.apache.tomcat.lite.io.IOReader;
+import org.apache.tomcat.lite.io.IOWriter;
+import org.apache.tomcat.lite.io.UrlEncoding;
+
+
+/**
+ * Basic Http request or response message.
+ * 
+ * Because the HttpChannel can be used for both client and
+ * server, and to make proxy and other code simpler - the request 
+ * and response are represented by the same class.
+ * 
+ * @author Costin Manolache
+ */
+public abstract class HttpMessage {
+
+    public static enum State {
+        HEAD,
+        BODY_DATA,
+        DONE
+    }
+
+    /**
+     * Raw, off-the-wire message.
+     */
+    public static class HttpMessageBytes {
+        BBuffer head1 = BBuffer.wrapper();
+        BBuffer head2 = BBuffer.wrapper();
+        BBuffer proto = BBuffer.wrapper();
+
+        BBuffer query = BBuffer.wrapper();
+        
+        List<BBuffer> headerNames = new ArrayList<BBuffer>();
+        List<BBuffer> headerValues  = new ArrayList<BBuffer>();
+        
+        int headerCount;
+        
+        public BBuffer status() {
+            return head1;
+        }
+
+        public BBuffer method() {
+            return head1;
+        }
+
+        public BBuffer url() {
+            return head2;
+        }
+
+        public BBuffer query() {
+            return query;
+        }
+
+        public BBuffer protocol() {
+            return proto;
+        }
+
+        public BBuffer message() {
+            return head2;
+        }
+        
+        public int addHeader() {
+            headerNames.add(BBuffer.wrapper());
+            headerValues.add(BBuffer.wrapper());
+            return headerCount++;
+        }
+        
+        public BBuffer getHeaderName(int i) {
+            if (i >= headerNames.size()) {
+                return null;
+            }
+            return headerNames.get(i);
+        }
+
+        public BBuffer getHeaderValue(int i) {
+            if (i >= headerValues.size()) {
+                return null;
+            }
+            return headerValues.get(i);
+        }
+
+        public void recycle() {
+            head1.recycle();
+            head2.recycle();
+            proto.recycle();
+            query.recycle();
+            headerCount = 0;
+            for (int i = 0; i < headerCount; i++) {
+                headerNames.get(i).recycle();
+                headerValues.get(i).recycle();
+            }
+        }
+    }
+    
+    private HttpMessageBytes msgBytes = new HttpMessageBytes();
+    
+    protected HttpMessage.State state = HttpMessage.State.HEAD;
+    
+    protected HttpChannel httpCh; 
+    
+    protected MultiMap headers = new MultiMap().insensitive();
+
+    protected CBuffer protoMB;
+    
+    // Cookies 
+    protected boolean cookiesParsed = false;
+    
+    // TODO: cookies parsed when headers are added !
+    protected ArrayList<ServerCookie> cookies;
+    protected ArrayList<ServerCookie> cookiesCache;
+
+    protected UrlEncoding urlDecoder = new UrlEncoding();
+    protected String charEncoding;
+
+    IOReader reader;
+    BufferedIOReader bufferedReader;
+    HttpWriter writer;
+    IOWriter conv;
+    
+    IOOutputStream out;
+    private IOInputStream in; 
+        
+    boolean commited;
+    
+    protected IOBuffer body;
+
+    long contentLength = -2;
+    boolean chunked;
+    
+    BBuffer clBuffer = BBuffer.allocate(64);
+    
+    public HttpMessage(HttpChannel httpCh) {
+        this.httpCh = httpCh;
+        
+        out = new IOOutputStream(httpCh.getOut(), this);
+        conv = new IOWriter(httpCh);
+        writer = new HttpWriter(this, out, conv);
+
+        in = new IOInputStream(httpCh, httpCh.getIOTimeout());
+        
+        reader = new IOReader(httpCh.getIn());
+        bufferedReader = new BufferedIOReader(reader);
+        
+        cookies = new ArrayList<ServerCookie>();    
+        cookiesCache = new ArrayList<ServerCookie>();    
+        protoMB = CBuffer.newInstance();        
+    }
+    
+    public void addHeader(String name, String value) {
+        getMimeHeaders().addValue(name).set(value);
+    }
+
+    public void setHeader(String name, String value) {
+        getMimeHeaders().setValue(name).set(value);
+    }
+
+    public void setMimeHeaders(MultiMap resHeaders) {
+        this.headers = resHeaders;
+    }
+
+    public String getHeader(String name) {
+        CBuffer cb = headers.getHeader(name);
+        return (cb == null) ? null : cb.toString();
+    }
+
+    public MultiMap getMimeHeaders() {
+        return headers;
+    }
+    
+    public Collection<String> getHeaderNames() {
+
+        MultiMap headers = getMimeHeaders();
+        int n = headers.size();
+        ArrayList<String> result = new ArrayList<String>();
+        for (int i = 0; i < n; i++) {
+            result.add(headers.getName(i).toString());
+        }
+        return result;
+    }
+    
+    public boolean containsHeader(String name) {
+        return headers.getHeader(name) != null;
+    }
+
+    public void setContentLength(long len) {
+        contentLength = len;
+        clBuffer.setLong(len);
+        setCLHeader();
+    }
+    
+    public void setContentLength(int len) {
+        contentLength = len;
+        clBuffer.setLong(len);
+        setCLHeader();
+    } 
+    
+    private void setCLHeader() {
+        MultiMap.Entry clB = headers.setEntry("content-length");
+        clB.valueB = clBuffer; 
+    }
+
+    public long getContentLengthLong() {
+        if (contentLength == -2) {
+            CBuffer clB = headers.getHeader("content-length");
+            contentLength = (clB == null) ? 
+                    -1 : clB.getLong();
+        }
+        return contentLength;
+    }
+    
+    public int getContentLength() {
+        long length = getContentLengthLong();
+
+        if (length < Integer.MAX_VALUE) {
+            return (int) length;
+        }
+        return -1;
+    }
+    
+    public String getContentType() {
+        CBuffer contentTypeMB = headers.getHeader("content-type");
+        if (contentTypeMB == null) {
+            return null;
+        }
+        return contentTypeMB.toString();
+    }
+    
+    public void setContentType(String contentType) {
+        CBuffer clB = getMimeHeaders().getHeader("content-type");
+        if (clB == null) {
+            setHeader("Content-Type", contentType);
+        } else {
+            clB.set(contentType);
+        }
+    }
+
+    /**
+     * Get the character encoding used for this request.
+     * Need a field because it can be overriden. Used to construct the 
+     * Reader.
+     */
+    public String getCharacterEncoding() {
+        if (charEncoding != null)
+            return charEncoding;
+
+        charEncoding = ContentType.getCharsetFromContentType(getContentType());
+        return charEncoding;
+    }
+    
+    private static final String DEFAULT_ENCODING = "ISO-8859-1";
+    
+    public String getEncoding() {
+        String charEncoding = getCharacterEncoding();
+        if (charEncoding == null) {
+            return DEFAULT_ENCODING; 
+        } else {
+            return charEncoding;
+        }
+    }
+
+    public void setCharacterEncoding(String enc) 
+            throws UnsupportedEncodingException {
+        this.charEncoding = enc;
+    }
+    
+    
+
+    public void recycle() {
+        commited = false;
+        headers.recycle();
+        protoMB.set("HTTP/1.1");
+        for (int i = 0; i < cookies.size(); i++) {
+            cookies.get(i).recycle();
+        }
+        cookies.clear();
+        charEncoding = null;
+        bufferedReader.recycle();
+        
+        writer.recycle();
+        conv.recycle();
+        
+        contentLength = -2;
+        chunked = false;
+        clBuffer.recycle();
+        state = State.HEAD;
+        cookiesParsed = false;
+        getMsgBytes().recycle();
+        
+    }
+    
+    
+    public String getProtocol() {
+        return protoMB.toString();
+    }
+    
+    public void setProtocol(String proto) {
+        protoMB.set(proto);
+    }
+    
+    public CBuffer protocol() {
+        return protoMB;
+    }
+    
+    public ServerCookie getCookie(String name) {
+        for (ServerCookie sc: getServerCookies()) {
+            if (sc.getName().equalsIgnoreCase(name)) {
+                return sc;
+            }
+        }
+        return null;
+    }
+    
+    public List<ServerCookie> getServerCookies() {
+        if (!cookiesParsed) {
+            cookiesParsed = true;
+            ServerCookie.processCookies(cookies, cookiesCache, getMsgBytes());
+        }
+        return cookies;
+    }
+    
+    public UrlEncoding getURLDecoder() {
+        return urlDecoder;
+    }
+    
+    public boolean isCommitted() {
+        return commited;
+    }
+
+    public void setCommitted(boolean b) {
+        commited = b;
+    }
+
+    // Not used in coyote connector ( hack )
+    
+    public void sendHead() throws IOException {
+    }
+    
+    public HttpChannel getHttpChannel() {
+        return httpCh;
+    }
+    
+    public IOBuffer getBody() {
+        return body;
+    }
+    
+    void setBody(IOBuffer body) {
+        this.body = body;
+    }
+    
+    public void flush() throws IOException {
+        httpCh.startSending();
+    }
+    
+    // not servlet input stream 
+    public IOInputStream getBodyInputStream() {
+        return in;
+    }
+    
+    public IOOutputStream getBodyOutputStream() {
+        return out;
+    }
+
+    public IOReader getBodyReader() throws IOException {
+        reader.setEncoding(getCharacterEncoding());
+        return reader;
+    }
+    
+    /** 
+     * Returns a buffered reader. 
+     */
+    public BufferedReader getReader() throws IOException {
+        reader.setEncoding(getCharacterEncoding());
+        return bufferedReader;
+    }
+    
+    public HttpWriter getBodyWriter() {
+        conv.setEncoding(getCharacterEncoding());
+        return writer;
+    }
+    
+    //
+    public abstract void serialize(IOBuffer out) throws IOException;
+    
+    
+    public void serializeHeaders(IOBuffer rawSendBuffers2) throws IOException {
+        MultiMap mimeHeaders = getMimeHeaders();
+        
+        for (int i = 0; i < mimeHeaders.size(); i++) {
+            CBuffer name = mimeHeaders.getName(i);
+            CBuffer value = mimeHeaders.getValue(i);
+            if (name.length() == 0 || value.length() == 0) {
+                continue;
+            }
+            rawSendBuffers2.append(name);
+            rawSendBuffers2.append(HttpChannel.COLON);
+            rawSendBuffers2.append(value);
+            rawSendBuffers2.append(BBuffer.CRLF_BYTES);
+        }
+        rawSendBuffers2.append(BBuffer.CRLF_BYTES);
+    }
+    
+    protected void processMimeHeaders() {
+        for (int idx = 0; idx < getMsgBytes().headerCount; idx++) {
+            BBuffer nameBuf = getMsgBytes().getHeaderName(idx);
+            BBuffer valBuf = getMsgBytes().getHeaderValue(idx);
+            
+            MultiMap.Entry header = headers.addEntry(nameBuf);
+            header.valueB = valBuf;
+        }
+    }
+
+    
+    protected abstract void processReceivedHeaders() throws IOException;
+    
+    public abstract boolean hasBody();
+
+    public HttpMessageBytes getMsgBytes() {
+        // TODO: serialize if not set
+        return msgBytes;
+    }
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpRequest.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpRequest.java
new file mode 100644 (file)
index 0000000..398d60d
--- /dev/null
@@ -0,0 +1,980 @@
+/*
+ */
+package org.apache.tomcat.lite.http;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tomcat.lite.http.MultiMap.Entry;
+import org.apache.tomcat.lite.io.BBuffer;
+import org.apache.tomcat.lite.io.CBuffer;
+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.IOReader;
+import org.apache.tomcat.lite.io.IOWriter;
+import org.apache.tomcat.lite.io.SocketIOChannel;
+import org.apache.tomcat.lite.io.UrlEncoding;
+
+public class HttpRequest extends HttpMessage {
+    public static final String DEFAULT_CHARACTER_ENCODING="ISO-8859-1";
+
+    protected CBuffer schemeMB;    
+    protected CBuffer methodMB;
+    protected CBuffer remoteAddrMB;
+    protected CBuffer remoteHostMB;
+    protected int remotePort;
+    
+    protected CBuffer localNameMB;
+    protected CBuffer localAddrMB;
+    protected int localPort = -1;
+
+    // Host: header, or default:80
+    protected CBuffer serverNameMB;
+    protected int serverPort = -1;
+    
+    
+    // ==== Derived fields, computed after request is received ===
+    
+    protected CBuffer requestURI;
+    protected CBuffer queryMB;
+    
+    protected BBuffer decodedUri = BBuffer.allocate();
+    protected CBuffer decodedUriMB;
+    
+    // Decoded query
+    protected MultiMap parameters;
+    
+    boolean parametersParsed = false;
+
+    protected IOWriter charEncoder = new IOWriter(null);
+    protected IOReader charDecoder = new IOReader(null);
+    protected UrlEncoding urlEncoding = new UrlEncoding();
+
+    // Reference to 'real' request object
+    // will not be recycled
+    public Object nativeRequest;
+    public Object wrapperRequest;
+    
+    boolean ssl = false;
+    
+    boolean async = false;
+    
+    private Map<String, Object> attributes = new HashMap<String, Object>();
+
+    /**
+     * Mapping data.
+     */
+    protected MappingData mappingData = new MappingData();
+
+
+    HttpRequest(HttpChannel httpCh) {
+        super(httpCh);
+        decodedUriMB = CBuffer.newInstance();
+        requestURI = CBuffer.newInstance();
+        queryMB = CBuffer.newInstance();
+        serverNameMB = CBuffer.newInstance();
+        
+        parameters = new MultiMap();
+        
+        schemeMB = 
+            CBuffer.newInstance();
+        methodMB = CBuffer.newInstance();
+        initRemote();
+    }
+   
+    protected void initRemote() {
+        remoteAddrMB = CBuffer.newInstance();
+        localNameMB = CBuffer.newInstance();
+        remoteHostMB = CBuffer.newInstance();
+        localAddrMB = CBuffer.newInstance();
+    }
+    
+    public void recycle() {
+        super.recycle();
+        schemeMB.recycle();
+        methodMB.set("GET");
+        requestURI.recycle();
+        queryMB.recycle();
+        decodedUriMB.recycle();
+        
+        parameters.recycle();
+        remoteAddrMB.recycle();
+        remoteHostMB.recycle();
+        parametersParsed = false;
+        ssl = false;
+        async = false;
+        asyncTimeout = -1;
+        charEncoder.recycle();
+        
+        localPort = -1;
+        remotePort = -1;
+        localAddrMB.recycle();
+        localNameMB.recycle();
+        
+        serverPort = -1;
+        serverNameMB.recycle();
+        decodedUri.recycle();
+        decodedQuery.recycle();
+    }
+    
+    public Object getAttribute(String name) {
+        return attributes.get(name);
+    }
+
+    public void setAttribute(String name, Object o) {
+        if (o == null) {
+            attributes.remove(name);
+        } else {
+            attributes.put(name, o);
+        }
+    }
+    // getAttributeNames not supported
+    
+    public Map<String, Object> attributes() {
+        return attributes;
+    }
+    
+    
+    
+    public CBuffer method() {
+        return methodMB;
+    }
+    
+    public String getMethod() {
+        return methodMB.toString();
+    }
+    
+    public void setMethod(String method) {
+        methodMB.set(method);
+    }
+    
+    public CBuffer scheme() {
+        return schemeMB;
+    }
+    
+    public String getScheme() {
+        String scheme = schemeMB.toString();
+        if (scheme == null) {
+            return "http";
+        }
+        return scheme;
+    }
+    
+    public void setScheme(String s) {
+        schemeMB.set(s);
+    }
+    
+    public MappingData getMappingData() {
+        return (mappingData);
+    }
+    
+    /** 
+     * Parse query parameters - but not POST body. 
+     * 
+     * If you don't call this method, getParameters() will 
+     * also read the body for POST with x-www-url-encoded 
+     * mime type. 
+     */
+    public void parseQueryParameters() {
+        parseQuery();
+    }
+
+    /**
+     * Explicitely parse the body, adding the parameters to 
+     * those from the query ( if already parsed ).
+     * 
+     * By default servlet mode ( both query and body ) is used.
+     */
+    public void parsePostParameters() {
+        parseBody();
+    }
+    
+    MultiMap getParameters() {
+        if (!parametersParsed) {
+            parseQuery();
+            parseBody();
+        }
+        return parameters;
+    }
+    
+    public Enumeration<String> getParameterNames() {
+        return getParameters().names();
+    }
+    
+    /** 
+     * Expensive, creates a copy on each call. 
+     * @param name
+     * @return
+     */
+    public String[] getParameterValues(String name) {
+        Entry entry = getParameters().getEntry(name);
+        if (entry == null) {
+            return null;
+        }
+        String[] values = new String[entry.values.size()];
+        for (int j = 0; j < values.length; j++) {
+            values[j] = entry.values.get(j).toString();
+        }
+        return values;
+    }
+    
+    // Inefficient - we convert from a different representation.
+    public Map<String, String[]> getParameterMap() {
+        // we could allow 'locking' - I don't think this is 
+        // a very useful optimization
+        Map<String, String[]> map = new HashMap();
+        for (int i = 0; i < getParameters().size(); i++) {
+            Entry entry = getParameters().getEntry(i);
+            if (entry == null) {
+                continue;
+            }
+            if (entry.key == null) {
+                continue;
+            }
+            String name = entry.key.toString();
+            String[] values = new String[entry.values.size()];
+            for (int j = 0; j < values.length; j++) {
+                values[j] = entry.values.get(j).toString();
+            }
+            map.put(name, values);
+        }
+        return map;
+    }
+    
+    public String getParameter(String name) {
+        CharSequence value = getParameters().get(name);
+        if (value == null) {
+            return null;
+        }
+        return value.toString();
+    }
+    
+    public void setParameter(String name, String value) {
+        getParameters().set(name, value);
+    }
+    
+    public void addParameter(String name, String values) {
+        getParameters().add(name, values);
+    }
+
+    public CBuffer queryString() {
+        return queryMB;
+    }
+
+    // TODO
+    void serializeParameters(Appendable cc) throws IOException {
+        int keys = parameters.size();
+        boolean notFirst = false;
+        for (int i = 0; i < parameters.size(); i++) {
+            Entry entry = parameters.getEntry(i);
+            for (int j = 0; j < entry.values.size(); j++) {
+                // TODO: Uencode
+                if (notFirst) {
+                    cc.append('&');
+                } else {
+                    notFirst = true;
+                }
+                cc.append(entry.key);
+                cc.append("=");
+                cc.append(entry.values.get(j).getValue());
+            }
+        }
+    }
+    
+    public void setURI(CharSequence encoded) {
+        decodedUriMB.recycle();
+        decodedUriMB.append(encoded);
+        // TODO: generate % encoding ( reverse of decodeRequest )
+    }
+    
+    public CBuffer decodedURI() {
+        return decodedUriMB;
+    }
+
+    public CBuffer requestURI() {
+        return requestURI;
+    }
+    
+    /** 
+     * Not decoded - %xx as in original.
+     * @return
+     */
+    public String getRequestURI() {
+        return requestURI.toString();
+    }
+
+    public void setRequestURI(String encodedUri) {
+        requestURI.set(encodedUri);
+    }
+
+    CBuffer getOrAdd(String name) {
+        CBuffer header = getMimeHeaders().getHeader(name);
+        if (header == null) {
+            header = getMimeHeaders().addValue(name);
+        }
+        return header;
+    }
+
+    /** 
+     * Set the Host header of the request.
+     * @param target
+     */
+    public void setHost(String target) {
+        serverNameMB.recycle();
+        getOrAdd("Host").set(target);
+    }
+    
+    // XXX
+    public CBuffer serverName() {
+        if (serverNameMB.length() == 0) {
+            parseHost();
+        }
+        return serverNameMB;
+    }
+
+    public String getServerName() {
+        return serverName().toString();
+    }
+    
+    public void setServerName(String name)  {
+        serverName().set(name);
+    }
+    
+    public int getServerPort() {
+        serverName();
+        return serverPort;
+    }
+    
+    public void setServerPort(int serverPort ) {
+        this.serverPort=serverPort;
+    }
+
+    public CBuffer remoteAddr() {
+        if (remoteAddrMB.length() == 0) {
+            HttpChannel asyncHttp = getHttpChannel();
+            IOChannel iochannel = asyncHttp.getNet().getFirst();
+            if (iochannel instanceof SocketIOChannel) {
+                SocketIOChannel channel = (SocketIOChannel) iochannel;
+
+                String addr = (channel == null) ?
+                        "127.0.0.1" : 
+                            channel.getAddress(true).getHostAddress();
+
+                remoteAddrMB.set(addr);
+            }
+        }
+        return remoteAddrMB;
+    }
+
+    public CBuffer remoteHost() {
+        if (remoteHostMB.length() == 0) {
+            HttpChannel asyncHttp = getHttpChannel();
+            IOChannel iochannel = asyncHttp.getNet().getFirst();
+            if (iochannel instanceof SocketIOChannel) {
+                SocketIOChannel channel = (SocketIOChannel) iochannel;
+                String addr = (channel == null) ?
+                        "127.0.0.1" : 
+                            channel.getAddress(true).getCanonicalHostName();
+
+                remoteHostMB.set(addr);
+            }
+        }
+        return remoteHostMB;
+    }
+
+    public CBuffer localName() {
+        return localNameMB;
+    }    
+
+    public CBuffer localAddr() {
+        return localAddrMB;
+    }
+    
+    public int getRemotePort(){
+        if (remotePort == -1) {
+            HttpChannel asyncHttp = getHttpChannel();
+            IOChannel iochannel = asyncHttp.getNet().getFirst();
+            if (iochannel instanceof SocketIOChannel) {
+                SocketIOChannel channel = (SocketIOChannel) iochannel;
+                remotePort = (channel == null) ?
+                            0 : channel.getPort(true);
+            }
+        }
+        return remotePort;
+    }
+        
+    public void setRemotePort(int port){
+        this.remotePort = port;
+    }
+    
+    public int getLocalPort(){
+        if (localPort == -1) {
+            HttpChannel asyncHttp = getHttpChannel();
+            IOChannel iochannel = asyncHttp.getNet().getFirst();
+            if (iochannel instanceof SocketIOChannel) {
+                SocketIOChannel channel = (SocketIOChannel) iochannel;
+                localPort = (channel == null) ?
+                            0 : channel.getPort(false);
+            }
+        }
+        return localPort;
+    }
+        
+    public void setLocalPort(int port){
+        this.localPort = port;
+    }
+    
+    public void sendHead() throws IOException {
+        httpCh.sendRequestHeaders(httpCh);
+    }
+
+    /** 
+     * Convert the request to bytes, ready to send.
+     */
+    public void serialize(IOBuffer rawSendBuffers2) throws IOException {
+        rawSendBuffers2.append(method());
+        rawSendBuffers2.append(BBuffer.SP);
+
+        // TODO: encode or use decoded
+        rawSendBuffers2.append(requestURI());
+        if (queryString().length() > 0) {
+            rawSendBuffers2.append("?");
+            rawSendBuffers2.append(queryString());
+        }
+
+        rawSendBuffers2.append(BBuffer.SP);
+        rawSendBuffers2.append(protocol());
+        rawSendBuffers2.append(BBuffer.CRLF_BYTES);
+        
+        super.serializeHeaders(rawSendBuffers2);
+    }
+
+    /**
+     * Parse host.
+     * @param serverNameMB2 
+     * @throws IOException 
+     */
+    boolean parseHost()  {
+        MultiMap.Entry hostHF = getMimeHeaders().getEntry("Host");
+        if (hostHF == null) {
+            // HTTP/1.0
+            // Default is what the socket tells us. Overriden if a host is
+            // found/parsed
+            return true;
+        }
+
+        BBuffer valueBC = hostHF.valueB;
+        byte[] valueB = valueBC.array();
+        int valueL = valueBC.getLength();
+        int valueS = valueBC.getStart();
+        int colonPos = -1;
+        
+        serverNameMB.recycle();
+
+        boolean ipv6 = (valueB[valueS] == '[');
+        boolean bracketClosed = false;
+        for (int i = 0; i < valueL; i++) {
+            char b = (char) valueB[i + valueS];
+            if (b == ':') {
+                if (!ipv6 || bracketClosed) {
+                    colonPos = i;
+                    break;
+                }
+            }
+            serverNameMB.append(b);
+            if (b == ']') {
+                bracketClosed = true;
+            } 
+        }
+
+        if (colonPos < 0) {
+            if (!ssl) {
+                // 80 - Default HTTP port
+                setServerPort(80);
+            } else {
+                // 443 - Default HTTPS port
+                setServerPort(443);
+            }
+        } else {
+            int port = 0;
+            int mult = 1;
+            for (int i = valueL - 1; i > colonPos; i--) {
+                int charValue = Hex.DEC[(int) valueB[i + valueS]];
+                if (charValue == -1) {
+                    // we don't return 400 - could do it
+                    return false;
+                }
+                port = port + (charValue * mult);
+                mult = 10 * mult;
+            }
+            setServerPort(port);
+
+        }
+        return true;
+    }
+    
+    // TODO: this is from coyote - MUST be rewritten !!!
+    // - cleaner
+    // - chunked encoding for body
+    // - buffer should be in a pool, etc.
+    /**
+     * Post data buffer.
+     */
+    public final static int CACHED_POST_LEN = 8192;
+    
+    public  byte[] postData = null;
+
+    private long asyncTimeout = -1;
+    
+    /**
+     * Parse request parameters.
+     */
+    protected void parseQuery() {
+
+        parametersParsed = true;
+
+        // getCharacterEncoding() may have been overridden to search for
+        // hidden form field containing request encoding
+        String enc = getEncoding();
+
+//        boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
+//        if (enc != null) {
+//            parameters.setEncoding(enc);
+////            if (useBodyEncodingForURI) {
+////                parameters.setQueryStringEncoding(enc);
+////            }
+//        } else {
+//            parameters.setEncoding(DEFAULT_CHARACTER_ENCODING);
+////            if (useBodyEncodingForURI) {
+////                parameters.setQueryStringEncoding
+////                    (DEFAULT_CHARACTER_ENCODING);
+////            }
+//        }
+
+        handleQueryParameters();
+    }
+
+    // Copy - will be modified by decoding
+    BBuffer decodedQuery = BBuffer.allocate(1024);
+    
+    CBuffer tmpNameC = CBuffer.newInstance();
+    BBuffer tmpName = BBuffer.wrapper();
+    BBuffer tmpValue = BBuffer.wrapper();
+
+    CBuffer tmpNameCB = CBuffer.newInstance();
+    CBuffer tmpValueCB = CBuffer.newInstance();
+
+    /** 
+     * Process the query string into parameters
+     */
+    public void handleQueryParameters() {
+        if( queryMB.length() == 0) {
+            return;
+        }
+        
+        decodedQuery.recycle();
+        decodedQuery.append(getMsgBytes().query());
+        // TODO: option 'useBodyEncodingForUri' - versus UTF or ASCII
+        String queryStringEncoding = getEncoding();
+        processParameters( decodedQuery, queryStringEncoding );
+    }
+    
+    public void processParameters( BBuffer bc, String encoding ) {
+        if( bc.isNull()) 
+            return;
+        if (bc.remaining() ==0) {
+            return;
+        }
+        processParameters( bc.array(), bc.getOffset(),
+                           bc.getLength(), encoding);
+    }
+    
+    public void processParameters( byte bytes[], int start, int len, 
+            String enc ) {
+        int end=start+len;
+        int pos=start;
+
+        do {
+            boolean noEq=false;
+            int valStart=-1;
+            int valEnd=-1;
+
+            int nameStart=pos;
+            int nameEnd=BBuffer.indexOf(bytes, nameStart, end, '=' );
+            // Workaround for a&b&c encoding
+            int nameEnd2=BBuffer.indexOf(bytes, nameStart, end, '&' );
+            if( (nameEnd2!=-1 ) &&
+                    ( nameEnd==-1 || nameEnd > nameEnd2) ) {
+                nameEnd=nameEnd2;
+                noEq=true;
+                valStart=nameEnd;
+                valEnd=nameEnd;
+            }
+            if( nameEnd== -1 ) 
+                nameEnd=end;
+
+            if( ! noEq ) {
+                valStart= (nameEnd < end) ? nameEnd+1 : end;
+                valEnd=BBuffer.indexOf(bytes, valStart, end, '&');
+                if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart;
+            }
+
+            pos=valEnd+1;
+
+            if( nameEnd<=nameStart ) {
+                // No name eg ...&=xx&... will trigger this
+                continue;
+            }
+            
+            // TODO: use CBuffer, recycle
+            tmpName.setBytes( bytes, nameStart, nameEnd-nameStart );
+            tmpValue.setBytes( bytes, valStart, valEnd-valStart );
+            
+            try {
+                parameters.add(urlDecode(tmpName, enc), 
+                        urlDecode(tmpValue, enc));
+            } catch (IOException e) {
+                // ignored
+            }
+        } while( pos<end );
+    }
+
+//    public void processParameters(char bytes[], int start, int len, 
+//            String enc ) {
+//        int end=start+len;
+//        int pos=start;
+//
+//        do {
+//            boolean noEq=false;
+//            int valStart=-1;
+//            int valEnd=-1;
+//
+//            int nameStart=pos;
+//            int nameEnd=CBuffer.indexOf(bytes, nameStart, end, '=' );
+//            // Workaround for a&b&c encoding
+//            int nameEnd2=CBuffer.indexOf(bytes, nameStart, end, '&' );
+//            if( (nameEnd2!=-1 ) &&
+//                    ( nameEnd==-1 || nameEnd > nameEnd2) ) {
+//                nameEnd=nameEnd2;
+//                noEq=true;
+//                valStart=nameEnd;
+//                valEnd=nameEnd;
+//            }
+//            if( nameEnd== -1 ) 
+//                nameEnd=end;
+//
+//            if( ! noEq ) {
+//                valStart= (nameEnd < end) ? nameEnd+1 : end;
+//                valEnd=CBuffer.indexOf(bytes, valStart, end, '&');
+//                if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart;
+//            }
+//
+//            pos=valEnd+1;
+//
+//            if( nameEnd<=nameStart ) {
+//                // No name eg ...&=xx&... will trigger this
+//                continue;
+//            }
+//            
+//            // TODO: use CBuffer, recycle
+//            tmpNameCB.recycle();
+//            tmpValueCB.recycle();
+//            
+//            tmpNameCB.wrap( bytes, nameStart, nameEnd );
+//            tmpValueCB.wrap( bytes, valStart, valEnd );
+//            
+//            //CharChunk name = new CharChunk();
+//            //CharChunk value = new CharChunk();
+//            // TODO:
+//            try {
+//                parameters.add(urlDecode(tmpName, enc), 
+//                        urlDecode(tmpValue, enc));
+//            } catch (IOException e) {
+//                // ignored
+//            }
+//        } while( pos<end );
+//    }
+    
+    private String urlDecode(BBuffer bc, String enc)
+            throws IOException {
+        // Replace %xx
+        urlDecoder.urlDecode(bc, true);
+        
+        String result = null;
+        if (enc != null) {
+            result = bc.toString(enc);
+        } else {
+            // Ascii
+            
+            CBuffer cc = tmpNameC;
+            cc.recycle();
+            int length = bc.getLength();
+            byte[] bbuf = bc.array();
+            int start = bc.getStart();
+            cc.appendAscii(bbuf, start, length);
+            result = cc.toString();
+            cc.recycle();
+        }
+        return result;
+    }
+
+    private void processParameters( byte bytes[], int start, int len ) {
+        processParameters(bytes, start, len, getEncoding());
+    }
+    
+    protected void parseBody() {
+
+        parametersParsed = true;
+        String enc = getCharacterEncoding();
+
+//      if (usingInputStream || usingReader)
+//      return;
+        if (!getMethod().equalsIgnoreCase("POST"))
+            return;
+        
+        String contentType = getContentType();
+        if (contentType == null)
+            contentType = "";
+        int semicolon = contentType.indexOf(';');
+        if (semicolon >= 0) {
+            contentType = contentType.substring(0, semicolon).trim();
+        } else {
+            contentType = contentType.trim();
+        }
+        if (!("application/x-www-form-urlencoded".equals(contentType)))
+            return;
+
+        int len = getContentLength();
+
+        if (len > 0) {
+            try {
+                byte[] formData = null;
+                if (len < CACHED_POST_LEN) {
+                    if (postData == null)
+                        postData = new byte[CACHED_POST_LEN];
+                    formData = postData;
+                } else {
+                    formData = new byte[len];
+                }
+                int actualLen = readPostBody(formData, len);
+                if (actualLen == len) {
+                    processParameters(formData, 0, len);
+                }
+            } catch (Throwable t) {
+                ; // Ignore
+            }
+        }
+
+    }
+
+    /**
+     * Read post body in an array.
+     */
+    protected int readPostBody(byte body[], int len)
+        throws IOException {
+
+        int offset = 0;
+        do {
+            int inputLen = getBodyInputStream().read(body, offset, len - offset);
+            if (inputLen <= 0) {
+                return offset;
+            }
+            offset += inputLen;
+        } while ((len - offset) > 0);
+        return len;
+
+    }
+    
+    // Async support - a subset of servlet spec, the fancy stuff is in the 
+    // facade.
+    
+    public boolean isAsyncStarted() {
+        return async;
+    }
+
+    public void async() {
+        this.async = true;
+    }
+
+    public void setAsyncTimeout(long timeout) {
+        this.asyncTimeout  = timeout;
+    }
+    
+    /** 
+     * Server mode, request just received.
+     */
+    protected void processReceivedHeaders() throws IOException {
+        if (!httpCh.normalize(getMsgBytes().url())) {
+            httpCh.getResponse().setStatus(400);
+            httpCh.abort("Error normalizing url " + 
+                    getMsgBytes().url());
+            return;                
+        }
+        
+        method().set(getMsgBytes().method()); 
+        requestURI().set(getMsgBytes().url());
+        queryString().set(getMsgBytes().query());
+        protocol().set(getMsgBytes().protocol());
+        
+        processMimeHeaders();
+
+        // URL decode and normalize
+        decodedUri.append(getMsgBytes().url());
+        getURLDecoder().urlDecode(decodedUri, false); 
+        
+        // Need to normalize again - %decoding may decode /
+        if (!httpCh.normalize(decodedUri)) {
+            httpCh.getResponse().setStatus(400);
+            httpCh.abort("Invalid decoded uri " + decodedUri);
+            return;                
+        }
+        decodedURI().set(decodedUri);
+
+        httpCh.processProtocol();
+
+        // default response protocol
+        httpCh.getResponse().protocol().set(getMsgBytes().protocol());            
+
+        // requested connection:close/keepAlive and proto
+        httpCh.processConnectionHeader(getMimeHeaders());
+
+        httpCh.processExpectation();
+
+        httpCh.receiveBody.processContentDelimitation();
+        // Spec: 
+        // The presence of a message-body in a request is signaled by the 
+        // inclusion of a Content-Length or Transfer-Encoding header field in 
+        // the request's message-headers
+        // Server should read - but ignore
+
+        httpCh.receiveBody.noBody = !httpCh.receiveBody.isContentDelimited();
+
+        httpCh.receiveBody.updateCloseOnEnd();
+
+        /*
+         * The presence of a message-body in a request is signaled by the 
+         * inclusion of a Content-Length or Transfer-Encoding header field in 
+         * the request's message-headers. A message-body MUST NOT be included 
+         * in a request if the specification of the request method 
+         * (section 5.1.1) does not allow sending an entity-body in requests. 
+         * A server SHOULD read and forward a message-body on any request; if the request method does not include defined semantics for an entity-body, then the message-body SHOULD be ignored when handling the request.
+         */
+        if (!httpCh.receiveBody.isContentDelimited()) {
+            // No body
+            httpCh.getIn().close();
+        } 
+
+        CBuffer valueMB = getMimeHeaders().getHeader("host");
+        // Check host header
+//        if (httpCh.http11 && (valueMB == null)) {
+//            httpCh.error = true;
+//            // 400 - Bad request
+//            httpCh.getResponse().setStatus(400);
+//        }
+    }
+
+    
+    public boolean hasBody() {
+        return chunked || contentLength >= 0; 
+    }
+
+    /**
+     * Convert (if necessary) and return the absolute URL that represents the
+     * resource referenced by this possibly relative URL.  If this URL is
+     * already absolute, return it unchanged.
+     *
+     * @param location URL to be (possibly) converted and then returned
+     *
+     * @exception IllegalArgumentException if a MalformedURLException is
+     *  thrown when converting the relative URL to an absolute one
+     */
+    public void toAbsolute(String location, CBuffer cb) {
+
+        cb.recycle();
+        if (location == null)
+            return;
+
+        boolean leadingSlash = location.startsWith("/");
+        if (leadingSlash || !hasScheme(location)) {
+
+            String scheme = getScheme();
+            String name = serverName().toString();
+            int port = getServerPort();
+
+            cb.append(scheme);                
+            cb.append("://", 0, 3);
+            cb.append(name);
+            if ((scheme.equals("http") && port != 80)
+                    || (scheme.equals("https") && port != 443)) {
+                cb.append(':');
+                String portS = port + "";
+                cb.append(portS);
+            }
+            if (!leadingSlash) {
+                String relativePath = decodedURI().toString();
+                int pos = relativePath.lastIndexOf('/');
+                relativePath = relativePath.substring(0, pos);
+
+                //String encodedURI = null;
+                urlEncoding.urlEncode(relativePath,  cb, charEncoder);
+                //encodedURI = urlEncoder.encodeURL(relativePath);
+                //redirectURLCC.append(encodedURI, 0, encodedURI.length());
+                cb.append('/');
+            }
+
+            cb.append(location);
+        } else {
+            cb.append(location);
+        }
+
+    }    
+    
+    /**
+     * Determine if a URI string has a <code>scheme</code> component.
+     */
+    public static boolean hasScheme(String uri) {
+        int len = uri.length();
+        for(int i=0; i < len ; i++) {
+            char c = uri.charAt(i);
+            if(c == ':') {
+                return i > 0;
+            } else if(!isSchemeChar(c)) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Determine if the character is allowed in the scheme of a URI.
+     * See RFC 2396, Section 3.1
+     */
+    private static boolean isSchemeChar(char c) {
+        return Character.isLetterOrDigit(c) ||
+            c == '+' || c == '-' || c == '.';
+    }
+    
+    public IOWriter getCharEncoder() {
+        return charEncoder;
+    }
+
+    public IOReader getCharDecoder() {
+        return charDecoder;
+    }
+    
+    public UrlEncoding getUrlEncoding() {
+        return urlEncoding;
+    }
+    
+    public BBuffer toBytes(CBuffer cb, BBuffer bb) {
+        if (bb == null) {
+            bb = BBuffer.allocate(cb.length());
+        }
+        getCharEncoder().encodeAll(cb, bb, "UTF-8");
+        return bb;
+    }
+
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpResponse.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpResponse.java
new file mode 100644 (file)
index 0000000..d4e8c73
--- /dev/null
@@ -0,0 +1,283 @@
+/*
+ */
+package org.apache.tomcat.lite.http;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.apache.tomcat.lite.io.BBucket;
+import org.apache.tomcat.lite.io.BBuffer;
+import org.apache.tomcat.lite.io.CBuffer;
+import org.apache.tomcat.lite.io.IOBuffer;
+
+public class HttpResponse extends HttpMessage {
+
+    // will not be recycled
+    public Object nativeResponse;
+
+    protected CBuffer message = CBuffer.newInstance();
+    
+    int status = -1;
+
+    HttpResponse(HttpChannel httpCh) {
+        super(httpCh);
+    }
+
+    public void recycle() {
+        super.recycle();
+        message.recycle();
+        status = -1;
+    }
+    
+    public void setMessage(String s) {
+        message.set(filter(s));
+    }
+    
+    public String getMessage() {
+        return message.toString();
+    }
+    
+    public CBuffer getMessageBuffer() {
+        return message;
+    }
+    
+    byte[] S_200 = new byte[] { '2', '0', '0' };
+    
+    public void setStatus(int i) {
+        status = i;
+    }
+    
+    public int getStatus() {
+        if (status >= 0) {
+            return status;
+        }
+        if (getMsgBytes().status().isNull()) {
+            status = 200;
+        } else {
+            try {
+                status = getMsgBytes().status().getInt();
+            } catch(NumberFormatException ex) {
+                status = 500;
+                httpCh.log.severe("Invalid status " + getMsgBytes().status());
+            }
+        }
+        return status;
+    }
+
+    public void sendHead() throws IOException {
+        httpCh.sendHeaders();
+    }
+
+    /** 
+     * Convert the response to bytes, ready to send.
+     */
+    public void serialize(IOBuffer rawSendBuffers2) throws IOException {
+        
+        rawSendBuffers2.append(protocol()).append(' ');
+        String status = Integer.toString(getStatus());   
+        rawSendBuffers2.append(status).append(' ');
+        if (getMessageBuffer().length() > 0) {
+            rawSendBuffers2.append(getMessage());
+        } else {
+            rawSendBuffers2
+                .append(getMessage(getStatus()));
+        }
+        rawSendBuffers2.append(BBuffer.CRLF_BYTES);
+        // Headers
+        super.serializeHeaders(rawSendBuffers2);
+    }
+
+    public HttpRequest getRequest() {
+        return getHttpChannel().getRequest();
+    }
+    
+    // Http client mode.
+    protected void processReceivedHeaders() throws IOException {
+        protocol().set(getMsgBytes().protocol());                
+        message.set(getMsgBytes().message());
+        processMimeHeaders();
+        
+        
+        // TODO: if protocol == 1.0 and we requested 1.1, downgrade getHttpChannel().pro
+        int status = 500;
+        try {
+            status = getStatus();
+        } catch (Throwable t) {
+            getHttpChannel().log.warning("Invalid status " + getMsgBytes().status() + " " + getMessage());
+        }
+        HttpBody body = (HttpBody) getBody();
+        body.noBody = !hasBody();
+
+        // Will parse 'connection:close', set close on end
+        getHttpChannel().processConnectionHeader(getMimeHeaders());
+        
+        body.processContentDelimitation();
+        
+        if (body.statusDropsConnection(status)) {
+            getHttpChannel().closeStreamOnEnd("response status drops connection");
+        }
+        
+        if (body.isDone()) {
+            body.close();
+        }
+
+        if (!body.isContentDelimited()) {
+            getHttpChannel().closeStreamOnEnd("not content delimited");
+        }
+        
+        
+    }
+
+    /**
+     * All responses to the HEAD request method MUST NOT include a 
+     * message-body, even though the presence of entity- header fields might
+     *  lead one to believe they do. All 1xx (informational), 204 (no content)
+     *  , and 304 (not modified) responses MUST NOT include a message-body. All 
+     *  other responses do include a message-body, although it MAY be of zero 
+     *  length.
+     */
+    public boolean hasBody() {
+        if (httpCh.getRequest().method().equals("HEAD")) {
+            return false;
+        }
+        if (status >= 100 && status < 200) {
+            return false;
+        }
+        // what about (status == 205) ?
+        if ((status == 204) 
+                || (status == 304)) {
+            return false;
+        }
+        return true;
+    }
+    
+    /** Get the status string associated with a status code.
+     *  No I18N - return the messages defined in the HTTP spec.
+     *  ( the user isn't supposed to see them, this is the last
+     *  thing to translate)
+     *
+     *  Common messages are cached.
+     *
+     */
+    private BBucket getMessage( int status ) {
+        // method from Response.
+
+        // Does HTTP requires/allow international messages or
+        // are pre-defined? The user doesn't see them most of the time
+        switch( status ) {
+        case 200:
+            return st_200;
+        case 302:
+            return st_302;
+        case 400:
+            return st_400;
+        case 404:
+            return st_404;
+        }
+        return stats.get(status);
+    }
+    
+    
+    static BBucket st_200 = BBuffer.wrapper("OK");
+    static BBucket st_302= BBuffer.wrapper("Moved Temporarily");
+    static BBucket st_400= BBuffer.wrapper("Bad Request");
+    static BBucket st_404= BBuffer.wrapper("Not Found");
+
+    static HashMap<Integer,BBucket> stats = new HashMap<Integer, BBucket>();
+    private static void addStatus(int stat, String msg) {
+        stats.put(stat, BBuffer.wrapper(msg));
+    }
+    
+    static {
+        addStatus(100, "Continue");
+        addStatus(101, "Switching Protocols");
+        addStatus(200, "OK");
+        addStatus(201, "Created");
+        addStatus(202, "Accepted");
+        addStatus(203, "Non-Authoritative Information");
+        addStatus(204, "No Content");
+        addStatus(205, "Reset Content");
+        addStatus(206, "Partial Content");
+        addStatus(207, "Multi-Status");
+        addStatus(300, "Multiple Choices");
+        addStatus(301, "Moved Permanently");
+        addStatus(302, "Moved Temporarily");
+        addStatus(303, "See Other");
+        addStatus(304, "Not Modified");
+        addStatus(305, "Use Proxy");
+        addStatus(307, "Temporary Redirect");
+        addStatus(400, "Bad Request");
+        addStatus(401, "Unauthorized");
+        addStatus(402, "Payment Required");
+        addStatus(403, "Forbidden");
+        addStatus(404, "Not Found");
+        addStatus(405, "Method Not Allowed");
+        addStatus(406, "Not Acceptable");
+        addStatus(407, "Proxy Authentication Required");
+        addStatus(408, "Request Timeout");
+        addStatus(409, "Conflict");
+        addStatus(410, "Gone");
+        addStatus(411, "Length Required");
+        addStatus(412, "Precondition Failed");
+        addStatus(413, "Request Entity Too Large");
+        addStatus(414, "Request-URI Too Long");
+        addStatus(415, "Unsupported Media Type");
+        addStatus(416, "Requested Range Not Satisfiable");
+        addStatus(417, "Expectation Failed");
+        addStatus(422, "Unprocessable Entity");
+        addStatus(423, "Locked");
+        addStatus(424, "Failed Dependency");
+        addStatus(500, "Internal Server Error");
+        addStatus(501, "Not Implemented");
+        addStatus(502, "Bad Gateway");
+        addStatus(503, "Service Unavailable");
+        addStatus(504, "Gateway Timeout");
+        addStatus(505, "HTTP Version Not Supported");
+        addStatus(507, "Insufficient Storage");
+        
+    }
+    
+    /**
+     * Filter the specified message string for characters that are sensitive
+     * in HTML.  This avoids potential attacks caused by including JavaScript
+     * codes in the request URL that is often reported in error messages.
+     *
+     * @param message The message string to be filtered
+     */
+    private static String filter(String message) {
+
+        if (message == null)
+            return (null);
+        if (message.indexOf('<') < 0 &&
+                message.indexOf('>') < 0 &&
+                message.indexOf('&') < 0 &&
+                message.indexOf('"') < 0) {
+            return message;
+        }
+        
+        char content[] = new char[message.length()];
+        message.getChars(0, message.length(), content, 0);
+
+        StringBuffer result = new StringBuffer(content.length + 50);
+        for (int i = 0; i < content.length; i++) {
+            switch (content[i]) {
+            case '<':
+                result.append("&lt;");
+                break;
+            case '>':
+                result.append("&gt;");
+                break;
+            case '&':
+                result.append("&amp;");
+                break;
+            case '"':
+                result.append("&quot;");
+                break;
+            default:
+                result.append(content[i]);
+            }
+        }
+        return (result.toString());
+    }
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpWriter.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpWriter.java
new file mode 100644 (file)
index 0000000..2d8f91a
--- /dev/null
@@ -0,0 +1,313 @@
+/*
+ */
+package org.apache.tomcat.lite.http;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.tomcat.lite.io.IOOutputStream;
+import org.apache.tomcat.lite.io.IOWriter;
+
+/**
+ * Implement character translation and buffering.
+ * 
+ * The actual buffering happens in the IOBuffer - we translate the 
+ * chars as soon as we get them. 
+ * 
+ * For servlet compat you can set a buffer size and a flush() will happen
+ * when the number of chars have been written. Note that writes at a lower 
+ * layer can be done and are not counted. 
+ * 
+ * @author Costin Manolache
+ */
+public class HttpWriter extends Writer {
+
+    public static final String DEFAULT_ENCODING = "ISO-8859-1";
+    public static final int DEFAULT_BUFFER_SIZE = 8*1024;
+
+    // ----------------------------------------------------- Instance Variables
+    HttpMessage message;
+
+    /**
+     * The byte buffer.
+     */
+    protected IOOutputStream bb;
+
+    int bufferSize = DEFAULT_BUFFER_SIZE;
+    
+    /**
+     * Number of chars written.
+     */
+    protected int wSinceFlush = 0;
+
+
+    /**
+     * Flag which indicates if the output buffer is closed.
+     */
+    protected boolean closed = false;
+
+    /**
+     * Encoding to use. 
+     * TODO: isn't it redundant ? enc, gotEnc, conv plus the enc in the bb
+     */
+    protected String enc;
+
+
+    /**
+     * Encoder is set.
+     */
+    protected boolean gotEnc = false;
+
+
+    /**
+     * List of encoders. The writer is reused - the encoder mapping 
+     * avoids creating expensive objects. In future it'll contain nio.Charsets
+     */
+    //protected Map<String, C2BConverter> encoders = new HashMap();
+
+
+    /**
+     * Current char to byte converter. TODO: replace with Charset
+     */
+    private IOWriter conv;
+
+    /**
+     * Suspended flag. All output bytes will be swallowed if this is true.
+     */
+    protected boolean suspended = false;
+
+
+    // ----------------------------------------------------------- Constructors
+
+
+    /**
+     * Default constructor. Allocate the buffer with the default buffer size.
+     * @param out 
+     */
+    public HttpWriter(HttpMessage message, IOOutputStream out,
+            IOWriter conv) {
+        this.message = message;
+        bb = out;
+        this.conv = conv;
+    }
+
+    // ------------------------------------------------------------- Properties
+
+
+    /**
+     * Is the response output suspended ?
+     * 
+     * @return suspended flag value
+     */
+    public boolean isSuspended() {
+        return this.suspended;
+    }
+
+
+    /**
+     * Set the suspended flag.
+     * 
+     * @param suspended New suspended flag value
+     */
+    public void setSuspended(boolean suspended) {
+        this.suspended = suspended;
+    }
+
+
+    // --------------------------------------------------------- Public Methods
+
+
+    /**
+     * Recycle the output buffer.
+     */
+    public void recycle() {
+        wSinceFlush = 0;
+        bb.recycle(); 
+        closed = false;
+        suspended = false;
+        
+//        if (conv != null) {
+//            conv.recycle();
+//        }
+        
+        gotEnc = false;
+        enc = null;
+    }
+
+    public void close()
+        throws IOException {
+
+        if (closed)
+            return;
+        if (suspended)
+            return;
+
+        push();
+        closed = true;
+        
+        bb.close();
+    }
+
+
+    /**
+     * Flush bytes or chars contained in the buffer.
+     * 
+     * @throws IOException An underlying IOException occurred
+     */
+    public void flush()
+            throws IOException {
+        push();
+        bb.flush(); // will send the data
+        wSinceFlush = 0;
+    }
+
+    /**
+     * Flush chars to the byte buffer.
+     */
+    public void push()
+        throws IOException {
+
+        if (suspended)
+            return;
+        getConv().push();
+        
+    }
+
+
+    private void updateSize(int cnt) throws IOException {
+        wSinceFlush += cnt;
+        if (wSinceFlush > bufferSize) {
+            flush();
+        }
+    }
+
+    public void write(int c)
+            throws IOException {
+        if (suspended)
+            return;
+        getConv().write(c);
+        updateSize(1);
+    }
+
+
+    public void write(char c[])
+            throws IOException {
+        write(c, 0, c.length);
+    }
+
+
+    public void write(char c[], int off, int len)
+            throws IOException {
+        if (suspended)
+            return;
+        getConv().write(c, off, len);
+        updateSize(len);
+    }
+
+
+    public void write(StringBuffer sb)
+            throws IOException {
+        if (suspended)
+            return;
+        int len = sb.length();
+        getConv().write(sb.toString());
+        updateSize(len);
+    }
+
+
+    /**
+     * Append a string to the buffer
+     */
+    public void write(String s, int off, int len)
+        throws IOException {
+        if (suspended)
+            return;
+        if (s==null)
+            s="null";
+        getConv().write( s, off, len );
+        updateSize(len);
+    }
+
+
+    public void write(String s)
+            throws IOException {
+        if (s==null)
+            s="null";
+        write(s, 0, s.length());
+    } 
+
+    public void println() throws IOException {
+        write("\n");
+    }
+
+    public void println(String s) throws IOException {
+        write(s);
+        write("\n");
+    }
+
+    public void print(String s) throws IOException {
+        write(s);
+    }
+
+    public void checkConverter() 
+            throws IOException {
+//        if (gotEnc) {
+//            return;
+//        }
+//        if (enc == null) {
+//            enc = message.getCharacterEncoding();
+//        }
+//
+//        gotEnc = true;
+//        if (enc == null)
+//            enc = DEFAULT_ENCODING;
+//        conv = (C2BConverter) encoders.get(enc);
+//        
+//        if (conv == null) {
+//            conv = C2BConverter.newConverter(message.getBodyOutputStream(), 
+//                    enc);
+//            encoders.put(enc, conv);
+//
+//        }
+    }
+
+    public int getWrittenSinceFlush() {
+        return wSinceFlush;
+    }
+
+
+    public void setBufferSize(int size) {
+        if (size > bufferSize) {
+            bufferSize = size;
+        }
+    }
+
+    /**
+     *  Clear any data that was buffered.
+     */
+    public void reset() {
+        if (conv != null) { 
+            conv.recycle();
+        }
+        wSinceFlush = 0;
+        gotEnc = false;
+        enc = null;
+        bb.reset();
+    }
+
+
+    public int getBufferSize() {
+        return bufferSize;
+    }
+
+    protected IOWriter getConv() throws IOException {
+        checkConverter();
+        return conv;
+    }
+
+    public void println(CharSequence key) throws IOException {
+        // TODO: direct
+        println(key.toString());
+    }
+
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MappingData.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MappingData.java
new file mode 100644 (file)
index 0000000..f0edeaf
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ *  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.tomcat.lite.http;
+
+import org.apache.tomcat.lite.io.CBuffer;
+
+
+/**
+ * Mapping data.
+ *
+ * @author Remy Maucherat
+ */
+public class MappingData {
+
+    public Object context = null; // ServletContextImpl
+    
+    public BaseMapper.ContextMapping contextMap;
+
+    public BaseMapper.ServiceMapping service = null;
+
+    public CBuffer contextPath = CBuffer.newInstance();
+    public CBuffer requestPath = CBuffer.newInstance();
+    public CBuffer wrapperPath = CBuffer.newInstance();
+    public CBuffer pathInfo = CBuffer.newInstance();
+
+    public CBuffer redirectPath = CBuffer.newInstance();
+
+    // Extension
+    CBuffer ext = CBuffer.newInstance();
+    CBuffer tmpPrefix = CBuffer.newInstance();
+    
+    // Excluding context path, with a '/' added if needed
+    CBuffer tmpServletPath = CBuffer.newInstance();
+
+    // Excluding context path, with a '/' added if needed
+    CBuffer tmpWelcome = CBuffer.newInstance();
+
+    public void recycle() {
+        service = null; 
+        context = null;
+        pathInfo.recycle();
+        requestPath.recycle();
+        wrapperPath.recycle();
+        contextPath.recycle();
+        redirectPath.recycle();
+        contextMap = null;
+    }
+
+
+    public Object getServiceObject() {
+        return service == null ? null : service.object;
+    }
+
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MultiMap.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MultiMap.java
new file mode 100644 (file)
index 0000000..515c2df
--- /dev/null
@@ -0,0 +1,358 @@
+/*
+ *  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.tomcat.lite.http;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tomcat.lite.io.BBuffer;
+import org.apache.tomcat.lite.io.CBucket;
+import org.apache.tomcat.lite.io.CBuffer;
+import org.apache.tomcat.util.buf.CharChunk;
+
+/**
+ * Map used to represent headers and parameters ( could be used 
+ * for cookies too )
+ * 
+ * It'll avoid garbage collection, like original tomcat classes, 
+ * by converting to chars and strings late.
+ * 
+ * Not thread safe.
+ */
+public class MultiMap {
+
+    public static class Entry {
+        // Wrappers from the head message bytes.
+        BBuffer nameB; 
+        BBuffer valueB;
+        
+        CBuffer key = CBuffer.newInstance();
+        private CBuffer value = CBuffer.newInstance();
+        
+        /** 
+         * For the first entry with a given name: list of all
+         * other entries, including this one, with same name.
+         * 
+         * For second or more: empty list
+         */
+        public List<Entry> values = new ArrayList<Entry>(); 
+    
+        public void recycle() {
+            key.recycle();
+            value.recycle();
+            //next=null;
+            nameB = null;
+            valueB = null;
+            values.clear();
+        }
+    
+        public CBuffer getName() {
+            if (key.length() == 0 && nameB != null) {
+                key.set(nameB);
+            }
+            return key;
+        }
+    
+        public CBuffer getValue() {
+            if (value.length() == 0 && valueB != null) {
+                value.set(valueB);
+            }
+            return value;
+        }
+
+        /** Important - used by values iterator, returns strings 
+         * from each entry
+         */
+        public String toString() {
+            return getValue().toString();
+        }
+        
+    }
+
+    // active entries
+    protected int count;
+
+    // The key will be converted to lower case
+    boolean toLower = false;
+    
+    // Some may be inactive - up to count.
+    protected List<Entry> entries = new ArrayList<Entry>();
+    
+    // 2 options: convert all header/param names to String
+    // or use a temp CBuffer to map
+    Map<CBuffer, Entry> map = 
+        new HashMap<CBuffer, Entry>();
+    
+    public void recycle() {
+        for (int i = 0; i < count; i++) {
+            Entry entry = entries.get(i);
+            entry.recycle();
+        }
+        count = 0;
+        map.clear();
+    }
+
+    // ----------- Mutations ------------------------
+
+    protected Entry newEntry()  {
+        return new Entry();
+    }
+    
+    /**
+     * Adds a partially constructed field entry.
+     * Updates count - but will not affect the map.
+     */
+    private Entry getEntryForAdd() {
+        Entry entry;
+        if (count >= entries.size()) {
+            entry = newEntry();
+            entries.add(entry);
+        } else {
+            entry = entries.get(count);
+        }
+        count++;
+        return entry;
+    }
+    
+
+    /** Create a new named header , return the CBuffer
+     *  container for the new value
+     */
+   public Entry addEntry(CharSequence name ) {
+       Entry mh = getEntryForAdd();
+       mh.getName().append(name);
+       if (toLower) {
+           mh.getName().toLower();
+       }
+       updateMap(mh);
+       return mh;
+   }
+
+   /** Create a new named header , return the CBuffer
+    *  container for the new value
+    */
+   public Entry addEntry(BBuffer name ) {
+       Entry mh = getEntryForAdd();
+       mh.nameB = name;
+       if (toLower) {
+           mh.getName().toLower();
+       }       
+       updateMap(mh);
+       
+       return mh;
+   }
+
+   private void updateMap(Entry mh) {
+       Entry topEntry = map.get(mh.getName());
+       
+       if (topEntry == null) {
+           map.put(mh.getName(), mh);
+           mh.values.add(mh);            
+       } else {
+           topEntry.values.add(mh);
+       }
+   }
+
+
+
+    public void remove(CharSequence key) {
+        CBucket ckey = key(key);
+        Entry entry = getEntry(ckey);
+        if (entry != null) {
+            map.remove(ckey);
+            
+            for (int i = count - 1; i >= 0; i--) {
+                entry = entries.get(i);
+                if (entry.getName().equals(key)) {
+                    entry.recycle();
+                    entries.remove(i);
+                    count--;
+                }
+            }
+        }            
+    }
+
+    // --------------- Key-based access --------------
+    CBuffer tmpKey = CBuffer.newInstance();
+    
+    /**
+     * Finds and returns a header field with the given name.  If no such
+     * field exists, null is returned.  If more than one such field is
+     * in the header, an arbitrary one is returned.
+     */
+    public CBuffer getHeader(String name) {
+        for (int i = 0; i < count; i++) {
+            if (entries.get(i).getName().equalsIgnoreCase(name)) {
+                return entries.get(i).getValue();
+            }
+        }
+        return null;
+    }
+
+    private CBucket key(CharSequence key) {
+        if (key instanceof CBucket) {
+            CBucket res = (CBucket) key;
+            if (!toLower || !res.hasUpper()) {
+                return res;
+            }
+        }
+        tmpKey.recycle();
+        tmpKey.append(key);
+        if (toLower) {
+            tmpKey.toLower();
+        }        
+        return tmpKey;
+    }
+    
+    public Entry getEntry(CharSequence key) {
+        Entry entry = map.get(key(key));
+        return entry;
+    }
+
+    public Entry getEntry(CBucket buf) {
+        // lowercase ?
+        Entry entry = map.get(buf);
+        return entry;
+    }
+
+    public Enumeration<String> names() {
+        return new IteratorEnumerator(map.keySet().iterator());
+    }    
+
+    // ----------- Index access --------------
+    
+    /**
+     *  Number of entries ( including those with same key
+     * 
+     * @return
+     */
+    public int size() {
+        return count;
+    }
+
+
+    public CharSequence getKey(int idx) {
+        return entries.get(idx).key;
+    }
+
+    public Entry getEntry(int idx) {
+        return entries.get(idx);
+    }
+
+    /**
+     * Returns the Nth header name, or null if there is no such header.
+     * This may be used to iterate through all header fields.
+     */
+    public CBuffer getName(int n) {
+        return n < count ? entries.get(n).getName() : null;
+    }
+
+    /**
+     * Returns the Nth header value, or null if there is no such header.
+     * This may be used to iterate through all header fields.
+     */
+    public CBuffer getValue(int n) {
+        return n >= 0 && n < count ? entries.get(n).getValue() : null;
+    }
+
+    // ----------- Helpers --------------
+    public void add(CharSequence key, CharSequence value) {
+        Entry mh = addEntry(key);
+        mh.value.append(value);
+    }
+    
+    /** Create a new named header , return the CBuffer
+     * container for the new value
+     */
+    public CBuffer addValue( String name ) {
+        return addEntry(name).getValue();
+    }
+
+     public Entry setEntry( String name ) {
+         remove(name);
+         return addEntry(name);
+     }
+
+     public void set(CharSequence key, CharSequence value) {
+         remove(key);
+         add(key, value);
+     }
+     
+     public CBuffer setValue( String name ) {
+         remove(name);
+         return addValue(name);
+     }
+
+     public CBuffer get(CharSequence key) {
+         Entry entry = getEntry(key);
+         return (entry == null) ? null : entry.value;
+     }
+
+     public String getString(CharSequence key) {
+         Entry entry = getEntry(key);
+         return (entry == null) ? null : entry.value.toString();
+     }
+     
+    
+    // -------------- support classes ----------------
+    
+    public static class IteratorEnumerator implements Enumeration<String> {
+        private final Iterator keyI;
+    
+        public IteratorEnumerator(Iterator iterator) {
+            this.keyI = iterator;
+        }
+    
+        
+        public boolean hasMoreElements() {
+            return keyI.hasNext();
+        }
+    
+        
+        public String nextElement() {
+            return keyI.next().toString();
+        }
+
+    }
+
+    public static final Enumeration<String> EMPTY = 
+        new Enumeration<String>() {
+
+            @Override
+            public boolean hasMoreElements() {
+                return false;
+            }
+
+            @Override
+            public String nextElement() {
+                return null;
+            }
+        
+    };
+
+    public MultiMap insensitive() {
+        toLower = true;
+        return this;
+    }
+
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ServerCookie.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ServerCookie.java
new file mode 100644 (file)
index 0000000..c80ef5d
--- /dev/null
@@ -0,0 +1,819 @@
+/*
+ *  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.tomcat.lite.http;
+
+import java.io.Serializable;
+import java.text.DateFormat;
+import java.text.FieldPosition;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.tomcat.lite.io.BBuffer;
+import org.apache.tomcat.lite.io.CBuffer;
+
+
+/**
+ *  Server-side cookie representation.
+ *  Allows recycling and uses MessageBytes as low-level
+ *  representation ( and thus the byte-> char conversion can be delayed
+ *  until we know the charset ).
+ *
+ *  Tomcat.core uses this recyclable object to represent cookies,
+ *  and the facade will convert it to the external representation.
+ */
+public class ServerCookie implements Serializable {
+    
+    // Version 0 (Netscape) attributes
+    private BBuffer name = BBuffer.allocate();
+    private BBuffer value = BBuffer.allocate();
+    
+    private CBuffer nameC = CBuffer.newInstance();
+
+    // Expires - Not stored explicitly. Generated from Max-Age (see V1)
+    private BBuffer path = BBuffer.allocate();
+    private BBuffer domain = BBuffer.allocate();
+    private boolean secure;
+    
+    // Version 1 (RFC2109) attributes
+    private BBuffer comment = BBuffer.allocate();
+    private int maxAge = -1;
+    private int version = 0;
+
+    // Other fields
+    private static final String OLD_COOKIE_PATTERN =
+        "EEE, dd-MMM-yyyy HH:mm:ss z";
+    private static final ThreadLocal<DateFormat> OLD_COOKIE_FORMAT =
+        new ThreadLocal<DateFormat>() {
+        protected DateFormat initialValue() {
+            DateFormat df =
+                new SimpleDateFormat(OLD_COOKIE_PATTERN, Locale.US);
+            df.setTimeZone(TimeZone.getTimeZone("GMT"));
+            return df;
+        }
+    };
+    
+    private static final String ancientDate;
+
+
+    static {
+        ancientDate = OLD_COOKIE_FORMAT.get().format(new Date(10000));
+    }
+
+    /**
+     * If set to true, we parse cookies according to the servlet spec,
+     */
+    public static final boolean STRICT_SERVLET_COMPLIANCE =
+        Boolean.valueOf(System.getProperty("org.apache.catalina.STRICT_SERVLET_COMPLIANCE", "false")).booleanValue();
+
+    /**
+     * If set to false, we don't use the IE6/7 Max-Age/Expires work around
+     */
+    public static final boolean ALWAYS_ADD_EXPIRES =
+        Boolean.valueOf(System.getProperty("org.apache.tomcat.util.http.ServerCookie.ALWAYS_ADD_EXPIRES", "true")).booleanValue();
+
+    // Note: Servlet Spec =< 2.5 only refers to Netscape and RFC2109,
+    // not RFC2965
+
+    // Version 1 (RFC2965) attributes
+    // TODO Add support for CommentURL
+    // Discard - implied by maxAge <0
+    // TODO Add support for Port
+
+    public ServerCookie() {
+    }
+
+    public void recycle() {        
+        path.recycle();
+        name.recycle();
+        value.recycle();
+        comment.recycle();
+        maxAge=-1;
+        path.recycle();
+        domain.recycle();
+        version=0;
+        secure=false;
+    }
+
+    public BBuffer getComment() {
+        return comment;
+    }
+
+    public BBuffer getDomain() {
+        return domain;
+    }
+
+    public void setMaxAge(int expiry) {
+        maxAge = expiry;
+    }
+
+    public int getMaxAge() {
+        return maxAge;
+    }
+
+    public BBuffer getPath() {
+        return path;
+    }
+
+    public void setSecure(boolean flag) {
+        secure = flag;
+    }
+
+    public boolean getSecure() {
+        return secure;
+    }
+
+    public BBuffer getName() {
+        return name;
+    }
+
+    public BBuffer getValue() {
+        return value;
+    }
+
+    public int getVersion() {
+        return version;
+    }
+
+    public void setVersion(int v) {
+        version = v;
+    }
+
+
+    // -------------------- utils --------------------
+
+    public String toString() {
+        return "Cookie " + getName() + "=" + getValue() + " ; "
+            + getVersion() + " " + getPath() + " " + getDomain();
+    }
+    
+    private static final String tspecials = ",; ";
+    private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t";
+    private static final String tspecials2NoSlash = "()<>@,;:\\\"[]?={} \t";
+
+    /*
+     * Tests a string and returns true if the string counts as a
+     * reserved token in the Java language.
+     *
+     * @param value the <code>String</code> to be tested
+     *
+     * @return      <code>true</code> if the <code>String</code> is a reserved
+     *              token; <code>false</code> if it is not
+     */
+    public static boolean isToken(String value) {
+        return isToken(value,null);
+    }
+    
+    public static boolean isToken(String value, String literals) {
+        String tspecials = (literals==null?ServerCookie.tspecials:literals);
+        if( value==null) return true;
+        int len = value.length();
+
+        for (int i = 0; i < len; i++) {
+            char c = value.charAt(i);
+
+            if (tspecials.indexOf(c) != -1)
+                return false;
+        }
+        return true;
+    }
+
+    public static boolean containsCTL(String value, int version) {
+        if( value==null) return false;
+        int len = value.length();
+        for (int i = 0; i < len; i++) {
+            char c = value.charAt(i);
+            if (c < 0x20 || c >= 0x7f) {
+                if (c == 0x09)
+                    continue; //allow horizontal tabs
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean isToken2(String value) {
+        return isToken2(value,null);
+    }
+
+    public static boolean isToken2(String value, String literals) {
+        String tspecials2 = (literals==null?ServerCookie.tspecials2:literals);
+        if( value==null) return true;
+        int len = value.length();
+
+        for (int i = 0; i < len; i++) {
+            char c = value.charAt(i);
+            if (tspecials2.indexOf(c) != -1)
+                return false;
+        }
+        return true;
+    }
+
+    // -------------------- Cookie parsing tools
+
+    
+    /**
+     * Return the header name to set the cookie, based on cookie version.
+     */
+    public String getCookieHeaderName() {
+        return getCookieHeaderName(version);
+    }
+
+    /**
+     * Return the header name to set the cookie, based on cookie version.
+     */
+    public static String getCookieHeaderName(int version) {
+        // TODO Re-enable logging when RFC2965 is implemented
+        // log( (version==1) ? "Set-Cookie2" : "Set-Cookie");
+        if (version == 1) {
+            // XXX RFC2965 not referenced in Servlet Spec
+            // Set-Cookie2 is not supported by Netscape 4, 6, IE 3, 5
+            // Set-Cookie2 is supported by Lynx and Opera
+            // Need to check on later IE and FF releases but for now... 
+            // RFC2109
+            return "Set-Cookie";
+            // return "Set-Cookie2";
+        } else {
+            // Old Netscape
+            return "Set-Cookie";
+        }
+    }
+
+    // TODO RFC2965 fields also need to be passed
+    public static void appendCookieValue( StringBuffer headerBuf,
+                                          int version,
+                                          String name,
+                                          String value,
+                                          String path,
+                                          String domain,
+                                          String comment,
+                                          int maxAge,
+                                          boolean isSecure,
+                                          boolean isHttpOnly)
+    {
+        StringBuffer buf = new StringBuffer();
+        // Servlet implementation checks name
+        buf.append( name );
+        buf.append("=");
+        // Servlet implementation does not check anything else
+        
+        version = maybeQuote2(version, buf, value,true);
+
+        // Add version 1 specific information
+        if (version == 1) {
+            // Version=1 ... required
+            buf.append ("; Version=1");
+
+            // Comment=comment
+            if ( comment!=null ) {
+                buf.append ("; Comment=");
+                maybeQuote2(version, buf, comment);
+            }
+        }
+        
+        // Add domain information, if present
+        if (domain!=null) {
+            buf.append("; Domain=");
+            maybeQuote2(version, buf, domain);
+        }
+
+        // Max-Age=secs ... or use old "Expires" format
+        // TODO RFC2965 Discard
+        if (maxAge >= 0) {
+            if (version > 0) {
+                buf.append ("; Max-Age=");
+                buf.append (maxAge);
+            }
+            // IE6, IE7 and possibly other browsers don't understand Max-Age.
+            // They do understand Expires, even with V1 cookies!
+            if (version == 0 || ALWAYS_ADD_EXPIRES) {
+                // Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format )
+                buf.append ("; Expires=");
+                // To expire immediately we need to set the time in past
+                if (maxAge == 0)
+                    buf.append( ancientDate );
+                else
+                    OLD_COOKIE_FORMAT.get().format(
+                            new Date(System.currentTimeMillis() +
+                                    maxAge*1000L),
+                            buf, new FieldPosition(0));
+            }
+        }
+
+        // Path=path
+        if (path!=null) {
+            buf.append ("; Path=");
+            if (version==0) {
+                maybeQuote2(version, buf, path);
+            } else {
+                maybeQuote2(version, buf, path, ServerCookie.tspecials2NoSlash, false);
+            }
+        }
+
+        // Secure
+        if (isSecure) {
+          buf.append ("; Secure");
+        }
+        
+        // HttpOnly
+        if (isHttpOnly) {
+            buf.append("; HttpOnly");
+        }
+        headerBuf.append(buf);
+    }
+
+    public static boolean alreadyQuoted (String value) {
+        if (value==null || value.length()==0) return false;
+        return (value.charAt(0)=='\"' && value.charAt(value.length()-1)=='\"');
+    }
+    
+    /**
+     * Quotes values using rules that vary depending on Cookie version.
+     * @param version
+     * @param buf
+     * @param value
+     */
+    public static int maybeQuote2 (int version, StringBuffer buf, String value) {
+        return maybeQuote2(version,buf,value,false);
+    }
+
+    public static int maybeQuote2 (int version, StringBuffer buf, String value, boolean allowVersionSwitch) {
+        return maybeQuote2(version,buf,value,null,allowVersionSwitch);
+    }
+
+    public static int maybeQuote2 (int version, StringBuffer buf, String value, String literals, boolean allowVersionSwitch) {
+        if (value==null || value.length()==0) {
+            buf.append("\"\"");
+        }else if (containsCTL(value,version)) 
+            throw new IllegalArgumentException("Control character in cookie value, consider BASE64 encoding your value");
+        else if (alreadyQuoted(value)) {
+            buf.append('"');
+            buf.append(escapeDoubleQuotes(value,1,value.length()-1));
+            buf.append('"');
+        } else if (allowVersionSwitch && (!STRICT_SERVLET_COMPLIANCE) && version==0 && !isToken2(value, literals)) {
+            buf.append('"');
+            buf.append(escapeDoubleQuotes(value,0,value.length()));
+            buf.append('"');
+            version = 1;
+        } else if (version==0 && !isToken(value,literals)) {
+            buf.append('"');
+            buf.append(escapeDoubleQuotes(value,0,value.length()));
+            buf.append('"');
+        } else if (version==1 && !isToken2(value,literals)) {
+            buf.append('"');
+            buf.append(escapeDoubleQuotes(value,0,value.length()));
+            buf.append('"');
+        }else {
+            buf.append(value);
+        }
+        return version;
+    }
+
+
+    /**
+     * Escapes any double quotes in the given string.
+     *
+     * @param s the input string
+     * @param beginIndex start index inclusive
+     * @param endIndex exclusive
+     * @return The (possibly) escaped string
+     */
+    private static String escapeDoubleQuotes(String s, int beginIndex, int endIndex) {
+
+        if (s == null || s.length() == 0 || s.indexOf('"') == -1) {
+            return s;
+        }
+
+        StringBuffer b = new StringBuffer();
+        for (int i = beginIndex; i < endIndex; i++) {
+            char c = s.charAt(i);
+            if (c == '\\' ) {
+                b.append(c);
+                //ignore the character after an escape, just append it
+                if (++i>=endIndex) throw new IllegalArgumentException("Invalid escape character in cookie value.");
+                b.append(s.charAt(i));
+            } else if (c == '"')
+                b.append('\\').append('"');
+            else
+                b.append(c);
+        }
+
+        return b.toString();
+    }
+
+    /**
+     * Unescapes any double quotes in the given cookie value.
+     *
+     * @param bc The cookie value to modify
+     */
+    public static void unescapeDoubleQuotes(BBuffer bc) {
+
+        if (bc == null || bc.getLength() == 0 || bc.indexOf('"', 0) == -1) {
+            return;
+        }
+
+        int src = bc.getStart();
+        int end = bc.getEnd();
+        int dest = src;
+        byte[] buffer = bc.array();
+        
+        while (src < end) {
+            if (buffer[src] == '\\' && src < end && buffer[src+1]  == '"') {
+                src++;
+            }
+            buffer[dest] = buffer[src];
+            dest ++;
+            src ++;
+        }
+        bc.setEnd(dest);
+    }
+    
+    /*
+    List of Separator Characters (see isSeparator())
+    Excluding the '/' char violates the RFC, but 
+    it looks like a lot of people put '/'
+    in unquoted values: '/': ; //47 
+    '\t':9 ' ':32 '\"':34 '\'':39 '(':40 ')':41 ',':44 ':':58 ';':59 '<':60 
+    '=':61 '>':62 '?':63 '@':64 '[':91 '\\':92 ']':93 '{':123 '}':125
+    */
+    public static final char SEPARATORS[] = { '\t', ' ', '\"', '\'', '(', ')', ',', 
+        ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' };
+
+    protected static final boolean separators[] = new boolean[128];
+    static {
+        for (int i = 0; i < 128; i++) {
+            separators[i] = false;
+        }
+        for (int i = 0; i < SEPARATORS.length; i++) {
+            separators[SEPARATORS[i]] = true;
+        }
+    }
+
+    /** Add all Cookie found in the headers of a request.
+     */
+    public  static void processCookies(List<ServerCookie> cookies,
+            List<ServerCookie> cookiesCache,
+            HttpMessage.HttpMessageBytes msgBytes ) {
+        
+        // process each "cookie" header
+        for (int i = 0; i < msgBytes.headerCount; i++) {
+            if (msgBytes.getHeaderName(i).equalsIgnoreCase("Cookie")) {
+                BBuffer bc = msgBytes.getHeaderValue(i);
+                if (bc.remaining() == 0) {
+                    continue;
+                }
+                processCookieHeader(cookies, cookiesCache,
+                        bc.array(),
+                        bc.getOffset(),
+                        bc.getLength());
+                
+            }
+
+        }
+    }
+    
+    /**
+     * Returns true if the byte is a separator character as
+     * defined in RFC2619. Since this is called often, this
+     * function should be organized with the most probable
+     * outcomes first.
+     * JVK
+     */
+    private static final boolean isSeparator(final byte c) {
+         if (c > 0 && c < 126)
+             return separators[c];
+         else
+             return false;
+    }
+    
+    /**
+     * Returns true if the byte is a whitespace character as
+     * defined in RFC2619
+     * JVK
+     */
+    private static final boolean isWhiteSpace(final byte c) {
+        // This switch statement is slightly slower
+        // for my vm than the if statement.
+        // Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_07-164)
+        /* 
+        switch (c) {
+        case ' ':;
+        case '\t':;
+        case '\n':;
+        case '\r':;
+        case '\f':;
+            return true;
+        default:;
+            return false;
+        }
+        */
+       if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f')
+           return true;
+       else
+           return false;
+    }
+
+    /**
+     * Parses a cookie header after the initial "Cookie:"
+     * [WS][$]token[WS]=[WS](token|QV)[;|,]
+     * RFC 2965
+     * JVK
+     */
+    public static final void processCookieHeader(
+            List<ServerCookie> cookies,
+            List<ServerCookie> cookiesCache,
+            byte bytes[], int off, int len){
+        if( len<=0 || bytes==null ) return;
+        int end=off+len;
+        int pos=off;
+        int nameStart=0;
+        int nameEnd=0;
+        int valueStart=0;
+        int valueEnd=0;
+        int version = 0;
+        ServerCookie sc=null;
+        boolean isSpecial;
+        boolean isQuoted;
+
+        while (pos < end) {
+            isSpecial = false;
+            isQuoted = false;
+
+            // Skip whitespace and non-token characters (separators)
+            while (pos < end && 
+                   (isSeparator(bytes[pos]) || isWhiteSpace(bytes[pos]))) 
+                {pos++; } 
+
+            if (pos >= end)
+                return;
+
+            // Detect Special cookies
+            if (bytes[pos] == '$') {
+                isSpecial = true;
+                pos++;
+            }
+
+            // Get the cookie name. This must be a token            
+            valueEnd = valueStart = nameStart = pos; 
+            pos = nameEnd = getTokenEndPosition(bytes,pos,end);
+
+            // Skip whitespace
+            while (pos < end && isWhiteSpace(bytes[pos])) {pos++; } 
+         
+
+            // Check for an '=' -- This could also be a name-only
+            // cookie at the end of the cookie header, so if we
+            // are past the end of the header, but we have a name
+            // skip to the name-only part.
+            if (pos < end && bytes[pos] == '=') {                
+
+                // Skip whitespace
+                do {
+                    pos++;
+                } while (pos < end && isWhiteSpace(bytes[pos])); 
+
+                if (pos >= end)
+                    return;
+
+                // Determine what type of value this is, quoted value,
+                // token, name-only with an '=', or other (bad)
+                switch (bytes[pos]) {
+                case '"': // Quoted Value
+                    isQuoted = true;
+                    valueStart=pos + 1; // strip "
+                    // getQuotedValue returns the position before 
+                    // at the last qoute. This must be dealt with
+                    // when the bytes are copied into the cookie
+                    valueEnd=getQuotedValueEndPosition(bytes, 
+                                                       valueStart, end);
+                    // We need pos to advance
+                    pos = valueEnd; 
+                    // Handles cases where the quoted value is 
+                    // unterminated and at the end of the header, 
+                    // e.g. [myname="value]
+                    if (pos >= end)
+                        return;
+                    break;
+                case ';':
+                case ',':
+                    // Name-only cookie with an '=' after the name token
+                    // This may not be RFC compliant
+                    valueStart = valueEnd = -1;
+                    // The position is OK (On a delimiter)
+                    break;
+                default:
+                    if (!isSeparator(bytes[pos])) {
+                        // Token
+                        valueStart=pos;
+                        // getToken returns the position at the delimeter
+                        // or other non-token character
+                        valueEnd=getTokenEndPosition(bytes, valueStart, end);
+                        // We need pos to advance
+                        pos = valueEnd;
+                    } else  {
+                        // INVALID COOKIE, advance to next delimiter
+                        // The starting character of the cookie value was
+                        // not valid.
+                        //log("Invalid cookie. Value not a token or quoted value");
+                        while (pos < end && bytes[pos] != ';' && 
+                               bytes[pos] != ',') 
+                            {pos++; }
+                        pos++;
+                        // Make sure no special avpairs can be attributed to 
+                        // the previous cookie by setting the current cookie
+                        // to null
+                        sc = null;
+                        continue;                        
+                    }
+                }
+            } else {
+                // Name only cookie
+                valueStart = valueEnd = -1;
+                pos = nameEnd;
+
+            }
+          
+            // We should have an avpair or name-only cookie at this
+            // point. Perform some basic checks to make sure we are
+            // in a good state.
+  
+            // Skip whitespace
+            while (pos < end && isWhiteSpace(bytes[pos])) {pos++; }
+
+
+            // Make sure that after the cookie we have a separator. This
+            // is only important if this is not the last cookie pair
+            while (pos < end && bytes[pos] != ';' && bytes[pos] != ',') { 
+                pos++;
+            }
+            
+            pos++;
+
+            /*
+            if (nameEnd <= nameStart || valueEnd < valueStart ) {
+                // Something is wrong, but this may be a case
+                // of having two ';' characters in a row.
+                // log("Cookie name/value does not conform to RFC 2965");
+                // Advance to next delimiter (ignoring everything else)
+                while (pos < end && bytes[pos] != ';' && bytes[pos] != ',') 
+                    { pos++; };
+                pos++;
+                // Make sure no special cookies can be attributed to 
+                // the previous cookie by setting the current cookie
+                // to null
+                sc = null;
+                continue;
+            }
+            */
+
+            // All checks passed. Add the cookie, start with the 
+            // special avpairs first
+            if (isSpecial) {
+                isSpecial = false;
+                // $Version must be the first avpair in the cookie header
+                // (sc must be null)
+                if (equals( "Version", bytes, nameStart, nameEnd) && 
+                    sc == null) {
+                    // Set version
+                    if( bytes[valueStart] =='1' && valueEnd == (valueStart+1)) {
+                        version=1;
+                    } else {
+                        // unknown version (Versioning is not very strict)
+                    }
+                    continue;
+                } 
+                
+                // We need an active cookie for Path/Port/etc.
+                if (sc == null) {
+                    continue;
+                }
+
+                // Domain is more common, so it goes first
+                if (equals( "Domain", bytes, nameStart, nameEnd)) {
+                    sc.getDomain().setBytes( bytes,
+                                           valueStart,
+                                           valueEnd-valueStart);
+                    continue;
+                } 
+
+                if (equals( "Path", bytes, nameStart, nameEnd)) {
+                    sc.getPath().setBytes( bytes,
+                                           valueStart,
+                                           valueEnd-valueStart);
+                    continue;
+                } 
+
+
+                if (equals( "Port", bytes, nameStart, nameEnd)) {
+                    // sc.getPort is not currently implemented.
+                    // sc.getPort().setBytes( bytes,
+                    //                        valueStart,
+                    //                        valueEnd-valueStart );
+                    continue;
+                } 
+
+                // Unknown cookie, complain
+                //log("Unknown Special Cookie");
+
+            } else { // Normal Cookie
+                // use a previous value from cache, if any (to avoid GC - tomcat 
+                // legacy )
+                if (cookiesCache.size() > cookies.size()) {
+                    sc = cookiesCache.get(cookies.size());
+                    cookies.add(sc);
+                } else {
+                    sc = new ServerCookie();
+                    cookiesCache.add(sc);
+                    cookies.add(sc);
+                }
+                sc.setVersion( version );
+                sc.getName().append( bytes, nameStart,
+                                       nameEnd-nameStart);
+                
+                if (valueStart != -1) { // Normal AVPair
+                    sc.getValue().append( bytes, valueStart,
+                            valueEnd-valueStart);
+                    if (isQuoted) {
+                        // We know this is a byte value so this is safe
+                        ServerCookie.unescapeDoubleQuotes(
+                                sc.getValue());
+                    }
+                } else {
+                    // Name Only
+                    sc.getValue().recycle(); 
+                }
+                sc.nameC.recycle();
+                sc.nameC.append(sc.getName());
+                continue;
+            }
+        }
+    }
+
+    /**
+     * Given the starting position of a token, this gets the end of the
+     * token, with no separator characters in between.
+     * JVK
+     */
+    private static final int getTokenEndPosition(byte bytes[], int off, int end){
+        int pos = off;
+        while (pos < end && !isSeparator(bytes[pos])) {pos++; }
+        
+        if (pos > end)
+            return end;
+        return pos;
+    }
+
+    /** 
+     * Given a starting position after an initial quote chracter, this gets
+     * the position of the end quote. This escapes anything after a '\' char
+     * JVK RFC 2616
+     */
+    private static final int getQuotedValueEndPosition(byte bytes[], int off, int end){
+        int pos = off;
+        while (pos < end) {
+            if (bytes[pos] == '"') {
+                return pos;                
+            } else if (bytes[pos] == '\\' && pos < (end - 1)) {
+                pos+=2;
+            } else {
+                pos++;
+            }
+        }
+        // Error, we have reached the end of the header w/o a end quote
+        return end;
+    }
+    
+    
+    public static boolean equals( String s, byte b[], int start, int end) {
+        int blen = end-start;
+        if (b == null || blen != s.length()) {
+            return false;
+        }
+        int boff = start;
+        for (int i = 0; i < blen; i++) {
+            if (b[boff++] != s.charAt(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+    
+}
+
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/package.html b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/package.html
new file mode 100644 (file)
index 0000000..e69de29