Merge branch 'java' of git.nikiroo.be:workspace/template
authorNiki Roo <niki@nikiroo.be>
Tue, 2 Jul 2024 15:17:36 +0000 (17:17 +0200)
committerNiki Roo <niki@nikiroo.be>
Tue, 2 Jul 2024 15:17:36 +0000 (17:17 +0200)
32 files changed:
.gitmodules [new file with mode: 0644]
Makefile
VERSION
bridge [new file with mode: 0755]
changelog.md
export.sh [new file with mode: 0755]
img2aa [new file with mode: 0755]
justify [new file with mode: 0755]
libs/NanoHTTPD.java [new file with mode: 0644]
libs/WrapLayout.java [new file with mode: 0644]
libs/bin/be/nikiroo/utils/android/ImageUtilsAndroid.class [new file with mode: 0644]
libs/bin/be/nikiroo/utils/android/test/TestAndroid.class [new file with mode: 0644]
libs/licenses/nanohttpd-2.3.1-LICENSE.md [new file with mode: 0644]
libs/licenses/unbescape_1.1.4_LICENSE.txt [new file with mode: 0644]
libs/unbescape-1.1.4-sources.jar [new file with mode: 0644]
src/be/nikiroo/tests/utils/BufferedInputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/BufferedOutputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/BundleTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/CryptUtilsTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/IOUtilsTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/NextableInputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/ProgressTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/ReplaceInputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/ReplaceOutputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/SerialServerTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/SerialTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/StringUtilsTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/TempFilesTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/Test.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/VersionTest.java [new file with mode: 0644]
src/be/nikiroo/tests/utils/bundle_test.properties [new file with mode: 0644]
src/be/nikiroo/utils [new submodule]

diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..f706148
--- /dev/null
@@ -0,0 +1,4 @@
+[submodule "src/be/nikiroo/utils"]
+       path = src/be/nikiroo/utils
+       url = ./
+       branch = subtree
index 2ac838d0dbbb62c2a782986671a5fba69822dc45..df7b7be1d54a0d9d8a31067ad0736622a606cce1 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -8,9 +8,9 @@
 # > SJAR_FLAGS: list of paths to include in the source jar file (`-C dir path`)
 # > PREFIX: the usual prefix to (un)install to -- you may of course override it
 #
-NAME  = program
-MAIN  = be/nikiroo/program/Main
-TEST  = be/nikiroo/tests/program/Test
+NAME  = nikiroo-utils
+MAIN  = be/nikiroo/tests/utils/Test
+TEST  = be/nikiroo/tests/utils/Test
 JAR_MISC    = -C ./ LICENSE -C ./ VERSION -C libs/ licenses
 JAR_FLAGS  += -C bin/ be -C bin/ org $(JAR_MISC)
 SJAR_FLAGS += -C src/ be -C src/ org $(JAR_MISC)
@@ -19,11 +19,11 @@ PREFIX = /usr/local
 
 #
 # Special Options for this program: you can modify the previous var if needed
-# > OPTION=non-default-value (or OPTION=default-value by default)
+# > UI=android (or UI=awt by default)
 #
-ifeq ($(OPTION),non-default-value)
-MORE += be/nikiroo/utils/android/test/TestAndroid
-TEST += be/nikiroo/utils/android/ImageUtilsAndroid
+ifeq ($(UI),android)
+MORE+= be/nikiroo/utils/android/ImageUtilsAndroid
+TEST += be/nikiroo/utils/android/test/TestAndroid
 else    
 MORE += be/nikiroo/utils/ui/ImageUtilsAwt 
 MORE += be/nikiroo/utils/ui/ImageTextAwt
diff --git a/VERSION b/VERSION
index 77d6f4ca23711533e724789a0a0045eab28c5ea6..f8ee15fd4ce7da0a0beadb0c403d95a3443454b5 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.0.0
+5.1.0-dev
diff --git a/bridge b/bridge
new file mode 100755 (executable)
index 0000000..a937348
--- /dev/null
+++ b/bridge
@@ -0,0 +1,19 @@
+#!/bin/sh
+NAME=nikiroo-utils.jar
+
+jar="$NAME"
+if [ ! -e "$jar" ]; then
+       jar="`dirname "$0"`/$NAME"
+fi
+
+if [ ! -e "$jar" ]; then
+       jar="`whereis "$NAME" | sed 's: :\n:g' | grep "$NAME" | head -n1`"
+fi
+
+if [ ! -e "$jar" ]; then
+       echo "$NAME not found." >&2
+       exit 1
+fi
+
+exec java -cp "$jar" be.nikiroo.utils.main.bridge "$@"
+
index ac7d779e33c5ece4d67b164ddb8f3b5fa8ca45c9..23ac627aad158819391bfc00181d1db91c71dbe5 100644 (file)
@@ -1,9 +1,377 @@
-# Program
+# nikiroo-utils
 
 ## Version WIP
 
-- new: something new
-- new: something else new
-- changed: something is not the same anymore
-- fix: fix/correction of a bug
+- new: IOUtils: openResource with anchor class
+- new: ConfigItem: easier to extend
+- fix: CacheMemory: fix stackoverflow bug
+- fix: Bundles/TransBundles: def values
+- new: Use git submodules instead of a sub branch
+- new: remove configure.sh, use simple Makefile
+
+## Version 5.1.0
+
+- Downloader: new Offline mode for cache accesses only
+- Cache: make sure an immedate SAVE/LOAD always work, even with 0 retention delays
+
+## Version 5.0.0
+
+- new: server: count the bytes we rec/send
+- new: CryptUtils
+- new: stream classes
+- new: Bundles can now also set Boolean, Integer... and not just get them
+- new: Bundles get/setList()
+- new: Bundles can now use the default values provided by the Meta
+- new: GUI presentation for Bundles is much improved
+- fix: IOUtils.readSmallStream and \n at the end
+- fix: Base64 implementation changed, no strange errors anymore
+- android: binary code (.class) is now bundled in the jar, too
+- change: StringUtils.unzip64(String) now returns a byte[] (StringUtils.unzip64s(String) can be used instead)
+- change: be.nikiroo.utils.markableFileInputStream moved to be.nikiroo.utils.streams (old class still present in @Deprecated state for now)
+- change: TestLauncher is now "silent" by default (no exception details, see setDetails(true))
+- runtime: serial: SSL -> CryptUtils (both are **runtime** incompatible and CryptUtils is slower)
+- runtime: break **runtime** compat with package utils.serial (Objects) -- ServerString should still be compatible (if not SSL obviously)
+
+## Version 4.7.3
+
+- fix: Downloader: POST and 302 redirects
+
+## Version 4.7.2
+
+- fix: Downloader issue with caching (input was spent when returned the first time)
+- fix: IOUtil.forceResetableStream
+
+## Version 4.7.1
+
+- new: can now select the TempFiles root
+- new: can now select the Image temporary root
+- fix: Cache now offer some tracing when cleaning the cache
+- fix: TemporaryFiles.close() was not indempotent (and was called in finalize!)
+
+## Version 4.7.0
+
+- tracer: traces on stderr by default
+- new: downloader one more public method
+- fix: downloader use original URL for cache ID
+
+## Version 4.6.2
+
+- fix: formatNumber/toNumber and decimals
+
+## Version 4.6.0
+
+- new: proxy
+- new: StringUtils formatNumber and toNumber
+- fix: UI Desc (NPE)
+
+## Version 4.5.2
+
+- Serial: fix b64/not b64 error
+- Serial: perf improvement
+- Base64: perf improvement
+- new: Proxy selector
+
+## Version 4.5.1
+
+- Progress: fix deadlock in rare cases
+
+## Version 4.5.0
+
+- Base64: allow access to streams
+- Deprecated: do not use our on deprecated functions
+- Serial: fix ZIP/noZIP error
+
+## Version 4.4.5
+
+- Base64: allow access to not-zipped Base64 utilities
+- Justify text: better handling of full text lines
+- jDoc: improve
+- IOUtils: new convenience method for reading a File into bytes
+
+## Version 4.4.4
+
+- Java 1.6: fix bad dependency so it can compiles on 1.6 again
+- TempFilesTest: fix test
+- Serial: fix for some constructors
+- Serial: better default choice for ZIP/noZIP content
+
+## Version 4.4.3
+
+- Test assertions: fix files/dir content comparison code
+
+## Version 4.4.2
+
+- Test assertions: can now compare files/dir content
+
+## Version 4.4.1
+
+- Image: fix undocumented exception on save images
+- TempFiles: crash early on error
+
+## Version 4.4.0
+
+- Text justification: now supports bullet lists and HR lines
+- Text justification: fix a bug with dashes (-) and a crash
+- Image to text converion fixes
+- Serial: now supports anonymous inner classes
+- Test: now allow an Exception argument to the "fail(..)" command
+- Downloader: add an optional cache
+- Cache: auto-clean when saving
+- Bridge: fix a NPE when tracing
+- New: justify, img2aa and bridge tools (see package Main)
+
+
+## Version 4.3.0
+
+- New: IOUtils.Unzip()
+- TestCase: better message for lists comparisons
+
+## Version 4.2.1
+
+- Fix small bug in Downloader
+
+## Version 4.2.0
+
+- New getLanguage() in TransBundle
+
+## Version 4.1.0
+
+- New TempFiles (Image.java now uses it instead of memory)
+- Auto cache cleaning + better errors in ImageUtilsAndroid
+- New String justification options
+
+## Version 4.0.1
+
+- Android compatibility (see configure.sh --android=yes)
+
+## Version 4.0.0
+
+- Deplace all dependencies on java.awt into its own package (ui)
+
+## Version 3.1.6
+
+- Fix Serialiser issue with custom objects and String in a custom object
+- Fix Progress/ProgressBar synchronisation issues
+- Fix Bridge default maxPrintSize parameter
+
+## Version 3.1.5
+
+- Fix Cache with no-parent file
+- Fix Progress (Error <> RuntimeException)
+
+## Version 3.1.4
+
+- Fix error handling for tracers in Server
+
+## Version 3.1.3
+
+- Fix ImageUtils.fromStream with non-resetable streams
+
+## Version 3.1.2
+
+- Fix Server regarding the client version passed to the handler
+- Improve ServerBridge options
+
+## Version 3.1.1
+
+- Some fixes and trace handling changes in ServerBridge
+- Some fixes in Import/Export (objects serialisation)
+
+## Version 3.1.0
+
+- New ServerBridge (including tests)
+
+## Version 3.0.0
+
+- jDoc
+- Fix bugs in Server (it was not possible to send objects back to client)
+- Improve code in Server (including tests), breaks API
+- Remove some deprecated things
+
+## Version 2.2.3
+
+- Fix in readSmallStream
+- Change traces handling
+
+## Version 2.2.2
+
+- New method in Cache: manually delete items
+
+## Version 2.2.1
+
+- Small fixes, especially in Progress
+
+## Version 2.2.0
+
+- New classes:
+  - Downloader: download URL from recalcitrant websites
+  - Cache: manage a local cache
+
+## Version 2.1.0
+
+- Better IOUtils
+
+## Version 2.0.0
+
+- API change
+  - IOUtils is now split between itself and ImageUtils -- some changes required in dependant projects
+  - Some slight renaming in StringUtils/IOUtils/ImageUtils
+
+- New class ImageText
+  - To create ASCII art
+
+## Version 1.6.3
+
+- Version.java
+  - Fix toString issues + test + update scripts
+
+## Version 1.6.2
+
+- Version.java
+  - Now supports "tag" on the versions (i.e., 0.0.4-niki1 -> tag is "niki", tagVersion is 1)
+
+## Version 1.6.1
+
+- Serialisation utilities
+  - Now supports enums and BufferedImages
+
+## Version 1.6.0
+
+- Serialisation utilities
+  - Server class to send/receive objects via network easily
+  - Serialiser now supports Arrays + fixes
+
+## Version 1.5.1
+
+- Serialisation utilities
+  - SerialUtils is now public and can be used to dynamically create an Object
+  - The Importer is now easier to use
+
+## Version 1.5.0
+
+- Bundles: change in Bundles and meta data
+  - The meta data is more complete now, but it breaks compatibility with both Bundles and @Meta
+  - A description can now be added to a bundle item in the graphical editor as a tooltip
+
+- Serialisation utilities
+  - A new set of utilities to quickly serialise objects
+
+## Version 1.4.3
+
+- Bugfix: unhtml
+  - Also replace non-breakable spaces by normal spaces
+
+## Version 1.4.2
+
+- Bugfix: Deltree
+  - Deltree was not OK for files...
+
+## Version 1.4.1
+
+- Progress
+  - Better handling of min==max case
+  - New methods .done() and .add(int step)
+
+## Version 1.4.0
+
+- R/W Bundles
+  - Bundle is now Read/Write
+
+- Bundle Configuration
+  - New UI controls to configure the Bundles graphically
+
+## Version 1.3.6
+
+- Fix for Java 1.6 compat
+  - Java 1.6 cannot compile it due to variables with ambigous names (which
+  - Java 1.8 can identify)
+
+## Version 1.3.5
+
+- Improve ProgressBar UI
+  - It now shows all the progression bars of the different steps of progression at the same time
+
+## Version 1.3.4
+
+- Improve TestCase error reporting
+  - We know display the full stack trace even for AssertionErrors
+
+- Extends Version
+  - ...with new methods: isOlderThan(Version) and isNewerThan(Version)
+
+## Version 1.3.3
+
+- New Version class
+  - Which can parse versions from the running program
+
+## Version 1.2.3
+
+- Add openResource and getVersion in IOUtils
+  - The file VERSION is supposed to exist
+
+- Give more informartion on AssertErrors
+  - The TestCase were not always helpful in case of AssertExceptions; they now print the stacktrace (they only used to do it for non-assert exceptions)
+
+- Fix configure.sh
+  - The VERSION file was not added, the Main method was not the correct one (so it was not producing working runnable JAR, yet it stated so)
+
+## Version 1.2.2
+
+- Fix bug in Bundle regarding \t handling
+  - ...tests should be written (later)
+
+## Version 1.2.1
+
+- New drawEllipse3D method
+  - ...in UIUtils
+
+## Version 1.1.1
+
+- Add UI component for Progress
+  - Still a WIP, it only show the current progress bar, still not the children bars (it's planned)
+
+## Version 1.1.0
+
+- Add progress reporting, move to ui package
+  - A new progress reporting system (and tests) in the new ui package (some other classes have been moved into ui, too: WrapLayout and UIUtils)
+
+## Version 1.0.0
+
+- Add WrapLayout and UIUtils
+  - A FlowLayout that automatically wrap to the next line (from existing code found on internet) and a method to set a fake-native look & feel
+
+## Version 0.9.7
+
+- Improve toImage and allow non-resetable InputStreams
+  - ...though they are then automatically saved onto disk then re-opened, then the file is deleted at the end of the process -- bad perfs
+  - Worse, it does it even if no EXIF metadata are present (because it cannot know that before reading the Stream, and cannot save a partially, non-resetable Stream to disk)
+
+- Reoarganize some methods from String to IO
+
+## Version 0.9.6
+
+- New test system
+  - Now some unit tests have been added, as well as the support classes
+
+## Version 0.9.5
+
+- Resource bundle bug
+  - UTF-8 strings were sometimes wrangled
+  - It is fixed by using a Bundle#Control, whih sadly is only available in Java 1.6+
+
+## Version 0.9.4
+
+- Compatibility bug
+  - Again... because of some useless imports made there for a wrong jDoc comment
+
+## Version 0.9.3
+
+- Compatibility bug
+  - The library did not work with JDK versions prior to 1.8 because of a dependency on Base64
+  - A new (public domain) class was used instead, which is compatible with Java 1.5 this time
+
+## Version 0.9.2
+
+- Initial version
+  - ...on GIT
 
diff --git a/export.sh b/export.sh
new file mode 100755 (executable)
index 0000000..c99ae22
--- /dev/null
+++ b/export.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+# Export script
+# 
+# Version:
+# - 1.1.1: use the new sjar command to make sources
+# - 1.1.0: allow multiple targets
+# - 1.0.0: add a version comment
+
+cd "`dirname "$0"`"
+
+if [ "$1" = "" ]; then
+       echo "You need to specify where to export it" >&2
+       exit 1
+fi
+
+LIBNAME="`cat configure.sh | grep '^echo "NAME = ' | cut -d'"' -f2 | cut -d= -f2`"
+LIBNAME="`echo $LIBNAME`"
+
+make mrpropre
+./configure.sh && make && make sjar
+if [ $? = 0 ]; then
+       while [ "$1" != "" ]; do
+               mkdir -p "$1"/libs/
+               cp "$LIBNAME"-`cat VERSION`-sources.jar "$1"/libs/
+               cp "$LIBNAME".jar "$1"/libs/
+               shift
+       done
+fi
+
diff --git a/img2aa b/img2aa
new file mode 100755 (executable)
index 0000000..8e5db09
--- /dev/null
+++ b/img2aa
@@ -0,0 +1,19 @@
+#!/bin/sh
+NAME=nikiroo-utils.jar
+
+jar="$NAME"
+if [ ! -e "$jar" ]; then
+       jar="`dirname "$0"`/$NAME"
+fi
+
+if [ ! -e "$jar" ]; then
+       jar="`whereis "$NAME" | sed 's: :\n:g' | grep "$NAME" | head -n1`"
+fi
+
+if [ ! -e "$jar" ]; then
+       echo "$NAME not found." >&2
+       exit 1
+fi
+
+exec java -cp "$jar" be.nikiroo.utils.main.img2aa "$@"
+
diff --git a/justify b/justify
new file mode 100755 (executable)
index 0000000..8d94a60
--- /dev/null
+++ b/justify
@@ -0,0 +1,19 @@
+#!/bin/sh
+NAME=nikiroo-utils.jar
+
+jar="$NAME"
+if [ ! -e "$jar" ]; then
+       jar="`dirname "$0"`/$NAME"
+fi
+
+if [ ! -e "$jar" ]; then
+       jar="`whereis "$NAME" | sed 's: :\n:g' | grep "$NAME" | head -n1`"
+fi
+
+if [ ! -e "$jar" ]; then
+       echo "$NAME not found." >&2
+       exit 1
+fi
+
+exec java -cp "$jar" be.nikiroo.utils.main.justify "$@"
+
diff --git a/libs/NanoHTTPD.java b/libs/NanoHTTPD.java
new file mode 100644 (file)
index 0000000..75174f3
--- /dev/null
@@ -0,0 +1,2358 @@
+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.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.nio.charset.CharsetEncoder;
+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.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+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.SSLException;
+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>
+ * 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>
+ * <ul>
+ * <p/>
+ * <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>Supports both dynamic content and file serving</li>
+ * <li>Supports file upload (since version 1.2, 2010)</li>
+ * <li>Supports partial content (streaming)</li>
+ * <li>Supports ETags</li>
+ * <li>Never caches anything</li>
+ * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
+ * <li>Default code serves files and shows all HTTP parameters and headers</li>
+ * <li>File server supports directory listing, index.html and index.htm</li>
+ * <li>File server supports partial content (streaming)</li>
+ * <li>File server supports ETags</li>
+ * <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 to lower case so they don't vary between
+ * browsers/clients</li>
+ * <p/>
+ * </ul>
+ * <p/>
+ * <p/>
+ * <b>How to use: </b>
+ * <ul>
+ * <p/>
+ * <li>Subclass and implement serve() and embed to your own program</li>
+ * <p/>
+ * </ul>
+ * <p/>
+ * See the separate "LICENSE.md" file for the distribution license (Modified BSD
+ * licence)
+ */
+public abstract class NanoHTTPD {
+
+    /**
+     * Pluggable strategy for asynchronously executing requests.
+     */
+    public interface AsyncRunner {
+
+        void closeAll();
+
+        void closed(ClientHandler clientHandler);
+
+        void exec(ClientHandler code);
+    }
+
+    /**
+     * The runnable that will be used for every new client connection.
+     */
+    public class ClientHandler implements Runnable {
+
+        private final InputStream inputStream;
+
+        private final Socket acceptSocket;
+
+        public ClientHandler(InputStream inputStream, Socket acceptSocket) {
+            this.inputStream = inputStream;
+            this.acceptSocket = acceptSocket;
+        }
+
+        public void close() {
+            safeClose(this.inputStream);
+            safeClose(this.acceptSocket);
+        }
+
+        @Override
+        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.SEVERE, "Communication with the client broken, or an bug in the handler code", 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 (#" + this.requestCount + ")");
+            this.running.add(clientHandler);
+            t.start();
+        }
+    }
+
+    /**
+     * Default strategy for creating and cleaning up temporary files.
+     * <p/>
+     * <p>
+     * By default, files are created by <code>File.createTempFile()</code> in
+     * the directory specified.
+     * </p>
+     */
+    public static class DefaultTempFile implements 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 void delete() throws Exception {
+            safeClose(this.fstream);
+            if (!this.file.delete()) {
+                throw new Exception("could not delete temporary file: " + this.file.getAbsolutePath());
+            }
+        }
+
+        @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>
+     * 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 DefaultTempFileManager implements TempFileManager {
+
+        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 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 TempFile createTempFile(String filename_hint) throws Exception {
+            DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir);
+            this.tempFiles.add(tempFile);
+            return tempFile;
+        }
+    }
+
+    /**
+     * Default strategy for creating and cleaning up temporary files.
+     */
+    private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
+
+        @Override
+        public TempFileManager create() {
+            return new DefaultTempFileManager();
+        }
+    }
+
+    /**
+     * Creates a normal ServerSocket for TCP connections
+     */
+    public static class DefaultServerSocketFactory implements ServerSocketFactory {
+
+        @Override
+        public ServerSocket create() throws IOException {
+            return new ServerSocket();
+        }
+
+    }
+
+    /**
+     * 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 static class ContentType {
+
+        private static final String ASCII_ENCODING = "US-ASCII";
+
+        private static final String MULTIPART_FORM_DATA_HEADER = "multipart/form-data";
+
+        private static final String CONTENT_REGEX = "[ |\t]*([^/^ ^;^,]+/[^ ^;^,]+)";
+
+        private static final Pattern MIME_PATTERN = Pattern.compile(CONTENT_REGEX, Pattern.CASE_INSENSITIVE);
+
+        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);
+
+        private final String contentTypeHeader;
+
+        private final String contentType;
+
+        private final String encoding;
+
+        private final String boundary;
+
+        public ContentType(String contentTypeHeader) {
+            this.contentTypeHeader = contentTypeHeader;
+            if (contentTypeHeader != null) {
+                contentType = getDetailFromContentHeader(contentTypeHeader, MIME_PATTERN, "", 1);
+                encoding = getDetailFromContentHeader(contentTypeHeader, CHARSET_PATTERN, null, 2);
+            } else {
+                contentType = "";
+                encoding = "UTF-8";
+            }
+            if (MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(contentType)) {
+                boundary = getDetailFromContentHeader(contentTypeHeader, BOUNDARY_PATTERN, null, 2);
+            } else {
+                boundary = null;
+            }
+        }
+
+        private String getDetailFromContentHeader(String contentTypeHeader, Pattern pattern, String defaultValue, int group) {
+            Matcher matcher = pattern.matcher(contentTypeHeader);
+            return matcher.find() ? matcher.group(group) : defaultValue;
+        }
+
+        public String getContentTypeHeader() {
+            return contentTypeHeader;
+        }
+
+        public String getContentType() {
+            return contentType;
+        }
+
+        public String getEncoding() {
+            return encoding == null ? ASCII_ENCODING : encoding;
+        }
+
+        public String getBoundary() {
+            return boundary;
+        }
+
+        public boolean isMultipart() {
+            return MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(contentType);
+        }
+
+        public ContentType tryUTF8() {
+            if (encoding == null) {
+                return new ContentType(this.contentTypeHeader + "; charset=UTF-8");
+            }
+            return this;
+        }
+    }
+
+    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 final BufferedInputStream inputStream;
+
+        private int splitbyte;
+
+        private int rlen;
+
+        private String uri;
+
+        private Method method;
+
+        private Map<String, List<String>> parms;
+
+        private Map<String, String> headers;
+
+        private CookieHandler cookies;
+
+        private String queryParameterString;
+
+        private String remoteIp;
+
+        private String remoteHostname;
+
+        private String protocolVersion;
+
+        public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
+            this.tempFileManager = tempFileManager;
+            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 BufferedInputStream(inputStream, HTTPSession.BUFSIZE);
+            this.outputStream = outputStream;
+            this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
+            this.remoteHostname = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "localhost" : inetAddress.getHostName().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, List<String>> parms, Map<String, String> headers) throws ResponseException {
+            try {
+                // Read the request line
+                String inLine = in.readLine();
+                if (inLine == null) {
+                    return;
+                }
+
+                StringTokenizer st = new StringTokenizer(inLine);
+                if (!st.hasMoreTokens()) {
+                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
+                }
+
+                pre.put("method", st.nextToken());
+
+                if (!st.hasMoreTokens()) {
+                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
+                }
+
+                String uri = st.nextToken();
+
+                // Decode parameters from the URI
+                int qmi = uri.indexOf('?');
+                if (qmi >= 0) {
+                    decodeParms(uri.substring(qmi + 1), parms);
+                    uri = decodePercent(uri.substring(0, qmi));
+                } else {
+                    uri = decodePercent(uri);
+                }
+
+                // 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()) {
+                    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().isEmpty()) {
+                    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);
+            } catch (IOException ioe) {
+                throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
+            }
+        }
+
+        /**
+         * Decodes the Multipart Body data and put it into Key/Value pairs.
+         */
+        private void decodeMultipartFormData(ContentType contentType, ByteBuffer fbuf, Map<String, List<String>> parms, Map<String, String> files) throws ResponseException {
+            int pcount = 0;
+            try {
+                int[] boundaryIdxs = getBoundaryPositions(fbuf, contentType.getBoundary().getBytes());
+                if (boundaryIdxs.length < 2) {
+                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings.");
+                }
+
+                byte[] partHeaderBuff = new byte[MAX_HEADER_SIZE];
+                for (int boundaryIdx = 0; boundaryIdx < boundaryIdxs.length - 1; boundaryIdx++) {
+                    fbuf.position(boundaryIdxs[boundaryIdx]);
+                    int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE;
+                    fbuf.get(partHeaderBuff, 0, len);
+                    BufferedReader in =
+                            new BufferedReader(new InputStreamReader(new ByteArrayInputStream(partHeaderBuff, 0, len), Charset.forName(contentType.getEncoding())), len);
+
+                    int headerLines = 0;
+                    // First line is boundary string
+                    String mpline = in.readLine();
+                    headerLines++;
+                    if (mpline == null || !mpline.contains(contentType.getBoundary())) {
+                        throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary.");
+                    }
+
+                    String partName = null, fileName = null, partContentType = 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 ("name".equalsIgnoreCase(key)) {
+                                    partName = matcher.group(2);
+                                } else if ("filename".equalsIgnoreCase(key)) {
+                                    fileName = matcher.group(2);
+                                    // add these two line to support multiple
+                                    // files uploaded using the same field Id
+                                    if (!fileName.isEmpty()) {
+                                        if (pcount > 0)
+                                            partName = partName + String.valueOf(pcount++);
+                                        else
+                                            pcount++;
+                                    }
+                                }
+                            }
+                        }
+                        matcher = CONTENT_TYPE_PATTERN.matcher(mpline);
+                        if (matcher.matches()) {
+                            partContentType = matcher.group(2).trim();
+                        }
+                        mpline = in.readLine();
+                        headerLines++;
+                    }
+                    int partHeaderLength = 0;
+                    while (headerLines-- > 0) {
+                        partHeaderLength = scipOverNewLine(partHeaderBuff, partHeaderLength);
+                    }
+                    // Read the part data
+                    if (partHeaderLength >= len - 4) {
+                        throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE.");
+                    }
+                    int partDataStart = boundaryIdxs[boundaryIdx] + partHeaderLength;
+                    int partDataEnd = boundaryIdxs[boundaryIdx + 1] - 4;
+
+                    fbuf.position(partDataStart);
+
+                    List<String> values = parms.get(partName);
+                    if (values == null) {
+                        values = new ArrayList<String>();
+                        parms.put(partName, values);
+                    }
+
+                    if (partContentType == null) {
+                        // Read the part into a string
+                        byte[] data_bytes = new byte[partDataEnd - partDataStart];
+                        fbuf.get(data_bytes);
+
+                        values.add(new String(data_bytes, contentType.getEncoding()));
+                    } else {
+                        // Read it into a file
+                        String path = saveTmpFile(fbuf, partDataStart, partDataEnd - partDataStart, fileName);
+                        if (!files.containsKey(partName)) {
+                            files.put(partName, path);
+                        } else {
+                            int count = 2;
+                            while (files.containsKey(partName + count)) {
+                                count++;
+                            }
+                            files.put(partName + count, path);
+                        }
+                        values.add(fileName);
+                    }
+                }
+            } catch (ResponseException re) {
+                throw re;
+            } catch (Exception e) {
+                throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString());
+            }
+        }
+
+        private int scipOverNewLine(byte[] partHeaderBuff, int index) {
+            while (partHeaderBuff[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.
+         */
+        private void decodeParms(String parms, Map<String, List<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('=');
+                String key = null;
+                String value = null;
+
+                if (sep >= 0) {
+                    key = decodePercent(e.substring(0, sep)).trim();
+                    value = decodePercent(e.substring(sep + 1));
+                } else {
+                    key = decodePercent(e).trim();
+                    value = "";
+                }
+
+                List<String> values = p.get(key);
+                if (values == null) {
+                    values = new ArrayList<String>();
+                    p.put(key, values);
+                }
+
+                values.add(value);
+            }
+        }
+
+        @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 (SSLException e) {
+                    throw e;
+                } catch (IOException 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, List<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. HTTP verb " + pre.get("method") + " unhandled.");
+                }
+
+                this.uri = pre.get("uri");
+
+                this.cookies = new CookieHandler(this.headers);
+
+                String connection = this.headers.get("connection");
+                boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (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 implementation 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 || r.isCloseConnection()) {
+                    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 (SSLException ssle) {
+                Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SSL PROTOCOL FAILURE: " + ssle.getMessage());
+                resp.send(this.outputStream);
+                safeClose(this.outputStream);
+            } catch (IOException 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.
+         */
+        private int findHeaderEnd(final byte[] buf, int rlen) {
+            int splitbyte = 0;
+            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. 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[] 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;
+                        }
+                    }
+                }
+                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;
+        }
+
+        /**
+         * @deprecated use {@link #getParameters()} instead.
+         */
+        @Override
+        @Deprecated
+        public final Map<String, String> getParms() {
+            Map<String, String> result = new HashMap<String, String>();
+            for (String key : this.parms.keySet()) {
+                result.put(key, this.parms.get(key).get(0));
+            }
+
+            return result;
+        }
+
+        @Override
+        public final Map<String, List<String>> getParameters() {
+            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
+            }
+        }
+
+        @Override
+        public final String getUri() {
+            return this.uri;
+        }
+
+        /**
+         * Deduce body length in bytes. Either from "content-length" header or
+         * read bytes.
+         */
+        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 requestDataOutput = null;
+
+                // Store the request in memory or a file, depending on size
+                if (size < MEMORY_STORE_LIMIT) {
+                    baos = new ByteArrayOutputStream();
+                    requestDataOutput = new DataOutputStream(baos);
+                } else {
+                    randomAccessFile = getTmpBucket();
+                    requestDataOutput = 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) {
+                        requestDataOutput.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)) {
+                    ContentType contentType = new ContentType(this.headers.get("content-type"));
+                    if (contentType.isMultipart()) {
+                        String boundary = contentType.getBoundary();
+                        if (boundary == null) {
+                            throw new ResponseException(Response.Status.BAD_REQUEST,
+                                    "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
+                        }
+                        decodeMultipartFormData(contentType, fbuf, this.parms, files);
+                    } else {
+                        byte[] postBytes = new byte[fbuf.remaining()];
+                        fbuf.get(postBytes);
+                        String postLine = new String(postBytes, contentType.getEncoding()).trim();
+                        // Handle application/x-www-form-urlencoded
+                        if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType.getContentType())) {
+                            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);
+            }
+        }
+
+        /**
+         * 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 = this.tempFileManager.createTempFile(filename_hint);
+                    ByteBuffer src = b.duplicate();
+                    fileOutputStream = new FileOutputStream(tempFile.getName());
+                    FileChannel dest = fileOutputStream.getChannel();
+                    src.position(offset).limit(offset + len);
+                    dest.write(src.slice());
+                    path = tempFile.getName();
+                } catch (Exception e) { // Catch exception if any
+                    throw new Error(e); // we won't recover, so throw an error
+                } finally {
+                    safeClose(fileOutputStream);
+                }
+            }
+            return path;
+        }
+
+        @Override
+        public String getRemoteIpAddress() {
+            return this.remoteIp;
+        }
+
+        @Override
+        public String getRemoteHostName() {
+            return this.remoteHostname;
+        }
+    }
+
+    /**
+     * Handles one session, i.e. parses the HTTP request and returns the
+     * response.
+     */
+    public interface IHTTPSession {
+
+        void execute() throws IOException;
+
+        CookieHandler getCookies();
+
+        Map<String, String> getHeaders();
+
+        InputStream getInputStream();
+
+        Method getMethod();
+
+        /**
+         * This method will only return the first value for a given parameter.
+         * You will want to use getParameters if you expect multiple values for
+         * a given key.
+         * 
+         * @deprecated use {@link #getParameters()} instead.
+         */
+        @Deprecated
+        Map<String, String> getParms();
+
+        Map<String, List<String>> getParameters();
+
+        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;
+
+        /**
+         * Get the remote ip address of the requester.
+         * 
+         * @return the IP address.
+         */
+        String getRemoteIpAddress();
+
+        /**
+         * Get the remote hostname of the requester.
+         * 
+         * @return the hostname.
+         */
+        String getRemoteHostName();
+    }
+
+    /**
+     * 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,
+        PROPFIND,
+        PROPPATCH,
+        MKCOL,
+        MOVE,
+        COPY,
+        LOCK,
+        UNLOCK;
+
+        static Method lookup(String method) {
+            if (method == null)
+                return null;
+
+            try {
+                return valueOf(method);
+            } catch (IllegalArgumentException e) {
+                // TODO: Log it?
+                return null;
+            }
+        }
+    }
+
+    /**
+     * HTTP response. Return one of these from serve().
+     */
+    public static class Response implements Closeable {
+
+        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"),
+            MULTI_STATUS(207, "Multi-Status"),
+
+            REDIRECT(301, "Moved Permanently"),
+            /**
+             * Many user agents mishandle 302 in ways that violate the RFC1945
+             * spec (i.e., redirect a POST to a GET). 303 and 307 were added in
+             * RFC2616 to address this. You should prefer 303 and 307 unless the
+             * calling user agent does not support 303 and 307 functionality
+             */
+            @Deprecated
+            FOUND(302, "Found"),
+            REDIRECT_SEE_OTHER(303, "See Other"),
+            NOT_MODIFIED(304, "Not Modified"),
+            TEMPORARY_REDIRECT(307, "Temporary Redirect"),
+
+            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"),
+            GONE(410, "Gone"),
+            LENGTH_REQUIRED(411, "Length Required"),
+            PRECONDITION_FAILED(412, "Precondition Failed"),
+            PAYLOAD_TOO_LARGE(413, "Payload Too Large"),
+            UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
+            RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
+            EXPECTATION_FAILED(417, "Expectation Failed"),
+            TOO_MANY_REQUESTS(429, "Too Many Requests"),
+
+            INTERNAL_ERROR(500, "Internal Server Error"),
+            NOT_IMPLEMENTED(501, "Not Implemented"),
+            SERVICE_UNAVAILABLE(503, "Service Unavailable"),
+            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;
+            }
+
+            public static Status lookup(int requestStatus) {
+                for (Status status : Status.values()) {
+                    if (status.getRequestStatus() == requestStatus) {
+                        return status;
+                    }
+                }
+                return null;
+            }
+
+            @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. the
+         * lowercase map is automatically kept up to date.
+         */
+        @SuppressWarnings("serial")
+        private final Map<String, String> header = new HashMap<String, String>() {
+
+            public String put(String key, String value) {
+                lowerCaseHeader.put(key == null ? key : key.toLowerCase(), value);
+                return super.put(key, value);
+            };
+        };
+
+        /**
+         * copy of the header map with all the keys lowercase for faster
+         * searching.
+         */
+        private final Map<String, String> lowerCaseHeader = 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);
+        }
+
+        /**
+         * Indicate to close the connection after the Response has been sent.
+         * 
+         * @param close
+         *            {@code true} to hint connection closing, {@code false} to
+         *            let connection be closed by client.
+         */
+        public void closeConnection(boolean close) {
+            if (close)
+                this.header.put("connection", "close");
+            else
+                this.header.remove("connection");
+        }
+
+        /**
+         * @return {@code true} if connection is to be closed after this
+         *         Response has been sent.
+         */
+        public boolean isCloseConnection() {
+            return "close".equals(getHeader("connection"));
+        }
+
+        public InputStream getData() {
+            return this.data;
+        }
+
+        public String getHeader(String name) {
+            return this.lowerCaseHeader.get(name.toLowerCase());
+        }
+
+        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;
+        }
+
+        /**
+         * Sends given response to the socket.
+         */
+        protected void send(OutputStream outputStream) {
+            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, new ContentType(this.mimeType).getEncoding())), false);
+                pw.append("HTTP/1.1 ").append(this.status.getDescription()).append(" \r\n");
+                if (this.mimeType != null) {
+                    printHeader(pw, "Content-Type", this.mimeType);
+                }
+                if (getHeader("date") == null) {
+                    printHeader(pw, "Date", gmtFrmt.format(new Date()));
+                }
+                for (Entry<String, String> entry : this.header.entrySet()) {
+                    printHeader(pw, entry.getKey(), entry.getValue());
+                }
+                if (getHeader("connection") == null) {
+                    printHeader(pw, "Connection", (this.keepAlive ? "keep-alive" : "close"));
+                }
+                if (getHeader("content-length") != null) {
+                    encodeAsGzip = false;
+                }
+                if (encodeAsGzip) {
+                    printHeader(pw, "Content-Encoding", "gzip");
+                    setChunkedTransfer(true);
+                }
+                long pending = this.data != null ? this.contentLength : 0;
+                if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
+                    printHeader(pw, "Transfer-Encoding", "chunked");
+                } else if (!encodeAsGzip) {
+                    pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending);
+                }
+                pw.append("\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);
+            }
+        }
+
+        @SuppressWarnings("static-method")
+        protected void printHeader(PrintWriter pw, String key, String value) {
+            pw.append(key).append(": ").append(value).append("\r\n");
+        }
+
+        protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, long defaultSize) {
+            String contentLengthString = getHeader("content-length");
+            long size = defaultSize;
+            if (contentLengthString != null) {
+                try {
+                    size = Long.parseLong(contentLengthString);
+                } catch (NumberFormatException ex) {
+                    LOG.severe("content-length was no number " + contentLengthString);
+                }
+            }
+            pw.print("Content-Length: " + size + "\r\n");
+            return size;
+        }
+
+        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;
+                }
+            }
+        }
+
+        public void setChunkedTransfer(boolean chunkedTransfer) {
+            this.chunkedTransfer = chunkedTransfer;
+        }
+
+        public void setData(InputStream data) {
+            this.data = data;
+        }
+
+        public void setMimeType(String mimeType) {
+            this.mimeType = mimeType;
+        }
+
+        public void setRequestMethod(Method requestMethod) {
+            this.requestMethod = requestMethod;
+        }
+
+        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;
+
+        public 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;
+    }
+
+    @SuppressWarnings({
+        "unchecked",
+        "rawtypes"
+    })
+    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(stream);
+                } 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);
+
+            if (keystoreStream == null) {
+                throw new IOException("Unable to load keystore from classpath: " + 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.
+     */
+    @SuppressWarnings("static-method")
+    protected boolean useGzipWhenAccepted(Response r) {
+        return r.getMimeType() != null && (r.getMimeType().toLowerCase().contains("text/") || r.getMimeType().toLowerCase().contains("/json"));
+    }
+
+    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) {
+        ContentType contentType = new ContentType(mimeType);
+        if (txt == null) {
+            return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0);
+        } else {
+            byte[] bytes;
+            try {
+                CharsetEncoder newEncoder = Charset.forName(contentType.getEncoding()).newEncoder();
+                if (!newEncoder.canEncode(txt)) {
+                    contentType = contentType.tryUTF8();
+                }
+                bytes = txt.getBytes(contentType.getEncoding());
+            } catch (UnsupportedEncodingException e) {
+                NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e);
+                bytes = new byte[0];
+            }
+            return newFixedLengthResponse(status, contentType.getContentTypeHeader(), 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/libs/WrapLayout.java b/libs/WrapLayout.java
new file mode 100644 (file)
index 0000000..da03c2f
--- /dev/null
@@ -0,0 +1,205 @@
+package be.nikiroo.utils;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+
+import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+
+/**
+ * FlowLayout subclass that fully supports wrapping of components.
+ * 
+ * @author https://tips4java.wordpress.com/2008/11/06/wrap-layout/
+ */
+public class WrapLayout extends FlowLayout {
+       private static final long serialVersionUID = 1L;
+
+       /**
+        * Constructs a new <code>WrapLayout</code> with a left alignment and a
+        * default 5-unit horizontal and vertical gap.
+        */
+       public WrapLayout() {
+               super();
+       }
+
+       /**
+        * Constructs a new <code>FlowLayout</code> with the specified alignment and
+        * a default 5-unit horizontal and vertical gap. The value of the alignment
+        * argument must be one of <code>WrapLayout</code>, <code>WrapLayout</code>,
+        * or <code>WrapLayout</code>.
+        * 
+        * @param align
+        *            the alignment value
+        */
+       public WrapLayout(int align) {
+               super(align);
+       }
+
+       /**
+        * Creates a new flow layout manager with the indicated alignment and the
+        * indicated horizontal and vertical gaps.
+        * <p>
+        * The value of the alignment argument must be one of
+        * <code>WrapLayout</code>, <code>WrapLayout</code>, or
+        * <code>WrapLayout</code>.
+        * 
+        * @param align
+        *            the alignment value
+        * @param hgap
+        *            the horizontal gap between components
+        * @param vgap
+        *            the vertical gap between components
+        */
+       public WrapLayout(int align, int hgap, int vgap) {
+               super(align, hgap, vgap);
+       }
+
+       /**
+        * Returns the preferred dimensions for this layout given the <i>visible</i>
+        * components in the specified target container.
+        * 
+        * @param target
+        *            the component which needs to be laid out
+        * @return the preferred dimensions to lay out the subcomponents of the
+        *         specified container
+        */
+       @Override
+       public Dimension preferredLayoutSize(Container target) {
+               return layoutSize(target, true);
+       }
+
+       /**
+        * Returns the minimum dimensions needed to layout the <i>visible</i>
+        * components contained in the specified target container.
+        * 
+        * @param target
+        *            the component which needs to be laid out
+        * @return the minimum dimensions to lay out the subcomponents of the
+        *         specified container
+        */
+       @Override
+       public Dimension minimumLayoutSize(Container target) {
+               Dimension minimum = layoutSize(target, false);
+               minimum.width -= (getHgap() + 1);
+               return minimum;
+       }
+
+       /**
+        * Returns the minimum or preferred dimension needed to layout the target
+        * container.
+        *
+        * @param target
+        *            target to get layout size for
+        * @param preferred
+        *            should preferred size be calculated
+        * @return the dimension to layout the target container
+        */
+       private Dimension layoutSize(Container target, boolean preferred) {
+               synchronized (target.getTreeLock()) {
+                       // Each row must fit with the width allocated to the containter.
+                       // When the container width = 0, the preferred width of the
+                       // container
+                       // has not yet been calculated so lets ask for the maximum.
+
+                       int targetWidth = target.getSize().width;
+                       Container container = target;
+
+                       while (container.getSize().width == 0
+                                       && container.getParent() != null) {
+                               container = container.getParent();
+                       }
+
+                       targetWidth = container.getSize().width;
+
+                       if (targetWidth == 0)
+                               targetWidth = Integer.MAX_VALUE;
+
+                       int hgap = getHgap();
+                       int vgap = getVgap();
+                       Insets insets = target.getInsets();
+                       int horizontalInsetsAndGap = insets.left + insets.right
+                                       + (hgap * 2);
+                       int maxWidth = targetWidth - horizontalInsetsAndGap;
+
+                       // Fit components into the allowed width
+
+                       Dimension dim = new Dimension(0, 0);
+                       int rowWidth = 0;
+                       int rowHeight = 0;
+
+                       int nmembers = target.getComponentCount();
+
+                       for (int i = 0; i < nmembers; i++) {
+                               Component m = target.getComponent(i);
+
+                               if (m.isVisible()) {
+                                       Dimension d = preferred ? m.getPreferredSize() : m
+                                                       .getMinimumSize();
+
+                                       // Can't add the component to current row. Start a new
+                                       // row.
+
+                                       if (rowWidth + d.width > maxWidth) {
+                                               addRow(dim, rowWidth, rowHeight);
+                                               rowWidth = 0;
+                                               rowHeight = 0;
+                                       }
+
+                                       // Add a horizontal gap for all components after the
+                                       // first
+
+                                       if (rowWidth != 0) {
+                                               rowWidth += hgap;
+                                       }
+
+                                       rowWidth += d.width;
+                                       rowHeight = Math.max(rowHeight, d.height);
+                               }
+                       }
+
+                       addRow(dim, rowWidth, rowHeight);
+
+                       dim.width += horizontalInsetsAndGap;
+                       dim.height += insets.top + insets.bottom + vgap * 2;
+
+                       // When using a scroll pane or the DecoratedLookAndFeel we need
+                       // to
+                       // make sure the preferred size is less than the size of the
+                       // target containter so shrinking the container size works
+                       // correctly. Removing the horizontal gap is an easy way to do
+                       // this.
+
+                       Container scrollPane = SwingUtilities.getAncestorOfClass(
+                                       JScrollPane.class, target);
+
+                       if (scrollPane != null && target.isValid()) {
+                               dim.width -= (hgap + 1);
+                       }
+
+                       return dim;
+               }
+       }
+
+       /*
+        * A new row has been completed. Use the dimensions of this row to update
+        * the preferred size for the container.
+        * 
+        * @param dim update the width and height when appropriate
+        * 
+        * @param rowWidth the width of the row to add
+        * 
+        * @param rowHeight the height of the row to add
+        */
+       private void addRow(Dimension dim, int rowWidth, int rowHeight) {
+               dim.width = Math.max(dim.width, rowWidth);
+
+               if (dim.height > 0) {
+                       dim.height += getVgap();
+               }
+
+               dim.height += rowHeight;
+       }
+}
\ No newline at end of file
diff --git a/libs/bin/be/nikiroo/utils/android/ImageUtilsAndroid.class b/libs/bin/be/nikiroo/utils/android/ImageUtilsAndroid.class
new file mode 100644 (file)
index 0000000..844712a
Binary files /dev/null and b/libs/bin/be/nikiroo/utils/android/ImageUtilsAndroid.class differ
diff --git a/libs/bin/be/nikiroo/utils/android/test/TestAndroid.class b/libs/bin/be/nikiroo/utils/android/test/TestAndroid.class
new file mode 100644 (file)
index 0000000..216aa20
Binary files /dev/null and b/libs/bin/be/nikiroo/utils/android/test/TestAndroid.class differ
diff --git a/libs/licenses/nanohttpd-2.3.1-LICENSE.md b/libs/licenses/nanohttpd-2.3.1-LICENSE.md
new file mode 100644 (file)
index 0000000..8dc4ca7
--- /dev/null
@@ -0,0 +1,12 @@
+Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+* 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.
+
+* Neither the name of the NanoHttpd organization 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/libs/licenses/unbescape_1.1.4_LICENSE.txt b/libs/licenses/unbescape_1.1.4_LICENSE.txt
new file mode 100644 (file)
index 0000000..d645695
--- /dev/null
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/libs/unbescape-1.1.4-sources.jar b/libs/unbescape-1.1.4-sources.jar
new file mode 100644 (file)
index 0000000..01ddb56
Binary files /dev/null and b/libs/unbescape-1.1.4-sources.jar differ
diff --git a/src/be/nikiroo/tests/utils/BufferedInputStreamTest.java b/src/be/nikiroo/tests/utils/BufferedInputStreamTest.java
new file mode 100644 (file)
index 0000000..ed753bf
--- /dev/null
@@ -0,0 +1,115 @@
+package be.nikiroo.tests.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.BufferedInputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class BufferedInputStreamTest extends TestLauncher {
+       public BufferedInputStreamTest(String[] args) {
+               super("BufferedInputStream test", args);
+
+               addTest(new TestCase("Simple InputStream reading") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] expected = new byte[] { 42, 12, 0, 127 };
+                               BufferedInputStream in = new BufferedInputStream(
+                                               new ByteArrayInputStream(expected));
+                               checkArrays(this, "FIRST", in, expected);
+                       }
+               });
+
+               addTest(new TestCase("Simple byte array reading") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] expected = new byte[] { 42, 12, 0, 127 };
+                               BufferedInputStream in = new BufferedInputStream(expected);
+                               checkArrays(this, "FIRST", in, expected);
+                       }
+               });
+
+               addTest(new TestCase("Byte array is(byte[])") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] expected = new byte[] { 42, 12, 0, 127 };
+                               BufferedInputStream in = new BufferedInputStream(expected);
+                               assertEquals(
+                                               "The array should be considered identical to its source",
+                                               true, in.is(expected));
+                               assertEquals(
+                                               "The array should be considered different to that one",
+                                               false, in.is(new byte[] { 42, 12, 0, 121 }));
+                               in.close();
+                       }
+               });
+
+               addTest(new TestCase("InputStream is(byte[])") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] expected = new byte[] { 42, 12, 0, 127 };
+                               BufferedInputStream in = new BufferedInputStream(
+                                               new ByteArrayInputStream(expected));
+                               assertEquals(
+                                               "The array should be considered identical to its source",
+                                               true, in.is(expected));
+                               assertEquals(
+                                               "The array should be considered different to that one",
+                                               false, in.is(new byte[] { 42, 12, 0, 121 }));
+                               in.close();
+                       }
+               });
+
+               addTest(new TestCase("Byte array is(String)") {
+                       @Override
+                       public void test() throws Exception {
+                               String expected = "Testy";
+                               BufferedInputStream in = new BufferedInputStream(
+                                               expected.getBytes("UTF-8"));
+                               assertEquals(
+                                               "The array should be considered identical to its source",
+                                               true, in.is(expected));
+                               assertEquals(
+                                               "The array should be considered different to that one",
+                                               false, in.is("Autre"));
+                               assertEquals(
+                                               "The array should be considered different to that one",
+                                               false, in.is("Test"));
+                               in.close();
+                       }
+               });
+
+               addTest(new TestCase("InputStream is(String)") {
+                       @Override
+                       public void test() throws Exception {
+                               String expected = "Testy";
+                               BufferedInputStream in = new BufferedInputStream(
+                                               new ByteArrayInputStream(expected.getBytes("UTF-8")));
+                               assertEquals(
+                                               "The array should be considered identical to its source",
+                                               true, in.is(expected));
+                               assertEquals(
+                                               "The array should be considered different to that one",
+                                               false, in.is("Autre"));
+                               assertEquals(
+                                               "The array should be considered different to that one",
+                                               false, in.is("Testy."));
+                               in.close();
+                       }
+               });
+       }
+
+       static void checkArrays(TestCase test, String prefix, InputStream in,
+                       byte[] expected) throws Exception {
+               byte[] actual = IOUtils.toByteArray(in);
+               test.assertEquals("The " + prefix
+                               + " resulting array has not the correct number of items",
+                               expected.length, actual.length);
+               for (int i = 0; i < actual.length; i++) {
+                       test.assertEquals(prefix + ": item " + i
+                                       + " (0-based) is not the same", expected[i], actual[i]);
+               }
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/BufferedOutputStreamTest.java b/src/be/nikiroo/tests/utils/BufferedOutputStreamTest.java
new file mode 100644 (file)
index 0000000..928faad
--- /dev/null
@@ -0,0 +1,136 @@
+package be.nikiroo.tests.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import be.nikiroo.utils.streams.BufferedOutputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class BufferedOutputStreamTest extends TestLauncher {
+       public BufferedOutputStreamTest(String[] args) {
+               super("BufferedOutputStream test", args);
+
+               addTest(new TestCase("Single write") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               BufferedOutputStream out = new BufferedOutputStream(bout);
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, data);
+                       }
+               });
+
+               addTest(new TestCase("Single write of 5000 bytes") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               BufferedOutputStream out = new BufferedOutputStream(bout);
+
+                               byte[] data = new byte[5000];
+                               for (int i = 0; i < data.length; i++) {
+                                       data[i] = (byte) (i % 255);
+                               }
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, data);
+                       }
+               });
+
+               addTest(new TestCase("Multiple writes") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               BufferedOutputStream out = new BufferedOutputStream(bout);
+
+                               byte[] data1 = new byte[] { 42, 12, 0, 127 };
+                               byte[] data2 = new byte[] { 15, 55 };
+                               byte[] data3 = new byte[] {};
+
+                               byte[] dataAll = new byte[] { 42, 12, 0, 127, 15, 55 };
+
+                               out.write(data1);
+                               out.write(data2);
+                               out.write(data3);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, dataAll);
+                       }
+               });
+
+               addTest(new TestCase("Multiple writes for a 5000 bytes total") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               BufferedOutputStream out = new BufferedOutputStream(bout);
+
+                               byte[] data = new byte[] { 42, 12, 0, 127, 51, 2, 32, 66, 7, 87 };
+
+                               List<Byte> bytes = new ArrayList<Byte>();
+
+                               // write 400 * 10 + 1000 bytes = 5000
+                               for (int i = 0; i < 400; i++) {
+                                       for (int j = 0; j < data.length; j++) {
+                                               bytes.add(data[j]);
+                                       }
+                                       out.write(data);
+                               }
+
+                               for (int i = 0; i < 1000; i++) {
+                                       for (int j = 0; j < data.length; j++) {
+                                               bytes.add(data[j]);
+                                       }
+                                       out.write(data);
+                               }
+
+                               out.close();
+
+                               byte[] abytes = new byte[bytes.size()];
+                               for (int i = 0; i < bytes.size(); i++) {
+                                       abytes[i] = bytes.get(i);
+                               }
+
+                               checkArrays(this, "FIRST", bout, abytes);
+                       }
+               });
+       }
+
+       static void checkArrays(TestCase test, String prefix,
+                       ByteArrayOutputStream bout, byte[] expected) throws Exception {
+               byte[] actual = bout.toByteArray();
+
+               if (false) {
+                       System.out.print("\nExpected data: [ ");
+                       for (int i = 0; i < expected.length; i++) {
+                               if (i > 0)
+                                       System.out.print(", ");
+                               System.out.print(expected[i]);
+                       }
+                       System.out.println(" ]");
+
+                       System.out.print("Actual data  : [ ");
+                       for (int i = 0; i < actual.length; i++) {
+                               if (i > 0)
+                                       System.out.print(", ");
+                               System.out.print(actual[i]);
+                       }
+                       System.out.println(" ]");
+               }
+
+               test.assertEquals("The " + prefix
+                               + " resulting array has not the correct number of items",
+                               expected.length, actual.length);
+               for (int i = 0; i < actual.length; i++) {
+                       test.assertEquals(prefix + ": item " + i
+                                       + " (0-based) is not the same", expected[i], actual[i]);
+               }
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/BundleTest.java b/src/be/nikiroo/tests/utils/BundleTest.java
new file mode 100644 (file)
index 0000000..f8e833f
--- /dev/null
@@ -0,0 +1,249 @@
+package be.nikiroo.tests.utils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.resources.Bundle;
+import be.nikiroo.utils.resources.Bundles;
+import be.nikiroo.utils.resources.Meta;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class BundleTest extends TestLauncher {
+       private File tmp;
+       private B b = new B();
+
+       public BundleTest(String[] args) {
+               this("Bundle test", args);
+       }
+
+       protected BundleTest(String name, String[] args) {
+               super(name, args);
+
+               for (TestCase test : getSimpleTests()) {
+                       addTest(test);
+               }
+
+               addSeries(new TestLauncher("After saving/reloading the resources", args) {
+                       {
+                               for (TestCase test : getSimpleTests()) {
+                                       addTest(test);
+                               }
+                       }
+
+                       @Override
+                       protected void start() throws Exception {
+                               tmp = File.createTempFile("nikiroo-utils", ".test");
+                               tmp.delete();
+                               tmp.mkdir();
+                               b.updateFile(tmp.getAbsolutePath());
+                               Bundles.setDirectory(tmp.getAbsolutePath());
+                               b.reload(false);
+                       }
+
+                       @Override
+                       protected void stop() {
+                               IOUtils.deltree(tmp);
+                       }
+               });
+
+               addSeries(new TestLauncher("Read/Write support", args) {
+                       {
+                               addTest(new TestCase("Reload") {
+                                       @Override
+                                       public void test() throws Exception {
+                                               String def = b.getString(E.ONE);
+                                               String val = "Something";
+
+                                               b.setString(E.ONE, val);
+                                               b.updateFile();
+                                               b.reload(true);
+
+                                               assertEquals("We should have reset the bundle", def,
+                                                               b.getString(E.ONE));
+
+                                               b.reload(false);
+
+                                               assertEquals("We should have reloaded the same files",
+                                                               val, b.getString(E.ONE));
+
+                                               // reset values for next tests
+                                               b.reload(true);
+                                               b.updateFile();
+                                       }
+                               });
+
+                               addTest(new TestCase("Set/Get") {
+                                       @Override
+                                       public void test() throws Exception {
+                                               String val = "Newp";
+                                               b.setString(E.ONE, val);
+                                               String setGet = b.getString(E.ONE);
+
+                                               assertEquals(val, setGet);
+
+                                               // reset values for next tests
+                                               b.restoreSnapshot(null);
+                                       }
+                               });
+
+                               addTest(new TestCase("Snapshots") {
+                                       @Override
+                                       public void test() throws Exception {
+                                               String val = "Newp";
+                                               String def = b.getString(E.ONE);
+
+                                               b.setString(E.ONE, val);
+                                               Object snap = b.takeSnapshot();
+
+                                               b.restoreSnapshot(null);
+                                               assertEquals(
+                                                               "restoreChanges(null) should clear the changes",
+                                                               def, b.getString(E.ONE));
+                                               b.restoreSnapshot(snap);
+                                               assertEquals(
+                                                               "restoreChanges(snapshot) should restore the changes",
+                                                               val, b.getString(E.ONE));
+
+                                               // reset values for next tests
+                                               b.restoreSnapshot(null);
+                                       }
+                               });
+
+                               addTest(new TestCase("updateFile with changes") {
+                                       @Override
+                                       public void test() throws Exception {
+                                               String val = "Go to disk! (UTF-8 test: 日本語)";
+
+                                               String def = b.getString(E.ONE);
+                                               b.setString(E.ONE, val);
+                                               b.updateFile(tmp.getAbsolutePath());
+                                               b.reload(false);
+
+                                               assertEquals(val, b.getString(E.ONE));
+
+                                               // reset values for next tests
+                                               b.setString(E.ONE, def);
+                                               b.updateFile(tmp.getAbsolutePath());
+                                               b.reload(false);
+                                       }
+                               });
+                       }
+
+                       @Override
+                       protected void start() throws Exception {
+                               tmp = File.createTempFile("nikiroo-utils", ".test");
+                               tmp.delete();
+                               tmp.mkdir();
+                               b.updateFile(tmp.getAbsolutePath());
+                               Bundles.setDirectory(tmp.getAbsolutePath());
+                               b.reload(false);
+                       }
+
+                       @Override
+                       protected void stop() {
+                               IOUtils.deltree(tmp);
+                       }
+               });
+       }
+
+       private List<TestCase> getSimpleTests() {
+               String pre = "";
+
+               List<TestCase> list = new ArrayList<TestCase>();
+
+               list.add(new TestCase(pre + "getString simple") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals("un", b.getString(E.ONE));
+                       }
+               });
+
+               list.add(new TestCase(pre + "getStringX with null suffix") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals("un", b.getStringX(E.ONE, null));
+                       }
+               });
+
+               list.add(new TestCase(pre + "getStringX with empty suffix") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals(null, b.getStringX(E.ONE, ""));
+                       }
+               });
+
+               list.add(new TestCase(pre + "getStringX with existing suffix") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals("un + suffix", b.getStringX(E.ONE, "suffix"));
+                       }
+               });
+
+               list.add(new TestCase(pre + "getStringX with not existing suffix") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals(null, b.getStringX(E.ONE, "fake"));
+                       }
+               });
+
+               list.add(new TestCase(pre + "getString with UTF-8 content") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals("日本語 Nihongo", b.getString(E.JAPANESE));
+                       }
+               });
+
+               return list;
+       }
+
+       /**
+        * {@link Bundle}.
+        * 
+        * @author niki
+        */
+       private class B extends Bundle<E> {
+               protected B() {
+                       super(E.class, N.bundle_test, null);
+               }
+
+               @Override
+               // ...and make it public
+               public Object takeSnapshot() {
+                       return super.takeSnapshot();
+               }
+
+               @Override
+               // ...and make it public
+               public void restoreSnapshot(Object snap) {
+                       super.restoreSnapshot(snap);
+               }
+       }
+
+       /**
+        * Key enum for the {@link Bundle}.
+        * 
+        * @author niki
+        */
+       private enum E {
+               @Meta
+               ONE, //
+               @Meta
+               ONE_SUFFIX, //
+               @Meta
+               TWO, //
+               @Meta
+               JAPANESE
+       }
+
+       /**
+        * Name enum for the {@link Bundle}.
+        * 
+        * @author niki
+        */
+       private enum N {
+               bundle_test
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/CryptUtilsTest.java b/src/be/nikiroo/tests/utils/CryptUtilsTest.java
new file mode 100644 (file)
index 0000000..826035e
--- /dev/null
@@ -0,0 +1,155 @@
+package be.nikiroo.tests.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+import be.nikiroo.utils.CryptUtils;
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class CryptUtilsTest extends TestLauncher {
+       private String key;
+       private CryptUtils crypt;
+
+       public CryptUtilsTest(String[] args) {
+               super("CryptUtils test", args);
+
+               String longKey = "some long string with more than 128 bits (=32 bytes) of data";
+
+               addSeries(new CryptUtilsTest(args, "Manual input wuth NULL key", null,
+                               1));
+               addSeries(new CryptUtilsTest(args, "Streams with NULL key", null, true));
+
+               addSeries(new CryptUtilsTest(args, "Manual input with emptykey", "", 1));
+               addSeries(new CryptUtilsTest(args, "Streams with empty key", "", true));
+
+               addSeries(new CryptUtilsTest(args, "Manual input with long key",
+                               longKey, 1));
+               addSeries(new CryptUtilsTest(args, "Streams with long key", longKey,
+                               true));
+       }
+
+       @Override
+       protected void addTest(final TestCase test) {
+               super.addTest(new TestCase(test.getName()) {
+                       @Override
+                       public void test() throws Exception {
+                               test.test();
+                       }
+
+                       @Override
+                       public void setUp() throws Exception {
+                               crypt = new CryptUtils(key);
+                               test.setUp();
+                       }
+
+                       @Override
+                       public void tearDown() throws Exception {
+                               test.tearDown();
+                               crypt = null;
+                       }
+               });
+       }
+
+       private CryptUtilsTest(String[] args, String title, String key,
+                       @SuppressWarnings("unused") int dummy) {
+               super(title, args);
+               this.key = key;
+
+               final String longData = "Le premier jour, Le Grand Barbu dans le cloud fit la lumière, et il vit que c'était bien. Ou quelque chose comme ça. Je préfère la Science-Fiction en général, je trouve ça plus sain :/";
+
+               addTest(new TestCase("Short") {
+                       @Override
+                       public void test() throws Exception {
+                               String orig = "data";
+                               byte[] encrypted = crypt.encrypt(orig);
+                               String decrypted = crypt.decrypts(encrypted);
+
+                               assertEquals(orig, decrypted);
+                       }
+               });
+
+               addTest(new TestCase("Short, base64") {
+                       @Override
+                       public void test() throws Exception {
+                               String orig = "data";
+                               String encrypted = crypt.encrypt64(orig);
+                               String decrypted = crypt.decrypt64s(encrypted);
+
+                               assertEquals(orig, decrypted);
+                       }
+               });
+
+               addTest(new TestCase("Empty") {
+                       @Override
+                       public void test() throws Exception {
+                               String orig = "";
+                               byte[] encrypted = crypt.encrypt(orig);
+                               String decrypted = crypt.decrypts(encrypted);
+
+                               assertEquals(orig, decrypted);
+                       }
+               });
+
+               addTest(new TestCase("Empty, base64") {
+                       @Override
+                       public void test() throws Exception {
+                               String orig = "";
+                               String encrypted = crypt.encrypt64(orig);
+                               String decrypted = crypt.decrypt64s(encrypted);
+
+                               assertEquals(orig, decrypted);
+                       }
+               });
+
+               addTest(new TestCase("Long") {
+                       @Override
+                       public void test() throws Exception {
+                               String orig = longData;
+                               byte[] encrypted = crypt.encrypt(orig);
+                               String decrypted = crypt.decrypts(encrypted);
+
+                               assertEquals(orig, decrypted);
+                       }
+               });
+
+               addTest(new TestCase("Long, base64") {
+                       @Override
+                       public void test() throws Exception {
+                               String orig = longData;
+                               String encrypted = crypt.encrypt64(orig);
+                               String decrypted = crypt.decrypt64s(encrypted);
+
+                               assertEquals(orig, decrypted);
+                       }
+               });
+       }
+
+       private CryptUtilsTest(String[] args, String title, String key,
+                       @SuppressWarnings("unused") boolean dummy) {
+               super(title, args);
+               this.key = key;
+
+               addTest(new TestCase("Simple test") {
+                       @Override
+                       public void test() throws Exception {
+                               InputStream in = new ByteArrayInputStream(new byte[] { 42, 127,
+                                               12 });
+                               crypt.encrypt(in);
+                               ByteArrayOutputStream out = new ByteArrayOutputStream();
+                               IOUtils.write(in, out);
+                               byte[] result = out.toByteArray();
+
+                               assertEquals(
+                                               "We wrote 3 bytes, we expected 3 bytes back but got: "
+                                                               + result.length, result.length, result.length);
+
+                               assertEquals(42, result[0]);
+                               assertEquals(127, result[1]);
+                               assertEquals(12, result[2]);
+                       }
+               });
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/IOUtilsTest.java b/src/be/nikiroo/tests/utils/IOUtilsTest.java
new file mode 100644 (file)
index 0000000..71acf1c
--- /dev/null
@@ -0,0 +1,24 @@
+package be.nikiroo.tests.utils;
+
+import java.io.InputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class IOUtilsTest extends TestLauncher {
+       public IOUtilsTest(String[] args) {
+               super("IOUtils test", args);
+
+               addTest(new TestCase("openResource") {
+                       @Override
+                       public void test() throws Exception {
+                               InputStream in = IOUtils.openResource("VERSION");
+                               assertNotNull(
+                                               "The VERSION file is supposed to be present in the binaries",
+                                               in);
+                               in.close();
+                       }
+               });
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/NextableInputStreamTest.java b/src/be/nikiroo/tests/utils/NextableInputStreamTest.java
new file mode 100644 (file)
index 0000000..3dc6fd1
--- /dev/null
@@ -0,0 +1,340 @@
+package be.nikiroo.tests.utils;
+
+import java.io.ByteArrayInputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+public class NextableInputStreamTest extends TestLauncher {
+       public NextableInputStreamTest(String[] args) {
+               super("NextableInputStream test", args);
+
+               addTest(new TestCase("Simple byte array reading") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] expected = new byte[] { 42, 12, 0, 127 };
+                               NextableInputStream in = new NextableInputStream(
+                                               new ByteArrayInputStream(expected), null);
+                               checkNext(this, "READ", in, expected);
+                       }
+               });
+
+               addTest(new TestCase("Stop at 12") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] expected = new byte[] { 42, 12, 0, 127 };
+                               NextableInputStream in = new NextableInputStream(
+                                               new ByteArrayInputStream(expected),
+                                               new NextableInputStreamStep(12));
+
+                               checkNext(this, "FIRST", in, new byte[] { 42 });
+                       }
+               });
+
+               addTest(new TestCase("Stop at 12, resume, stop again, resume") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+                               NextableInputStream in = new NextableInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new NextableInputStreamStep(12));
+
+                               checkNext(this, "FIRST", in, new byte[] { 42 });
+                               checkNext(this, "SECOND", in, new byte[] { 0, 127 });
+                               checkNext(this, "THIRD", in, new byte[] { 51, 11 });
+                       }
+               });
+
+               addTest(new TestCase("Encapsulation") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 4, 127, 12, 5 };
+                               NextableInputStream in4 = new NextableInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new NextableInputStreamStep(4));
+                               NextableInputStream subIn12 = new NextableInputStream(in4,
+                                               new NextableInputStreamStep(12));
+
+                               in4.next();
+                               checkNext(this, "SUB FIRST", subIn12, new byte[] { 42 });
+                               checkNext(this, "SUB SECOND", subIn12, new byte[] { 0 });
+
+                               assertEquals("The subIn still has some data", false,
+                                               subIn12.next());
+
+                               checkNext(this, "MAIN LAST", in4, new byte[] { 127, 12, 5 });
+                       }
+               });
+
+               addTest(new TestCase("UTF-8 text lines test") {
+                       @Override
+                       public void test() throws Exception {
+                               String ln1 = "Ligne première";
+                               String ln2 = "Ligne la deuxième du nom";
+                               byte[] data = (ln1 + "\n" + ln2).getBytes("UTF-8");
+                               NextableInputStream in = new NextableInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new NextableInputStreamStep('\n'));
+
+                               checkNext(this, "FIRST", in, ln1.getBytes("UTF-8"));
+                               checkNext(this, "SECOND", in, ln2.getBytes("UTF-8"));
+                       }
+               });
+
+               addTest(new TestCase("nextAll()") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+                               NextableInputStream in = new NextableInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new NextableInputStreamStep(12));
+
+                               checkNext(this, "FIRST", in, new byte[] { 42 });
+                               checkNextAll(this, "REST", in, new byte[] { 0, 127, 12, 51, 11,
+                                               12 });
+                               assertEquals("The stream still has some data", false, in.next());
+                       }
+               });
+
+               addTest(new TestCase("getBytesRead()") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+                               NextableInputStream in = new NextableInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new NextableInputStreamStep(12));
+
+                               in.nextAll();
+                               IOUtils.toByteArray(in);
+
+                               assertEquals("The number of bytes read is not correct",
+                                               data.length, in.getBytesRead());
+                       }
+               });
+
+               addTest(new TestCase("bytes array input") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+                               NextableInputStream in = new NextableInputStream(data,
+                                               new NextableInputStreamStep(12));
+
+                               checkNext(this, "FIRST", in, new byte[] { 42 });
+                               checkNext(this, "SECOND", in, new byte[] { 0, 127 });
+                               checkNext(this, "THIRD", in, new byte[] { 51, 11 });
+                       }
+               });
+
+               addTest(new TestCase("Skip data") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+                               NextableInputStream in = new NextableInputStream(data, null);
+                               in.next();
+
+                               byte[] rest = new byte[] { 12, 51, 11, 12 };
+
+                               in.skip(4);
+                               assertEquals("STARTS_WITH OK_1", true, in.startsWith(rest));
+                               assertEquals("STARTS_WITH KO_1", false,
+                                               in.startsWith(new byte[] { 0 }));
+                               assertEquals("STARTS_WITH KO_2", false, in.startsWith(data));
+                               assertEquals("STARTS_WITH KO_3", false,
+                                               in.startsWith(new byte[] { 1, 2, 3 }));
+                               assertEquals("STARTS_WITH OK_2", true, in.startsWith(rest));
+                               assertEquals("READ REST", IOUtils.readSmallStream(in),
+                                               new String(rest));
+                               in.close();
+                       }
+               });
+
+               addTest(new TestCase("Starts with") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+                               NextableInputStream in = new NextableInputStream(data, null);
+                               in.next();
+
+                               // yes
+                               assertEquals("It actually starts with that", true,
+                                               in.startsWith(new byte[] { 42 }));
+                               assertEquals("It actually starts with that", true,
+                                               in.startsWith(new byte[] { 42, 12 }));
+                               assertEquals("It actually is the same array", true,
+                                               in.startsWith(data));
+
+                               // no
+                               assertEquals("It actually does not start with that", false,
+                                               in.startsWith(new byte[] { 12 }));
+                               assertEquals(
+                                               "It actually does not start with that",
+                                               false,
+                                               in.startsWith(new byte[] { 42, 12, 0, 127, 12, 51, 11,
+                                                               11 }));
+
+                               // too big
+                               assertEquals(
+                                               "A search term bigger than the whole data cannot be found in the data",
+                                               false, in.startsWith(new byte[] { 42, 12, 0, 127, 12,
+                                                               51, 11, 12, 0 }));
+
+                               in.close();
+                       }
+               });
+
+               addTest(new TestCase("Starts with strings") {
+                       @Override
+                       public void test() throws Exception {
+                               String text = "Fanfan et Toto vont à la mer";
+                               byte[] data = text.getBytes("UTF-8");
+                               NextableInputStream in = new NextableInputStream(data, null);
+                               in.next();
+
+                               // yes
+                               assertEquals("It actually starts with that", true,
+                                               in.startsWith("F"));
+                               assertEquals("It actually starts with that", true,
+                                               in.startsWith("Fanfan et"));
+                               assertEquals("It actually is the same text", true,
+                                               in.startsWith(text));
+
+                               // no
+                               assertEquals("It actually does not start with that", false,
+                                               in.startsWith("Toto"));
+                               assertEquals("It actually does not start with that", false,
+                                               in.startsWith("Fanfan et Toto vont à la mee"));
+                               
+                               // too big
+                               assertEquals(
+                                               "A search term bigger than the whole data cannot be found in the data",
+                                               false, in.startsWith("Fanfan et Toto vont à la mer."));
+
+                               in.close();
+                       }
+               });
+
+               addTest(new TestCase("Starts With strings + steps") {
+                       @Override
+                       public void test() throws Exception {
+                               String data = "{\nREF: fanfan\n}";
+                               NextableInputStream in = new NextableInputStream(
+                                               data.getBytes("UTF-8"), new NextableInputStreamStep(
+                                                               '\n'));
+                               in.next();
+
+                               assertEquals("STARTS_WITH OK", true, in.startsWith("{"));
+                               in.skip(1);
+                               assertEquals("STARTS_WITH WHEN SPENT", false,
+                                               in.startsWith("{"));
+
+                               checkNext(this, "PARTIAL CONTENT", in,
+                                               "REF: fanfan".getBytes("UTF-8"));
+                       }
+               });
+
+               addTest(new TestCase("InputStream is(String)") {
+                       @Override
+                       public void test() throws Exception {
+                               String data = "{\nREF: fanfan\n}";
+                               NextableInputStream in = new NextableInputStream(
+                                               new ByteArrayInputStream(data.getBytes("UTF-8")),
+                                               new NextableInputStreamStep('\n'));
+
+                               in.next();
+                               assertEquals("Item 1 OK", true, in.is("{"));
+                               assertEquals("Item 1 KO_1", false, in.is("|"));
+                               assertEquals("Item 1 KO_2", false, in.is("{}"));
+                               in.skip(1);
+                               in.next();
+                               assertEquals("Item 2 OK", true, in.is("REF: fanfan"));
+                               assertEquals("Item 2 KO", false, in.is("REF: fanfan."));
+                               IOUtils.readSmallStream(in);
+                               in.next();
+                               assertEquals("Item 3 OK", true, in.is("}"));
+
+                               in.close();
+                       }
+               });
+
+               addTest(new TestCase("Bytes NextAll test") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+                               NextableInputStream in = new NextableInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new NextableInputStreamStep(12));
+
+                               checkNext(this, "FIRST", in, new byte[] { 42 });
+                               checkNextAll(this, "SECOND", in, new byte[] { 0, 127, 12, 51,
+                                               11, 12 });
+                       }
+               });
+
+               addTest(new TestCase("String NextAll test") {
+                       @Override
+                       public void test() throws Exception {
+                               String d1 = "^java.lang.String";
+                               String d2 = "\"http://example.com/query.html\"";
+                               String data = d1 + ":" + d2;
+                               NextableInputStream in = new NextableInputStream(
+                                               new ByteArrayInputStream(data.getBytes("UTF-8")),
+                                               new NextableInputStreamStep(':'));
+
+                               checkNext(this, "FIRST", in, d1.getBytes("UTF-8"));
+                               checkNextAll(this, "SECOND", in, d2.getBytes("UTF-8"));
+                       }
+               });
+
+               addTest(new TestCase("NextAll in Next test") {
+                       @Override
+                       public void test() throws Exception {
+                               String line1 = "première ligne";
+                               String d1 = "^java.lang.String";
+                               String d2 = "\"http://example.com/query.html\"";
+                               String line3 = "end of lines";
+                               String data = line1 + "\n" + d1 + ":" + d2 + "\n" + line3;
+
+                               NextableInputStream inL = new NextableInputStream(
+                                               new ByteArrayInputStream(data.getBytes("UTF-8")),
+                                               new NextableInputStreamStep('\n'));
+
+                               checkNext(this, "Line 1", inL, line1.getBytes("UTF-8"));
+                               inL.next();
+
+                               NextableInputStream in = new NextableInputStream(inL,
+                                               new NextableInputStreamStep(':'));
+
+                               checkNext(this, "Line 2 FIRST", in, d1.getBytes("UTF-8"));
+                               checkNextAll(this, "Line 2 SECOND", in, d2.getBytes("UTF-8"));
+                       }
+               });
+       }
+
+       static void checkNext(TestCase test, String prefix, NextableInputStream in,
+                       byte[] expected) throws Exception {
+               test.assertEquals("Cannot get " + prefix + " entry", true, in.next());
+               checkArrays(test, prefix, in, expected);
+       }
+
+       static void checkNextAll(TestCase test, String prefix,
+                       NextableInputStream in, byte[] expected) throws Exception {
+               test.assertEquals("Cannot get " + prefix + " entries", true,
+                               in.nextAll());
+               checkArrays(test, prefix, in, expected);
+       }
+
+       static void checkArrays(TestCase test, String prefix,
+                       NextableInputStream in, byte[] expected) throws Exception {
+               byte[] actual = IOUtils.toByteArray(in);
+               test.assertEquals("The " + prefix
+                               + " resulting array has not the correct number of items",
+                               expected.length, actual.length);
+               for (int i = 0; i < actual.length; i++) {
+                       test.assertEquals("Item " + i + " (0-based) is not the same",
+                                       expected[i], actual[i]);
+               }
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/ProgressTest.java b/src/be/nikiroo/tests/utils/ProgressTest.java
new file mode 100644 (file)
index 0000000..c829e70
--- /dev/null
@@ -0,0 +1,319 @@
+package be.nikiroo.tests.utils;
+
+import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class ProgressTest extends TestLauncher {
+       public ProgressTest(String[] args) {
+               super("Progress reporting", args);
+
+               addSeries(new TestLauncher("Simple progress", args) {
+                       {
+                               addTest(new TestCase("Relative values and direct values") {
+                                       @Override
+                                       public void test() throws Exception {
+                                               Progress p = new Progress();
+                                               assertEquals(0, p.getProgress());
+                                               assertEquals(0, p.getRelativeProgress());
+                                               p.setProgress(33);
+                                               assertEquals(33, p.getProgress());
+                                               assertEquals(0.33, p.getRelativeProgress());
+                                               p.setMax(3);
+                                               p.setProgress(1);
+                                               assertEquals(1, p.getProgress());
+                                               assertEquals(
+                                                               generateAssertMessage("0.33..",
+                                                                               p.getRelativeProgress()), true,
+                                                               p.getRelativeProgress() >= 0.332);
+                                               assertEquals(
+                                                               generateAssertMessage("0.33..",
+                                                                               p.getRelativeProgress()), true,
+                                                               p.getRelativeProgress() <= 0.334);
+                                       }
+                               });
+
+                               addTest(new TestCase("Listeners at first level") {
+                                       int pg;
+
+                                       @Override
+                                       public void test() throws Exception {
+                                               Progress p = new Progress();
+                                               p.addProgressListener(new Progress.ProgressListener() {
+                                                       @Override
+                                                       public void progress(Progress progress, String name) {
+                                                               pg = progress.getProgress();
+                                                       }
+                                               });
+
+                                               p.setProgress(42);
+                                               assertEquals(42, pg);
+                                               p.setProgress(0);
+                                               assertEquals(0, pg);
+                                       }
+                               });
+                       }
+               });
+
+               addSeries(new TestLauncher("Progress with children", args) {
+                       {
+                               addTest(new TestCase("One child") {
+                                       @Override
+                                       public void test() throws Exception {
+                                               Progress p = new Progress();
+                                               Progress child = new Progress();
+
+                                               p.addProgress(child, 100);
+
+                                               child.setProgress(42);
+                                               assertEquals(42, p.getProgress());
+                                       }
+                               });
+
+                               addTest(new TestCase("Multiple children") {
+                                       @Override
+                                       public void test() throws Exception {
+                                               Progress p = new Progress();
+                                               Progress child1 = new Progress();
+                                               Progress child2 = new Progress();
+                                               Progress child3 = new Progress();
+
+                                               p.addProgress(child1, 20);
+                                               p.addProgress(child2, 60);
+                                               p.addProgress(child3, 20);
+
+                                               child1.setProgress(50);
+                                               assertEquals(10, p.getProgress());
+                                               child2.setProgress(100);
+                                               assertEquals(70, p.getProgress());
+                                               child3.setProgress(100);
+                                               assertEquals(90, p.getProgress());
+                                               child1.setProgress(100);
+                                               assertEquals(100, p.getProgress());
+                                       }
+                               });
+
+                               addTest(new TestCase("Listeners with children") {
+                                       int pg;
+
+                                       @Override
+                                       public void test() throws Exception {
+                                               final Progress p = new Progress();
+                                               Progress child1 = new Progress();
+                                               Progress child2 = new Progress();
+                                               p.addProgress(child1, 50);
+                                               p.addProgress(child2, 50);
+
+                                               p.addProgressListener(new Progress.ProgressListener() {
+                                                       @Override
+                                                       public void progress(Progress progress, String name) {
+                                                               pg = p.getProgress();
+                                                       }
+                                               });
+
+                                               child1.setProgress(50);
+                                               assertEquals(25, pg);
+                                               child2.setProgress(100);
+                                               assertEquals(75, pg);
+                                               child1.setProgress(100);
+                                               assertEquals(100, pg);
+                                       }
+                               });
+
+                               addTest(new TestCase("Listeners with children, not 1-100") {
+                                       int pg;
+
+                                       @Override
+                                       public void test() throws Exception {
+                                               final Progress p = new Progress();
+                                               p.setMax(1000);
+
+                                               Progress child1 = new Progress();
+                                               child1.setMax(2);
+
+                                               Progress child2 = new Progress();
+                                               p.addProgress(child1, 500);
+                                               p.addProgress(child2, 500);
+
+                                               p.addProgressListener(new Progress.ProgressListener() {
+                                                       @Override
+                                                       public void progress(Progress progress, String name) {
+                                                               pg = p.getProgress();
+                                                       }
+                                               });
+
+                                               child1.setProgress(1);
+                                               assertEquals(250, pg);
+                                               child2.setProgress(100);
+                                               assertEquals(750, pg);
+                                               child1.setProgress(2);
+                                               assertEquals(1000, pg);
+                                       }
+                               });
+
+                               addTest(new TestCase(
+                                               "Listeners with children, not 1-100, local progress") {
+                                       int pg;
+
+                                       @Override
+                                       public void test() throws Exception {
+                                               final Progress p = new Progress();
+                                               p.setMax(1000);
+
+                                               Progress child1 = new Progress();
+                                               child1.setMax(2);
+
+                                               Progress child2 = new Progress();
+                                               p.addProgress(child1, 400);
+                                               p.addProgress(child2, 400);
+                                               // 200 = local progress
+
+                                               p.addProgressListener(new Progress.ProgressListener() {
+                                                       @Override
+                                                       public void progress(Progress progress, String name) {
+                                                               pg = p.getProgress();
+                                                       }
+                                               });
+
+                                               child1.setProgress(1);
+                                               assertEquals(200, pg);
+                                               child2.setProgress(100);
+                                               assertEquals(600, pg);
+                                               p.setProgress(100);
+                                               assertEquals(700, pg);
+                                               child1.setProgress(2);
+                                               assertEquals(900, pg);
+                                               p.setProgress(200);
+                                               assertEquals(1000, pg);
+                                       }
+                               });
+
+                               addTest(new TestCase("Listeners with 5+ children, 4+ depth") {
+                                       int pg;
+
+                                       @Override
+                                       public void test() throws Exception {
+                                               final Progress p = new Progress();
+                                               Progress child1 = new Progress();
+                                               Progress child2 = new Progress();
+                                               p.addProgress(child1, 50);
+                                               p.addProgress(child2, 50);
+                                               Progress child11 = new Progress();
+                                               child1.addProgress(child11, 100);
+                                               Progress child111 = new Progress();
+                                               child11.addProgress(child111, 100);
+                                               Progress child1111 = new Progress();
+                                               child111.addProgress(child1111, 20);
+                                               Progress child1112 = new Progress();
+                                               child111.addProgress(child1112, 20);
+                                               Progress child1113 = new Progress();
+                                               child111.addProgress(child1113, 20);
+                                               Progress child1114 = new Progress();
+                                               child111.addProgress(child1114, 20);
+                                               Progress child1115 = new Progress();
+                                               child111.addProgress(child1115, 20);
+
+                                               p.addProgressListener(new Progress.ProgressListener() {
+                                                       @Override
+                                                       public void progress(Progress progress, String name) {
+                                                               pg = p.getProgress();
+                                                       }
+                                               });
+
+                                               child1111.setProgress(100);
+                                               child1112.setProgress(50);
+                                               child1113.setProgress(25);
+                                               child1114.setProgress(25);
+                                               child1115.setProgress(50);
+                                               assertEquals(25, pg);
+                                               child2.setProgress(100);
+                                               assertEquals(75, pg);
+                                               child1111.setProgress(100);
+                                               child1112.setProgress(100);
+                                               child1113.setProgress(100);
+                                               child1114.setProgress(100);
+                                               child1115.setProgress(100);
+                                               assertEquals(100, pg);
+                                       }
+                               });
+
+                               addTest(new TestCase("Listeners with children, multi-thread") {
+                                       int pg;
+                                       boolean decrease;
+                                       Object lock1 = new Object();
+                                       Object lock2 = new Object();
+                                       int currentStep1;
+                                       int currentStep2;
+
+                                       @Override
+                                       public void test() throws Exception {
+                                               final Progress p = new Progress(0, 200);
+
+                                               final Progress child1 = new Progress();
+                                               final Progress child2 = new Progress();
+                                               p.addProgress(child1, 100);
+                                               p.addProgress(child2, 100);
+
+                                               p.addProgressListener(new Progress.ProgressListener() {
+                                                       @Override
+                                                       public void progress(Progress progress, String name) {
+                                                               int now = p.getProgress();
+                                                               if (now < pg) {
+                                                                       decrease = true;
+                                                               }
+                                                               pg = now;
+                                                       }
+                                               });
+
+                                               // Run 200 concurrent threads, 2 at a time allowed to
+                                               // make progress (each on a different child)
+                                               for (int i = 0; i <= 100; i++) {
+                                                       final int step = i;
+                                                       new Thread(new Runnable() {
+                                                               @Override
+                                                               public void run() {
+                                                                       synchronized (lock1) {
+                                                                               if (step > currentStep1) {
+                                                                                       currentStep1 = step;
+                                                                                       child1.setProgress(step);
+                                                                               }
+                                                                       }
+                                                               }
+                                                       }).start();
+
+                                                       new Thread(new Runnable() {
+                                                               @Override
+                                                               public void run() {
+                                                                       synchronized (lock2) {
+                                                                               if (step > currentStep2) {
+                                                                                       currentStep2 = step;
+                                                                                       child2.setProgress(step);
+                                                                               }
+                                                                       }
+                                                               }
+                                                       }).start();
+                                               }
+
+                                               int i;
+                                               int timeout = 20; // in 1/10th of seconds
+                                               for (i = 0; i < timeout
+                                                               && (currentStep1 + currentStep2) < 200; i++) {
+                                                       Thread.sleep(100);
+                                               }
+
+                                               assertEquals("The test froze at step " + currentStep1
+                                                               + " + " + currentStep2, true, i < timeout);
+                                               assertEquals(
+                                                               "There should not have any decresing steps",
+                                                               decrease, false);
+                                               assertEquals("The progress should have reached 200",
+                                                               200, p.getProgress());
+                                               assertEquals(
+                                                               "The progress should have reached completion",
+                                                               true, p.isDone());
+                                       }
+                               });
+                       }
+               });
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/ReplaceInputStreamTest.java b/src/be/nikiroo/tests/utils/ReplaceInputStreamTest.java
new file mode 100644 (file)
index 0000000..08213e1
--- /dev/null
@@ -0,0 +1,210 @@
+package be.nikiroo.tests.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.ReplaceInputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class ReplaceInputStreamTest extends TestLauncher {
+       public ReplaceInputStreamTest(String[] args) {
+               super("ReplaceInputStream test", args);
+
+               addTest(new TestCase("Empty replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data), new byte[0],
+                                               new byte[0]);
+
+                               checkArrays(this, "FIRST", in, data);
+                       }
+               });
+
+               addTest(new TestCase("Simple replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data), new byte[] { 0 },
+                                               new byte[] { 10 });
+
+                               checkArrays(this, "FIRST", in, new byte[] { 42, 12, 10, 127 });
+                       }
+               });
+
+               addTest(new TestCase("3/4 replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new byte[] { 12, 0, 127 }, new byte[] { 10, 10, 10 });
+
+                               checkArrays(this, "FIRST", in, new byte[] { 42, 10, 10, 10 });
+                       }
+               });
+
+               addTest(new TestCase("Longer replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data), new byte[] { 0 },
+                                               new byte[] { 10, 10, 10 });
+
+                               checkArrays(this, "FIRST", in, new byte[] { 42, 12, 10, 10, 10,
+                                               127 });
+                       }
+               });
+
+               addTest(new TestCase("Shorter replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new byte[] { 42, 12, 0 }, new byte[] { 10 });
+
+                               checkArrays(this, "FIRST", in, new byte[] { 10, 127 });
+                       }
+               });
+
+               addTest(new TestCase("String replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = "I like red".getBytes("UTF-8");
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data),
+                                               "red", "blue");
+
+                               checkArrays(this, "FIRST", in, "I like blue".getBytes("UTF-8"));
+
+                               data = "I like blue hammers".getBytes("UTF-8");
+                               in = new ReplaceInputStream(new ByteArrayInputStream(data),
+                                               "blue", "red");
+
+                               checkArrays(this, "SECOND", in, "I like red hammers".getBytes("UTF-8"));
+                       }
+               });
+               
+               addTest(new TestCase("Multiple replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = "I like red cabage".getBytes("UTF-8");
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data), //
+                                               new String[] { "red", "like" }, //
+                                               new String[] { "green", "very very much like" } //
+                               );
+                               
+                               String result = new String(IOUtils.toByteArray(in), "UTF-8");
+                               assertEquals("I very very much like green cabage", result);
+                       }
+               });
+               
+               addTest(new TestCase("Multiple replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               String str= ("" //
+                                               + "<!DOCTYPE html>\n" //
+                                               + "<html>\n" //
+                                               + "<head>\n" //
+                                               + "<!--\n" //
+                                               + "\tCopyright 2020 David ROULET\n" //
+                                               + "\t\n" //
+                                               + "\tThis file is part of fanfix.\n" //
+                                               + "\t\n" //
+                                               + "\tfanfix is free software: you can redistribute it and/or modify\n" //
+                                               + "\tit under the terms of the GNU Affero General Public License as published by\n" //
+                                               + "\tthe Free Software Foundation, either version 3 of the License, or\n" //
+                                               + "\t(at your option) any later version.\n" //
+                                               + "\t\n" //
+                                               + "\tfanfix is distributed in the hope that it will be useful,\n" //
+                                               + "\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n" //
+                                               + "\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n" //
+                                               + "\tGNU Affero General Public License for more details.\n" //
+                                               + "\t\n" //
+                                               + "\tYou should have received a copy of the GNU Affero General Public License\n" //
+                                               + "\talong with fanfix.  If not, see <https://www.gnu.org/licenses/>.\n" //
+                                               + "\t___________________________________________________________________________\n" //
+                                               + "\n" //
+                                               + "       This website was coded by:\n" //
+                                               + "       \t\tA kangaroo.\n" //
+                                               + "                                                  _  _\n" //
+                                               + "                                                 (\\\\( \\\n" //
+                                               + "                                                  `.\\-.)\n" //
+                                               + "                              _...._            _,-'   `-.\n" //
+                                               + "\\                           ,'      `-._.- -.,-'       .  \\\n" //
+                                               + " \\`.                      ,'                               `.\n" //
+                                               + "  \\ `-...__              /                           .   .:  y\n" //
+                                               + "   `._     ``-...__     /                           ,'```-._/\n" //
+                                               + "      `-._         ```-'                      |    /_          //\n" //
+                                               + "          `.._                   _            ;   <_ \\        //\n" //
+                                               + "              ``-.___             `.           `-._ \\ \\      //\n" //
+                                               + "                     `- <           `.     (\\ _/)/ `.\\/     //\n" //
+                                               + "                         \\            \\     `       ^^^^^^^^^\n" //
+                                               + "\t___________________________________________________________________________\n" //
+                                               + "\t\n" //
+                                               + "-->\n" //
+                                               + "\t<meta http-equiv='content-type' content='text/html; charset=UTF-8'>\n" //
+                                               + "\t<meta name='viewport' content='width=device-width, initial-scale=1.0'>\n" //
+                                               + "\t<title>${title}</title>\n" //
+                                               + "\t<link rel='stylesheet' type='text/css' href='/style.css' />\n" //
+                                               + "\t<link rel='icon' type='image/x-icon' href='/${favicon}' />\n" //
+                                               + "</head>\n" //
+                                               + "<body>\n" //
+                                               + "\t<div class='main'>\n" //
+                                               + "${banner}${content}\t</div>\n" //
+                                               + "</body>\n" //
+                                               + "" //
+                               );
+                               byte[] data = str.getBytes("UTF-8");
+
+                               String title = "Fanfix";
+                               String banner = "<div class='banner'>Super banner v3</div>";
+                               String content = "";
+
+                               InputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data), //
+                                               new String[] { "${title}", "${banner}", "${content}" }, //
+                                               new String[] { title, banner, content } //
+                               );
+
+                               String result = new String(IOUtils.toByteArray(in), "UTF-8");
+                               assertEquals(str //
+                                               .replace("${title}", title) //
+                                               .replace("${banner}", banner) //
+                                               .replace("${content}", content) //
+                               , result);
+                       }
+               });
+               
+               
+       }
+
+       static void checkArrays(TestCase test, String prefix, InputStream in,
+                       byte[] expected) throws Exception {
+               byte[] actual = IOUtils.toByteArray(in);
+               
+//             System.out.println("\nActual:");
+//             for(byte byt : actual) {
+//                     System.out.print(byt+" ");
+//             }
+//             System.out.println("\nExpected:");
+//             for(byte byt : expected) {
+//                     System.out.print(byt+" ");
+//             }
+               
+               test.assertEquals("The " + prefix
+                               + " resulting array has not the correct number of items",
+                               expected.length, actual.length);
+               for (int i = 0; i < actual.length; i++) {
+                       test.assertEquals("Item " + i + " (0-based) is not the same",
+                                       expected[i], actual[i]);
+               }
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/ReplaceOutputStreamTest.java b/src/be/nikiroo/tests/utils/ReplaceOutputStreamTest.java
new file mode 100644 (file)
index 0000000..4453320
--- /dev/null
@@ -0,0 +1,168 @@
+package be.nikiroo.tests.utils;
+
+import java.io.ByteArrayOutputStream;
+
+import be.nikiroo.utils.streams.ReplaceOutputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class ReplaceOutputStreamTest extends TestLauncher {
+       public ReplaceOutputStreamTest(String[] args) {
+               super("ReplaceOutputStream test", args);
+
+               addTest(new TestCase("Single write, empty bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[0], new byte[0]);
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, data);
+                       }
+               });
+
+               addTest(new TestCase("Multiple writes, empty Strings replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout, "", "");
+
+                               byte[] data1 = new byte[] { 42, 12, 0, 127 };
+                               byte[] data2 = new byte[] { 15, 55 };
+                               byte[] data3 = new byte[] {};
+
+                               byte[] dataAll = new byte[] { 42, 12, 0, 127, 15, 55 };
+
+                               out.write(data1);
+                               out.write(data2);
+                               out.write(data3);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, dataAll);
+                       }
+               });
+
+               addTest(new TestCase("Single write, bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[] { 12 }, new byte[] { 55 });
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, new byte[] { 42, 55, 0, 127 });
+                       }
+               });
+
+               addTest(new TestCase("Multiple writes, Strings replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout, "(-)",
+                                               "(.)");
+
+                               byte[] data1 = "un mot ".getBytes("UTF-8");
+                               byte[] data2 = "(-) of twee ".getBytes("UTF-8");
+                               byte[] data3 = "(-) makes the difference".getBytes("UTF-8");
+
+                               out.write(data1);
+                               out.write(data2);
+                               out.write(data3);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout,
+                                               "un mot (.) of twee (.) makes the difference"
+                                                               .getBytes("UTF-8"));
+                       }
+               });
+
+               addTest(new TestCase("Single write, longer bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[] { 12 }, new byte[] { 55, 55, 66 });
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, new byte[] { 42, 55, 55, 66,
+                                               0, 127 });
+                       }
+               });
+
+               addTest(new TestCase("Single write, shorter bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[] { 12, 0 }, new byte[] { 55 });
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, new byte[] { 42, 55, 127 });
+                       }
+               });
+
+               addTest(new TestCase("Single write, remove bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[] { 12 }, new byte[] {});
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, new byte[] { 42, 0, 127 });
+                       }
+               });
+       }
+
+       static void checkArrays(TestCase test, String prefix,
+                       ByteArrayOutputStream bout, byte[] expected) throws Exception {
+               byte[] actual = bout.toByteArray();
+
+               if (false) {
+                       System.out.print("\nExpected data: [ ");
+                       for (int i = 0; i < expected.length; i++) {
+                               if (i > 0)
+                                       System.out.print(", ");
+                               System.out.print(expected[i]);
+                       }
+                       System.out.println(" ]");
+
+                       System.out.print("Actual data  : [ ");
+                       for (int i = 0; i < actual.length; i++) {
+                               if (i > 0)
+                                       System.out.print(", ");
+                               System.out.print(actual[i]);
+                       }
+                       System.out.println(" ]");
+               }
+
+               test.assertEquals("The " + prefix
+                               + " resulting array has not the correct number of items",
+                               expected.length, actual.length);
+               for (int i = 0; i < actual.length; i++) {
+                       test.assertEquals(prefix + ": item " + i
+                                       + " (0-based) is not the same", expected[i], actual[i]);
+               }
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/SerialServerTest.java b/src/be/nikiroo/tests/utils/SerialServerTest.java
new file mode 100644 (file)
index 0000000..61d01a4
--- /dev/null
@@ -0,0 +1,637 @@
+package be.nikiroo.tests.utils;
+
+import java.net.URL;
+
+import be.nikiroo.utils.Version;
+import be.nikiroo.utils.serial.server.ConnectActionClientObject;
+import be.nikiroo.utils.serial.server.ConnectActionClientString;
+import be.nikiroo.utils.serial.server.ConnectActionServerObject;
+import be.nikiroo.utils.serial.server.ConnectActionServerString;
+import be.nikiroo.utils.serial.server.ServerBridge;
+import be.nikiroo.utils.serial.server.ServerObject;
+import be.nikiroo.utils.serial.server.ServerString;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class SerialServerTest extends TestLauncher {
+       public SerialServerTest(String[] args) {
+               super("SerialServer test", args);
+
+               for (String key : new String[] { null,
+                               "some super secret encryption key" }) {
+                       for (boolean bridge : new Boolean[] { false, true }) {
+                               final String skey = (key != null ? "(encrypted)"
+                                               : "(plain text)");
+                               final String sbridge = (bridge ? " with bridge" : "");
+
+                               addSeries(new SerialServerTest(args, key, skey, bridge,
+                                               sbridge, "ServerString"));
+
+                               addSeries(new SerialServerTest(args, key, skey, bridge,
+                                               sbridge, new Object() {
+                                                       @Override
+                                                       public String toString() {
+                                                               return "ServerObject";
+                                                       }
+                                               }));
+                       }
+               }
+       }
+
+       private SerialServerTest(final String[] args, final String key,
+                       final String skey, final boolean bridge, final String sbridge,
+                       final String title) {
+
+               super(title + " " + skey + sbridge, args);
+
+               addTest(new TestCase("Simple connection " + skey) {
+                       @Override
+                       public void test() throws Exception {
+                               final String[] rec = new String[1];
+
+                               ServerString server = new ServerString(this.getName(), 0, key) {
+                                       @Override
+                                       protected String onRequest(
+                                                       ConnectActionServerString action, Version version,
+                                                       String data, long id) throws Exception {
+                                               return null;
+                                       }
+
+                                       @Override
+                                       protected void onError(Exception e) {
+                                       }
+                               };
+
+                               int port = server.getPort();
+                               assertEquals("A port should have been assigned", true, port > 0);
+
+                               server.start();
+
+                               ServerBridge br = null;
+                               if (bridge) {
+                                       br = new ServerBridge(0, key, "", port, key);
+                                       br.setTraceHandler(null);
+
+                                       port = br.getPort();
+                                       assertEquals(
+                                                       "A port should have been assigned to the bridge",
+                                                       true, port > 0);
+
+                                       br.start();
+                               }
+
+                               try {
+                                       try {
+                                               new ConnectActionClientString(null, port, key) {
+                                                       @Override
+                                                       public void action(Version version)
+                                                                       throws Exception {
+                                                               rec[0] = "ok";
+                                                       }
+                                               }.connect();
+                                       } finally {
+                                               server.stop();
+                                       }
+                               } finally {
+                                       if (br != null) {
+                                               br.stop();
+                                       }
+                               }
+
+                               assertNotNull("The client action was not run", rec[0]);
+                               assertEquals("ok", rec[0]);
+                       }
+               });
+
+               addTest(new TestCase("Simple exchange " + skey) {
+                       final String[] sent = new String[1];
+                       final String[] recd = new String[1];
+                       final Exception[] err = new Exception[1];
+
+                       @Override
+                       public void test() throws Exception {
+                               ServerString server = new ServerString(this.getName(), 0, key) {
+                                       @Override
+                                       protected String onRequest(
+                                                       ConnectActionServerString action, Version version,
+                                                       String data, long id) throws Exception {
+                                               sent[0] = data;
+                                               return "pong";
+                                       }
+
+                                       @Override
+                                       protected void onError(Exception e) {
+                                               err[0] = e;
+                                       }
+                               };
+
+                               int port = server.getPort();
+
+                               server.start();
+
+                               ServerBridge br = null;
+                               if (bridge) {
+                                       br = new ServerBridge(0, key, "", port, key);
+                                       br.setTraceHandler(null);
+                                       port = br.getPort();
+                                       br.start();
+                               }
+
+                               try {
+                                       try {
+                                               new ConnectActionClientString(null, port, key) {
+                                                       @Override
+                                                       public void action(Version version)
+                                                                       throws Exception {
+                                                               recd[0] = send("ping");
+                                                       }
+                                               }.connect();
+                                       } finally {
+                                               server.stop();
+                                       }
+                               } finally {
+                                       if (br != null) {
+                                               br.stop();
+                                       }
+                               }
+
+                               if (err[0] != null) {
+                                       fail("An exception was thrown: " + err[0].getMessage(),
+                                                       err[0]);
+                               }
+
+                               assertEquals("ping", sent[0]);
+                               assertEquals("pong", recd[0]);
+                       }
+               });
+
+               addTest(new TestCase("Multiple exchanges " + skey) {
+                       final String[] sent = new String[3];
+                       final String[] recd = new String[3];
+                       final Exception[] err = new Exception[1];
+
+                       @Override
+                       public void test() throws Exception {
+                               ServerString server = new ServerString(this.getName(), 0, key) {
+                                       @Override
+                                       protected String onRequest(
+                                                       ConnectActionServerString action, Version version,
+                                                       String data, long id) throws Exception {
+                                               sent[0] = data;
+                                               action.send("pong");
+                                               sent[1] = action.rec();
+                                               return "pong2";
+                                       }
+
+                                       @Override
+                                       protected void onError(Exception e) {
+                                               err[0] = e;
+                                       }
+                               };
+
+                               int port = server.getPort();
+
+                               server.start();
+
+                               ServerBridge br = null;
+                               if (bridge) {
+                                       br = new ServerBridge(0, key, "", port, key);
+                                       br.setTraceHandler(null);
+                                       port = br.getPort();
+                                       br.start();
+                               }
+
+                               try {
+                                       try {
+                                               new ConnectActionClientString(null, port, key) {
+                                                       @Override
+                                                       public void action(Version version)
+                                                                       throws Exception {
+                                                               recd[0] = send("ping");
+                                                               recd[1] = send("ping2");
+                                                       }
+                                               }.connect();
+                                       } finally {
+                                               server.stop();
+                                       }
+                               } finally {
+                                       if (br != null) {
+                                               br.stop();
+                                       }
+                               }
+
+                               if (err[0] != null) {
+                                       fail("An exception was thrown: " + err[0].getMessage(),
+                                                       err[0]);
+                               }
+
+                               assertEquals("ping", sent[0]);
+                               assertEquals("pong", recd[0]);
+                               assertEquals("ping2", sent[1]);
+                               assertEquals("pong2", recd[1]);
+                       }
+               });
+
+               addTest(new TestCase("Multiple call from client " + skey) {
+                       final String[] sent = new String[3];
+                       final String[] recd = new String[3];
+                       final Exception[] err = new Exception[1];
+
+                       @Override
+                       public void test() throws Exception {
+                               ServerString server = new ServerString(this.getName(), 0, key) {
+                                       @Override
+                                       protected String onRequest(
+                                                       ConnectActionServerString action, Version version,
+                                                       String data, long id) throws Exception {
+                                               sent[Integer.parseInt(data)] = data;
+                                               return "" + (Integer.parseInt(data) * 2);
+                                       }
+
+                                       @Override
+                                       protected void onError(Exception e) {
+                                               err[0] = e;
+                                       }
+                               };
+
+                               int port = server.getPort();
+
+                               server.start();
+
+                               ServerBridge br = null;
+                               if (bridge) {
+                                       br = new ServerBridge(0, key, "", port, key);
+                                       br.setTraceHandler(null);
+                                       port = br.getPort();
+                                       br.start();
+                               }
+
+                               try {
+                                       try {
+                                               new ConnectActionClientString(null, port, key) {
+                                                       @Override
+                                                       public void action(Version version)
+                                                                       throws Exception {
+                                                               for (int i = 0; i < 3; i++) {
+                                                                       recd[i] = send("" + i);
+                                                               }
+                                                       }
+                                               }.connect();
+                                       } finally {
+                                               server.stop();
+                                       }
+                               } finally {
+                                       if (br != null) {
+                                               br.stop();
+                                       }
+                               }
+
+                               if (err[0] != null) {
+                                       fail("An exception was thrown: " + err[0].getMessage(),
+                                                       err[0]);
+                               }
+
+                               assertEquals("0", sent[0]);
+                               assertEquals("0", recd[0]);
+                               assertEquals("1", sent[1]);
+                               assertEquals("2", recd[1]);
+                               assertEquals("2", sent[2]);
+                               assertEquals("4", recd[2]);
+                       }
+               });
+       }
+
+       private SerialServerTest(final String[] args, final String key,
+                       final String skey, final boolean bridge, final String sbridge,
+                       final Object title) {
+
+               super(title + " " + skey + sbridge, args);
+
+               addTest(new TestCase("Simple connection " + skey) {
+                       @Override
+                       public void test() throws Exception {
+                               final Object[] rec = new Object[1];
+
+                               ServerObject server = new ServerObject(this.getName(), 0, key) {
+                                       @Override
+                                       protected Object onRequest(
+                                                       ConnectActionServerObject action, Version version,
+                                                       Object data, long id) throws Exception {
+                                               return null;
+                                       }
+
+                                       @Override
+                                       protected void onError(Exception e) {
+                                       }
+                               };
+
+                               int port = server.getPort();
+                               assertEquals("A port should have been assigned", true, port > 0);
+
+                               server.start();
+
+                               ServerBridge br = null;
+                               if (bridge) {
+                                       br = new ServerBridge(0, key, "", port, key);
+                                       br.setTraceHandler(null);
+                                       port = br.getPort();
+                                       br.start();
+                               }
+
+                               try {
+                                       try {
+                                               new ConnectActionClientObject(null, port, key) {
+                                                       @Override
+                                                       public void action(Version version)
+                                                                       throws Exception {
+                                                               rec[0] = true;
+                                                       }
+
+                                                       @Override
+                                                       protected void onError(Exception e) {
+                                                       }
+                                               }.connect();
+                                       } finally {
+                                               server.stop();
+                                       }
+                               } finally {
+                                       if (br != null) {
+                                               br.stop();
+                                       }
+                               }
+
+                               assertNotNull("The client action was not run", rec[0]);
+                               assertEquals(true, (boolean) ((Boolean) rec[0]));
+                       }
+               });
+
+               addTest(new TestCase("Simple exchange " + skey) {
+                       final Object[] sent = new Object[1];
+                       final Object[] recd = new Object[1];
+                       final Exception[] err = new Exception[1];
+
+                       @Override
+                       public void test() throws Exception {
+                               ServerObject server = new ServerObject(this.getName(), 0, key) {
+                                       @Override
+                                       protected Object onRequest(
+                                                       ConnectActionServerObject action, Version version,
+                                                       Object data, long id) throws Exception {
+                                               sent[0] = data;
+                                               return "pong";
+                                       }
+
+                                       @Override
+                                       protected void onError(Exception e) {
+                                               err[0] = e;
+                                       }
+                               };
+
+                               int port = server.getPort();
+
+                               server.start();
+
+                               ServerBridge br = null;
+                               if (bridge) {
+                                       br = new ServerBridge(0, key, "", port, key);
+                                       br.setTraceHandler(null);
+                                       port = br.getPort();
+                                       br.start();
+                               }
+
+                               try {
+                                       try {
+                                               new ConnectActionClientObject(null, port, key) {
+                                                       @Override
+                                                       public void action(Version version)
+                                                                       throws Exception {
+                                                               recd[0] = send("ping");
+                                                       }
+                                               }.connect();
+                                       } finally {
+                                               server.stop();
+                                       }
+                               } finally {
+                                       if (br != null) {
+                                               br.stop();
+                                       }
+                               }
+
+                               if (err[0] != null) {
+                                       fail("An exception was thrown: " + err[0].getMessage(),
+                                                       err[0]);
+                               }
+
+                               assertEquals("ping", sent[0]);
+                               assertEquals("pong", recd[0]);
+                       }
+               });
+
+               addTest(new TestCase("Multiple exchanges " + skey) {
+                       final Object[] sent = new Object[3];
+                       final Object[] recd = new Object[3];
+                       final Exception[] err = new Exception[1];
+
+                       @Override
+                       public void test() throws Exception {
+                               ServerObject server = new ServerObject(this.getName(), 0, key) {
+                                       @Override
+                                       protected Object onRequest(
+                                                       ConnectActionServerObject action, Version version,
+                                                       Object data, long id) throws Exception {
+                                               sent[0] = data;
+                                               action.send("pong");
+                                               sent[1] = action.rec();
+                                               return "pong2";
+                                       }
+
+                                       @Override
+                                       protected void onError(Exception e) {
+                                               err[0] = e;
+                                       }
+                               };
+
+                               int port = server.getPort();
+
+                               server.start();
+
+                               ServerBridge br = null;
+                               if (bridge) {
+                                       br = new ServerBridge(0, key, "", port, key);
+                                       br.setTraceHandler(null);
+                                       port = br.getPort();
+                                       br.start();
+                               }
+
+                               try {
+                                       try {
+                                               new ConnectActionClientObject(null, port, key) {
+                                                       @Override
+                                                       public void action(Version version)
+                                                                       throws Exception {
+                                                               recd[0] = send("ping");
+                                                               recd[1] = send("ping2");
+                                                       }
+                                               }.connect();
+                                       } finally {
+                                               server.stop();
+                                       }
+                               } finally {
+                                       if (br != null) {
+                                               br.stop();
+                                       }
+                               }
+
+                               if (err[0] != null) {
+                                       fail("An exception was thrown: " + err[0].getMessage(),
+                                                       err[0]);
+                               }
+
+                               assertEquals("ping", sent[0]);
+                               assertEquals("pong", recd[0]);
+                               assertEquals("ping2", sent[1]);
+                               assertEquals("pong2", recd[1]);
+                       }
+               });
+
+               addTest(new TestCase("Object array of URLs " + skey) {
+                       final Object[] sent = new Object[1];
+                       final Object[] recd = new Object[1];
+                       final Exception[] err = new Exception[1];
+
+                       @Override
+                       public void test() throws Exception {
+                               ServerObject server = new ServerObject(this.getName(), 0, key) {
+                                       @Override
+                                       protected Object onRequest(
+                                                       ConnectActionServerObject action, Version version,
+                                                       Object data, long id) throws Exception {
+                                               sent[0] = data;
+                                               return new Object[] { "ACK" };
+                                       }
+
+                                       @Override
+                                       protected void onError(Exception e) {
+                                               err[0] = e;
+                                       }
+                               };
+
+                               int port = server.getPort();
+
+                               server.start();
+
+                               ServerBridge br = null;
+                               if (bridge) {
+                                       br = new ServerBridge(0, key, "", port, key);
+                                       br.setTraceHandler(null);
+                                       port = br.getPort();
+                                       br.start();
+                               }
+
+                               try {
+                                       try {
+                                               new ConnectActionClientObject(null, port, key) {
+                                                       @Override
+                                                       public void action(Version version)
+                                                                       throws Exception {
+                                                               recd[0] = send(new Object[] {
+                                                                               "key",
+                                                                               new URL(
+                                                                                               "https://example.com/from_client"),
+                                                                               "https://example.com/from_client" });
+                                                       }
+                                               }.connect();
+                                       } finally {
+                                               server.stop();
+                                       }
+                               } finally {
+                                       if (br != null) {
+                                               br.stop();
+                                       }
+                               }
+
+                               if (err[0] != null) {
+                                       fail("An exception was thrown: " + err[0].getMessage(),
+                                                       err[0]);
+                               }
+
+                               Object[] sento = (Object[]) (sent[0]);
+                               Object[] recdo = (Object[]) (recd[0]);
+
+                               assertEquals("key", sento[0]);
+                               assertEquals("https://example.com/from_client",
+                                               ((URL) sento[1]).toString());
+                               assertEquals("https://example.com/from_client", sento[2]);
+                               assertEquals("ACK", recdo[0]);
+                       }
+               });
+
+               addTest(new TestCase("Multiple call from client " + skey) {
+                       final Object[] sent = new Object[3];
+                       final Object[] recd = new Object[3];
+                       final Exception[] err = new Exception[1];
+
+                       @Override
+                       public void test() throws Exception {
+                               ServerObject server = new ServerObject(this.getName(), 0, key) {
+                                       @Override
+                                       protected Object onRequest(
+                                                       ConnectActionServerObject action, Version version,
+                                                       Object data, long id) throws Exception {
+                                               sent[(Integer) data] = data;
+                                               return ((Integer) data) * 2;
+                                       }
+
+                                       @Override
+                                       protected void onError(Exception e) {
+                                               err[0] = e;
+                                       }
+                               };
+
+                               int port = server.getPort();
+
+                               server.start();
+
+                               ServerBridge br = null;
+                               if (bridge) {
+                                       br = new ServerBridge(0, key, "", port, key);
+                                       br.setTraceHandler(null);
+                                       port = br.getPort();
+                                       br.start();
+                               }
+
+                               try {
+                                       try {
+                                               new ConnectActionClientObject(null, port, key) {
+                                                       @Override
+                                                       public void action(Version version)
+                                                                       throws Exception {
+                                                               for (int i = 0; i < 3; i++) {
+                                                                       recd[i] = send(i);
+                                                               }
+                                                       }
+                                               }.connect();
+                                       } finally {
+                                               server.stop();
+                                       }
+                               } finally {
+                                       if (br != null) {
+                                               br.stop();
+                                       }
+                               }
+
+                               if (err[0] != null) {
+                                       fail("An exception was thrown: " + err[0].getMessage(),
+                                                       err[0]);
+                               }
+
+                               assertEquals(0, sent[0]);
+                               assertEquals(0, recd[0]);
+                               assertEquals(1, sent[1]);
+                               assertEquals(2, recd[1]);
+                               assertEquals(2, sent[2]);
+                               assertEquals(4, recd[2]);
+                       }
+               });
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/SerialTest.java b/src/be/nikiroo/tests/utils/SerialTest.java
new file mode 100644 (file)
index 0000000..46cda26
--- /dev/null
@@ -0,0 +1,281 @@
+package be.nikiroo.tests.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.NotSerializableException;
+import java.net.URL;
+import java.util.Arrays;
+
+import be.nikiroo.utils.serial.Exporter;
+import be.nikiroo.utils.serial.Importer;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class SerialTest extends TestLauncher {
+       /**
+        * Required for Import/Export of objects.
+        */
+       public SerialTest() {
+               this(null);
+       }
+
+       private void encodeRecodeTest(TestCase test, Object data) throws Exception {
+               byte[] encoded = toBytes(data, true);
+               Object redata = fromBytes(toBytes(data, false));
+               byte[] reencoded = toBytes(redata, true);
+
+               // We suppose text mode
+               if (encoded.length < 256 && reencoded.length < 256) {
+                       test.assertEquals("Different data after encode/decode/encode",
+                                       new String(encoded, "UTF-8"),
+                                       new String(reencoded, "UTF-8"));
+               } else {
+                       test.assertEquals("Different data after encode/decode/encode",
+                                       true, Arrays.equals(encoded, reencoded));
+               }
+       }
+
+       // try to remove pointer addresses
+       private byte[] toBytes(Object data, boolean clearRefs)
+                       throws NotSerializableException, IOException {
+               ByteArrayOutputStream out = new ByteArrayOutputStream();
+               new Exporter(out).append(data);
+               out.flush();
+
+               if (clearRefs) {
+                       String tmp = new String(out.toByteArray(), "UTF-8");
+                       tmp = tmp.replaceAll("@[0-9]*", "@REF");
+                       return tmp.getBytes("UTF-8");
+               }
+
+               return out.toByteArray();
+       }
+
+       private Object fromBytes(byte[] data) throws NoSuchFieldException,
+                       NoSuchMethodException, ClassNotFoundException,
+                       NullPointerException, IOException {
+
+               InputStream in = new ByteArrayInputStream(data);
+               try {
+                       return new Importer().read(in).getValue();
+               } finally {
+                       in.close();
+               }
+       }
+
+       public SerialTest(String[] args) {
+               super("Serial test", args);
+
+               addTest(new TestCase("Simple class Import/Export") {
+                       @Override
+                       public void test() throws Exception {
+                               Data data = new Data(42);
+                               encodeRecodeTest(this, data);
+                       }
+               });
+
+               addTest(new TestCase() {
+                       @SuppressWarnings("unused")
+                       private TestCase me = setName("Anonymous inner class");
+
+                       @Override
+                       public void test() throws Exception {
+                               Data data = new Data() {
+                                       @SuppressWarnings("unused")
+                                       int value = 42;
+                               };
+                               encodeRecodeTest(this, data);
+                       }
+               });
+               addTest(new TestCase() {
+                       @SuppressWarnings("unused")
+                       private TestCase me = setName("Array of anonymous inner classes");
+
+                       @Override
+                       public void test() throws Exception {
+                               Data[] data = new Data[] { new Data() {
+                                       @SuppressWarnings("unused")
+                                       int value = 42;
+                               } };
+
+                               byte[] encoded = toBytes(data, false);
+                               Object redata = fromBytes(encoded);
+
+                               // Comparing the 2 arrays won't be useful, because the @REFs
+                               // will be ZIP-encoded; so we parse and re-encode each object
+
+                               byte[] encoded1 = toBytes(data[0], true);
+                               byte[] reencoded1 = toBytes(((Object[]) redata)[0], true);
+
+                               assertEquals("Different data after encode/decode/encode", true,
+                                               Arrays.equals(encoded1, reencoded1));
+                       }
+               });
+               addTest(new TestCase("URL Import/Export") {
+                       @Override
+                       public void test() throws Exception {
+                               URL data = new URL("https://fanfan.be/");
+                               encodeRecodeTest(this, data);
+                       }
+               });
+               addTest(new TestCase("URL-String Import/Export") {
+                       @Override
+                       public void test() throws Exception {
+                               String data = new URL("https://fanfan.be/").toString();
+                               encodeRecodeTest(this, data);
+                       }
+               });
+               addTest(new TestCase("URL/URL-String arrays Import/Export") {
+                       @Override
+                       public void test() throws Exception {
+                               final String url = "https://fanfan.be/";
+                               Object[] data = new Object[] { new URL(url), url };
+
+                               byte[] encoded = toBytes(data, false);
+                               Object redata = fromBytes(encoded);
+
+                               // Comparing the 2 arrays won't be useful, because the @REFs
+                               // will be ZIP-encoded; so we parse and re-encode each object
+
+                               byte[] encoded1 = toBytes(data[0], true);
+                               byte[] reencoded1 = toBytes(((Object[]) redata)[0], true);
+                               byte[] encoded2 = toBytes(data[1], true);
+                               byte[] reencoded2 = toBytes(((Object[]) redata)[1], true);
+
+                               assertEquals("Different data 1 after encode/decode/encode",
+                                               true, Arrays.equals(encoded1, reencoded1));
+                               assertEquals("Different data 2 after encode/decode/encode",
+                                               true, Arrays.equals(encoded2, reencoded2));
+                       }
+               });
+               addTest(new TestCase("Import/Export with nested objects") {
+                       @Override
+                       public void test() throws Exception {
+                               Data data = new DataObject(new Data(21));
+                               encodeRecodeTest(this, data);
+                       }
+               });
+               addTest(new TestCase("Import/Export String in object") {
+                       @Override
+                       public void test() throws Exception {
+                               Data data = new DataString("fanfan");
+                               encodeRecodeTest(this, data);
+                               data = new DataString("http://example.com/query.html");
+                               encodeRecodeTest(this, data);
+                               data = new DataString("Test|Ché|http://|\"\\\"Pouch\\");
+                               encodeRecodeTest(this, data);
+                               data = new DataString("Test|Ché\\n|\nhttp://|\"\\\"Pouch\\");
+                               encodeRecodeTest(this, data);
+                       }
+               });
+               addTest(new TestCase("Import/Export with nested objects forming a loop") {
+                       @Override
+                       public void test() throws Exception {
+                               DataLoop data = new DataLoop("looping");
+                               data.next = new DataLoop("level 2");
+                               data.next.next = data;
+                               encodeRecodeTest(this, data);
+                       }
+               });
+               addTest(new TestCase("Array in Object Import/Export") {
+                       @Override
+                       public void test() throws Exception {
+                               Object data = new DataArray();// new String[] { "un", "deux" };
+                               encodeRecodeTest(this, data);
+                       }
+               });
+               addTest(new TestCase("Array Import/Export") {
+                       @Override
+                       public void test() throws Exception {
+                               Object data = new String[] { "un", "deux" };
+                               encodeRecodeTest(this, data);
+                       }
+               });
+               addTest(new TestCase("Enum Import/Export") {
+                       @Override
+                       public void test() throws Exception {
+                               Object data = EnumToSend.FANFAN;
+                               encodeRecodeTest(this, data);
+                       }
+               });
+       }
+
+       class DataArray {
+               public String[] data = new String[] { "un", "deux" };
+       }
+
+       class Data {
+               private int value;
+
+               private Data() {
+               }
+
+               public Data(int value) {
+                       this.value = value;
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       if (obj instanceof Data) {
+                               Data other = (Data) obj;
+                               return other.value == this.value;
+                       }
+
+                       return false;
+               }
+
+               @Override
+               public int hashCode() {
+                       return new Integer(value).hashCode();
+               }
+       }
+
+       @SuppressWarnings("unused")
+       class DataObject extends Data {
+               private Data data;
+
+               @SuppressWarnings("synthetic-access")
+               private DataObject() {
+               }
+
+               @SuppressWarnings("synthetic-access")
+               public DataObject(Data data) {
+                       this.data = data;
+               }
+       }
+
+       @SuppressWarnings("unused")
+       class DataString extends Data {
+               private String data;
+
+               @SuppressWarnings("synthetic-access")
+               private DataString() {
+               }
+
+               @SuppressWarnings("synthetic-access")
+               public DataString(String data) {
+                       this.data = data;
+               }
+       }
+
+       @SuppressWarnings("unused")
+       class DataLoop extends Data {
+               public DataLoop next;
+               private String value;
+
+               @SuppressWarnings("synthetic-access")
+               private DataLoop() {
+               }
+
+               @SuppressWarnings("synthetic-access")
+               public DataLoop(String value) {
+                       this.value = value;
+               }
+       }
+
+       enum EnumToSend {
+               FANFAN, TULIPE,
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/StringUtilsTest.java b/src/be/nikiroo/tests/utils/StringUtilsTest.java
new file mode 100644 (file)
index 0000000..a17731f
--- /dev/null
@@ -0,0 +1,304 @@
+package be.nikiroo.tests.utils;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.StringUtils.Alignment;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class StringUtilsTest extends TestLauncher {
+       public StringUtilsTest(String[] args) {
+               super("StringUtils test", args);
+
+               addTest(new TestCase("Time serialisation") {
+                       @Override
+                       public void test() throws Exception {
+                               for (long fullTime : new Long[] { 0l, 123456l, 123456000l,
+                                               new Date().getTime() }) {
+                                       // precise to the second, no more
+                                       long time = (fullTime / 1000) * 1000;
+
+                                       String displayTime = StringUtils.fromTime(time);
+                                       assertNotNull("The stringified time for " + time
+                                                       + " should not be null", displayTime);
+                                       assertEquals("The stringified time for " + time
+                                                       + " should not be empty", false, displayTime.trim()
+                                                       .isEmpty());
+
+                                       assertEquals("The time " + time
+                                                       + " should be loop-convertable", time,
+                                                       StringUtils.toTime(displayTime));
+
+                                       assertEquals("The time " + displayTime
+                                                       + " should be loop-convertable", displayTime,
+                                                       StringUtils.fromTime(StringUtils
+                                                                       .toTime(displayTime)));
+                               }
+                       }
+               });
+
+               addTest(new TestCase("MD5") {
+                       @Override
+                       public void test() throws Exception {
+                               String mess = "The String we got is not what 'md5sum' said it should heve been";
+                               assertEquals(mess, "34ded48fcff4221d644be9a37e1cb1d9",
+                                               StringUtils.getMd5Hash("fanfan la tulipe"));
+                               assertEquals(mess, "7691b0cb74ed0f94b4d8cd858abe1165",
+                                               StringUtils.getMd5Hash("je te do-o-o-o-o-o-nne"));
+                       }
+               });
+
+               addTest(new TestCase("Padding") {
+                       @Override
+                       public void test() throws Exception {
+                               for (String data : new String[] { "fanfan", "la tulipe",
+                                               "1234567890", "12345678901234567890", "1", "" }) {
+                                       String result = StringUtils.padString(data, -1);
+                                       assertEquals("A size of -1 is expected to produce a noop",
+                                                       true, data.equals(result));
+                                       for (int size : new Integer[] { 0, 1, 5, 10, 40 }) {
+                                               result = StringUtils.padString(data, size);
+                                               assertEquals(
+                                                               "Padding a String at a certain size should give a String of the given size",
+                                                               size, result.length());
+                                               assertEquals(
+                                                               "Padding a String should not change the content",
+                                                               true, data.trim().startsWith(result.trim()));
+
+                                               result = StringUtils.padString(data, size, false, null);
+                                               assertEquals(
+                                                               "Padding a String without cutting should not shorten the String",
+                                                               true, data.length() <= result.length());
+                                               assertEquals(
+                                                               "Padding a String without cutting should keep the whole content",
+                                                               true, data.trim().equals(result.trim()));
+
+                                               result = StringUtils.padString(data, size, false,
+                                                               Alignment.RIGHT);
+                                               if (size > data.length()) {
+                                                       assertEquals(
+                                                                       "Padding a String to the end should work as expected",
+                                                                       true, result.endsWith(data));
+                                               }
+
+                                               result = StringUtils.padString(data, size, false,
+                                                               Alignment.JUSTIFY);
+                                               if (size > data.length()) {
+                                                       String unspacedData = data.trim();
+                                                       String unspacedResult = result.trim();
+                                                       for (int i = 0; i < size; i++) {
+                                                               unspacedData = unspacedData.replace("  ", " ");
+                                                               unspacedResult = unspacedResult.replace("  ",
+                                                                               " ");
+                                                       }
+
+                                                       assertEquals(
+                                                                       "Justified text trimmed with all spaces collapsed "
+                                                                                       + "sould be identical to original text "
+                                                                                       + "trimmed with all spaces collapsed",
+                                                                       unspacedData, unspacedResult);
+                                               }
+
+                                               result = StringUtils.padString(data, size, false,
+                                                               Alignment.CENTER);
+                                               if (size > data.length()) {
+                                                       int before = 0;
+                                                       for (int i = 0; i < result.length()
+                                                                       && result.charAt(i) == ' '; i++) {
+                                                               before++;
+                                                       }
+
+                                                       int after = 0;
+                                                       for (int i = result.length() - 1; i >= 0
+                                                                       && result.charAt(i) == ' '; i--) {
+                                                               after++;
+                                                       }
+
+                                                       if (result.trim().isEmpty()) {
+                                                               after = before / 2;
+                                                               if (before > (2 * after)) {
+                                                                       before = after + 1;
+                                                               } else {
+                                                                       before = after;
+                                                               }
+                                                       }
+
+                                                       assertEquals(
+                                                                       "Padding a String on center should work as expected",
+                                                                       result.length(), before + data.length()
+                                                                                       + after);
+                                                       assertEquals(
+                                                                       "Padding a String on center should not uncenter the content",
+                                                                       true, Math.abs(before - after) <= 1);
+                                               }
+                                       }
+                               }
+                       }
+               });
+
+               addTest(new TestCase("Justifying") {
+                       @Override
+                       public void test() throws Exception {
+                               Map<String, Map<Integer, Entry<Alignment, List<String>>>> source = new HashMap<String, Map<Integer, Entry<Alignment, List<String>>>>();
+                               addValue(source, Alignment.LEFT, "testy", -1, "testy");
+                               addValue(source, Alignment.RIGHT, "testy", -1, "testy");
+                               addValue(source, Alignment.CENTER, "testy", -1, "testy");
+                               addValue(source, Alignment.JUSTIFY, "testy", -1, "testy");
+                               addValue(source, Alignment.LEFT, "testy", 5, "testy");
+                               addValue(source, Alignment.LEFT, "testy", 3, "te-", "sty");
+                               addValue(source, Alignment.LEFT,
+                                               "Un petit texte qui se mettra sur plusieurs lignes",
+                                               10, "Un petit", "texte qui", "se mettra", "sur",
+                                               "plusieurs", "lignes");
+                               addValue(source, Alignment.LEFT,
+                                               "Un petit texte qui se mettra sur plusieurs lignes", 7,
+                                               "Un", "petit", "texte", "qui se", "mettra", "sur",
+                                               "plusie-", "urs", "lignes");
+                               addValue(source, Alignment.RIGHT,
+                                               "Un petit texte qui se mettra sur plusieurs lignes", 7,
+                                               "     Un", "  petit", "  texte", " qui se", " mettra",
+                                               "    sur", "plusie-", "    urs", " lignes");
+                               addValue(source, Alignment.CENTER,
+                                               "Un petit texte qui se mettra sur plusieurs lignes", 7,
+                                               "  Un   ", " petit ", " texte ", "qui se ", "mettra ",
+                                               "  sur  ", "plusie-", "  urs  ", "lignes ");
+                               addValue(source, Alignment.JUSTIFY,
+                                               "Un petit texte qui se mettra sur plusieurs lignes", 7,
+                                               "Un pet-", "it tex-", "te  qui", "se met-", "tra sur",
+                                               "plusie-", "urs li-", "gnes");
+                               addValue(source, Alignment.JUSTIFY,
+                                               "Un petit texte qui se mettra sur plusieurs lignes",
+                                               14, "Un       petit", "texte  qui  se",
+                                               "mettra     sur", "plusieurs lig-", "nes");
+                               addValue(source, Alignment.JUSTIFY, "le dash-test", 9,
+                                               "le  dash-", "test");
+
+                               for (String data : source.keySet()) {
+                                       for (int size : source.get(data).keySet()) {
+                                               Alignment align = source.get(data).get(size).getKey();
+                                               List<String> values = source.get(data).get(size)
+                                                               .getValue();
+
+                                               List<String> result = StringUtils.justifyText(data,
+                                                               size, align);
+
+                                               // System.out.println("[" + data + " (" + size + ")" +
+                                               // "] -> [");
+                                               // for (int i = 0; i < result.size(); i++) {
+                                               // String resultLine = result.get(i);
+                                               // System.out.println(i + ": " + resultLine);
+                                               // }
+                                               // System.out.println("]");
+
+                                               assertEquals(values, result);
+                                       }
+                               }
+                       }
+               });
+
+               addTest(new TestCase("unhtml") {
+                       @Override
+                       public void test() throws Exception {
+                               Map<String, String> data = new HashMap<String, String>();
+                               data.put("aa", "aa");
+                               data.put("test with spaces ", "test with spaces ");
+                               data.put("<a href='truc://target/'>link</a>", "link");
+                               data.put("<html>Digimon</html>", "Digimon");
+                               data.put("", "");
+                               data.put(" ", " ");
+
+                               for (Entry<String, String> entry : data.entrySet()) {
+                                       String result = StringUtils.unhtml(entry.getKey());
+                                       assertEquals("Result is not what we expected",
+                                                       entry.getValue(), result);
+                               }
+                       }
+               });
+
+               addTest(new TestCase("zip64") {
+                       @Override
+                       public void test() throws Exception {
+                               String orig = "test";
+                               String zipped = StringUtils.zip64(orig);
+                               String unzipped = StringUtils.unzip64s(zipped);
+                               assertEquals(orig, unzipped);
+                       }
+               });
+
+               addTest(new TestCase("format/toNumber simple") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals(263l, StringUtils.toNumber("263"));
+                               assertEquals(21200l, StringUtils.toNumber("21200"));
+                               assertEquals(0l, StringUtils.toNumber("0"));
+                               assertEquals("263", StringUtils.formatNumber(263l));
+                               assertEquals("21 k", StringUtils.formatNumber(21000l));
+                               assertEquals("0", StringUtils.formatNumber(0l));
+                       }
+               });
+
+               addTest(new TestCase("format/toNumber not 000") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals(263200l, StringUtils.toNumber("263.2 k"));
+                               assertEquals(42000l, StringUtils.toNumber("42.0 k"));
+                               assertEquals(12000000l, StringUtils.toNumber("12 M"));
+                               assertEquals(2000000000l, StringUtils.toNumber("2 G"));
+                               assertEquals("263 k", StringUtils.formatNumber(263012l));
+                               assertEquals("42 k", StringUtils.formatNumber(42012l));
+                               assertEquals("12 M", StringUtils.formatNumber(12012121l));
+                               assertEquals("7 G", StringUtils.formatNumber(7364635928l));
+                       }
+               });
+
+               addTest(new TestCase("format/toNumber decimals") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals(263200l, StringUtils.toNumber("263.2 k"));
+                               assertEquals(1200l, StringUtils.toNumber("1.2 k"));
+                               assertEquals(42700000l, StringUtils.toNumber("42.7 M"));
+                               assertEquals(1220l, StringUtils.toNumber("1.22 k"));
+                               assertEquals(1432l, StringUtils.toNumber("1.432 k"));
+                               assertEquals(6938l, StringUtils.toNumber("6.938 k"));
+                               assertEquals("1.3 k", StringUtils.formatNumber(1300l, 1));
+                               assertEquals("263.2020 k", StringUtils.formatNumber(263202l, 4));
+                               assertEquals("1.26 k", StringUtils.formatNumber(1267l, 2));
+                               assertEquals("42.7 M", StringUtils.formatNumber(42712121l, 1));
+                               assertEquals("5.09 G", StringUtils.formatNumber(5094837485l, 2));
+                       }
+               });
+       }
+
+       static private void addValue(
+                       Map<String, Map<Integer, Entry<Alignment, List<String>>>> source,
+                       final Alignment align, String input, int size,
+                       final String... result) {
+               if (!source.containsKey(input)) {
+                       source.put(input,
+                                       new HashMap<Integer, Entry<Alignment, List<String>>>());
+               }
+
+               source.get(input).put(size, new Entry<Alignment, List<String>>() {
+                       @Override
+                       public Alignment getKey() {
+                               return align;
+                       }
+
+                       @Override
+                       public List<String> getValue() {
+                               return Arrays.asList(result);
+                       }
+
+                       @Override
+                       public List<String> setValue(List<String> value) {
+                               return null;
+                       }
+               });
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/TempFilesTest.java b/src/be/nikiroo/tests/utils/TempFilesTest.java
new file mode 100644 (file)
index 0000000..19ed207
--- /dev/null
@@ -0,0 +1,109 @@
+package be.nikiroo.tests.utils;
+
+import java.io.File;
+import java.io.IOException;
+
+import be.nikiroo.utils.TempFiles;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class TempFilesTest extends TestLauncher {
+       public TempFilesTest(String[] args) {
+               super("TempFiles", args);
+
+               addTest(new TestCase("Name is correct") {
+                       @Override
+                       public void test() throws Exception {
+                               RootTempFiles files = new RootTempFiles("testy");
+                               try {
+                                       assertEquals("The root was not created", true, files
+                                                       .getRoot().exists());
+                                       assertEquals(
+                                                       "The root is not prefixed with the expected name",
+                                                       true, files.getRoot().getName().startsWith("testy"));
+
+                               } finally {
+                                       files.close();
+                               }
+                       }
+               });
+
+               addTest(new TestCase("Clean after itself no use") {
+                       @Override
+                       public void test() throws Exception {
+                               RootTempFiles files = new RootTempFiles("testy2");
+                               try {
+                                       assertEquals("The root was not created", true, files
+                                                       .getRoot().exists());
+                               } finally {
+                                       files.close();
+                                       assertEquals("The root was not deleted", false, files
+                                                       .getRoot().exists());
+                               }
+                       }
+               });
+
+               addTest(new TestCase("Clean after itself after usage") {
+                       @Override
+                       public void test() throws Exception {
+                               RootTempFiles files = new RootTempFiles("testy3");
+                               try {
+                                       assertEquals("The root was not created", true, files
+                                                       .getRoot().exists());
+                                       files.createTempFile("test");
+                               } finally {
+                                       files.close();
+                                       assertEquals("The root was not deleted", false, files
+                                                       .getRoot().exists());
+                                       assertEquals("The main root in /tmp was not deleted",
+                                                       false, files.getRoot().getParentFile().exists());
+                               }
+                       }
+               });
+
+               addTest(new TestCase("Temporary directories") {
+                       @Override
+                       public void test() throws Exception {
+                               RootTempFiles files = new RootTempFiles("testy4");
+                               File file = null;
+                               try {
+                                       File dir = files.createTempDir("test");
+                                       file = new File(dir, "fanfan");
+                                       file.createNewFile();
+                                       assertEquals(
+                                                       "Cannot create a file in a temporary directory",
+                                                       true, file.exists());
+                               } finally {
+                                       files.close();
+                                       if (file != null) {
+                                               assertEquals(
+                                                               "A file created in a temporary directory should be deleted on close",
+                                                               false, file.exists());
+                                       }
+                                       assertEquals("The root was not deleted", false, files
+                                                       .getRoot().exists());
+                               }
+                       }
+               });
+       }
+
+       private class RootTempFiles extends TempFiles {
+               private File root = null;
+
+               public RootTempFiles(String name) throws IOException {
+                       super(name);
+               }
+
+               public File getRoot() {
+                       if (root != null)
+                               return root;
+                       return super.root;
+               }
+
+               @Override
+               public synchronized void close() throws IOException {
+                       root = super.root;
+                       super.close();
+               }
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/Test.java b/src/be/nikiroo/tests/utils/Test.java
new file mode 100644 (file)
index 0000000..23f4946
--- /dev/null
@@ -0,0 +1,68 @@
+package be.nikiroo.tests.utils;
+
+import be.nikiroo.utils.Cache;
+import be.nikiroo.utils.CacheMemory;
+import be.nikiroo.utils.Downloader;
+import be.nikiroo.utils.Proxy;
+import be.nikiroo.utils.main.bridge;
+import be.nikiroo.utils.main.img2aa;
+import be.nikiroo.utils.main.justify;
+import be.nikiroo.utils.test.TestLauncher;
+import be.nikiroo.utils.ui.UIUtils;
+
+/**
+ * Tests for nikiroo-utils.
+ * 
+ * @author niki
+ */
+public class Test extends TestLauncher {
+       /**
+        * Start the tests.
+        * 
+        * @param args
+        *            the arguments (which are passed as-is to the other test
+        *            classes)
+        */
+       public Test(String[] args) {
+               super("Nikiroo-utils", args);
+
+               // setDetails(true);
+
+               addSeries(new ProgressTest(args));
+               addSeries(new BundleTest(args));
+               addSeries(new IOUtilsTest(args));
+               addSeries(new VersionTest(args));
+               addSeries(new SerialTest(args));
+               addSeries(new SerialServerTest(args));
+               addSeries(new StringUtilsTest(args));
+               addSeries(new TempFilesTest(args));
+               addSeries(new CryptUtilsTest(args));
+               addSeries(new BufferedInputStreamTest(args));
+               addSeries(new NextableInputStreamTest(args));
+               addSeries(new ReplaceInputStreamTest(args));
+               addSeries(new BufferedOutputStreamTest(args));
+               addSeries(new ReplaceOutputStreamTest(args));
+
+               // TODO: test cache and downloader
+               Cache cache = null;
+               CacheMemory memcache = null;
+               Downloader downloader = null;
+               
+               // To include the sources:
+               img2aa siu;
+               justify ssu;
+               bridge aa;
+               Proxy proxy;
+               UIUtils uiUtils;
+       }
+
+       /**
+        * Main entry point of the program.
+        * 
+        * @param args
+        *            the arguments passed to the {@link TestLauncher}s.
+        */
+       static public void main(String[] args) {
+               System.exit(new Test(args).launch());
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/VersionTest.java b/src/be/nikiroo/tests/utils/VersionTest.java
new file mode 100644 (file)
index 0000000..52d80e7
--- /dev/null
@@ -0,0 +1,140 @@
+package be.nikiroo.tests.utils;
+
+import be.nikiroo.utils.Version;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class VersionTest extends TestLauncher {
+       public VersionTest(String[] args) {
+               super("Version test", args);
+
+               addTest(new TestCase("String <-> int") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals("Cannot parse version 1.2.3 from int to String",
+                                               "1.2.3", new Version(1, 2, 3).toString());
+                               assertEquals(
+                                               "Cannot parse major version \"1.2.3\" from String to int",
+                                               1, new Version("1.2.3").getMajor());
+                               assertEquals(
+                                               "Cannot parse minor version \"1.2.3\" from String to int",
+                                               2, new Version("1.2.3").getMinor());
+                               assertEquals(
+                                               "Cannot parse patch version \"1.2.3\" from String to int",
+                                               3, new Version("1.2.3").getPatch());
+                       }
+               });
+
+               addTest(new TestCase("Bad input") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals(
+                                               "Bad input should return an empty version",
+                                               true,
+                                               new Version(
+                                                               "Doors 98 SE Special Deluxe Edition Pro++ Not-Home")
+                                                               .isEmpty());
+
+                               assertEquals(
+                                               "Bad input should return [unknown]",
+                                               "[unknown]",
+                                               new Version(
+                                                               "Doors 98 SE Special Deluxe Edition Pro++ Not-Home")
+                                                               .toString());
+                       }
+               });
+
+               addTest(new TestCase("Read current version") {
+                       @Override
+                       public void test() throws Exception {
+                               assertNotNull("The version should not be NULL (in any case!)",
+                                               Version.getCurrentVersion());
+                               assertEquals("The current version should not be empty", false,
+                                               Version.getCurrentVersion().isEmpty());
+                       }
+               });
+
+               addTest(new TestCase("Tag version") {
+                       @Override
+                       public void test() throws Exception {
+                               Version version = new Version("1.0.0-debian0");
+                               assertEquals("debian", version.getTag());
+                               assertEquals(0, version.getTagVersion());
+                               version = new Version("1.0.0-debian.0");
+                               assertEquals("debian.", version.getTag());
+                               assertEquals(0, version.getTagVersion());
+                               version = new Version("1.0.0-debian-0");
+                               assertEquals("debian-", version.getTag());
+                               assertEquals(0, version.getTagVersion());
+                               version = new Version("1.0.0-debian-12");
+                               assertEquals("debian-", version.getTag());
+                               assertEquals(12, version.getTagVersion());
+
+                               // tag with no tag version
+                               version = new Version("1.0.0-dev");
+                               assertEquals(1, version.getMajor());
+                               assertEquals(0, version.getMinor());
+                               assertEquals(0, version.getPatch());
+                               assertEquals("dev", version.getTag());
+                               assertEquals(-1, version.getTagVersion());
+                       }
+               });
+
+               addTest(new TestCase("Comparing versions") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals(true,
+                                               new Version(1, 1, 1).isNewerThan(new Version(1, 1, 0)));
+                               assertEquals(true,
+                                               new Version(2, 0, 0).isNewerThan(new Version(1, 1, 1)));
+                               assertEquals(true,
+                                               new Version(10, 7, 8).isNewerThan(new Version(9, 9, 9)));
+                               assertEquals(true,
+                                               new Version(0, 0, 0).isOlderThan(new Version(0, 0, 1)));
+                               assertEquals(1,
+                                               new Version(1, 1, 1).compareTo(new Version(0, 1, 1)));
+                               assertEquals(-1,
+                                               new Version(0, 0, 1).compareTo(new Version(0, 1, 1)));
+                               assertEquals(0,
+                                               new Version(0, 0, 1).compareTo(new Version(0, 0, 1)));
+                               assertEquals(true,
+                                               new Version(0, 0, 1).equals(new Version(0, 0, 1)));
+                               assertEquals(false,
+                                               new Version(0, 2, 1).equals(new Version(0, 0, 1)));
+
+                               assertEquals(true,
+                                               new Version(1, 0, 1, "my.tag.", 2).equals(new Version(
+                                                               1, 0, 1, "my.tag.", 2)));
+                               assertEquals(false,
+                                               new Version(1, 0, 1, "my.tag.", 2).equals(new Version(
+                                                               1, 0, 0, "my.tag.", 2)));
+                               assertEquals(false,
+                                               new Version(1, 0, 1, "my.tag.", 2).equals(new Version(
+                                                               1, 0, 1, "not-my.tag.", 2)));
+                       }
+               });
+
+               addTest(new TestCase("toString") {
+                       @Override
+                       public void test() throws Exception {
+                               // Check leading 0s:
+                               Version version = new Version("01.002.4");
+                               assertEquals("Leading 0s not working", "1.2.4",
+                                               version.toString());
+
+                               // Check spacing
+                               version = new Version("1 . 2.4 ");
+                               assertEquals("Additional spaces not working", "1.2.4",
+                                               version.toString());
+
+                               String[] tests = new String[] { "1.0.0", "1.2.3", "1.0.0-dev",
+                                               "1.1.2-niki0" };
+                               for (String test : tests) {
+                                       version = new Version(test);
+                                       assertEquals("toString and back conversion failed", test,
+                                                       version.toString());
+                               }
+                       }
+               });
+       }
+}
diff --git a/src/be/nikiroo/tests/utils/bundle_test.properties b/src/be/nikiroo/tests/utils/bundle_test.properties
new file mode 100644 (file)
index 0000000..5222c59
--- /dev/null
@@ -0,0 +1,3 @@
+ONE = un
+ONE_SUFFIX = un + suffix
+JAPANESE = 日本語 Nihongo
\ No newline at end of file
diff --git a/src/be/nikiroo/utils b/src/be/nikiroo/utils
new file mode 160000 (submodule)
index 0000000..34835dd
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 34835dd204994ab0be1899848c4de2f9dde9f766