net.kseek.http.TinyHttpServer.java Source code

Java tutorial

Introduction

Here is the source code for net.kseek.http.TinyHttpServer.java

Source

/*
 * Copyright (C) 2011-2013 GUIGUI Simon, fyhertz@gmail.com
 * 
 * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/)
 * 
 * Spydroid is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This source code is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this source code; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * Based on that: http://hc.apache.org/httpcomponents-core-ga/examples.html.
 * 
 */

package net.kseek.http;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Date;
import java.util.LinkedList;
import java.util.Map;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.X509KeyManager;

import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpException;
import org.apache.http.HttpServerConnection;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;
import org.apache.http.protocol.ResponseDate;
import org.apache.http.protocol.ResponseServer;
import org.apache.http.protocol.UriPatternMatcher;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;

/**
 * 
 * A Service that contains a HTTP server and a HTTPS server.
 * 
 * Check out the github page of this project for further information.
 * 
 * You may add some logic to this server with {@link #addRequestHandler(String, HttpRequestHandler)}.
 * By default it serves files from /assets/www.
 * 
 */
public class TinyHttpServer extends Service {

    /** The tag used by the server. */
    public static final String TAG = "TinyHttpServer";

    /** Default port for HTTP. */
    public final static int DEFAULT_HTTP_PORT = 8080;

    /** Default port for HTTPS. */
    public final static int DEFAULT_HTTPS_PORT = 8443;

    /** Port already in use. */
    public final static int ERROR_HTTP_BIND_FAILED = 0x00;

    /** Port already in use. */
    public final static int ERROR_HTTPS_BIND_FAILED = 0x01;

    /** You need to add the class {@link ModSSL} to the package {@link net.kseek.http}. */
    public final static int ERROR_HTTPS_NOT_SUPPORTED = 0x02;

    /** An error occured with the HTTPS server :( */
    public final static int ERROR_HTTPS_SERVER_CRASHED = 0x03;

    /** Key used in the SharedPreferences to store whether the HTTP server is enabled or not. */
    public final static String KEY_HTTP_ENABLED = "http_enabled";

    /** Key used in the SharedPreferences to store whether the HTTPS server is enabled or not. */
    public final static String KEY_HTTPS_ENABLED = "https_enabled";

    /** Key used in the SharedPreferences for the port used by the HTTP server. */
    public final static String KEY_HTTP_PORT = "http_port";

    /** Key used in the SharedPreferences for the port used by the HTTPS server. */
    public final static String KEY_HTTPS_PORT = "https_port";

    /** Key used in the SharedPreferences for storing the password of the keystore. */
    public final static String KEY_PASSWORD = "https_password";

    /** Name of the file used to store the keystore containing the certificates for HTTPS. */
    public final static String KEYSTORE_FILE_NAME = "keystore.jks";

    /** 
     * Common name that will appear in the root certificate. 
     * You might want to extend {@link TinyHttpServer} to change that. 
     **/
    protected String mCACommonName = "TinyHttpServer CA";

    protected String[] MODULES = new String[] { "ModAssetServer", "ModInternationalization" };

    protected int mHttpPort = DEFAULT_HTTP_PORT;
    protected int mHttpsPort = DEFAULT_HTTPS_PORT;
    protected boolean mHttpEnabled = true, mHttpsEnabled = false;
    protected LinkedList<CallbackListener> mListeners = new LinkedList<CallbackListener>();

    private BasicHttpProcessor mHttpProcessor;
    private HttpParams mParams;
    private HttpRequestListener mHttpRequestListener = null;
    private HttpsRequestListener mHttpsRequestListener = null;
    private SharedPreferences mSharedPreferences;
    private boolean mHttpsUpdate = false, mHttpUpdate = false;

    Date mLastModified;
    MHttpRequestHandlerRegistry mRegistry;
    Context mContext;

    /** Be careful: those callbacks won't necessarily be called from the ui thread ! */
    public interface CallbackListener {

        /** Called when an error occurs. */
        void onError(TinyHttpServer server, Exception e, int error);

        void onMessage(TinyHttpServer server, int message);

    }

    /**
     * See {@link TinyHttpServer.CallbackListener} to check out what events will be fired once you set up a listener.
     * @param listener The listener
     */
    public void addCallbackListener(CallbackListener listener) {
        synchronized (mListeners) {
            mListeners.add(listener);
        }
    }

    /**
     * Removes the listener.
     * @param listener The listener
     */
    public void removeCallbackListener(CallbackListener listener) {
        synchronized (mListeners) {
            mListeners.remove(listener);
        }
    }

    /** 
     * You may add some HttpRequestHandler to modify the default behavior of the server.
     * @param pattern Patterns may have three formats: * or *<uri> or <uri>*
     * @param handler A HttpRequestHandler
     */
    protected void addRequestHandler(String pattern, HttpRequestHandler handler) {
        mRegistry.register(pattern, handler);
    }

    /**
     * Sets the port for the HTTP server to use.
     * @param port The port to use
     */
    public void setHttpPort(int port) {
        Editor editor = mSharedPreferences.edit();
        editor.putString(KEY_HTTP_PORT, String.valueOf(port));
        editor.commit();
    }

    /**
     * Sets the port for the HTTPS server to use.
     * @param port The port to use
     */
    public void setHttpsPort(int port) {
        Editor editor = mSharedPreferences.edit();
        editor.putString(KEY_HTTPS_PORT, String.valueOf(port));
        editor.commit();
    }

    /** Enables the HTTP server. */
    public void setHttpEnabled(boolean enable) {
        Editor editor = mSharedPreferences.edit();
        editor.putBoolean(KEY_HTTP_ENABLED, enable);
        editor.commit();
    }

    /** Enables the HTTPS server. */
    public void setHttpsEnabled(boolean enable) {
        Editor editor = mSharedPreferences.edit();
        editor.putBoolean(KEY_HTTPS_ENABLED, enable);
        editor.commit();
    }

    /** Returns the port used by the HTTP server. */
    public int getHttpPort() {
        return mHttpPort;
    }

    /** Returns the port used by the HTTPS server. */
    public int getHttpsPort() {
        return mHttpsPort;
    }

    /** Indicates whether or not the HTTP server is enabled. */
    public boolean isHttpEnabled() {
        return mHttpEnabled;
    }

    /** Indicates whether or not the HTTPS server is enabled. */
    public boolean isHttpsEnabled() {
        return mHttpsEnabled;
    }

    /** Starts (or restart if needed) the HTTP server. */
    public void start() {

        // Stops the HTTP server if it has been disabled or if it needs to be restarted
        if ((!mHttpEnabled || mHttpUpdate) && mHttpRequestListener != null) {
            mHttpRequestListener.kill();
            mHttpRequestListener = null;
        }
        // Stops the HTTPS server if it has been disabled or if it needs to be restarted
        if ((!mHttpsEnabled || mHttpsUpdate) && mHttpsRequestListener != null) {
            mHttpsRequestListener.kill();
            mHttpsRequestListener = null;
        }
        // Starts the HTTP server if needed
        if (mHttpEnabled && mHttpRequestListener == null) {
            try {
                mHttpRequestListener = new HttpRequestListener(mHttpPort);
            } catch (Exception e) {
                mHttpRequestListener = null;
            }
        }
        // Starts the HTTPS server if needed
        if (mHttpsEnabled && mHttpsRequestListener == null) {
            try {
                mHttpsRequestListener = new HttpsRequestListener(mHttpsPort);
            } catch (Exception e) {
                mHttpsRequestListener = null;
            }
        }

        mHttpUpdate = false;
        mHttpsUpdate = false;

    }

    /** Stops the HTTP server and/or the HTTPS server but not the Android service. */
    public void stop() {
        if (mHttpRequestListener != null) {
            // Stops the HTTP server
            mHttpRequestListener.kill();
            mHttpRequestListener = null;
        }
        if (mHttpsRequestListener != null) {
            // Stops the HTTPS server
            mHttpsRequestListener.kill();
            mHttpsRequestListener = null;
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();

        mContext = getApplicationContext();
        mRegistry = new MHttpRequestHandlerRegistry();
        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

        mParams = new BasicHttpParams();
        mParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000)
                .setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024)
                .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
                .setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
                .setParameter(CoreProtocolPNames.ORIGIN_SERVER, "MajorKernelPanic HTTP Server");

        // Set up the HTTP protocol processor
        mHttpProcessor = new BasicHttpProcessor();
        mHttpProcessor.addInterceptor(new ResponseDate());
        mHttpProcessor.addInterceptor(new ResponseServer());
        mHttpProcessor.addInterceptor(new ResponseContent());
        mHttpProcessor.addInterceptor(new ResponseConnControl());

        // Will be used in the "Last-Modifed" entity-header field
        try {
            String packageName = mContext.getPackageName();
            mLastModified = new Date(mContext.getPackageManager().getPackageInfo(packageName, 0).lastUpdateTime);
        } catch (NameNotFoundException e) {
            mLastModified = new Date(0);
        }

        // Restores the state of the service 
        mHttpPort = Integer.parseInt(mSharedPreferences.getString(KEY_HTTP_PORT, String.valueOf(mHttpPort)));
        mHttpsPort = Integer.parseInt(mSharedPreferences.getString(KEY_HTTPS_PORT, String.valueOf(mHttpsPort)));
        mHttpEnabled = mSharedPreferences.getBoolean(KEY_HTTP_ENABLED, mHttpEnabled);
        mHttpsEnabled = mSharedPreferences.getBoolean(KEY_HTTPS_ENABLED, mHttpsEnabled);

        // If the configuration is modified, the server will adjust
        mSharedPreferences.registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener);

        // Loads plugins available in the package net.kseek.http
        for (int i = 0; i < MODULES.length; i++) {
            try {
                Class<?> pluginClass = Class
                        .forName(TinyHttpServer.class.getPackage().getName() + "." + MODULES[i]);
                Constructor<?> pluginConstructor = pluginClass.getConstructor(new Class[] { TinyHttpServer.class });
                addRequestHandler((String) pluginClass.getField("PATTERN").get(null),
                        (HttpRequestHandler) pluginConstructor.newInstance(this));
            } catch (ClassNotFoundException ignore) {
                // Module disabled
            } catch (Exception e) {
                Log.e(TAG, "Bad module: " + MODULES[i]);
                e.printStackTrace();
            }
        }

        start();

    }

    @Override
    public void onDestroy() {
        stop();
        mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //Log.d(TAG,"TinyServerHttp started !");
        return START_STICKY;
    }

    private OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {

            if (key.equals(KEY_HTTP_PORT)) {
                int port = Integer.parseInt(sharedPreferences.getString(KEY_HTTP_PORT, String.valueOf(mHttpPort)));
                if (port != mHttpPort) {
                    mHttpPort = port;
                    mHttpUpdate = true;
                    start();
                }
            }

            else if (key.equals(KEY_HTTPS_PORT)) {
                int port = Integer
                        .parseInt(sharedPreferences.getString(KEY_HTTPS_PORT, String.valueOf(mHttpsPort)));
                if (port != mHttpsPort) {
                    mHttpsPort = port;
                    mHttpsUpdate = true;
                    start();
                }
            }

            else if (key.equals(KEY_HTTPS_ENABLED)) {
                mHttpsEnabled = sharedPreferences.getBoolean(KEY_HTTPS_ENABLED, true);
                start();
            }

            else if (key.equals(KEY_HTTP_ENABLED)) {
                mHttpEnabled = sharedPreferences.getBoolean(KEY_HTTP_ENABLED, true);
                start();
            }
        }
    };

    /** The Binder you obtain when a connection with the Service is established. */
    public class LocalBinder extends Binder {
        public TinyHttpServer getService() {
            return TinyHttpServer.this;
        }
    }

    /** See {@link TinyHttpServer.LocalBinder}. */
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    private final IBinder mBinder = new LocalBinder();

    protected void postError(Exception exception, int id) {
        synchronized (mListeners) {
            if (mListeners.size() > 0) {
                for (CallbackListener cl : mListeners) {
                    cl.onError(this, exception, id);
                }
            }
        }
    }

    protected void postMessage(int id) {
        synchronized (mListeners) {
            if (mListeners.size() > 0) {
                for (CallbackListener cl : mListeners) {
                    cl.onMessage(this, id);
                }
            }
        }
    }

    protected class HttpRequestListener extends RequestListener {

        public HttpRequestListener(final int port) throws Exception {
            try {
                ServerSocket serverSocket = new ServerSocket(port);
                construct(serverSocket);
                Log.i(TAG, "HTTP server listening on port " + serverSocket.getLocalPort());
            } catch (BindException e) {
                postError(e, ERROR_HTTP_BIND_FAILED);
                throw e;
            }
        }

        protected void kill() {
            super.kill();
            Log.i(TAG, "HTTP server stopped !");
        }

    }

    protected class HttpsRequestListener extends RequestListener {

        private X509KeyManager mKeyManager = null;
        private char[] mPassword;
        private boolean mNotSupported = false;

        private final String mClasspath = TinyHttpServer.class.getPackage().getName() + ".ModSSL$X509KeyManager";

        public HttpsRequestListener(final int port) throws Exception {

            SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(TinyHttpServer.this);

            if (!settings.contains(KEY_PASSWORD)) {
                // Generates a password for the keystore
                // TODO: entropy of Math.random() ?
                String password = Integer.toString((int) (Math.random() * Integer.MAX_VALUE), 36);
                Editor editor = settings.edit();
                editor.putString(KEY_PASSWORD, password);
                editor.commit();
                mPassword = password.toCharArray();
                mContext.deleteFile(KEYSTORE_FILE_NAME);
            } else {
                mPassword = settings.getString(KEY_PASSWORD, "XX").toCharArray();
            }

            // We create the X509KeyManager through reflexion so that SSL support can easily be removed if not needed
            try {
                Class<?> X509KeyManager = Class.forName(mClasspath);
                Method loadFromKeyStore = X509KeyManager.getDeclaredMethod("loadFromKeyStore", InputStream.class,
                        char[].class);

                try {
                    InputStream is = mContext.openFileInput(KEYSTORE_FILE_NAME);
                    mKeyManager = (X509KeyManager) loadFromKeyStore.invoke(null, is, mPassword);
                } catch (FileNotFoundException e) {
                } catch (Exception e) {
                    Log.e(TAG, "Could not open keystore, a new one will be created...");
                    e.printStackTrace();
                }

                if (mKeyManager == null) {
                    Constructor<?> constructor = X509KeyManager
                            .getConstructor(new Class[] { char[].class, String.class });
                    mKeyManager = (javax.net.ssl.X509KeyManager) constructor.newInstance(mPassword, mCACommonName);
                }

                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(new KeyManager[] { mKeyManager }, null, null);
                SSLServerSocket serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory()
                        .createServerSocket(port);

                serverSocket.setUseClientMode(false);
                serverSocket.setEnableSessionCreation(true);
                serverSocket.setWantClientAuth(false);

                Log.d(TAG, "Protocol: " + sslContext.getProtocol());
                Log.d(TAG, "Provider: " + sslContext.getProvider());
                Log.d(TAG, "Cipher suites: " + arrToString(serverSocket.getEnabledCipherSuites()));
                Log.d(TAG, "Protocols enabled: " + arrToString(serverSocket.getEnabledProtocols()));

                serverSocket.setEnabledProtocols(new String[] { "TLSv1" });
                serverSocket.setEnabledCipherSuites(new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA" });
                Log.d(TAG, "Cipher suites: " + arrToString(serverSocket.getEnabledCipherSuites()));
                Log.d(TAG, "Protocols enabled: " + arrToString(serverSocket.getEnabledProtocols()));

                construct(serverSocket);
                Log.i(TAG, "HTTPS server listening on port " + serverSocket.getLocalPort());

            } catch (NoSuchMethodException e) {
                // HTTPS support disabled !
                Log.e(TAG, "HTTPS not supported !");
                postError(e, ERROR_HTTPS_NOT_SUPPORTED);
                throw e;
            } catch (BindException e) {
                postError(e, ERROR_HTTPS_BIND_FAILED);
                throw e;
            } catch (Exception e) {
                Log.e(TAG, "HTTPS server crashed !");
                e.printStackTrace();
                postError(e, ERROR_HTTPS_SERVER_CRASHED);
                throw e;
            }

        }

        private String arrToString(String[] list) {
            String str = "";
            for (int i = 0; i < list.length; i++) {
                str += list[i] + ";";
            }
            return str;
        }

        /** Stops the {@link TinyHttpServer.RequestListener} */
        protected void kill() {
            if (!mNotSupported) {
                super.kill();
                // Saves all the certificates generated by the our KeyManager in a keystore
                try {
                    Method saveToKeyStore = Class.forName(mClasspath).getDeclaredMethod("saveToKeyStore",
                            OutputStream.class, char[].class);
                    // Prevents concurrent write operation in the keystore  
                    OutputStream os = mContext.openFileOutput(KEYSTORE_FILE_NAME, Context.MODE_PRIVATE);
                    saveToKeyStore.invoke(mKeyManager, os, mPassword);
                } catch (NoSuchMethodException e) {
                    // HTTPS support disabled !
                    Log.e(TAG, "HTTPS not supported !");
                    postError(e, ERROR_HTTPS_NOT_SUPPORTED);
                } catch (Exception e) {
                    System.out.println("An error occured while saving the KeyStore");
                    e.printStackTrace();
                }
                Log.i(TAG, "HTTPS server stopped !");
            }
        }

    }

    private class RequestListener extends Thread {

        private ServerSocket mServerSocket;
        private final org.apache.http.protocol.HttpService mHttpService;

        protected RequestListener() throws Exception {

            mHttpService = new org.apache.http.protocol.HttpService(mHttpProcessor,
                    new DefaultConnectionReuseStrategy(), new DefaultHttpResponseFactory());
            mHttpService.setHandlerResolver(mRegistry);
            mHttpService.setParams(mParams);

        }

        protected void construct(ServerSocket serverSocket) {
            mServerSocket = serverSocket;
            start();
        }

        protected void kill() {
            try {
                mServerSocket.close();
            } catch (IOException ignore) {
            }
            try {
                this.join();
            } catch (InterruptedException ignore) {
            }
        }

        public void run() {
            while (!Thread.interrupted()) {
                try {
                    // Set up HTTP connection
                    Socket socket = this.mServerSocket.accept();
                    DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
                    Log.d(TAG, "Incoming connection from " + socket.getInetAddress());
                    conn.bind(socket, mParams);

                    // Start worker thread
                    Thread t = new WorkerThread(this.mHttpService, conn, socket);
                    t.setDaemon(true);
                    t.start();
                } catch (SocketException e) {
                    break;
                } catch (InterruptedIOException ex) {
                    Log.e(TAG, "Interrupted !");
                    break;
                } catch (IOException e) {
                    Log.d(TAG, "I/O error initialising connection thread: " + e.getMessage());
                    break;
                }
            }
        }
    }

    static class WorkerThread extends Thread {

        private final org.apache.http.protocol.HttpService httpservice;
        private final HttpServerConnection conn;
        private final Socket socket;

        public WorkerThread(final org.apache.http.protocol.HttpService httpservice, final HttpServerConnection conn,
                final Socket socket) {
            super();
            this.httpservice = httpservice;
            this.conn = conn;
            this.socket = socket;
        }

        public void run() {
            Log.d(TAG, "New connection thread");
            HttpContext context = new MHttpContext(socket);
            try {
                while (!Thread.interrupted() && this.conn.isOpen()) {
                    try {
                        this.httpservice.handleRequest(this.conn, context);
                    } catch (UnsupportedOperationException e) {
                        e.printStackTrace();
                        // shutdownOutput is not implemented by SSLSocket, and it is called in the implementation
                        // of org.apache.http.impl.SocketHttpServerConnection.close().
                    }
                }
            } catch (ConnectionClosedException e) {
                Log.d(TAG, "Client closed connection");
                e.printStackTrace();
            } catch (SocketTimeoutException e) {
                Log.d(TAG, "Socket timeout");
            } catch (IOException e) {
                Log.e(TAG, "I/O error: " + e.getMessage());
            } catch (HttpException e) {
                Log.e(TAG, "Unrecoverable HTTP protocol violation: " + e.getMessage());
            } finally {
                try {
                    OutputStream sockOutOStream = socket.getOutputStream();
                    sockOutOStream.write(new byte[0]);
                    sockOutOStream.flush();
                    socket.close();
                } catch (IOException e) {
                }
                try {
                    this.conn.shutdown();
                } catch (Exception ignore) {
                }
            }
        }
    }

    /** Little modification of BasicHttpContext to add access to the underlying tcp socket. */
    public static class MHttpContext extends BasicHttpContext {

        private Socket socket;

        public MHttpContext(Socket socket) {
            super(null);
            this.socket = socket;
        }

        /** Returns a reference to the underlying socket of the connection. */
        public Socket getSocket() {
            return socket;
        }

    }

    /**
     * A slightly modified version of org.apache.http.protocol.HttpRequestHandlerRegistry
     * that allows the registry to be modified while some threads may be using it.
     * Recent versions of this file are thread-safe, but Android seems to be using an old version. 
     */
    public static class MHttpRequestHandlerRegistry extends HttpRequestHandlerRegistry {

        private final UriPatternMatcher matcher;

        public MHttpRequestHandlerRegistry() {
            matcher = new UriPatternMatcher();
        }

        public synchronized void register(final String pattern, final HttpRequestHandler handler) {
            matcher.register(pattern, handler);
        }

        public synchronized void unregister(final String pattern) {
            matcher.unregister(pattern);
        }

        public synchronized void setHandlers(@SuppressWarnings("rawtypes") final Map map) {
            matcher.setHandlers(map);
        }

        public synchronized HttpRequestHandler lookup(final String requestURI) {
            // This is the only function that will often be called by threads of the HTTP server
            // and it seems like a rather small crtical section to me, so it should not slow things down much
            return (HttpRequestHandler) matcher.lookup(requestURI);
        }

    }

}