Integrate upstream changes to 2.2.0.
am: f47b11cc8e

* commit 'f47b11cc8e8e27f27d1772c18f09135a948dd59f': (179 commits)
  [maven-release-plugin] prepare release nanohttpd-project-2.2.0
  added some descriptions to the pom
  code formating #253
  add a gradle dependecy description #222
  Decode headers before write remote-addr
  Rename NanoWebSocketServer to NanoWSD.
  attempt to fix the test
  Detached the WebSocket from the Server: It makes more sense if each socket knows how to handle itself.
  Updated WebSocket class to handle itself in an overridable manner.
  Added more tests; all coverage checks are met now.
  Added SSLServerSocketFactoryTest; Fixed non-static classes and wrong return.
  read the encode request header exact fix #240
  Added ServerSocketFactory JUnit test
  encoded multipart requests #240
  little imp correction and more test coverage #243
  Fix: strange compilation error
  Unit test - NanoHttpd throws an exception if temp directory doesn't exist #243
  Fix: Issue #243 : NanoHttpd throws an exception if temp directory doesn't exist #243
  Allows WebSocket servers to easily override default HTTP behavior.
  Fix content-length not being parsed as long
  ...
diff --git a/.gitignore b/.gitignore
index ff80817..dcc91d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@
 .swp
 .*.swp
 
+/.settings/
+/LICENSE.txt
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..d6b893b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,30 @@
+language: java
+jdk:
+- oraclejdk7
+
+before_install:
+  - cat ~/.m2/settings.xml
+  - rm  ~/.m2/settings.xml
+
+install:
+  - mvn install -DskipTests=true
+
+script:
+    - mvn test; export MAVEN_RESULT=$?
+    - if [ "$MAVEN_RESULT" -ne 0 ]; then exit 1; fi
+    - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then mvn clean deploy --quiet --settings settings.xml; fi
+
+after_success:
+  - mvn clean test jacoco:report coveralls:report
+  
+ 
+notifications:
+  email:
+    - richard.vannieuwenhoven@adesso.at
+    - elonen@iki.fi
+    
+env:
+ global:
+  - secure: "OtD0z3y4/OjSzg8irVD8v/u0TElcw8AiCCXb7a0UQEnTpGxcf5DOdkvHv0hF4xjHKFtlMMHxevW+a4C4NuFR8it8ZJ/i2m24reB28JicDcRQY9nwV/BR/T08CRG9KDz5EuTHgfPJDF0y+5MiVNwJVhHFviBWKuXyIuYouJ5IHgc="
+  - secure: "tgTVycNLwYDM3U0EVK1TkffylQHfZihvSDC9QoZEo+wz9aBBLoAtUJP7DWltRQFOkfTeGltGHEfGM2/qkIG6Wz+hNFG/fveHqyI01JWXc64d7yBm7agaCP5uTtt2wjaZ7ZK5Mps5QoufblYu+j9gb2v31t9IdsJ9PUs0+wgE/WU="
+  
diff --git a/README.md b/README.md
index 60b2578..e3b5eaa 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,252 @@
-## What is "nanohttpd"?
+## NanoHTTPD – a tiny web server in Java
 
-*NanoHttpd* is a light-weight HTTP server designed for embedding in other applications.
+*NanoHTTPD* is a light-weight HTTP server designed for embedding in other applications, released under a Modified BSD licence.
 
-*NanoHttpd* has been released under a Modified BSD licence.
+It is being developed at Github and uses Apache Maven for builds & unit testing:
 
-## Core Features
+ * Build status: [![Build Status](https://api.travis-ci.org/NanoHttpd/nanohttpd.png)](https://travis-ci.org/NanoHttpd/nanohttpd)
+ * Coverage Status: [![Coverage Status](https://coveralls.io/repos/NanoHttpd/nanohttpd/badge.svg)](https://coveralls.io/r/NanoHttpd/nanohttpd)
+ * Current central released version: [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.nanohttpd/nanohttpd/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.nanohttpd/nanohttpd)
+
+## Quickstart
+
+We'll create a custom HTTP server project using Maven for build/dep system. This tutorial assumes you are using a Unix variant and a shell. First, install Maven and Java SDK if not already installed. Then run:
+
+    mvn compile
+    mvn exec:java -pl webserver -Dexec.mainClass="fi.iki.elonen.SimpleWebServer"
+    
+You should now have a HTTP file server running on <http://localhost:8080/>.
+
+### Custom web app
+
+Let's raise the bar and build a custom web application next:
+
+    mvn archetype:generate -DgroupId=com.example -DartifactId=myHellopApp -DinteractiveMode=false
+    cd myHellopApp
+    
+Edit `pom.xml`, and add this between \<dependencies\>:
+ 
+	<dependency>
+		<groupId>org.nanohttpd</groupId> <!-- <groupId>com.nanohttpd</groupId> for 2.1.0 and earlier -->
+		<artifactId>nanohttpd</artifactId>
+		<version>2.2.0-SNAPSHOT</version>
+	</dependency>
+	
+Edit `src/main/java/com/example/App.java` and replace it with:
+```java
+package com.example;
+
+import java.util.Map;
+import java.io.IOException;
+import fi.iki.elonen.NanoHTTPD;
+
+public class App extends NanoHTTPD {
+
+    public App() throws IOException {
+        super(8080);
+        start();
+		System.out.println( "\nRunning! Point your browers to http://localhost:8080/ \n" );
+    }
+
+    public static void main(String[] args) {
+		try {
+		    new App();
+		}
+		catch( IOException ioe ) {
+			System.err.println( "Couldn't start server:\n" + ioe );
+		}
+    }
+
+    @Override
+    public Response serve(IHTTPSession session) {
+        String msg = "<html><body><h1>Hello server</h1>\n";
+        Map<String, String> parms = session.getParms();
+        if (parms.get("username") == null) {
+            msg += "<form action='?' method='get'>\n  <p>Your name: <input type='text' name='username'></p>\n" + "</form>\n";
+        } else {
+            msg += "<p>Hello, " + parms.get("username") + "!</p>";
+        }
+        return newFixedLengthResponse( msg + "</body></html>\n" );
+    }
+}
+```
+
+Compile and run the server:
+ 
+    mvn compile
+    mvn exec:java -Dexec.mainClass="com.example.App"
+    
+If it started ok, point your browser at <http://localhost:8080/> and enjoy a web server that asks your name and replies with a greeting. 
+
+### Nanolets
+
+Nanolets are like sevlet's only that they have a extrem low profile. They offer an easy to use system for a more complex server application. 
+This text has to be extrended with an example, so for now take a look at the unit tests for the usage. <https://github.com/NanoHttpd/nanohttpd/blob/master/nanolets/src/test/java/fi/iki/elonen/router/AppNanolets.java>
+
+## Status
+
+We are currently in the process of stabilizing NanoHttpd from the many pull requests and feature requests that were integrated over the last few months. The next release will come soon, and there will not be any more "intended" major changes before the next release. If you want to use the bleeding edge version, you can clone it from Github, or get it from sonatype.org (see "Maven dependencies / Living on the edge" below).
+
+## Project structure
+
+NanoHTTPD project currently consist of four parts:
+
+ * `/core` – Fully functional HTTP(s) server consisting of one (1) Java file, ready to be customized/inherited for your own project
+
+ * `/samples` – Simple examples on how to customize NanoHTTPD. See *HelloServer.java* for a killer app that greets you enthusiastically!
+
+ * `/websocket` – Websocket implementation, also in a single Java file. Depends on core.
+
+ * `/webserver` – Standalone file server. Run & enjoy. A popular use seems to be serving files out off an Android device.
+
+ * `/nanolets` – Standalone nano app server, giving a servlet like system to the implementor.
+
+ * `/fileupload` – integration of the apache common file upload library.
+
+## Features
+### Core
 * Only one Java file, providing HTTP 1.1 support.
-* 2 "flavors" - one at "current" standards and one strictly Java 1.1 compatible.
-* Released as open source, free software, under a Modified BSD licence.
-* No fixed config files, logging, authorization etc. (Implement by yourself if you need them.)
-* Experimental support for SSL (see the 'ssl-support' branch in git)
+* No fixed config files, logging, authorization etc. (Implement by yourself if you need them. Errors are passed to java.util.logging, though.)
+* Support for HTTPS (SSL)
 * Basic support for cookies
 * Supports parameter parsing of GET and POST methods.
-* Rudimentary PUT support (added in 1.25).
-* Support for HEAD and DELETE requests.
-* Supports single and multi-value parameters (w/ a helper method) if needed.
-* Supports file upload (since version 1.2, 2010) with minimal memory overhead.
+* Some built-in support for HEAD, POST and DELETE requests. You can easily implement/customize any HTTP method, though.
+* Supports file upload. Uses memory for small uploads, temp files for large ones.
 * Never caches anything.
-* Doesn't limit bandwidth, request time or simultaneous connections.
-* All header names are converted lowercase so they don't vary between browsers/clients.
-* Very low memory overhead when processing even the largest of requests.
-* Temp file usage and threading model are easily cutomized.
+* Does not limit bandwidth, request time or simultaneous connections by default.
+* All header names are converted to lower case so they don't vary between browsers/clients.
 * Persistent connections (Connection "keep-alive") support allowing multiple requests to be served over a single socket connection.
 
-## Websocket Support
+### Websocket
 * Tested on Firefox, Chrome and IE.
 
-## Webserver Features
+### Webserver
+* Default code serves files and shows (prints on console) all HTTP parameters and headers.
 * Supports both dynamic content and file serving.
-* Default code serves files and shows all HTTP parameters and headers.
-* File server supports directory listing, ```index.html``` and ```index.htm```.
-* File server supports partial content (streaming).
+* File server supports directory listing, `index.html` and `index.htm`.
+* File server supports partial content (streaming & continue download).
 * File server supports ETags.
-* File server does the 301 redirection trick for directories without ```/```.
-* File server supports simple skipping for files (continue download).
+* File server does the 301 redirection trick for directories without `/`.
 * File server serves also very long files without memory overhead.
-* Contains a built-in list of most common mime types.
-* Runtime extension support (extensions that serve particular mime types) - example extension that serves Markdown formatted files. Simply including an extension JAR in the webserver classpath is enough for the extension to be loaded.
+* Contains a built-in list of most common MIME types.
+* Runtime extension support (extensions that serve particular MIME types) - example extension that serves Markdown formatted files. Simply including an extension JAR in the webserver classpath is enough for the extension to be loaded.
+* Simple [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) support via `--cors` paramater
+  * by default serves `Access-Control-Allow-Headers: origin,accept,content-type`
+  * possibility to set `Access-Control-Allow-Headers` by setting System property: `AccessControlAllowHeader`
+  * _example: _ `-DAccessControlAllowHeader=origin,accept,content-type,Authorization`
+  * possible values:
+      * `--cors`: activates CORS support, `Access-Control-Allow-Origin` will be set to `*`
+      * `--cors=some_value`: `Access-Control-Allow-Origin` will be set to `some_value`. 
+
+**_CORS argument examples_**
+
+
+* `--cors=http://appOne.company.com`
+* `--cors="http://appOne.company.com, http://appTwo.company.com"`: note the double quotes so that the 2 URLs are considered part of a single argument.
+
+## Maven dependencies
+
+NanoHTTPD is a Maven based project and deployed to central. Most development environments have means to access the central repository. The coordinates to use in Maven are: 
+
+	<dependencies>
+		<dependency>
+			<groupId>org.nanohttpd</groupId> <!-- <groupId>com.nanohttpd</groupId> for 2.1.0 and earlier -->
+			<artifactId>nanohttpd</artifactId>
+			<version>CURRENT_VERSION</version>
+		</dependency>
+	</dependencies>
+
+(Replace `CURRENT_VERSION` with whatever is reported latest at <http://nanohttpd.org/>.)
+
+The coordinates for your development environment should correspond to these. When looking for an older version take care because we switched groupId from *com.nanohttpd* to *org.nanohttpd* in mid 2015.
+
+Next it depends what you are useing nanohttpd for, there are tree main usages.
+
+## Gradle dependencies
+
+In gradle you can use nano http the same way because gradle accesses the same central repository:
+
+	dependencies {
+		runtime(
+			[group: 'org.nanohttpd', name: 'nanohttpd', version: 'CURRENT_VERSION'],
+		)
+	}
+
+(Replace `CURRENT_VERSION` with whatever is reported latest at <http://nanohttpd.org/>.)
+
+Just replace the name with the artifact id of the module you want to use and gradle will find it for you. 
+
+### Develop your own specialized HTTP service
+
+For a specialized HTTP (HTTPS) service you can use the module with artifactId *nanohttpd*.
+
+		<dependency>
+			<groupId>org.nanohttpd</groupId> <!-- <groupId>com.nanohttpd</groupId> for 2.1.0 and earlier -->
+			<artifactId>nanohttpd</artifactId>
+			<version>CURRENT_VERSION</version>
+		</dependency>
+		
+Here you write your own subclass of *fi.iki.elonen.NanoHTTPD* to configure and to serve the requests.
+  
+### Develop a websocket based service    
+
+For a specialized websocket service you can use the module with artifactId *nanohttpd-websocket*.
+
+		<dependency>
+			<groupId>org.nanohttpd</groupId> <!-- <groupId>com.nanohttpd</groupId> for 2.1.0 and earlier -->
+			<artifactId>nanohttpd-websocket</artifactId>
+			<version>CURRENT_VERSION</version>
+		</dependency>
+
+Here you write your own subclass of *fi.iki.elonen.NanoWebSocketServer* to configure and to serve the websocket requests. A small standard echo example is included as *fi.iki.elonen.samples.echo.DebugWebSocketServer*. You can use it as a starting point to implement your own services.
+
+### Develop a custom HTTP file server    
+
+For a more classic aproach, perhaps to just create a HTTP server serving mostly service files from your disk, you can use the module with artifactId *nanohttpd-webserver*.
+
+		<dependency>
+			<groupId>org.nanohttpd</groupId>
+			<artifactId>nanohttpd-webserver</artifactId>
+			<version>CURRENT_VERSION</version>
+		</dependency>
+
+The included class *fi.iki.elonen.SimpleWebServer* is intended to be used as a starting point for your own implementation but it also can be used as is. Staring the class as is will start a http server on port 8080 and publishing the current directory.  
+
+### Living on the edge
+
+The latest Github master version can be fetched through sonatype.org:
+
+	<dependencies>
+		<dependency>
+			<artifactId>nanohttpd</artifactId>
+			<groupId>org.nanohttpd</groupId>
+			<version>XXXXX-SNAPSHOT</version>
+		</dependency>
+	</dependencies>
+	...
+	<repositories>
+		<repository>
+			<id>sonatype-snapshots</id>
+			<url>https://oss.sonatype.org/content/repositories/snapshots</url>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+		</repository>
+	</repositories>
+
+### generating an self signed ssl certificate
+
+Just a hint how to generate a certificate for localhost.
+
+	keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 360 -keysize 2048 -ext SAN=DNS:localhost,IP:127.0.0.1  -validity 9999
+
+This will generate a keystore file named 'keystore.jks' with a self signed certificate for a host named localhost with the ip adress 127.0.0.1 . Now 
+you can use:
+
+	server.makeSecure(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray()));
+
+Before you start the server to make Nanohttpd serve https connections, when you make sure 'keystore.jks' is in your classpath .  
+ 
+-----
 
 *Thank you to everyone who has reported bugs and suggested fixes.*
diff --git a/core/.gitignore b/core/.gitignore
new file mode 100644
index 0000000..868a6b2
--- /dev/null
+++ b/core/.gitignore
@@ -0,0 +1,2 @@
+/.settings/
+/LICENSE.txt
diff --git a/core/pom.xml b/core/pom.xml
index e82d9d0..0e4762d 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -1,83 +1,28 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-
-    <groupId>fi.iki.elonen</groupId>
-    <artifactId>nanohttpd</artifactId>
-    <version>2.1.0</version>
-    <packaging>jar</packaging>
-
-    <name>NanoHttpd-Core</name>
-    <url>https://github.com/NanoHttpd/nanohttpd</url>
-
-    <dependencies>
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>4.8.2</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.httpcomponents</groupId>
-            <artifactId>httpclient</artifactId>
-            <version>4.2.5</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.httpcomponents</groupId>
-            <artifactId>httpmime</artifactId>
-            <version>4.2.5</version>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-
-    <build>
-        <extensions>
-            <extension>
-                <groupId>org.jvnet.wagon-svn</groupId>
-                <artifactId>wagon-svn</artifactId>
-                <version>1.8</version>
-            </extension>
-            <extension>
-                <groupId>org.apache.maven.wagon</groupId>
-                <artifactId>wagon-ftp</artifactId>
-                <version>1.0-alpha-6</version>
-            </extension>
-        </extensions>
-
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>2.2.1</version>
-                <executions>
-                    <execution>
-                        <id>attach-sources</id>
-                        <goals>
-                            <goal>jar</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-release-plugin</artifactId>
-                <version>2.4</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>2.9</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <version>2.3.1</version>
-                <configuration>
-                    <source>1.6</source>
-                    <target>1.6</target>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.nanohttpd</groupId>
+		<artifactId>nanohttpd-project</artifactId>
+		<version>2.2.0</version>
+	</parent>
+	<artifactId>nanohttpd</artifactId>
+	<packaging>jar</packaging>
+	<name>NanoHttpd-Core</name>
+	<dependencies>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+			<version>4.2.5</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpmime</artifactId>
+			<version>4.2.5</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<properties>
+		<minimal.coverage>0.82</minimal.coverage>
+	</properties>
 </project>
diff --git a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
index 73fea3c..394db9f 100644
--- a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
+++ b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
@@ -1,35 +1,108 @@
 package fi.iki.elonen;
 
-import java.io.*;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.RandomAccessFile;
+import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.SocketException;
 import java.net.SocketTimeoutException;
+import java.net.URL;
 import java.net.URLDecoder;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
+import java.security.KeyStore;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.Date;
+import java.util.Enumeration;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Set;
+import java.util.Properties;
 import java.util.StringTokenizer;
 import java.util.TimeZone;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPOutputStream;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.TrustManagerFactory;
+
+import fi.iki.elonen.NanoHTTPD.Response.IStatus;
+import fi.iki.elonen.NanoHTTPD.Response.Status;
 
 /**
  * A simple, tiny, nicely embeddable HTTP server in Java
  * <p/>
  * <p/>
  * NanoHTTPD
- * <p></p>Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias</p>
+ * <p>
+ * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen,
+ * 2010 by Konstantinos Togias
+ * </p>
  * <p/>
  * <p/>
  * <b>Features + limitations: </b>
@@ -38,8 +111,10 @@
  * <li>Only one Java file</li>
  * <li>Java 5 compatible</li>
  * <li>Released as open source, Modified BSD licence</li>
- * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li>
- * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li>
+ * <li>No fixed config files, logging, authorization etc. (Implement yourself if
+ * you need them.)</li>
+ * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT
+ * support in 1.25)</li>
  * <li>Supports both dynamic content and file serving</li>
  * <li>Supports file upload (since version 1.2, 2010)</li>
  * <li>Supports partial content (streaming)</li>
@@ -53,8 +128,9 @@
  * <li>File server does the 301 redirection trick for directories without '/'</li>
  * <li>File server supports simple skipping for files (continue download)</li>
  * <li>File server serves also very long files without memory overhead</li>
- * <li>Contains a built-in list of most common mime types</li>
- * <li>All header names are converted lowercase so they don't vary between browsers/clients</li>
+ * <li>Contains a built-in list of most common MIME types</li>
+ * <li>All header names are converted to lower case so they don't vary between
+ * browsers/clients</li>
  * <p/>
  * </ul>
  * <p/>
@@ -66,406 +142,234 @@
  * <p/>
  * </ul>
  * <p/>
- * See the separate "LICENSE.md" file for the distribution license (Modified BSD licence)
+ * See the separate "LICENSE.md" file for the distribution license (Modified BSD
+ * licence)
  */
 public abstract class NanoHTTPD {
-    /**
-     * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
-     * This is required as the Keep-Alive HTTP connections would otherwise
-     * block the socket reading thread forever (or as long the browser is open).
-     */
-    public static final int SOCKET_READ_TIMEOUT = 5000;
-    /**
-     * Common mime type for dynamic content: plain text
-     */
-    public static final String MIME_PLAINTEXT = "text/plain";
-    /**
-     * Common mime type for dynamic content: html
-     */
-    public static final String MIME_HTML = "text/html";
-    /**
-     * Pseudo-Parameter to use to store the actual query string in the parameters map for later re-processing.
-     */
-    private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
-    private final String hostname;
-    private final int myPort;
-    private ServerSocket myServerSocket;
-    private Set<Socket> openConnections = new HashSet<Socket>();
-    private Thread myThread;
-    /**
-     * Pluggable strategy for asynchronously executing requests.
-     */
-    private AsyncRunner asyncRunner;
-    /**
-     * Pluggable strategy for creating and cleaning up temporary files.
-     */
-    private TempFileManagerFactory tempFileManagerFactory;
-
-    /**
-     * Constructs an HTTP server on given port.
-     */
-    public NanoHTTPD(int port) {
-        this(null, port);
-    }
-
-    /**
-     * Constructs an HTTP server on given hostname and port.
-     */
-    public NanoHTTPD(String hostname, int port) {
-        this.hostname = hostname;
-        this.myPort = port;
-        setTempFileManagerFactory(new DefaultTempFileManagerFactory());
-        setAsyncRunner(new DefaultAsyncRunner());
-    }
-
-    private static final void safeClose(Closeable closeable) {
-        if (closeable != null) {
-            try {
-                closeable.close();
-            } catch (IOException e) {
-            }
-        }
-    }
-
-    private static final void safeClose(Socket closeable) {
-        if (closeable != null) {
-            try {
-                closeable.close();
-            } catch (IOException e) {
-            }
-        }
-    }
-
-    private static final void safeClose(ServerSocket closeable) {
-        if (closeable != null) {
-            try {
-                closeable.close();
-            } catch (IOException e) {
-            }
-        }
-    }
-
-    /**
-     * Start the server.
-     *
-     * @throws IOException if the socket is in use.
-     */
-    public void start() throws IOException {
-        myServerSocket = new ServerSocket();
-        myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
-
-        myThread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                do {
-                    try {
-                        final Socket finalAccept = myServerSocket.accept();
-                        registerConnection(finalAccept);
-                        finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);
-                        final InputStream inputStream = finalAccept.getInputStream();
-                        asyncRunner.exec(new Runnable() {
-                            @Override
-                            public void run() {
-                                OutputStream outputStream = null;
-                                try {
-                                    outputStream = finalAccept.getOutputStream();
-                                    TempFileManager tempFileManager = tempFileManagerFactory.create();
-                                    HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress());
-                                    while (!finalAccept.isClosed()) {
-                                        session.execute();
-                                    }
-                                } catch (Exception e) {
-                                    // When the socket is closed by the client, we throw our own SocketException
-                                    // to break the  "keep alive" loop above.
-                                    if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) {
-                                        e.printStackTrace();
-                                    }
-                                } finally {
-                                    safeClose(outputStream);
-                                    safeClose(inputStream);
-                                    safeClose(finalAccept);
-                                    unRegisterConnection(finalAccept);
-                                }
-                            }
-                        });
-                    } catch (IOException e) {
-                    }
-                } while (!myServerSocket.isClosed());
-            }
-        });
-        myThread.setDaemon(true);
-        myThread.setName("NanoHttpd Main Listener");
-        myThread.start();
-    }
-
-    /**
-     * Stop the server.
-     */
-    public void stop() {
-        try {
-            safeClose(myServerSocket);
-            closeAllConnections();
-            if (myThread != null) {
-                myThread.join();
-            }
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    }
-
-    /**
-     * Registers that a new connection has been set up.
-     *
-     * @param socket the {@link Socket} for the connection.
-     */
-    public synchronized void registerConnection(Socket socket) {
-        openConnections.add(socket);
-    }
-
-    /**
-     * Registers that a connection has been closed
-     *
-     * @param socket
-     *            the {@link Socket} for the connection.
-     */
-    public synchronized void unRegisterConnection(Socket socket) {
-        openConnections.remove(socket);
-    }
-
-    /**
-     * Forcibly closes all connections that are open.
-     */
-    public synchronized void closeAllConnections() {
-        for (Socket socket : openConnections) {
-            safeClose(socket);
-        }
-    }
-
-    public final int getListeningPort() {
-        return myServerSocket == null ? -1 : myServerSocket.getLocalPort();
-    }
-
-    public final boolean wasStarted() {
-        return myServerSocket != null && myThread != null;
-    }
-
-    public final boolean isAlive() {
-        return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive();
-    }
-
-    /**
-     * Create a response with known length.
-     *
-     * TODO: Remove this implementation when updating to v2.2.0.
-     */
-    public static Response newFixedLengthResponse(Response.Status status, String mimeType, String txt) {
-        return new Response(status, mimeType, txt);
-    }
-
-    /**
-     * Override this to customize the server.
-     * <p/>
-     * <p/>
-     * (By default, this delegates to serveFile() and allows directory listing.)
-     *
-     * @param uri     Percent-decoded URI without parameters, for example "/index.cgi"
-     * @param method  "GET", "POST" etc.
-     * @param parms   Parsed, percent decoded parameters from URI and, in case of POST, data.
-     * @param headers Header entries, percent decoded
-     * @return HTTP response, see class Response for details
-     */
-    @Deprecated
-    public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms,
-                                   Map<String, String> files) {
-        return new Response(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found");
-    }
-
-    /**
-     * Override this to customize the server.
-     * <p/>
-     * <p/>
-     * (By default, this delegates to serveFile() and allows directory listing.)
-     *
-     * @param session The HTTP session
-     * @return HTTP response, see class Response for details
-     */
-    public Response serve(IHTTPSession session) {
-        Map<String, String> files = new HashMap<String, String>();
-        Method method = session.getMethod();
-        if (Method.PUT.equals(method) || Method.POST.equals(method)) {
-            try {
-                session.parseBody(files);
-            } catch (IOException ioe) {
-                return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
-            } catch (ResponseException re) {
-                return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
-            }
-        }
-
-        Map<String, String> parms = session.getParms();
-        parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString());
-        return serve(session.getUri(), method, session.getHeaders(), parms, files);
-    }
-
-    /**
-     * Decode percent encoded <code>String</code> values.
-     *
-     * @param str the percent encoded <code>String</code>
-     * @return expanded form of the input, for example "foo%20bar" becomes "foo bar"
-     */
-    protected String decodePercent(String str) {
-        String decoded = null;
-        try {
-            decoded = URLDecoder.decode(str, "UTF8");
-        } catch (UnsupportedEncodingException ignored) {
-        }
-        return decoded;
-    }
-
-    /**
-     * Decode parameters from a URL, handing the case where a single parameter name might have been
-     * supplied several times, by return lists of values.  In general these lists will contain a single
-     * element.
-     *
-     * @param parms original <b>NanoHttpd</b> parameters values, as passed to the <code>serve()</code> method.
-     * @return a map of <code>String</code> (parameter name) to <code>List&lt;String&gt;</code> (a list of the values supplied).
-     */
-    protected Map<String, List<String>> decodeParameters(Map<String, String> parms) {
-        return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER));
-    }
-
-    /**
-     * Decode parameters from a URL, handing the case where a single parameter name might have been
-     * supplied several times, by return lists of values.  In general these lists will contain a single
-     * element.
-     *
-     * @param queryString a query string pulled from the URL.
-     * @return a map of <code>String</code> (parameter name) to <code>List&lt;String&gt;</code> (a list of the values supplied).
-     */
-    protected Map<String, List<String>> decodeParameters(String queryString) {
-        Map<String, List<String>> parms = new HashMap<String, List<String>>();
-        if (queryString != null) {
-            StringTokenizer st = new StringTokenizer(queryString, "&");
-            while (st.hasMoreTokens()) {
-                String e = st.nextToken();
-                int sep = e.indexOf('=');
-                String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
-                if (!parms.containsKey(propertyName)) {
-                    parms.put(propertyName, new ArrayList<String>());
-                }
-                String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null;
-                if (propertyValue != null) {
-                    parms.get(propertyName).add(propertyValue);
-                }
-            }
-        }
-        return parms;
-    }
-
-    // ------------------------------------------------------------------------------- //
-    //
-    // Threading Strategy.
-    //
-    // ------------------------------------------------------------------------------- //
-
-    /**
-     * Pluggable strategy for asynchronously executing requests.
-     *
-     * @param asyncRunner new strategy for handling threads.
-     */
-    public void setAsyncRunner(AsyncRunner asyncRunner) {
-        this.asyncRunner = asyncRunner;
-    }
-
-    // ------------------------------------------------------------------------------- //
-    //
-    // Temp file handling strategy.
-    //
-    // ------------------------------------------------------------------------------- //
-
-    /**
-     * Pluggable strategy for creating and cleaning up temporary files.
-     *
-     * @param tempFileManagerFactory new strategy for handling temp files.
-     */
-    public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
-        this.tempFileManagerFactory = tempFileManagerFactory;
-    }
-
-    /**
-     * HTTP Request methods, with the ability to decode a <code>String</code> back to its enum value.
-     */
-    public enum Method {
-        GET, PUT, POST, DELETE, HEAD, OPTIONS;
-
-        static Method lookup(String method) {
-            for (Method m : Method.values()) {
-                if (m.toString().equalsIgnoreCase(method)) {
-                    return m;
-                }
-            }
-            return null;
-        }
-    }
 
     /**
      * Pluggable strategy for asynchronously executing requests.
      */
     public interface AsyncRunner {
-        void exec(Runnable code);
+
+        void closeAll();
+
+        void closed(ClientHandler clientHandler);
+
+        void exec(ClientHandler code);
     }
 
     /**
-     * Factory to create temp file managers.
+     * The runnable that will be used for every new client connection.
      */
-    public interface TempFileManagerFactory {
-        TempFileManager create();
-    }
+    public class ClientHandler implements Runnable {
 
-    // ------------------------------------------------------------------------------- //
+        private final InputStream inputStream;
 
-    /**
-     * Temp file manager.
-     * <p/>
-     * <p>Temp file managers are created 1-to-1 with incoming requests, to create and cleanup
-     * temporary files created as a result of handling the request.</p>
-     */
-    public interface TempFileManager {
-        TempFile createTempFile() throws Exception;
+        private final Socket acceptSocket;
 
-        void clear();
-    }
+        private ClientHandler(InputStream inputStream, Socket acceptSocket) {
+            this.inputStream = inputStream;
+            this.acceptSocket = acceptSocket;
+        }
 
-    /**
-     * A temp file.
-     * <p/>
-     * <p>Temp files are responsible for managing the actual temporary storage and cleaning
-     * themselves up when no longer needed.</p>
-     */
-    public interface TempFile {
-        OutputStream open() throws Exception;
-
-        void delete() throws Exception;
-
-        String getName();
-    }
-
-    /**
-     * Default threading strategy for NanoHttpd.
-     * <p/>
-     * <p>By default, the server spawns a new Thread for every incoming request.  These are set
-     * to <i>daemon</i> status, and named according to the request number.  The name is
-     * useful when profiling the application.</p>
-     */
-    public static class DefaultAsyncRunner implements AsyncRunner {
-        private long requestCount;
+        public void close() {
+            safeClose(this.inputStream);
+            safeClose(this.acceptSocket);
+        }
 
         @Override
-        public void exec(Runnable code) {
-            ++requestCount;
-            Thread t = new Thread(code);
+        public void run() {
+            OutputStream outputStream = null;
+            try {
+                outputStream = this.acceptSocket.getOutputStream();
+                TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create();
+                HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress());
+                while (!this.acceptSocket.isClosed()) {
+                    session.execute();
+                }
+            } catch (Exception e) {
+                // When the socket is closed by the client,
+                // we throw our own SocketException
+                // to break the "keep alive" loop above. If
+                // the exception was anything other
+                // than the expected SocketException OR a
+                // SocketTimeoutException, print the
+                // stacktrace
+                if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) {
+                    NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
+                }
+            } finally {
+                safeClose(outputStream);
+                safeClose(this.inputStream);
+                safeClose(this.acceptSocket);
+                NanoHTTPD.this.asyncRunner.closed(this);
+            }
+        }
+    }
+
+    public static class Cookie {
+
+        public static String getHTTPTime(int days) {
+            Calendar calendar = Calendar.getInstance();
+            SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
+            dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+            calendar.add(Calendar.DAY_OF_MONTH, days);
+            return dateFormat.format(calendar.getTime());
+        }
+
+        private final String n, v, e;
+
+        public Cookie(String name, String value) {
+            this(name, value, 30);
+        }
+
+        public Cookie(String name, String value, int numDays) {
+            this.n = name;
+            this.v = value;
+            this.e = getHTTPTime(numDays);
+        }
+
+        public Cookie(String name, String value, String expires) {
+            this.n = name;
+            this.v = value;
+            this.e = expires;
+        }
+
+        public String getHTTPHeader() {
+            String fmt = "%s=%s; expires=%s";
+            return String.format(fmt, this.n, this.v, this.e);
+        }
+    }
+
+    /**
+     * Provides rudimentary support for cookies. Doesn't support 'path',
+     * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported
+     * features.
+     * 
+     * @author LordFokas
+     */
+    public class CookieHandler implements Iterable<String> {
+
+        private final HashMap<String, String> cookies = new HashMap<String, String>();
+
+        private final ArrayList<Cookie> queue = new ArrayList<Cookie>();
+
+        public CookieHandler(Map<String, String> httpHeaders) {
+            String raw = httpHeaders.get("cookie");
+            if (raw != null) {
+                String[] tokens = raw.split(";");
+                for (String token : tokens) {
+                    String[] data = token.trim().split("=");
+                    if (data.length == 2) {
+                        this.cookies.put(data[0], data[1]);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Set a cookie with an expiration date from a month ago, effectively
+         * deleting it on the client side.
+         * 
+         * @param name
+         *            The cookie name.
+         */
+        public void delete(String name) {
+            set(name, "-delete-", -30);
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            return this.cookies.keySet().iterator();
+        }
+
+        /**
+         * Read a cookie from the HTTP Headers.
+         * 
+         * @param name
+         *            The cookie's name.
+         * @return The cookie's value if it exists, null otherwise.
+         */
+        public String read(String name) {
+            return this.cookies.get(name);
+        }
+
+        public void set(Cookie cookie) {
+            this.queue.add(cookie);
+        }
+
+        /**
+         * Sets a cookie.
+         * 
+         * @param name
+         *            The cookie's name.
+         * @param value
+         *            The cookie's value.
+         * @param expires
+         *            How many days until the cookie expires.
+         */
+        public void set(String name, String value, int expires) {
+            this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
+        }
+
+        /**
+         * Internally used by the webserver to add all queued cookies into the
+         * Response's HTTP Headers.
+         * 
+         * @param response
+         *            The Response object to which headers the queued cookies
+         *            will be added.
+         */
+        public void unloadQueue(Response response) {
+            for (Cookie cookie : this.queue) {
+                response.addHeader("Set-Cookie", cookie.getHTTPHeader());
+            }
+        }
+    }
+
+    /**
+     * Default threading strategy for NanoHTTPD.
+     * <p/>
+     * <p>
+     * By default, the server spawns a new Thread for every incoming request.
+     * These are set to <i>daemon</i> status, and named according to the request
+     * number. The name is useful when profiling the application.
+     * </p>
+     */
+    public static class DefaultAsyncRunner implements AsyncRunner {
+
+        private long requestCount;
+
+        private final List<ClientHandler> running = Collections.synchronizedList(new ArrayList<NanoHTTPD.ClientHandler>());
+
+        /**
+         * @return a list with currently running clients.
+         */
+        public List<ClientHandler> getRunning() {
+            return running;
+        }
+
+        @Override
+        public void closeAll() {
+            // copy of the list for concurrency
+            for (ClientHandler clientHandler : new ArrayList<ClientHandler>(this.running)) {
+                clientHandler.close();
+            }
+        }
+
+        @Override
+        public void closed(ClientHandler clientHandler) {
+            this.running.remove(clientHandler);
+        }
+
+        @Override
+        public void exec(ClientHandler clientHandler) {
+            ++this.requestCount;
+            Thread t = new Thread(clientHandler);
             t.setDaemon(true);
-            t.setName("NanoHttpd Request Processor (#" + requestCount + ")");
+            t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")");
+            this.running.add(clientHandler);
             t.start();
         }
     }
@@ -473,342 +377,82 @@
     /**
      * Default strategy for creating and cleaning up temporary files.
      * <p/>
-     * <p></p>This class stores its files in the standard location (that is,
-     * wherever <code>java.io.tmpdir</code> points to).  Files are added
-     * to an internal list, and deleted when no longer needed (that is,
-     * when <code>clear()</code> is invoked at the end of processing a
-     * request).</p>
+     * <p>
+     * By default, files are created by <code>File.createTempFile()</code> in
+     * the directory specified.
+     * </p>
      */
-    public static class DefaultTempFileManager implements TempFileManager {
-        private final String tmpdir;
-        private final List<TempFile> tempFiles;
+    public static class DefaultTempFile implements TempFile {
 
-        public DefaultTempFileManager() {
-            tmpdir = System.getProperty("java.io.tmpdir");
-            tempFiles = new ArrayList<TempFile>();
+        private final File file;
+
+        private final OutputStream fstream;
+
+        public DefaultTempFile(File tempdir) throws IOException {
+            this.file = File.createTempFile("NanoHTTPD-", "", tempdir);
+            this.fstream = new FileOutputStream(this.file);
         }
 
         @Override
-        public TempFile createTempFile() throws Exception {
-            DefaultTempFile tempFile = new DefaultTempFile(tmpdir);
-            tempFiles.add(tempFile);
-            return tempFile;
-        }
-
-        @Override
-        public void clear() {
-            for (TempFile file : tempFiles) {
-                try {
-                    file.delete();
-                } catch (Exception ignored) {
-                }
+        public void delete() throws Exception {
+            safeClose(this.fstream);
+            if (!this.file.delete()) {
+                throw new Exception("could not delete temporary file");
             }
-            tempFiles.clear();
+        }
+
+        @Override
+        public String getName() {
+            return this.file.getAbsolutePath();
+        }
+
+        @Override
+        public OutputStream open() throws Exception {
+            return this.fstream;
         }
     }
 
     /**
      * Default strategy for creating and cleaning up temporary files.
      * <p/>
-     * <p></p></[>By default, files are created by <code>File.createTempFile()</code> in
-     * the directory specified.</p>
+     * <p>
+     * This class stores its files in the standard location (that is, wherever
+     * <code>java.io.tmpdir</code> points to). Files are added to an internal
+     * list, and deleted when no longer needed (that is, when
+     * <code>clear()</code> is invoked at the end of processing a request).
+     * </p>
      */
-    public static class DefaultTempFile implements TempFile {
-        private File file;
-        private OutputStream fstream;
+    public static class DefaultTempFileManager implements TempFileManager {
 
-        public DefaultTempFile(String tempdir) throws IOException {
-            file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
-            fstream = new FileOutputStream(file);
+        private final File tmpdir;
+
+        private final List<TempFile> tempFiles;
+
+        public DefaultTempFileManager() {
+            this.tmpdir = new File(System.getProperty("java.io.tmpdir"));
+            if (!tmpdir.exists()) {
+                tmpdir.mkdirs();
+            }
+            this.tempFiles = new ArrayList<TempFile>();
         }
 
         @Override
-        public OutputStream open() throws Exception {
-            return fstream;
+        public void clear() {
+            for (TempFile file : this.tempFiles) {
+                try {
+                    file.delete();
+                } catch (Exception ignored) {
+                    NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored);
+                }
+            }
+            this.tempFiles.clear();
         }
 
         @Override
-        public void delete() throws Exception {
-            safeClose(fstream);
-            file.delete();
-        }
-
-        @Override
-        public String getName() {
-            return file.getAbsolutePath();
-        }
-    }
-
-    /**
-     * HTTP response. Return one of these from serve().
-     */
-    public static class Response {
-        /**
-         * HTTP status code after processing, e.g. "200 OK", HTTP_OK
-         */
-        private IStatus status;
-        /**
-         * MIME type of content, e.g. "text/html"
-         */
-        private String mimeType;
-        /**
-         * Data of the response, may be null.
-         */
-        private InputStream data;
-        /**
-         * Headers for the HTTP response. Use addHeader() to add lines.
-         */
-        private Map<String, String> header = new HashMap<String, String>();
-        /**
-         * The request method that spawned this response.
-         */
-        private Method requestMethod;
-        /**
-         * Use chunkedTransfer
-         */
-        private boolean chunkedTransfer;
-
-        /**
-         * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message
-         */
-        public Response(String msg) {
-            this(Status.OK, MIME_HTML, msg);
-        }
-
-        /**
-         * Basic constructor.
-         */
-        public Response(IStatus status, String mimeType, InputStream data) {
-            this.status = status;
-            this.mimeType = mimeType;
-            this.data = data;
-        }
-
-        /**
-         * Convenience method that makes an InputStream out of given text.
-         */
-        public Response(IStatus status, String mimeType, String txt) {
-            this.status = status;
-            this.mimeType = mimeType;
-            try {
-                this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null;
-            } catch (java.io.UnsupportedEncodingException uee) {
-                uee.printStackTrace();
-            }
-        }
-
-        /**
-         * Adds given line to the header.
-         */
-        public void addHeader(String name, String value) {
-            header.put(name, value);
-        }
-
-        public String getHeader(String name) {
-            return header.get(name);
-        }
-
-        /**
-         * Sends given response to the socket.
-         */
-        protected void send(OutputStream outputStream) {
-            String mime = mimeType;
-            SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
-            gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
-
-            try {
-                if (status == null) {
-                    throw new Error("sendResponse(): Status can't be null.");
-                }
-                PrintWriter pw = new PrintWriter(outputStream);
-                pw.print("HTTP/1.1 " + status.getDescription() + " \r\n");
-
-                if (mime != null) {
-                    pw.print("Content-Type: " + mime + "\r\n");
-                }
-
-                if (header == null || header.get("Date") == null) {
-                    pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
-                }
-
-                if (header != null) {
-                    for (String key : header.keySet()) {
-                        String value = header.get(key);
-                        pw.print(key + ": " + value + "\r\n");
-                    }
-                }
-
-                sendConnectionHeaderIfNotAlreadyPresent(pw, header);
-
-                if (requestMethod != Method.HEAD && chunkedTransfer) {
-                    sendAsChunked(outputStream, pw);
-                } else {
-                    int pending = data != null ? data.available() : 0;
-                    sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending);
-                    pw.print("\r\n");
-                    pw.flush();
-                    sendAsFixedLength(outputStream, pending);
-                }
-                outputStream.flush();
-                safeClose(data);
-            } catch (IOException ioe) {
-                // Couldn't write? No can do.
-            }
-        }
-
-        protected void sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size) {
-            if (!headerAlreadySent(header, "content-length")) {
-                pw.print("Content-Length: "+ size +"\r\n");
-            }
-        }
-
-        protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) {
-            if (!headerAlreadySent(header, "connection")) {
-                pw.print("Connection: keep-alive\r\n");
-            }
-        }
-
-        private boolean headerAlreadySent(Map<String, String> header, String name) {
-            boolean alreadySent = false;
-            for (String headerName : header.keySet()) {
-                alreadySent |= headerName.equalsIgnoreCase(name);
-            }
-            return alreadySent;
-        }
-
-        private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {
-            pw.print("Transfer-Encoding: chunked\r\n");
-            pw.print("\r\n");
-            pw.flush();
-            int BUFFER_SIZE = 16 * 1024;
-            byte[] CRLF = "\r\n".getBytes();
-            byte[] buff = new byte[BUFFER_SIZE];
-            int read;
-            while ((read = data.read(buff)) > 0) {
-                outputStream.write(String.format("%x\r\n", read).getBytes());
-                outputStream.write(buff, 0, read);
-                outputStream.write(CRLF);
-            }
-            outputStream.write(String.format("0\r\n\r\n").getBytes());
-        }
-
-        private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException {
-            if (requestMethod != Method.HEAD && data != null) {
-                int BUFFER_SIZE = 16 * 1024;
-                byte[] buff = new byte[BUFFER_SIZE];
-                while (pending > 0) {
-                    int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
-                    if (read <= 0) {
-                        break;
-                    }
-                    outputStream.write(buff, 0, read);
-                    pending -= read;
-                }
-            }
-        }
-
-        public IStatus getStatus() {
-            return status;
-        }
-
-        public void setStatus(Status status) {
-            this.status = status;
-        }
-
-        public String getMimeType() {
-            return mimeType;
-        }
-
-        public void setMimeType(String mimeType) {
-            this.mimeType = mimeType;
-        }
-
-        public InputStream getData() {
-            return data;
-        }
-
-        public void setData(InputStream data) {
-            this.data = data;
-        }
-
-        public Method getRequestMethod() {
-            return requestMethod;
-        }
-
-        public void setRequestMethod(Method requestMethod) {
-            this.requestMethod = requestMethod;
-        }
-
-        public void setChunkedTransfer(boolean chunkedTransfer) {
-            this.chunkedTransfer = chunkedTransfer;
-        }
-
-        public interface IStatus {
-            int getRequestStatus();
-            String getDescription();
-        }
-
-        /**
-         * Some HTTP response status codes
-         */
-        public enum Status implements IStatus {
-            SWITCH_PROTOCOL(101, "Switching Protocols"),
-
-            OK(200, "OK"),
-            CREATED(201, "Created"),
-            ACCEPTED(202, "Accepted"),
-            NO_CONTENT(204, "No Content"),
-            PARTIAL_CONTENT(206, "Partial Content"),
-
-            REDIRECT(301, "Moved Permanently"),
-            TEMPORARY_REDIRECT(302, "Moved Temporarily"),
-            NOT_MODIFIED(304, "Not Modified"),
-
-            BAD_REQUEST(400, "Bad Request"),
-            UNAUTHORIZED(401, "Unauthorized"),
-            FORBIDDEN(403, "Forbidden"),
-            NOT_FOUND(404, "Not Found"),
-            METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
-            RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
-
-            INTERNAL_ERROR(500, "Internal Server Error");
-
-            private final int requestStatus;
-            private final String description;
-
-            Status(int requestStatus, String description) {
-                this.requestStatus = requestStatus;
-                this.description = description;
-            }
-
-            @Override
-            public int getRequestStatus() {
-                return this.requestStatus;
-            }
-
-            @Override
-            public String getDescription() {
-                return "" + this.requestStatus + " " + description;
-            }
-        }
-    }
-
-    public static final class ResponseException extends Exception {
-
-        private final Response.Status status;
-
-        public ResponseException(Response.Status status, String message) {
-            super(message);
-            this.status = status;
-        }
-
-        public ResponseException(Response.Status status, String message, Exception e) {
-            super(message, e);
-            this.status = status;
-        }
-
-        public Response.Status getStatus() {
-            return status;
+        public TempFile createTempFile(String filename_hint) throws Exception {
+            DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir);
+            this.tempFiles.add(tempFile);
+            return tempFile;
         }
     }
 
@@ -816,255 +460,130 @@
      * Default strategy for creating and cleaning up temporary files.
      */
     private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
+
         @Override
         public TempFileManager create() {
             return new DefaultTempFileManager();
         }
     }
 
+    private static final String CHARSET_REGEX = "[ |\t]*(charset)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;]*)['|\"]?";
+
+    private static final Pattern CHARSET_PATTERN = Pattern.compile(CHARSET_REGEX, Pattern.CASE_INSENSITIVE);
+
+    private static final String BOUNDARY_REGEX = "[ |\t]*(boundary)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;]*)['|\"]?";
+
+    private static final Pattern BOUNDARY_PATTERN = Pattern.compile(BOUNDARY_REGEX, Pattern.CASE_INSENSITIVE);
+
     /**
-     * Handles one session, i.e. parses the HTTP request and returns the response.
+     * Creates a normal ServerSocket for TCP connections
      */
-    public interface IHTTPSession {
-        void execute() throws IOException;
+    public static class DefaultServerSocketFactory implements ServerSocketFactory {
 
-        Map<String, String> getParms();
+        @Override
+        public ServerSocket create() throws IOException {
+            return new ServerSocket();
+        }
 
-        Map<String, String> getHeaders();
-
-        /**
-         * @return the path part of the URL.
-         */
-        String getUri();
-
-        String getQueryParameterString();
-
-        Method getMethod();
-
-        InputStream getInputStream();
-
-        CookieHandler getCookies();
-
-        /**
-         * Adds the files in the request body to the files map.
-         * @arg files - map to modify
-         */
-        void parseBody(Map<String, String> files) throws IOException, ResponseException;
     }
 
+    /**
+     * Creates a new SSLServerSocket
+     */
+    public static class SecureServerSocketFactory implements ServerSocketFactory {
+
+        private SSLServerSocketFactory sslServerSocketFactory;
+
+        private String[] sslProtocols;
+
+        public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) {
+            this.sslServerSocketFactory = sslServerSocketFactory;
+            this.sslProtocols = sslProtocols;
+        }
+
+        @Override
+        public ServerSocket create() throws IOException {
+            SSLServerSocket ss = null;
+            ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket();
+            if (this.sslProtocols != null) {
+                ss.setEnabledProtocols(this.sslProtocols);
+            } else {
+                ss.setEnabledProtocols(ss.getSupportedProtocols());
+            }
+            ss.setUseClientMode(false);
+            ss.setWantClientAuth(false);
+            ss.setNeedClientAuth(false);
+            return ss;
+        }
+
+    }
+
+    private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)";
+
+    private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE);
+
+    private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)";
+
+    private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE);
+
+    private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]";
+
+    private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX);
+
     protected class HTTPSession implements IHTTPSession {
+
+        private static final int REQUEST_BUFFER_LEN = 512;
+
+        private static final int MEMORY_STORE_LIMIT = 1024;
+
         public static final int BUFSIZE = 8192;
+
+        public static final int MAX_HEADER_SIZE = 1024;
+
         private final TempFileManager tempFileManager;
+
         private final OutputStream outputStream;
-        private PushbackInputStream inputStream;
+
+        private final BufferedInputStream inputStream;
+
         private int splitbyte;
+
         private int rlen;
+
         private String uri;
+
         private Method method;
+
         private Map<String, String> parms;
+
         private Map<String, String> headers;
+
         private CookieHandler cookies;
+
         private String queryParameterString;
 
+        private String remoteIp;
+
+        private String protocolVersion;
+
         public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
             this.tempFileManager = tempFileManager;
-            this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
+            this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE);
             this.outputStream = outputStream;
         }
 
         public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
             this.tempFileManager = tempFileManager;
-            this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
+            this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE);
             this.outputStream = outputStream;
-            String remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
-            headers = new HashMap<String, String>();
-
-            headers.put("remote-addr", remoteIp);
-            headers.put("http-client-ip", remoteIp);
-        }
-
-        @Override
-        public void execute() throws IOException {
-            try {
-                // Read the first 8192 bytes.
-                // The full header should fit in here.
-                // Apache's default header limit is 8KB.
-                // Do NOT assume that a single read will get the entire header at once!
-                byte[] buf = new byte[BUFSIZE];
-                splitbyte = 0;
-                rlen = 0;
-                {
-                    int read = -1;
-                    try {
-                        read = inputStream.read(buf, 0, BUFSIZE);
-                    } catch (Exception e) {
-                        safeClose(inputStream);
-                        safeClose(outputStream);
-                        throw new SocketException("NanoHttpd Shutdown");
-                    }
-                    if (read == -1) {
-                        // socket was been closed
-                        safeClose(inputStream);
-                        safeClose(outputStream);
-                        throw new SocketException("NanoHttpd Shutdown");
-                    }
-                    while (read > 0) {
-                        rlen += read;
-                        splitbyte = findHeaderEnd(buf, rlen);
-                        if (splitbyte > 0)
-                            break;
-                        read = inputStream.read(buf, rlen, BUFSIZE - rlen);
-                    }
-                }
-
-                if (splitbyte < rlen) {
-                    inputStream.unread(buf, splitbyte, rlen - splitbyte);
-                }
-
-                parms = new HashMap<String, String>();
-                if(null == headers) {
-                    headers = new HashMap<String, String>();
-                }
-
-                // Create a BufferedReader for parsing the header.
-                BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));
-
-                // Decode the header into parms and header java properties
-                Map<String, String> pre = new HashMap<String, String>();
-                decodeHeader(hin, pre, parms, headers);
-
-                method = Method.lookup(pre.get("method"));
-                if (method == null) {
-                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
-                }
-
-                uri = pre.get("uri");
-
-                cookies = new CookieHandler(headers);
-
-                // Ok, now do the serve()
-                Response r = serve(this);
-                if (r == null) {
-                    throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
-                } else {
-                    cookies.unloadQueue(r);
-                    r.setRequestMethod(method);
-                    r.send(outputStream);
-                }
-            } catch (SocketException e) {
-                // throw it out to close socket object (finalAccept)
-                throw e;
-            } catch (SocketTimeoutException ste) {
-            	throw ste;
-            } catch (IOException ioe) {
-                Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
-                r.send(outputStream);
-                safeClose(outputStream);
-            } catch (ResponseException re) {
-                Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
-                r.send(outputStream);
-                safeClose(outputStream);
-            } finally {
-                tempFileManager.clear();
-            }
-        }
-
-        @Override
-        public void parseBody(Map<String, String> files) throws IOException, ResponseException {
-            RandomAccessFile randomAccessFile = null;
-            BufferedReader in = null;
-            try {
-
-                randomAccessFile = getTmpBucket();
-
-                long size;
-                if (headers.containsKey("content-length")) {
-                    size = Integer.parseInt(headers.get("content-length"));
-                } else if (splitbyte < rlen) {
-                    size = rlen - splitbyte;
-                } else {
-                    size = 0;
-                }
-
-                // Now read all the body and write it to f
-                byte[] buf = new byte[512];
-                while (rlen >= 0 && size > 0) {
-                    rlen = inputStream.read(buf, 0, (int)Math.min(size, 512));
-                    size -= rlen;
-                    if (rlen > 0) {
-                        randomAccessFile.write(buf, 0, rlen);
-                    }
-                }
-
-                // Get the raw body as a byte []
-                ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
-                randomAccessFile.seek(0);
-
-                // Create a BufferedReader for easily reading it as string.
-                InputStream bin = new FileInputStream(randomAccessFile.getFD());
-                in = new BufferedReader(new InputStreamReader(bin));
-
-                // If the method is POST, there may be parameters
-                // in data section, too, read it:
-                if (Method.POST.equals(method)) {
-                    String contentType = "";
-                    String contentTypeHeader = headers.get("content-type");
-
-                    StringTokenizer st = null;
-                    if (contentTypeHeader != null) {
-                        st = new StringTokenizer(contentTypeHeader, ",; ");
-                        if (st.hasMoreTokens()) {
-                            contentType = st.nextToken();
-                        }
-                    }
-
-                    if ("multipart/form-data".equalsIgnoreCase(contentType)) {
-                        // Handle multipart/form-data
-                        if (!st.hasMoreTokens()) {
-                            throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
-                        }
-
-                        String boundaryStartString = "boundary=";
-                        int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
-                        String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
-                        if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
-                            boundary = boundary.substring(1, boundary.length() - 1);
-                        }
-
-                        decodeMultipartData(boundary, fbuf, in, parms, files);
-                    } else {
-                        String postLine = "";
-                        StringBuilder postLineBuffer = new StringBuilder();
-                        char pbuf[] = new char[512];
-                        int read = in.read(pbuf);
-                        while (read >= 0 && !postLine.endsWith("\r\n")) {
-                            postLine = String.valueOf(pbuf, 0, read);
-                            postLineBuffer.append(postLine);
-                            read = in.read(pbuf);
-                        }
-                        postLine = postLineBuffer.toString().trim();
-                        // Handle application/x-www-form-urlencoded
-                        if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
-                        	decodeParms(postLine, parms);
-                        } else if (postLine.length() != 0) {
-                        	// Special case for raw POST data => create a special files entry "postData" with raw content data
-                        	files.put("postData", postLine);
-                        }
-                    }
-                } else if (Method.PUT.equals(method)) {
-                    files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
-                }
-            } finally {
-                safeClose(randomAccessFile);
-                safeClose(in);
-            }
+            this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
+            this.headers = new HashMap<String, String>();
         }
 
         /**
          * Decodes the sent headers and loads the data into Key/value pairs
          */
-        private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)
-            throws ResponseException {
+        private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) throws ResponseException {
             try {
                 // Read the request line
                 String inLine = in.readLine();
@@ -1094,18 +613,23 @@
                     uri = decodePercent(uri);
                 }
 
-                // If there's another token, it's protocol version,
-                // followed by HTTP headers. Ignore version but parse headers.
-                // NOTE: this now forces header names lowercase since they are
+                // If there's another token, its protocol version,
+                // followed by HTTP headers.
+                // NOTE: this now forces header names lower case since they are
                 // case insensitive and vary by client.
                 if (st.hasMoreTokens()) {
-                    String line = in.readLine();
-                    while (line != null && line.trim().length() > 0) {
-                        int p = line.indexOf(':');
-                        if (p >= 0)
-                            headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
-                        line = in.readLine();
+                    protocolVersion = st.nextToken();
+                } else {
+                    protocolVersion = "HTTP/1.1";
+                    NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1.");
+                }
+                String line = in.readLine();
+                while (line != null && line.trim().length() > 0) {
+                    int p = line.indexOf(':');
+                    if (p >= 0) {
+                        headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
                     }
+                    line = in.readLine();
                 }
 
                 pre.put("uri", uri);
@@ -1117,130 +641,460 @@
         /**
          * Decodes the Multipart Body data and put it into Key/Value pairs.
          */
-        private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms,
-                                         Map<String, String> files) throws ResponseException {
+        private void decodeMultipartFormData(String boundary, String encoding, ByteBuffer fbuf, Map<String, String> parms, Map<String, String> files) throws ResponseException {
             try {
-                int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes());
-                int boundarycount = 1;
-                String mpline = in.readLine();
-                while (mpline != null) {
-                    if (!mpline.contains(boundary)) {
-                        throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html");
-                    }
-                    boundarycount++;
-                    Map<String, String> item = new HashMap<String, String>();
-                    mpline = in.readLine();
-                    while (mpline != null && mpline.trim().length() > 0) {
-                        int p = mpline.indexOf(':');
-                        if (p != -1) {
-                            item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim());
-                        }
-                        mpline = in.readLine();
-                    }
-                    if (mpline != null) {
-                        String contentDisposition = item.get("content-disposition");
-                        if (contentDisposition == null) {
-                            throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html");
-                        }
-                        StringTokenizer st = new StringTokenizer(contentDisposition, ";");
-                        Map<String, String> disposition = new HashMap<String, String>();
-                        while (st.hasMoreTokens()) {
-                            String token = st.nextToken().trim();
-                            int p = token.indexOf('=');
-                            if (p != -1) {
-                                disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim());
-                            }
-                        }
-                        String pname = disposition.get("name");
-                        pname = pname.substring(1, pname.length() - 1);
+                int[] boundary_idxs = getBoundaryPositions(fbuf, boundary.getBytes());
+                if (boundary_idxs.length < 2) {
+                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings.");
+                }
 
-                        String value = "";
-                        if (item.get("content-type") == null) {
-                            while (mpline != null && !mpline.contains(boundary)) {
-                                mpline = in.readLine();
-                                if (mpline != null) {
-                                    int d = mpline.indexOf(boundary);
-                                    if (d == -1) {
-                                        value += mpline;
-                                    } else {
-                                        value += mpline.substring(0, d - 2);
-                                    }
+                byte[] part_header_buff = new byte[MAX_HEADER_SIZE];
+                for (int bi = 0; bi < boundary_idxs.length - 1; bi++) {
+                    fbuf.position(boundary_idxs[bi]);
+                    int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE;
+                    fbuf.get(part_header_buff, 0, len);
+                    BufferedReader in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(part_header_buff, 0, len), Charset.forName(encoding)), len);
+
+                    int headerLines = 0;
+                    // First line is boundary string
+                    String mpline = in.readLine();
+                    headerLines++;
+                    if (!mpline.contains(boundary)) {
+                        throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary.");
+                    }
+
+                    String part_name = null, file_name = null, content_type = null;
+                    // Parse the reset of the header lines
+                    mpline = in.readLine();
+                    headerLines++;
+                    while (mpline != null && mpline.trim().length() > 0) {
+                        Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline);
+                        if (matcher.matches()) {
+                            String attributeString = matcher.group(2);
+                            matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString);
+                            while (matcher.find()) {
+                                String key = matcher.group(1);
+                                if (key.equalsIgnoreCase("name")) {
+                                    part_name = matcher.group(2);
+                                } else if (key.equalsIgnoreCase("filename")) {
+                                    file_name = matcher.group(2);
                                 }
                             }
-                        } else {
-                            if (boundarycount > bpositions.length) {
-                                throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request");
-                            }
-                            int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]);
-                            String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4);
-                            files.put(pname, path);
-                            value = disposition.get("filename");
-                            value = value.substring(1, value.length() - 1);
-                            do {
-                                mpline = in.readLine();
-                            } while (mpline != null && !mpline.contains(boundary));
                         }
-                        parms.put(pname, value);
+                        matcher = CONTENT_TYPE_PATTERN.matcher(mpline);
+                        if (matcher.matches()) {
+                            content_type = matcher.group(2).trim();
+                        }
+                        mpline = in.readLine();
+                        headerLines++;
+                    }
+                    int part_header_len = 0;
+                    while (headerLines-- > 0) {
+                        part_header_len = scipOverNewLine(part_header_buff, part_header_len);
+                    }
+                    // Read the part data
+                    if (part_header_len >= len - 4) {
+                        throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE.");
+                    }
+                    int part_data_start = boundary_idxs[bi] + part_header_len;
+                    int part_data_end = boundary_idxs[bi + 1] - 4;
+
+                    fbuf.position(part_data_start);
+                    if (content_type == null) {
+                        // Read the part into a string
+                        byte[] data_bytes = new byte[part_data_end - part_data_start];
+                        fbuf.get(data_bytes);
+                        parms.put(part_name, new String(data_bytes, encoding));
+                    } else {
+                        // Read it into a file
+                        String path = saveTmpFile(fbuf, part_data_start, part_data_end - part_data_start, file_name);
+                        if (!files.containsKey(part_name)) {
+                            files.put(part_name, path);
+                        } else {
+                            int count = 2;
+                            while (files.containsKey(part_name + count)) {
+                                count++;
+                            }
+                            files.put(part_name + count, path);
+                        }
+                        parms.put(part_name, file_name);
                     }
                 }
+            } catch (ResponseException re) {
+                throw re;
+            } catch (Exception e) {
+                throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString());
+            }
+        }
+
+        private int scipOverNewLine(byte[] part_header_buff, int index) {
+            while (part_header_buff[index] != '\n') {
+                index++;
+            }
+            return ++index;
+        }
+
+        /**
+         * Decodes parameters in percent-encoded URI-format ( e.g.
+         * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
+         * Map. NOTE: this doesn't support multiple identical keys due to the
+         * simplicity of Map.
+         */
+        private void decodeParms(String parms, Map<String, String> p) {
+            if (parms == null) {
+                this.queryParameterString = "";
+                return;
+            }
+
+            this.queryParameterString = parms;
+            StringTokenizer st = new StringTokenizer(parms, "&");
+            while (st.hasMoreTokens()) {
+                String e = st.nextToken();
+                int sep = e.indexOf('=');
+                if (sep >= 0) {
+                    p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
+                } else {
+                    p.put(decodePercent(e).trim(), "");
+                }
+            }
+        }
+
+        @Override
+        public void execute() throws IOException {
+            Response r = null;
+            try {
+                // Read the first 8192 bytes.
+                // The full header should fit in here.
+                // Apache's default header limit is 8KB.
+                // Do NOT assume that a single read will get the entire header
+                // at once!
+                byte[] buf = new byte[HTTPSession.BUFSIZE];
+                this.splitbyte = 0;
+                this.rlen = 0;
+
+                int read = -1;
+                this.inputStream.mark(HTTPSession.BUFSIZE);
+                try {
+                    read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE);
+                } catch (Exception e) {
+                    safeClose(this.inputStream);
+                    safeClose(this.outputStream);
+                    throw new SocketException("NanoHttpd Shutdown");
+                }
+                if (read == -1) {
+                    // socket was been closed
+                    safeClose(this.inputStream);
+                    safeClose(this.outputStream);
+                    throw new SocketException("NanoHttpd Shutdown");
+                }
+                while (read > 0) {
+                    this.rlen += read;
+                    this.splitbyte = findHeaderEnd(buf, this.rlen);
+                    if (this.splitbyte > 0) {
+                        break;
+                    }
+                    read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
+                }
+
+                if (this.splitbyte < this.rlen) {
+                    this.inputStream.reset();
+                    this.inputStream.skip(this.splitbyte);
+                }
+
+                this.parms = new HashMap<String, String>();
+                if (null == this.headers) {
+                    this.headers = new HashMap<String, String>();
+                } else {
+                    this.headers.clear();
+                }
+
+                // Create a BufferedReader for parsing the header.
+                BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen)));
+
+                // Decode the header into parms and header java properties
+                Map<String, String> pre = new HashMap<String, String>();
+                decodeHeader(hin, pre, this.parms, this.headers);
+
+                if (null != this.remoteIp) {
+                    this.headers.put("remote-addr", this.remoteIp);
+                    this.headers.put("http-client-ip", this.remoteIp);
+                }
+
+                this.method = Method.lookup(pre.get("method"));
+                if (this.method == null) {
+                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
+                }
+
+                this.uri = pre.get("uri");
+
+                this.cookies = new CookieHandler(this.headers);
+
+                String connection = this.headers.get("connection");
+                boolean keepAlive = protocolVersion.equals("HTTP/1.1") && (connection == null || !connection.matches("(?i).*close.*"));
+
+                // Ok, now do the serve()
+
+                // TODO: long body_size = getBodySize();
+                // TODO: long pos_before_serve = this.inputStream.totalRead()
+                // (requires implementaion for totalRead())
+                r = serve(this);
+                // TODO: this.inputStream.skip(body_size -
+                // (this.inputStream.totalRead() - pos_before_serve))
+
+                if (r == null) {
+                    throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
+                } else {
+                    String acceptEncoding = this.headers.get("accept-encoding");
+                    this.cookies.unloadQueue(r);
+                    r.setRequestMethod(this.method);
+                    r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip"));
+                    r.setKeepAlive(keepAlive);
+                    r.send(this.outputStream);
+                }
+                if (!keepAlive || "close".equalsIgnoreCase(r.getHeader("connection"))) {
+                    throw new SocketException("NanoHttpd Shutdown");
+                }
+            } catch (SocketException e) {
+                // throw it out to close socket object (finalAccept)
+                throw e;
+            } catch (SocketTimeoutException ste) {
+                // treat socket timeouts the same way we treat socket exceptions
+                // i.e. close the stream & finalAccept object by throwing the
+                // exception up the call stack.
+                throw ste;
             } catch (IOException ioe) {
-                throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
+                Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
+                resp.send(this.outputStream);
+                safeClose(this.outputStream);
+            } catch (ResponseException re) {
+                Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
+                resp.send(this.outputStream);
+                safeClose(this.outputStream);
+            } finally {
+                safeClose(r);
+                this.tempFileManager.clear();
             }
         }
 
         /**
-         * Find byte index separating header from body. It must be the last byte of the first two sequential new lines.
+         * Find byte index separating header from body. It must be the last byte
+         * of the first two sequential new lines.
          */
         private int findHeaderEnd(final byte[] buf, int rlen) {
             int splitbyte = 0;
-            while (splitbyte + 3 < rlen) {
-                if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
+            while (splitbyte + 1 < rlen) {
+
+                // RFC2616
+                if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && splitbyte + 3 < rlen && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
                     return splitbyte + 4;
                 }
+
+                // tolerance
+                if (buf[splitbyte] == '\n' && buf[splitbyte + 1] == '\n') {
+                    return splitbyte + 2;
+                }
                 splitbyte++;
             }
             return 0;
         }
 
         /**
-         * Find the byte positions where multipart boundaries start.
+         * Find the byte positions where multipart boundaries start. This reads
+         * a large block at a time and uses a temporary buffer to optimize
+         * (memory mapped) file access.
          */
         private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) {
-            int matchcount = 0;
-            int matchbyte = -1;
-            List<Integer> matchbytes = new ArrayList<Integer>();
-            for (int i = 0; i < b.limit(); i++) {
-                if (b.get(i) == boundary[matchcount]) {
-                    if (matchcount == 0)
-                        matchbyte = i;
-                    matchcount++;
-                    if (matchcount == boundary.length) {
-                        matchbytes.add(matchbyte);
-                        matchcount = 0;
-                        matchbyte = -1;
+            int[] res = new int[0];
+            if (b.remaining() < boundary.length) {
+                return res;
+            }
+
+            int search_window_pos = 0;
+            byte[] search_window = new byte[4 * 1024 + boundary.length];
+
+            int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length;
+            b.get(search_window, 0, first_fill);
+            int new_bytes = first_fill - boundary.length;
+
+            do {
+                // Search the search_window
+                for (int j = 0; j < new_bytes; j++) {
+                    for (int i = 0; i < boundary.length; i++) {
+                        if (search_window[j + i] != boundary[i])
+                            break;
+                        if (i == boundary.length - 1) {
+                            // Match found, add it to results
+                            int[] new_res = new int[res.length + 1];
+                            System.arraycopy(res, 0, new_res, 0, res.length);
+                            new_res[res.length] = search_window_pos + j;
+                            res = new_res;
+                        }
                     }
-                } else {
-                    i -= matchcount;
-                    matchcount = 0;
-                    matchbyte = -1;
                 }
+                search_window_pos += new_bytes;
+
+                // Copy the end of the buffer to the start
+                System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length);
+
+                // Refill search_window
+                new_bytes = search_window.length - boundary.length;
+                new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes;
+                b.get(search_window, boundary.length, new_bytes);
+            } while (new_bytes > 0);
+            return res;
+        }
+
+        @Override
+        public CookieHandler getCookies() {
+            return this.cookies;
+        }
+
+        @Override
+        public final Map<String, String> getHeaders() {
+            return this.headers;
+        }
+
+        @Override
+        public final InputStream getInputStream() {
+            return this.inputStream;
+        }
+
+        @Override
+        public final Method getMethod() {
+            return this.method;
+        }
+
+        @Override
+        public final Map<String, String> getParms() {
+            return this.parms;
+        }
+
+        @Override
+        public String getQueryParameterString() {
+            return this.queryParameterString;
+        }
+
+        private RandomAccessFile getTmpBucket() {
+            try {
+                TempFile tempFile = this.tempFileManager.createTempFile(null);
+                return new RandomAccessFile(tempFile.getName(), "rw");
+            } catch (Exception e) {
+                throw new Error(e); // we won't recover, so throw an error
             }
-            int[] ret = new int[matchbytes.size()];
-            for (int i = 0; i < ret.length; i++) {
-                ret[i] = matchbytes.get(i);
-            }
-            return ret;
+        }
+
+        @Override
+        public final String getUri() {
+            return this.uri;
         }
 
         /**
-         * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned.
+         * Deduce body length in bytes. Either from "content-length" header or
+         * read bytes.
          */
-        private String saveTmpFile(ByteBuffer b, int offset, int len) {
+        public long getBodySize() {
+            if (this.headers.containsKey("content-length")) {
+                return Long.parseLong(this.headers.get("content-length"));
+            } else if (this.splitbyte < this.rlen) {
+                return this.rlen - this.splitbyte;
+            }
+            return 0;
+        }
+
+        @Override
+        public void parseBody(Map<String, String> files) throws IOException, ResponseException {
+            RandomAccessFile randomAccessFile = null;
+            try {
+                long size = getBodySize();
+                ByteArrayOutputStream baos = null;
+                DataOutput request_data_output = null;
+
+                // Store the request in memory or a file, depending on size
+                if (size < MEMORY_STORE_LIMIT) {
+                    baos = new ByteArrayOutputStream();
+                    request_data_output = new DataOutputStream(baos);
+                } else {
+                    randomAccessFile = getTmpBucket();
+                    request_data_output = randomAccessFile;
+                }
+
+                // Read all the body and write it to request_data_output
+                byte[] buf = new byte[REQUEST_BUFFER_LEN];
+                while (this.rlen >= 0 && size > 0) {
+                    this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN));
+                    size -= this.rlen;
+                    if (this.rlen > 0) {
+                        request_data_output.write(buf, 0, this.rlen);
+                    }
+                }
+
+                ByteBuffer fbuf = null;
+                if (baos != null) {
+                    fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size());
+                } else {
+                    fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
+                    randomAccessFile.seek(0);
+                }
+
+                // If the method is POST, there may be parameters
+                // in data section, too, read it:
+                if (Method.POST.equals(this.method)) {
+                    String contentType = "";
+                    String contentTypeHeader = this.headers.get("content-type");
+
+                    StringTokenizer st = null;
+                    if (contentTypeHeader != null) {
+                        st = new StringTokenizer(contentTypeHeader, ",; ");
+                        if (st.hasMoreTokens()) {
+                            contentType = st.nextToken();
+                        }
+                    }
+
+                    if ("multipart/form-data".equalsIgnoreCase(contentType)) {
+                        // Handle multipart/form-data
+                        if (!st.hasMoreTokens()) {
+                            throw new ResponseException(Response.Status.BAD_REQUEST,
+                                    "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
+                        }
+                        decodeMultipartFormData(getAttributeFromContentHeader(contentTypeHeader, BOUNDARY_PATTERN, null), //
+                                getAttributeFromContentHeader(contentTypeHeader, CHARSET_PATTERN, "US-ASCII"), fbuf, this.parms, files);
+                    } else {
+                        byte[] postBytes = new byte[fbuf.remaining()];
+                        fbuf.get(postBytes);
+                        String postLine = new String(postBytes).trim();
+                        // Handle application/x-www-form-urlencoded
+                        if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
+                            decodeParms(postLine, this.parms);
+                        } else if (postLine.length() != 0) {
+                            // Special case for raw POST data => create a
+                            // special files entry "postData" with raw content
+                            // data
+                            files.put("postData", postLine);
+                        }
+                    }
+                } else if (Method.PUT.equals(this.method)) {
+                    files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null));
+                }
+            } finally {
+                safeClose(randomAccessFile);
+            }
+        }
+
+        private String getAttributeFromContentHeader(String contentTypeHeader, Pattern pattern, String defaultValue) {
+            Matcher matcher = pattern.matcher(contentTypeHeader);
+            return matcher.find() ? matcher.group(2) : defaultValue;
+        }
+
+        /**
+         * Retrieves the content of a sent file and saves it to a temporary
+         * file. The full path to the saved file is returned.
+         */
+        private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) {
             String path = "";
             if (len > 0) {
                 FileOutputStream fileOutputStream = null;
                 try {
-                    TempFile tempFile = tempFileManager.createTempFile();
+                    TempFile tempFile = this.tempFileManager.createTempFile(filename_hint);
                     ByteBuffer src = b.duplicate();
                     fileOutputStream = new FileOutputStream(tempFile.getName());
                     FileChannel dest = fileOutputStream.getChannel();
@@ -1255,192 +1109,1037 @@
             }
             return path;
         }
-
-        private RandomAccessFile getTmpBucket() {
-            try {
-                TempFile tempFile = tempFileManager.createTempFile();
-                return new RandomAccessFile(tempFile.getName(), "rw");
-            } catch (Exception e) {
-            	throw new Error(e); // we won't recover, so throw an error
-            }
-        }
-
-        /**
-         * It returns the offset separating multipart file headers from the file's data.
-         */
-        private int stripMultipartHeaders(ByteBuffer b, int offset) {
-            int i;
-            for (i = offset; i < b.limit(); i++) {
-                if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') {
-                    break;
-                }
-            }
-            return i + 1;
-        }
-
-        /**
-         * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
-         * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map.
-         */
-        private void decodeParms(String parms, Map<String, String> p) {
-            if (parms == null) {
-                queryParameterString = "";
-                return;
-            }
-
-            queryParameterString = parms;
-            StringTokenizer st = new StringTokenizer(parms, "&");
-            while (st.hasMoreTokens()) {
-                String e = st.nextToken();
-                int sep = e.indexOf('=');
-                if (sep >= 0) {
-                    p.put(decodePercent(e.substring(0, sep)).trim(),
-                        decodePercent(e.substring(sep + 1)));
-                } else {
-                    p.put(decodePercent(e).trim(), "");
-                }
-            }
-        }
-
-        @Override
-        public final Map<String, String> getParms() {
-            return parms;
-        }
-
-		public String getQueryParameterString() {
-            return queryParameterString;
-        }
-
-        @Override
-        public final Map<String, String> getHeaders() {
-            return headers;
-        }
-
-        @Override
-        public final String getUri() {
-            return uri;
-        }
-
-        @Override
-        public final Method getMethod() {
-            return method;
-        }
-
-        @Override
-        public final InputStream getInputStream() {
-            return inputStream;
-        }
-
-        @Override
-        public CookieHandler getCookies() {
-            return cookies;
-        }
     }
 
-    public static class Cookie {
-        private String n, v, e;
+    /**
+     * Handles one session, i.e. parses the HTTP request and returns the
+     * response.
+     */
+    public interface IHTTPSession {
 
-        public Cookie(String name, String value, String expires) {
-            n = name;
-            v = value;
-            e = expires;
-        }
+        void execute() throws IOException;
 
-        public Cookie(String name, String value) {
-            this(name, value, 30);
-        }
+        CookieHandler getCookies();
 
-        public Cookie(String name, String value, int numDays) {
-            n = name;
-            v = value;
-            e = getHTTPTime(numDays);
-        }
+        Map<String, String> getHeaders();
 
-        public String getHTTPHeader() {
-            String fmt = "%s=%s; expires=%s";
-            return String.format(fmt, n, v, e);
-        }
+        InputStream getInputStream();
 
-        public static String getHTTPTime(int days) {
-            Calendar calendar = Calendar.getInstance();
-            SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
-            dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
-            calendar.add(Calendar.DAY_OF_MONTH, days);
-            return dateFormat.format(calendar.getTime());
+        Method getMethod();
+
+        Map<String, String> getParms();
+
+        String getQueryParameterString();
+
+        /**
+         * @return the path part of the URL.
+         */
+        String getUri();
+
+        /**
+         * Adds the files in the request body to the files map.
+         * 
+         * @param files
+         *            map to modify
+         */
+        void parseBody(Map<String, String> files) throws IOException, ResponseException;
+    }
+
+    /**
+     * HTTP Request methods, with the ability to decode a <code>String</code>
+     * back to its enum value.
+     */
+    public enum Method {
+        GET,
+        PUT,
+        POST,
+        DELETE,
+        HEAD,
+        OPTIONS,
+        TRACE,
+        CONNECT,
+        PATCH;
+
+        static Method lookup(String method) {
+            for (Method m : Method.values()) {
+                if (m.toString().equalsIgnoreCase(method)) {
+                    return m;
+                }
+            }
+            return null;
         }
     }
 
     /**
-     * Provides rudimentary support for cookies.
-     * Doesn't support 'path', 'secure' nor 'httpOnly'.
-     * Feel free to improve it and/or add unsupported features.
-     *
-     * @author LordFokas
+     * HTTP response. Return one of these from serve().
      */
-    public class CookieHandler implements Iterable<String> {
-        private HashMap<String, String> cookies = new HashMap<String, String>();
-        private ArrayList<Cookie> queue = new ArrayList<Cookie>();
+    public static class Response implements Closeable {
 
-        public CookieHandler(Map<String, String> httpHeaders) {
-            String raw = httpHeaders.get("cookie");
-            if (raw != null) {
-                String[] tokens = raw.split(";");
-                for (String token : tokens) {
-                    String[] data = token.trim().split("=");
-                    if (data.length == 2) {
-                        cookies.put(data[0], data[1]);
+        public interface IStatus {
+
+            String getDescription();
+
+            int getRequestStatus();
+        }
+
+        /**
+         * Some HTTP response status codes
+         */
+        public enum Status implements IStatus {
+            SWITCH_PROTOCOL(101, "Switching Protocols"),
+            OK(200, "OK"),
+            CREATED(201, "Created"),
+            ACCEPTED(202, "Accepted"),
+            NO_CONTENT(204, "No Content"),
+            PARTIAL_CONTENT(206, "Partial Content"),
+            REDIRECT(301, "Moved Permanently"),
+            TEMPORARY_REDIRECT(302, "Moved Temporarily"),
+            NOT_MODIFIED(304, "Not Modified"),
+            BAD_REQUEST(400, "Bad Request"),
+            UNAUTHORIZED(401, "Unauthorized"),
+            FORBIDDEN(403, "Forbidden"),
+            NOT_FOUND(404, "Not Found"),
+            METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
+            NOT_ACCEPTABLE(406, "Not Acceptable"),
+            REQUEST_TIMEOUT(408, "Request Timeout"),
+            CONFLICT(409, "Conflict"),
+            RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
+            INTERNAL_ERROR(500, "Internal Server Error"),
+            NOT_IMPLEMENTED(501, "Not Implemented"),
+            UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported");
+
+            private final int requestStatus;
+
+            private final String description;
+
+            Status(int requestStatus, String description) {
+                this.requestStatus = requestStatus;
+                this.description = description;
+            }
+
+            @Override
+            public String getDescription() {
+                return "" + this.requestStatus + " " + this.description;
+            }
+
+            @Override
+            public int getRequestStatus() {
+                return this.requestStatus;
+            }
+
+        }
+
+        /**
+         * Output stream that will automatically send every write to the wrapped
+         * OutputStream according to chunked transfer:
+         * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
+         */
+        private static class ChunkedOutputStream extends FilterOutputStream {
+
+            public ChunkedOutputStream(OutputStream out) {
+                super(out);
+            }
+
+            @Override
+            public void write(int b) throws IOException {
+                byte[] data = {
+                    (byte) b
+                };
+                write(data, 0, 1);
+            }
+
+            @Override
+            public void write(byte[] b) throws IOException {
+                write(b, 0, b.length);
+            }
+
+            @Override
+            public void write(byte[] b, int off, int len) throws IOException {
+                if (len == 0)
+                    return;
+                out.write(String.format("%x\r\n", len).getBytes());
+                out.write(b, off, len);
+                out.write("\r\n".getBytes());
+            }
+
+            public void finish() throws IOException {
+                out.write("0\r\n\r\n".getBytes());
+            }
+
+        }
+
+        /**
+         * HTTP status code after processing, e.g. "200 OK", Status.OK
+         */
+        private IStatus status;
+
+        /**
+         * MIME type of content, e.g. "text/html"
+         */
+        private String mimeType;
+
+        /**
+         * Data of the response, may be null.
+         */
+        private InputStream data;
+
+        private long contentLength;
+
+        /**
+         * Headers for the HTTP response. Use addHeader() to add lines.
+         */
+        private final Map<String, String> header = new HashMap<String, String>();
+
+        /**
+         * The request method that spawned this response.
+         */
+        private Method requestMethod;
+
+        /**
+         * Use chunkedTransfer
+         */
+        private boolean chunkedTransfer;
+
+        private boolean encodeAsGzip;
+
+        private boolean keepAlive;
+
+        /**
+         * Creates a fixed length response if totalBytes>=0, otherwise chunked.
+         */
+        protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) {
+            this.status = status;
+            this.mimeType = mimeType;
+            if (data == null) {
+                this.data = new ByteArrayInputStream(new byte[0]);
+                this.contentLength = 0L;
+            } else {
+                this.data = data;
+                this.contentLength = totalBytes;
+            }
+            this.chunkedTransfer = this.contentLength < 0;
+            keepAlive = true;
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (this.data != null) {
+                this.data.close();
+            }
+        }
+
+        /**
+         * Adds given line to the header.
+         */
+        public void addHeader(String name, String value) {
+            this.header.put(name, value);
+        }
+
+        public InputStream getData() {
+            return this.data;
+        }
+
+        public String getHeader(String name) {
+            for (String headerName : header.keySet()) {
+                if (headerName.equalsIgnoreCase(name)) {
+                    return header.get(headerName);
+                }
+            }
+            return null;
+        }
+
+        public String getMimeType() {
+            return this.mimeType;
+        }
+
+        public Method getRequestMethod() {
+            return this.requestMethod;
+        }
+
+        public IStatus getStatus() {
+            return this.status;
+        }
+
+        public void setGzipEncoding(boolean encodeAsGzip) {
+            this.encodeAsGzip = encodeAsGzip;
+        }
+
+        public void setKeepAlive(boolean useKeepAlive) {
+            this.keepAlive = useKeepAlive;
+        }
+
+        private static boolean headerAlreadySent(Map<String, String> header, String name) {
+            boolean alreadySent = false;
+            for (String headerName : header.keySet()) {
+                alreadySent |= headerName.equalsIgnoreCase(name);
+            }
+            return alreadySent;
+        }
+
+        /**
+         * Sends given response to the socket.
+         */
+        protected void send(OutputStream outputStream) {
+            String mime = this.mimeType;
+            SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
+            gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
+
+            try {
+                if (this.status == null) {
+                    throw new Error("sendResponse(): Status can't be null.");
+                }
+                PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false);
+                pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n");
+
+                if (mime != null) {
+                    pw.print("Content-Type: " + mime + "\r\n");
+                }
+
+                if (this.header == null || this.header.get("Date") == null) {
+                    pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
+                }
+
+                if (this.header != null) {
+                    for (String key : this.header.keySet()) {
+                        String value = this.header.get(key);
+                        pw.print(key + ": " + value + "\r\n");
                     }
                 }
+
+                if (!headerAlreadySent(header, "connection")) {
+                    pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "\r\n");
+                }
+
+                if (headerAlreadySent(this.header, "content-length")) {
+                    encodeAsGzip = false;
+                }
+
+                if (encodeAsGzip) {
+                    pw.print("Content-Encoding: gzip\r\n");
+                    setChunkedTransfer(true);
+                }
+
+                long pending = this.data != null ? this.contentLength : 0;
+                if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
+                    pw.print("Transfer-Encoding: chunked\r\n");
+                } else if (!encodeAsGzip) {
+                    pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending);
+                }
+                pw.print("\r\n");
+                pw.flush();
+                sendBodyWithCorrectTransferAndEncoding(outputStream, pending);
+                outputStream.flush();
+                safeClose(this.data);
+            } catch (IOException ioe) {
+                NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe);
+            }
+        }
+
+        private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException {
+            if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
+                ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream);
+                sendBodyWithCorrectEncoding(chunkedOutputStream, -1);
+                chunkedOutputStream.finish();
+            } else {
+                sendBodyWithCorrectEncoding(outputStream, pending);
+            }
+        }
+
+        private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException {
+            if (encodeAsGzip) {
+                GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
+                sendBody(gzipOutputStream, -1);
+                gzipOutputStream.finish();
+            } else {
+                sendBody(outputStream, pending);
+            }
+        }
+
+        /**
+         * Sends the body to the specified OutputStream. The pending parameter
+         * limits the maximum amounts of bytes sent unless it is -1, in which
+         * case everything is sent.
+         * 
+         * @param outputStream
+         *            the OutputStream to send data to
+         * @param pending
+         *            -1 to send everything, otherwise sets a max limit to the
+         *            number of bytes sent
+         * @throws IOException
+         *             if something goes wrong while sending the data.
+         */
+        private void sendBody(OutputStream outputStream, long pending) throws IOException {
+            long BUFFER_SIZE = 16 * 1024;
+            byte[] buff = new byte[(int) BUFFER_SIZE];
+            boolean sendEverything = pending == -1;
+            while (pending > 0 || sendEverything) {
+                long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE);
+                int read = this.data.read(buff, 0, (int) bytesToRead);
+                if (read <= 0) {
+                    break;
+                }
+                outputStream.write(buff, 0, read);
+                if (!sendEverything) {
+                    pending -= read;
+                }
             }
         }
 
-        @Override public Iterator<String> iterator() {
-            return cookies.keySet().iterator();
+        protected static long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, long size) {
+            for (String headerName : header.keySet()) {
+                if (headerName.equalsIgnoreCase("content-length")) {
+                    try {
+                        return Long.parseLong(header.get(headerName));
+                    } catch (NumberFormatException ex) {
+                        return size;
+                    }
+                }
+            }
+
+            pw.print("Content-Length: " + size + "\r\n");
+            return size;
         }
 
-        /**
-         * Read a cookie from the HTTP Headers.
-         *
-         * @param name The cookie's name.
-         * @return The cookie's value if it exists, null otherwise.
-         */
-        public String read(String name) {
-            return cookies.get(name);
+        public void setChunkedTransfer(boolean chunkedTransfer) {
+            this.chunkedTransfer = chunkedTransfer;
         }
 
-        /**
-         * Sets a cookie.
-         *
-         * @param name    The cookie's name.
-         * @param value   The cookie's value.
-         * @param expires How many days until the cookie expires.
-         */
-        public void set(String name, String value, int expires) {
-            queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
+        public void setData(InputStream data) {
+            this.data = data;
         }
 
-        public void set(Cookie cookie) {
-            queue.add(cookie);
+        public void setMimeType(String mimeType) {
+            this.mimeType = mimeType;
         }
 
-        /**
-         * Set a cookie with an expiration date from a month ago, effectively deleting it on the client side.
-         *
-         * @param name The cookie name.
-         */
-        public void delete(String name) {
-            set(name, "-delete-", -30);
+        public void setRequestMethod(Method requestMethod) {
+            this.requestMethod = requestMethod;
         }
 
-        /**
-         * Internally used by the webserver to add all queued cookies into the Response's HTTP Headers.
-         *
-         * @param response The Response object to which headers the queued cookies will be added.
-         */
-        public void unloadQueue(Response response) {
-            for (Cookie cookie : queue) {
-                response.addHeader("Set-Cookie", cookie.getHTTPHeader());
+        public void setStatus(IStatus status) {
+            this.status = status;
+        }
+    }
+
+    public static final class ResponseException extends Exception {
+
+        private static final long serialVersionUID = 6569838532917408380L;
+
+        private final Response.Status status;
+
+        public ResponseException(Response.Status status, String message) {
+            super(message);
+            this.status = status;
+        }
+
+        public ResponseException(Response.Status status, String message, Exception e) {
+            super(message, e);
+            this.status = status;
+        }
+
+        public Response.Status getStatus() {
+            return this.status;
+        }
+    }
+
+    /**
+     * The runnable that will be used for the main listening thread.
+     */
+    public class ServerRunnable implements Runnable {
+
+        private final int timeout;
+
+        private IOException bindException;
+
+        private boolean hasBinded = false;
+
+        private ServerRunnable(int timeout) {
+            this.timeout = timeout;
+        }
+
+        @Override
+        public void run() {
+            try {
+                myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
+                hasBinded = true;
+            } catch (IOException e) {
+                this.bindException = e;
+                return;
+            }
+            do {
+                try {
+                    final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
+                    if (this.timeout > 0) {
+                        finalAccept.setSoTimeout(this.timeout);
+                    }
+                    final InputStream inputStream = finalAccept.getInputStream();
+                    NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream));
+                } catch (IOException e) {
+                    NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
+                }
+            } while (!NanoHTTPD.this.myServerSocket.isClosed());
+        }
+    }
+
+    /**
+     * A temp file.
+     * <p/>
+     * <p>
+     * Temp files are responsible for managing the actual temporary storage and
+     * cleaning themselves up when no longer needed.
+     * </p>
+     */
+    public interface TempFile {
+
+        public void delete() throws Exception;
+
+        public String getName();
+
+        public OutputStream open() throws Exception;
+    }
+
+    /**
+     * Temp file manager.
+     * <p/>
+     * <p>
+     * Temp file managers are created 1-to-1 with incoming requests, to create
+     * and cleanup temporary files created as a result of handling the request.
+     * </p>
+     */
+    public interface TempFileManager {
+
+        void clear();
+
+        public TempFile createTempFile(String filename_hint) throws Exception;
+    }
+
+    /**
+     * Factory to create temp file managers.
+     */
+    public interface TempFileManagerFactory {
+
+        public TempFileManager create();
+    }
+
+    /**
+     * Factory to create ServerSocketFactories.
+     */
+    public interface ServerSocketFactory {
+
+        public ServerSocket create() throws IOException;
+
+    }
+
+    /**
+     * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
+     * This is required as the Keep-Alive HTTP connections would otherwise block
+     * the socket reading thread forever (or as long the browser is open).
+     */
+    public static final int SOCKET_READ_TIMEOUT = 5000;
+
+    /**
+     * Common MIME type for dynamic content: plain text
+     */
+    public static final String MIME_PLAINTEXT = "text/plain";
+
+    /**
+     * Common MIME type for dynamic content: html
+     */
+    public static final String MIME_HTML = "text/html";
+
+    /**
+     * Pseudo-Parameter to use to store the actual query string in the
+     * parameters map for later re-processing.
+     */
+    private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
+
+    /**
+     * logger to log to.
+     */
+    private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName());
+
+    /**
+     * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
+     */
+    protected static Map<String, String> MIME_TYPES;
+
+    public static Map<String, String> mimeTypes() {
+        if (MIME_TYPES == null) {
+            MIME_TYPES = new HashMap<String, String>();
+            loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties");
+            loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties");
+            if (MIME_TYPES.isEmpty()) {
+                LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties");
             }
         }
+        return MIME_TYPES;
+    }
+
+    private static void loadMimeTypes(Map<String, String> result, String resourceName) {
+        try {
+            Enumeration<URL> resources = NanoHTTPD.class.getClassLoader().getResources(resourceName);
+            while (resources.hasMoreElements()) {
+                URL url = (URL) resources.nextElement();
+                Properties properties = new Properties();
+                InputStream stream = null;
+                try {
+                    stream = url.openStream();
+                    properties.load(url.openStream());
+                } catch (IOException e) {
+                    LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e);
+                } finally {
+                    safeClose(stream);
+                }
+                result.putAll((Map) properties);
+            }
+        } catch (IOException e) {
+            LOG.log(Level.INFO, "no mime types available at " + resourceName);
+        }
+    };
+
+    /**
+     * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an
+     * array of loaded KeyManagers. These objects must properly
+     * loaded/initialized by the caller.
+     */
+    public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException {
+        SSLServerSocketFactory res = null;
+        try {
+            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            trustManagerFactory.init(loadedKeyStore);
+            SSLContext ctx = SSLContext.getInstance("TLS");
+            ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null);
+            res = ctx.getServerSocketFactory();
+        } catch (Exception e) {
+            throw new IOException(e.getMessage());
+        }
+        return res;
+    }
+
+    /**
+     * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a
+     * loaded KeyManagerFactory. These objects must properly loaded/initialized
+     * by the caller.
+     */
+    public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException {
+        try {
+            return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers());
+        } catch (Exception e) {
+            throw new IOException(e.getMessage());
+        }
+    }
+
+    /**
+     * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your
+     * certificate and passphrase
+     */
+    public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException {
+        try {
+            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+            InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath);
+            keystore.load(keystoreStream, passphrase);
+            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            keyManagerFactory.init(keystore, passphrase);
+            return makeSSLSocketFactory(keystore, keyManagerFactory);
+        } catch (Exception e) {
+            throw new IOException(e.getMessage());
+        }
+    }
+
+    /**
+     * Get MIME type from file name extension, if possible
+     * 
+     * @param uri
+     *            the string representing a file
+     * @return the connected mime/type
+     */
+    public static String getMimeTypeForFile(String uri) {
+        int dot = uri.lastIndexOf('.');
+        String mime = null;
+        if (dot >= 0) {
+            mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase());
+        }
+        return mime == null ? "application/octet-stream" : mime;
+    }
+
+    private static final void safeClose(Object closeable) {
+        try {
+            if (closeable != null) {
+                if (closeable instanceof Closeable) {
+                    ((Closeable) closeable).close();
+                } else if (closeable instanceof Socket) {
+                    ((Socket) closeable).close();
+                } else if (closeable instanceof ServerSocket) {
+                    ((ServerSocket) closeable).close();
+                } else {
+                    throw new IllegalArgumentException("Unknown object to close");
+                }
+            }
+        } catch (IOException e) {
+            NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e);
+        }
+    }
+
+    private final String hostname;
+
+    private final int myPort;
+
+    private volatile ServerSocket myServerSocket;
+
+    private ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();
+
+    private Thread myThread;
+
+    /**
+     * Pluggable strategy for asynchronously executing requests.
+     */
+    protected AsyncRunner asyncRunner;
+
+    /**
+     * Pluggable strategy for creating and cleaning up temporary files.
+     */
+    private TempFileManagerFactory tempFileManagerFactory;
+
+    /**
+     * Constructs an HTTP server on given port.
+     */
+    public NanoHTTPD(int port) {
+        this(null, port);
+    }
+
+    // -------------------------------------------------------------------------------
+    // //
+    //
+    // Threading Strategy.
+    //
+    // -------------------------------------------------------------------------------
+    // //
+
+    /**
+     * Constructs an HTTP server on given hostname and port.
+     */
+    public NanoHTTPD(String hostname, int port) {
+        this.hostname = hostname;
+        this.myPort = port;
+        setTempFileManagerFactory(new DefaultTempFileManagerFactory());
+        setAsyncRunner(new DefaultAsyncRunner());
+    }
+
+    /**
+     * Forcibly closes all connections that are open.
+     */
+    public synchronized void closeAllConnections() {
+        stop();
+    }
+
+    /**
+     * create a instance of the client handler, subclasses can return a subclass
+     * of the ClientHandler.
+     * 
+     * @param finalAccept
+     *            the socket the cleint is connected to
+     * @param inputStream
+     *            the input stream
+     * @return the client handler
+     */
+    protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) {
+        return new ClientHandler(inputStream, finalAccept);
+    }
+
+    /**
+     * Instantiate the server runnable, can be overwritten by subclasses to
+     * provide a subclass of the ServerRunnable.
+     * 
+     * @param timeout
+     *            the socet timeout to use.
+     * @return the server runnable.
+     */
+    protected ServerRunnable createServerRunnable(final int timeout) {
+        return new ServerRunnable(timeout);
+    }
+
+    /**
+     * Decode parameters from a URL, handing the case where a single parameter
+     * name might have been supplied several times, by return lists of values.
+     * In general these lists will contain a single element.
+     * 
+     * @param parms
+     *            original <b>NanoHTTPD</b> parameters values, as passed to the
+     *            <code>serve()</code> method.
+     * @return a map of <code>String</code> (parameter name) to
+     *         <code>List&lt;String&gt;</code> (a list of the values supplied).
+     */
+    protected static Map<String, List<String>> decodeParameters(Map<String, String> parms) {
+        return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER));
+    }
+
+    // -------------------------------------------------------------------------------
+    // //
+
+    /**
+     * Decode parameters from a URL, handing the case where a single parameter
+     * name might have been supplied several times, by return lists of values.
+     * In general these lists will contain a single element.
+     * 
+     * @param queryString
+     *            a query string pulled from the URL.
+     * @return a map of <code>String</code> (parameter name) to
+     *         <code>List&lt;String&gt;</code> (a list of the values supplied).
+     */
+    protected static Map<String, List<String>> decodeParameters(String queryString) {
+        Map<String, List<String>> parms = new HashMap<String, List<String>>();
+        if (queryString != null) {
+            StringTokenizer st = new StringTokenizer(queryString, "&");
+            while (st.hasMoreTokens()) {
+                String e = st.nextToken();
+                int sep = e.indexOf('=');
+                String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
+                if (!parms.containsKey(propertyName)) {
+                    parms.put(propertyName, new ArrayList<String>());
+                }
+                String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null;
+                if (propertyValue != null) {
+                    parms.get(propertyName).add(propertyValue);
+                }
+            }
+        }
+        return parms;
+    }
+
+    /**
+     * Decode percent encoded <code>String</code> values.
+     * 
+     * @param str
+     *            the percent encoded <code>String</code>
+     * @return expanded form of the input, for example "foo%20bar" becomes
+     *         "foo bar"
+     */
+    protected static String decodePercent(String str) {
+        String decoded = null;
+        try {
+            decoded = URLDecoder.decode(str, "UTF8");
+        } catch (UnsupportedEncodingException ignored) {
+            NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
+        }
+        return decoded;
+    }
+
+    /**
+     * @return true if the gzip compression should be used if the client
+     *         accespts it. Default this option is on for text content and off
+     *         for everything. Override this for custom semantics.
+     */
+    protected boolean useGzipWhenAccepted(Response r) {
+        return r.getMimeType() != null && r.getMimeType().toLowerCase().contains("text/");
+    }
+
+    public final int getListeningPort() {
+        return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort();
+    }
+
+    public final boolean isAlive() {
+        return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive();
+    }
+
+    public ServerSocketFactory getServerSocketFactory() {
+        return serverSocketFactory;
+    }
+
+    public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
+        this.serverSocketFactory = serverSocketFactory;
+    }
+
+    public String getHostname() {
+        return hostname;
+    }
+
+    public TempFileManagerFactory getTempFileManagerFactory() {
+        return tempFileManagerFactory;
+    }
+
+    /**
+     * Call before start() to serve over HTTPS instead of HTTP
+     */
+    public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) {
+        this.serverSocketFactory = new SecureServerSocketFactory(sslServerSocketFactory, sslProtocols);
+    }
+
+    /**
+     * Create a response with unknown length (using HTTP 1.1 chunking).
+     */
+    public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) {
+        return new Response(status, mimeType, data, -1);
+    }
+
+    /**
+     * Create a response with known length.
+     */
+    public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) {
+        return new Response(status, mimeType, data, totalBytes);
+    }
+
+    /**
+     * Create a text response with known length.
+     */
+    public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) {
+        if (txt == null) {
+            return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0);
+        } else {
+            byte[] bytes;
+            try {
+                bytes = txt.getBytes("UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e);
+                bytes = new byte[0];
+            }
+            return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length);
+        }
+    }
+
+    /**
+     * Create a text response with known length.
+     */
+    public static Response newFixedLengthResponse(String msg) {
+        return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg);
+    }
+
+    /**
+     * Override this to customize the server.
+     * <p/>
+     * <p/>
+     * (By default, this returns a 404 "Not Found" plain text error response.)
+     * 
+     * @param session
+     *            The HTTP session
+     * @return HTTP response, see class Response for details
+     */
+    public Response serve(IHTTPSession session) {
+        Map<String, String> files = new HashMap<String, String>();
+        Method method = session.getMethod();
+        if (Method.PUT.equals(method) || Method.POST.equals(method)) {
+            try {
+                session.parseBody(files);
+            } catch (IOException ioe) {
+                return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
+            } catch (ResponseException re) {
+                return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
+            }
+        }
+
+        Map<String, String> parms = session.getParms();
+        parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString());
+        return serve(session.getUri(), method, session.getHeaders(), parms, files);
+    }
+
+    /**
+     * Override this to customize the server.
+     * <p/>
+     * <p/>
+     * (By default, this returns a 404 "Not Found" plain text error response.)
+     * 
+     * @param uri
+     *            Percent-decoded URI without parameters, for example
+     *            "/index.cgi"
+     * @param method
+     *            "GET", "POST" etc.
+     * @param parms
+     *            Parsed, percent decoded parameters from URI and, in case of
+     *            POST, data.
+     * @param headers
+     *            Header entries, percent decoded
+     * @return HTTP response, see class Response for details
+     */
+    @Deprecated
+    public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
+        return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
+    }
+
+    /**
+     * Pluggable strategy for asynchronously executing requests.
+     * 
+     * @param asyncRunner
+     *            new strategy for handling threads.
+     */
+    public void setAsyncRunner(AsyncRunner asyncRunner) {
+        this.asyncRunner = asyncRunner;
+    }
+
+    /**
+     * Pluggable strategy for creating and cleaning up temporary files.
+     * 
+     * @param tempFileManagerFactory
+     *            new strategy for handling temp files.
+     */
+    public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
+        this.tempFileManagerFactory = tempFileManagerFactory;
+    }
+
+    /**
+     * Start the server.
+     * 
+     * @throws IOException
+     *             if the socket is in use.
+     */
+    public void start() throws IOException {
+        start(NanoHTTPD.SOCKET_READ_TIMEOUT);
+    }
+
+    /**
+     * Starts the server (in setDaemon(true) mode).
+     */
+    public void start(final int timeout) throws IOException {
+        start(timeout, true);
+    }
+
+    /**
+     * Start the server.
+     * 
+     * @param timeout
+     *            timeout to use for socket connections.
+     * @param daemon
+     *            start the thread daemon or not.
+     * @throws IOException
+     *             if the socket is in use.
+     */
+    public void start(final int timeout, boolean daemon) throws IOException {
+        this.myServerSocket = this.getServerSocketFactory().create();
+        this.myServerSocket.setReuseAddress(true);
+
+        ServerRunnable serverRunnable = createServerRunnable(timeout);
+        this.myThread = new Thread(serverRunnable);
+        this.myThread.setDaemon(daemon);
+        this.myThread.setName("NanoHttpd Main Listener");
+        this.myThread.start();
+        while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
+            try {
+                Thread.sleep(10L);
+            } catch (Throwable e) {
+                // on android this may not be allowed, that's why we
+                // catch throwable the wait should be very short because we are
+                // just waiting for the bind of the socket
+            }
+        }
+        if (serverRunnable.bindException != null) {
+            throw serverRunnable.bindException;
+        }
+    }
+
+    /**
+     * Stop the server.
+     */
+    public void stop() {
+        try {
+            safeClose(this.myServerSocket);
+            this.asyncRunner.closeAll();
+            if (this.myThread != null) {
+                this.myThread.join();
+            }
+        } catch (Exception e) {
+            NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e);
+        }
+    }
+
+    public final boolean wasStarted() {
+        return this.myServerSocket != null && this.myThread != null;
     }
 }
diff --git a/core/src/main/java/fi/iki/elonen/util/ServerRunner.java b/core/src/main/java/fi/iki/elonen/util/ServerRunner.java
new file mode 100644
index 0000000..e0aa3db
--- /dev/null
+++ b/core/src/main/java/fi/iki/elonen/util/ServerRunner.java
@@ -0,0 +1,75 @@
+package fi.iki.elonen.util;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import fi.iki.elonen.NanoHTTPD;
+
+public class ServerRunner {
+
+    /**
+     * logger to log to.
+     */
+    private static final Logger LOG = Logger.getLogger(ServerRunner.class.getName());
+
+    public static void executeInstance(NanoHTTPD server) {
+        try {
+            server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
+        } catch (IOException ioe) {
+            System.err.println("Couldn't start server:\n" + ioe);
+            System.exit(-1);
+        }
+
+        System.out.println("Server started, Hit Enter to stop.\n");
+
+        try {
+            System.in.read();
+        } catch (Throwable ignored) {
+        }
+
+        server.stop();
+        System.out.println("Server stopped.\n");
+    }
+
+    public static <T extends NanoHTTPD> void run(Class<T> serverClass) {
+        try {
+            executeInstance(serverClass.newInstance());
+        } catch (Exception e) {
+            ServerRunner.LOG.log(Level.SEVERE, "Cound nor create server", e);
+        }
+    }
+}
diff --git a/core/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties b/core/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties
new file mode 100644
index 0000000..3fb242f
--- /dev/null
+++ b/core/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties
@@ -0,0 +1,30 @@
+#default mime types for nanohttpd, use META-INF/mimetypes.properties for user defined mimetypes
+css=text/css
+htm=text/html
+html=text/html
+xml=text/xml
+java=text/x-java-source, text/java
+md=text/plain
+txt=text/plain
+asc=text/plain
+gif=image/gif
+jpg=image/jpeg
+jpeg=image/jpeg
+png=image/png
+svg=image/svg+xml
+mp3=audio/mpeg
+m3u=audio/mpeg-url
+mp4=video/mp4
+ogv=video/ogg
+flv=video/x-flv
+mov=video/quicktime
+swf=application/x-shockwave-flash
+js=application/javascript
+pdf=application/pdf
+doc=application/msword
+ogg=application/x-ogg
+zip=application/octet-stream
+exe=application/octet-stream
+class=application/octet-stream
+m3u8=application/vnd.apple.mpegurl
+ts=video/mp2t
\ No newline at end of file
diff --git a/core/src/main/resources/META-INF/nanohttpd/mimetypes.properties b/core/src/main/resources/META-INF/nanohttpd/mimetypes.properties
new file mode 100644
index 0000000..7166a88
--- /dev/null
+++ b/core/src/main/resources/META-INF/nanohttpd/mimetypes.properties
@@ -0,0 +1 @@
+#mime types for nanohttpd, use a file like this for user defined mimetypes
\ No newline at end of file
diff --git a/core/src/site/site.xml b/core/src/site/site.xml
new file mode 100644
index 0000000..4270945
--- /dev/null
+++ b/core/src/site/site.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="${project.name}">
+	<skin>
+		<groupId>org.apache.maven.skins</groupId>
+		<artifactId>maven-fluido-skin</artifactId>
+		<version>1.3.0</version>
+	</skin>
+	<bannerLeft>
+		<src>../images/nanohttpd_logo.png</src>
+	</bannerLeft>
+	<bannerRight>
+		<src>../images/nanohttpd_logo_text.png</src>
+	</bannerRight>
+	<publishDate position="left" format="yyyy-MM-dd" />
+	<version position="right" />
+	<poweredBy>
+		<logo name="Maven" href="http://maven.apache.org/"
+			img="http://maven.apache.org/images/logos/maven-feather.png" />
+	</poweredBy>
+	<custom>
+		<fluidoSkin>
+			<topBarEnabled>false</topBarEnabled>
+			<sideBarEnabled>true</sideBarEnabled>
+			<gitHub>
+				<projectId>Nanohttpd/nanohttpd</projectId>
+				<ribbonOrientation>right</ribbonOrientation>
+				<ribbonColor>black</ribbonColor>
+			</gitHub>
+		</fluidoSkin>
+	</custom>
+	<body>
+		<breadcrumbs>
+			<item name="${project.name}" href="index.html" />
+		</breadcrumbs>
+		<menu name="Documentation">
+			<item name="About" href="index.html" />
+		</menu>
+		<menu ref="modules" />
+		<menu ref="reports" />
+	</body>
+</project>
\ No newline at end of file
diff --git a/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java b/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java
index c3fb1f0..8853efa 100644
--- a/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java
@@ -1,46 +1,50 @@
 package fi.iki.elonen;
 
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.PipedInputStream;
 
-import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;
-
 public class HttpChunkedResponseTest extends HttpServerTest {
-    @org.junit.Test
-    public void thatChunkedContentIsChunked() throws Exception {
-        PipedInputStream pipedInputStream = new ChunkedInputStream(new String[]{
-                "some",
-                "thing which is longer than sixteen characters",
-                "whee!",
-                ""
-        });
-        String[] expected = {
-                "HTTP/1.1 200 OK",
-                "Content-Type: what/ever",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Transfer-Encoding: chunked",
-                "",
-                "4",
-                "some",
-                "2d",
-                "thing which is longer than sixteen characters",
-                "5",
-                "whee!",
-                "0",
-                ""
-        };
-        testServer.response = new NanoHTTPD.Response(OK, "what/ever", pipedInputStream);
-        testServer.response.setChunkedTransfer(true);
-
-        ByteArrayOutputStream byteArrayOutputStream = invokeServer("GET / HTTP/1.0");
-
-        assertResponse(byteArrayOutputStream, expected);
-    }
 
     private static class ChunkedInputStream extends PipedInputStream {
+
         int chunk = 0;
+
         String[] chunks;
 
         private ChunkedInputStream(String[] chunks) {
@@ -48,12 +52,45 @@
         }
 
         @Override
-        public synchronized int read(byte[] buffer) throws IOException {
+        public synchronized int read(byte[] buffer, int off, int len) throws IOException {
             // Too implementation-linked, but...
-            for (int i = 0; i < chunks[chunk].length(); ++i) {
-                buffer[i] = (byte) chunks[chunk].charAt(i);
+            for (int i = 0; i < this.chunks[this.chunk].length(); ++i) {
+                buffer[i] = (byte) this.chunks[this.chunk].charAt(i);
             }
-            return chunks[chunk++].length();
+            return this.chunks[this.chunk++].length();
         }
     }
+
+    @org.junit.Test
+    public void thatChunkedContentIsChunked() throws Exception {
+        PipedInputStream pipedInputStream = new ChunkedInputStream(new String[]{
+            "some",
+            "thing which is longer than sixteen characters",
+            "whee!",
+            ""
+        });
+        String[] expected = {
+            "HTTP/1.1 200 OK",
+            "Content-Type: what/ever",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Transfer-Encoding: chunked",
+            "",
+            "4",
+            "some",
+            "2d",
+            "thing which is longer than sixteen characters",
+            "5",
+            "whee!",
+            "0",
+            ""
+        };
+        this.testServer.response = new NanoHTTPD(0) {
+        }.newChunkedResponse(OK, "what/ever", pipedInputStream);
+        this.testServer.response.setChunkedTransfer(true);
+
+        ByteArrayOutputStream byteArrayOutputStream = invokeServer("GET / HTTP/1.1");
+
+        assertResponse(byteArrayOutputStream, expected);
+    }
 }
diff --git a/core/src/test/java/fi/iki/elonen/HttpDeleteRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpDeleteRequestTest.java
index 517ad29..8ce49e1 100644
--- a/core/src/test/java/fi/iki/elonen/HttpDeleteRequestTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpDeleteRequestTest.java
@@ -1,46 +1,58 @@
 package fi.iki.elonen;
 
-import org.junit.Test;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
 
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
-import java.util.List;
 
-import static junit.framework.Assert.*;
+import org.junit.Test;
 
 public class HttpDeleteRequestTest extends HttpServerTest {
 
     @Test
     public void testDeleteRequestThatDoesntSendBackResponseBody_EmptyString() throws Exception {
-        testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, "");
+        this.testServer.response = NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, "");
 
-        ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1");
+        ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1");
 
         String[] expected = {
-                "HTTP/1.1 204 No Content",
-                "Content-Type: text/html",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 0",
-                ""
-        };
-
-        assertResponse(outputStream, expected);
-    }
-
-    @Test
-    public void testDeleteRequestThatDoesntSendBackResponseBody_NullString() throws Exception {
-        testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, (String)null);
-
-        ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1");
-
-        String[] expected = {
-                "HTTP/1.1 204 No Content",
-                "Content-Type: text/html",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 0",
-                ""
+            "HTTP/1.1 204 No Content",
+            "Content-Type: text/html",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 0",
+            ""
         };
 
         assertResponse(outputStream, expected);
@@ -48,36 +60,35 @@
 
     @Test
     public void testDeleteRequestThatDoesntSendBackResponseBody_NullInputStream() throws Exception {
-        testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, (InputStream)null);
+        this.testServer.response = NanoHTTPD.newChunkedResponse(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, (InputStream) null);
 
-        ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1");
+        ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1");
 
         String[] expected = {
-                "HTTP/1.1 204 No Content",
-                "Content-Type: text/html",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 0",
-                ""
+            "HTTP/1.1 204 No Content",
+            "Content-Type: text/html",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 0",
+            ""
         };
 
         assertResponse(outputStream, expected);
     }
 
     @Test
-    public void testDeleteRequestThatSendsBackResponseBody_Success() throws Exception {
-        testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "application/xml", "<body />");
+    public void testDeleteRequestThatDoesntSendBackResponseBody_NullString() throws Exception {
+        this.testServer.response = NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.NO_CONTENT, NanoHTTPD.MIME_HTML, (String) null);
 
-        ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1");
+        ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1");
 
         String[] expected = {
-                "HTTP/1.1 200 OK",
-                "Content-Type: application/xml",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 8",
-                "",
-                "<body />"
+            "HTTP/1.1 204 No Content",
+            "Content-Type: text/html",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 0",
+            ""
         };
 
         assertResponse(outputStream, expected);
@@ -85,18 +96,37 @@
 
     @Test
     public void testDeleteRequestThatSendsBackResponseBody_Accepted() throws Exception {
-        testServer.response = new NanoHTTPD.Response(NanoHTTPD.Response.Status.ACCEPTED, "application/xml", "<body />");
+        this.testServer.response = NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.ACCEPTED, "application/xml", "<body />");
 
-        ByteArrayOutputStream outputStream = invokeServer("DELETE " + URI + " HTTP/1.1");
+        ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1");
 
         String[] expected = {
-                "HTTP/1.1 202 Accepted",
-                "Content-Type: application/xml",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 8",
-                "",
-                "<body />"
+            "HTTP/1.1 202 Accepted",
+            "Content-Type: application/xml",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 8",
+            "",
+            "<body />"
+        };
+
+        assertResponse(outputStream, expected);
+    }
+
+    @Test
+    public void testDeleteRequestThatSendsBackResponseBody_Success() throws Exception {
+        this.testServer.response = NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/xml", "<body />");
+
+        ByteArrayOutputStream outputStream = invokeServer("DELETE " + HttpServerTest.URI + " HTTP/1.1");
+
+        String[] expected = {
+            "HTTP/1.1 200 OK",
+            "Content-Type: application/xml",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 8",
+            "",
+            "<body />"
         };
 
         assertResponse(outputStream, expected);
diff --git a/core/src/test/java/fi/iki/elonen/HttpGetRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpGetRequestTest.java
index 598e50b..e1a6b8b 100644
--- a/core/src/test/java/fi/iki/elonen/HttpGetRequestTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpGetRequestTest.java
@@ -1,171 +1,208 @@
 package fi.iki.elonen;
 
-import org.junit.Test;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
 
 import java.io.ByteArrayOutputStream;
 import java.util.List;
 
-import static junit.framework.Assert.*;
+import org.junit.Test;
 
 public class HttpGetRequestTest extends HttpServerTest {
 
     @Test
-    public void testFullyQualifiedWorkingGetRequest() throws Exception {
-        ByteArrayOutputStream outputStream = invokeServer("GET " + URI + " HTTP/1.1");
-
-        String[] expected = {
-                "HTTP/1.1 200 OK",
-                "Content-Type: text/html",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 0",
-                ""
-        };
-
-        assertResponse(outputStream, expected);
+    public void testDecodingFieldWithEmptyValueAndFieldWithMissingValueGiveDifferentResults() {
+        invokeServer("GET " + HttpServerTest.URI + "?foo&bar= HTTP/1.1");
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(0, this.testServer.decodedParamters.get("foo").size());
+        assertTrue(this.testServer.decodedParamters.get("bar") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("bar").size());
+        assertEquals("", this.testServer.decodedParamters.get("bar").get(0));
     }
 
     @Test
-    public void testOutputOfServeSentBackToClient() throws Exception {
-        String responseBody = "Success!";
-        testServer.response = new NanoHTTPD.Response(responseBody);
-        ByteArrayOutputStream outputStream = invokeServer("GET " + URI + " HTTP/1.1");
+    public void testDecodingMixtureOfParameters() {
+        invokeServer("GET " + HttpServerTest.URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1");
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(2, this.testServer.decodedParamters.get("foo").size());
+        assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0));
+        assertEquals("baz", this.testServer.decodedParamters.get("foo").get(1));
+        assertTrue(this.testServer.decodedParamters.get("zot") instanceof List);
+        assertEquals(0, this.testServer.decodedParamters.get("zot").size());
+        assertTrue(this.testServer.decodedParamters.get("zim") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("zim").size());
+        assertEquals("", this.testServer.decodedParamters.get("zim").get(0));
+    }
 
-        String[] expected = {
-                "HTTP/1.1 200 OK",
-                "Content-Type: text/html",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 8",
-                "",
-                responseBody
-        };
+    @Test
+    public void testDecodingParametersFromParameterMap() {
+        invokeServer("GET " + HttpServerTest.URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1");
+        assertEquals(this.testServer.decodedParamters, this.testServer.decodedParamtersFromParameter);
+    }
 
-        assertResponse(outputStream, expected);
+    // --------------------------------------------------------------------------------------------------------
+    // //
+
+    @Test
+    public void testDecodingParametersWithSingleValue() {
+        invokeServer("GET " + HttpServerTest.URI + "?foo=bar&baz=zot HTTP/1.1");
+        assertEquals("foo=bar&baz=zot", this.testServer.queryParameterString);
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("foo").size());
+        assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0));
+        assertTrue(this.testServer.decodedParamters.get("baz") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("baz").size());
+        assertEquals("zot", this.testServer.decodedParamters.get("baz").get(0));
+    }
+
+    @Test
+    public void testDecodingParametersWithSingleValueAndMissingValue() {
+        invokeServer("GET " + HttpServerTest.URI + "?foo&baz=zot HTTP/1.1");
+        assertEquals("foo&baz=zot", this.testServer.queryParameterString);
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(0, this.testServer.decodedParamters.get("foo").size());
+        assertTrue(this.testServer.decodedParamters.get("baz") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("baz").size());
+        assertEquals("zot", this.testServer.decodedParamters.get("baz").get(0));
+    }
+
+    @Test
+    public void testDecodingSingleFieldRepeated() {
+        invokeServer("GET " + HttpServerTest.URI + "?foo=bar&foo=baz HTTP/1.1");
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(2, this.testServer.decodedParamters.get("foo").size());
+        assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0));
+        assertEquals("baz", this.testServer.decodedParamters.get("foo").get(1));
     }
 
     @Test
     public void testEmptyHeadersSuppliedToServeMethodFromSimpleWorkingGetRequest() {
-        invokeServer("GET " + URI + " HTTP/1.1");
-        assertNotNull(testServer.parms);
-        assertNotNull(testServer.header);
-        assertNotNull(testServer.files);
-        assertNotNull(testServer.uri);
+        invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1");
+        assertNotNull(this.testServer.parms);
+        assertNotNull(this.testServer.header);
+        assertNotNull(this.testServer.files);
+        assertNotNull(this.testServer.uri);
     }
 
     @Test
-    public void testSingleUserAgentHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() {
-        String userAgent = "jUnit 4.8.2 Unit Test";
-        invokeServer("GET " + URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\n");
-        assertEquals(userAgent, testServer.header.get("user-agent"));
-        assertEquals(NanoHTTPD.Method.GET, testServer.method);
-        assertEquals(URI, testServer.uri);
+    public void testFullyQualifiedWorkingGetRequest() throws Exception {
+        ByteArrayOutputStream outputStream = invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1");
+
+        String[] expected = {
+            "HTTP/1.1 200 OK",
+            "Content-Type: text/html",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 0",
+            ""
+        };
+
+        assertResponse(outputStream, expected);
+    }
+
+    @Test
+    public void testMultipleGetParameters() {
+        invokeServer("GET " + HttpServerTest.URI + "?foo=bar&baz=zot HTTP/1.1");
+        assertEquals("bar", this.testServer.parms.get("foo"));
+        assertEquals("zot", this.testServer.parms.get("baz"));
+    }
+
+    @Test
+    public void testMultipleGetParametersWithMissingValue() {
+        invokeServer("GET " + HttpServerTest.URI + "?foo=&baz=zot HTTP/1.1");
+        assertEquals("", this.testServer.parms.get("foo"));
+        assertEquals("zot", this.testServer.parms.get("baz"));
+    }
+
+    @Test
+    public void testMultipleGetParametersWithMissingValueAndRequestHeaders() {
+        invokeServer("GET " + HttpServerTest.URI + "?foo=&baz=zot HTTP/1.1\nAccept: text/html");
+        assertEquals("", this.testServer.parms.get("foo"));
+        assertEquals("zot", this.testServer.parms.get("baz"));
+        assertEquals("text/html", this.testServer.header.get("accept"));
     }
 
     @Test
     public void testMultipleHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() {
         String userAgent = "jUnit 4.8.2 Unit Test";
         String accept = "text/html";
-        invokeServer("GET " + URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\nAccept: " + accept);
-        assertEquals(userAgent, testServer.header.get("user-agent"));
-        assertEquals(accept, testServer.header.get("accept"));
+        invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\nAccept: " + accept);
+        assertEquals(userAgent, this.testServer.header.get("user-agent"));
+        assertEquals(accept, this.testServer.header.get("accept"));
+    }
+
+    @Test
+    public void testOutputOfServeSentBackToClient() throws Exception {
+        String responseBody = "Success!";
+        this.testServer.response = NanoHTTPD.newFixedLengthResponse(responseBody);
+        ByteArrayOutputStream outputStream = invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1");
+
+        String[] expected = {
+            "HTTP/1.1 200 OK",
+            "Content-Type: text/html",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 8",
+            "",
+            responseBody
+        };
+
+        assertResponse(outputStream, expected);
     }
 
     @Test
     public void testSingleGetParameter() {
-        invokeServer("GET " + URI + "?foo=bar HTTP/1.1");
-        assertEquals("bar", testServer.parms.get("foo"));
+        invokeServer("GET " + HttpServerTest.URI + "?foo=bar HTTP/1.1");
+        assertEquals("bar", this.testServer.parms.get("foo"));
     }
 
     @Test
     public void testSingleGetParameterWithNoValue() {
-        invokeServer("GET " + URI + "?foo HTTP/1.1");
-        assertEquals("", testServer.parms.get("foo"));
+        invokeServer("GET " + HttpServerTest.URI + "?foo HTTP/1.1");
+        assertEquals("", this.testServer.parms.get("foo"));
     }
 
     @Test
-    public void testMultipleGetParameters() {
-        invokeServer("GET " + URI + "?foo=bar&baz=zot HTTP/1.1");
-        assertEquals("bar", testServer.parms.get("foo"));
-        assertEquals("zot", testServer.parms.get("baz"));
+    public void testSingleUserAgentHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() {
+        String userAgent = "jUnit 4.8.2 Unit Test";
+        invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\n");
+        assertEquals(userAgent, this.testServer.header.get("user-agent"));
+        assertEquals(NanoHTTPD.Method.GET, this.testServer.method);
+        assertEquals(HttpServerTest.URI, this.testServer.uri);
     }
 
-    @Test
-    public void testMultipleGetParametersWithMissingValue() {
-        invokeServer("GET " + URI + "?foo=&baz=zot HTTP/1.1");
-        assertEquals("", testServer.parms.get("foo"));
-        assertEquals("zot", testServer.parms.get("baz"));
-    }
-
-    @Test
-    public void testMultipleGetParametersWithMissingValueAndRequestHeaders() {
-        invokeServer("GET " + URI + "?foo=&baz=zot HTTP/1.1\nAccept: text/html");
-        assertEquals("", testServer.parms.get("foo"));
-        assertEquals("zot", testServer.parms.get("baz"));
-        assertEquals("text/html", testServer.header.get("accept"));
-    }
-
-    @Test
-    public void testDecodingParametersWithSingleValue() {
-        invokeServer("GET " + URI + "?foo=bar&baz=zot HTTP/1.1");
-        assertEquals("foo=bar&baz=zot", testServer.queryParameterString);
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("foo").size());
-        assertEquals("bar", testServer.decodedParamters.get("foo").get(0));
-        assertTrue(testServer.decodedParamters.get("baz") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("baz").size());
-        assertEquals("zot", testServer.decodedParamters.get("baz").get(0));
-    }
-
-    @Test
-    public void testDecodingParametersWithSingleValueAndMissingValue() {
-        invokeServer("GET " + URI + "?foo&baz=zot HTTP/1.1");
-        assertEquals("foo&baz=zot", testServer.queryParameterString);
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(0, testServer.decodedParamters.get("foo").size());
-        assertTrue(testServer.decodedParamters.get("baz") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("baz").size());
-        assertEquals("zot", testServer.decodedParamters.get("baz").get(0));
-    }
-
-    @Test
-    public void testDecodingFieldWithEmptyValueAndFieldWithMissingValueGiveDifferentResults() {
-        invokeServer("GET " + URI + "?foo&bar= HTTP/1.1");
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(0, testServer.decodedParamters.get("foo").size());
-        assertTrue(testServer.decodedParamters.get("bar") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("bar").size());
-        assertEquals("", testServer.decodedParamters.get("bar").get(0));
-    }
-
-    @Test
-    public void testDecodingSingleFieldRepeated() {
-        invokeServer("GET " + URI + "?foo=bar&foo=baz HTTP/1.1");
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(2, testServer.decodedParamters.get("foo").size());
-        assertEquals("bar", testServer.decodedParamters.get("foo").get(0));
-        assertEquals("baz", testServer.decodedParamters.get("foo").get(1));
-    }
-
-    @Test
-    public void testDecodingMixtureOfParameters() {
-        invokeServer("GET " + URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1");
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(2, testServer.decodedParamters.get("foo").size());
-        assertEquals("bar", testServer.decodedParamters.get("foo").get(0));
-        assertEquals("baz", testServer.decodedParamters.get("foo").get(1));
-        assertTrue(testServer.decodedParamters.get("zot") instanceof List);
-        assertEquals(0, testServer.decodedParamters.get("zot").size());
-        assertTrue(testServer.decodedParamters.get("zim") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("zim").size());
-        assertEquals("", testServer.decodedParamters.get("zim").get(0));
-    }
-
-    @Test
-    public void testDecodingParametersFromParameterMap() {
-        invokeServer("GET " + URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1");
-        assertEquals(testServer.decodedParamters, testServer.decodedParamtersFromParameter);
-    }
-    // -------------------------------------------------------------------------------------------------------- //
-
 }
diff --git a/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java
index 1c5901f..9b5983e 100644
--- a/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java
@@ -1,158 +1,196 @@
 package fi.iki.elonen;
 
-import org.junit.Test;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
 
 import java.io.ByteArrayOutputStream;
 import java.util.List;
 
-import static junit.framework.Assert.*;
+import org.junit.Test;
 
 public class HttpHeadRequestTest extends HttpServerTest {
+
     @Override
-    public void setUp() {
+    public void setUp() throws Exception {
         super.setUp();
         String responseBody = "Success!";
-        testServer.response = new NanoHTTPD.Response(responseBody);
+        this.testServer.response = NanoHTTPD.newFixedLengthResponse(responseBody);
+    }
+
+    @Test
+    public void testDecodingFieldWithEmptyValueAndFieldWithMissingValueGiveDifferentResults() {
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo&bar= HTTP/1.1");
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(0, this.testServer.decodedParamters.get("foo").size());
+        assertTrue(this.testServer.decodedParamters.get("bar") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("bar").size());
+        assertEquals("", this.testServer.decodedParamters.get("bar").get(0));
+    }
+
+    @Test
+    public void testDecodingMixtureOfParameters() {
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1");
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(2, this.testServer.decodedParamters.get("foo").size());
+        assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0));
+        assertEquals("baz", this.testServer.decodedParamters.get("foo").get(1));
+        assertTrue(this.testServer.decodedParamters.get("zot") instanceof List);
+        assertEquals(0, this.testServer.decodedParamters.get("zot").size());
+        assertTrue(this.testServer.decodedParamters.get("zim") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("zim").size());
+        assertEquals("", this.testServer.decodedParamters.get("zim").get(0));
+    }
+
+    @Test
+    public void testDecodingParametersFromParameterMap() {
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1");
+        assertEquals(this.testServer.decodedParamters, this.testServer.decodedParamtersFromParameter);
+    }
+
+    // --------------------------------------------------------------------------------------------------------
+    // //
+
+    @Test
+    public void testDecodingParametersWithSingleValue() {
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&baz=zot HTTP/1.1");
+        assertEquals("foo=bar&baz=zot", this.testServer.queryParameterString);
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("foo").size());
+        assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0));
+        assertTrue(this.testServer.decodedParamters.get("baz") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("baz").size());
+        assertEquals("zot", this.testServer.decodedParamters.get("baz").get(0));
+    }
+
+    @Test
+    public void testDecodingParametersWithSingleValueAndMissingValue() {
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo&baz=zot HTTP/1.1");
+        assertEquals("foo&baz=zot", this.testServer.queryParameterString);
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(0, this.testServer.decodedParamters.get("foo").size());
+        assertTrue(this.testServer.decodedParamters.get("baz") instanceof List);
+        assertEquals(1, this.testServer.decodedParamters.get("baz").size());
+        assertEquals("zot", this.testServer.decodedParamters.get("baz").get(0));
+    }
+
+    @Test
+    public void testDecodingSingleFieldRepeated() {
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&foo=baz HTTP/1.1");
+        assertTrue(this.testServer.decodedParamters.get("foo") instanceof List);
+        assertEquals(2, this.testServer.decodedParamters.get("foo").size());
+        assertEquals("bar", this.testServer.decodedParamters.get("foo").get(0));
+        assertEquals("baz", this.testServer.decodedParamters.get("foo").get(1));
+    }
+
+    @Test
+    public void testEmptyHeadersSuppliedToServeMethodFromSimpleWorkingGetRequest() {
+        invokeServer("HEAD " + HttpServerTest.URI + " HTTP/1.1");
+        assertNotNull(this.testServer.parms);
+        assertNotNull(this.testServer.header);
+        assertNotNull(this.testServer.files);
+        assertNotNull(this.testServer.uri);
     }
 
     @Test
     public void testHeadRequestDoesntSendBackResponseBody() throws Exception {
-        ByteArrayOutputStream outputStream = invokeServer("HEAD " + URI + " HTTP/1.1");
+        ByteArrayOutputStream outputStream = invokeServer("HEAD " + HttpServerTest.URI + " HTTP/1.1");
 
         String[] expected = {
-                "HTTP/1.1 200 OK",
-                "Content-Type: text/html",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 8",
-                ""
+            "HTTP/1.1 200 OK",
+            "Content-Type: text/html",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 8",
+            ""
         };
 
         assertResponse(outputStream, expected);
     }
 
     @Test
-    public void testEmptyHeadersSuppliedToServeMethodFromSimpleWorkingGetRequest() {
-        invokeServer("HEAD " + URI + " HTTP/1.1");
-        assertNotNull(testServer.parms);
-        assertNotNull(testServer.header);
-        assertNotNull(testServer.files);
-        assertNotNull(testServer.uri);
+    public void testMultipleGetParameters() {
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar&baz=zot HTTP/1.1");
+        assertEquals("bar", this.testServer.parms.get("foo"));
+        assertEquals("zot", this.testServer.parms.get("baz"));
     }
 
     @Test
-    public void testSingleUserAgentHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() {
-        String userAgent = "jUnit 4.8.2 Unit Test";
-        invokeServer("HEAD " + URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\n");
-        assertEquals(userAgent, testServer.header.get("user-agent"));
-        assertEquals(NanoHTTPD.Method.HEAD, testServer.method);
-        assertEquals(URI, testServer.uri);
+    public void testMultipleGetParametersWithMissingValue() {
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo=&baz=zot HTTP/1.1");
+        assertEquals("", this.testServer.parms.get("foo"));
+        assertEquals("zot", this.testServer.parms.get("baz"));
+    }
+
+    @Test
+    public void testMultipleGetParametersWithMissingValueAndRequestHeaders() {
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo=&baz=zot HTTP/1.1\nAccept: text/html");
+        assertEquals("", this.testServer.parms.get("foo"));
+        assertEquals("zot", this.testServer.parms.get("baz"));
+        assertEquals("text/html", this.testServer.header.get("accept"));
     }
 
     @Test
     public void testMultipleHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() {
         String userAgent = "jUnit 4.8.2 Unit Test";
         String accept = "text/html";
-        invokeServer("HEAD " + URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\nAccept: " + accept);
-        assertEquals(userAgent, testServer.header.get("user-agent"));
-        assertEquals(accept, testServer.header.get("accept"));
+        invokeServer("HEAD " + HttpServerTest.URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\nAccept: " + accept);
+        assertEquals(userAgent, this.testServer.header.get("user-agent"));
+        assertEquals(accept, this.testServer.header.get("accept"));
     }
 
     @Test
     public void testSingleGetParameter() {
-        invokeServer("HEAD " + URI + "?foo=bar HTTP/1.1");
-        assertEquals("bar", testServer.parms.get("foo"));
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo=bar HTTP/1.1");
+        assertEquals("bar", this.testServer.parms.get("foo"));
     }
 
     @Test
     public void testSingleGetParameterWithNoValue() {
-        invokeServer("HEAD " + URI + "?foo HTTP/1.1");
-        assertEquals("", testServer.parms.get("foo"));
+        invokeServer("HEAD " + HttpServerTest.URI + "?foo HTTP/1.1");
+        assertEquals("", this.testServer.parms.get("foo"));
     }
 
     @Test
-    public void testMultipleGetParameters() {
-        invokeServer("HEAD " + URI + "?foo=bar&baz=zot HTTP/1.1");
-        assertEquals("bar", testServer.parms.get("foo"));
-        assertEquals("zot", testServer.parms.get("baz"));
+    public void testSingleUserAgentHeaderSuppliedToServeMethodFromSimpleWorkingGetRequest() {
+        String userAgent = "jUnit 4.8.2 Unit Test";
+        invokeServer("HEAD " + HttpServerTest.URI + " HTTP/1.1\nUser-Agent: " + userAgent + "\n");
+        assertEquals(userAgent, this.testServer.header.get("user-agent"));
+        assertEquals(NanoHTTPD.Method.HEAD, this.testServer.method);
+        assertEquals(HttpServerTest.URI, this.testServer.uri);
     }
 
-    @Test
-    public void testMultipleGetParametersWithMissingValue() {
-        invokeServer("HEAD " + URI + "?foo=&baz=zot HTTP/1.1");
-        assertEquals("", testServer.parms.get("foo"));
-        assertEquals("zot", testServer.parms.get("baz"));
-    }
-
-    @Test
-    public void testMultipleGetParametersWithMissingValueAndRequestHeaders() {
-        invokeServer("HEAD " + URI + "?foo=&baz=zot HTTP/1.1\nAccept: text/html");
-        assertEquals("", testServer.parms.get("foo"));
-        assertEquals("zot", testServer.parms.get("baz"));
-        assertEquals("text/html", testServer.header.get("accept"));
-    }
-
-    @Test
-    public void testDecodingParametersWithSingleValue() {
-        invokeServer("HEAD " + URI + "?foo=bar&baz=zot HTTP/1.1");
-        assertEquals("foo=bar&baz=zot", testServer.queryParameterString);
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("foo").size());
-        assertEquals("bar", testServer.decodedParamters.get("foo").get(0));
-        assertTrue(testServer.decodedParamters.get("baz") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("baz").size());
-        assertEquals("zot", testServer.decodedParamters.get("baz").get(0));
-    }
-
-    @Test
-    public void testDecodingParametersWithSingleValueAndMissingValue() {
-        invokeServer("HEAD " + URI + "?foo&baz=zot HTTP/1.1");
-        assertEquals("foo&baz=zot", testServer.queryParameterString);
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(0, testServer.decodedParamters.get("foo").size());
-        assertTrue(testServer.decodedParamters.get("baz") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("baz").size());
-        assertEquals("zot", testServer.decodedParamters.get("baz").get(0));
-    }
-
-    @Test
-    public void testDecodingFieldWithEmptyValueAndFieldWithMissingValueGiveDifferentResults() {
-        invokeServer("HEAD " + URI + "?foo&bar= HTTP/1.1");
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(0, testServer.decodedParamters.get("foo").size());
-        assertTrue(testServer.decodedParamters.get("bar") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("bar").size());
-        assertEquals("", testServer.decodedParamters.get("bar").get(0));
-    }
-
-    @Test
-    public void testDecodingSingleFieldRepeated() {
-        invokeServer("HEAD " + URI + "?foo=bar&foo=baz HTTP/1.1");
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(2, testServer.decodedParamters.get("foo").size());
-        assertEquals("bar", testServer.decodedParamters.get("foo").get(0));
-        assertEquals("baz", testServer.decodedParamters.get("foo").get(1));
-    }
-
-    @Test
-    public void testDecodingMixtureOfParameters() {
-        invokeServer("HEAD " + URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1");
-        assertTrue(testServer.decodedParamters.get("foo") instanceof List);
-        assertEquals(2, testServer.decodedParamters.get("foo").size());
-        assertEquals("bar", testServer.decodedParamters.get("foo").get(0));
-        assertEquals("baz", testServer.decodedParamters.get("foo").get(1));
-        assertTrue(testServer.decodedParamters.get("zot") instanceof List);
-        assertEquals(0, testServer.decodedParamters.get("zot").size());
-        assertTrue(testServer.decodedParamters.get("zim") instanceof List);
-        assertEquals(1, testServer.decodedParamters.get("zim").size());
-        assertEquals("", testServer.decodedParamters.get("zim").get(0));
-    }
-
-    @Test
-    public void testDecodingParametersFromParameterMap() {
-        invokeServer("HEAD " + URI + "?foo=bar&foo=baz&zot&zim= HTTP/1.1");
-        assertEquals(testServer.decodedParamters, testServer.decodedParamtersFromParameter);
-    }
-    // -------------------------------------------------------------------------------------------------------- //
-
 }
diff --git a/core/src/test/java/fi/iki/elonen/HttpKeepAliveTest.java b/core/src/test/java/fi/iki/elonen/HttpKeepAliveTest.java
index f349ee5..e168814 100644
--- a/core/src/test/java/fi/iki/elonen/HttpKeepAliveTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpKeepAliveTest.java
@@ -1,5 +1,38 @@
 package fi.iki.elonen;
 
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
 import static junit.framework.Assert.fail;
 
 import java.io.ByteArrayOutputStream;
@@ -10,68 +43,96 @@
 
 public class HttpKeepAliveTest extends HttpServerTest {
 
+    private Throwable error = null;
+
     @Test
     public void testManyGetRequests() throws Exception {
-        String request = "GET " + URI + " HTTP/1.1\r\n\r\n";
+        String request = "GET " + HttpServerTest.URI + " HTTP/1.1\r\n\r\n";
         String[] expected = {
-                "HTTP/1.1 200 OK",
-                "Content-Type: text/html",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 0",
-                ""
-        };
-        testManyRequests(request, expected);
-    }
-    
-    @Test
-    public void testManyPutRequests() throws Exception {
-        String data = "BodyData 1\nLine 2";
-        String request = "PUT " + URI + " HTTP/1.1\r\nContent-Length: " + data.length() + "\r\n\r\n" + data;
-        String[] expected = {
-                "HTTP/1.1 200 OK",
-                "Content-Type: text/html",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 0",
-                ""
+            "HTTP/1.1 200 OK",
+            "Content-Type: text/html",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 0",
+            ""
         };
         testManyRequests(request, expected);
     }
 
-    private Throwable error = null;
-    
+    @Test
+    public void testManyPutRequests() throws Exception {
+        String data = "BodyData 1\nLine 2";
+        String request = "PUT " + HttpServerTest.URI + " HTTP/1.1\r\nContent-Length: " + data.length() + "\r\n\r\n" + data;
+        String[] expected = {
+            "HTTP/1.1 200 OK",
+            "Content-Type: text/html",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 0",
+            ""
+        };
+        testManyRequests(request, expected);
+    }
+
     /**
-     * Issue the given request many times to check whether an error occurs.
-     * For this test, a small stack size is used, since a stack overflow is among the possible errors.
-     * @param request The request to issue
-     * @param expected The expected response
+     * Issue the given request many times to check whether an error occurs. For
+     * this test, a small stack size is used, since a stack overflow is among
+     * the possible errors.
+     * 
+     * @param request
+     *            The request to issue
+     * @param expected
+     *            The expected response
      */
     public void testManyRequests(final String request, final String[] expected) throws Exception {
         Runnable r = new Runnable() {
+
+            @Override
             public void run() {
                 try {
                     PipedOutputStream requestStream = new PipedOutputStream();
                     PipedInputStream inputStream = new PipedInputStream(requestStream);
                     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-                    NanoHTTPD.HTTPSession session = testServer.createSession(new TestTempFileManager(), inputStream, outputStream);
-                    for (int i = 0; i < 2048; i++) {
-                        requestStream.write(request.getBytes());
+                    NanoHTTPD.DefaultTempFileManager tempFileManager = new NanoHTTPD.DefaultTempFileManager();
+                    try {
+                        NanoHTTPD.HTTPSession session = HttpKeepAliveTest.this.testServer.createSession(tempFileManager, inputStream, outputStream);
+                        for (int i = 0; i < 2048; i++) {
+                            requestStream.write(request.getBytes());
+                            requestStream.flush();
+                            outputStream.reset();
+                            session.execute();
+                            assertResponse(outputStream, expected);
+                        }
+
+                        // Finally, try "Connection: Close"
+                        String closeReq = request.replaceAll("HTTP/1.1", "HTTP/1.1\r\nConnection: Close");
+                        expected[3] = "Connection: close";
+                        requestStream.write(closeReq.getBytes());
+                        outputStream.reset();
                         requestStream.flush();
-                        session.execute();
+                        // Server should now close the socket by throwing a
+                        // SocketException:
+                        try {
+                            session.execute();
+                        } catch (java.net.SocketException se) {
+                            junit.framework.Assert.assertEquals(se.getMessage(), "NanoHttpd Shutdown");
+                        }
                         assertResponse(outputStream, expected);
+
+                    } finally {
+                        tempFileManager.clear();
                     }
                 } catch (Throwable t) {
-                    error = t;
+                    HttpKeepAliveTest.this.error = t;
                 }
             }
         };
         Thread t = new Thread(null, r, "Request Thread", 1 << 17);
         t.start();
         t.join();
-        if (error != null) {
-            fail(""+error);
-            error.printStackTrace();
+        if (this.error != null) {
+            fail("" + this.error);
+            this.error.printStackTrace();
         }
     }
 }
diff --git a/core/src/test/java/fi/iki/elonen/HttpParsingTest.java b/core/src/test/java/fi/iki/elonen/HttpParsingTest.java
index 9705c5b..1f40f63 100644
--- a/core/src/test/java/fi/iki/elonen/HttpParsingTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpParsingTest.java
@@ -1,32 +1,63 @@
 package fi.iki.elonen;
 
-import org.junit.Test;
-
-import java.net.URLDecoder;
-import java.net.URLEncoder;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
 
 import static junit.framework.Assert.assertEquals;
 
+import org.junit.Test;
+
 public class HttpParsingTest extends HttpServerTest {
+
+    @Test
+    public void testMultibyteCharacterSupport() throws Exception {
+        String expected = "Chinese \u738b Letters";
+        String input = "Chinese+%e7%8e%8b+Letters";
+        assertEquals(expected, this.testServer.decodePercent(input));
+    }
+
     @Test
     public void testNormalCharacters() throws Exception {
         for (int i = 0x20; i < 0x80; i++) {
             String hex = Integer.toHexString(i);
             String input = "%" + hex;
             char expected = (char) i;
-            assertEquals("" + expected, testServer.decodePercent(input));
+            assertEquals("" + expected, this.testServer.decodePercent(input));
         }
     }
 
     @Test
-    public void testMultibyteCharacterSupport() throws Exception {
-        String expected = "Chinese \u738b Letters";
-        String input = "Chinese+%e7%8e%8b+Letters";
-        assertEquals(expected, testServer.decodePercent(input));
-    }
-
-    @Test
     public void testPlusInQueryParams() throws Exception {
-        assertEquals("foo bar", testServer.decodePercent("foo+bar"));
+        assertEquals("foo bar", this.testServer.decodePercent("foo+bar"));
     }
 }
diff --git a/core/src/test/java/fi/iki/elonen/HttpPostRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpPostRequestTest.java
index 3cb37e0..db82e1c 100644
--- a/core/src/test/java/fi/iki/elonen/HttpPostRequestTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpPostRequestTest.java
@@ -1,157 +1,184 @@
 package fi.iki.elonen;
 
-import org.junit.Test;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static junit.framework.Assert.assertEquals;
 
 import java.io.BufferedReader;
 import java.io.FileReader;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.UUID;
 
-import static junit.framework.Assert.assertEquals;
+import org.junit.Test;
 
 public class HttpPostRequestTest extends HttpServerTest {
 
     public static final String CONTENT_LENGTH = "Content-Length: ";
+
     public static final String FIELD = "caption";
+
     public static final String VALUE = "Summer vacation";
+
     public static final String FIELD2 = "location";
+
     public static final String VALUE2 = "Grand Canyon";
+
     public static final String POST_RAW_CONTENT_FILE_ENTRY = "postData";
+
     public static final String VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS = "Test raw data & Result value";
 
-    @Test
-    public void testSimpleRawPostData() throws Exception {
-        String header = "POST " + URI + " HTTP/1.1\n";
-        String content = VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS + "\n";
+    /**
+     * contains common preparation steps for testing POST with Multipart Form
+     * 
+     * @param fileName
+     *            Name of file to be uploaded
+     * @param fileContent
+     *            Content of file to be uploaded
+     * @return input String with POST request complete information including
+     *         header, length and content
+     */
+    private String preparePostWithMultipartForm(String fileName, String fileContent) {
+        String divider = UUID.randomUUID().toString();
+        String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data, boundary=" + divider + "\r\n";
+        String content =
+                "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"; filename=\"" + fileName + "\"\r\n"
+                        + "Content-Type: image/jpeg\r\n" + "\r\n" + fileContent + "\r\n" + "--" + divider + "--\r\n";
         int size = content.length() + header.length();
         int contentLengthHeaderValueSize = String.valueOf(size).length();
-        int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length();
-        String input = header + CONTENT_LENGTH + (contentLength+4) + "\r\n\r\n" + content;
-        invokeServer(input);
-        assertEquals(0, testServer.parms.size());
-        assertEquals(1, testServer.files.size());
-        assertEquals(VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS, testServer.files.get(POST_RAW_CONTENT_FILE_ENTRY));
+        int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length();
+        String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 5) + "\r\n\r\n" + content;
+
+        return input;
     }
 
     @Test
-    public void testSimplePostWithSingleMultipartFormField() throws Exception {
-        String divider = UUID.randomUUID().toString();
-        String header = "POST " + URI + " HTTP/1.1\nContent-Type: " +
-                "multipart/form-data; boundary=" + divider + "\n";
-        String content = "--" + divider + "\n" +
-                "Content-Disposition: form-data; name=\""+FIELD+"\"\n" +
-                "\n" +
-                VALUE +"\n" +
-                "--" + divider + "--\n";
-        int size = content.length() + header.length();
-        int contentLengthHeaderValueSize = String.valueOf(size).length();
-        int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length();
-        String input = header + CONTENT_LENGTH + (contentLength+4) + "\r\n\r\n" + content;
+    public void testPostWithMultipartFormUpload() throws Exception {
+        String filename = "GrandCanyon.txt";
+        String fileContent = HttpPostRequestTest.VALUE;
+        String input = preparePostWithMultipartForm(filename, fileContent);
+
         invokeServer(input);
 
-        assertEquals(1, testServer.parms.size());
-        assertEquals(VALUE, testServer.parms.get(FIELD));
+        assertEquals(1, this.testServer.parms.size());
+        BufferedReader reader = new BufferedReader(new FileReader(this.testServer.files.get(HttpPostRequestTest.FIELD)));
+        List<String> lines = readLinesFromFile(reader);
+        assertLinesOfText(new String[]{
+            fileContent
+        }, lines);
+    }
+
+    @Test
+    public void testPostWithMultipartFormUploadFilenameHasSpaces() throws Exception {
+        String fileNameWithSpace = "Grand Canyon.txt";
+        String fileContent = HttpPostRequestTest.VALUE;
+        String input = preparePostWithMultipartForm(fileNameWithSpace, fileContent);
+
+        invokeServer(input);
+
+        String fileNameAfter = new ArrayList<String>(this.testServer.parms.values()).get(0);
+
+        assertEquals(fileNameWithSpace, fileNameAfter);
     }
 
     @Test
     public void testPostWithMultipleMultipartFormFields() throws Exception {
         String divider = UUID.randomUUID().toString();
-        String header = "POST " + URI + " HTTP/1.1\nContent-Type: " +
-                "multipart/form-data; boundary=" + divider + "\n";
-        String content = "--" + divider + "\n" +
-                "Content-Disposition: form-data; name=\""+FIELD+"\"\n" +
-                "\n" +
-                VALUE +"\n" +"--" + divider + "\n" +
-                "Content-Disposition: form-data; name=\""+FIELD2+"\"\n" +
-                "\n" +
-                VALUE2 +"\n" +
-                "--" + divider + "--\n";
+        String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data; boundary=" + divider + "\n";
+        String content =
+                "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE + "\r\n"
+                        + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD2 + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE2
+                        + "\r\n" + "--" + divider + "--\r\n";
         int size = content.length() + header.length();
         int contentLengthHeaderValueSize = String.valueOf(size).length();
-        int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length();
-        String input = header + CONTENT_LENGTH + (contentLength+4) + "\r\n\r\n" + content;
+        int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length();
+        String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content;
         invokeServer(input);
 
-        assertEquals(2, testServer.parms.size());
-        assertEquals(VALUE, testServer.parms.get(FIELD));
-        assertEquals(VALUE2, testServer.parms.get(FIELD2));
+        assertEquals(2, this.testServer.parms.size());
+        assertEquals(HttpPostRequestTest.VALUE, this.testServer.parms.get(HttpPostRequestTest.FIELD));
+        assertEquals(HttpPostRequestTest.VALUE2, this.testServer.parms.get(HttpPostRequestTest.FIELD2));
     }
 
     @Test
     public void testPostWithMultipleMultipartFormFieldsWhereContentTypeWasSeparatedByComma() throws Exception {
         String divider = UUID.randomUUID().toString();
-        String header = "POST " + URI + " HTTP/1.1\nContent-Type: " +
-                "multipart/form-data, boundary=" + divider + "\n";
-        String content = "--" + divider + "\n" +
-                "Content-Disposition: form-data; name=\""+FIELD+"\"\n" +
-                "\n" +
-                VALUE +"\n" +"--" + divider + "\n" +
-                "Content-Disposition: form-data; name=\""+FIELD2+"\"\n" +
-                "\n" +
-                VALUE2 +"\n" +
-                "--" + divider + "--\n";
+        String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data, boundary=" + divider + "\r\n";
+        String content =
+                "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE + "\r\n"
+                        + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD2 + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE2
+                        + "\r\n" + "--" + divider + "--\r\n";
         int size = content.length() + header.length();
         int contentLengthHeaderValueSize = String.valueOf(size).length();
-        int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length();
-        String input = header + CONTENT_LENGTH + (contentLength+4) + "\r\n\r\n" + content;
+        int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length();
+        String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content;
         invokeServer(input);
 
-        assertEquals(2, testServer.parms.size());
-        assertEquals(VALUE, testServer.parms.get(FIELD));
-        assertEquals(VALUE2, testServer.parms.get(FIELD2));
+        assertEquals(2, this.testServer.parms.size());
+        assertEquals(HttpPostRequestTest.VALUE, this.testServer.parms.get(HttpPostRequestTest.FIELD));
+        assertEquals(HttpPostRequestTest.VALUE2, this.testServer.parms.get(HttpPostRequestTest.FIELD2));
     }
-    
+
     @Test
-    public void testPostWithMultipartFormUpload() throws Exception {
-        String filename = "GrandCanyon.txt";
-        String fileContent = VALUE;
-        String input = preparePostWithMultipartForm(filename, fileContent);
-    
-        invokeServer(input);
-    
-        assertEquals(1, testServer.parms.size());
-        BufferedReader reader = new BufferedReader(new FileReader(testServer.files.get(FIELD)));
-        List<String> lines = readLinesFromFile(reader);
-        assertLinesOfText(new String[]{fileContent}, lines);
-    }
-    
-    @Test
-    public void testPostWithMultipartFormUploadFilenameHasSpaces() throws Exception {
-      String fileNameWithSpace = "Grand Canyon.txt";
-      String fileContent = VALUE;
-      String input = preparePostWithMultipartForm(fileNameWithSpace, fileContent);
-      
-      invokeServer(input);
-      
-      String fileNameAfter = new ArrayList<String>(testServer.parms.values()).get(0);
-      
-      assertEquals(fileNameWithSpace, fileNameAfter);
-    }
-    
-    /**
-     * contains common preparation steps for testing POST with Multipart Form
-     * @param fileName Name of file to be uploaded
-     * @param fileContent Content of file to be uploaded
-     * @return input String with POST request complete information including header, length and content
-     */
-    private String preparePostWithMultipartForm(String fileName, String fileContent) {
+    public void testSimplePostWithSingleMultipartFormField() throws Exception {
         String divider = UUID.randomUUID().toString();
-        String header = "POST " + URI + " HTTP/1.1\nContent-Type: " +
-                "multipart/form-data, boundary=" + divider + "\n";
-        String content = "--" + divider + "\n" +
-                "Content-Disposition: form-data; name=\""+FIELD+"\"; filename=\""+fileName+"\"\n" +
-                "Content-Type: image/jpeg\r\n"+
-                "\r\n" +
-                fileContent +"\r\n" +
-                "--" + divider + "--\n";
+        String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data; boundary=" + divider + "\r\n";
+        String content =
+                "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE + "\r\n"
+                        + "--" + divider + "--\r\n";
         int size = content.length() + header.length();
         int contentLengthHeaderValueSize = String.valueOf(size).length();
-        int contentLength = size + contentLengthHeaderValueSize + CONTENT_LENGTH.length();
-        String input = header + CONTENT_LENGTH + (contentLength+5) + "\r\n\r\n" + content;
-        
-        return input;
+        int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length();
+        String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content;
+        invokeServer(input);
+
+        assertEquals(1, this.testServer.parms.size());
+        assertEquals(HttpPostRequestTest.VALUE, this.testServer.parms.get(HttpPostRequestTest.FIELD));
+    }
+
+    @Test
+    public void testSimpleRawPostData() throws Exception {
+        String header = "POST " + HttpServerTest.URI + " HTTP/1.1\n";
+        String content = HttpPostRequestTest.VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS + "\r\n";
+        int size = content.length() + header.length();
+        int contentLengthHeaderValueSize = String.valueOf(size).length();
+        int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length();
+        String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content;
+        invokeServer(input);
+        assertEquals(0, this.testServer.parms.size());
+        assertEquals(1, this.testServer.files.size());
+        assertEquals(HttpPostRequestTest.VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS, this.testServer.files.get(HttpPostRequestTest.POST_RAW_CONTENT_FILE_ENTRY));
     }
 
 }
diff --git a/core/src/test/java/fi/iki/elonen/HttpPutRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpPutRequestTest.java
index 912113e..54a3343 100644
--- a/core/src/test/java/fi/iki/elonen/HttpPutRequestTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpPutRequestTest.java
@@ -1,39 +1,72 @@
 package fi.iki.elonen;
 
-import org.junit.Test;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static junit.framework.Assert.assertTrue;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.FileReader;
 import java.util.List;
 
-import static junit.framework.Assert.*;
+import org.junit.Test;
 
 public class HttpPutRequestTest extends HttpServerTest {
 
     @Test
     public void testPutRequestSendsContent() throws Exception {
-        ByteArrayOutputStream outputStream = invokeServer("PUT " + URI + " HTTP/1.1\r\n\r\nBodyData 1\nLine 2");
+        ByteArrayOutputStream outputStream = invokeServer("PUT " + HttpServerTest.URI + " HTTP/1.1\r\n\r\nBodyData 1\nLine 2");
 
         String[] expectedOutput = {
-                "HTTP/1.1 200 OK",
-                "Content-Type: text/html",
-                "Date: .*",
-                "Connection: keep-alive",
-                "Content-Length: 0",
-                ""
+            "HTTP/1.1 200 OK",
+            "Content-Type: text/html",
+            "Date: .*",
+            "Connection: keep-alive",
+            "Content-Length: 0",
+            ""
         };
 
         assertResponse(outputStream, expectedOutput);
 
-        assertTrue(testServer.files.containsKey("content"));
+        assertTrue(this.testServer.files.containsKey("content"));
         BufferedReader reader = null;
         try {
             String[] expectedInputToServeMethodViaFile = {
-                    "BodyData 1",
-                    "Line 2"
+                "BodyData 1",
+                "Line 2"
             };
-            reader = new BufferedReader(new FileReader(testServer.files.get("content")));
+            reader = new BufferedReader(new FileReader(this.testServer.files.get("content")));
             List<String> lines = readLinesFromFile(reader);
             assertLinesOfText(expectedInputToServeMethodViaFile, lines);
         } finally {
diff --git a/core/src/test/java/fi/iki/elonen/HttpSSLServerTest.java b/core/src/test/java/fi/iki/elonen/HttpSSLServerTest.java
new file mode 100644
index 0000000..30fb48c
--- /dev/null
+++ b/core/src/test/java/fi/iki/elonen/HttpSSLServerTest.java
@@ -0,0 +1,89 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class HttpSSLServerTest extends HttpServerTest {
+
+    @Test
+    public void testSSLConnection() throws ClientProtocolException, IOException {
+        DefaultHttpClient httpclient = new DefaultHttpClient();
+        HttpTrace httphead = new HttpTrace("https://localhost:9043/index.html");
+        HttpResponse response = httpclient.execute(httphead);
+        HttpEntity entity = response.getEntity();
+        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+
+        Assert.assertEquals(9043, this.testServer.getListeningPort());
+        Assert.assertTrue(this.testServer.isAlive());
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        System.setProperty("javax.net.ssl.trustStore", new File("src/test/resources/keystore.jks").getAbsolutePath());
+        this.testServer = new TestServer(9043);
+        this.testServer.makeSecure(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray()), null);
+        this.tempFileManager = new TestTempFileManager();
+        this.testServer.start();
+        try {
+            long start = System.currentTimeMillis();
+            Thread.sleep(100L);
+            while (!this.testServer.wasStarted()) {
+                Thread.sleep(100L);
+                if (System.currentTimeMillis() - start > 2000) {
+                    Assert.fail("could not start server");
+                }
+            }
+        } catch (InterruptedException e) {
+        }
+    }
+
+    @After
+    public void tearDown() {
+        this.testServer.stop();
+    }
+}
diff --git a/core/src/test/java/fi/iki/elonen/HttpServerTest.java b/core/src/test/java/fi/iki/elonen/HttpServerTest.java
index ef209d1..e8be61e 100644
--- a/core/src/test/java/fi/iki/elonen/HttpServerTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpServerTest.java
@@ -1,41 +1,143 @@
 package fi.iki.elonen;
 
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
 
-import java.io.*;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StringReader;
 import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import static junit.framework.Assert.*;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
 
 /**
- * @author Paul S. Hawke (paul.hawke@gmail.com)
- *         On: 3/10/13 at 8:32 PM
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 3/10/13 at 8:32 PM
  */
 public class HttpServerTest {
+
+    public static class TestServer extends NanoHTTPD {
+
+        public Response response = newFixedLengthResponse("");
+
+        public String uri;
+
+        public Method method;
+
+        public Map<String, String> header;
+
+        public Map<String, String> parms;
+
+        public Map<String, String> files;
+
+        public Map<String, List<String>> decodedParamters;
+
+        public Map<String, List<String>> decodedParamtersFromParameter;
+
+        public String queryParameterString;
+
+        public TestServer() {
+            super(8192);
+        }
+
+        public TestServer(int port) {
+            super(port);
+        }
+
+        public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
+            return new HTTPSession(tempFileManager, inputStream, outputStream);
+        }
+
+        public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
+            return new HTTPSession(tempFileManager, inputStream, outputStream, inetAddress);
+        }
+
+        @Override
+        public Response serve(IHTTPSession session) {
+            this.uri = session.getUri();
+            this.method = session.getMethod();
+            this.header = session.getHeaders();
+            this.parms = session.getParms();
+            this.files = new HashMap<String, String>();
+            try {
+                session.parseBody(this.files);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            this.queryParameterString = session.getQueryParameterString();
+            this.decodedParamtersFromParameter = decodeParameters(this.queryParameterString);
+            this.decodedParamters = decodeParameters(session.getQueryParameterString());
+            return this.response;
+        }
+    }
+
+    public static class TestTempFileManager extends NanoHTTPD.DefaultTempFileManager {
+
+        public void _clear() {
+            super.clear();
+        }
+
+        @Override
+        public void clear() {
+            // ignore
+        }
+    }
+
     public static final String URI = "http://www.myserver.org/pub/WWW/someFile.html";
+
     protected TestServer testServer;
-    private TestTempFileManager tempFileManager;
 
-    @Before
-    public void setUp() {
-        testServer = new TestServer();
-        tempFileManager = new TestTempFileManager();
-    }
+    protected TestTempFileManager tempFileManager;
 
-    @After
-    public void tearDown() {
-        tempFileManager._clear();
-    }
-
-    @Test
-    public void testServerExists() {
-        assertNotNull(testServer);
+    protected void assertLinesOfText(String[] expected, List<String> lines) {
+        // assertEquals(expected.length, lines.size());
+        for (int i = 0; i < expected.length; i++) {
+            String line = lines.get(i);
+            assertTrue("Output line " + i + " doesn't match expectation.\n" + "  Output: " + line + "\n" + "Expected: " + expected[i], line.matches(expected[i]));
+        }
     }
 
     protected void assertResponse(ByteArrayOutputStream outputStream, String[] expected) throws IOException {
@@ -43,34 +145,24 @@
         assertLinesOfText(expected, lines);
     }
 
-    protected void assertLinesOfText(String[] expected, List<String> lines) {
-//        assertEquals(expected.length, lines.size());
-        for (int i = 0; i < expected.length; i++) {
-            String line = lines.get(i);
-            assertTrue("Output line " + i + " doesn't match expectation.\n" +
-                    "  Output: " + line + "\n" +
-                    "Expected: " + expected[i], line.matches(expected[i]));
-        }
+    protected List<String> getOutputLines(ByteArrayOutputStream outputStream) throws IOException {
+        BufferedReader reader = new BufferedReader(new StringReader(outputStream.toString()));
+        return readLinesFromFile(reader);
     }
 
     protected ByteArrayOutputStream invokeServer(String request) {
         ByteArrayInputStream inputStream = new ByteArrayInputStream(request.getBytes());
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        NanoHTTPD.HTTPSession session = testServer.createSession(tempFileManager, inputStream, outputStream);
+        NanoHTTPD.HTTPSession session = this.testServer.createSession(this.tempFileManager, inputStream, outputStream);
         try {
             session.execute();
         } catch (IOException e) {
-            fail(""+e);
+            fail("" + e);
             e.printStackTrace();
         }
         return outputStream;
     }
 
-    protected List<String> getOutputLines(ByteArrayOutputStream outputStream) throws IOException {
-        BufferedReader reader = new BufferedReader(new StringReader(outputStream.toString()));
-        return readLinesFromFile(reader);
-    }
-
     protected List<String> readLinesFromFile(BufferedReader reader) throws IOException {
         List<String> lines = new ArrayList<String>();
         String line = "";
@@ -83,60 +175,19 @@
         return lines;
     }
 
-    public static class TestTempFileManager extends NanoHTTPD.DefaultTempFileManager {
-        public void _clear() {
-            super.clear();
-        }
-
-        @Override
-        public void clear() {
-            // ignore
-        }
+    @Before
+    public void setUp() throws Exception {
+        this.testServer = new TestServer();
+        this.tempFileManager = new TestTempFileManager();
     }
 
-    public static class TestServer extends NanoHTTPD {
-        public Response response = new Response("");
-        public String uri;
-        public Method method;
-        public Map<String, String> header;
-        public Map<String, String> parms;
-        public Map<String, String> files;
-        public Map<String, List<String>> decodedParamters;
-        public Map<String, List<String>> decodedParamtersFromParameter;
-        public String queryParameterString;
+    @After
+    public void tearDown() {
+        this.tempFileManager._clear();
+    }
 
-        public TestServer() {
-            super(8192);
-        }
-
-        public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
-            return new HTTPSession(tempFileManager, inputStream, outputStream);
-        }
-
-        public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
-            return new HTTPSession(tempFileManager, inputStream, outputStream, inetAddress);
-        }
-
-        @Override public Response serve(IHTTPSession session) {
-            this.uri = session.getUri();
-            this.method = session.getMethod();
-            this.header = session.getHeaders();
-            this.parms = session.getParms();
-            this.files = new HashMap<String, String>();
-            try {
-                session.parseBody(files);
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-            queryParameterString = session.getQueryParameterString();
-            this.decodedParamtersFromParameter = decodeParameters(queryParameterString);
-            this.decodedParamters = decodeParameters(session.getQueryParameterString());
-            return response;
-        }
-
-        @Override
-        public String decodePercent(String str) {
-            return super.decodePercent(str);
-        }
+    @Test
+    public void testServerExists() {
+        assertNotNull(this.testServer);
     }
 }
diff --git a/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java b/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java
index 75a0a49..1494e28 100644
--- a/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java
@@ -1,30 +1,66 @@
 package fi.iki.elonen;
 
-import org.junit.Test;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static org.junit.Assert.assertEquals;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.net.InetAddress;
 
-import static org.junit.Assert.assertEquals;
+import org.junit.Ignore;
+import org.junit.Test;
 
 public class HttpSessionHeadersTest extends HttpServerTest {
+
     private static final String DUMMY_REQUEST_CONTENT = "dummy request content";
+
     private static final TestTempFileManager TEST_TEMP_FILE_MANAGER = new TestTempFileManager();
 
-    @Override
-    public void setUp() {
-        super.setUp();
-    }
-
     @Test
+    @Ignore
     public void testHeadersRemoteIp() throws Exception {
-        ByteArrayInputStream inputStream = new ByteArrayInputStream(DUMMY_REQUEST_CONTENT.getBytes());
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(HttpSessionHeadersTest.DUMMY_REQUEST_CONTENT.getBytes());
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        String[] ipAddresses = { "127.0.0.1", "192.168.1.1", "192.30.252.129" };
-        for(String ipAddress : ipAddresses) {
+        String[] ipAddresses = {
+            "127.0.0.1",
+            "192.168.1.1",
+            "192.30.252.129"
+        };
+        for (String ipAddress : ipAddresses) {
             InetAddress inetAddress = InetAddress.getByName(ipAddress);
-            NanoHTTPD.HTTPSession session = testServer.createSession(TEST_TEMP_FILE_MANAGER, inputStream, outputStream, inetAddress);
+            NanoHTTPD.HTTPSession session = this.testServer.createSession(HttpSessionHeadersTest.TEST_TEMP_FILE_MANAGER, inputStream, outputStream, inetAddress);
             assertEquals(ipAddress, session.getHeaders().get("remote-addr"));
             assertEquals(ipAddress, session.getHeaders().get("http-client-ip"));
         }
diff --git a/core/src/test/java/fi/iki/elonen/InvalidRequestTest.java b/core/src/test/java/fi/iki/elonen/InvalidRequestTest.java
new file mode 100644
index 0000000..eda60a3
--- /dev/null
+++ b/core/src/test/java/fi/iki/elonen/InvalidRequestTest.java
@@ -0,0 +1,79 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class InvalidRequestTest extends HttpServerTest {
+
+    @Test
+    public void testGetRequestWithoutProtocol() {
+        invokeServer("GET " + HttpServerTest.URI + "\r\nX-Important-Header: foo");
+
+        assertNotNull(this.testServer.parms);
+        assertTrue(this.testServer.header.size() > 0);
+        assertNotNull(this.testServer.files);
+        assertNotNull(this.testServer.uri);
+    }
+
+    @Test
+    public void testGetRequestWithProtocol() {
+        invokeServer("GET " + HttpServerTest.URI + " HTTP/1.1\r\nX-Important-Header: foo");
+
+        assertNotNull(this.testServer.parms);
+        assertTrue(this.testServer.header.size() > 0);
+        assertNotNull(this.testServer.files);
+        assertNotNull(this.testServer.uri);
+    }
+
+    @Test
+    public void testPostRequestWithoutProtocol() {
+        invokeServer("POST " + HttpServerTest.URI + "\r\nContent-Length: 123");
+        assertNotNull(this.testServer.parms);
+        assertTrue(this.testServer.header.size() > 0);
+        assertNotNull(this.testServer.files);
+        assertNotNull(this.testServer.uri);
+    }
+
+    @Test
+    public void testPostRequestWithProtocol() {
+        invokeServer("POST " + HttpServerTest.URI + " HTTP/1.1\r\nContent-Length: 123");
+        assertNotNull(this.testServer.parms);
+        assertTrue(this.testServer.header.size() > 0);
+        assertNotNull(this.testServer.files);
+        assertNotNull(this.testServer.uri);
+    }
+}
diff --git a/core/src/test/java/fi/iki/elonen/JavaIOTempDirExistTest.java b/core/src/test/java/fi/iki/elonen/JavaIOTempDirExistTest.java
new file mode 100644
index 0000000..b586bc2
--- /dev/null
+++ b/core/src/test/java/fi/iki/elonen/JavaIOTempDirExistTest.java
@@ -0,0 +1,87 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import fi.iki.elonen.NanoHTTPD.DefaultTempFile;
+
+/**
+ * Created by Victor Nikiforov on 10/16/15.
+ */
+public class JavaIOTempDirExistTest {
+
+    @Test
+    public void testJavaIoTempDefault() throws Exception {
+        String tmpdir = System.getProperty("java.io.tmpdir");
+        NanoHTTPD.DefaultTempFileManager manager = new NanoHTTPD.DefaultTempFileManager();
+        DefaultTempFile tempFile = (DefaultTempFile) manager.createTempFile("xx");
+        File tempFileBackRef = new File(tempFile.getName());
+        Assert.assertEquals(tempFileBackRef.getParentFile(), new File(tmpdir));
+
+        // force an exception
+        tempFileBackRef.delete();
+        Exception e = null;
+        try {
+            tempFile.delete();
+        } catch (Exception ex) {
+            e = ex;
+        }
+        Assert.assertNotNull(e);
+        manager.clear();
+    }
+
+    @Test
+    public void testJavaIoTempSpecific() throws IOException {
+        final String tmpdir = System.getProperty("java.io.tmpdir");
+        try {
+            String tempFileName = UUID.randomUUID().toString();
+            File newDir = new File("target", tempFileName);
+            System.setProperty("java.io.tmpdir", newDir.getAbsolutePath());
+            Assert.assertEquals(false, newDir.exists());
+            new NanoHTTPD.DefaultTempFileManager();
+            Assert.assertEquals(true, newDir.exists());
+            newDir.delete();
+        } finally {
+            System.setProperty("java.io.tmpdir", tmpdir);
+        }
+
+    }
+
+}
diff --git a/core/src/test/java/fi/iki/elonen/MimeTest.java b/core/src/test/java/fi/iki/elonen/MimeTest.java
new file mode 100644
index 0000000..046ef00
--- /dev/null
+++ b/core/src/test/java/fi/iki/elonen/MimeTest.java
@@ -0,0 +1,62 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MimeTest {
+
+    @Test
+    public void testExistingMimeType() throws Exception {
+        Assert.assertEquals("text/html", NanoHTTPD.getMimeTypeForFile("xxxx.html"));
+    }
+
+    @Test
+    public void testNotExistingMimeType() throws Exception {
+        Assert.assertNull(NanoHTTPD.mimeTypes().get("notExistent"));
+        Assert.assertEquals("application/octet-stream", NanoHTTPD.getMimeTypeForFile("xxxx.notExistent"));
+    }
+
+    @Test
+    public void testOverwritenMimeType() throws Exception {
+        Assert.assertEquals("video/wrongOverwrite", NanoHTTPD.getMimeTypeForFile("xxxx.ts"));
+    }
+
+    @Test
+    public void testManualMimeType() throws Exception {
+        NanoHTTPD.mimeTypes().put("flv", "video/manualOverwrite");
+        Assert.assertEquals("video/manualOverwrite", NanoHTTPD.getMimeTypeForFile("xxxx.flv"));
+    }
+}
diff --git a/core/src/test/java/fi/iki/elonen/SSLServerSocketFactoryTest.java b/core/src/test/java/fi/iki/elonen/SSLServerSocketFactoryTest.java
new file mode 100644
index 0000000..1722058
--- /dev/null
+++ b/core/src/test/java/fi/iki/elonen/SSLServerSocketFactoryTest.java
@@ -0,0 +1,90 @@
+package fi.iki.elonen;
+
+import java.io.File;
+
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.IOException;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import fi.iki.elonen.NanoHTTPD.SecureServerSocketFactory;
+
+public class SSLServerSocketFactoryTest extends HttpServerTest {
+
+    @Test
+    public void testSSLConnection() throws ClientProtocolException, IOException {
+        DefaultHttpClient httpclient = new DefaultHttpClient();
+        HttpTrace httphead = new HttpTrace("https://localhost:9043/index.html");
+        HttpResponse response = httpclient.execute(httphead);
+        HttpEntity entity = response.getEntity();
+        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+
+        Assert.assertEquals(9043, this.testServer.getListeningPort());
+        Assert.assertTrue(this.testServer.isAlive());
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        System.setProperty("javax.net.ssl.trustStore", new File("src/test/resources/keystore.jks").getAbsolutePath());
+        this.testServer = new TestServer(9043);
+        this.testServer.setServerSocketFactory(new SecureServerSocketFactory(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray()), null));
+        this.tempFileManager = new TestTempFileManager();
+        this.testServer.start();
+        try {
+            long start = System.currentTimeMillis();
+            Thread.sleep(100L);
+            while (!this.testServer.wasStarted()) {
+                Thread.sleep(100L);
+                if (System.currentTimeMillis() - start > 2000) {
+                    Assert.fail("could not start server");
+                }
+            }
+        } catch (InterruptedException e) {
+        }
+    }
+
+    @After
+    public void tearDown() {
+        this.testServer.stop();
+    }
+}
diff --git a/core/src/test/java/fi/iki/elonen/ServerSocketFactoryTest.java b/core/src/test/java/fi/iki/elonen/ServerSocketFactoryTest.java
new file mode 100644
index 0000000..17112ef
--- /dev/null
+++ b/core/src/test/java/fi/iki/elonen/ServerSocketFactoryTest.java
@@ -0,0 +1,102 @@
+package fi.iki.elonen;
+
+import java.io.File;
+
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.net.ServerSocket;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import fi.iki.elonen.HttpServerTest.TestServer;
+import fi.iki.elonen.NanoHTTPD.SecureServerSocketFactory;
+
+public class ServerSocketFactoryTest extends NanoHTTPD {
+
+    public static final int PORT = 8192;
+
+    public ServerSocketFactoryTest() {
+        super(PORT);
+
+        this.setServerSocketFactory(new TestFactory());
+    }
+
+    @Test
+    public void isCustomServerSocketFactory() {
+        System.out.println("CustomServerSocketFactory test");
+        Assert.assertTrue(this.getServerSocketFactory() instanceof TestFactory);
+    }
+
+    @Test
+    public void testCreateServerSocket() {
+        System.out.println("CreateServerSocket test");
+        ServerSocket ss = null;
+        try {
+            ss = this.getServerSocketFactory().create();
+        } catch (IOException e) {
+        }
+        Assert.assertTrue(ss != null);
+    }
+
+    @Test
+    public void testSSLServerSocketFail() {
+        String[] protocols = {
+            ""
+        };
+        System.setProperty("javax.net.ssl.trustStore", new File("src/test/resources/keystore.jks").getAbsolutePath());
+        ServerSocketFactory ssFactory = new SecureServerSocketFactory(null, protocols);
+        ServerSocket ss = null;
+        try {
+            ss = ssFactory.create();
+        } catch (Exception e) {
+        }
+        Assert.assertTrue(ss == null);
+
+    }
+
+    private class TestFactory implements ServerSocketFactory {
+
+        @Override
+        public ServerSocket create() {
+            try {
+                return new ServerSocket();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+            return null;
+        }
+    }
+}
diff --git a/core/src/test/java/fi/iki/elonen/integration/CookieIntegrationTest.java b/core/src/test/java/fi/iki/elonen/integration/CookieIntegrationTest.java
index 0d54b37..5cf5719 100644
--- a/core/src/test/java/fi/iki/elonen/integration/CookieIntegrationTest.java
+++ b/core/src/test/java/fi/iki/elonen/integration/CookieIntegrationTest.java
@@ -1,6 +1,45 @@
 package fi.iki.elonen.integration;
 
-import fi.iki.elonen.NanoHTTPD;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
 import org.apache.http.client.CookieStore;
 import org.apache.http.client.ResponseHandler;
 import org.apache.http.client.methods.HttpGet;
@@ -8,78 +47,77 @@
 import org.apache.http.impl.cookie.BasicClientCookie;
 import org.junit.Test;
 
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-
-import static org.junit.Assert.*;
+import fi.iki.elonen.NanoHTTPD;
 
 /**
- * @author Paul S. Hawke (paul.hawke@gmail.com)
- *         On: 9/2/13 at 10:10 PM
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 9/2/13 at 10:10 PM
  */
 public class CookieIntegrationTest extends IntegrationTestBase<CookieIntegrationTest.CookieTestServer> {
 
+    public static class CookieTestServer extends NanoHTTPD {
+
+        List<Cookie> cookiesReceived = new ArrayList<Cookie>();
+
+        List<Cookie> cookiesToSend = new ArrayList<Cookie>();
+
+        public CookieTestServer() {
+            super(8192);
+        }
+
+        @Override
+        public Response serve(IHTTPSession session) {
+            CookieHandler cookies = session.getCookies();
+            for (String cookieName : cookies) {
+                this.cookiesReceived.add(new Cookie(cookieName, cookies.read(cookieName)));
+            }
+            for (Cookie c : this.cookiesToSend) {
+                cookies.set(c);
+            }
+            return newFixedLengthResponse("Cookies!");
+        }
+    }
+
+    @Override
+    public CookieTestServer createTestServer() {
+        return new CookieTestServer();
+    }
+
+    @Test
+    public void testCookieSentBackToClient() throws Exception {
+        this.testServer.cookiesToSend.add(new NanoHTTPD.Cookie("name", "value", 30));
+        HttpGet httpget = new HttpGet("http://localhost:8192/");
+        ResponseHandler<String> responseHandler = new BasicResponseHandler();
+        this.httpclient.execute(httpget, responseHandler);
+
+        CookieStore cookies = this.httpclient.getCookieStore();
+        assertEquals(1, cookies.getCookies().size());
+        assertEquals("name", cookies.getCookies().get(0).getName());
+        assertEquals("value", cookies.getCookies().get(0).getValue());
+    }
+
     @Test
     public void testNoCookies() throws Exception {
         HttpGet httpget = new HttpGet("http://localhost:8192/");
         ResponseHandler<String> responseHandler = new BasicResponseHandler();
-        httpclient.execute(httpget, responseHandler);
+        this.httpclient.execute(httpget, responseHandler);
 
-        CookieStore cookies = httpclient.getCookieStore();
+        CookieStore cookies = this.httpclient.getCookieStore();
         assertEquals(0, cookies.getCookies().size());
     }
 
     @Test
-    public void testCookieSentBackToClient() throws Exception {
-        testServer.cookiesToSend.add(new NanoHTTPD.Cookie("name", "value", 30));
-        HttpGet httpget = new HttpGet("http://localhost:8192/");
-        ResponseHandler<String> responseHandler = new BasicResponseHandler();
-        httpclient.execute(httpget, responseHandler);
-
-        CookieStore cookies = httpclient.getCookieStore();
-        assertEquals(1, cookies.getCookies().size());
-        assertEquals("name", cookies.getCookies().get(0).getName());
-        assertEquals("value", cookies.getCookies().get(0).getValue());
-    }
-
-    @Test
     public void testServerReceivesCookiesSentFromClient() throws Exception {
         BasicClientCookie clientCookie = new BasicClientCookie("name", "value");
         Calendar calendar = Calendar.getInstance();
         calendar.add(Calendar.DAY_OF_YEAR, 100);
         clientCookie.setExpiryDate(calendar.getTime());
         clientCookie.setDomain("localhost");
-        httpclient.getCookieStore().addCookie(clientCookie);
+        this.httpclient.getCookieStore().addCookie(clientCookie);
         HttpGet httpget = new HttpGet("http://localhost:8192/");
         ResponseHandler<String> responseHandler = new BasicResponseHandler();
-        httpclient.execute(httpget, responseHandler);
+        this.httpclient.execute(httpget, responseHandler);
 
-        assertEquals(1, testServer.cookiesReceived.size());
-        assertTrue(testServer.cookiesReceived.get(0).getHTTPHeader().contains("name=value"));
-    }
-
-    @Override public CookieTestServer createTestServer() {
-        return new CookieTestServer();
-    }
-
-    public static class CookieTestServer extends NanoHTTPD {
-        List<Cookie> cookiesReceived = new ArrayList<Cookie>();
-        List<Cookie> cookiesToSend = new ArrayList<Cookie>();
-
-        public CookieTestServer() {
-            super(8192);
-        }
-
-        @Override public Response serve(IHTTPSession session) {
-            CookieHandler cookies = session.getCookies();
-            for (String cookieName : cookies) {
-                cookiesReceived.add(new Cookie(cookieName, cookies.read(cookieName)));
-            }
-            for (Cookie c : cookiesToSend) {
-                cookies.set(c);
-            }
-            return new Response("Cookies!");
-        }
+        assertEquals(1, this.testServer.cookiesReceived.size());
+        assertTrue(this.testServer.cookiesReceived.get(0).getHTTPHeader().contains("name=value"));
     }
 }
diff --git a/core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java b/core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java
new file mode 100644
index 0000000..a278406
--- /dev/null
+++ b/core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java
@@ -0,0 +1,168 @@
+package fi.iki.elonen.integration;
+
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DecompressingHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.junit.Test;
+
+import fi.iki.elonen.NanoHTTPD;
+
+public class GZipIntegrationTest extends IntegrationTestBase<GZipIntegrationTest.TestServer> {
+
+    public static class TestServer extends NanoHTTPD {
+
+        public Response response;
+
+        public TestServer() {
+            super(8192);
+        }
+
+        @Override
+        public Response serve(IHTTPSession session) {
+            return response;
+        }
+
+        @Override
+        protected boolean useGzipWhenAccepted(Response r) {
+            return true;
+        }
+    }
+
+    @Override
+    public TestServer createTestServer() {
+        return new TestServer();
+    }
+
+    @Test
+    public void contentEncodingShouldBeAddedToFixedLengthResponses() throws IOException {
+        testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test");
+        HttpGet request = new HttpGet("http://localhost:8192/");
+        request.addHeader("Accept-encoding", "gzip");
+        HttpResponse response = httpclient.execute(request);
+        Header contentEncoding = response.getFirstHeader("content-encoding");
+        assertNotNull("Content-Encoding should be set", contentEncoding);
+        assertEquals("gzip", contentEncoding.getValue());
+    }
+
+    @Test
+    public void contentEncodingShouldBeAddedToChunkedResponses() throws IOException {
+        InputStream data = new ByteArrayInputStream("This is a test".getBytes("UTF-8"));
+        testServer.response = NanoHTTPD.newChunkedResponse(NanoHTTPD.Response.Status.OK, "text/plain", data);
+        HttpGet request = new HttpGet("http://localhost:8192/");
+        request.addHeader("Accept-encoding", "gzip");
+        HttpResponse response = httpclient.execute(request);
+        Header contentEncoding = response.getFirstHeader("content-encoding");
+        assertNotNull("Content-Encoding should be set", contentEncoding);
+        assertEquals("gzip", contentEncoding.getValue());
+    }
+
+    @Test
+    public void shouldFindCorrectAcceptEncodingAmongMany() throws IOException {
+        testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test");
+        HttpGet request = new HttpGet("http://localhost:8192/");
+        request.addHeader("Accept-encoding", "deflate,gzip");
+        HttpResponse response = httpclient.execute(request);
+        Header contentEncoding = response.getFirstHeader("content-encoding");
+        assertNotNull("Content-Encoding should be set", contentEncoding);
+        assertEquals("gzip", contentEncoding.getValue());
+    }
+
+    @Test
+    public void contentLengthShouldBeRemovedFromZippedResponses() throws IOException {
+        testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test");
+        HttpGet request = new HttpGet("http://localhost:8192/");
+        request.addHeader("Accept-encoding", "gzip");
+        HttpResponse response = httpclient.execute(request);
+        Header contentLength = response.getFirstHeader("content-length");
+        assertNull("Content-Length should not be set when gzipping response", contentLength);
+    }
+
+    @Test
+    public void fixedLengthContentIsEncodedProperly() throws IOException {
+        testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test");
+        HttpGet request = new HttpGet("http://localhost:8192/");
+        request.addHeader("Accept-encoding", "gzip");
+        HttpResponse response = new DecompressingHttpClient(httpclient).execute(request);
+        assertEquals("This is a test", EntityUtils.toString(response.getEntity()));
+    }
+
+    @Test
+    public void chunkedContentIsEncodedProperly() throws IOException {
+        InputStream data = new ByteArrayInputStream("This is a test".getBytes("UTF-8"));
+        testServer.response = NanoHTTPD.newChunkedResponse(NanoHTTPD.Response.Status.OK, "text/plain", data);
+        HttpGet request = new HttpGet("http://localhost:8192/");
+        request.addHeader("Accept-encoding", "gzip");
+        HttpResponse response = new DecompressingHttpClient(httpclient).execute(request);
+        assertEquals("This is a test", EntityUtils.toString(response.getEntity()));
+    }
+
+    @Test
+    public void noGzipWithoutAcceptEncoding() throws IOException {
+        testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test");
+        HttpGet request = new HttpGet("http://localhost:8192/");
+        HttpResponse response = httpclient.execute(request);
+        Header contentEncoding = response.getFirstHeader("content-encoding");
+        assertThat(contentEncoding, is(nullValue()));
+        assertEquals("This is a test", EntityUtils.toString(response.getEntity()));
+    }
+
+    @Test
+    public void contentShouldNotBeGzippedIfContentLengthIsAddedManually() throws IOException {
+        testServer.response = NanoHTTPD.newFixedLengthResponse("This is a test");
+        testServer.response.addHeader("Content-Length", "" + ("This is a test".getBytes("UTF-8").length));
+        HttpGet request = new HttpGet("http://localhost:8192/");
+        request.addHeader("Accept-encoding", "gzip");
+        HttpResponse response = httpclient.execute(request);
+        Header contentEncoding = response.getFirstHeader("content-encoding");
+        assertNull("Content-Encoding should not be set when manually setting content-length", contentEncoding);
+        assertEquals("This is a test", EntityUtils.toString(response.getEntity()));
+
+    }
+
+}
diff --git a/core/src/test/java/fi/iki/elonen/integration/GetAndPostIntegrationTest.java b/core/src/test/java/fi/iki/elonen/integration/GetAndPostIntegrationTest.java
index bc0a9d9..eef2f22 100644
--- a/core/src/test/java/fi/iki/elonen/integration/GetAndPostIntegrationTest.java
+++ b/core/src/test/java/fi/iki/elonen/integration/GetAndPostIntegrationTest.java
@@ -1,8 +1,49 @@
 package fi.iki.elonen.integration;
 
-import fi.iki.elonen.NanoHTTPD;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static org.junit.Assert.assertEquals;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
 import org.apache.http.NameValuePair;
-import org.apache.http.client.HttpClient;
 import org.apache.http.client.ResponseHandler;
 import org.apache.http.client.entity.UrlEncodedFormEntity;
 import org.apache.http.client.methods.HttpGet;
@@ -11,96 +52,20 @@
 import org.apache.http.entity.mime.MultipartEntity;
 import org.apache.http.entity.mime.content.StringBody;
 import org.apache.http.impl.client.BasicResponseHandler;
-import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.http.message.BasicNameValuePair;
-import org.junit.After;
-import org.junit.Before;
+import org.apache.http.util.EntityUtils;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.Assert.assertEquals;
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.NanoHTTPD.Response.Status;
 
 /**
- * @author Paul S. Hawke (paul.hawke@gmail.com)
- *         On: 5/19/13 at 5:36 PM
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 5/19/13 at 5:36 PM
  */
 public class GetAndPostIntegrationTest extends IntegrationTestBase<GetAndPostIntegrationTest.TestServer> {
 
-    @Test
-    public void testSimpleGetRequest() throws Exception {
-        testServer.response = "testSimpleGetRequest";
-
-        HttpGet httpget = new HttpGet("http://localhost:8192/");
-        ResponseHandler<String> responseHandler = new BasicResponseHandler();
-        String responseBody = httpclient.execute(httpget, responseHandler);
-
-        assertEquals("GET:testSimpleGetRequest", responseBody);
-    }
-
-    @Test
-    public void testGetRequestWithParameters() throws Exception {
-        testServer.response = "testGetRequestWithParameters";
-
-        HttpGet httpget = new HttpGet("http://localhost:8192/?age=120&gender=Male");
-        ResponseHandler<String> responseHandler = new BasicResponseHandler();
-        String responseBody = httpclient.execute(httpget, responseHandler);
-
-        assertEquals("GET:testGetRequestWithParameters-params=2;age=120;gender=Male", responseBody);
-    }
-
-    @Test
-    public void testPostWithNoParameters() throws Exception {
-        testServer.response = "testPostWithNoParameters";
-
-        HttpPost httppost = new HttpPost("http://localhost:8192/");
-        ResponseHandler<String> responseHandler = new BasicResponseHandler();
-        String responseBody = httpclient.execute(httppost, responseHandler);
-
-        assertEquals("POST:testPostWithNoParameters", responseBody);
-    }
-
-    @Test
-    public void testPostRequestWithFormEncodedParameters() throws Exception {
-        testServer.response = "testPostRequestWithFormEncodedParameters";
-
-        HttpPost httppost = new HttpPost("http://localhost:8192/");
-        List<NameValuePair> postParameters = new ArrayList<NameValuePair>();
-        postParameters.add(new BasicNameValuePair("age", "120"));
-        postParameters.add(new BasicNameValuePair("gender", "Male"));
-        httppost.setEntity(new UrlEncodedFormEntity(postParameters));
-
-        ResponseHandler<String> responseHandler = new BasicResponseHandler();
-        String responseBody = httpclient.execute(httppost, responseHandler);
-
-        assertEquals("POST:testPostRequestWithFormEncodedParameters-params=2;age=120;gender=Male", responseBody);
-    }
-
-    @Test
-    public void testPostRequestWithMultipartEncodedParameters() throws Exception {
-        testServer.response = "testPostRequestWithMultipartEncodedParameters";
-
-        HttpPost httppost = new HttpPost("http://localhost:8192/");
-        MultipartEntity reqEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
-        reqEntity.addPart("age", new StringBody("120"));
-        reqEntity.addPart("gender", new StringBody("Male"));
-        httppost.setEntity(reqEntity);
-
-        ResponseHandler<String> responseHandler = new BasicResponseHandler();
-        String responseBody = httpclient.execute(httppost, responseHandler);
-
-        assertEquals("POST:testPostRequestWithMultipartEncodedParameters-params=2;age=120;gender=Male", responseBody);
-    }
-
-    @Override public TestServer createTestServer() {
-        return new TestServer();
-    }
-
     public static class TestServer extends NanoHTTPD {
+
         public String response;
 
         public TestServer() {
@@ -109,7 +74,7 @@
 
         @Override
         public Response serve(String uri, Method method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) {
-            StringBuilder sb = new StringBuilder(String.valueOf(method) + ':' + response);
+            StringBuilder sb = new StringBuilder(String.valueOf(method) + ':' + this.response);
 
             if (parms.size() > 1) {
                 parms.remove("NanoHttpd.QUERY_STRING");
@@ -120,8 +85,104 @@
                     sb.append(';').append(k).append('=').append(parms.get(k));
                 }
             }
-
-            return new Response(sb.toString());
+            if ("/chin".equals(uri)) {
+                return newFixedLengthResponse(Status.OK, "application/octet-stream", sb.toString());
+            } else {
+                return newFixedLengthResponse(sb.toString());
+            }
         }
     }
+
+    @Override
+    public TestServer createTestServer() {
+        return new TestServer();
+    }
+
+    @Test
+    public void testGetRequestWithParameters() throws Exception {
+        this.testServer.response = "testGetRequestWithParameters";
+
+        HttpGet httpget = new HttpGet("http://localhost:8192/?age=120&gender=Male");
+        ResponseHandler<String> responseHandler = new BasicResponseHandler();
+        String responseBody = this.httpclient.execute(httpget, responseHandler);
+
+        assertEquals("GET:testGetRequestWithParameters-params=2;age=120;gender=Male", responseBody);
+    }
+
+    @Test
+    public void testPostRequestWithFormEncodedParameters() throws Exception {
+        this.testServer.response = "testPostRequestWithFormEncodedParameters";
+
+        HttpPost httppost = new HttpPost("http://localhost:8192/");
+        List<NameValuePair> postParameters = new ArrayList<NameValuePair>();
+        postParameters.add(new BasicNameValuePair("age", "120"));
+        postParameters.add(new BasicNameValuePair("gender", "Male"));
+        httppost.setEntity(new UrlEncodedFormEntity(postParameters));
+
+        ResponseHandler<String> responseHandler = new BasicResponseHandler();
+        String responseBody = this.httpclient.execute(httppost, responseHandler);
+
+        assertEquals("POST:testPostRequestWithFormEncodedParameters-params=2;age=120;gender=Male", responseBody);
+    }
+
+    @Test
+    public void testPostRequestWithMultipartEncodedParameters() throws Exception {
+        this.testServer.response = "testPostRequestWithMultipartEncodedParameters";
+
+        HttpPost httppost = new HttpPost("http://localhost:8192/");
+        MultipartEntity reqEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
+        reqEntity.addPart("age", new StringBody("120"));
+        reqEntity.addPart("gender", new StringBody("Male"));
+        httppost.setEntity(reqEntity);
+
+        ResponseHandler<String> responseHandler = new BasicResponseHandler();
+        String responseBody = this.httpclient.execute(httppost, responseHandler);
+
+        assertEquals("POST:testPostRequestWithMultipartEncodedParameters-params=2;age=120;gender=Male", responseBody);
+    }
+
+    @Test
+    public void testPostWithNoParameters() throws Exception {
+        this.testServer.response = "testPostWithNoParameters";
+
+        HttpPost httppost = new HttpPost("http://localhost:8192/");
+        ResponseHandler<String> responseHandler = new BasicResponseHandler();
+        String responseBody = this.httpclient.execute(httppost, responseHandler);
+
+        assertEquals("POST:testPostWithNoParameters", responseBody);
+    }
+
+    @Test
+    public void testSimpleGetRequest() throws Exception {
+        this.testServer.response = "testSimpleGetRequest";
+
+        HttpGet httpget = new HttpGet("http://localhost:8192/");
+        ResponseHandler<String> responseHandler = new BasicResponseHandler();
+        String responseBody = this.httpclient.execute(httpget, responseHandler);
+
+        assertEquals("GET:testSimpleGetRequest", responseBody);
+    }
+
+    @Test
+    public void testPostRequestWithMultipartExtremEncodedParameters() throws Exception {
+        this.testServer.response = "testPostRequestWithMultipartEncodedParameters";
+
+        HttpPost httppost = new HttpPost("http://localhost:8192/chin");
+        MultipartEntity reqEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, "sfsadfasdf", Charset.forName("UTF-8"));
+        reqEntity.addPart("specialString", new StringBody("拖拉图片到浏览器,可以实现预览功能", "text/plain", Charset.forName("UTF-8")));
+        reqEntity.addPart("gender", new StringBody("图片名称", Charset.forName("UTF-8")) {
+
+            @Override
+            public String getFilename() {
+                return "图片名称";
+            }
+        });
+        httppost.setEntity(reqEntity);
+        HttpResponse response = this.httpclient.execute(httppost);
+
+        HttpEntity entity = response.getEntity();
+        String responseBody = EntityUtils.toString(entity, "UTF-8");
+
+        assertEquals("POST:testPostRequestWithMultipartEncodedParameters-params=2;gender=图片名称;specialString=拖拉图片到浏览器,可以实现预览功能", responseBody);
+    }
 }
diff --git a/core/src/test/java/fi/iki/elonen/integration/IntegrationTestBase.java b/core/src/test/java/fi/iki/elonen/integration/IntegrationTestBase.java
index eb34033..fd3d6d3 100644
--- a/core/src/test/java/fi/iki/elonen/integration/IntegrationTestBase.java
+++ b/core/src/test/java/fi/iki/elonen/integration/IntegrationTestBase.java
@@ -1,27 +1,63 @@
 package fi.iki.elonen.integration;
 
-import fi.iki.elonen.NanoHTTPD;
-import org.apache.http.client.HttpClient;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.IOException;
+
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.junit.After;
 import org.junit.Before;
 
-import java.io.IOException;
+import fi.iki.elonen.NanoHTTPD;
 
 /**
- * @author Paul S. Hawke (paul.hawke@gmail.com)
- *         On: 9/2/13 at 10:02 PM
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 9/2/13 at 10:02 PM
  */
 public abstract class IntegrationTestBase<T extends NanoHTTPD> {
+
     protected DefaultHttpClient httpclient;
+
     protected T testServer;
 
+    public abstract T createTestServer();
+
     @Before
     public void setUp() {
-        testServer = createTestServer();
-        httpclient = new DefaultHttpClient();
+        this.testServer = createTestServer();
+        this.httpclient = new DefaultHttpClient();
         try {
-            testServer.start();
+            this.testServer.start();
         } catch (IOException e) {
             e.printStackTrace();
         }
@@ -29,9 +65,7 @@
 
     @After
     public void tearDown() {
-        httpclient.getConnectionManager().shutdown();
-        testServer.stop();
+        this.httpclient.getConnectionManager().shutdown();
+        this.testServer.stop();
     }
-
-    public abstract T createTestServer();
 }
diff --git a/core/src/test/java/fi/iki/elonen/integration/PutStreamIntegrationTest.java b/core/src/test/java/fi/iki/elonen/integration/PutStreamIntegrationTest.java
index 1e260b2..41b84fd 100644
--- a/core/src/test/java/fi/iki/elonen/integration/PutStreamIntegrationTest.java
+++ b/core/src/test/java/fi/iki/elonen/integration/PutStreamIntegrationTest.java
@@ -1,5 +1,38 @@
 package fi.iki.elonen.integration;
 
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
 import static org.junit.Assert.assertEquals;
 
 import java.io.DataInputStream;
@@ -16,34 +49,13 @@
 
 public class PutStreamIntegrationTest extends IntegrationTestBase<PutStreamIntegrationTest.TestServer> {
 
-    @Test
-    public void testSimplePutRequest() throws Exception {
-        String expected = "This HttpPut request has a content-length of 48.";
-
-        HttpPut httpput = new HttpPut("http://localhost:8192/");
-        httpput.setEntity(new ByteArrayEntity(expected.getBytes()));
-        ResponseHandler<String> responseHandler = new BasicResponseHandler();
-        String responseBody = httpclient.execute(httpput, responseHandler);
-
-        assertEquals("PUT:" + expected, responseBody);
-    }
-
-    @Override public TestServer createTestServer() {
-        return new TestServer();
-    }
-
     public static class TestServer extends NanoHTTPD {
+
         public TestServer() {
             super(8192);
         }
 
         @Override
-        public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files)
-        {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
         public Response serve(IHTTPSession session) {
             Method method = session.getMethod();
             Map<String, String> headers = session.getHeaders();
@@ -54,13 +66,34 @@
                 DataInputStream dataInputStream = new DataInputStream(session.getInputStream());
                 body = new byte[contentLength];
                 dataInputStream.readFully(body, 0, contentLength);
-            }
-            catch(IOException e) {
-                return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, e.getMessage());
+            } catch (IOException e) {
+                return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, e.getMessage());
             }
 
             String response = String.valueOf(method) + ':' + new String(body);
-            return new Response(response);
+            return newFixedLengthResponse(response);
         }
+
+        @Override
+        public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public TestServer createTestServer() {
+        return new TestServer();
+    }
+
+    @Test
+    public void testSimplePutRequest() throws Exception {
+        String expected = "This HttpPut request has a content-length of 48.";
+
+        HttpPut httpput = new HttpPut("http://localhost:8192/");
+        httpput.setEntity(new ByteArrayEntity(expected.getBytes()));
+        ResponseHandler<String> responseHandler = new BasicResponseHandler();
+        String responseBody = this.httpclient.execute(httpput, responseHandler);
+
+        assertEquals("PUT:" + expected, responseBody);
     }
 }
diff --git a/core/src/test/java/fi/iki/elonen/integration/ShutdownTest.java b/core/src/test/java/fi/iki/elonen/integration/ShutdownTest.java
index 0fcb275..cd305ea 100644
--- a/core/src/test/java/fi/iki/elonen/integration/ShutdownTest.java
+++ b/core/src/test/java/fi/iki/elonen/integration/ShutdownTest.java
@@ -1,9 +1,39 @@
 package fi.iki.elonen.integration;
 
-import static org.junit.Assert.*;
-import fi.iki.elonen.NanoHTTPD;
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
 
-import org.junit.Test;
+import static org.junit.Assert.fail;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -11,8 +41,24 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 
+import org.junit.Test;
+
+import fi.iki.elonen.NanoHTTPD;
+
 public class ShutdownTest {
 
+    private class TestServer extends NanoHTTPD {
+
+        public TestServer() {
+            super(8092);
+        }
+
+        @Override
+        public Response serve(IHTTPSession session) {
+            return newFixedLengthResponse("Whatever");
+        }
+    }
+
     @Test
     public void connectionsAreClosedWhenServerStops() throws IOException {
         TestServer server = new TestServer();
@@ -38,16 +84,4 @@
         in.close();
     }
 
-    private class TestServer extends NanoHTTPD {
-
-        public TestServer() {
-            super(8092);
-        }
-
-        @Override
-        public Response serve(IHTTPSession session) {
-            return new Response("Whatever");
-        }
-    }
-
 }
diff --git a/core/src/test/resources/META-INF/nanohttpd/mimetypes.properties b/core/src/test/resources/META-INF/nanohttpd/mimetypes.properties
new file mode 100644
index 0000000..2f353d8
--- /dev/null
+++ b/core/src/test/resources/META-INF/nanohttpd/mimetypes.properties
@@ -0,0 +1,3 @@
+#test mime types for nanohttpd
+blabla=text/blabla
+ts=video/wrongOverwrite
\ No newline at end of file
diff --git a/core/src/test/resources/file-upload-test.htm b/core/src/test/resources/file-upload-test.htm
index 7d553bf..e64c516 100644
--- a/core/src/test/resources/file-upload-test.htm
+++ b/core/src/test/resources/file-upload-test.htm
@@ -1,3 +1,35 @@
+<!--
+  #%L
+  NanoHttpd-Core
+  %%
+  Copyright (C) 2012 - 2015 nanohttpd
+  %%
+  Redistribution and use in source and binary forms, with or without modification,
+  are permitted provided that the following conditions are met:
+  
+  1. Redistributions of source code must retain the above copyright notice, this
+     list of conditions and the following disclaimer.
+  
+  2. Redistributions in binary form must reproduce the above copyright notice,
+     this list of conditions and the following disclaimer in the documentation
+     and/or other materials provided with the distribution.
+  
+  3. Neither the name of the nanohttpd nor the names of its contributors
+     may be used to endorse or promote products derived from this software without
+     specific prior written permission.
+  
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+  OF THE POSSIBILITY OF SUCH DAMAGE.
+  #L%
+  -->
 <html>
  <body>
   <p>This is a file upload test for NanoHTTPD.</p>
diff --git a/core/src/test/resources/keystore.jks b/core/src/test/resources/keystore.jks
new file mode 100644
index 0000000..354b5d3
--- /dev/null
+++ b/core/src/test/resources/keystore.jks
Binary files differ
diff --git a/core/src/test/resources/multipart-form-test.htm b/core/src/test/resources/multipart-form-test.htm
index eab4dc7..8504475 100644
--- a/core/src/test/resources/multipart-form-test.htm
+++ b/core/src/test/resources/multipart-form-test.htm
@@ -1,3 +1,35 @@
+<!--
+  #%L
+  NanoHttpd-Core
+  %%
+  Copyright (C) 2012 - 2015 nanohttpd
+  %%
+  Redistribution and use in source and binary forms, with or without modification,
+  are permitted provided that the following conditions are met:
+  
+  1. Redistributions of source code must retain the above copyright notice, this
+     list of conditions and the following disclaimer.
+  
+  2. Redistributions in binary form must reproduce the above copyright notice,
+     this list of conditions and the following disclaimer in the documentation
+     and/or other materials provided with the distribution.
+  
+  3. Neither the name of the nanohttpd nor the names of its contributors
+     may be used to endorse or promote products derived from this software without
+     specific prior written permission.
+  
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+  OF THE POSSIBILITY OF SUCH DAMAGE.
+  #L%
+  -->
 <html>
  <body>
   <p>This is a multipart-form test for NanoHTTPD.</p>
diff --git a/fileupload/.gitignore b/fileupload/.gitignore
new file mode 100644
index 0000000..868a6b2
--- /dev/null
+++ b/fileupload/.gitignore
@@ -0,0 +1,2 @@
+/.settings/
+/LICENSE.txt
diff --git a/fileupload/pom.xml b/fileupload/pom.xml
new file mode 100644
index 0000000..a90469a
--- /dev/null
+++ b/fileupload/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<parent>
+		<artifactId>nanohttpd-project</artifactId>
+		<groupId>org.nanohttpd</groupId>
+		<version>2.2.0</version>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+	<artifactId>nanohttpd-apache-fileupload</artifactId>
+	<name>NanoHttpd-apache file upload integration</name>
+	<description>nanohttpd-apache-fileupload integrates the apache file upload framework into nanohttpd</description>
+	<dependencies>
+		<dependency>
+			<groupId>org.nanohttpd</groupId>
+			<artifactId>nanohttpd</artifactId>
+			<version>2.2.0</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>commons-fileupload</groupId>
+			<artifactId>commons-fileupload</artifactId>
+			<version>1.3.1</version>
+		</dependency>
+		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>servlet-api</artifactId>
+			<version>2.5</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+			<version>4.4.1</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpmime</artifactId>
+			<version>4.4.1</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<properties>
+		<minimal.coverage>0.99</minimal.coverage>
+	</properties>
+</project>
\ No newline at end of file
diff --git a/fileupload/src/main/java/fi/iki/elonen/NanoFileUpload.java b/fileupload/src/main/java/fi/iki/elonen/NanoFileUpload.java
new file mode 100644
index 0000000..ec02d4a
--- /dev/null
+++ b/fileupload/src/main/java/fi/iki/elonen/NanoFileUpload.java
@@ -0,0 +1,118 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * apache-fileupload-integration
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static fi.iki.elonen.NanoHTTPD.Method.POST;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileItemFactory;
+import org.apache.commons.fileupload.FileItemIterator;
+import org.apache.commons.fileupload.FileUpload;
+import org.apache.commons.fileupload.FileUploadBase;
+import org.apache.commons.fileupload.FileUploadException;
+import org.apache.commons.fileupload.UploadContext;
+
+/**
+ * @author victor & ritchieGitHub
+ */
+public class NanoFileUpload extends FileUpload {
+
+    public static class NanoHttpdContext implements UploadContext {
+
+        private NanoHTTPD.IHTTPSession session;
+
+        public NanoHttpdContext(NanoHTTPD.IHTTPSession session) {
+            this.session = session;
+        }
+
+        @Override
+        public long contentLength() {
+            long size;
+            try {
+                String cl1 = session.getHeaders().get("content-length");
+                size = Long.parseLong(cl1);
+            } catch (NumberFormatException var4) {
+                size = -1L;
+            }
+
+            return size;
+        }
+
+        @Override
+        public String getCharacterEncoding() {
+            return "UTF-8";
+        }
+
+        @Override
+        public String getContentType() {
+            return this.session.getHeaders().get("content-type");
+        }
+
+        @Override
+        public int getContentLength() {
+            return (int) contentLength();
+        }
+
+        @Override
+        public InputStream getInputStream() throws IOException {
+            return session.getInputStream();
+        }
+    }
+
+    public static final boolean isMultipartContent(NanoHTTPD.IHTTPSession session) {
+        return session.getMethod() == POST && FileUploadBase.isMultipartContent(new NanoHttpdContext(session));
+    }
+
+    public NanoFileUpload(FileItemFactory fileItemFactory) {
+        super(fileItemFactory);
+    }
+
+    public List<FileItem> parseRequest(NanoHTTPD.IHTTPSession session) throws FileUploadException {
+        return this.parseRequest(new NanoHttpdContext(session));
+    }
+
+    public Map<String, List<FileItem>> parseParameterMap(NanoHTTPD.IHTTPSession session) throws FileUploadException {
+        return this.parseParameterMap(new NanoHttpdContext(session));
+    }
+
+    public FileItemIterator getItemIterator(NanoHTTPD.IHTTPSession session) throws FileUploadException, IOException {
+        return super.getItemIterator(new NanoHttpdContext(session));
+    }
+
+}
diff --git a/fileupload/src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java b/fileupload/src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java
new file mode 100644
index 0000000..ac18e3a
--- /dev/null
+++ b/fileupload/src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java
@@ -0,0 +1,246 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-apache file upload integration
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileItemIterator;
+import org.apache.commons.fileupload.FileItemStream;
+import org.apache.commons.fileupload.FileUploadException;
+import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.apache.commons.fileupload.util.Streams;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.entity.mime.content.FileBody;
+import org.apache.http.entity.mime.content.StringBody;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.internal.runners.statements.Fail;
+
+import fi.iki.elonen.NanoHTTPD.Response.Status;
+
+/**
+ * very strange but if the file upload is the first request the test fails.
+ * 
+ * @author ritchieGitHub
+ */
+@FixMethodOrder
+public class TestNanoFileUpLoad {
+
+    protected TestServer testServer;
+
+    public static class TestServer extends NanoHTTPD {
+
+        public Response response = newFixedLengthResponse("");
+
+        public String uri;
+
+        public Method method;
+
+        public Map<String, String> header;
+
+        public Map<String, String> parms;
+
+        public Map<String, List<FileItem>> files;
+
+        public Map<String, List<String>> decodedParamters;
+
+        public Map<String, List<String>> decodedParamtersFromParameter;
+
+        public String queryParameterString;
+
+        public TestServer() {
+            super(8192);
+            uploader = new NanoFileUpload(new DiskFileItemFactory());
+        }
+
+        public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
+            return new HTTPSession(tempFileManager, inputStream, outputStream);
+        }
+
+        public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
+            return new HTTPSession(tempFileManager, inputStream, outputStream, inetAddress);
+        }
+
+        NanoFileUpload uploader;
+
+        @Override
+        public Response serve(IHTTPSession session) {
+
+            this.uri = session.getUri();
+            this.method = session.getMethod();
+            this.header = session.getHeaders();
+            this.parms = session.getParms();
+            if (NanoFileUpload.isMultipartContent(session)) {
+                try {
+                    if ("/uploadFile1".equals(this.uri)) {
+                        session.getHeaders().put("content-length", "AA");
+                        files = uploader.parseParameterMap(session);
+                    }
+                    if ("/uploadFile2".equals(this.uri)) {
+                        files = new HashMap<String, List<FileItem>>();
+                        List<FileItem> parseRequest = uploader.parseRequest(session);
+                        files.put(parseRequest.get(0).getFieldName(), parseRequest);
+                    }
+                    if ("/uploadFile3".equals(this.uri)) {
+                        files = new HashMap<String, List<FileItem>>();
+                        FileItemIterator iter = uploader.getItemIterator(session);
+                        while (iter.hasNext()) {
+                            FileItemStream item = iter.next();
+                            final String fileName = item.getName();
+                            FileItem fileItem = uploader.getFileItemFactory().createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName);
+                            files.put(fileItem.getFieldName(), Arrays.asList(new FileItem[]{
+                                fileItem
+                            }));
+                            try {
+                                Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
+                            } catch (Exception e) {
+                            }
+                            fileItem.setHeaders(item.getHeaders());
+                        }
+                    }
+                } catch (Exception e) {
+                    this.response.setStatus(Status.INTERNAL_ERROR);
+                    e.printStackTrace();
+                }
+            }
+            this.queryParameterString = session.getQueryParameterString();
+            this.decodedParamtersFromParameter = decodeParameters(this.queryParameterString);
+            this.decodedParamters = decodeParameters(session.getQueryParameterString());
+            return this.response;
+        }
+
+    }
+
+    @Test
+    public void testNormalRequest() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        HttpTrace httphead = new HttpTrace("http://localhost:8192/index.html");
+        CloseableHttpResponse response = httpclient.execute(httphead);
+        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+        response.close();
+    }
+
+    @Test
+    public void testPostWithMultipartFormUpload1() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        String textFileName = "src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java";
+        HttpPost post = new HttpPost("http://localhost:8192/uploadFile1");
+
+        executeUpload(httpclient, textFileName, post);
+        FileItem file = this.testServer.files.get("upfile").get(0);
+        Assert.assertEquals(file.getSize(), new File(textFileName).length());
+    }
+
+    @Test
+    public void testPostWithMultipartFormUpload2() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        String textFileName = "src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java";
+        HttpPost post = new HttpPost("http://localhost:8192/uploadFile2");
+
+        executeUpload(httpclient, textFileName, post);
+        FileItem file = this.testServer.files.get("upfile").get(0);
+        Assert.assertEquals(file.getSize(), new File(textFileName).length());
+    }
+
+    @Test
+    public void testPostWithMultipartFormUpload3() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        String textFileName = "src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java";
+        HttpPost post = new HttpPost("http://localhost:8192/uploadFile3");
+
+        executeUpload(httpclient, textFileName, post);
+        FileItem file = this.testServer.files.get("upfile").get(0);
+        Assert.assertEquals(file.getSize(), new File(textFileName).length());
+    }
+
+    private void executeUpload(CloseableHttpClient httpclient, String textFileName, HttpPost post) throws IOException, ClientProtocolException {
+        FileBody fileBody = new FileBody(new File(textFileName), ContentType.DEFAULT_BINARY);
+        StringBody stringBody1 = new StringBody("Message 1", ContentType.MULTIPART_FORM_DATA);
+
+        MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
+        builder.addPart("upfile", fileBody);
+        builder.addPart("text1", stringBody1);
+        HttpEntity entity = builder.build();
+        //
+        post.setEntity(entity);
+        HttpResponse response = httpclient.execute(post);
+        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+    }
+
+    @Before
+    public void setUp() throws IOException {
+        this.testServer = new TestServer();
+        this.testServer.start();
+        try {
+            long start = System.currentTimeMillis();
+            Thread.sleep(100L);
+            while (!this.testServer.wasStarted()) {
+                Thread.sleep(100L);
+                if (System.currentTimeMillis() - start > 2000) {
+                    Assert.fail("could not start server");
+                }
+            }
+        } catch (InterruptedException e) {
+        }
+    }
+
+    @After
+    public void tearDown() {
+        this.testServer.stop();
+    }
+
+}
diff --git a/markdown-plugin/.gitignore b/markdown-plugin/.gitignore
new file mode 100644
index 0000000..868a6b2
--- /dev/null
+++ b/markdown-plugin/.gitignore
@@ -0,0 +1,2 @@
+/.settings/
+/LICENSE.txt
diff --git a/markdown-plugin/pom.xml b/markdown-plugin/pom.xml
new file mode 100644
index 0000000..64518c1
--- /dev/null
+++ b/markdown-plugin/pom.xml
@@ -0,0 +1,62 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.nanohttpd</groupId>
+		<artifactId>nanohttpd-project</artifactId>
+		<version>2.2.0</version>
+	</parent>
+	<artifactId>nanohttpd-webserver-markdown-plugin</artifactId>
+	<packaging>jar</packaging>
+	<name>NanoHttpd-Webserver-Markdown-Plugin</name>
+	<url>https://github.com/NanoHttpd/nanohttpd</url>
+	<dependencies>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>nanohttpd</artifactId>
+			<version>${project.version}</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>nanohttpd-webserver</artifactId>
+			<version>${project.version}</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.pegdown</groupId>
+			<artifactId>pegdown</artifactId>
+			<version>1.4.1</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<version>2.2-beta-5</version>
+				<configuration>
+					<descriptorRefs>
+						<descriptorRef>jar-with-dependencies</descriptorRef>
+					</descriptorRefs>
+					<archive>
+						<manifest>
+							<mainClass>fi.iki.elonen.SimpleWebServer</mainClass>
+						</manifest>
+					</archive>
+				</configuration>
+				<executions>
+					<execution>
+						<phase>package</phase>
+						<goals>
+							<goal>single</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+	<properties>
+		<minimal.coverage>0.0</minimal.coverage>
+	</properties>
+</project>
diff --git a/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java b/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java
new file mode 100644
index 0000000..5144fea
--- /dev/null
+++ b/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java
@@ -0,0 +1,121 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver-Markdown-Plugin
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.pegdown.PegDownProcessor;
+
+/**
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 9/13/13 at 4:03 AM
+ */
+public class MarkdownWebServerPlugin implements WebServerPlugin {
+
+    /**
+     * logger to log to.
+     */
+    private static final Logger LOG = Logger.getLogger(MarkdownWebServerPlugin.class.getName());
+
+    private final PegDownProcessor processor;
+
+    public MarkdownWebServerPlugin() {
+        this.processor = new PegDownProcessor();
+    }
+
+    @Override
+    public boolean canServeUri(String uri, File rootDir) {
+        File f = new File(rootDir, uri);
+        return f.exists();
+    }
+
+    @Override
+    public void initialize(Map<String, String> commandLineOptions) {
+    }
+
+    private String readSource(File file) {
+        FileReader fileReader = null;
+        BufferedReader reader = null;
+        try {
+            fileReader = new FileReader(file);
+            reader = new BufferedReader(fileReader);
+            String line = null;
+            StringBuilder sb = new StringBuilder();
+            do {
+                line = reader.readLine();
+                if (line != null) {
+                    sb.append(line).append("\n");
+                }
+            } while (line != null);
+            reader.close();
+            return sb.toString();
+        } catch (Exception e) {
+            MarkdownWebServerPlugin.LOG.log(Level.SEVERE, "could not read source", e);
+            return null;
+        } finally {
+            try {
+                if (fileReader != null) {
+                    fileReader.close();
+                }
+                if (reader != null) {
+                    reader.close();
+                }
+            } catch (IOException ignored) {
+                MarkdownWebServerPlugin.LOG.log(Level.FINEST, "close failed", ignored);
+            }
+        }
+    }
+
+    @Override
+    public NanoHTTPD.Response serveFile(String uri, Map<String, String> headers, NanoHTTPD.IHTTPSession session, File file, String mimeType) {
+        String markdownSource = readSource(file);
+        byte[] bytes;
+        try {
+            bytes = this.processor.markdownToHtml(markdownSource).getBytes("UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            MarkdownWebServerPlugin.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e);
+            bytes = new byte[0];
+        }
+        return markdownSource == null ? null : new NanoHTTPD.Response(OK, NanoHTTPD.MIME_HTML, new ByteArrayInputStream(bytes), bytes.length);
+    }
+}
diff --git a/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java b/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java
new file mode 100644
index 0000000..29cdc1e
--- /dev/null
+++ b/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java
@@ -0,0 +1,59 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver-Markdown-Plugin
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+/**
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 9/13/13 at 4:01 AM
+ */
+public class MarkdownWebServerPluginInfo implements WebServerPluginInfo {
+
+    @Override
+    public String[] getIndexFilesForMimeType(String mime) {
+        return new String[]{
+            "index.md"
+        };
+    }
+
+    @Override
+    public String[] getMimeTypes() {
+        return new String[]{
+            "text/markdown"
+        };
+    }
+
+    @Override
+    public WebServerPlugin getWebServerPlugin(String mimeType) {
+        return new MarkdownWebServerPlugin();
+    }
+}
diff --git a/webserver/markdown-plugin/src/main/resources/META-INF/services/fi.iki.elonen.WebServerPluginInfo b/markdown-plugin/src/main/resources/META-INF/services/fi.iki.elonen.WebServerPluginInfo
similarity index 100%
rename from webserver/markdown-plugin/src/main/resources/META-INF/services/fi.iki.elonen.WebServerPluginInfo
rename to markdown-plugin/src/main/resources/META-INF/services/fi.iki.elonen.WebServerPluginInfo
diff --git a/markdown-plugin/src/site/site.xml b/markdown-plugin/src/site/site.xml
new file mode 100644
index 0000000..4270945
--- /dev/null
+++ b/markdown-plugin/src/site/site.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="${project.name}">
+	<skin>
+		<groupId>org.apache.maven.skins</groupId>
+		<artifactId>maven-fluido-skin</artifactId>
+		<version>1.3.0</version>
+	</skin>
+	<bannerLeft>
+		<src>../images/nanohttpd_logo.png</src>
+	</bannerLeft>
+	<bannerRight>
+		<src>../images/nanohttpd_logo_text.png</src>
+	</bannerRight>
+	<publishDate position="left" format="yyyy-MM-dd" />
+	<version position="right" />
+	<poweredBy>
+		<logo name="Maven" href="http://maven.apache.org/"
+			img="http://maven.apache.org/images/logos/maven-feather.png" />
+	</poweredBy>
+	<custom>
+		<fluidoSkin>
+			<topBarEnabled>false</topBarEnabled>
+			<sideBarEnabled>true</sideBarEnabled>
+			<gitHub>
+				<projectId>Nanohttpd/nanohttpd</projectId>
+				<ribbonOrientation>right</ribbonOrientation>
+				<ribbonColor>black</ribbonColor>
+			</gitHub>
+		</fluidoSkin>
+	</custom>
+	<body>
+		<breadcrumbs>
+			<item name="${project.name}" href="index.html" />
+		</breadcrumbs>
+		<menu name="Documentation">
+			<item name="About" href="index.html" />
+		</menu>
+		<menu ref="modules" />
+		<menu ref="reports" />
+	</body>
+</project>
\ No newline at end of file
diff --git a/nanolets/.settings/org.eclipse.core.resources.prefs b/nanolets/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..29abf99
--- /dev/null
+++ b/nanolets/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
+encoding/<project>=UTF-8
diff --git a/nanolets/.settings/org.eclipse.jdt.core.prefs b/nanolets/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..60105c1
--- /dev/null
+++ b/nanolets/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,5 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/nanolets/.settings/org.eclipse.m2e.core.prefs b/nanolets/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/nanolets/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/nanolets/LICENSE.txt b/nanolets/LICENSE.txt
new file mode 100644
index 0000000..fcebfe1
--- /dev/null
+++ b/nanolets/LICENSE.txt
@@ -0,0 +1,26 @@
+Copyright (c) 2012 - 2015, nanohttpd
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the nanohttpd nor the names of its contributors
+   may be used to endorse or promote products derived from this software without
+   specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/nanolets/pom.xml b/nanolets/pom.xml
new file mode 100644
index 0000000..6750728
--- /dev/null
+++ b/nanolets/pom.xml
@@ -0,0 +1,28 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.nanohttpd</groupId>
+		<artifactId>nanohttpd-project</artifactId>
+		<version>2.2.0</version>
+	</parent>
+	<artifactId>nanohttpd-nanolets</artifactId>
+	<packaging>jar</packaging>
+	<name>NanoHttpd-nano application server</name>
+	<description>nanohttpd-nanolets add a very easy to use version of servlets into nanohttpd.</description>
+	<dependencies>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>nanohttpd</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+			<version>4.4.1</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<properties>
+		<minimal.coverage>0.99</minimal.coverage>
+	</properties>
+</project>
diff --git a/nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java b/nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java
new file mode 100644
index 0000000..11f5d92
--- /dev/null
+++ b/nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java
@@ -0,0 +1,560 @@
+package fi.iki.elonen.router;
+
+/*
+ * #%L
+ * NanoHttpd-Samples
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.NanoHTTPD.Response.IStatus;
+import fi.iki.elonen.NanoHTTPD.Response.Status;
+
+/**
+ * @author vnnv
+ * @author ritchieGitHub
+ */
+public class RouterNanoHTTPD extends NanoHTTPD {
+
+    /**
+     * logger to log to.
+     */
+    private static final Logger LOG = Logger.getLogger(RouterNanoHTTPD.class.getName());
+
+    public interface UriResponder {
+
+        public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
+
+        public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
+
+        public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
+
+        public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
+
+        public Response other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
+    }
+
+    /**
+     * General nanolet to inherit from if you provide stream data, only chucked
+     * responses will be generated.
+     */
+    public static abstract class DefaultStreamHandler implements UriResponder {
+
+        public abstract String getMimeType();
+
+        public abstract IStatus getStatus();
+
+        public abstract InputStream getData();
+
+        public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+            return NanoHTTPD.newChunkedResponse(getStatus(), getMimeType(), getData());
+        }
+
+        public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+            return get(uriResource, urlParams, session);
+        }
+
+        public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+            return get(uriResource, urlParams, session);
+        }
+
+        public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+            return get(uriResource, urlParams, session);
+        }
+
+        public Response other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+            return get(uriResource, urlParams, session);
+        }
+    }
+
+    /**
+     * General nanolet to inherit from if you provide text or html data, only
+     * fixed size responses will be generated.
+     */
+    public static abstract class DefaultHandler extends DefaultStreamHandler {
+
+        public abstract String getText();
+
+        public abstract IStatus getStatus();
+
+        public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+            return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), getText());
+        }
+
+        @Override
+        public InputStream getData() {
+            throw new IllegalStateException("this method should not be called in a text based nanolet");
+        }
+    }
+
+    /**
+     * General nanolet to print debug info's as a html page.
+     */
+    public static class GeneralHandler extends DefaultHandler {
+
+        @Override
+        public String getText() {
+            throw new IllegalStateException("this method should not be called");
+        }
+
+        @Override
+        public String getMimeType() {
+            return "text/html";
+        }
+
+        @Override
+        public IStatus getStatus() {
+            return Status.OK;
+        }
+
+        public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+            StringBuilder text = new StringBuilder("<html><body>");
+            text.append("<h1>Url: ");
+            text.append(session.getUri());
+            text.append("</h1><br>");
+            Map<String, String> queryParams = session.getParms();
+            if (queryParams.size() > 0) {
+                for (Map.Entry<String, String> entry : queryParams.entrySet()) {
+                    String key = entry.getKey();
+                    String value = entry.getValue();
+                    text.append("<p>Param '");
+                    text.append(key);
+                    text.append("' = ");
+                    text.append(value);
+                    text.append("</p>");
+                }
+            } else {
+                text.append("<p>no params in url</p><br>");
+            }
+            return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), text.toString());
+        }
+    }
+
+    /**
+     * General nanolet to print debug info's as a html page.
+     */
+    public static class StaticPageHandler extends DefaultHandler {
+
+        private static String[] getPathArray(String uri) {
+            String array[] = uri.split("/");
+            ArrayList<String> pathArray = new ArrayList<String>();
+
+            for (String s : array) {
+                if (s.length() > 0)
+                    pathArray.add(s);
+            }
+
+            return pathArray.toArray(new String[]{});
+
+        }
+
+        @Override
+        public String getText() {
+            throw new IllegalStateException("this method should not be called");
+        }
+
+        @Override
+        public String getMimeType() {
+            throw new IllegalStateException("this method should not be called");
+        }
+
+        @Override
+        public IStatus getStatus() {
+            return Status.OK;
+        }
+
+        public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+            String baseUri = uriResource.getUri();
+            String realUri = normalizeUri(session.getUri());
+            for (int index = 0; index < Math.min(baseUri.length(), realUri.length()); index++) {
+                if (baseUri.charAt(index) != realUri.charAt(index)) {
+                    realUri = normalizeUri(realUri.substring(index));
+                    break;
+                }
+            }
+            File fileOrdirectory = uriResource.initParameter(File.class);
+            for (String pathPart : getPathArray(realUri)) {
+                fileOrdirectory = new File(fileOrdirectory, pathPart);
+            }
+            if (fileOrdirectory.isDirectory()) {
+                fileOrdirectory = new File(fileOrdirectory, "index.html");
+                if (!fileOrdirectory.exists()) {
+                    fileOrdirectory = new File(fileOrdirectory.getParentFile(), "index.htm");
+                }
+            }
+            if (!fileOrdirectory.exists() || !fileOrdirectory.isFile()) {
+                return new Error404UriHandler().get(uriResource, urlParams, session);
+            } else {
+                try {
+                    return NanoHTTPD.newChunkedResponse(getStatus(), getMimeTypeForFile(fileOrdirectory.getName()), fileToInputStream(fileOrdirectory));
+                } catch (IOException ioe) {
+                    return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.REQUEST_TIMEOUT, "text/plain", null);
+                }
+            }
+        }
+
+        protected BufferedInputStream fileToInputStream(File fileOrdirectory) throws IOException {
+            return new BufferedInputStream(new FileInputStream(fileOrdirectory));
+        }
+    }
+
+    /**
+     * Handling error 404 - unrecognized urls
+     */
+    public static class Error404UriHandler extends DefaultHandler {
+
+        public String getText() {
+            return "<html><body><h3>Error 404: the requested page doesn't exist.</h3></body></html>";
+        }
+
+        @Override
+        public String getMimeType() {
+            return "text/html";
+        }
+
+        @Override
+        public IStatus getStatus() {
+            return Status.NOT_FOUND;
+        }
+    }
+
+    /**
+     * Handling index
+     */
+    public static class IndexHandler extends DefaultHandler {
+
+        public String getText() {
+            return "<html><body><h2>Hello world!</h3></body></html>";
+        }
+
+        @Override
+        public String getMimeType() {
+            return "text/html";
+        }
+
+        @Override
+        public IStatus getStatus() {
+            return Status.OK;
+        }
+
+    }
+
+    public static class NotImplementedHandler extends DefaultHandler {
+
+        public String getText() {
+            return "<html><body><h2>The uri is mapped in the router, but no handler is specified. <br> Status: Not implemented!</h3></body></html>";
+        }
+
+        @Override
+        public String getMimeType() {
+            return "text/html";
+        }
+
+        @Override
+        public IStatus getStatus() {
+            return Status.OK;
+        }
+    }
+
+    public static String normalizeUri(String value) {
+        if (value == null) {
+            return value;
+        }
+        if (value.startsWith("/")) {
+            value = value.substring(1);
+        }
+        if (value.endsWith("/")) {
+            value = value.substring(0, value.length() - 1);
+        }
+        return value;
+
+    }
+
+    public static class UriResource {
+
+        private static final Pattern PARAM_PATTERN = Pattern.compile("(?<=(^|/)):[a-zA-Z0-9_-]+(?=(/|$))");
+
+        private static final String PARAM_MATCHER = "([A-Za-z0-9\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=]+)";
+
+        private static final Map<String, String> EMPTY = Collections.unmodifiableMap(new HashMap<String, String>());
+
+        private final String uri;
+
+        private final Pattern uriPattern;
+
+        private final int priority;
+
+        private final Class<?> handler;
+
+        private final Object[] initParameter;
+
+        private List<String> uriParams = new ArrayList<String>();
+
+        public UriResource(String uri, int priority, Class<?> handler, Object... initParameter) {
+            this.handler = handler;
+            this.initParameter = initParameter;
+            if (uri != null) {
+                this.uri = normalizeUri(uri);
+                parse();
+                this.uriPattern = createUriPattern();
+            } else {
+                this.uriPattern = null;
+                this.uri = null;
+            }
+            this.priority = priority + uriParams.size() * 1000;
+        }
+
+        private void parse() {
+        }
+
+        private Pattern createUriPattern() {
+            String patternUri = uri;
+            Matcher matcher = PARAM_PATTERN.matcher(patternUri);
+            int start = 0;
+            while (matcher.find(start)) {
+                uriParams.add(patternUri.substring(matcher.start() + 1, matcher.end()));
+                patternUri = new StringBuilder(patternUri.substring(0, matcher.start()))//
+                        .append(PARAM_MATCHER)//
+                        .append(patternUri.substring(matcher.end())).toString();
+                start = matcher.start() + PARAM_MATCHER.length();
+                matcher = PARAM_PATTERN.matcher(patternUri);
+            }
+            return Pattern.compile(patternUri);
+        }
+
+        public Response process(Map<String, String> urlParams, IHTTPSession session) {
+            String error = "General error!";
+            if (handler != null) {
+                try {
+                    Object object = handler.newInstance();
+                    if (object instanceof UriResponder) {
+                        UriResponder responder = (UriResponder) object;
+                        switch (session.getMethod()) {
+                            case GET:
+                                return responder.get(this, urlParams, session);
+                            case POST:
+                                return responder.post(this, urlParams, session);
+                            case PUT:
+                                return responder.put(this, urlParams, session);
+                            case DELETE:
+                                return responder.delete(this, urlParams, session);
+                            default:
+                                return responder.other(session.getMethod().toString(), this, urlParams, session);
+                        }
+                    } else {
+                        return NanoHTTPD.newFixedLengthResponse(Status.OK, "text/plain", //
+                                new StringBuilder("Return: ")//
+                                        .append(handler.getCanonicalName())//
+                                        .append(".toString() -> ")//
+                                        .append(object)//
+                                        .toString());
+                    }
+                } catch (Exception e) {
+                    error = "Error: " + e.getClass().getName() + " : " + e.getMessage();
+                    LOG.log(Level.SEVERE, error, e);
+                }
+            }
+            return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, "text/plain", error);
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder("UrlResource{uri='").append((uri == null ? "/" : uri))//
+                    .append("', urlParts=").append(uriParams)//
+                    .append('}')//
+                    .toString();
+        }
+
+        public String getUri() {
+            return uri;
+        }
+
+        public <T> T initParameter(Class<T> paramClazz) {
+            return initParameter(0, paramClazz);
+        }
+
+        public <T> T initParameter(int parameterIndex, Class<T> paramClazz) {
+            if (initParameter.length > parameterIndex) {
+                return paramClazz.cast(initParameter[parameterIndex]);
+            }
+            LOG.severe("init parameter index not available " + parameterIndex);
+            return null;
+        }
+
+        public Map<String, String> match(String url) {
+            Matcher matcher = uriPattern.matcher(url);
+            if (matcher.matches()) {
+                if (uriParams.size() > 0) {
+                    Map<String, String> result = new HashMap<String, String>();
+                    for (int i = 1; i <= matcher.groupCount(); i++) {
+                        result.put(uriParams.get(i - 1), matcher.group(i));
+                    }
+                    return result;
+                } else {
+                    return EMPTY;
+                }
+            }
+            return null;
+        }
+
+    }
+
+    public static class UriRouter {
+
+        private List<UriResource> mappings;
+
+        private UriResource error404Url;
+
+        private Class<?> notImplemented;
+
+        public UriRouter() {
+            mappings = new ArrayList<UriResource>();
+        }
+
+        /**
+         * Search in the mappings if the given url matches some of the rules If
+         * there are more than one marches returns the rule with less parameters
+         * e.g. mapping 1 = /user/:id mapping 2 = /user/help if the incoming uri
+         * is www.example.com/user/help - mapping 2 is returned if the incoming
+         * uri is www.example.com/user/3232 - mapping 1 is returned
+         * 
+         * @param url
+         * @return
+         */
+        public Response process(IHTTPSession session) {
+            String work = normalizeUri(session.getUri());
+            Map<String, String> params = null;
+            UriResource uriResource = error404Url;
+            for (UriResource u : mappings) {
+                params = u.match(work);
+                if (params != null) {
+                    uriResource = u;
+                    break;
+                }
+            }
+            return uriResource.process(params, session);
+        }
+
+        private void addRoute(String url, int priority, Class<?> handler, Object... initParameter) {
+            if (url != null) {
+                if (handler != null) {
+                    mappings.add(new UriResource(url, priority + mappings.size(), handler, initParameter));
+                } else {
+                    mappings.add(new UriResource(url, priority + mappings.size(), notImplemented));
+                }
+                sortMappings();
+            }
+        }
+
+        private void sortMappings() {
+            Collections.sort(mappings, new Comparator<UriResource>() {
+
+                @Override
+                public int compare(UriResource o1, UriResource o2) {
+                    return o1.priority - o2.priority;
+                }
+            });
+        }
+
+        private void removeRoute(String url) {
+            String uriToDelete = normalizeUri(url);
+            Iterator<UriResource> iter = mappings.iterator();
+            while (iter.hasNext()) {
+                UriResource uriResource = iter.next();
+                if (uriToDelete.equals(uriResource.getUri())) {
+                    iter.remove();
+                    break;
+                }
+            }
+        }
+
+        public void setNotFoundHandler(Class<?> handler) {
+            error404Url = new UriResource(null, 100, handler);
+        }
+
+        public void setNotImplemented(Class<?> handler) {
+            notImplemented = handler;
+        }
+
+    }
+
+    private UriRouter router;
+
+    public RouterNanoHTTPD(int port) {
+        super(port);
+        router = new UriRouter();
+    }
+
+    /**
+     * default routings, they are over writable.
+     * 
+     * <pre>
+     * router.setNotFoundHandler(GeneralHandler.class);
+     * </pre>
+     */
+
+    public void addMappings() {
+        router.setNotImplemented(NotImplementedHandler.class);
+        router.setNotFoundHandler(Error404UriHandler.class);
+        router.addRoute("/", Integer.MAX_VALUE / 2, IndexHandler.class);
+        router.addRoute("/index.html", Integer.MAX_VALUE / 2, IndexHandler.class);
+    }
+
+    public void addRoute(String url, Class<?> handler, Object... initParameter) {
+        router.addRoute(url, 100, handler, initParameter);
+    }
+
+    public void removeRoute(String url) {
+        router.removeRoute(url);
+    }
+
+    @Override
+    public Response serve(IHTTPSession session) {
+        // Try to find match
+        return router.process(session);
+    }
+}
diff --git a/nanolets/src/test/java/fi/iki/elonen/router/AppNanolets.java b/nanolets/src/test/java/fi/iki/elonen/router/AppNanolets.java
new file mode 100644
index 0000000..2d96eb8
--- /dev/null
+++ b/nanolets/src/test/java/fi/iki/elonen/router/AppNanolets.java
@@ -0,0 +1,173 @@
+package fi.iki.elonen.router;
+
+/*
+ * #%L
+ * NanoHttpd-Samples
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+/**
+ * Created by vnnv on 7/17/15.
+ * Simple httpd server based on NanoHTTPD
+ * Read the source. Everything is there.
+ */
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.NanoHTTPD.Response.IStatus;
+import fi.iki.elonen.NanoHTTPD.Response.Status;
+import fi.iki.elonen.util.ServerRunner;
+
+public class AppNanolets extends RouterNanoHTTPD {
+
+    private static final int PORT = 9090;
+
+    public static class UserHandler extends DefaultHandler {
+
+        @Override
+        public String getText() {
+            return "not implemented";
+        }
+
+        public String getText(Map<String, String> urlParams, NanoHTTPD.IHTTPSession session) {
+            String text = "<html><body>User handler. Method: " + session.getMethod().toString() + "<br>";
+            text += "<h1>Uri parameters:</h1>";
+            for (Map.Entry<String, String> entry : urlParams.entrySet()) {
+                String key = entry.getKey();
+                String value = entry.getValue();
+                text += "<div> Param: " + key + "&nbsp;Value: " + value + "</div>";
+            }
+            text += "<h1>Query parameters:</h1>";
+            for (Map.Entry<String, String> entry : session.getParms().entrySet()) {
+                String key = entry.getKey();
+                String value = entry.getValue();
+                text += "<div> Query Param: " + key + "&nbsp;Value: " + value + "</div>";
+            }
+            text += "</body></html>";
+
+            return text;
+        }
+
+        @Override
+        public String getMimeType() {
+            return "text/html";
+        }
+
+        @Override
+        public NanoHTTPD.Response.IStatus getStatus() {
+            return NanoHTTPD.Response.Status.OK;
+        }
+
+        public NanoHTTPD.Response get(UriResource uriResource, Map<String, String> urlParams, NanoHTTPD.IHTTPSession session) {
+            String text = getText(urlParams, session);
+            ByteArrayInputStream inp = new ByteArrayInputStream(text.getBytes());
+            int size = text.getBytes().length;
+            return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), inp, size);
+        }
+
+    }
+
+    static public class StreamUrl extends DefaultStreamHandler {
+
+        @Override
+        public String getMimeType() {
+            return "text/plain";
+        }
+
+        @Override
+        public IStatus getStatus() {
+            return Status.OK;
+        }
+
+        @Override
+        public InputStream getData() {
+            return new ByteArrayInputStream("a stream of data ;-)".getBytes());
+        }
+
+    }
+
+    static class StaticPageTestHandler extends StaticPageHandler {
+
+        @Override
+        protected BufferedInputStream fileToInputStream(File fileOrdirectory) throws IOException {
+            if (fileOrdirectory.getName().equals("exception.html")) {
+                throw new IOException("trigger something wrong");
+            }
+            return super.fileToInputStream(fileOrdirectory);
+        }
+    }
+
+    /**
+     * Create the server instance
+     */
+    public AppNanolets() throws IOException {
+        super(PORT);
+        addMappings();
+        System.out.println("\nRunning! Point your browers to http://localhost:" + PORT + "/ \n");
+    }
+
+    /**
+     * Add the routes Every route is an absolute path Parameters starts with ":"
+     * Handler class should implement @UriResponder interface If the handler not
+     * implement UriResponder interface - toString() is used
+     */
+    @Override
+    public void addMappings() {
+        super.addMappings();
+        addRoute("/user", UserHandler.class);
+        addRoute("/user/:id", UserHandler.class);
+        addRoute("/user/help", GeneralHandler.class);
+        addRoute("/general/:param1/:param2", GeneralHandler.class);
+        addRoute("/photos/:customer_id/:photo_id", null);
+        addRoute("/test", String.class);
+        addRoute("/interface", UriResponder.class); // this will cause an error
+                                                    // when called
+        addRoute("/toBeDeleted", String.class);
+        removeRoute("/toBeDeleted");
+        addRoute("/stream", StreamUrl.class);
+        addRoute("/browse/(.)+", StaticPageTestHandler.class, new File("src/test/resources").getAbsoluteFile());
+    }
+
+    /**
+     * Main entry point
+     * 
+     * @param args
+     */
+    public static void main(String[] args) {
+        ServerRunner.run(AppNanolets.class);
+    }
+}
diff --git a/nanolets/src/test/java/fi/iki/elonen/router/TestNanolets.java b/nanolets/src/test/java/fi/iki/elonen/router/TestNanolets.java
new file mode 100644
index 0000000..7b1ae56
--- /dev/null
+++ b/nanolets/src/test/java/fi/iki/elonen/router/TestNanolets.java
@@ -0,0 +1,375 @@
+package fi.iki.elonen.router;
+
+/*
+ * #%L
+ * NanoHttpd nano application server
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.router.RouterNanoHTTPD.GeneralHandler;
+import fi.iki.elonen.router.RouterNanoHTTPD.UriResource;
+
+public class TestNanolets {
+
+    private static PipedOutputStream stdIn;
+
+    private static Thread serverStartThread;
+
+    @BeforeClass
+    public static void setUp() throws Exception {
+        stdIn = new PipedOutputStream();
+        System.setIn(new PipedInputStream(stdIn));
+        serverStartThread = new Thread(new Runnable() {
+
+            @Override
+            public void run() {
+                String[] args = {};
+                AppNanolets.main(args);
+            }
+        });
+        serverStartThread.start();
+        // give the server some tine to start.
+        Thread.sleep(200);
+    }
+
+    public static void main(String[] args) {
+        {
+            String uri = "def";
+            Pattern.compile("([A-Za-z0-9\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=]+)");
+            Pattern URI_PATTERN = Pattern.compile("([A-Za-z0-9\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=]+)");
+            System.out.println(URI_PATTERN.matcher(uri).matches());
+        }
+
+        String uri = "photos/abc/def";
+        Pattern URI_PATTERN = Pattern.compile("photos/([A-Za-z0-9\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=]+)/([A-Za-z0-9\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=]+)");
+        Matcher matcher = URI_PATTERN.matcher(uri);
+        System.out.println("--------------->" + "/" + uri);
+        while (matcher.matches()) {
+
+            System.out.println(matcher.group());
+        }
+        // for (int index = 0; index < matcher.groupCount(); index++) {
+        // System.out.println(matcher.group());
+        // }
+    }
+
+    @Test
+    public void doSomeBasicMethodTest() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpGet httpget = new HttpGet("http://localhost:9090/user/blabla");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals(
+                "<html><body>User handler. Method: GET<br><h1>Uri parameters:</h1><div> Param: id&nbsp;Value: blabla</div><h1>Query parameters:</h1></body></html>", string);
+        response.close();
+
+        HttpPost httppost = new HttpPost("http://localhost:9090/user/blabla");
+        response = httpclient.execute(httppost);
+        entity = response.getEntity();
+        string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals(
+                "<html><body>User handler. Method: POST<br><h1>Uri parameters:</h1><div> Param: id&nbsp;Value: blabla</div><h1>Query parameters:</h1></body></html>", string);
+        response.close();
+
+        HttpPut httpgput = new HttpPut("http://localhost:9090/user/blabla");
+        response = httpclient.execute(httpgput);
+        entity = response.getEntity();
+        string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals(
+                "<html><body>User handler. Method: PUT<br><h1>Uri parameters:</h1><div> Param: id&nbsp;Value: blabla</div><h1>Query parameters:</h1></body></html>", string);
+        response.close();
+
+        HttpDelete httpdelete = new HttpDelete("http://localhost:9090/user/blabla");
+        response = httpclient.execute(httpdelete);
+        entity = response.getEntity();
+        string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals(
+                "<html><body>User handler. Method: DELETE<br><h1>Uri parameters:</h1><div> Param: id&nbsp;Value: blabla</div><h1>Query parameters:</h1></body></html>", string);
+        response.close();
+    }
+
+    @Test
+    public void doNonRouterRequest() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpGet httpget = new HttpGet("http://localhost:9090/test");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("Return: java.lang.String.toString() -> ", string);
+        response.close();
+    }
+
+    @Test
+    public void doExceptionRequest() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpGet httpget = new HttpGet("http://localhost:9090/interface");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("Error: java.lang.InstantiationException : fi.iki.elonen.router.RouterNanoHTTPD$UriResponder", string);
+        response.close();
+    }
+
+    @Test
+    public void doDeletedRoute() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpGet httpget = new HttpGet("http://localhost:9090/toBeDeleted");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h3>Error 404: the requested page doesn't exist.</h3></body></html>", string);
+        response.close();
+    }
+
+    @Test
+    public void doUriSelection1() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpGet httpget = new HttpGet("http://localhost:9090/user/help");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h1>Url: /user/help</h1><br><p>no params in url</p><br>", string);
+        response.close();
+    }
+
+    @Test
+    public void doStreamOfData() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpGet httpget = new HttpGet("http://localhost:9090/stream");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("a stream of data ;-)", string);
+        response.close();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void illegalMethod1() throws Exception {
+        new AppNanolets.UserHandler().getData();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void illegalMethod2() throws Exception {
+        new RouterNanoHTTPD.GeneralHandler().getText();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void illegalMethod3() throws Exception {
+        new RouterNanoHTTPD.StaticPageHandler().getText();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void illegalMethod4() throws Exception {
+        new RouterNanoHTTPD.StaticPageHandler().getMimeType();
+    }
+
+    @Test(expected = ClassCastException.class)
+    public void checkIniParameter1() throws Exception {
+        new RouterNanoHTTPD.UriResource("browse", 100, null, "init").initParameter(String.class);
+        new RouterNanoHTTPD.UriResource("browse", 100, null, "init").initParameter(Integer.class);
+    }
+
+    @Test
+    public void checkIniParameter2() throws Exception {
+        Assert.assertEquals("init", new RouterNanoHTTPD.UriResource("browse", 100, null, "init").initParameter(String.class));
+        Assert.assertNull(new RouterNanoHTTPD.UriResource("browse", 100, null).initParameter(String.class));
+    }
+
+    @Test
+    public void doGeneralParams() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpGet httpget = new HttpGet("http://localhost:9090/general/value1/value2?param3=value3&param4=value4");
+
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h1>Url: /general/value1/value2</h1><br><p>Param 'param3' = value3</p><p>Param 'param4' = value4</p>", string);
+        response.close();
+    }
+
+    @Test
+    public void doIndexHandler() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpGet httpget = new HttpGet("http://localhost:9090/index.html");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h2>Hello world!</h3></body></html>", string);
+        response.close();
+    }
+
+    @Test
+    public void doMissingHandler() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpGet httpget = new HttpGet("http://localhost:9090/photos/abc/def");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h2>The uri is mapped in the router, but no handler is specified. <br> Status: Not implemented!</h3></body></html>", string);
+        response.close();
+    }
+
+    @Test
+    public void uriToString() throws Exception {
+        Assert.assertEquals(//
+                "UrlResource{uri='photos/:customer_id/:photo_id', urlParts=[customer_id, photo_id]}",//
+                new UriResource("/photos/:customer_id/:photo_id", 100, GeneralHandler.class).toString());
+    }
+
+    @Test
+    public void doOtherMethod() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpTrace httphead = new HttpTrace("http://localhost:9090/index.html");
+        CloseableHttpResponse response = httpclient.execute(httphead);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h2>Hello world!</h3></body></html>", string);
+        response.close();
+    }
+
+    @Test
+    public void normalize() throws Exception {
+        Assert.assertNull(RouterNanoHTTPD.normalizeUri(null));
+        Assert.assertEquals("", RouterNanoHTTPD.normalizeUri("/"));
+        Assert.assertEquals("xxx/yyy", RouterNanoHTTPD.normalizeUri("/xxx/yyy"));
+        Assert.assertEquals("xxx/yyy", RouterNanoHTTPD.normalizeUri("/xxx/yyy/"));
+    }
+
+    private byte[] readContents(HttpEntity entity) throws IOException {
+        InputStream instream = entity.getContent();
+        return readContents(instream);
+    }
+
+    private byte[] readContents(InputStream instream) throws IOException {
+        byte[] bytes;
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+        try {
+            byte[] buffer = new byte[1024];
+            int count;
+            while ((count = instream.read(buffer)) >= 0) {
+                out.write(buffer, 0, count);
+            }
+            bytes = out.toByteArray();
+        } finally {
+            instream.close();
+        }
+        return bytes;
+    }
+
+    @Test
+    public void staticFiles() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+
+        HttpTrace httphead = new HttpTrace("http://localhost:9090/browse/blabla.html");
+        CloseableHttpResponse response = httpclient.execute(httphead);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h3>just a page</h3></body></html>", string);
+        response.close();
+
+        httphead = new HttpTrace("http://localhost:9090/browse/dir/blabla.html");
+        response = httpclient.execute(httphead);
+        entity = response.getEntity();
+        string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h3>just an other page</h3></body></html>", string);
+        response.close();
+
+        httphead = new HttpTrace("http://localhost:9090/browse/dir/nanohttpd_logo.png");
+        response = httpclient.execute(httphead);
+        entity = response.getEntity();
+        Assert.assertEquals("image/png", entity.getContentType().getValue());
+        response.close();
+
+        httphead = new HttpTrace("http://localhost:9090/browse/dir/xxx.html");
+        response = httpclient.execute(httphead);
+        entity = response.getEntity();
+        string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h3>Error 404: the requested page doesn't exist.</h3></body></html>", string);
+        response.close();
+
+        httphead = new HttpTrace("http://localhost:9090/browse/dir/");
+        response = httpclient.execute(httphead);
+        entity = response.getEntity();
+        string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html><body><h3>just an index page</h3></body></html>", string);
+        response.close();
+
+        httphead = new HttpTrace("http://localhost:9090/browse/exception.html");
+        response = httpclient.execute(httphead);
+        Assert.assertEquals(NanoHTTPD.Response.Status.REQUEST_TIMEOUT.getRequestStatus(), response.getStatusLine().getStatusCode());
+        entity = response.getEntity();
+        string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("", string);
+        response.close();
+    }
+
+    @AfterClass
+    public static void tearDown() throws Exception {
+        stdIn.write("\n\n".getBytes());
+        serverStartThread.join(2000);
+        Assert.assertFalse(serverStartThread.isAlive());
+    }
+
+}
diff --git a/nanolets/src/test/resources/blabla.html b/nanolets/src/test/resources/blabla.html
new file mode 100644
index 0000000..f346c87
--- /dev/null
+++ b/nanolets/src/test/resources/blabla.html
@@ -0,0 +1 @@
+<html><body><h3>just a page</h3></body></html>
\ No newline at end of file
diff --git a/nanolets/src/test/resources/dir/blabla.html b/nanolets/src/test/resources/dir/blabla.html
new file mode 100644
index 0000000..2fd951b
--- /dev/null
+++ b/nanolets/src/test/resources/dir/blabla.html
@@ -0,0 +1 @@
+<html><body><h3>just an other page</h3></body></html>
\ No newline at end of file
diff --git a/nanolets/src/test/resources/dir/index.htm b/nanolets/src/test/resources/dir/index.htm
new file mode 100644
index 0000000..f410e3a
--- /dev/null
+++ b/nanolets/src/test/resources/dir/index.htm
@@ -0,0 +1 @@
+<html><body><h3>just an index page</h3></body></html>
\ No newline at end of file
diff --git a/nanolets/src/test/resources/dir/nanohttpd_logo.png b/nanolets/src/test/resources/dir/nanohttpd_logo.png
new file mode 100644
index 0000000..0eff47c
--- /dev/null
+++ b/nanolets/src/test/resources/dir/nanohttpd_logo.png
Binary files differ
diff --git a/nanolets/src/test/resources/exception.html b/nanolets/src/test/resources/exception.html
new file mode 100644
index 0000000..1a9c47b
--- /dev/null
+++ b/nanolets/src/test/resources/exception.html
@@ -0,0 +1 @@
+this will throw an io exception
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 6dd6be2..bba5b10 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,69 +1,500 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.sonatype.oss</groupId>
+		<artifactId>oss-parent</artifactId>
+		<version>7</version>
+	</parent>
+	<groupId>org.nanohttpd</groupId>
+	<artifactId>nanohttpd-project</artifactId>
+	<version>2.2.0</version>
+	<packaging>pom</packaging>
+	<name>NanoHttpd-Project</name>
+	<description>NanoHttpd is a light-weight HTTP server designed for embedding in other applications.</description>
+	<url>http://www.nanohttpd.org</url>
+	<scm>
+		<connection>scm:git:https://github.com/NanoHttpd/nanohttpd.git</connection>
+		<developerConnection>scm:git:https://github.com/NanoHttpd/nanohttpd.git</developerConnection>
+		<url>https://github.com/NanoHttpd/nanohttpd</url>
+		<tag>nanohttpd-project-2.2.0</tag>
+	</scm>
+	<organization>
+		<name>nanohttpd</name>
+	</organization>
+	<distributionManagement>
+		<repository>
+			<id>sonatype-nexus-staging</id>
+			<name>nanohttpd sonytype Maven 2 repository</name>
+			<url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
+		</repository>
+		<snapshotRepository>
+			<id>sonatype-nexus-staging</id>
+			<name>nanohttpd sonytype Snapshot Maven 2 repository</name>
+			<url>https://oss.sonatype.org/content/repositories/snapshots</url>
+		</snapshotRepository>
+		<site>
+			<id>private-deploy</id>
+			<name>private Project Site</name>
+			<url>scp://nanohttpd.org:33333/home/nanohttpd/public_html</url>
+		</site>
+	</distributionManagement>
+	<inceptionYear>2012</inceptionYear>
+	<developers>
+		<developer>
+			<id>psh</id>
+			<name>Paul Hawke</name>
+			<url>http://www.linkedin.com/in/paulhawke</url>
+			<roles>
+				<role>Administrator</role>
+				<role>Developer</role>
+			</roles>
+			<properties>
+				<picUrl>https://avatars2.githubusercontent.com/u/407647</picUrl>
+			</properties>
+		</developer>
+		<developer>
+			<id>elonen</id>
+			<name>Jarno Elonen</name>
+			<email>elonen [at] iki [dot] fi</email>
+			<roles>
+				<role>Administrator</role>
+				<role>Developer</role>
+			</roles>
+			<properties>
+				<picUrl>https://avatars0.githubusercontent.com/u/2125766</picUrl>
+			</properties>
+		</developer>
+		<developer>
+			<id>ritchieGitHub</id>
+			<name>Richard van Nieuwenhoven</name>
+			<email>ritchie [at] gmx [dot] at</email>
+			<roles>
+				<role>Administrator</role>
+				<role>Developer</role>
+			</roles>
+			<properties>
+				<picUrl>http://www.gravatar.com/avatar/9e2c2e7aa94335b72952a4b2d56bfc89.png</picUrl>
+			</properties>
+		</developer>
+	</developers>
+	<issueManagement>
+		<system>github</system>
+		<url>https://github.com/NanoHttpd/nanohttpd/issues</url>
+	</issueManagement>
+	<modules>
+		<module>core</module>
+		<module>samples</module>
+		<module>webserver</module>
+		<module>websocket</module>
+		<module>markdown-plugin</module>
+		<module>nanolets</module>
+		<module>fileupload</module>
+	</modules>
+	<licenses>
+		<license>
+			<name>The BSD 3-Clause License</name>
+			<url>http://opensource.org/licenses/BSD-3-Clause</url>
+			<distribution>repo</distribution>
+		</license>
+	</licenses>
+	<build>
+		<extensions>
+			<extension>
+				<groupId>org.apache.maven.wagon</groupId>
+				<artifactId>wagon-ssh</artifactId>
+				<version>2.7</version>
+			</extension>
+		</extensions>
 
-    <groupId>fi.iki.elonen</groupId>
-    <artifactId>nanohttpd-project</artifactId>
-    <version>2.1.0</version>
-    <packaging>pom</packaging>
-
-    <name>NanoHttpd-Project</name>
-    <url>https://github.com/NanoHttpd/nanohttpd</url>
-
-    <modules>
-        <module>core</module>
-        <module>samples</module>
-        <module>webserver</module>
-        <module>webserver/markdown-plugin</module>
-        <module>websocket</module>
-    </modules>
-
-    <build>
-        <extensions>
-            <extension>
-                <groupId>org.jvnet.wagon-svn</groupId>
-                <artifactId>wagon-svn</artifactId>
-                <version>1.8</version>
-            </extension>
-            <extension>
-                <groupId>org.apache.maven.wagon</groupId>
-                <artifactId>wagon-ftp</artifactId>
-                <version>1.0-alpha-6</version>
-            </extension>
-        </extensions>
-
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>2.2.1</version>
-                <executions>
-                    <execution>
-                        <id>attach-sources</id>
-                        <goals>
-                            <goal>jar</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-release-plugin</artifactId>
-                <version>2.4</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>2.9</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <version>2.3.1</version>
-                <configuration>
-                    <source>1.6</source>
-                    <target>1.6</target>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<version>3.3</version>
+				<configuration>
+					<source>1.6</source>
+					<target>1.6</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-javadoc-plugin</artifactId>
+				<version>2.10.1</version>
+				<configuration>
+					<aggregate>false</aggregate>
+					<failOnError>false</failOnError>
+				</configuration>
+				<executions>
+					<execution>
+						<id>attach-javadocs</id>
+						<goals>
+							<goal>jar</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-release-plugin</artifactId>
+				<version>2.5.1</version>
+				<configuration>
+					<mavenExecutorId>forked-path</mavenExecutorId>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.tinyjee.dim</groupId>
+				<artifactId>doxia-include-macro</artifactId>
+				<version>1.1</version>
+				<executions>
+					<execution>
+						<id>initialize-doxia-include-macro</id>
+						<phase>pre-site</phase>
+						<goals>
+							<goal>initialize</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-site-plugin</artifactId>
+				<version>3.4</version>
+				<configuration>
+					<chmod>false</chmod>
+				</configuration>
+				<dependencies>
+					<dependency>
+						<groupId>org.apache.maven.wagon</groupId>
+						<artifactId>wagon-ssh</artifactId>
+						<version>2.7</version>
+					</dependency>
+					<dependency>
+						<groupId>org.tinyjee.dim</groupId>
+						<artifactId>doxia-include-macro</artifactId>
+						<version>1.1</version>
+					</dependency>
+					<dependency>
+						<groupId>net.ju-n.maven.doxia</groupId>
+						<artifactId>doxia-module-markdown</artifactId>
+						<version>1.0.0</version>
+					</dependency>
+				</dependencies>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>license-maven-plugin</artifactId>
+				<version>1.7</version>
+				<configuration>
+					<verbose>false</verbose>
+				</configuration>
+				<executions>
+					<execution>
+						<id>first</id>
+						<goals>
+							<goal>update-file-header</goal>
+							<goal>update-project-license</goal>
+						</goals>
+						<phase>process-sources</phase>
+						<configuration>
+							<licenseName>bsd_3</licenseName>
+							<roots>
+								<root>src/main/java</root>
+								<root>src/test/java</root>
+							</roots>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.18.1</version>
+				<configuration>
+					<forkCount>1</forkCount>
+					<reuseForks>false</reuseForks>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<version>0.7.4.201502262128</version>
+				<executions>
+					<execution>
+						<id>default-prepare-agent</id>
+						<goals>
+							<goal>prepare-agent</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>default-report</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>report</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>default-check</id>
+						<goals>
+							<goal>check</goal>
+						</goals>
+						<configuration>
+							<rules>
+								<rule>
+									<element>BUNDLE</element>
+									<limits>
+										<limit>
+											<counter>LINE</counter>
+											<value>COVEREDRATIO</value>
+											<minimum>${minimal.coverage}</minimum>
+										</limit>
+									</limits>
+								</rule>
+							</rules>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+	<reporting>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-project-info-reports-plugin</artifactId>
+				<version>2.8</version>
+				<configuration>
+					<dependencyDetailsEnabled>false</dependencyDetailsEnabled>
+					<dependencyLocationsEnabled>false</dependencyLocationsEnabled>
+				</configuration>
+				<reportSets>
+					<reportSet>
+						<reports>
+							<report>index</report>
+							<report>dependencies</report>
+							<report>project-team</report>
+							<report>mailing-list</report>
+							<report>cim</report>
+							<report>issue-tracking</report>
+							<report>license</report>
+							<report>scm</report>
+						</reports>
+					</reportSet>
+				</reportSets>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-javadoc-plugin</artifactId>
+				<version>2.10.1</version>
+				<configuration>
+					<failOnError>false</failOnError>
+					<aggregate>false</aggregate>
+				</configuration>
+				<reportSets>
+					<reportSet><!-- by default, id = "default" -->
+						<reports><!-- select non-aggregate reports -->
+							<report>javadoc-no-fork</report>
+						</reports>
+					</reportSet>
+				</reportSets>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jxr-plugin</artifactId>
+				<version>2.4</version>
+				<configuration>
+					<linkJavadoc>true</linkJavadoc>
+					<skip>${nanohttpd.nonjavamodule}</skip>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-pmd-plugin</artifactId>
+				<version>3.2</version>
+				<configuration>
+					<linkXref>true</linkXref>
+					<sourceEncoding>UTF-8</sourceEncoding>
+					<targetJdk>1.6</targetJdk>
+					<skipEmptyReport>false</skipEmptyReport>
+					<minimumTokens>50</minimumTokens>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>taglist-maven-plugin</artifactId>
+				<version>2.4</version>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>findbugs-maven-plugin</artifactId>
+				<version>3.0.0</version>
+			</plugin>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<version>0.7.4.201502262128</version>
+			</plugin>
+		</plugins>
+	</reporting>
+	<dependencies>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<version>4.12</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<profiles>
+		<profile>
+			<id>release-sign-artifacts</id>
+			<activation>
+				<property>
+					<name>performRelease</name>
+					<value>true</value>
+				</property>
+			</activation>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-gpg-plugin</artifactId>
+						<executions>
+							<execution>
+								<id>sign-artifacts</id>
+								<phase>verify</phase>
+								<goals>
+									<goal>sign</goal>
+								</goals>
+								<!-- if the local user is not the key user use -Dgpg.keyname=XXXX -->
+							</execution>
+						</executions>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+		<profile>
+			<id>java</id>
+			<activation>
+				<file>
+					<exists>src/main/java</exists>
+				</file>
+			</activation>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-checkstyle-plugin</artifactId>
+						<version>2.15</version>
+						<configuration>
+							<configLocation>${project.basedir}/../src/main/checkstyle/nanohttpd-style.xml</configLocation>
+							<suppressionsLocation>${project.basedir}/../src/main/checkstyle/checkstyle-suppressions.xml</suppressionsLocation>
+							<encoding>UTF-8</encoding>
+							<consoleOutput>true</consoleOutput>
+							<failsOnError>false</failsOnError>
+							<linkXRef>true</linkXRef>
+						</configuration>
+					</plugin>
+					<plugin>
+						<groupId>com.googlecode.maven-java-formatter-plugin</groupId>
+						<artifactId>maven-java-formatter-plugin</artifactId>
+						<version>0.4</version>
+						<executions>
+							<execution>
+								<goals>
+									<goal>format</goal>
+								</goals>
+							</execution>
+						</executions>
+						<configuration>
+							<configFile>${project.basedir}/../src/main/formatter/formatter.xml</configFile>
+							<lineEnding>LF</lineEnding>
+						</configuration>
+					</plugin>
+				</plugins>
+			</build>
+			<reporting>
+				<plugins>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-checkstyle-plugin</artifactId>
+						<version>2.15</version>
+						<reportSets>
+							<reportSet>
+								<reports>
+									<report>checkstyle</report>
+								</reports>
+							</reportSet>
+						</reportSets>
+					</plugin>
+				</plugins>
+			</reporting>
+		</profile>
+		<profile>
+			<id>parent-build</id>
+			<activation>
+				<file>
+					<exists>core/pom.xml</exists>
+				</file>
+			</activation>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.jacoco</groupId>
+						<artifactId>jacoco-maven-plugin</artifactId>
+						<version>0.7.4.201502262128</version>
+						<executions>
+							<execution>
+								<id>merge-report</id>
+								<phase>package</phase>
+								<goals>
+									<goal>merge</goal>
+								</goals>
+							</execution>
+						</executions>
+						<configuration>
+							<fileSets>
+								<fileSet>
+									<directory>${project.basedir}/..</directory>
+									<includes>
+										<include>*.exec</include>
+									</includes>
+								</fileSet>
+							</fileSets>
+						</configuration>
+					</plugin>
+					<plugin>
+						<groupId>org.eluder.coveralls</groupId>
+						<artifactId>coveralls-maven-plugin</artifactId>
+						<version>3.1.0</version>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+		<profile>
+			<id>use 1.6 compiler</id>
+			<activation>
+				<file>
+					<exists>/usr/lib/jvm/java-6-openjdk-amd64/bin/javac</exists>
+				</file>
+			</activation>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-compiler-plugin</artifactId>
+						<version>3.3</version>
+						<configuration>
+							<source>1.6</source>
+							<target>1.6</target>
+							<verbose>true</verbose>
+							<fork>true</fork>
+							<executable>/usr/lib/jvm/java-6-openjdk-amd64/bin/javac</executable>
+						</configuration>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+	</profiles>
+	<properties>
+		<minimal.coverage>0.77</minimal.coverage>
+	</properties>
 </project>
diff --git a/samples/.gitignore b/samples/.gitignore
new file mode 100644
index 0000000..868a6b2
--- /dev/null
+++ b/samples/.gitignore
@@ -0,0 +1,2 @@
+/.settings/
+/LICENSE.txt
diff --git a/samples/pom.xml b/samples/pom.xml
index a32b07b..63e8f7b 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -1,75 +1,27 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-
-    <groupId>fi.iki.elonen</groupId>
-    <artifactId>nanohttpd-samples</artifactId>
-    <version>2.1.0</version>
-    <packaging>jar</packaging>
-
-    <name>NanoHttpd-Samples</name>
-    <url>https://github.com/NanoHttpd/nanohttpd</url>
-
-    <dependencies>
-        <dependency>
-            <groupId>fi.iki.elonen</groupId>
-            <artifactId>nanohttpd</artifactId>
-            <version>2.1.0</version>
-        </dependency>
-        <dependency>
-            <groupId>fi.iki.elonen</groupId>
-            <artifactId>nanohttpd-webserver</artifactId>
-            <version>2.1.0</version>
-        </dependency>
-    </dependencies>
-
-    <build>
-        <extensions>
-            <extension>
-                <groupId>org.jvnet.wagon-svn</groupId>
-                <artifactId>wagon-svn</artifactId>
-                <version>1.8</version>
-            </extension>
-            <extension>
-                <groupId>org.apache.maven.wagon</groupId>
-                <artifactId>wagon-ftp</artifactId>
-                <version>1.0-alpha-6</version>
-            </extension>
-        </extensions>
-
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>2.2.1</version>
-                <executions>
-                    <execution>
-                        <id>attach-sources</id>
-                        <goals>
-                            <goal>jar</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-release-plugin</artifactId>
-                <version>2.4</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>2.9</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <version>2.3.1</version>
-                <configuration>
-                    <source>1.6</source>
-                    <target>1.6</target>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.nanohttpd</groupId>
+		<artifactId>nanohttpd-project</artifactId>
+		<version>2.2.0</version>
+	</parent>
+	<artifactId>nanohttpd-samples</artifactId>
+	<description>samples for nanohttpd</description>
+	<packaging>jar</packaging>
+	<name>NanoHttpd-Samples</name>
+	<dependencies>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>nanohttpd</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>nanohttpd-webserver</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+	</dependencies>
+	<properties>
+		<minimal.coverage>0.0</minimal.coverage>
+	</properties>
 </project>
diff --git a/samples/src/main/java/fi/iki/elonen/HelloServer.java b/samples/src/main/java/fi/iki/elonen/HelloServer.java
index c7c7da4..bc91231 100644
--- a/samples/src/main/java/fi/iki/elonen/HelloServer.java
+++ b/samples/src/main/java/fi/iki/elonen/HelloServer.java
@@ -1,37 +1,77 @@
 package fi.iki.elonen;
 
+/*
+ * #%L
+ * NanoHttpd-Samples
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
 import java.util.Map;
+import java.util.logging.Logger;
+
+import fi.iki.elonen.util.ServerRunner;
 
 /**
  * An example of subclassing NanoHTTPD to make a custom HTTP server.
  */
 public class HelloServer extends NanoHTTPD {
+
+    /**
+     * logger to log to.
+     */
+    private static final Logger LOG = Logger.getLogger(HelloServer.class.getName());
+
+    public static void main(String[] args) {
+        ServerRunner.run(HelloServer.class);
+    }
+
     public HelloServer() {
         super(8080);
     }
 
-    @Override public Response serve(IHTTPSession session) {
+    @Override
+    public Response serve(IHTTPSession session) {
         Method method = session.getMethod();
         String uri = session.getUri();
-        System.out.println(method + " '" + uri + "' ");
+        HelloServer.LOG.info(method + " '" + uri + "' ");
 
         String msg = "<html><body><h1>Hello server</h1>\n";
         Map<String, String> parms = session.getParms();
-        if (parms.get("username") == null)
-            msg +=
-                    "<form action='?' method='get'>\n" +
-                            "  <p>Your name: <input type='text' name='username'></p>\n" +
-                            "</form>\n";
-        else
+        if (parms.get("username") == null) {
+            msg += "<form action='?' method='get'>\n" + "  <p>Your name: <input type='text' name='username'></p>\n" + "</form>\n";
+        } else {
             msg += "<p>Hello, " + parms.get("username") + "!</p>";
+        }
 
         msg += "</body></html>\n";
 
-        return new NanoHTTPD.Response(msg);
-    }
-
-
-    public static void main(String[] args) {
-        ServerRunner.run(HelloServer.class);
+        return newFixedLengthResponse(msg);
     }
 }
diff --git a/samples/src/main/java/fi/iki/elonen/TempFilesServer.java b/samples/src/main/java/fi/iki/elonen/TempFilesServer.java
index fb96911..964f2b2 100644
--- a/samples/src/main/java/fi/iki/elonen/TempFilesServer.java
+++ b/samples/src/main/java/fi/iki/elonen/TempFilesServer.java
@@ -1,57 +1,96 @@
 package fi.iki.elonen;
 
-import fi.iki.elonen.debug.DebugServer;
+/*
+ * #%L
+ * NanoHttpd-Samples
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 
+import fi.iki.elonen.debug.DebugServer;
+import fi.iki.elonen.util.ServerRunner;
+
 /**
- * @author Paul S. Hawke (paul.hawke@gmail.com)
- *         On: 3/9/13 at 12:47 AM
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 3/9/13 at 12:47 AM
  */
 public class TempFilesServer extends DebugServer {
-    public static void main(String[] args) {
-        TempFilesServer server = new TempFilesServer();
-        server.setTempFileManagerFactory(new ExampleManagerFactory());
-        ServerRunner.executeInstance(server);
+
+    private static class ExampleManager implements TempFileManager {
+
+        private final File tmpdir;
+
+        private final List<TempFile> tempFiles;
+
+        private ExampleManager() {
+            this.tmpdir = new File(System.getProperty("java.io.tmpdir"));
+            this.tempFiles = new ArrayList<TempFile>();
+        }
+
+        @Override
+        public void clear() {
+            if (!this.tempFiles.isEmpty()) {
+                System.out.println("Cleaning up:");
+            }
+            for (TempFile file : this.tempFiles) {
+                try {
+                    System.out.println("   " + file.getName());
+                    file.delete();
+                } catch (Exception ignored) {
+                }
+            }
+            this.tempFiles.clear();
+        }
+
+        @Override
+        public TempFile createTempFile(String filename_hint) throws Exception {
+            DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir);
+            this.tempFiles.add(tempFile);
+            System.out.println("Created tempFile: " + tempFile.getName());
+            return tempFile;
+        }
     }
 
     private static class ExampleManagerFactory implements TempFileManagerFactory {
+
         @Override
         public TempFileManager create() {
             return new ExampleManager();
         }
     }
 
-    private static class ExampleManager implements TempFileManager {
-        private final String tmpdir;
-        private final List<TempFile> tempFiles;
-
-        private ExampleManager() {
-            tmpdir = System.getProperty("java.io.tmpdir");
-            tempFiles = new ArrayList<TempFile>();
-        }
-
-        @Override
-        public TempFile createTempFile() throws Exception {
-            DefaultTempFile tempFile = new DefaultTempFile(tmpdir);
-            tempFiles.add(tempFile);
-            System.out.println("Created tempFile: " + tempFile.getName());
-            return tempFile;
-        }
-
-        @Override
-        public void clear() {
-            if (!tempFiles.isEmpty()) {
-                System.out.println("Cleaning up:");
-            }
-            for (TempFile file : tempFiles) {
-                try {
-                    System.out.println("   "+file.getName());
-                    file.delete();
-                } catch (Exception ignored) {}
-            }
-            tempFiles.clear();
-        }
+    public static void main(String[] args) {
+        TempFilesServer server = new TempFilesServer();
+        server.setTempFileManagerFactory(new ExampleManagerFactory());
+        ServerRunner.executeInstance(server);
     }
 }
diff --git a/samples/src/main/java/fi/iki/elonen/debug/DebugServer.java b/samples/src/main/java/fi/iki/elonen/debug/DebugServer.java
index 1c83876..0ffc34f 100644
--- a/samples/src/main/java/fi/iki/elonen/debug/DebugServer.java
+++ b/samples/src/main/java/fi/iki/elonen/debug/DebugServer.java
@@ -1,24 +1,62 @@
 package fi.iki.elonen.debug;
 
-import fi.iki.elonen.NanoHTTPD;
-import fi.iki.elonen.ServerRunner;
+/*
+ * #%L
+ * NanoHttpd-Samples
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.util.ServerRunner;
+
 public class DebugServer extends NanoHTTPD {
-    public DebugServer() {
-        super(8080);
-    }
 
     public static void main(String[] args) {
         ServerRunner.run(DebugServer.class);
     }
 
-    @Override public Response serve(IHTTPSession session) {
-        Map<String, List<String>> decodedQueryParameters =
-            decodeParameters(session.getQueryParameterString());
+    public DebugServer() {
+        super(8080);
+    }
+
+    private void listItem(StringBuilder sb, Map.Entry<String, ? extends Object> entry) {
+        sb.append("<li><code><b>").append(entry.getKey()).append("</b> = ").append(entry.getValue()).append("</code></li>");
+    }
+
+    @Override
+    public Response serve(IHTTPSession session) {
+        Map<String, List<String>> decodedQueryParameters = decodeParameters(session.getQueryParameterString());
 
         StringBuilder sb = new StringBuilder();
         sb.append("<html>");
@@ -26,33 +64,27 @@
         sb.append("<body>");
         sb.append("<h1>Debug Server</h1>");
 
-        sb.append("<p><blockquote><b>URI</b> = ").append(
-            String.valueOf(session.getUri())).append("<br />");
+        sb.append("<p><blockquote><b>URI</b> = ").append(String.valueOf(session.getUri())).append("<br />");
 
-        sb.append("<b>Method</b> = ").append(
-            String.valueOf(session.getMethod())).append("</blockquote></p>");
+        sb.append("<b>Method</b> = ").append(String.valueOf(session.getMethod())).append("</blockquote></p>");
 
-        sb.append("<h3>Headers</h3><p><blockquote>").
-            append(toString(session.getHeaders())).append("</blockquote></p>");
+        sb.append("<h3>Headers</h3><p><blockquote>").append(toString(session.getHeaders())).append("</blockquote></p>");
 
-        sb.append("<h3>Parms</h3><p><blockquote>").
-            append(toString(session.getParms())).append("</blockquote></p>");
+        sb.append("<h3>Parms</h3><p><blockquote>").append(toString(session.getParms())).append("</blockquote></p>");
 
-        sb.append("<h3>Parms (multi values?)</h3><p><blockquote>").
-            append(toString(decodedQueryParameters)).append("</blockquote></p>");
+        sb.append("<h3>Parms (multi values?)</h3><p><blockquote>").append(toString(decodedQueryParameters)).append("</blockquote></p>");
 
         try {
             Map<String, String> files = new HashMap<String, String>();
             session.parseBody(files);
-            sb.append("<h3>Files</h3><p><blockquote>").
-                append(toString(files)).append("</blockquote></p>");
+            sb.append("<h3>Files</h3><p><blockquote>").append(toString(files)).append("</blockquote></p>");
         } catch (Exception e) {
             e.printStackTrace();
         }
 
         sb.append("</body>");
         sb.append("</html>");
-        return new Response(sb.toString());
+        return newFixedLengthResponse(sb.toString());
     }
 
     private String toString(Map<String, ? extends Object> map) {
@@ -65,15 +97,10 @@
     private String unsortedList(Map<String, ? extends Object> map) {
         StringBuilder sb = new StringBuilder();
         sb.append("<ul>");
-        for (Map.Entry entry : map.entrySet()) {
+        for (Map.Entry<String, ? extends Object> entry : map.entrySet()) {
             listItem(sb, entry);
         }
         sb.append("</ul>");
         return sb.toString();
     }
-
-    private void listItem(StringBuilder sb, Map.Entry entry) {
-        sb.append("<li><code><b>").append(entry.getKey()).
-            append("</b> = ").append(entry.getValue()).append("</code></li>");
-    }
 }
diff --git a/samples/src/site/site.xml b/samples/src/site/site.xml
new file mode 100644
index 0000000..4270945
--- /dev/null
+++ b/samples/src/site/site.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="${project.name}">
+	<skin>
+		<groupId>org.apache.maven.skins</groupId>
+		<artifactId>maven-fluido-skin</artifactId>
+		<version>1.3.0</version>
+	</skin>
+	<bannerLeft>
+		<src>../images/nanohttpd_logo.png</src>
+	</bannerLeft>
+	<bannerRight>
+		<src>../images/nanohttpd_logo_text.png</src>
+	</bannerRight>
+	<publishDate position="left" format="yyyy-MM-dd" />
+	<version position="right" />
+	<poweredBy>
+		<logo name="Maven" href="http://maven.apache.org/"
+			img="http://maven.apache.org/images/logos/maven-feather.png" />
+	</poweredBy>
+	<custom>
+		<fluidoSkin>
+			<topBarEnabled>false</topBarEnabled>
+			<sideBarEnabled>true</sideBarEnabled>
+			<gitHub>
+				<projectId>Nanohttpd/nanohttpd</projectId>
+				<ribbonOrientation>right</ribbonOrientation>
+				<ribbonColor>black</ribbonColor>
+			</gitHub>
+		</fluidoSkin>
+	</custom>
+	<body>
+		<breadcrumbs>
+			<item name="${project.name}" href="index.html" />
+		</breadcrumbs>
+		<menu name="Documentation">
+			<item name="About" href="index.html" />
+		</menu>
+		<menu ref="modules" />
+		<menu ref="reports" />
+	</body>
+</project>
\ No newline at end of file
diff --git a/settings.xml b/settings.xml
new file mode 100644
index 0000000..6d09214
--- /dev/null
+++ b/settings.xml
@@ -0,0 +1,15 @@
+<settings>
+	<proxies>
+	</proxies>
+	<servers>
+		<server>
+			<id>sonatype-nexus-staging</id>
+			<username>${env.CI_DEPLOY_USERNAME}</username>
+			<password>${env.CI_DEPLOY_PASSWORD}</password>
+		</server>
+	</servers>
+	<mirrors>
+	</mirrors>
+	<profiles>
+	</profiles>
+</settings>
\ No newline at end of file
diff --git a/src/main/checkstyle/checkstyle-suppressions.xml b/src/main/checkstyle/checkstyle-suppressions.xml
new file mode 100644
index 0000000..2edb43b
--- /dev/null
+++ b/src/main/checkstyle/checkstyle-suppressions.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<!DOCTYPE suppressions PUBLIC
+     "-//Puppy Crawl//DTD Suppressions 1.0//EN"
+     "http://www.puppycrawl.com/dtds/suppressions_1_0.dtd">
+
+<suppressions>
+
+	<suppress files=".jpg" checks="NewlineAtEndOfFile|FileTabCharacter|FileLength" />
+	<suppress files=".png" checks="NewlineAtEndOfFile|FileTabCharacter|FileLength" />
+	<suppress files=".fxml" checks="FileTabCharacter" />
+	<suppress files=".css" checks="FileTabCharacter" />
+	<suppress files="META[\\-]INF[\\/]services[\\/]" checks="NewlineAtEndOfFile|FileTabCharacter" />
+
+	<!-- JUnit Rules require the rules field to have public visibility. -->
+	<!-- See http://checkstyle.sourceforge.net/config_design.html#VisibilityModifier -->
+	<suppress files=".*Test\.java" checks="VisibilityModifier" />
+	<suppress files="Test.*\.java" checks="VisibilityModifier" />
+
+	<!-- For our tests we do not demand API documentation. -->
+	<!-- See http://checkstyle.sf.net/config_javadoc.html -->
+	<suppress files=".*Test\.java" checks="JavadocPackage" />
+	<suppress files=".*Test\.java" checks="JavadocMethod" />
+	<suppress files=".*Test\.java" checks="JavadocType" />
+	<suppress files=".*Test\.java" checks="JavadocVariable" />
+	<suppress files=".*Test\.java" checks="JavadocStyle" />
+
+</suppressions>
\ No newline at end of file
diff --git a/src/main/checkstyle/nanohttpd-style.xml b/src/main/checkstyle/nanohttpd-style.xml
new file mode 100644
index 0000000..038ca32
--- /dev/null
+++ b/src/main/checkstyle/nanohttpd-style.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+          "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
+          "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
+
+<!-- Checkstyle configuration that checks the sun coding conventions from: - the Java Language Specification at http://java.sun.com/docs/books/jls/second_edition/html/index.html - the 
+	Sun Code Conventions at http://java.sun.com/docs/codeconv/ - the Javadoc guidelines at http://java.sun.com/j2se/javadoc/writingdoccomments/index.html - the JDK Api documentation http://java.sun.com/j2se/docs/api/index.html 
+	- some best practices Checkstyle is very configurable. Be sure to read the documentation at http://checkstyle.sf.net (or in your downloaded distribution). Most Checks are configurable, 
+	be sure to consult the documentation. To completely disable a check, just comment it out or delete it from the file. Finally, it is worth reading the documentation. -->
+
+<module name="Checker">
+	<module name="SuppressionFilter">
+    	<property name="file" value="${checkstyle.suppressions.file}" default="src/main/checkstyle/checkstyle-suppressions.xml"/>
+	</module>
+	<!-- If you set the basedir property below, then all reported file names will be relative to the specified directory. See http://checkstyle.sourceforge.net/5.x/config.html#Checker 
+		<property name="basedir" value="${basedir}"/> -->
+
+	<!-- Checks whether files end with a new line. -->
+	<!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->
+	<module name="NewlineAtEndOfFile" />
+
+	<!-- Checks that property files contain the same keys. -->
+	<!-- See http://checkstyle.sf.net/config_misc.html#Translation -->
+	<module name="Translation" />
+
+	<!-- Checks for Size Violations. -->
+	<!-- See http://checkstyle.sf.net/config_sizes.html -->
+	<module name="FileLength" />
+
+	<!-- Checks for whitespace -->
+	<!-- See http://checkstyle.sf.net/config_whitespace.html -->
+	<module name="FileTabCharacter" />
+
+
+	<!-- Checks for Headers -->
+	<!-- See http://checkstyle.sf.net/config_header.html -->
+	<!-- <module name="Header"> -->
+	<!-- <property name="headerFile" value="${checkstyle.header.file}"/> -->
+	<!-- <property name="fileExtensions" value="java"/> -->
+	<!-- </module> -->
+
+	<module name="TreeWalker">
+
+		<!-- Checks for Javadoc comments. -->
+		<!-- See http://checkstyle.sf.net/config_javadoc.html -->
+		<module name="JavadocMethod" >
+			<property name="scope" value="protected" />
+		</module>
+		<module name="JavadocType" >
+			<property name="scope" value="protected" />
+		</module>
+		<module name="JavadocVariable" >
+			<property name="scope" value="protected" />
+		</module>
+		<module name="JavadocStyle" >
+			<property name="scope" value="protected" />
+		</module>
+
+
+		<!-- Checks for Naming Conventions. -->
+		<!-- See http://checkstyle.sf.net/config_naming.html -->
+		<module name="ConstantName" />
+		<module name="LocalFinalVariableName" />
+		<module name="LocalVariableName" />
+		<module name="MemberName" />
+		<module name="MethodName" />
+		<module name="PackageName" />
+		<module name="ParameterName" />
+		<module name="StaticVariableName" />
+		<module name="TypeName" />
+
+
+		<!-- Checks for imports -->
+		<!-- See http://checkstyle.sf.net/config_import.html -->
+		<module name="AvoidStarImport" />
+		<module name="IllegalImport" /> <!-- defaults to sun.* packages -->
+		<module name="RedundantImport" />
+		<module name="UnusedImports" />
+
+
+		<!-- Checks for Size Violations. -->
+		<!-- See http://checkstyle.sf.net/config_sizes.html -->
+		<module name="LineLength">
+			<property name="max" value="180" />
+		</module>
+		<module name="MethodLength" />
+		<module name="ParameterNumber" />
+
+
+		<!-- Checks for whitespace -->
+		<!-- See http://checkstyle.sf.net/config_whitespace.html -->
+		<module name="EmptyForIteratorPad" />
+		<module name="GenericWhitespace" />
+		<module name="MethodParamPad" />
+		<module name="NoWhitespaceAfter" />
+		<module name="NoWhitespaceBefore" />
+		<module name="OperatorWrap" />
+		<module name="ParenPad" />
+		<module name="TypecastParenPad" />
+		<module name="WhitespaceAfter" />
+		<module name="WhitespaceAround" />
+
+
+		<!-- Modifier Checks -->
+		<!-- See http://checkstyle.sf.net/config_modifiers.html -->
+		<module name="ModifierOrder" />
+		<module name="RedundantModifier" />
+
+
+		<!-- Checks for blocks. You know, those {}'s -->
+		<!-- See http://checkstyle.sf.net/config_blocks.html -->
+		<module name="AvoidNestedBlocks" />
+		<module name="EmptyBlock" />
+		<module name="LeftCurly" />
+		<module name="NeedBraces" />
+		<module name="RightCurly" />
+
+
+		<!-- Checks for common coding problems -->
+		<!-- See http://checkstyle.sf.net/config_coding.html -->
+		<!-- <module name="AvoidInlineConditionals" /> i think simple cases are ok, not generally bad -->
+		<module name="EmptyStatement" />
+		<module name="EqualsHashCode" />
+		<module name="HiddenField">
+			<property name="ignoreSetter" value="true" />
+			<property name="ignoreConstructorParameter" value="true" />
+		</module>
+		<module name="IllegalInstantiation" />
+		<module name="InnerAssignment" />
+
+		<module name="MissingSwitchDefault" />
+		<module name="RedundantThrows">
+			<property name="suppressLoadErrors" value="true" />
+		</module>
+		<module name="SimplifyBooleanExpression" />
+		<module name="SimplifyBooleanReturn" />
+
+		<!-- Checks for class design -->
+		<!-- See http://checkstyle.sf.net/config_design.html -->
+		<!-- <module name="DesignForExtension" /> part of this would be good but it goes to much to the core -->
+		<module name="FinalClass" />
+		<module name="HideUtilityClassConstructor" />
+		<module name="InterfaceIsType" />
+		<module name="VisibilityModifier">
+			<property name="protectedAllowed" value="true" />
+		</module>
+
+
+		<!-- Miscellaneous other checks. -->
+		<!-- See http://checkstyle.sf.net/config_misc.html -->
+		<module name="ArrayTypeStyle" />
+		<!-- <module name="TodoComment" /> is better done by the taglist plugin -->
+		<module name="UpperEll" />
+
+	</module>
+
+</module>
diff --git a/src/main/formatter/formatter.xml b/src/main/formatter/formatter.xml
new file mode 100644
index 0000000..2f4ca22
--- /dev/null
+++ b/src/main/formatter/formatter.xml
@@ -0,0 +1,267 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profiles version="11">
+<profile kind="CodeFormatterProfile" name="CFC" version="11">
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="175"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="49"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.compiler.source" value="1.5"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.5"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.5"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="49"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
+</profile>
+</profiles>
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
new file mode 100644
index 0000000..1a78b2a
--- /dev/null
+++ b/src/site/markdown/index.md
@@ -0,0 +1,248 @@
+## NanoHTTPD – a tiny web server in Java
+
+*NanoHTTPD* is a light-weight HTTP server designed for embedding in other applications, released under a Modified BSD licence.
+
+It is being developed at Github and uses Apache Maven for builds & unit testing:
+
+ * Build status: [![Build Status](https://api.travis-ci.org/NanoHttpd/nanohttpd.png)](https://travis-ci.org/NanoHttpd/nanohttpd)
+ * Coverage Status: [![Coverage Status](https://coveralls.io/repos/NanoHttpd/nanohttpd/badge.svg)](https://coveralls.io/r/NanoHttpd/nanohttpd)
+ * Current central released version: [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.nanohttpd/nanohttpd/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.nanohttpd/nanohttpd)
+
+## Quickstart
+
+We'll create a custom HTTP server project using Maven for build/dep system. This tutorial assumes you are using a Unix variant and a shell. First, install Maven and Java SDK if not already installed. Then run:
+
+    mvn compile
+    mvn exec:java -pl webserver -Dexec.mainClass="fi.iki.elonen.SimpleWebServer"
+    
+You should now have a HTTP file server running on <http://localhost:8080/>.
+
+### Custom web app
+
+Let's raise the bar and build a custom web application next:
+
+    mvn archetype:generate -DgroupId=com.example -DartifactId=myHellopApp -DinteractiveMode=false
+    cd myHellopApp
+    
+Edit `pom.xml`, and add this between \<dependencies\>:
+ 
+	<dependency>
+		<groupId>org.nanohttpd</groupId> <!-- <groupId>com.nanohttpd</groupId> for 2.1.0 and earlier -->
+		<artifactId>nanohttpd</artifactId>
+		<version>2.2.0</version>
+	</dependency>
+	
+Edit `src/main/java/com/example/App.java` and replace it with:
+```java
+package com.example;
+
+import java.util.Map;
+import java.io.IOException;
+import fi.iki.elonen.NanoHTTPD;
+
+public class App extends NanoHTTPD {
+
+    public App() throws IOException {
+        super(8080);
+        start();
+		System.out.println( "\nRunning! Point your browers to http://localhost:8080/ \n" );
+    }
+
+    public static void main(String[] args) {
+		try {
+		    new App();
+		}
+		catch( IOException ioe ) {
+			System.err.println( "Couldn't start server:\n" + ioe );
+		}
+    }
+
+    @Override
+    public Response serve(IHTTPSession session) {
+        String msg = "<html><body><h1>Hello server</h1>\n";
+        Map<String, String> parms = session.getParms();
+        if (parms.get("username") == null) {
+            msg += "<form action='?' method='get'>\n  <p>Your name: <input type='text' name='username'></p>\n" + "</form>\n";
+        } else {
+            msg += "<p>Hello, " + parms.get("username") + "!</p>";
+        }
+        return newFixedLengthResponse( msg + "</body></html>\n" );
+    }
+}
+```
+
+Compile and run the server:
+ 
+    mvn compile
+    mvn exec:java -Dexec.mainClass="com.example.App"
+    
+If it started ok, point your browser at <http://localhost:8080/> and enjoy a web server that asks your name and replies with a greeting. 
+
+### Nanolets
+
+Nanolets are like sevlet's only that they have a extrem low profile. They offer an easy to use system for a more complex server application. 
+This text has to be extrended with an example, so for now take a look at the unit tests for the usage. <https://github.com/NanoHttpd/nanohttpd/blob/master/nanolets/src/test/java/fi/iki/elonen/router/AppNanolets.java>
+
+## Status
+
+We are currently in the process of stabilizing NanoHttpd from the many pull requests and feature requests that were integrated over the last few months. The next release will come soon, and there will not be any more "intended" major changes before the next release. If you want to use the bleeding edge version, you can clone it from Github, or get it from sonatype.org (see "Maven dependencies / Living on the edge" below).
+
+## Project structure
+
+NanoHTTPD project currently consist of four parts:
+
+ * `/core` – Fully functional HTTP(s) server consisting of one (1) Java file, ready to be customized/inherited for your own project
+
+ * `/samples` – Simple examples on how to customize NanoHTTPD. See *HelloServer.java* for a killer app that greets you enthusiastically!
+
+ * `/websocket` – Websocket implementation, also in a single Java file. Depends on core.
+
+ * `/webserver` – Standalone file server. Run & enjoy. A popular use seems to be serving files out off an Android device.
+
+ * `/nanolets` – Standalone nano app server, giving a servlet like system to the implementor.
+
+ * `/fileupload` – integration of the apache common file upload library.
+
+## Features
+### Core
+* Only one Java file, providing HTTP 1.1 support.
+* No fixed config files, logging, authorization etc. (Implement by yourself if you need them. Errors are passed to java.util.logging, though.)
+* Support for HTTPS (SSL)
+* Basic support for cookies
+* Supports parameter parsing of GET and POST methods.
+* Some built-in support for HEAD, POST and DELETE requests. You can easily implement/customize any HTTP method, though.
+* Supports file upload. Uses memory for small uploads, temp files for large ones.
+* Never caches anything.
+* Does not limit bandwidth, request time or simultaneous connections by default.
+* All header names are converted to lower case so they don't vary between browsers/clients.
+* Persistent connections (Connection "keep-alive") support allowing multiple requests to be served over a single socket connection.
+
+### Websocket
+* Tested on Firefox, Chrome and IE.
+
+### Webserver
+* Default code serves files and shows (prints on console) all HTTP parameters and headers.
+* Supports both dynamic content and file serving.
+* File server supports directory listing, `index.html` and `index.htm`.
+* File server supports partial content (streaming & continue download).
+* File server supports ETags.
+* File server does the 301 redirection trick for directories without `/`.
+* File server serves also very long files without memory overhead.
+* Contains a built-in list of most common MIME types.
+* Runtime extension support (extensions that serve particular MIME types) - example extension that serves Markdown formatted files. Simply including an extension JAR in the webserver classpath is enough for the extension to be loaded.
+* Simple [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) support via `--cors` paramater
+  * by default serves `Access-Control-Allow-Headers: origin,accept,content-type`
+  * possibility to set `Access-Control-Allow-Headers` by setting System property: `AccessControlAllowHeader`
+  * _example: _ `-DAccessControlAllowHeader=origin,accept,content-type,Authorization`
+  * possible values:
+      * `--cors`: activates CORS support, `Access-Control-Allow-Origin` will be set to `*`
+      * `--cors=some_value`: `Access-Control-Allow-Origin` will be set to `some_value`. 
+
+**_CORS argument examples_**
+
+
+* `--cors=http://appOne.company.com`
+* `--cors="http://appOne.company.com, http://appTwo.company.com"`: note the double quotes so that the 2 URLs are considered part of a single argument.
+
+## Maven dependencies
+
+NanoHTTPD is a Maven based project and deployed to central. Most development environments have means to access the central repository. The coordinates to use in Maven are: 
+
+	<dependencies>
+		<dependency>
+			<groupId>org.nanohttpd</groupId> <!-- <groupId>com.nanohttpd</groupId> for 2.1.0 and earlier -->
+			<artifactId>nanohttpd</artifactId>
+			<version>2.2.0</version>
+		</dependency>
+	</dependencies>
+
+The coordinates for your development environment should correspond to these. When looking for an older version take care because we switched groupId from *com.nanohttpd* to *org.nanohttpd* in mid 2015.
+
+Next it depends what you are useing nanohttpd for, there are tree main usages.
+
+## Gradle dependencies
+
+In gradle you can use nano http the same way because gradle accesses the same central repository:
+
+	dependencies {
+		runtime(
+			[group: 'org.nanohttpd', name: 'nanohttpd', version: '2.2.0'],
+		)
+	}
+
+Just replace the name with the artifact id of the module you want to use and gradle will find it for you. 
+
+### Develop your own specialized HTTP service
+
+For a specialized HTTP (HTTPS) service you can use the module with artifactId *nanohttpd*.
+
+		<dependency>
+			<groupId>org.nanohttpd</groupId> <!-- <groupId>com.nanohttpd</groupId> for 2.1.0 and earlier -->
+			<artifactId>nanohttpd</artifactId>
+			<version>2.2.0VERSION</version>
+		</dependency>
+		
+Here you write your own subclass of *fi.iki.elonen.NanoHTTPD* to configure and to serve the requests.
+  
+### Develop a websocket based service    
+
+For a specialized websocket service you can use the module with artifactId *nanohttpd-websocket*.
+
+		<dependency>
+			<groupId>org.nanohttpd</groupId> <!-- <groupId>com.nanohttpd</groupId> for 2.1.0 and earlier -->
+			<artifactId>nanohttpd-websocket</artifactId>
+			<version>2.2.0</version>
+		</dependency>
+
+Here you write your own subclass of *fi.iki.elonen.NanoWebSocketServer* to configure and to serve the websocket requests. A small standard echo example is included as *fi.iki.elonen.samples.echo.DebugWebSocketServer*. You can use it as a starting point to implement your own services.
+
+### Develop a custom HTTP file server    
+
+For a more classic aproach, perhaps to just create a HTTP server serving mostly service files from your disk, you can use the module with artifactId *nanohttpd-webserver*.
+
+		<dependency>
+			<groupId>org.nanohttpd</groupId>
+			<artifactId>nanohttpd-webserver</artifactId>
+			<version>2.2.0</version>
+		</dependency>
+
+The included class *fi.iki.elonen.SimpleWebServer* is intended to be used as a starting point for your own implementation but it also can be used as is. Staring the class as is will start a http server on port 8080 and publishing the current directory.  
+
+### Living on the edge
+
+The latest Github master version can be fetched through sonatype.org:
+
+	<dependencies>
+		<dependency>
+			<artifactId>nanohttpd</artifactId>
+			<groupId>org.nanohttpd</groupId>
+			<version>XXXXX-SNAPSHOT</version>
+		</dependency>
+	</dependencies>
+	...
+	<repositories>
+		<repository>
+			<id>sonatype-snapshots</id>
+			<url>https://oss.sonatype.org/content/repositories/snapshots</url>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+		</repository>
+	</repositories>
+
+### generating an self signed ssl certificate
+
+Just a hint how to generate a certificate for localhost.
+
+	keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 360 -keysize 2048 -ext SAN=DNS:localhost,IP:127.0.0.1  -validity 9999
+
+This will generate a keystore file named 'keystore.jks' with a self signed certificate for a host named localhost with the ip adress 127.0.0.1 . Now 
+you can use:
+
+	server.makeSecure(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray()));
+
+Before you start the server to make Nanohttpd serve https connections, when you make sure 'keystore.jks' is in your classpath .  
+ 
+-----
+
+*Thank you to everyone who has reported bugs and suggested fixes.*
diff --git a/src/site/resources/images/nanohttpd_logo.png b/src/site/resources/images/nanohttpd_logo.png
new file mode 100644
index 0000000..0eff47c
--- /dev/null
+++ b/src/site/resources/images/nanohttpd_logo.png
Binary files differ
diff --git a/src/site/resources/images/nanohttpd_logo_text.png b/src/site/resources/images/nanohttpd_logo_text.png
new file mode 100644
index 0000000..034353f
--- /dev/null
+++ b/src/site/resources/images/nanohttpd_logo_text.png
Binary files differ
diff --git a/src/site/site.xml b/src/site/site.xml
new file mode 100644
index 0000000..5bfccc0
--- /dev/null
+++ b/src/site/site.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="${project.name}">
+	<skin>
+		<groupId>org.apache.maven.skins</groupId>
+		<artifactId>maven-fluido-skin</artifactId>
+		<version>1.3.0</version>
+	</skin>
+	<bannerLeft>
+		<src>images/nanohttpd_logo.png</src>
+	</bannerLeft>
+	<bannerRight>
+		<src>images/nanohttpd_logo_text.png</src>
+	</bannerRight>
+	<publishDate position="left" format="yyyy-MM-dd" />
+	<version position="right" />
+	<poweredBy>
+		<logo name="Maven" href="http://maven.apache.org/" img="http://maven.apache.org/images/logos/maven-feather.png" />
+	</poweredBy>
+	<custom>
+		<fluidoSkin>
+			<topBarEnabled>false</topBarEnabled>
+			<sideBarEnabled>true</sideBarEnabled>
+			<gitHub>
+				<projectId>Nanohttpd/nanohttpd</projectId>
+				<ribbonOrientation>right</ribbonOrientation>
+				<ribbonColor>black</ribbonColor>
+			</gitHub>
+		</fluidoSkin>
+	</custom>
+	<body>
+		<breadcrumbs>
+			<item name="${project.name}" href="index.html" />
+		</breadcrumbs>
+		<menu name="Documentation">
+			<item name="About" href="index.html" />
+		</menu>
+		<menu ref="modules" />
+		<menu ref="reports" />
+	</body>
+</project>
\ No newline at end of file
diff --git a/webserver/.gitignore b/webserver/.gitignore
new file mode 100644
index 0000000..868a6b2
--- /dev/null
+++ b/webserver/.gitignore
@@ -0,0 +1,2 @@
+/.settings/
+/LICENSE.txt
diff --git a/webserver/markdown-plugin/pom.xml b/webserver/markdown-plugin/pom.xml
deleted file mode 100644
index 278eb28..0000000
--- a/webserver/markdown-plugin/pom.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-
-    <groupId>fi.iki.elonen</groupId>
-    <artifactId>nanohttpd-webserver-markdown-plugin</artifactId>
-    <version>2.1.0</version>
-    <packaging>jar</packaging>
-
-    <name>NanoHttpd-Webserver-Markdown-Plugin</name>
-    <url>https://github.com/NanoHttpd/nanohttpd</url>
-
-    <dependencies>
-        <dependency>
-            <groupId>fi.iki.elonen</groupId>
-            <artifactId>nanohttpd</artifactId>
-            <version>2.1.0</version>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>fi.iki.elonen</groupId>
-            <artifactId>nanohttpd-webserver</artifactId>
-            <version>2.1.0</version>
-            <scope>provided</scope>
-        </dependency>
-      <dependency>
-          <groupId>org.pegdown</groupId>
-          <artifactId>pegdown</artifactId>
-          <version>1.4.1</version>
-      </dependency>
-    </dependencies>
-
-    <build>
-        <extensions>
-            <extension>
-                <groupId>org.jvnet.wagon-svn</groupId>
-                <artifactId>wagon-svn</artifactId>
-                <version>1.8</version>
-            </extension>
-            <extension>
-                <groupId>org.apache.maven.wagon</groupId>
-                <artifactId>wagon-ftp</artifactId>
-                <version>1.0-alpha-6</version>
-            </extension>
-        </extensions>
-
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>2.2.1</version>
-                <executions>
-                    <execution>
-                        <id>attach-sources</id>
-                        <goals>
-                            <goal>jar</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-release-plugin</artifactId>
-                <version>2.4</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>2.9</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <version>2.3.1</version>
-                <configuration>
-                    <source>1.6</source>
-                    <target>1.6</target>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-assembly-plugin</artifactId>
-                <version>2.2-beta-5</version>
-                <configuration>
-                    <descriptorRefs>
-                        <descriptorRef>jar-with-dependencies</descriptorRef>
-                    </descriptorRefs>
-                    <archive>
-                        <manifest>
-                            <mainClass>fi.iki.elonen.SimpleWebServer</mainClass>
-                        </manifest>
-                    </archive>
-                </configuration>
-                <executions>
-                    <execution>
-                        <phase>package</phase>
-                        <goals>
-                            <goal>single</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
-</project>
diff --git a/webserver/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java b/webserver/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java
deleted file mode 100644
index 647006e..0000000
--- a/webserver/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPlugin.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package fi.iki.elonen;
-
-import org.pegdown.PegDownProcessor;
-
-import java.io.*;
-import java.util.Map;
-
-import static fi.iki.elonen.NanoHTTPD.MIME_HTML;
-import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;
-
-/**
- * @author Paul S. Hawke (paul.hawke@gmail.com)
- *         On: 9/13/13 at 4:03 AM
- */
-public class MarkdownWebServerPlugin implements WebServerPlugin {
-
-    private final PegDownProcessor processor;
-
-    public MarkdownWebServerPlugin() {
-        processor = new PegDownProcessor();
-    }
-
-    @Override public void initialize(Map<String, String> commandLineOptions) {
-    }
-
-    @Override public boolean canServeUri(String uri, File rootDir) {
-        File f = new File(rootDir, uri);
-        return f.exists();
-    }
-
-    @Override
-    public NanoHTTPD.Response serveFile(String uri, Map<String, String> headers, NanoHTTPD.IHTTPSession session, File file, String mimeType) {
-        String markdownSource = readSource(file);
-        return markdownSource == null ? null :
-            new NanoHTTPD.Response(OK, MIME_HTML, processor.markdownToHtml(markdownSource));
-    }
-
-    private String readSource(File file) {
-        FileReader fileReader = null;
-        BufferedReader reader = null;
-        try {
-            fileReader = new FileReader(file);
-            reader = new BufferedReader(fileReader);
-            String line = null;
-            StringBuilder sb = new StringBuilder();
-            do {
-                line = reader.readLine();
-                if (line != null) {
-                    sb.append(line).append("\n");
-                }
-            } while (line != null);
-            reader.close();
-            return sb.toString();
-        } catch (Exception e) {
-            e.printStackTrace();
-            return null;
-        } finally {
-            try {
-                if (fileReader != null) {
-                    fileReader.close();
-                }
-                if (reader != null) {
-                    reader.close();
-                }
-            } catch (IOException ignored) {}
-        }
-    }
-}
diff --git a/webserver/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java b/webserver/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java
deleted file mode 100644
index ff0e34e..0000000
--- a/webserver/markdown-plugin/src/main/java/fi/iki/elonen/MarkdownWebServerPluginInfo.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package fi.iki.elonen;
-
-/**
- * @author Paul S. Hawke (paul.hawke@gmail.com)
- *         On: 9/13/13 at 4:01 AM
- */
-public class MarkdownWebServerPluginInfo implements WebServerPluginInfo {
-    @Override public String[] getMimeTypes() {
-        return new String[]{"text/markdown"};
-    }
-
-    @Override public String[] getIndexFilesForMimeType(String mime) {
-        return new String[]{"index.md"};
-    }
-
-    @Override public WebServerPlugin getWebServerPlugin(String mimeType) {
-        return new MarkdownWebServerPlugin();
-    }
-}
diff --git a/webserver/pom.xml b/webserver/pom.xml
index cfc0bd3..18fd9a4 100644
--- a/webserver/pom.xml
+++ b/webserver/pom.xml
@@ -1,93 +1,55 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-
-    <groupId>fi.iki.elonen</groupId>
-    <artifactId>nanohttpd-webserver</artifactId>
-    <version>2.1.0</version>
-    <packaging>jar</packaging>
-
-    <name>NanoHttpd-Webserver</name>
-    <url>https://github.com/NanoHttpd/nanohttpd</url>
-
-    <dependencies>
-        <dependency>
-            <groupId>fi.iki.elonen</groupId>
-            <artifactId>nanohttpd</artifactId>
-            <version>2.1.0</version>
-        </dependency>
-    </dependencies>
-
-    <build>
-        <extensions>
-            <extension>
-                <groupId>org.jvnet.wagon-svn</groupId>
-                <artifactId>wagon-svn</artifactId>
-                <version>1.8</version>
-            </extension>
-            <extension>
-                <groupId>org.apache.maven.wagon</groupId>
-                <artifactId>wagon-ftp</artifactId>
-                <version>1.0-alpha-6</version>
-            </extension>
-        </extensions>
-
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>2.2.1</version>
-                <executions>
-                    <execution>
-                        <id>attach-sources</id>
-                        <goals>
-                            <goal>jar</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-release-plugin</artifactId>
-                <version>2.4</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>2.9</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <version>2.3.1</version>
-                <configuration>
-                    <source>1.6</source>
-                    <target>1.6</target>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-assembly-plugin</artifactId>
-                <version>2.2-beta-5</version>
-                <configuration>
-                    <descriptorRefs>
-                        <descriptorRef>jar-with-dependencies</descriptorRef>
-                    </descriptorRefs>
-                    <archive>
-                        <manifest>
-                            <mainClass>fi.iki.elonen.SimpleWebServer</mainClass>
-                        </manifest>
-                    </archive>
-                </configuration>
-                <executions>
-                    <execution>
-                        <phase>package</phase>
-                        <goals>
-                            <goal>single</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.nanohttpd</groupId>
+		<artifactId>nanohttpd-project</artifactId>
+		<version>2.2.0</version>
+	</parent>
+	<artifactId>nanohttpd-webserver</artifactId>
+	<packaging>jar</packaging>
+	<name>NanoHttpd-Webserver</name>
+	<description>nanohttpd-webserver can serve any local directory as a webserver using nanohttpd.</description>
+	<dependencies>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>nanohttpd</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+			<version>4.4.1</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<version>2.2-beta-5</version>
+				<configuration>
+					<descriptorRefs>
+						<descriptorRef>jar-with-dependencies</descriptorRef>
+					</descriptorRefs>
+					<archive>
+						<manifest>
+							<mainClass>fi.iki.elonen.SimpleWebServer</mainClass>
+						</manifest>
+					</archive>
+				</configuration>
+				<executions>
+					<execution>
+						<phase>package</phase>
+						<goals>
+							<goal>single</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+	<properties>
+		<minimal.coverage>0.75</minimal.coverage>
+	</properties>
 </project>
diff --git a/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java b/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java
index b84d88e..960f2e7 100644
--- a/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java
+++ b/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java
@@ -1,28 +1,63 @@
 package fi.iki.elonen;
 
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.ByteArrayInputStream;
 import java.util.Map;
 
-import static fi.iki.elonen.NanoHTTPD.Response;
+import fi.iki.elonen.NanoHTTPD.Response;
 
 /**
- * @author Paul S. Hawke (paul.hawke@gmail.com)
- *         On: 9/15/13 at 2:52 PM
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 9/15/13 at 2:52 PM
  */
 public class InternalRewrite extends Response {
+
     private final String uri;
+
     private final Map<String, String> headers;
 
     public InternalRewrite(Map<String, String> headers, String uri) {
-        super(null);
+        super(Status.OK, NanoHTTPD.MIME_HTML, new ByteArrayInputStream(new byte[0]), 0);
         this.headers = headers;
         this.uri = uri;
     }
 
-    public String getUri() {
-        return uri;
+    public Map<String, String> getHeaders() {
+        return this.headers;
     }
 
-    public Map<String, String> getHeaders() {
-        return headers;
+    public String getUri() {
+        return this.uri;
     }
 }
diff --git a/webserver/src/main/java/fi/iki/elonen/ServerRunner.java b/webserver/src/main/java/fi/iki/elonen/ServerRunner.java
deleted file mode 100644
index 313097a..0000000
--- a/webserver/src/main/java/fi/iki/elonen/ServerRunner.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package fi.iki.elonen;
-
-import java.io.IOException;
-
-public class ServerRunner {
-    public static void run(Class serverClass) {
-        try {
-            executeInstance((NanoHTTPD) serverClass.newInstance());
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    }
-
-    public static void executeInstance(NanoHTTPD server) {
-        try {
-            server.start();
-        } catch (IOException ioe) {
-            System.err.println("Couldn't start server:\n" + ioe);
-            System.exit(-1);
-        }
-
-        System.out.println("Server started, Hit Enter to stop.\n");
-
-        try {
-            System.in.read();
-        } catch (Throwable ignored) {
-        }
-
-        server.stop();
-        System.out.println("Server stopped.\n");
-    }
-}
diff --git a/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java b/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java
index ed32dd7..3f176fe 100644
--- a/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java
+++ b/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java
@@ -1,7 +1,41 @@
 package fi.iki.elonen;
 
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.InputStream;
@@ -17,103 +51,46 @@
 import java.util.ServiceLoader;
 import java.util.StringTokenizer;
 
+import fi.iki.elonen.NanoHTTPD.Response.IStatus;
+import fi.iki.elonen.util.ServerRunner;
+
 public class SimpleWebServer extends NanoHTTPD {
-    /**
-     * Common mime type for dynamic content: binary
-     */
-    public static final String MIME_DEFAULT_BINARY = "application/octet-stream";
+
     /**
      * Default Index file names.
      */
-    public static final List<String> INDEX_FILE_NAMES = new ArrayList<String>() {{
-        add("index.html");
-        add("index.htm");
-    }};
-    /**
-     * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
-     */
-    private static final Map<String, String> MIME_TYPES = new HashMap<String, String>() {{
-        put("css", "text/css");
-        put("htm", "text/html");
-        put("html", "text/html");
-        put("xml", "text/xml");
-        put("java", "text/x-java-source, text/java");
-        put("md", "text/plain");
-        put("txt", "text/plain");
-        put("asc", "text/plain");
-        put("gif", "image/gif");
-        put("jpg", "image/jpeg");
-        put("jpeg", "image/jpeg");
-        put("png", "image/png");
-        put("mp3", "audio/mpeg");
-        put("m3u", "audio/mpeg-url");
-        put("mp4", "video/mp4");
-        put("ogv", "video/ogg");
-        put("flv", "video/x-flv");
-        put("mov", "video/quicktime");
-        put("swf", "application/x-shockwave-flash");
-        put("js", "application/javascript");
-        put("pdf", "application/pdf");
-        put("doc", "application/msword");
-        put("ogg", "application/x-ogg");
-        put("zip", "application/octet-stream");
-        put("exe", "application/octet-stream");
-        put("class", "application/octet-stream");
-    }};
+    @SuppressWarnings("serial")
+    public static final List<String> INDEX_FILE_NAMES = new ArrayList<String>() {
+
+        {
+            add("index.html");
+            add("index.htm");
+        }
+    };
+
     /**
      * The distribution licence
      */
-    private static final String LICENCE =
-        "Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias\n"
-            + "\n"
-            + "Redistribution and use in source and binary forms, with or without\n"
-            + "modification, are permitted provided that the following conditions\n"
-            + "are met:\n"
-            + "\n"
-            + "Redistributions of source code must retain the above copyright notice,\n"
-            + "this list of conditions and the following disclaimer. Redistributions in\n"
-            + "binary form must reproduce the above copyright notice, this list of\n"
-            + "conditions and the following disclaimer in the documentation and/or other\n"
-            + "materials provided with the distribution. The name of the author may not\n"
-            + "be used to endorse or promote products derived from this software without\n"
-            + "specific prior written permission. \n"
-            + " \n"
-            + "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"
-            + "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"
-            + "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"
-            + "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"
-            + "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"
-            + "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"
-            + "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"
-            + "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"
-            + "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"
-            + "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
+    private static final String LICENCE;
+    static {
+        mimeTypes();
+        InputStream stream = SimpleWebServer.class.getResourceAsStream("/LICENSE.txt");
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int count;
+        String text;
+        try {
+            while ((count = stream.read(buffer)) >= 0) {
+                bytes.write(buffer, 0, count);
+            }
+            text = bytes.toString("UTF-8");
+        } catch (IOException e) {
+            text = "unknown";
+        }
+        LICENCE = text;
+    }
+
     private static Map<String, WebServerPlugin> mimeTypeHandlers = new HashMap<String, WebServerPlugin>();
-    private final List<File> rootDirs;
-    private final boolean quiet;
-
-    public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) {
-        super(host, port);
-        this.quiet = quiet;
-        this.rootDirs = new ArrayList<File>();
-        this.rootDirs.add(wwwroot);
-
-        this.init();
-    }
-
-    public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet) {
-        super(host, port);
-        this.quiet = quiet;
-        this.rootDirs = new ArrayList<File>(wwwroots);
-
-        this.init();
-    }
-
-	/**
-	 * Used to initialize and customize the server.
-	 */
-    public void init() {
-    }
 
     /**
      * Starts as a standalone file server and waits for Enter.
@@ -122,9 +99,10 @@
         // Defaults
         int port = 8080;
 
-        String host = "127.0.0.1";
+        String host = null; // bind to all interfaces by default
         List<File> rootDirs = new ArrayList<File>();
         boolean quiet = false;
+        String cors = null;
         Map<String, String> options = new HashMap<String, String>();
 
         // Parse command-line, with short and long versions of the options.
@@ -137,8 +115,14 @@
                 quiet = true;
             } else if (args[i].equalsIgnoreCase("-d") || args[i].equalsIgnoreCase("--dir")) {
                 rootDirs.add(new File(args[i + 1]).getAbsoluteFile());
+            } else if (args[i].startsWith("--cors")) {
+                cors = "*";
+                int equalIdx = args[i].indexOf('=');
+                if (equalIdx > 0) {
+                    cors = args[i].substring(equalIdx + 1);
+                }
             } else if (args[i].equalsIgnoreCase("--licence")) {
-                System.out.println(LICENCE + "\n");
+                System.out.println(SimpleWebServer.LICENCE + "\n");
             } else if (args[i].startsWith("-X:")) {
                 int dot = args[i].indexOf('=');
                 if (dot > 0) {
@@ -152,9 +136,8 @@
         if (rootDirs.isEmpty()) {
             rootDirs.add(new File(".").getAbsoluteFile());
         }
-
         options.put("host", host);
-        options.put("port", ""+port);
+        options.put("port", "" + port);
         options.put("quiet", String.valueOf(quiet));
         StringBuilder sb = new StringBuilder();
         for (File dir : rootDirs) {
@@ -163,10 +146,10 @@
             }
             try {
                 sb.append(dir.getCanonicalPath());
-            } catch (IOException ignored) {}
+            } catch (IOException ignored) {
+            }
         }
         options.put("home", sb.toString());
-
         ServiceLoader<WebServerPluginInfo> serviceLoader = ServiceLoader.load(WebServerPluginInfo.class);
         for (WebServerPluginInfo info : serviceLoader) {
             String[] mimeTypes = info.getMimeTypes();
@@ -185,8 +168,7 @@
                 registerPluginForMimeType(indexFiles, mime, info.getWebServerPlugin(mime), options);
             }
         }
-
-        ServerRunner.executeInstance(new SimpleWebServer(host, port, rootDirs, quiet));
+        ServerRunner.executeInstance(new SimpleWebServer(host, port, rootDirs, quiet, cors));
     }
 
     protected static void registerPluginForMimeType(String[] indexFiles, String mimeType, WebServerPlugin plugin, Map<String, String> commandLineOptions) {
@@ -199,40 +181,69 @@
                 int dot = filename.lastIndexOf('.');
                 if (dot >= 0) {
                     String extension = filename.substring(dot + 1).toLowerCase();
-                    MIME_TYPES.put(extension, mimeType);
+                    mimeTypes().put(extension, mimeType);
                 }
             }
-            INDEX_FILE_NAMES.addAll(Arrays.asList(indexFiles));
+            SimpleWebServer.INDEX_FILE_NAMES.addAll(Arrays.asList(indexFiles));
         }
-        mimeTypeHandlers.put(mimeType, plugin);
+        SimpleWebServer.mimeTypeHandlers.put(mimeType, plugin);
         plugin.initialize(commandLineOptions);
     }
 
-    private File getRootDir() {
-        return rootDirs.get(0);
+    private final boolean quiet;
+
+    private final String cors;
+
+    protected List<File> rootDirs;
+
+    public SimpleWebServer(String host, int port, File wwwroot, boolean quiet, String cors) {
+        this(host, port, Collections.singletonList(wwwroot), quiet, cors);
     }
 
-    private List<File> getRootDirs() {
-        return rootDirs;
+    public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) {
+        this(host, port, Collections.singletonList(wwwroot), quiet, null);
     }
 
-    private void addWwwRootDir(File wwwroot) {
-        rootDirs.add(wwwroot);
+    public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet) {
+        this(host, port, wwwroots, quiet, null);
+    }
+
+    public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet, String cors) {
+        super(host, port);
+        this.quiet = quiet;
+        this.cors = cors;
+        this.rootDirs = new ArrayList<File>(wwwroots);
+
+        init();
+    }
+
+    private boolean canServeUri(String uri, File homeDir) {
+        boolean canServeUri;
+        File f = new File(homeDir, uri);
+        canServeUri = f.exists();
+        if (!canServeUri) {
+            WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(getMimeTypeForFile(uri));
+            if (plugin != null) {
+                canServeUri = plugin.canServeUri(uri, homeDir);
+            }
+        }
+        return canServeUri;
     }
 
     /**
-     * URL-encodes everything between "/"-characters. Encodes spaces as '%20' instead of '+'.
+     * URL-encodes everything between "/"-characters. Encodes spaces as '%20'
+     * instead of '+'.
      */
     private String encodeUri(String uri) {
         String newUri = "";
         StringTokenizer st = new StringTokenizer(uri, "/ ", true);
         while (st.hasMoreTokens()) {
             String tok = st.nextToken();
-            if (tok.equals("/"))
+            if (tok.equals("/")) {
                 newUri += "/";
-            else if (tok.equals(" "))
+            } else if (tok.equals(" ")) {
                 newUri += "%20";
-            else {
+            } else {
                 try {
                     newUri += URLEncoder.encode(tok, "UTF-8");
                 } catch (UnsupportedEncodingException ignored) {
@@ -242,12 +253,194 @@
         return newUri;
     }
 
+    private String findIndexFileInDirectory(File directory) {
+        for (String fileName : SimpleWebServer.INDEX_FILE_NAMES) {
+            File indexFile = new File(directory, fileName);
+            if (indexFile.isFile()) {
+                return fileName;
+            }
+        }
+        return null;
+    }
+
+    protected Response getForbiddenResponse(String s) {
+        return newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " + s);
+    }
+
+    protected Response getInternalErrorResponse(String s) {
+        return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERROR: " + s);
+    }
+
+    protected Response getNotFoundResponse() {
+        return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Error 404, file not found.");
+    }
+
+    /**
+     * Used to initialize and customize the server.
+     */
+    public void init() {
+    }
+
+    protected String listDirectory(String uri, File f) {
+        String heading = "Directory " + uri;
+        StringBuilder msg =
+                new StringBuilder("<html><head><title>" + heading + "</title><style><!--\n" + "span.dirname { font-weight: bold; }\n" + "span.filesize { font-size: 75%; }\n"
+                        + "// -->\n" + "</style>" + "</head><body><h1>" + heading + "</h1>");
+
+        String up = null;
+        if (uri.length() > 1) {
+            String u = uri.substring(0, uri.length() - 1);
+            int slash = u.lastIndexOf('/');
+            if (slash >= 0 && slash < u.length()) {
+                up = uri.substring(0, slash + 1);
+            }
+        }
+
+        List<String> files = Arrays.asList(f.list(new FilenameFilter() {
+
+            @Override
+            public boolean accept(File dir, String name) {
+                return new File(dir, name).isFile();
+            }
+        }));
+        Collections.sort(files);
+        List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
+
+            @Override
+            public boolean accept(File dir, String name) {
+                return new File(dir, name).isDirectory();
+            }
+        }));
+        Collections.sort(directories);
+        if (up != null || directories.size() + files.size() > 0) {
+            msg.append("<ul>");
+            if (up != null || directories.size() > 0) {
+                msg.append("<section class=\"directories\">");
+                if (up != null) {
+                    msg.append("<li><a rel=\"directory\" href=\"").append(up).append("\"><span class=\"dirname\">..</span></a></b></li>");
+                }
+                for (String directory : directories) {
+                    String dir = directory + "/";
+                    msg.append("<li><a rel=\"directory\" href=\"").append(encodeUri(uri + dir)).append("\"><span class=\"dirname\">").append(dir)
+                            .append("</span></a></b></li>");
+                }
+                msg.append("</section>");
+            }
+            if (files.size() > 0) {
+                msg.append("<section class=\"files\">");
+                for (String file : files) {
+                    msg.append("<li><a href=\"").append(encodeUri(uri + file)).append("\"><span class=\"filename\">").append(file).append("</span></a>");
+                    File curFile = new File(f, file);
+                    long len = curFile.length();
+                    msg.append("&nbsp;<span class=\"filesize\">(");
+                    if (len < 1024) {
+                        msg.append(len).append(" bytes");
+                    } else if (len < 1024 * 1024) {
+                        msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB");
+                    } else {
+                        msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10000 % 100).append(" MB");
+                    }
+                    msg.append(")</span></li>");
+                }
+                msg.append("</section>");
+            }
+            msg.append("</ul>");
+        }
+        msg.append("</body></html>");
+        return msg.toString();
+    }
+
+    public static Response newFixedLengthResponse(IStatus status, String mimeType, String message) {
+        Response response = NanoHTTPD.newFixedLengthResponse(status, mimeType, message);
+        response.addHeader("Accept-Ranges", "bytes");
+        return response;
+    }
+
+    private Response respond(Map<String, String> headers, IHTTPSession session, String uri) {
+        // First let's handle CORS OPTION query
+        Response r;
+        if (cors != null && Method.OPTIONS.equals(session.getMethod())) {
+            r = new NanoHTTPD.Response(Response.Status.OK, MIME_PLAINTEXT, null, 0);
+        } else {
+            r = defaultRespond(headers, session, uri);
+        }
+
+        if (cors != null) {
+            r = addCORSHeaders(headers, r, cors);
+        }
+        return r;
+    }
+
+    private Response defaultRespond(Map<String, String> headers, IHTTPSession session, String uri) {
+        // Remove URL arguments
+        uri = uri.trim().replace(File.separatorChar, '/');
+        if (uri.indexOf('?') >= 0) {
+            uri = uri.substring(0, uri.indexOf('?'));
+        }
+
+        // Prohibit getting out of current directory
+        if (uri.contains("../")) {
+            return getForbiddenResponse("Won't serve ../ for security reasons.");
+        }
+
+        boolean canServeUri = false;
+        File homeDir = null;
+        for (int i = 0; !canServeUri && i < this.rootDirs.size(); i++) {
+            homeDir = this.rootDirs.get(i);
+            canServeUri = canServeUri(uri, homeDir);
+        }
+        if (!canServeUri) {
+            return getNotFoundResponse();
+        }
+
+        // Browsers get confused without '/' after the directory, send a
+        // redirect.
+        File f = new File(homeDir, uri);
+        if (f.isDirectory() && !uri.endsWith("/")) {
+            uri += "/";
+            Response res =
+                    newFixedLengthResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, "<html><body>Redirected: <a href=\"" + uri + "\">" + uri + "</a></body></html>");
+            res.addHeader("Location", uri);
+            return res;
+        }
+
+        if (f.isDirectory()) {
+            // First look for index files (index.html, index.htm, etc) and if
+            // none found, list the directory if readable.
+            String indexFile = findIndexFileInDirectory(f);
+            if (indexFile == null) {
+                if (f.canRead()) {
+                    // No index file, list the directory if it is readable
+                    return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, listDirectory(uri, f));
+                } else {
+                    return getForbiddenResponse("No directory listing.");
+                }
+            } else {
+                return respond(headers, session, uri + indexFile);
+            }
+        }
+        String mimeTypeForFile = getMimeTypeForFile(uri);
+        WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(mimeTypeForFile);
+        Response response = null;
+        if (plugin != null && plugin.canServeUri(uri, homeDir)) {
+            response = plugin.serveFile(uri, headers, session, f, mimeTypeForFile);
+            if (response != null && response instanceof InternalRewrite) {
+                InternalRewrite rewrite = (InternalRewrite) response;
+                return respond(rewrite.getHeaders(), session, rewrite.getUri());
+            }
+        } else {
+            response = serveFile(uri, headers, f, mimeTypeForFile);
+        }
+        return response != null ? response : getNotFoundResponse();
+    }
+
+    @Override
     public Response serve(IHTTPSession session) {
         Map<String, String> header = session.getHeaders();
         Map<String, String> parms = session.getParms();
         String uri = session.getUri();
 
-        if (!quiet) {
+        if (!this.quiet) {
             System.out.println(session.getMethod() + " '" + uri + "' ");
 
             Iterator<String> e = header.keySet().iterator();
@@ -262,7 +455,7 @@
             }
         }
 
-        for (File homeDir : getRootDirs()) {
+        for (File homeDir : this.rootDirs) {
             // Make sure we won't die of an exception later
             if (!homeDir.isDirectory()) {
                 return getInternalErrorResponse("given path is not a directory (" + homeDir + ").");
@@ -271,100 +464,9 @@
         return respond(Collections.unmodifiableMap(header), session, uri);
     }
 
-    private Response respond(Map<String, String> headers, IHTTPSession session, String uri) {
-        // Remove URL arguments
-        uri = uri.trim().replace(File.separatorChar, '/');
-        if (uri.indexOf('?') >= 0) {
-            uri = uri.substring(0, uri.indexOf('?'));
-        }
-
-        // Prohibit getting out of current directory
-        if (uri.startsWith("src/main") || uri.endsWith("src/main") || uri.contains("../")) {
-            return getForbiddenResponse("Won't serve ../ for security reasons.");
-        }
-
-        boolean canServeUri = false;
-        File homeDir = null;
-        List<File> roots = getRootDirs();
-        for (int i = 0; !canServeUri && i < roots.size(); i++) {
-            homeDir = roots.get(i);
-            canServeUri = canServeUri(uri, homeDir);
-        }
-        if (!canServeUri) {
-            return getNotFoundResponse();
-        }
-
-        // Browsers get confused without '/' after the directory, send a redirect.
-        File f = new File(homeDir, uri);
-        if (f.isDirectory() && !uri.endsWith("/")) {
-            uri += "/";
-            Response res = createResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, "<html><body>Redirected: <a href=\"" +
-                uri + "\">" + uri + "</a></body></html>");
-            res.addHeader("Location", uri);
-            return res;
-        }
-
-        if (f.isDirectory()) {
-            // First look for index files (index.html, index.htm, etc) and if none found, list the directory if readable.
-            String indexFile = findIndexFileInDirectory(f);
-            if (indexFile == null) {
-                if (f.canRead()) {
-                    // No index file, list the directory if it is readable
-                    return createResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, listDirectory(uri, f));
-                } else {
-                    return getForbiddenResponse("No directory listing.");
-                }
-            } else {
-                return respond(headers, session, uri + indexFile);
-            }
-        }
-
-        String mimeTypeForFile = getMimeTypeForFile(uri);
-        WebServerPlugin plugin = mimeTypeHandlers.get(mimeTypeForFile);
-        Response response = null;
-        if (plugin != null) {
-            response = plugin.serveFile(uri, headers, session, f, mimeTypeForFile);
-            if (response != null && response instanceof InternalRewrite) {
-                InternalRewrite rewrite = (InternalRewrite) response;
-                return respond(rewrite.getHeaders(), session, rewrite.getUri());
-            }
-        } else {
-            response = serveFile(uri, headers, f, mimeTypeForFile);
-        }
-        return response != null ? response : getNotFoundResponse();
-    }
-
-    protected Response getNotFoundResponse() {
-        return createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
-            "Error 404, file not found.");
-    }
-
-    protected Response getForbiddenResponse(String s) {
-        return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: "
-            + s);
-    }
-
-    protected Response getInternalErrorResponse(String s) {
-        return createResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
-            "INTERNAL ERRROR: " + s);
-    }
-
-    private boolean canServeUri(String uri, File homeDir) {
-        boolean canServeUri;
-        File f = new File(homeDir, uri);
-        canServeUri = f.exists();
-        if (!canServeUri) {
-            String mimeTypeForFile = getMimeTypeForFile(uri);
-            WebServerPlugin plugin = mimeTypeHandlers.get(mimeTypeForFile);
-            if (plugin != null) {
-                canServeUri = plugin.canServeUri(uri, homeDir);
-            }
-        }
-        return canServeUri;
-    }
-
     /**
-     * Serves file from homeDir and its' subdirectories (only). Uses only URI, ignores all headers and HTTP parameters.
+     * Serves file from homeDir and its' subdirectories (only). Uses only URI,
+     * ignores all headers and HTTP parameters.
      */
     Response serveFile(String uri, Map<String, String> header, File file, String mime) {
         Response res;
@@ -390,12 +492,27 @@
                 }
             }
 
-            // Change return code and add Content-Range header when skipping is requested
+            // get if-range header. If present, it must match etag or else we
+            // should ignore the range request
+            String ifRange = header.get("if-range");
+            boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange));
+
+            String ifNoneMatch = header.get("if-none-match");
+            boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && (ifNoneMatch.equals("*") || ifNoneMatch.equals(etag));
+
+            // Change return code and add Content-Range header when skipping is
+            // requested
             long fileLen = file.length();
-            if (range != null && startFrom >= 0) {
-                if (startFrom >= fileLen) {
-                    res = createResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, "");
-                    res.addHeader("Content-Range", "bytes 0-0/" + fileLen);
+
+            if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) {
+                // range request that matches current etag
+                // and the startFrom of the range is satisfiable
+                if (headerIfNoneMatchPresentAndMatching) {
+                    // range request that matches current etag
+                    // and the startFrom of the range is satisfiable
+                    // would return range from file
+                    // respond with not-modified
+                    res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
                     res.addHeader("ETag", etag);
                 } else {
                     if (endAt < 0) {
@@ -406,25 +523,39 @@
                         newLen = 0;
                     }
 
-                    final long dataLen = newLen;
-                    FileInputStream fis = new FileInputStream(file) {
-                        @Override
-                        public int available() throws IOException {
-                            return (int) dataLen;
-                        }
-                    };
+                    FileInputStream fis = new FileInputStream(file);
                     fis.skip(startFrom);
 
-                    res = createResponse(Response.Status.PARTIAL_CONTENT, mime, fis);
-                    res.addHeader("Content-Length", "" + dataLen);
+                    res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, fis, newLen);
+                    res.addHeader("Accept-Ranges", "bytes");
+                    res.addHeader("Content-Length", "" + newLen);
                     res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
                     res.addHeader("ETag", etag);
                 }
             } else {
-                if (etag.equals(header.get("if-none-match")))
-                    res = createResponse(Response.Status.NOT_MODIFIED, mime, "");
-                else {
-                    res = createResponse(Response.Status.OK, mime, new FileInputStream(file));
+
+                if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) {
+                    // return the size of the file
+                    // 4xx responses are not trumped by if-none-match
+                    res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, "");
+                    res.addHeader("Content-Range", "bytes */" + fileLen);
+                    res.addHeader("ETag", etag);
+                } else if (range == null && headerIfNoneMatchPresentAndMatching) {
+                    // full-file-fetch request
+                    // would return entire file
+                    // respond with not-modified
+                    res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
+                    res.addHeader("ETag", etag);
+                } else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) {
+                    // range request that doesn't match current etag
+                    // would return entire (different) file
+                    // respond with not-modified
+
+                    res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
+                    res.addHeader("ETag", etag);
+                } else {
+                    // supply the file
+                    res = newFixedFileResponse(file, mime);
                     res.addHeader("Content-Length", "" + fileLen);
                     res.addHeader("ETag", etag);
                 }
@@ -436,106 +567,37 @@
         return res;
     }
 
-    // Get MIME type from file name extension, if possible
-    private String getMimeTypeForFile(String uri) {
-        int dot = uri.lastIndexOf('.');
-        String mime = null;
-        if (dot >= 0) {
-            mime = MIME_TYPES.get(uri.substring(dot + 1).toLowerCase());
-        }
-        return mime == null ? MIME_DEFAULT_BINARY : mime;
-    }
-
-    // Announce that the file server accepts partial content requests
-    private Response createResponse(Response.Status status, String mimeType, InputStream message) {
-        Response res = new Response(status, mimeType, message);
+    private Response newFixedFileResponse(File file, String mime) throws FileNotFoundException {
+        Response res;
+        res = newFixedLengthResponse(Response.Status.OK, mime, new FileInputStream(file), (int) file.length());
         res.addHeader("Accept-Ranges", "bytes");
         return res;
     }
 
-    // Announce that the file server accepts partial content requests
-    private Response createResponse(Response.Status status, String mimeType, String message) {
-        Response res = new Response(status, mimeType, message);
-        res.addHeader("Accept-Ranges", "bytes");
-        return res;
+    protected Response addCORSHeaders(Map<String, String> queryHeaders, Response resp, String cors) {
+        resp.addHeader("Access-Control-Allow-Origin", cors);
+        resp.addHeader("Access-Control-Allow-Headers", calculateAllowHeaders(queryHeaders));
+        resp.addHeader("Access-Control-Allow-Credentials", "true");
+        resp.addHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
+        resp.addHeader("Access-Control-Max-Age", "" + MAX_AGE);
+
+        return resp;
     }
 
-    private String findIndexFileInDirectory(File directory) {
-        for (String fileName : INDEX_FILE_NAMES) {
-            File indexFile = new File(directory, fileName);
-            if (indexFile.exists()) {
-                return fileName;
-            }
-        }
-        return null;
+    private String calculateAllowHeaders(Map<String, String> queryHeaders) {
+        // here we should use the given asked headers
+        // but NanoHttpd uses a Map whereas it is possible for requester to send
+        // several time the same header
+        // let's just use default values for this version
+        return System.getProperty(ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME, DEFAULT_ALLOWED_HEADERS);
     }
 
-    protected String listDirectory(String uri, File f) {
-        String heading = "Directory " + uri;
-        StringBuilder msg = new StringBuilder("<html><head><title>" + heading + "</title><style><!--\n" +
-            "span.dirname { font-weight: bold; }\n" +
-            "span.filesize { font-size: 75%; }\n" +
-            "// -->\n" +
-            "</style>" +
-            "</head><body><h1>" + heading + "</h1>");
+    private final static String ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS, HEAD";
 
-        String up = null;
-        if (uri.length() > 1) {
-            String u = uri.substring(0, uri.length() - 1);
-            int slash = u.lastIndexOf('/');
-            if (slash >= 0 && slash < u.length()) {
-                up = uri.substring(0, slash + 1);
-            }
-        }
+    private final static int MAX_AGE = 42 * 60 * 60;
 
-        List<String> files = Arrays.asList(f.list(new FilenameFilter() {
-            @Override
-            public boolean accept(File dir, String name) {
-                return new File(dir, name).isFile();
-            }
-        }));
-        Collections.sort(files);
-        List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
-            @Override
-            public boolean accept(File dir, String name) {
-                return new File(dir, name).isDirectory();
-            }
-        }));
-        Collections.sort(directories);
-        if (up != null || directories.size() + files.size() > 0) {
-            msg.append("<ul>");
-            if (up != null || directories.size() > 0) {
-                msg.append("<section class=\"directories\">");
-                if (up != null) {
-                    msg.append("<li><a rel=\"directory\" href=\"").append(up).append("\"><span class=\"dirname\">..</span></a></b></li>");
-                }
-                for (String directory : directories) {
-                    String dir = directory + "/";
-                    msg.append("<li><a rel=\"directory\" href=\"").append(encodeUri(uri + dir)).append("\"><span class=\"dirname\">").append(dir).append("</span></a></b></li>");
-                }
-                msg.append("</section>");
-            }
-            if (files.size() > 0) {
-                msg.append("<section class=\"files\">");
-                for (String file : files) {
-                    msg.append("<li><a href=\"").append(encodeUri(uri + file)).append("\"><span class=\"filename\">").append(file).append("</span></a>");
-                    File curFile = new File(f, file);
-                    long len = curFile.length();
-                    msg.append("&nbsp;<span class=\"filesize\">(");
-                    if (len < 1024) {
-                        msg.append(len).append(" bytes");
-                    } else if (len < 1024 * 1024) {
-                        msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB");
-                    } else {
-                        msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10 % 100).append(" MB");
-                    }
-                    msg.append(")</span></li>");
-                }
-                msg.append("</section>");
-            }
-            msg.append("</ul>");
-        }
-        msg.append("</body></html>");
-        return msg.toString();
-    }
+    // explicitly relax visibility to package for tests purposes
+    final static String DEFAULT_ALLOWED_HEADERS = "origin,accept,content-type";
+
+    public final static String ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME = "AccessControlAllowHeader";
 }
diff --git a/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java b/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java
index 08a9f22..8b490d3 100644
--- a/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java
+++ b/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java
@@ -1,19 +1,51 @@
 package fi.iki.elonen;
 
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
 import java.io.File;
 import java.util.Map;
 
 import fi.iki.elonen.NanoHTTPD.IHTTPSession;
 
 /**
-* @author Paul S. Hawke (paul.hawke@gmail.com)
-*         On: 9/14/13 at 8:09 AM
-*/
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 9/14/13 at 8:09 AM
+ */
 public interface WebServerPlugin {
 
-    void initialize(Map<String, String> commandLineOptions);
-
     boolean canServeUri(String uri, File rootDir);
 
+    void initialize(Map<String, String> commandLineOptions);
+
     NanoHTTPD.Response serveFile(String uri, Map<String, String> headers, IHTTPSession session, File file, String mimeType);
 }
diff --git a/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java b/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java
index 1e3deb7..0fe5f4e 100644
--- a/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java
+++ b/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java
@@ -1,13 +1,46 @@
 package fi.iki.elonen;
 
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
 /**
-* @author Paul S. Hawke (paul.hawke@gmail.com)
-*         On: 9/14/13 at 8:09 AM
-*/
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 9/14/13 at 8:09 AM
+ */
 public interface WebServerPluginInfo {
-    String[] getMimeTypes();
 
     String[] getIndexFilesForMimeType(String mime);
 
+    String[] getMimeTypes();
+
     WebServerPlugin getWebServerPlugin(String mimeType);
 }
diff --git a/webserver/src/site/site.xml b/webserver/src/site/site.xml
new file mode 100644
index 0000000..4270945
--- /dev/null
+++ b/webserver/src/site/site.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="${project.name}">
+	<skin>
+		<groupId>org.apache.maven.skins</groupId>
+		<artifactId>maven-fluido-skin</artifactId>
+		<version>1.3.0</version>
+	</skin>
+	<bannerLeft>
+		<src>../images/nanohttpd_logo.png</src>
+	</bannerLeft>
+	<bannerRight>
+		<src>../images/nanohttpd_logo_text.png</src>
+	</bannerRight>
+	<publishDate position="left" format="yyyy-MM-dd" />
+	<version position="right" />
+	<poweredBy>
+		<logo name="Maven" href="http://maven.apache.org/"
+			img="http://maven.apache.org/images/logos/maven-feather.png" />
+	</poweredBy>
+	<custom>
+		<fluidoSkin>
+			<topBarEnabled>false</topBarEnabled>
+			<sideBarEnabled>true</sideBarEnabled>
+			<gitHub>
+				<projectId>Nanohttpd/nanohttpd</projectId>
+				<ribbonOrientation>right</ribbonOrientation>
+				<ribbonColor>black</ribbonColor>
+			</gitHub>
+		</fluidoSkin>
+	</custom>
+	<body>
+		<breadcrumbs>
+			<item name="${project.name}" href="index.html" />
+		</breadcrumbs>
+		<menu name="Documentation">
+			<item name="About" href="index.html" />
+		</menu>
+		<menu ref="modules" />
+		<menu ref="reports" />
+	</body>
+</project>
\ No newline at end of file
diff --git a/webserver/src/test/java/fi/iki/elonen/AbstractTestHttpServer.java b/webserver/src/test/java/fi/iki/elonen/AbstractTestHttpServer.java
new file mode 100644
index 0000000..b56c2b0
--- /dev/null
+++ b/webserver/src/test/java/fi/iki/elonen/AbstractTestHttpServer.java
@@ -0,0 +1,67 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.http.HttpEntity;
+
+/**
+ * @author Matthieu Brouillard [matthieu@brouillard.fr]
+ */
+public class AbstractTestHttpServer {
+
+    protected byte[] readContents(HttpEntity entity) throws IOException {
+        InputStream instream = entity.getContent();
+        return readContents(instream);
+    }
+
+    protected byte[] readContents(InputStream instream) throws IOException {
+        byte[] bytes;
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try {
+            byte[] buffer = new byte[1024];
+            int count;
+            while ((count = instream.read(buffer)) >= 0) {
+                out.write(buffer, 0, count);
+            }
+            bytes = out.toByteArray();
+        } finally {
+            instream.close();
+        }
+        return bytes;
+    }
+
+}
diff --git a/webserver/src/test/java/fi/iki/elonen/DummyPlugin.java b/webserver/src/test/java/fi/iki/elonen/DummyPlugin.java
new file mode 100644
index 0000000..09d5cc4
--- /dev/null
+++ b/webserver/src/test/java/fi/iki/elonen/DummyPlugin.java
@@ -0,0 +1,63 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.util.Map;
+
+import fi.iki.elonen.NanoHTTPD.IHTTPSession;
+import fi.iki.elonen.NanoHTTPD.Response;
+import fi.iki.elonen.NanoHTTPD.Response.Status;
+
+public class DummyPlugin implements WebServerPlugin {
+
+    @Override
+    public boolean canServeUri(String uri, File rootDir) {
+        return true;
+    }
+
+    @Override
+    public void initialize(Map<String, String> commandLineOptions) {
+    }
+
+    @Override
+    public Response serveFile(String uri, Map<String, String> headers, IHTTPSession session, File file, String mimeType) {
+        byte[] bytes = "<xml/>".getBytes();
+        InputStream data = new ByteArrayInputStream(bytes);
+        return new Response(Status.OK, "text/xml", data, bytes.length);
+    }
+
+}
diff --git a/webserver/src/test/java/fi/iki/elonen/DummyPluginInfo.java b/webserver/src/test/java/fi/iki/elonen/DummyPluginInfo.java
new file mode 100644
index 0000000..7187573
--- /dev/null
+++ b/webserver/src/test/java/fi/iki/elonen/DummyPluginInfo.java
@@ -0,0 +1,58 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+public class DummyPluginInfo implements WebServerPluginInfo {
+
+    @Override
+    public String[] getIndexFilesForMimeType(String mime) {
+
+        return new String[]{
+            "index.xml"
+        };
+    }
+
+    @Override
+    public String[] getMimeTypes() {
+        return new String[]{
+            "text/xml"
+        };
+    }
+
+    @Override
+    public WebServerPlugin getWebServerPlugin(String mimeType) {
+        return new DummyPlugin();
+    }
+
+}
diff --git a/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServer.java b/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServer.java
new file mode 100644
index 0000000..93f4699
--- /dev/null
+++ b/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServer.java
@@ -0,0 +1,154 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * @author Matthieu Brouillard [matthieu@brouillard.fr]
+ */
+public class TestCorsHttpServer extends AbstractTestHttpServer {
+
+    private static PipedOutputStream stdIn;
+
+    private static Thread serverStartThread;
+
+    @BeforeClass
+    public static void setUp() throws Exception {
+        stdIn = new PipedOutputStream();
+        System.setIn(new PipedInputStream(stdIn));
+        serverStartThread = new Thread(new Runnable() {
+
+            @Override
+            public void run() {
+                String[] args = {
+                    "--host",
+                    "localhost",
+                    "--port",
+                    "9090",
+                    "--dir",
+                    "src/test/resources",
+                    "--cors"
+                };
+                SimpleWebServer.main(args);
+            }
+        });
+        serverStartThread.start();
+        // give the server some tine to start.
+        Thread.sleep(100);
+    }
+
+    @AfterClass
+    public static void tearDown() throws Exception {
+        stdIn.write("\n\n".getBytes());
+        serverStartThread.join(2000);
+        Assert.assertFalse(serverStartThread.isAlive());
+    }
+
+    @Test
+    public void doTestOption() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        HttpOptions httpOption = new HttpOptions("http://localhost:9090/xxx/yyy.html");
+        CloseableHttpResponse response = httpclient.execute(httpOption);
+        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+        Assert.assertNotNull("Cors should have added a header: Access-Control-Allow-Origin", response.getLastHeader("Access-Control-Allow-Origin"));
+        Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Origin: *", "*", response.getLastHeader("Access-Control-Allow-Origin").getValue());
+        response.close();
+    }
+
+    @Test
+    public void doSomeBasicTest() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        HttpGet httpget = new HttpGet("http://localhost:9090/testdir/test.html");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+
+        Assert.assertNotNull("Cors should have added a header: Access-Control-Allow-Origin", response.getLastHeader("Access-Control-Allow-Origin"));
+        Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Origin: *", "*", response.getLastHeader("Access-Control-Allow-Origin").getValue());
+        Assert.assertEquals("<html>\n<head>\n<title>dummy</title>\n</head>\n<body>\n\t<h1>it works</h1>\n</body>\n</html>", string);
+        response.close();
+    }
+
+    @Test
+    public void testAccessControlAllowHeaderUsesDefaultsWithoutSystemProperty() throws Exception {
+        Assert.assertNull("no System " + SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME + " shoudl be set",
+                System.getProperty(SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME));
+
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        HttpOptions httpOption = new HttpOptions("http://localhost:9090/xxx/yyy.html");
+        CloseableHttpResponse response = httpclient.execute(httpOption);
+        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+        Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Headers: " + SimpleWebServer.DEFAULT_ALLOWED_HEADERS,
+                SimpleWebServer.DEFAULT_ALLOWED_HEADERS, response.getLastHeader("Access-Control-Allow-Headers").getValue());
+        response.close();
+    }
+
+    @Test
+    public void testAccessControlAllowHeaderUsesSystemPropertyWhenSet() throws Exception {
+        Assert.assertNull("no System " + SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME + " shoudl be set",
+                System.getProperty(SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME));
+
+        final String expectedValue = "origin";
+        System.setProperty(SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME, expectedValue);
+
+        try {
+            CloseableHttpClient httpclient = HttpClients.createDefault();
+            HttpOptions httpOption = new HttpOptions("http://localhost:9090/xxx/yyy.html");
+            CloseableHttpResponse response = httpclient.execute(httpOption);
+            Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+            Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Headers: " + expectedValue, expectedValue,
+                    response.getLastHeader("Access-Control-Allow-Headers").getValue());
+            response.close();
+        } finally {
+            System.clearProperty(SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME);
+        }
+    }
+}
diff --git a/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServerWithSingleOrigin.java b/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServerWithSingleOrigin.java
new file mode 100644
index 0000000..dbd2c4e
--- /dev/null
+++ b/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServerWithSingleOrigin.java
@@ -0,0 +1,121 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * @author Matthieu Brouillard [matthieu@brouillard.fr]
+ */
+public class TestCorsHttpServerWithSingleOrigin extends AbstractTestHttpServer {
+
+    private static PipedOutputStream stdIn;
+
+    private static Thread serverStartThread;
+
+    @BeforeClass
+    public static void setUp() throws Exception {
+        stdIn = new PipedOutputStream();
+        System.setIn(new PipedInputStream(stdIn));
+        serverStartThread = new Thread(new Runnable() {
+
+            @Override
+            public void run() {
+                String[] args = {
+                    "--host",
+                    "localhost",
+                    "--port",
+                    "9090",
+                    "--dir",
+                    "src/test/resources",
+                    "--cors=http://localhost:9090"
+                };
+                SimpleWebServer.main(args);
+            }
+        });
+        serverStartThread.start();
+        // give the server some tine to start.
+        Thread.sleep(100);
+    }
+
+    @AfterClass
+    public static void tearDown() throws Exception {
+        stdIn.write("\n\n".getBytes());
+        serverStartThread.join(2000);
+        Assert.assertFalse(serverStartThread.isAlive());
+    }
+
+    @Test
+    public void doTestOption() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        HttpOptions httpOption = new HttpOptions("http://localhost:9090/xxx/yyy.html");
+        CloseableHttpResponse response = httpclient.execute(httpOption);
+        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+        Assert.assertNotNull("Cors should have added a header: Access-Control-Allow-Origin", response.getLastHeader("Access-Control-Allow-Origin"));
+        Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Origin: http://localhost:9090", "http://localhost:9090",
+                response.getLastHeader("Access-Control-Allow-Origin").getValue());
+        response.close();
+    }
+
+    @Test
+    public void doSomeBasicTest() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        HttpGet httpget = new HttpGet("http://localhost:9090/testdir/test.html");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+
+        Assert.assertNotNull("Cors should have added a header: Access-Control-Allow-Origin", response.getLastHeader("Access-Control-Allow-Origin"));
+        Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Origin: http://localhost:9090", "http://localhost:9090",
+                response.getLastHeader("Access-Control-Allow-Origin").getValue());
+        Assert.assertEquals("<html>\n<head>\n<title>dummy</title>\n</head>\n<body>\n\t<h1>it works</h1>\n</body>\n</html>", string);
+        response.close();
+    }
+}
diff --git a/webserver/src/test/java/fi/iki/elonen/TestHttpServer.java b/webserver/src/test/java/fi/iki/elonen/TestHttpServer.java
new file mode 100644
index 0000000..bb16741
--- /dev/null
+++ b/webserver/src/test/java/fi/iki/elonen/TestHttpServer.java
@@ -0,0 +1,149 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestHttpServer extends AbstractTestHttpServer {
+
+    private static PipedOutputStream stdIn;
+
+    private static Thread serverStartThread;
+
+    @BeforeClass
+    public static void setUp() throws Exception {
+        stdIn = new PipedOutputStream();
+        System.setIn(new PipedInputStream(stdIn));
+        serverStartThread = new Thread(new Runnable() {
+
+            @Override
+            public void run() {
+                String[] args = {
+                    "--host",
+                    "localhost",
+                    "--port",
+                    "9090",
+                    "--dir",
+                    "src/test/resources"
+                };
+                SimpleWebServer.main(args);
+            }
+        });
+        serverStartThread.start();
+        // give the server some tine to start.
+        Thread.sleep(100);
+    }
+
+    @AfterClass
+    public static void tearDown() throws Exception {
+        stdIn.write("\n\n".getBytes());
+        serverStartThread.join(2000);
+        Assert.assertFalse(serverStartThread.isAlive());
+    }
+
+    @Test
+    public void doTest404() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        HttpGet httpget = new HttpGet("http://localhost:9090/xxx/yyy.html");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        Assert.assertEquals(404, response.getStatusLine().getStatusCode());
+        response.close();
+    }
+
+    @Test
+    public void doPlugin() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        HttpGet httpget = new HttpGet("http://localhost:9090/index.xml");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        String string = new String(readContents(response.getEntity()), "UTF-8");
+        Assert.assertEquals("<xml/>", string);
+        response.close();
+
+        httpget = new HttpGet("http://localhost:9090/testdir/testdir/different.xml");
+        response = httpclient.execute(httpget);
+        string = new String(readContents(response.getEntity()), "UTF-8");
+        Assert.assertEquals("<xml/>", string);
+        response.close();
+    }
+
+    @Test
+    public void doSomeBasicTest() throws Exception {
+        CloseableHttpClient httpclient = HttpClients.createDefault();
+        HttpGet httpget = new HttpGet("http://localhost:9090/testdir/test.html");
+        CloseableHttpResponse response = httpclient.execute(httpget);
+        HttpEntity entity = response.getEntity();
+        String string = new String(readContents(entity), "UTF-8");
+        Assert.assertEquals("<html>\n<head>\n<title>dummy</title>\n</head>\n<body>\n\t<h1>it works</h1>\n</body>\n</html>", string);
+        response.close();
+
+        httpget = new HttpGet("http://localhost:9090/");
+        response = httpclient.execute(httpget);
+        entity = response.getEntity();
+        string = new String(readContents(entity), "UTF-8");
+        Assert.assertTrue(string.indexOf("testdir") > 0);
+        response.close();
+
+        httpget = new HttpGet("http://localhost:9090/testdir");
+        response = httpclient.execute(httpget);
+        entity = response.getEntity();
+        string = new String(readContents(entity), "UTF-8");
+        Assert.assertTrue(string.indexOf("test.html") > 0);
+        response.close();
+
+        httpget = new HttpGet("http://localhost:9090/testdir/testpdf.pdf");
+        response = httpclient.execute(httpget);
+        entity = response.getEntity();
+
+        byte[] actual = readContents(entity);
+        byte[] expected = readContents(new FileInputStream("src/test/resources/testdir/testpdf.pdf"));
+        Assert.assertArrayEquals(expected, actual);
+        response.close();
+
+    }
+}
diff --git a/webserver/src/test/resources/META-INF/services/fi.iki.elonen.WebServerPluginInfo b/webserver/src/test/resources/META-INF/services/fi.iki.elonen.WebServerPluginInfo
new file mode 100644
index 0000000..8819204
--- /dev/null
+++ b/webserver/src/test/resources/META-INF/services/fi.iki.elonen.WebServerPluginInfo
@@ -0,0 +1 @@
+fi.iki.elonen.DummyPluginInfo
\ No newline at end of file
diff --git a/webserver/src/test/resources/testdir/test.html b/webserver/src/test/resources/testdir/test.html
new file mode 100644
index 0000000..4cb157c
--- /dev/null
+++ b/webserver/src/test/resources/testdir/test.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>dummy</title>
+</head>
+<body>
+	<h1>it works</h1>
+</body>
+</html>
\ No newline at end of file
diff --git a/webserver/src/test/resources/testdir/testdir/different.xml b/webserver/src/test/resources/testdir/testdir/different.xml
new file mode 100644
index 0000000..e7a01d5
--- /dev/null
+++ b/webserver/src/test/resources/testdir/testdir/different.xml
@@ -0,0 +1,3 @@
+<xml>
+	This sould not show up ;-)
+</xml>
\ No newline at end of file
diff --git a/webserver/src/test/resources/testdir/testpdf.pdf b/webserver/src/test/resources/testdir/testpdf.pdf
new file mode 100644
index 0000000..da8161f
--- /dev/null
+++ b/webserver/src/test/resources/testdir/testpdf.pdf
Binary files differ
diff --git a/websocket/.gitignore b/websocket/.gitignore
new file mode 100644
index 0000000..868a6b2
--- /dev/null
+++ b/websocket/.gitignore
@@ -0,0 +1,2 @@
+/.settings/
+/LICENSE.txt
diff --git a/websocket/pom.xml b/websocket/pom.xml
index f9d9703..4f66283 100644
--- a/websocket/pom.xml
+++ b/websocket/pom.xml
@@ -1,104 +1,68 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-
-    <groupId>fi.iki.elonen</groupId>
-    <artifactId>nanohttpd-websocket</artifactId>
-    <version>2.1.0</version>
-    <packaging>jar</packaging>
-
-    <name>NanoHttpd-Websocket</name>
-    <url>https://github.com/NanoHttpd/nanohttpd</url>
-
-    <dependencies>
-        <dependency>
-          <groupId>junit</groupId>
-          <artifactId>junit</artifactId>
-          <version>4.8.2</version>
-          <scope>test</scope>
-        </dependency>
-        <dependency>
-        	<groupId>org.mockito</groupId>
-        	<artifactId>mockito-all</artifactId>
-        	<version>1.9.5</version>
-        </dependency>
-        <dependency>
-            <groupId>fi.iki.elonen</groupId>
-            <artifactId>nanohttpd</artifactId>
-            <version>2.1.0</version>
-        </dependency>
-    </dependencies>
-
-    <build>
-        <extensions>
-            <extension>
-                <groupId>org.jvnet.wagon-svn</groupId>
-                <artifactId>wagon-svn</artifactId>
-                <version>1.8</version>
-            </extension>
-            <extension>
-                <groupId>org.apache.maven.wagon</groupId>
-                <artifactId>wagon-ftp</artifactId>
-                <version>1.0-alpha-6</version>
-            </extension>
-        </extensions>
-
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>2.2.1</version>
-                <executions>
-                    <execution>
-                        <id>attach-sources</id>
-                        <goals>
-                            <goal>jar</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-release-plugin</artifactId>
-                <version>2.4</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>2.9</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <version>2.3.1</version>
-                <configuration>
-                    <source>1.6</source>
-                    <target>1.6</target>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-assembly-plugin</artifactId>
-                <version>2.2-beta-5</version>
-                <configuration>
-                    <descriptorRefs>
-                        <descriptorRef>jar-with-dependencies</descriptorRef>
-                    </descriptorRefs>
-                    <archive>
-                        <manifest>
-                            <mainClass>fi.iki.elonen.NanoWebSocketServer</mainClass>
-                        </manifest>
-                    </archive>
-                </configuration>
-                <executions>
-                    <execution>
-                        <phase>package</phase>
-                        <goals>
-                            <goal>single</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.nanohttpd</groupId>
+		<artifactId>nanohttpd-project</artifactId>
+		<version>2.2.0</version>
+	</parent>
+	<artifactId>nanohttpd-websocket</artifactId>
+	<packaging>jar</packaging>
+	<name>NanoHttpd-Websocket</name>
+	<description>nanohttpd-websocket is a very low profile websocket server based on nanohttpd.</description>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>default-jar</id>
+						<configuration>
+							<excludes>
+								<exclude>**/samples/**</exclude>
+							</excludes>
+						</configuration>
+					</execution>
+					<execution>
+						<id>echo-jar</id>
+						<phase>package</phase>
+						<goals>
+							<goal>jar</goal>
+						</goals>
+						<configuration>
+							<classifier>echo</classifier>
+							<archive>
+								<manifest>
+									<addClasspath>true</addClasspath>
+									<mainClass>fi.iki.elonen.samples.echo.EchoSocketSample</mainClass>
+								</manifest>
+							</archive>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+	<dependencies>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>nanohttpd</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.mockito</groupId>
+			<artifactId>mockito-all</artifactId>
+			<version>1.9.5</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.jetty.websocket</groupId>
+			<artifactId>websocket-client</artifactId>
+			<version>9.3.0.M2</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<properties>
+		<minimal.coverage>0.67</minimal.coverage>
+	</properties>
 </project>
diff --git a/websocket/src/main/java/fi/iki/elonen/IWebSocketFactory.java b/websocket/src/main/java/fi/iki/elonen/IWebSocketFactory.java
deleted file mode 100644
index 3e38cd2..0000000
--- a/websocket/src/main/java/fi/iki/elonen/IWebSocketFactory.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package fi.iki.elonen;
-
-import fi.iki.elonen.NanoHTTPD.IHTTPSession;
-
-public interface IWebSocketFactory {
-    WebSocket openWebSocket(IHTTPSession handshake);
-}
diff --git a/websocket/src/main/java/fi/iki/elonen/NanoWSD.java b/websocket/src/main/java/fi/iki/elonen/NanoWSD.java
new file mode 100644
index 0000000..b0c72f5
--- /dev/null
+++ b/websocket/src/main/java/fi/iki/elonen/NanoWSD.java
@@ -0,0 +1,876 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode;
+import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseFrame;
+import fi.iki.elonen.NanoWSD.WebSocketFrame.OpCode;
+
+public abstract class NanoWSD extends NanoHTTPD {
+
+    public static enum State {
+        UNCONNECTED,
+        CONNECTING,
+        OPEN,
+        CLOSING,
+        CLOSED
+    }
+
+    public static abstract class WebSocket {
+
+        private final InputStream in;
+
+        private OutputStream out;
+
+        private WebSocketFrame.OpCode continuousOpCode = null;
+
+        private final List<WebSocketFrame> continuousFrames = new LinkedList<WebSocketFrame>();
+
+        private State state = State.UNCONNECTED;
+
+        private final NanoHTTPD.IHTTPSession handshakeRequest;
+
+        private final NanoHTTPD.Response handshakeResponse = new NanoHTTPD.Response(NanoHTTPD.Response.Status.SWITCH_PROTOCOL, null, (InputStream) null, 0) {
+
+            @Override
+            protected void send(OutputStream out) {
+                WebSocket.this.out = out;
+                WebSocket.this.state = State.CONNECTING;
+                super.send(out);
+                WebSocket.this.state = State.OPEN;
+                WebSocket.this.onOpen();
+                readWebsocket();
+            }
+        };
+
+        public WebSocket(NanoHTTPD.IHTTPSession handshakeRequest) {
+            this.handshakeRequest = handshakeRequest;
+            this.in = handshakeRequest.getInputStream();
+
+            this.handshakeResponse.addHeader(NanoWSD.HEADER_UPGRADE, NanoWSD.HEADER_UPGRADE_VALUE);
+            this.handshakeResponse.addHeader(NanoWSD.HEADER_CONNECTION, NanoWSD.HEADER_CONNECTION_VALUE);
+        }
+
+        public boolean isOpen() {
+            return state == State.OPEN;
+        }
+
+        protected abstract void onOpen();
+
+        protected abstract void onClose(CloseCode code, String reason, boolean initiatedByRemote);
+
+        protected abstract void onMessage(WebSocketFrame message);
+
+        protected abstract void onPong(WebSocketFrame pong);
+
+        protected abstract void onException(IOException exception);
+
+        /**
+         * Debug method. <b>Do not Override unless for debug purposes!</b>
+         * 
+         * @param frame
+         *            The received WebSocket Frame.
+         */
+        protected void debugFrameReceived(WebSocketFrame frame) {
+        }
+
+        /**
+         * Debug method. <b>Do not Override unless for debug purposes!</b><br>
+         * This method is called before actually sending the frame.
+         * 
+         * @param frame
+         *            The sent WebSocket Frame.
+         */
+        protected void debugFrameSent(WebSocketFrame frame) {
+        }
+
+        public void close(CloseCode code, String reason, boolean initiatedByRemote) throws IOException {
+            State oldState = this.state;
+            this.state = State.CLOSING;
+            if (oldState == State.OPEN) {
+                sendFrame(new CloseFrame(code, reason));
+            } else {
+                doClose(code, reason, initiatedByRemote);
+            }
+        }
+
+        private void doClose(CloseCode code, String reason, boolean initiatedByRemote) {
+            if (this.state == State.CLOSED) {
+                return;
+            }
+            if (this.in != null) {
+                try {
+                    this.in.close();
+                } catch (IOException e) {
+                    NanoWSD.LOG.log(Level.FINE, "close failed", e);
+                }
+            }
+            if (this.out != null) {
+                try {
+                    this.out.close();
+                } catch (IOException e) {
+                    NanoWSD.LOG.log(Level.FINE, "close failed", e);
+                }
+            }
+            this.state = State.CLOSED;
+            onClose(code, reason, initiatedByRemote);
+        }
+
+        // --------------------------------IO--------------------------------------
+
+        public NanoHTTPD.IHTTPSession getHandshakeRequest() {
+            return this.handshakeRequest;
+        }
+
+        public NanoHTTPD.Response getHandshakeResponse() {
+            return this.handshakeResponse;
+        }
+
+        private void handleCloseFrame(WebSocketFrame frame) throws IOException {
+            CloseCode code = CloseCode.NormalClosure;
+            String reason = "";
+            if (frame instanceof CloseFrame) {
+                code = ((CloseFrame) frame).getCloseCode();
+                reason = ((CloseFrame) frame).getCloseReason();
+            }
+            if (this.state == State.CLOSING) {
+                // Answer for my requested close
+                doClose(code, reason, false);
+            } else {
+                close(code, reason, true);
+            }
+        }
+
+        private void handleFrameFragment(WebSocketFrame frame) throws IOException {
+            if (frame.getOpCode() != OpCode.Continuation) {
+                // First
+                if (this.continuousOpCode != null) {
+                    throw new WebSocketException(CloseCode.ProtocolError, "Previous continuous frame sequence not completed.");
+                }
+                this.continuousOpCode = frame.getOpCode();
+                this.continuousFrames.clear();
+                this.continuousFrames.add(frame);
+            } else if (frame.isFin()) {
+                // Last
+                if (this.continuousOpCode == null) {
+                    throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
+                }
+                onMessage(new WebSocketFrame(this.continuousOpCode, this.continuousFrames));
+                this.continuousOpCode = null;
+                this.continuousFrames.clear();
+            } else if (this.continuousOpCode == null) {
+                // Unexpected
+                throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
+            } else {
+                // Intermediate
+                this.continuousFrames.add(frame);
+            }
+        }
+
+        private void handleWebsocketFrame(WebSocketFrame frame) throws IOException {
+            debugFrameReceived(frame);
+            if (frame.getOpCode() == OpCode.Close) {
+                handleCloseFrame(frame);
+            } else if (frame.getOpCode() == OpCode.Ping) {
+                sendFrame(new WebSocketFrame(OpCode.Pong, true, frame.getBinaryPayload()));
+            } else if (frame.getOpCode() == OpCode.Pong) {
+                onPong(frame);
+            } else if (!frame.isFin() || frame.getOpCode() == OpCode.Continuation) {
+                handleFrameFragment(frame);
+            } else if (this.continuousOpCode != null) {
+                throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence not completed.");
+            } else if (frame.getOpCode() == OpCode.Text || frame.getOpCode() == OpCode.Binary) {
+                onMessage(frame);
+            } else {
+                throw new WebSocketException(CloseCode.ProtocolError, "Non control or continuous frame expected.");
+            }
+        }
+
+        // --------------------------------Close-----------------------------------
+
+        public void ping(byte[] payload) throws IOException {
+            sendFrame(new WebSocketFrame(OpCode.Ping, true, payload));
+        }
+
+        // --------------------------------Public
+        // Facade---------------------------
+
+        private void readWebsocket() {
+            try {
+                while (this.state == State.OPEN) {
+                    handleWebsocketFrame(WebSocketFrame.read(this.in));
+                }
+            } catch (CharacterCodingException e) {
+                onException(e);
+                doClose(CloseCode.InvalidFramePayloadData, e.toString(), false);
+            } catch (IOException e) {
+                onException(e);
+                if (e instanceof WebSocketException) {
+                    doClose(((WebSocketException) e).getCode(), ((WebSocketException) e).getReason(), false);
+                }
+            } finally {
+                doClose(CloseCode.InternalServerError, "Handler terminated without closing the connection.", false);
+            }
+        }
+
+        public void send(byte[] payload) throws IOException {
+            sendFrame(new WebSocketFrame(OpCode.Binary, true, payload));
+        }
+
+        public void send(String payload) throws IOException {
+            sendFrame(new WebSocketFrame(OpCode.Text, true, payload));
+        }
+
+        public synchronized void sendFrame(WebSocketFrame frame) throws IOException {
+            debugFrameSent(frame);
+            frame.write(this.out);
+        }
+    }
+
+    public static class WebSocketException extends IOException {
+
+        private static final long serialVersionUID = 1L;
+
+        private final CloseCode code;
+
+        private final String reason;
+
+        public WebSocketException(CloseCode code, String reason) {
+            this(code, reason, null);
+        }
+
+        public WebSocketException(CloseCode code, String reason, Exception cause) {
+            super(code + ": " + reason, cause);
+            this.code = code;
+            this.reason = reason;
+        }
+
+        public WebSocketException(Exception cause) {
+            this(CloseCode.InternalServerError, cause.toString(), cause);
+        }
+
+        public CloseCode getCode() {
+            return this.code;
+        }
+
+        public String getReason() {
+            return this.reason;
+        }
+    }
+
+    public static class WebSocketFrame {
+
+        public static enum CloseCode {
+            NormalClosure(1000),
+            GoingAway(1001),
+            ProtocolError(1002),
+            UnsupportedData(1003),
+            NoStatusRcvd(1005),
+            AbnormalClosure(1006),
+            InvalidFramePayloadData(1007),
+            PolicyViolation(1008),
+            MessageTooBig(1009),
+            MandatoryExt(1010),
+            InternalServerError(1011),
+            TLSHandshake(1015);
+
+            public static CloseCode find(int value) {
+                for (CloseCode code : values()) {
+                    if (code.getValue() == value) {
+                        return code;
+                    }
+                }
+                return null;
+            }
+
+            private final int code;
+
+            private CloseCode(int code) {
+                this.code = code;
+            }
+
+            public int getValue() {
+                return this.code;
+            }
+        }
+
+        public static class CloseFrame extends WebSocketFrame {
+
+            private static byte[] generatePayload(CloseCode code, String closeReason) throws CharacterCodingException {
+                if (code != null) {
+                    byte[] reasonBytes = text2Binary(closeReason);
+                    byte[] payload = new byte[reasonBytes.length + 2];
+                    payload[0] = (byte) (code.getValue() >> 8 & 0xFF);
+                    payload[1] = (byte) (code.getValue() & 0xFF);
+                    System.arraycopy(reasonBytes, 0, payload, 2, reasonBytes.length);
+                    return payload;
+                } else {
+                    return new byte[0];
+                }
+            }
+
+            private CloseCode _closeCode;
+
+            private String _closeReason;
+
+            public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException {
+                super(OpCode.Close, true, generatePayload(code, closeReason));
+            }
+
+            private CloseFrame(WebSocketFrame wrap) throws CharacterCodingException {
+                super(wrap);
+                assert wrap.getOpCode() == OpCode.Close;
+                if (wrap.getBinaryPayload().length >= 2) {
+                    this._closeCode = CloseCode.find((wrap.getBinaryPayload()[0] & 0xFF) << 8 | wrap.getBinaryPayload()[1] & 0xFF);
+                    this._closeReason = binary2Text(getBinaryPayload(), 2, getBinaryPayload().length - 2);
+                }
+            }
+
+            public CloseCode getCloseCode() {
+                return this._closeCode;
+            }
+
+            public String getCloseReason() {
+                return this._closeReason;
+            }
+        }
+
+        public static enum OpCode {
+            Continuation(0),
+            Text(1),
+            Binary(2),
+            Close(8),
+            Ping(9),
+            Pong(10);
+
+            public static OpCode find(byte value) {
+                for (OpCode opcode : values()) {
+                    if (opcode.getValue() == value) {
+                        return opcode;
+                    }
+                }
+                return null;
+            }
+
+            private final byte code;
+
+            private OpCode(int code) {
+                this.code = (byte) code;
+            }
+
+            public byte getValue() {
+                return this.code;
+            }
+
+            public boolean isControlFrame() {
+                return this == Close || this == Ping || this == Pong;
+            }
+        }
+
+        public static final Charset TEXT_CHARSET = Charset.forName("UTF-8");
+
+        public static String binary2Text(byte[] payload) throws CharacterCodingException {
+            return new String(payload, WebSocketFrame.TEXT_CHARSET);
+        }
+
+        public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException {
+            return new String(payload, offset, length, WebSocketFrame.TEXT_CHARSET);
+        }
+
+        private static int checkedRead(int read) throws IOException {
+            if (read < 0) {
+                throw new EOFException();
+            }
+            return read;
+        }
+
+        public static WebSocketFrame read(InputStream in) throws IOException {
+            byte head = (byte) checkedRead(in.read());
+            boolean fin = (head & 0x80) != 0;
+            OpCode opCode = OpCode.find((byte) (head & 0x0F));
+            if ((head & 0x70) != 0) {
+                throw new WebSocketException(CloseCode.ProtocolError, "The reserved bits (" + Integer.toBinaryString(head & 0x70) + ") must be 0.");
+            }
+            if (opCode == null) {
+                throw new WebSocketException(CloseCode.ProtocolError, "Received frame with reserved/unknown opcode " + (head & 0x0F) + ".");
+            } else if (opCode.isControlFrame() && !fin) {
+                throw new WebSocketException(CloseCode.ProtocolError, "Fragmented control frame.");
+            }
+
+            WebSocketFrame frame = new WebSocketFrame(opCode, fin);
+            frame.readPayloadInfo(in);
+            frame.readPayload(in);
+            if (frame.getOpCode() == OpCode.Close) {
+                return new CloseFrame(frame);
+            } else {
+                return frame;
+            }
+        }
+
+        public static byte[] text2Binary(String payload) throws CharacterCodingException {
+            return payload.getBytes(WebSocketFrame.TEXT_CHARSET);
+        }
+
+        private OpCode opCode;
+
+        private boolean fin;
+
+        private byte[] maskingKey;
+
+        private byte[] payload;
+
+        // --------------------------------GETTERS---------------------------------
+
+        private transient int _payloadLength;
+
+        private transient String _payloadString;
+
+        private WebSocketFrame(OpCode opCode, boolean fin) {
+            setOpCode(opCode);
+            setFin(fin);
+        }
+
+        public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload) {
+            this(opCode, fin, payload, null);
+        }
+
+        public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey) {
+            this(opCode, fin);
+            setMaskingKey(maskingKey);
+            setBinaryPayload(payload);
+        }
+
+        public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException {
+            this(opCode, fin, payload, null);
+        }
+
+        public WebSocketFrame(OpCode opCode, boolean fin, String payload, byte[] maskingKey) throws CharacterCodingException {
+            this(opCode, fin);
+            setMaskingKey(maskingKey);
+            setTextPayload(payload);
+        }
+
+        public WebSocketFrame(OpCode opCode, List<WebSocketFrame> fragments) throws WebSocketException {
+            setOpCode(opCode);
+            setFin(true);
+
+            long _payloadLength = 0;
+            for (WebSocketFrame inter : fragments) {
+                _payloadLength += inter.getBinaryPayload().length;
+            }
+            if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
+                throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
+            }
+            this._payloadLength = (int) _payloadLength;
+            byte[] payload = new byte[this._payloadLength];
+            int offset = 0;
+            for (WebSocketFrame inter : fragments) {
+                System.arraycopy(inter.getBinaryPayload(), 0, payload, offset, inter.getBinaryPayload().length);
+                offset += inter.getBinaryPayload().length;
+            }
+            setBinaryPayload(payload);
+        }
+
+        public WebSocketFrame(WebSocketFrame clone) {
+            setOpCode(clone.getOpCode());
+            setFin(clone.isFin());
+            setBinaryPayload(clone.getBinaryPayload());
+            setMaskingKey(clone.getMaskingKey());
+        }
+
+        public byte[] getBinaryPayload() {
+            return this.payload;
+        }
+
+        public byte[] getMaskingKey() {
+            return this.maskingKey;
+        }
+
+        public OpCode getOpCode() {
+            return this.opCode;
+        }
+
+        // --------------------------------SERIALIZATION---------------------------
+
+        public String getTextPayload() {
+            if (this._payloadString == null) {
+                try {
+                    this._payloadString = binary2Text(getBinaryPayload());
+                } catch (CharacterCodingException e) {
+                    throw new RuntimeException("Undetected CharacterCodingException", e);
+                }
+            }
+            return this._payloadString;
+        }
+
+        public boolean isFin() {
+            return this.fin;
+        }
+
+        public boolean isMasked() {
+            return this.maskingKey != null && this.maskingKey.length == 4;
+        }
+
+        private String payloadToString() {
+            if (this.payload == null) {
+                return "null";
+            } else {
+                final StringBuilder sb = new StringBuilder();
+                sb.append('[').append(this.payload.length).append("b] ");
+                if (getOpCode() == OpCode.Text) {
+                    String text = getTextPayload();
+                    if (text.length() > 100) {
+                        sb.append(text.substring(0, 100)).append("...");
+                    } else {
+                        sb.append(text);
+                    }
+                } else {
+                    sb.append("0x");
+                    for (int i = 0; i < Math.min(this.payload.length, 50); ++i) {
+                        sb.append(Integer.toHexString(this.payload[i] & 0xFF));
+                    }
+                    if (this.payload.length > 50) {
+                        sb.append("...");
+                    }
+                }
+                return sb.toString();
+            }
+        }
+
+        private void readPayload(InputStream in) throws IOException {
+            this.payload = new byte[this._payloadLength];
+            int read = 0;
+            while (read < this._payloadLength) {
+                read += checkedRead(in.read(this.payload, read, this._payloadLength - read));
+            }
+
+            if (isMasked()) {
+                for (int i = 0; i < this.payload.length; i++) {
+                    this.payload[i] ^= this.maskingKey[i % 4];
+                }
+            }
+
+            // Test for Unicode errors
+            if (getOpCode() == OpCode.Text) {
+                this._payloadString = binary2Text(getBinaryPayload());
+            }
+        }
+
+        // --------------------------------ENCODING--------------------------------
+
+        private void readPayloadInfo(InputStream in) throws IOException {
+            byte b = (byte) checkedRead(in.read());
+            boolean masked = (b & 0x80) != 0;
+
+            this._payloadLength = (byte) (0x7F & b);
+            if (this._payloadLength == 126) {
+                // checkedRead must return int for this to work
+                this._payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF;
+                if (this._payloadLength < 126) {
+                    throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 2byte length. (not using minimal length encoding)");
+                }
+            } else if (this._payloadLength == 127) {
+                long _payloadLength =
+                        (long) checkedRead(in.read()) << 56 | (long) checkedRead(in.read()) << 48 | (long) checkedRead(in.read()) << 40 | (long) checkedRead(in.read()) << 32
+                                | checkedRead(in.read()) << 24 | checkedRead(in.read()) << 16 | checkedRead(in.read()) << 8 | checkedRead(in.read());
+                if (_payloadLength < 65536) {
+                    throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 4byte length. (not using minimal length encoding)");
+                }
+                if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
+                    throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
+                }
+                this._payloadLength = (int) _payloadLength;
+            }
+
+            if (this.opCode.isControlFrame()) {
+                if (this._payloadLength > 125) {
+                    throw new WebSocketException(CloseCode.ProtocolError, "Control frame with payload length > 125 bytes.");
+                }
+                if (this.opCode == OpCode.Close && this._payloadLength == 1) {
+                    throw new WebSocketException(CloseCode.ProtocolError, "Received close frame with payload len 1.");
+                }
+            }
+
+            if (masked) {
+                this.maskingKey = new byte[4];
+                int read = 0;
+                while (read < this.maskingKey.length) {
+                    read += checkedRead(in.read(this.maskingKey, read, this.maskingKey.length - read));
+                }
+            }
+        }
+
+        public void setBinaryPayload(byte[] payload) {
+            this.payload = payload;
+            this._payloadLength = payload.length;
+            this._payloadString = null;
+        }
+
+        public void setFin(boolean fin) {
+            this.fin = fin;
+        }
+
+        public void setMaskingKey(byte[] maskingKey) {
+            if (maskingKey != null && maskingKey.length != 4) {
+                throw new IllegalArgumentException("MaskingKey " + Arrays.toString(maskingKey) + " hasn't length 4");
+            }
+            this.maskingKey = maskingKey;
+        }
+
+        public void setOpCode(OpCode opcode) {
+            this.opCode = opcode;
+        }
+
+        public void setTextPayload(String payload) throws CharacterCodingException {
+            this.payload = text2Binary(payload);
+            this._payloadLength = payload.length();
+            this._payloadString = payload;
+        }
+
+        // --------------------------------CONSTANTS-------------------------------
+
+        public void setUnmasked() {
+            setMaskingKey(null);
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder("WS[");
+            sb.append(getOpCode());
+            sb.append(", ").append(isFin() ? "fin" : "inter");
+            sb.append(", ").append(isMasked() ? "masked" : "unmasked");
+            sb.append(", ").append(payloadToString());
+            sb.append(']');
+            return sb.toString();
+        }
+
+        // ------------------------------------------------------------------------
+
+        public void write(OutputStream out) throws IOException {
+            byte header = 0;
+            if (this.fin) {
+                header |= 0x80;
+            }
+            header |= this.opCode.getValue() & 0x0F;
+            out.write(header);
+
+            this._payloadLength = getBinaryPayload().length;
+            if (this._payloadLength <= 125) {
+                out.write(isMasked() ? 0x80 | (byte) this._payloadLength : (byte) this._payloadLength);
+            } else if (this._payloadLength <= 0xFFFF) {
+                out.write(isMasked() ? 0xFE : 126);
+                out.write(this._payloadLength >>> 8);
+                out.write(this._payloadLength);
+            } else {
+                out.write(isMasked() ? 0xFF : 127);
+                out.write(this._payloadLength >>> 56 & 0); // integer only
+                                                           // contains
+                // 31 bit
+                out.write(this._payloadLength >>> 48 & 0);
+                out.write(this._payloadLength >>> 40 & 0);
+                out.write(this._payloadLength >>> 32 & 0);
+                out.write(this._payloadLength >>> 24);
+                out.write(this._payloadLength >>> 16);
+                out.write(this._payloadLength >>> 8);
+                out.write(this._payloadLength);
+            }
+
+            if (isMasked()) {
+                out.write(this.maskingKey);
+                for (int i = 0; i < this._payloadLength; i++) {
+                    out.write(getBinaryPayload()[i] ^ this.maskingKey[i % 4]);
+                }
+            } else {
+                out.write(getBinaryPayload());
+            }
+            out.flush();
+        }
+    }
+
+    /**
+     * logger to log to.
+     */
+    private static final Logger LOG = Logger.getLogger(NanoWSD.class.getName());
+
+    public static final String HEADER_UPGRADE = "upgrade";
+
+    public static final String HEADER_UPGRADE_VALUE = "websocket";
+
+    public static final String HEADER_CONNECTION = "connection";
+
+    public static final String HEADER_CONNECTION_VALUE = "Upgrade";
+
+    public static final String HEADER_WEBSOCKET_VERSION = "sec-websocket-version";
+
+    public static final String HEADER_WEBSOCKET_VERSION_VALUE = "13";
+
+    public static final String HEADER_WEBSOCKET_KEY = "sec-websocket-key";
+
+    public static final String HEADER_WEBSOCKET_ACCEPT = "sec-websocket-accept";
+
+    public static final String HEADER_WEBSOCKET_PROTOCOL = "sec-websocket-protocol";
+
+    private final static String WEBSOCKET_KEY_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+    private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
+
+    /**
+     * Translates the specified byte array into Base64 string.
+     * <p>
+     * Android has android.util.Base64, sun has sun.misc.Base64Encoder, Java 8
+     * hast java.util.Base64, I have this from stackoverflow:
+     * http://stackoverflow.com/a/4265472
+     * </p>
+     * 
+     * @param buf
+     *            the byte array (not null)
+     * @return the translated Base64 string (not null)
+     */
+    private static String encodeBase64(byte[] buf) {
+        int size = buf.length;
+        char[] ar = new char[(size + 2) / 3 * 4];
+        int a = 0;
+        int i = 0;
+        while (i < size) {
+            byte b0 = buf[i++];
+            byte b1 = i < size ? buf[i++] : 0;
+            byte b2 = i < size ? buf[i++] : 0;
+
+            int mask = 0x3F;
+            ar[a++] = NanoWSD.ALPHABET[b0 >> 2 & mask];
+            ar[a++] = NanoWSD.ALPHABET[(b0 << 4 | (b1 & 0xFF) >> 4) & mask];
+            ar[a++] = NanoWSD.ALPHABET[(b1 << 2 | (b2 & 0xFF) >> 6) & mask];
+            ar[a++] = NanoWSD.ALPHABET[b2 & mask];
+        }
+        switch (size % 3) {
+            case 1:
+                ar[--a] = '=';
+            case 2:
+                ar[--a] = '=';
+        }
+        return new String(ar);
+    }
+
+    public static String makeAcceptKey(String key) throws NoSuchAlgorithmException {
+        MessageDigest md = MessageDigest.getInstance("SHA-1");
+        String text = key + NanoWSD.WEBSOCKET_KEY_MAGIC;
+        md.update(text.getBytes(), 0, text.length());
+        byte[] sha1hash = md.digest();
+        return encodeBase64(sha1hash);
+    }
+
+    public NanoWSD(int port) {
+        super(port);
+    }
+
+    public NanoWSD(String hostname, int port) {
+        super(hostname, port);
+    }
+
+    private boolean isWebSocketConnectionHeader(Map<String, String> headers) {
+        String connection = headers.get(NanoWSD.HEADER_CONNECTION);
+        return connection != null && connection.toLowerCase().contains(NanoWSD.HEADER_CONNECTION_VALUE.toLowerCase());
+    }
+
+    protected boolean isWebsocketRequested(IHTTPSession session) {
+        Map<String, String> headers = session.getHeaders();
+        String upgrade = headers.get(NanoWSD.HEADER_UPGRADE);
+        boolean isCorrectConnection = isWebSocketConnectionHeader(headers);
+        boolean isUpgrade = NanoWSD.HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade);
+        return isUpgrade && isCorrectConnection;
+    }
+
+    // --------------------------------Listener--------------------------------
+
+    protected abstract WebSocket openWebSocket(IHTTPSession handshake);
+
+    @Override
+    public Response serve(final IHTTPSession session) {
+        Map<String, String> headers = session.getHeaders();
+        if (isWebsocketRequested(session)) {
+            if (!NanoWSD.HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION))) {
+                return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT,
+                        "Invalid Websocket-Version " + headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION));
+            }
+
+            if (!headers.containsKey(NanoWSD.HEADER_WEBSOCKET_KEY)) {
+                return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Missing Websocket-Key");
+            }
+
+            WebSocket webSocket = openWebSocket(session);
+            Response handshakeResponse = webSocket.getHandshakeResponse();
+            try {
+                handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(NanoWSD.HEADER_WEBSOCKET_KEY)));
+            } catch (NoSuchAlgorithmException e) {
+                return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
+                        "The SHA-1 Algorithm required for websockets is not available on the server.");
+            }
+
+            if (headers.containsKey(NanoWSD.HEADER_WEBSOCKET_PROTOCOL)) {
+                handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_PROTOCOL, headers.get(NanoWSD.HEADER_WEBSOCKET_PROTOCOL).split(",")[0]);
+            }
+
+            return handshakeResponse;
+        } else {
+            return serveHttp(session);
+        }
+    }
+
+    protected Response serveHttp(final IHTTPSession session) {
+        return super.serve(session);
+    }
+
+    /**
+     * not all websockets implementations accept gzip compression.
+     */
+    @Override
+    protected boolean useGzipWhenAccepted(Response r) {
+        return false;
+    }
+}
diff --git a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java
deleted file mode 100644
index 0712371..0000000
--- a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package fi.iki.elonen;
-
-
-public class NanoWebSocketServer extends NanoHTTPD implements IWebSocketFactory {
-    public static final String MISSING_FACTORY_MESSAGE = "You must either override this method or supply a WebSocketFactory in the constructor";
-
-    private final WebSocketResponseHandler responseHandler;
-
-    public NanoWebSocketServer(int port) {
-        super(port);
-        responseHandler = new WebSocketResponseHandler(this);
-    }
-
-    public NanoWebSocketServer(String hostname, int port) {
-        super(hostname, port);
-        responseHandler = new WebSocketResponseHandler(this);
-    }
-
-    public NanoWebSocketServer(int port, IWebSocketFactory webSocketFactory) {
-        super(port);
-        responseHandler = new WebSocketResponseHandler(webSocketFactory);
-    }
-
-    public NanoWebSocketServer(String hostname, int port, IWebSocketFactory webSocketFactory) {
-        super(hostname, port);
-        responseHandler = new WebSocketResponseHandler(webSocketFactory);
-    }
-
-    @Override
-    public Response serve(IHTTPSession session) {
-        Response candidate = responseHandler.serve(session);
-        return candidate == null ? super.serve(session) : candidate;
-    }
-
-    public WebSocket openWebSocket(IHTTPSession handshake) {
-        throw new Error(MISSING_FACTORY_MESSAGE);
-    }
-}
-
diff --git a/websocket/src/main/java/fi/iki/elonen/WebSocket.java b/websocket/src/main/java/fi/iki/elonen/WebSocket.java
deleted file mode 100644
index 22b07fb..0000000
--- a/websocket/src/main/java/fi/iki/elonen/WebSocket.java
+++ /dev/null
@@ -1,207 +0,0 @@
-package fi.iki.elonen;
-
-import fi.iki.elonen.WebSocketFrame.CloseCode;
-import fi.iki.elonen.WebSocketFrame.CloseFrame;
-import fi.iki.elonen.WebSocketFrame.OpCode;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.charset.CharacterCodingException;
-import java.util.LinkedList;
-import java.util.List;
-
-public abstract class WebSocket {
-    public static enum State {
-        UNCONNECTED, CONNECTING, OPEN, CLOSING, CLOSED
-    }
-
-    protected InputStream in;
-
-    protected OutputStream out;
-
-    protected WebSocketFrame.OpCode continuousOpCode = null;
-
-    protected List<WebSocketFrame> continuousFrames = new LinkedList<WebSocketFrame>();
-
-    protected State state = State.UNCONNECTED;
-
-    protected final NanoHTTPD.IHTTPSession handshakeRequest;
-
-    protected final NanoHTTPD.Response handshakeResponse = new NanoHTTPD.Response(
-            NanoHTTPD.Response.Status.SWITCH_PROTOCOL, null, (InputStream) null) {
-        @Override
-        protected void send(OutputStream out) {
-            WebSocket.this.out = out;
-            state = State.CONNECTING;
-            super.send(out);
-            state = State.OPEN;
-            readWebsocket();
-        }
-    };
-
-    public WebSocket(NanoHTTPD.IHTTPSession handshakeRequest) {
-        this.handshakeRequest = handshakeRequest;
-        this.in = handshakeRequest.getInputStream();
-
-        handshakeResponse.addHeader(WebSocketResponseHandler.HEADER_UPGRADE,
-                WebSocketResponseHandler.HEADER_UPGRADE_VALUE);
-        handshakeResponse.addHeader(WebSocketResponseHandler.HEADER_CONNECTION,
-                WebSocketResponseHandler.HEADER_CONNECTION_VALUE);
-    }
-
-    public NanoHTTPD.IHTTPSession getHandshakeRequest() {
-        return handshakeRequest;
-    }
-
-    public NanoHTTPD.Response getHandshakeResponse() {
-        return handshakeResponse;
-    }
-
-    // --------------------------------IO--------------------------------------
-
-    protected void readWebsocket() {
-        try {
-            while (state == State.OPEN) {
-                handleWebsocketFrame(WebSocketFrame.read(in));
-            }
-        } catch (CharacterCodingException e) {
-            onException(e);
-            doClose(CloseCode.InvalidFramePayloadData, e.toString(), false);
-        } catch (IOException e) {
-            onException(e);
-            if (e instanceof WebSocketException) {
-                doClose(((WebSocketException) e).getCode(), ((WebSocketException) e).getReason(), false);
-            }
-        } finally {
-            doClose(CloseCode.InternalServerError, "Handler terminated without closing the connection.", false);
-        }
-    }
-
-    protected void handleWebsocketFrame(WebSocketFrame frame) throws IOException {
-        if (frame.getOpCode() == OpCode.Close) {
-            handleCloseFrame(frame);
-        } else if (frame.getOpCode() == OpCode.Ping) {
-            sendFrame(new WebSocketFrame(OpCode.Pong, true, frame.getBinaryPayload()));
-        } else if (frame.getOpCode() == OpCode.Pong) {
-            onPong(frame);
-        } else if (!frame.isFin() || frame.getOpCode() == OpCode.Continuation) {
-            handleFrameFragment(frame);
-        } else if (continuousOpCode != null) {
-            throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence not completed.");
-        } else if (frame.getOpCode() == OpCode.Text || frame.getOpCode() == OpCode.Binary) {
-            onMessage(frame);
-        } else {
-            throw new WebSocketException(CloseCode.ProtocolError, "Non control or continuous frame expected.");
-        }
-    }
-
-    protected void handleCloseFrame(WebSocketFrame frame) throws IOException {
-        CloseCode code = CloseCode.NormalClosure;
-        String reason = "";
-        if (frame instanceof CloseFrame) {
-            code = ((CloseFrame) frame).getCloseCode();
-            reason = ((CloseFrame) frame).getCloseReason();
-        }
-        if (state == State.CLOSING) {
-            //Answer for my requested close
-            doClose(code, reason, false);
-        } else {
-            //Answer close request from other endpoint and close self
-            State oldState = state;
-            state = State.CLOSING;
-            if (oldState == State.OPEN) {
-                sendFrame(new CloseFrame(code, reason));
-            }
-            doClose(code, reason, true);
-        }
-    }
-
-    protected void handleFrameFragment(WebSocketFrame frame) throws IOException {
-        if (frame.getOpCode() != OpCode.Continuation) {
-            //First
-            if (continuousOpCode != null) {
-                throw new WebSocketException(CloseCode.ProtocolError, "Previous continuous frame sequence not completed.");
-            }
-            continuousOpCode = frame.getOpCode();
-            continuousFrames.clear();
-            continuousFrames.add(frame);
-        } else if (frame.isFin()) {
-            //Last
-            if (continuousOpCode == null) {
-                throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
-            }
-            onMessage(new WebSocketFrame(continuousOpCode, continuousFrames));
-            continuousOpCode = null;
-            continuousFrames.clear();
-        } else if (continuousOpCode == null) {
-            //Unexpected
-            throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
-        } else {
-            //Intermediate
-            continuousFrames.add(frame);
-        }
-    }
-
-    public synchronized void sendFrame(WebSocketFrame frame) throws IOException {
-        frame.write(out);
-    }
-
-    // --------------------------------Close-----------------------------------
-
-    protected void doClose(CloseCode code, String reason, boolean initiatedByRemote) {
-        if (state == State.CLOSED) {
-            return;
-        }
-        if (in != null) {
-            try {
-                in.close();
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        if (out != null) {
-            try {
-                out.close();
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        state = State.CLOSED;
-        onClose(code, reason, initiatedByRemote);
-    }
-
-    // --------------------------------Listener--------------------------------
-
-    protected abstract void onPong(WebSocketFrame pongFrame);
-
-    protected abstract void onMessage(WebSocketFrame messageFrame);
-
-    protected abstract void onClose(CloseCode code, String reason, boolean initiatedByRemote);
-
-    protected abstract void onException(IOException e);
-
-    // --------------------------------Public Facade---------------------------
-
-    public void ping(byte[] payload) throws IOException {
-        sendFrame(new WebSocketFrame(OpCode.Ping, true, payload));
-    }
-
-    public void send(byte[] payload) throws IOException {
-        sendFrame(new WebSocketFrame(OpCode.Binary, true, payload));
-    }
-
-    public void send(String payload) throws IOException {
-        sendFrame(new WebSocketFrame(OpCode.Text, true, payload));
-    }
-
-    public void close(CloseCode code, String reason) throws IOException {
-        State oldState = state;
-        state = State.CLOSING;
-        if (oldState == State.OPEN) {
-            sendFrame(new CloseFrame(code, reason));
-        } else {
-            doClose(code, reason, false);
-        }
-    }
-}
diff --git a/websocket/src/main/java/fi/iki/elonen/WebSocketException.java b/websocket/src/main/java/fi/iki/elonen/WebSocketException.java
deleted file mode 100644
index 31cb6c8..0000000
--- a/websocket/src/main/java/fi/iki/elonen/WebSocketException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package fi.iki.elonen;
-
-import fi.iki.elonen.WebSocketFrame.CloseCode;
-
-import java.io.IOException;
-
-public class WebSocketException extends IOException {
-    private CloseCode code;
-    private String reason;
-
-    public WebSocketException(Exception cause) {
-        this(CloseCode.InternalServerError, cause.toString(), cause);
-    }
-
-    public WebSocketException(CloseCode code, String reason) {
-        this(code, reason, null);
-    }
-
-    public WebSocketException(CloseCode code, String reason, Exception cause) {
-        super(code + ": " + reason, cause);
-        this.code = code;
-        this.reason = reason;
-    }
-
-    public CloseCode getCode() {
-        return code;
-    }
-
-    public String getReason() {
-        return reason;
-    }
-}
diff --git a/websocket/src/main/java/fi/iki/elonen/WebSocketFrame.java b/websocket/src/main/java/fi/iki/elonen/WebSocketFrame.java
deleted file mode 100644
index 0e209df..0000000
--- a/websocket/src/main/java/fi/iki/elonen/WebSocketFrame.java
+++ /dev/null
@@ -1,430 +0,0 @@
-package fi.iki.elonen;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.Charset;
-import java.nio.charset.CharsetDecoder;
-import java.nio.charset.CharsetEncoder;
-import java.util.Arrays;
-import java.util.List;
-
-public class WebSocketFrame {
-    private OpCode opCode;
-    private boolean fin;
-    private byte[] maskingKey;
-
-    private byte[] payload;
-
-    private transient int _payloadLength;
-    private transient String _payloadString;
-
-    private WebSocketFrame(OpCode opCode, boolean fin) {
-        setOpCode(opCode);
-        setFin(fin);
-    }
-
-    public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey) {
-        this(opCode, fin);
-        setMaskingKey(maskingKey);
-        setBinaryPayload(payload);
-    }
-
-    public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload) {
-        this(opCode, fin, payload, null);
-    }
-
-    public WebSocketFrame(OpCode opCode, boolean fin, String payload, byte[] maskingKey) throws CharacterCodingException {
-        this(opCode, fin);
-        setMaskingKey(maskingKey);
-        setTextPayload(payload);
-    }
-
-    public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException {
-        this(opCode, fin, payload, null);
-    }
-
-    public WebSocketFrame(WebSocketFrame clone) {
-        setOpCode(clone.getOpCode());
-        setFin(clone.isFin());
-        setBinaryPayload(clone.getBinaryPayload());
-        setMaskingKey(clone.getMaskingKey());
-    }
-
-    public WebSocketFrame(OpCode opCode, List<WebSocketFrame> fragments) throws WebSocketException {
-        setOpCode(opCode);
-        setFin(true);
-
-        long _payloadLength = 0;
-        for (WebSocketFrame inter : fragments) {
-            _payloadLength += inter.getBinaryPayload().length;
-        }
-        if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
-            throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
-        }
-        this._payloadLength = (int) _payloadLength;
-        byte[] payload = new byte[this._payloadLength];
-        int offset = 0;
-        for (WebSocketFrame inter : fragments) {
-            System.arraycopy(inter.getBinaryPayload(), 0, payload, offset, inter.getBinaryPayload().length);
-            offset += inter.getBinaryPayload().length;
-        }
-        setBinaryPayload(payload);
-    }
-
-    // --------------------------------GETTERS---------------------------------
-
-    public OpCode getOpCode() {
-        return opCode;
-    }
-
-    public void setOpCode(OpCode opcode) {
-        this.opCode = opcode;
-    }
-
-    public boolean isFin() {
-        return fin;
-    }
-
-    public void setFin(boolean fin) {
-        this.fin = fin;
-    }
-
-    public boolean isMasked() {
-        return maskingKey != null && maskingKey.length == 4;
-    }
-
-    public byte[] getMaskingKey() {
-        return maskingKey;
-    }
-
-    public void setMaskingKey(byte[] maskingKey) {
-        if (maskingKey != null && maskingKey.length != 4) {
-            throw new IllegalArgumentException("MaskingKey " + Arrays.toString(maskingKey) + " hasn't length 4");
-        }
-        this.maskingKey = maskingKey;
-    }
-
-    public void setUnmasked() {
-        setMaskingKey(null);
-    }
-
-    public byte[] getBinaryPayload() {
-        return payload;
-    }
-
-    public void setBinaryPayload(byte[] payload) {
-        this.payload = payload;
-        this._payloadLength = payload.length;
-        this._payloadString = null;
-    }
-
-    public String getTextPayload() {
-        if (_payloadString == null) {
-            try {
-                _payloadString = binary2Text(getBinaryPayload());
-            } catch (CharacterCodingException e) {
-                throw new RuntimeException("Undetected CharacterCodingException", e);
-            }
-        }
-        return _payloadString;
-    }
-
-    public void setTextPayload(String payload) throws CharacterCodingException {
-        this.payload = text2Binary(payload);
-        this._payloadLength = payload.length();
-        this._payloadString = payload;
-    }
-
-    // --------------------------------SERIALIZATION---------------------------
-
-    public static WebSocketFrame read(InputStream in) throws IOException {
-        byte head = (byte) checkedRead(in.read());
-        boolean fin = ((head & 0x80) != 0);
-        OpCode opCode = OpCode.find((byte) (head & 0x0F));
-        if ((head & 0x70) != 0) {
-            throw new WebSocketException(CloseCode.ProtocolError, "The reserved bits (" + Integer.toBinaryString(head & 0x70) + ") must be 0.");
-        }
-        if (opCode == null) {
-            throw new WebSocketException(CloseCode.ProtocolError, "Received frame with reserved/unknown opcode " + (head & 0x0F) + ".");
-        } else if (opCode.isControlFrame() && !fin) {
-            throw new WebSocketException(CloseCode.ProtocolError, "Fragmented control frame.");
-        }
-
-        WebSocketFrame frame = new WebSocketFrame(opCode, fin);
-        frame.readPayloadInfo(in);
-        frame.readPayload(in);
-        if (frame.getOpCode() == OpCode.Close) {
-            return new CloseFrame(frame);
-        } else {
-            return frame;
-        }
-    }
-
-    private static int checkedRead(int read) throws IOException {
-        if (read < 0) {
-            throw new EOFException();
-        }
-        //System.out.println(Integer.toBinaryString(read) + "/" + read + "/" + Integer.toHexString(read));
-        return read;
-    }
-
-
-    private void readPayloadInfo(InputStream in) throws IOException {
-        byte b = (byte) checkedRead(in.read());
-        boolean masked = ((b & 0x80) != 0);
-
-        _payloadLength = (byte) (0x7F & b);
-        if (_payloadLength == 126) {
-            // checkedRead must return int for this to work
-            _payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF;
-            if (_payloadLength < 126) {
-                throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 2byte length. (not using minimal length encoding)");
-            }
-        } else if (_payloadLength == 127) {
-            long _payloadLength = ((long) checkedRead(in.read())) << 56 |
-                    ((long) checkedRead(in.read())) << 48 |
-                    ((long) checkedRead(in.read())) << 40 |
-                    ((long) checkedRead(in.read())) << 32 |
-                    checkedRead(in.read()) << 24 | checkedRead(in.read()) << 16 | checkedRead(in.read()) << 8 | checkedRead(in.read());
-            if (_payloadLength < 65536) {
-                throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 4byte length. (not using minimal length encoding)");
-            }
-            if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
-                throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
-            }
-            this._payloadLength = (int) _payloadLength;
-        }
-
-        if (opCode.isControlFrame()) {
-            if (_payloadLength > 125) {
-                throw new WebSocketException(CloseCode.ProtocolError, "Control frame with payload length > 125 bytes.");
-            }
-            if (opCode == OpCode.Close && _payloadLength == 1) {
-                throw new WebSocketException(CloseCode.ProtocolError, "Received close frame with payload len 1.");
-            }
-        }
-
-        if (masked) {
-            maskingKey = new byte[4];
-            int read = 0;
-            while (read < maskingKey.length) {
-                read += checkedRead(in.read(maskingKey, read, maskingKey.length - read));
-            }
-        }
-    }
-
-    private void readPayload(InputStream in) throws IOException {
-        payload = new byte[_payloadLength];
-        int read = 0;
-        while (read < _payloadLength) {
-            read += checkedRead(in.read(payload, read, _payloadLength - read));
-        }
-
-        if (isMasked()) {
-            for (int i = 0; i < payload.length; i++) {
-                payload[i] ^= maskingKey[i % 4];
-            }
-        }
-
-        //Test for Unicode errors
-        if (getOpCode() == OpCode.Text) {
-            _payloadString = binary2Text(getBinaryPayload());
-        }
-    }
-
-    public void write(OutputStream out) throws IOException {
-        byte header = 0;
-        if (fin) {
-            header |= 0x80;
-        }
-        header |= opCode.getValue() & 0x0F;
-        out.write(header);
-
-        _payloadLength = getBinaryPayload().length;
-        if (_payloadLength <= 125) {
-            out.write(isMasked() ? 0x80 | (byte) _payloadLength : (byte) _payloadLength);
-        } else if (_payloadLength <= 0xFFFF) {
-            out.write(isMasked() ? 0xFE : 126);
-            out.write(_payloadLength >>> 8);
-            out.write(_payloadLength);
-        } else {
-            out.write(isMasked() ? 0xFF : 127);
-            out.write(_payloadLength >>> 56 & 0); //integer only contains 31 bit
-            out.write(_payloadLength >>> 48 & 0);
-            out.write(_payloadLength >>> 40 & 0);
-            out.write(_payloadLength >>> 32 & 0);
-            out.write(_payloadLength >>> 24);
-            out.write(_payloadLength >>> 16);
-            out.write(_payloadLength >>> 8);
-            out.write(_payloadLength);
-        }
-
-
-        if (isMasked()) {
-            out.write(maskingKey);
-            for (int i = 0; i < _payloadLength; i++) {
-                out.write(getBinaryPayload()[i] ^ maskingKey[i % 4]);
-            }
-        } else {
-            out.write(getBinaryPayload());
-        }
-        out.flush();
-    }
-
-    // --------------------------------ENCODING--------------------------------
-
-    public static final Charset TEXT_CHARSET = Charset.forName("UTF-8");
-    public static final CharsetDecoder TEXT_DECODER = TEXT_CHARSET.newDecoder();
-    public static final CharsetEncoder TEXT_ENCODER = TEXT_CHARSET.newEncoder();
-
-
-    public static String binary2Text(byte[] payload) throws CharacterCodingException {
-        return TEXT_DECODER.decode(ByteBuffer.wrap(payload)).toString();
-    }
-
-    public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException {
-        return TEXT_DECODER.decode(ByteBuffer.wrap(payload, offset, length)).toString();
-    }
-
-    public static byte[] text2Binary(String payload) throws CharacterCodingException {
-        return TEXT_ENCODER.encode(CharBuffer.wrap(payload)).array();
-    }
-
-    @Override
-    public String toString() {
-        final StringBuilder sb = new StringBuilder("WS[");
-        sb.append(getOpCode());
-        sb.append(", ").append(isFin() ? "fin" : "inter");
-        sb.append(", ").append(isMasked() ? "masked" : "unmasked");
-        sb.append(", ").append(payloadToString());
-        sb.append(']');
-        return sb.toString();
-    }
-
-    protected String payloadToString() {
-        if (payload == null) return "null";
-        else {
-            final StringBuilder sb = new StringBuilder();
-            sb.append('[').append(payload.length).append("b] ");
-            if (getOpCode() == OpCode.Text) {
-                String text = getTextPayload();
-                if (text.length() > 100)
-                    sb.append(text.substring(0, 100)).append("...");
-                else
-                    sb.append(text);
-            } else {
-                sb.append("0x");
-                for (int i = 0; i < Math.min(payload.length, 50); ++i)
-                    sb.append(Integer.toHexString((int) payload[i] & 0xFF));
-                if (payload.length > 50)
-                    sb.append("...");
-            }
-            return sb.toString();
-        }
-    }
-
-    // --------------------------------CONSTANTS-------------------------------
-
-    public static enum OpCode {
-        Continuation(0), Text(1), Binary(2), Close(8), Ping(9), Pong(10);
-
-        private final byte code;
-
-        private OpCode(int code) {
-            this.code = (byte) code;
-        }
-
-        public byte getValue() {
-            return code;
-        }
-
-        public boolean isControlFrame() {
-            return this == Close || this == Ping || this == Pong;
-        }
-
-        public static OpCode find(byte value) {
-            for (OpCode opcode : values()) {
-                if (opcode.getValue() == value) {
-                    return opcode;
-                }
-            }
-            return null;
-        }
-    }
-
-    public static enum CloseCode {
-        NormalClosure(1000), GoingAway(1001), ProtocolError(1002), UnsupportedData(1003), NoStatusRcvd(1005),
-        AbnormalClosure(1006), InvalidFramePayloadData(1007), PolicyViolation(1008), MessageTooBig(1009),
-        MandatoryExt(1010), InternalServerError(1011), TLSHandshake(1015);
-
-        private final int code;
-
-        private CloseCode(int code) {
-            this.code = code;
-        }
-
-        public int getValue() {
-            return code;
-        }
-
-        public static CloseCode find(int value) {
-            for (CloseCode code : values()) {
-                if (code.getValue() == value) {
-                    return code;
-                }
-            }
-            return null;
-        }
-    }
-
-    // ------------------------------------------------------------------------
-
-    public static class CloseFrame extends WebSocketFrame {
-        private CloseCode _closeCode;
-        private String _closeReason;
-
-        private CloseFrame(WebSocketFrame wrap) throws CharacterCodingException {
-            super(wrap);
-            assert wrap.getOpCode() == OpCode.Close;
-            if (wrap.getBinaryPayload().length >= 2) {
-                _closeCode = CloseCode.find((wrap.getBinaryPayload()[0] & 0xFF) << 8 |
-                        (wrap.getBinaryPayload()[1] & 0xFF));
-                _closeReason = binary2Text(getBinaryPayload(), 2, getBinaryPayload().length - 2);
-            }
-        }
-
-        public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException {
-            super(OpCode.Close, true, generatePayload(code, closeReason));
-        }
-
-        private static byte[] generatePayload(CloseCode code, String closeReason) throws CharacterCodingException {
-            if (code != null) {
-                byte[] reasonBytes = text2Binary(closeReason);
-                byte[] payload = new byte[reasonBytes.length + 2];
-                payload[0] = (byte) ((code.getValue() >> 8) & 0xFF);
-                payload[1] = (byte) ((code.getValue()) & 0xFF);
-                System.arraycopy(reasonBytes, 0, payload, 2, reasonBytes.length);
-                return payload;
-            } else {
-                return new byte[0];
-            }
-        }
-
-        protected String payloadToString() {
-            return (_closeCode != null ? _closeCode : "UnknownCloseCode[" + _closeCode + "]") + (_closeReason != null && !_closeReason.isEmpty() ? ": " + _closeReason : "");
-        }
-
-        public CloseCode getCloseCode() {
-            return _closeCode;
-        }
-
-        public String getCloseReason() {
-            return _closeReason;
-        }
-    }
-}
diff --git a/websocket/src/main/java/fi/iki/elonen/WebSocketResponseHandler.java b/websocket/src/main/java/fi/iki/elonen/WebSocketResponseHandler.java
deleted file mode 100644
index 5c042af..0000000
--- a/websocket/src/main/java/fi/iki/elonen/WebSocketResponseHandler.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package fi.iki.elonen;
-
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Map;
-
-import fi.iki.elonen.NanoHTTPD.IHTTPSession;
-import fi.iki.elonen.NanoHTTPD.Response;
-
-public class WebSocketResponseHandler {
-    public static final String HEADER_UPGRADE = "upgrade";
-    public static final String HEADER_UPGRADE_VALUE = "websocket";
-    public static final String HEADER_CONNECTION = "connection";
-    public static final String HEADER_CONNECTION_VALUE = "Upgrade";
-    public static final String HEADER_WEBSOCKET_VERSION = "sec-websocket-version";
-    public static final String HEADER_WEBSOCKET_VERSION_VALUE = "13";
-    public static final String HEADER_WEBSOCKET_KEY = "sec-websocket-key";
-    public static final String HEADER_WEBSOCKET_ACCEPT = "sec-websocket-accept";
-    public static final String HEADER_WEBSOCKET_PROTOCOL = "sec-websocket-protocol";
-
-    public final static String WEBSOCKET_KEY_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
-
-    private final IWebSocketFactory webSocketFactory;
-
-    public WebSocketResponseHandler(IWebSocketFactory webSocketFactory) {
-        this.webSocketFactory = webSocketFactory;
-    }
-
-    public Response serve(final IHTTPSession session) {
-        Map<String, String> headers = session.getHeaders();
-        if (isWebsocketRequested(session)) {
-            if (!HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(HEADER_WEBSOCKET_VERSION))) {
-                return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT,
-                        "Invalid Websocket-Version " + headers.get(HEADER_WEBSOCKET_VERSION));
-            }
-
-            if (!headers.containsKey(HEADER_WEBSOCKET_KEY)) {
-                return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT,
-                        "Missing Websocket-Key");
-            }
-
-            WebSocket webSocket = webSocketFactory.openWebSocket(session);
-            Response handshakeResponse = webSocket.getHandshakeResponse();
-            try {
-                handshakeResponse.addHeader(HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(HEADER_WEBSOCKET_KEY)));
-            } catch (NoSuchAlgorithmException e) {
-                return new Response(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
-                        "The SHA-1 Algorithm required for websockets is not available on the server.");
-            }
-
-            if (headers.containsKey(HEADER_WEBSOCKET_PROTOCOL)) {
-                handshakeResponse.addHeader(HEADER_WEBSOCKET_PROTOCOL, headers.get(HEADER_WEBSOCKET_PROTOCOL).split(",")[0]);
-            }
-
-            return handshakeResponse;
-        } else {
-            return null;
-        }
-    }
-
-    protected boolean isWebsocketRequested(IHTTPSession session) {
-        Map<String, String> headers = session.getHeaders();
-        String upgrade = headers.get(HEADER_UPGRADE);
-        boolean isCorrectConnection = isWebSocketConnectionHeader(headers);
-        boolean isUpgrade = HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade);
-        return (isUpgrade && isCorrectConnection);
-    }
-
-    private boolean isWebSocketConnectionHeader(Map<String, String> headers) {
-        String connection = headers.get(HEADER_CONNECTION);
-        return (connection != null && connection.toLowerCase().contains(HEADER_CONNECTION_VALUE.toLowerCase()));
-    }
-
-    public static String makeAcceptKey(String key) throws NoSuchAlgorithmException {
-        MessageDigest md = MessageDigest.getInstance("SHA-1");
-        String text = key + WEBSOCKET_KEY_MAGIC;
-        md.update(text.getBytes(), 0, text.length());
-        byte[] sha1hash = md.digest();
-        return encodeBase64(sha1hash);
-    }
-
-    private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
-
-    /**
-     * Translates the specified byte array into Base64 string.
-     * <p>
-     * Android has android.util.Base64, sun has sun.misc.Base64Encoder, Java 8 hast java.util.Base64,
-     * I have this from stackoverflow: http://stackoverflow.com/a/4265472
-     * </p>
-     *
-     * @param buf the byte array (not null)
-     * @return the translated Base64 string (not null)
-     */
-    private static String encodeBase64(byte[] buf) {
-        int size = buf.length;
-        char[] ar = new char[((size + 2) / 3) * 4];
-        int a = 0;
-        int i = 0;
-        while (i < size) {
-            byte b0 = buf[i++];
-            byte b1 = (i < size) ? buf[i++] : 0;
-            byte b2 = (i < size) ? buf[i++] : 0;
-
-            int mask = 0x3F;
-            ar[a++] = ALPHABET[(b0 >> 2) & mask];
-            ar[a++] = ALPHABET[((b0 << 4) | ((b1 & 0xFF) >> 4)) & mask];
-            ar[a++] = ALPHABET[((b1 << 2) | ((b2 & 0xFF) >> 6)) & mask];
-            ar[a++] = ALPHABET[b2 & mask];
-        }
-        switch (size % 3) {
-            case 1:
-                ar[--a] = '=';
-            case 2:
-                ar[--a] = '=';
-        }
-        return new String(ar);
-    }
-}
diff --git a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocket.java b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocket.java
deleted file mode 100644
index badd7ee..0000000
--- a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocket.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package fi.iki.elonen.samples.echo;
-
-import fi.iki.elonen.NanoHTTPD;
-import fi.iki.elonen.WebSocket;
-import fi.iki.elonen.WebSocketFrame;
-
-import java.io.IOException;
-
-/**
-* @author Paul S. Hawke (paul.hawke@gmail.com)
-*         On: 4/23/14 at 10:34 PM
-*/
-class DebugWebSocket extends WebSocket {
-    private final boolean debug;
-
-    public DebugWebSocket(NanoHTTPD.IHTTPSession handshake, boolean debug) {
-        super(handshake);
-        this.debug = debug;
-    }
-
-    @Override
-    protected void onPong(WebSocketFrame pongFrame) {
-        if (debug) {
-            System.out.println("P " + pongFrame);
-        }
-    }
-
-    @Override
-    protected void onMessage(WebSocketFrame messageFrame) {
-        try {
-            messageFrame.setUnmasked();
-            sendFrame(messageFrame);
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    @Override
-    protected void onClose(WebSocketFrame.CloseCode code, String reason, boolean initiatedByRemote) {
-        if (debug) {
-            System.out.println("C [" + (initiatedByRemote ? "Remote" : "Self") + "] " +
-                    (code != null ? code : "UnknownCloseCode[" + code + "]") +
-                    (reason != null && !reason.isEmpty() ? ": " + reason : ""));
-        }
-    }
-
-    @Override
-    protected void onException(IOException e) {
-        e.printStackTrace();
-    }
-
-    @Override
-    protected void handleWebsocketFrame(WebSocketFrame frame) throws IOException {
-        if (debug) {
-            System.out.println("R " + frame);
-        }
-        super.handleWebsocketFrame(frame);
-    }
-
-    @Override
-    public synchronized void sendFrame(WebSocketFrame frame) throws IOException {
-        if (debug) {
-            System.out.println("S " + frame);
-        }
-        super.sendFrame(frame);
-    }
-}
diff --git a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java
index 5faf72a..1f4623e 100644
--- a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java
+++ b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java
@@ -1,13 +1,55 @@
 package fi.iki.elonen.samples.echo;
 
-import fi.iki.elonen.NanoWebSocketServer;
-import fi.iki.elonen.WebSocket;
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import fi.iki.elonen.NanoWSD;
+import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode;
 
 /**
-* @author Paul S. Hawke (paul.hawke@gmail.com)
-*         On: 4/23/14 at 10:31 PM
-*/
-class DebugWebSocketServer extends NanoWebSocketServer {
+ * @author Paul S. Hawke (paul.hawke@gmail.com) On: 4/23/14 at 10:31 PM
+ */
+public class DebugWebSocketServer extends NanoWSD {
+
+    /**
+     * logger to log to.
+     */
+    private static final Logger LOG = Logger.getLogger(DebugWebSocketServer.class.getName());
+
     private final boolean debug;
 
     public DebugWebSocketServer(int port, boolean debug) {
@@ -16,7 +58,65 @@
     }
 
     @Override
-    public WebSocket openWebSocket(IHTTPSession handshake) {
-        return new DebugWebSocket(handshake, debug);
+    protected WebSocket openWebSocket(IHTTPSession handshake) {
+        return new DebugWebSocket(this, handshake);
+    }
+
+    private static class DebugWebSocket extends WebSocket {
+
+        private final DebugWebSocketServer server;
+
+        public DebugWebSocket(DebugWebSocketServer server, IHTTPSession handshakeRequest) {
+            super(handshakeRequest);
+            this.server = server;
+        }
+
+        @Override
+        protected void onOpen() {
+        }
+
+        @Override
+        protected void onClose(CloseCode code, String reason, boolean initiatedByRemote) {
+            if (server.debug) {
+                System.out.println("C [" + (initiatedByRemote ? "Remote" : "Self") + "] " + (code != null ? code : "UnknownCloseCode[" + code + "]")
+                        + (reason != null && !reason.isEmpty() ? ": " + reason : ""));
+            }
+        }
+
+        @Override
+        protected void onMessage(WebSocketFrame message) {
+            try {
+                message.setUnmasked();
+                sendFrame(message);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        protected void onPong(WebSocketFrame pong) {
+            if (server.debug) {
+                System.out.println("P " + pong);
+            }
+        }
+
+        @Override
+        protected void onException(IOException exception) {
+            DebugWebSocketServer.LOG.log(Level.SEVERE, "exception occured", exception);
+        }
+
+        @Override
+        protected void debugFrameReceived(WebSocketFrame frame) {
+            if (server.debug) {
+                System.out.println("R " + frame);
+            }
+        }
+
+        @Override
+        protected void debugFrameSent(WebSocketFrame frame) {
+            if (server.debug) {
+                System.out.println("S " + frame);
+            }
+        }
     }
 }
diff --git a/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java b/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java
index b28a42c..e7d7e3b 100644
--- a/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java
+++ b/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java
@@ -1,13 +1,47 @@
 package fi.iki.elonen.samples.echo;
 
-import fi.iki.elonen.NanoWebSocketServer;
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
 
 import java.io.IOException;
 
+import fi.iki.elonen.NanoWSD;
+
 public class EchoSocketSample {
+
     public static void main(String[] args) throws IOException {
         final boolean debugMode = args.length >= 2 && args[1].toLowerCase().equals("-d");
-        NanoWebSocketServer ws = new DebugWebSocketServer(Integer.parseInt(args[0]), debugMode);
+        NanoWSD ws = new DebugWebSocketServer(args.length > 0 ? Integer.parseInt(args[0]) : 9090, debugMode);
         ws.start();
         System.out.println("Server started, hit Enter to stop.\n");
         try {
@@ -19,4 +53,3 @@
     }
 
 }
-
diff --git a/websocket/src/site/site.xml b/websocket/src/site/site.xml
new file mode 100644
index 0000000..4270945
--- /dev/null
+++ b/websocket/src/site/site.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="${project.name}">
+	<skin>
+		<groupId>org.apache.maven.skins</groupId>
+		<artifactId>maven-fluido-skin</artifactId>
+		<version>1.3.0</version>
+	</skin>
+	<bannerLeft>
+		<src>../images/nanohttpd_logo.png</src>
+	</bannerLeft>
+	<bannerRight>
+		<src>../images/nanohttpd_logo_text.png</src>
+	</bannerRight>
+	<publishDate position="left" format="yyyy-MM-dd" />
+	<version position="right" />
+	<poweredBy>
+		<logo name="Maven" href="http://maven.apache.org/"
+			img="http://maven.apache.org/images/logos/maven-feather.png" />
+	</poweredBy>
+	<custom>
+		<fluidoSkin>
+			<topBarEnabled>false</topBarEnabled>
+			<sideBarEnabled>true</sideBarEnabled>
+			<gitHub>
+				<projectId>Nanohttpd/nanohttpd</projectId>
+				<ribbonOrientation>right</ribbonOrientation>
+				<ribbonColor>black</ribbonColor>
+			</gitHub>
+		</fluidoSkin>
+	</custom>
+	<body>
+		<breadcrumbs>
+			<item name="${project.name}" href="index.html" />
+		</breadcrumbs>
+		<menu name="Documentation">
+			<item name="About" href="index.html" />
+		</menu>
+		<menu ref="modules" />
+		<menu ref="reports" />
+	</body>
+</project>
\ No newline at end of file
diff --git a/websocket/src/test/java/fi/iki/elonen/NanoWebSocketServerTest.java b/websocket/src/test/java/fi/iki/elonen/NanoWebSocketServerTest.java
deleted file mode 100644
index 7cd06b7..0000000
--- a/websocket/src/test/java/fi/iki/elonen/NanoWebSocketServerTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package fi.iki.elonen;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import java.util.Map;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class NanoWebSocketServerTest {
-    @Mock
-    private NanoHTTPD.IHTTPSession session;
-
-    private NanoWebSocketServer server;
-
-    @Before
-    public void setUp() {
-        server = new NanoWebSocketServer(9090);
-    }
-
-    @Test(expected = Error.class)
-    public void testMissingResponseFactoryThrowsErrorOnServe() {
-        server.openWebSocket(session);
-    }
-
-    @Test
-    public void testMissingResponseFactoryThrowsErrorWithCorrectMessageOnServe() {
-        NanoWebSocketServer server = new NanoWebSocketServer(9090);
-        try {
-            server.openWebSocket(session);
-        } catch (Error e) {
-            assertEquals(NanoWebSocketServer.MISSING_FACTORY_MESSAGE, e.getMessage());
-        }
-    }
-}
\ No newline at end of file
diff --git a/websocket/src/test/java/fi/iki/elonen/WebSocketResponseHandlerTest.java b/websocket/src/test/java/fi/iki/elonen/WebSocketResponseHandlerTest.java
index 04a497b..8c083ee 100644
--- a/websocket/src/test/java/fi/iki/elonen/WebSocketResponseHandlerTest.java
+++ b/websocket/src/test/java/fi/iki/elonen/WebSocketResponseHandlerTest.java
@@ -1,120 +1,169 @@
 package fi.iki.elonen;
 
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
 
 import fi.iki.elonen.NanoHTTPD.IHTTPSession;
 import fi.iki.elonen.NanoHTTPD.Response;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import static junit.framework.Assert.*;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import fi.iki.elonen.NanoWSD.WebSocketFrame;
+import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode;
 
 @RunWith(MockitoJUnitRunner.class)
 public class WebSocketResponseHandlerTest {
 
     @Mock
     private IHTTPSession session;
-    @Mock
-    private WebSocket webSocket;
-    @Mock
-    private IWebSocketFactory webSocketFactory;
-    @Mock
-    private Response response;
-    @Captor
-    private ArgumentCaptor<String> headerNameCaptor;
-    @Captor
-    private ArgumentCaptor<String> headerCaptor;
+
+    private NanoWSD nanoWebSocketServer;
 
     private Map<String, String> headers;
 
-    private WebSocketResponseHandler responseHandler;
+    private static class MockedWSD extends NanoWSD {
+
+        public MockedWSD(int port) {
+            super(port);
+        }
+
+        public MockedWSD(String hostname, int port) {
+            super(hostname, port);
+        }
+
+        @Override
+        protected WebSocket openWebSocket(IHTTPSession handshake) {
+            return new WebSocket(handshake) { // Dummy websocket inner class.
+
+                @Override
+                protected void onPong(WebSocketFrame pong) {
+                }
+
+                @Override
+                protected void onOpen() {
+                }
+
+                @Override
+                protected void onMessage(WebSocketFrame message) {
+                }
+
+                @Override
+                protected void onException(IOException exception) {
+                }
+
+                @Override
+                protected void onClose(CloseCode code, String reason, boolean initiatedByRemote) {
+                }
+            };
+        }
+    }
 
     @Before
     public void setUp() {
-        headers = new HashMap<String, String>();
-        headers.put("upgrade", "websocket");
-        headers.put("connection", "Upgrade");
-        headers.put("sec-websocket-key", "x3JJHMbDL1EzLkh9GBhXDw==");
-        headers.put("sec-websocket-protocol", "chat, superchat");
-        headers.put("sec-websocket-version", "13");
+        this.nanoWebSocketServer = Mockito.mock(MockedWSD.class, Mockito.CALLS_REAL_METHODS);
 
-        when(session.getHeaders()).thenReturn(headers);
-        when(webSocketFactory.openWebSocket(any(IHTTPSession.class))).thenReturn(webSocket);
-        when(webSocket.getHandshakeResponse()).thenReturn(response);
+        this.headers = new HashMap<String, String>();
+        this.headers.put("upgrade", "websocket");
+        this.headers.put("connection", "Upgrade");
+        this.headers.put("sec-websocket-key", "x3JJHMbDL1EzLkh9GBhXDw==");
+        this.headers.put("sec-websocket-protocol", "chat, superchat");
+        this.headers.put("sec-websocket-version", "13");
 
-        responseHandler = new WebSocketResponseHandler(webSocketFactory);
-    }
-
-    @Test
-    public void testHandshakeReturnsResponseWithExpectedHeaders() {
-        Response handshakeResponse = responseHandler.serve(session);
-
-        verify(webSocket).getHandshakeResponse();
-        assertNotNull(handshakeResponse);
-        assertSame(response, handshakeResponse);
-
-        verify(response, atLeast(1)).addHeader(headerNameCaptor.capture(), headerCaptor.capture());
-        assertHeader(0, "sec-websocket-accept", "HSmrc0sMlYUkAGmm5OPpG2HaGWk=");
-        assertHeader(1, "sec-websocket-protocol", "chat");
-    }
-
-    @Test
-    public void testWrongWebsocketVersionReturnsErrorResponse() {
-        headers.put("sec-websocket-version", "12");
-
-        Response handshakeResponse = responseHandler.serve(session);
-
-        assertNotNull(handshakeResponse);
-        assertEquals(Response.Status.BAD_REQUEST, handshakeResponse.getStatus());
-    }
-
-    @Test
-    public void testMissingKeyReturnsErrorResponse() {
-        headers.remove("sec-websocket-key");
-
-        Response handshakeResponse = responseHandler.serve(session);
-
-        assertNotNull(handshakeResponse);
-        assertEquals(Response.Status.BAD_REQUEST, handshakeResponse.getStatus());
-    }
-
-    @Test
-    public void testWrongUpgradeHeaderReturnsNullResponse() {
-        headers.put("upgrade", "not a websocket");
-        Response handshakeResponse = responseHandler.serve(session);
-        assertNull(handshakeResponse);
-    }
-
-    @Test
-    public void testWrongConnectionHeaderReturnsNullResponse() {
-        headers.put("connection", "Junk");
-        Response handshakeResponse = responseHandler.serve(session);
-        assertNull(handshakeResponse);
+        when(this.session.getHeaders()).thenReturn(this.headers);
     }
 
     @Test
     public void testConnectionHeaderHandlesKeepAlive_FixingFirefoxConnectIssue() {
-        headers.put("connection", "keep-alive, Upgrade");
-        Response handshakeResponse = responseHandler.serve(session);
+        this.headers.put("connection", "keep-alive, Upgrade");
+        Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
 
-        verify(webSocket).getHandshakeResponse();
         assertNotNull(handshakeResponse);
-        assertSame(response, handshakeResponse);
     }
 
-    private void assertHeader(int index, String name, String value) {
-        assertEquals(name, headerNameCaptor.getAllValues().get(index));
-        assertEquals(value, headerCaptor.getAllValues().get(index));
+    @Test
+    public void testHandshakeReturnsResponseWithExpectedHeaders() {
+        Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
+
+        assertNotNull(handshakeResponse);
+
+        assertEquals(handshakeResponse.getHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT), "HSmrc0sMlYUkAGmm5OPpG2HaGWk=");
+        assertEquals(handshakeResponse.getHeader(NanoWSD.HEADER_WEBSOCKET_PROTOCOL), "chat");
+    }
+
+    @Test
+    public void testMissingKeyReturnsErrorResponse() {
+        this.headers.remove("sec-websocket-key");
+
+        Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
+
+        assertNotNull(handshakeResponse);
+        assertEquals(Response.Status.BAD_REQUEST, handshakeResponse.getStatus());
+    }
+
+    @Test
+    public void testWrongConnectionHeaderReturnsNullResponse() {
+        this.headers.put("connection", "Junk");
+        Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
+        assertNull(handshakeResponse.getHeader(NanoWSD.HEADER_UPGRADE));
+    }
+
+    @Test
+    public void testWrongUpgradeHeaderReturnsNullResponse() {
+        this.headers.put("upgrade", "not a websocket");
+        Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
+        assertNull(handshakeResponse.getHeader(NanoWSD.HEADER_UPGRADE));
+    }
+
+    @Test
+    public void testWrongWebsocketVersionReturnsErrorResponse() {
+        this.headers.put("sec-websocket-version", "12");
+
+        Response handshakeResponse = this.nanoWebSocketServer.serve(this.session);
+
+        assertNotNull(handshakeResponse);
+        assertEquals(Response.Status.BAD_REQUEST, handshakeResponse.getStatus());
     }
 }
diff --git a/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java b/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java
new file mode 100644
index 0000000..d8b96ab
--- /dev/null
+++ b/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java
@@ -0,0 +1,102 @@
+package fi.iki.elonen.samples.echo;
+
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import fi.iki.elonen.NanoWSD;
+
+public class EchoWebSocketsTest {
+
+    private static NanoWSD server;
+
+    @BeforeClass
+    public static void setUp() throws Exception {
+        EchoWebSocketsTest.server = new DebugWebSocketServer(9191, true);
+        EchoWebSocketsTest.server.start();
+    }
+
+    @AfterClass
+    public static void tearDown() throws Exception {
+        EchoWebSocketsTest.server.stop();
+    }
+
+    @Test
+    public void testWebsocketClient() throws Exception {
+        String destUri = "ws://localhost:9191";
+
+        WebSocketClient client = new WebSocketClient();
+        SimpleEchoSocket socket = new SimpleEchoSocket();
+        socket.getToSendMessages().add("Hello");
+        socket.getToSendMessages().add("Thanks for the conversation.");
+        socket.getToSendMessages().add(createString(31000));
+        socket.getToSendMessages().add(createString(65400));
+        try {
+            client.start();
+            URI echoUri = new URI(destUri);
+            ClientUpgradeRequest request = new ClientUpgradeRequest();
+            client.connect(socket, echoUri, request);
+            System.out.printf("Connecting to : %s%n", echoUri);
+            socket.awaitClose(5, TimeUnit.SECONDS);
+        } catch (Throwable t) {
+            t.printStackTrace();
+        } finally {
+            try {
+                client.stop();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        Assert.assertEquals(4, socket.getReceivedMessages().size());
+        Assert.assertEquals("Hello", socket.getReceivedMessages().get(0));
+        Assert.assertEquals("Thanks for the conversation.", socket.getReceivedMessages().get(1));
+
+    }
+
+    private String createString(int i) {
+        StringBuilder builder = new StringBuilder();
+        while (builder.length() < i) {
+            builder.append("A very long text.");
+        }
+        return builder.toString();
+    }
+}
diff --git a/websocket/src/test/java/fi/iki/elonen/samples/echo/SimpleEchoSocket.java b/websocket/src/test/java/fi/iki/elonen/samples/echo/SimpleEchoSocket.java
new file mode 100644
index 0000000..45524d2
--- /dev/null
+++ b/websocket/src/test/java/fi/iki/elonen/samples/echo/SimpleEchoSocket.java
@@ -0,0 +1,104 @@
+package fi.iki.elonen.samples.echo;
+
+/*
+ * #%L
+ * NanoHttpd-Websocket
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software without
+ *    specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+
+/**
+ * Basic Echo Client Socket
+ */
+@WebSocket(maxTextMessageSize = 64 * 1024)
+public class SimpleEchoSocket {
+
+    private final List<String> receivedMessages = new ArrayList<String>();
+
+    private final List<String> toSendMessages = new ArrayList<String>();
+
+    private final CountDownLatch closeLatch;
+
+    public SimpleEchoSocket() {
+        this.closeLatch = new CountDownLatch(1);
+    }
+
+    public boolean awaitClose(int duration, TimeUnit unit) throws InterruptedException {
+        return this.closeLatch.await(duration, unit);
+    }
+
+    public List<String> getReceivedMessages() {
+        return this.receivedMessages;
+    }
+
+    public List<String> getToSendMessages() {
+        return this.toSendMessages;
+    }
+
+    @OnWebSocketClose
+    public void onClose(int statusCode, String reason) {
+        System.out.printf("Connection closed: %d - %s%n", statusCode, reason);
+        this.closeLatch.countDown();
+    }
+
+    @OnWebSocketConnect
+    public void onConnect(Session session) {
+        System.out.printf("Got connect: %s%n", session);
+        try {
+            Future<Void> fut;
+
+            for (String message : this.toSendMessages) {
+                fut = session.getRemote().sendStringByFuture(message);
+                fut.get(5, TimeUnit.SECONDS);
+            }
+            session.close(StatusCode.NORMAL, "I'm done");
+        } catch (Throwable t) {
+            t.printStackTrace();
+        }
+    }
+
+    @OnWebSocketMessage
+    public void onMessage(String msg) {
+        System.out.printf("Got msg: %s%n", msg);
+        this.receivedMessages.add(msg);
+    }
+}
diff --git a/websocket/src/test/resources/echo-test.html b/websocket/src/test/resources/echo-test.html
index 4b60b80..3f036f2 100644
--- a/websocket/src/test/resources/echo-test.html
+++ b/websocket/src/test/resources/echo-test.html
@@ -1,3 +1,35 @@
+<!--
+  #%L
+  NanoHttpd-Websocket
+  %%
+  Copyright (C) 2012 - 2015 nanohttpd
+  %%
+  Redistribution and use in source and binary forms, with or without modification,
+  are permitted provided that the following conditions are met:
+  
+  1. Redistributions of source code must retain the above copyright notice, this
+     list of conditions and the following disclaimer.
+  
+  2. Redistributions in binary form must reproduce the above copyright notice,
+     this list of conditions and the following disclaimer in the documentation
+     and/or other materials provided with the distribution.
+  
+  3. Neither the name of the nanohttpd nor the names of its contributors
+     may be used to endorse or promote products derived from this software without
+     specific prior written permission.
+  
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+  OF THE POSSIBILITY OF SUCH DAMAGE.
+  #L%
+  -->
 <html>
 <head>
     <meta charset="utf-8"/>