com.sector91.wit.server.WebServer.java Source code

Java tutorial

Introduction

Here is the source code for com.sector91.wit.server.WebServer.java

Source

// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //

////   ///   /// ///       
//////  ////   // ////  //// 
/// ////  ////  //  ////  //// 
///  //// /////  //        ///  
///  //// ///// //  //// ////// 
//   /// /////  //  ////  ////  
// //// ///// //  ////  ////   
/////////////  ////  ////   
////////////   ///   ///    
///// /////   ////  ////    
///// /////   //// ///// // 
////  ////    /// ///// //  
///// /////   ////////////   
////  ////     ////  ////    

// The Web framework with class.

// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //

// Copyright (c) 2013 Adam R. Nelson
//
// 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.

package com.sector91.wit.server;

import java.io.IOException;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

import org.simpleframework.http.Method;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import org.simpleframework.http.Status;
import org.simpleframework.http.core.Container;
import org.simpleframework.http.core.ContainerServer;
import org.simpleframework.transport.Server;
import org.simpleframework.transport.connect.Connection;
import org.simpleframework.transport.connect.SocketConnection;

import com.esotericsoftware.minlog.Log;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.net.HttpHeaders;
import com.sector91.wit.ErrorResponder;
import com.sector91.wit.WebApp;
import com.sector91.wit.WebApps;
import com.sector91.wit.http.HttpException;
import com.sector91.wit.params.Arg.Nil;
import com.sector91.wit.routes.Options;
import com.sector91.wit.routes.RouteBuilder;
import com.sector91.wit.routes.RouteConfigurationException;
import com.sector91.wit.routes.RouteHandler;
import com.sector91.wit.routes.Routes;
import com.sector91.wit.server.ServerConfig.WebAppEntry;
import com.sector91.wit.server.events.AppCreatedEvent;
import com.sector91.wit.server.events.AppDeletedEvent;
import com.sector91.wit.server.events.AppModifiedEvent;
import com.sector91.wit.server.events.KillServerEvent;

public class WebServer implements Runnable, Container {

    public static final String TAG = WebServer.class.getSimpleName();
    public static final String ACCESS_TAG = "Access";

    private final EventBus bus;

    final List<WebAppInstance> apps = new CopyOnWriteArrayList<>();
    private final Map<String, WebAppInstance> appsById = new ConcurrentHashMap<>();

    private volatile boolean started = false;
    private volatile boolean stopped = false;

    private final int port;
    final int threads;

    final ErrorResponder responderFor404;

    final Executor executor;
    private final CountDownLatch startLatch = new CountDownLatch(1), stopLatch = new CountDownLatch(1),
            shutdownLatch = new CountDownLatch(1);

    public WebServer(ServerConfig config) {
        this.port = config.httpPort;
        this.threads = config.threads;
        this.responderFor404 = config.responderFor404;
        this.bus = config.bus;
        bus.register(this);
        if (config.threads == 0) {
            this.executor = Executors.newCachedThreadPool();
        } else {
            this.executor = Executors.newFixedThreadPool(config.threads);
        }
        for (WebAppEntry entry : config.apps) {
            try {
                installWebApp(entry.app.getClass().getSimpleName(),
                        entry.prefix.isPresent()
                                ? Iterables.toArray(Splitter.on('/').omitEmptyStrings().split(entry.prefix.get()),
                                        String.class)
                                : new String[0],
                        entry.app);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
        Log.setLogger(config.logConfig);
    }

    @Subscribe
    public void onKillServer(KillServerEvent event) {
        requestStop();
    }

    @Subscribe
    public void onAppCreated(AppCreatedEvent event) {
        try {
            Log.trace(TAG, "Received AppCreatedEvent for app '" + event.appId() + "'.");
            installWebApp(event.appId(), event.prefixes(), event.app());
        } catch (InterruptedException ex) {
            Log.warn(TAG, "Interrupted while installing app '" + event.appId() + "'.");
            Thread.currentThread().interrupt();
        }
    }

    @Subscribe
    public void onAppModified(AppModifiedEvent event) {
        try {
            Log.trace(TAG, "Received AppModifiedEvent for app '" + event.appId() + "'.");
            installWebApp(event.appId(), event.prefixes(), event.app());
        } catch (InterruptedException ex) {
            Log.warn(TAG, "Interrupted while installing app '" + event.appId() + "'.");
            Thread.currentThread().interrupt();
        }
    }

    @Subscribe
    public void onAppDeleted(AppDeletedEvent event) {
        try {
            Log.trace(TAG, "Received AppDeletedEvent for app '" + event.appId() + "'.");
            removeWebApp(event.appId());
        } catch (InterruptedException ex) {
            Log.warn(TAG, "Interrupted while removing app '" + event.appId() + "'.");
            Thread.currentThread().interrupt();
        }
    }

    public synchronized void installWebApp(String id, String[] prefixes, WebApp app) throws InterruptedException {
        WebAppInstance instance = appsById.get(id);
        if (instance == null) {
            instance = new WebAppInstance(id, prefixes);
            if (started)
                instance.init();
            appsById.put(id, instance);
            apps.add(instance);
        } else {
            instance.stop();
        }
        instance.app(app);
        if (started)
            instance.start();
    }

    public synchronized boolean removeWebApp(String id) throws InterruptedException {
        if (appsById.containsKey(id)) {
            final WebAppInstance instance = appsById.get(id);
            instance.stop();
            appsById.remove(id);
            apps.remove(instance);
            instance.kill();
            return true;
        }
        return false;
    }

    @Override
    public void run() {
        synchronized (this) {
            if (started)
                throw new IllegalStateException("A WebServer instance can only be run once.");
            started = true;
            for (WebAppInstance instance : apps)
                instance.init();
        }
        Log.info(TAG, "Starting server...");

        try {
            Server server = new ContainerServer(this);
            ConnectionKillerThread shutdownHook = null;
            try (Connection connection = new SocketConnection(server)) {
                shutdownHook = new ConnectionKillerThread(connection);
                Runtime.getRuntime().addShutdownHook(shutdownHook);
                SocketAddress address = new InetSocketAddress(port);
                connection.connect(address);
                Log.info(TAG, "Server listening on port " + port + ".");
                synchronized (this) {
                    for (WebAppInstance instance : apps)
                        instance.start();
                }
                startLatch.countDown();
                stopLatch.await();
            } finally {
                if (shutdownHook != null)
                    Runtime.getRuntime().removeShutdownHook(shutdownHook);
            }
        } catch (IOException | InterruptedException ex) {
            Log.error(TAG, ex);
        } finally {
            startLatch.countDown();
            synchronized (this) {
                for (WebAppInstance instance : apps) {
                    try {
                        instance.stop();
                    } catch (Exception ex) {
                        Log.error(TAG, ex);
                    }
                }
            }
            shutdownLatch.countDown();
        }
    }

    public Thread start() throws InterruptedException {
        final Thread t = new Thread(this);
        t.start();
        waitUntilFullyStarted();
        return t;
    }

    public void requestStop() {
        if (!stopped) {
            stopped = true;
            Log.info("WebServer", "Stopping server...");
            stopLatch.countDown();
        }
    }

    public void stop() throws InterruptedException {
        if (!stopped) {
            requestStop();
            shutdownLatch.await();
        }
    }

    public void waitUntilFullyStarted() throws InterruptedException {
        startLatch.await();
    }

    @Override
    public void handle(Request request, Response response) {
        Task task = new Task(request, response);
        executor.execute(task);
    }

    private static class ConnectionKillerThread extends Thread {

        private final Connection connection;

        ConnectionKillerThread(Connection conn) {
            this.connection = conn;
        }

        @Override
        public void run() {
            System.err.println("WARNING: A connection is still open. This means the server was not shut"
                    + " down properly. Attempting to close connection...");
            try {
                connection.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    private class Task implements Runnable {
        private final Request request;
        private final Response response;

        Task(Request request, Response response) {
            this.request = request;
            this.response = response;
        }

        @Override
        public void run() {
            try {
                for (WebAppInstance app : apps) {
                    if (app.processRequest(request, response)) {
                        Log.info(ACCESS_TAG,
                                request.getMethod() + " " + request.getPath() + " ("
                                        + request.getClientAddress().getHostString() + ") -> "
                                        + response.getStatus().code + " " + response.getStatus().description);
                        return;
                    }
                }
                // If no app responded to the request, a 404 error occurred.
                responderFor404.respond(new HttpException(Status.NOT_FOUND), request, response);
                Log.info("Access", request.getMethod() + " " + request.getPath() + " ("
                        + request.getClientAddress().getHostString() + ") -> 404 Not Found");
            } catch (IOException ex) {
                Log.error("WebServer", "I/O error occurred.", ex);
            } catch (InterruptedException ex) {
                Log.warn("WebServer", "Responder thread interrupted.");
                Thread.currentThread().interrupt();
            }
        }
    }

    private class WebAppInstance {
        private final String id;
        private final String[] prefixes;
        private WebApp app = null;
        private Semaphore lock = null;
        private volatile boolean active = false;
        private RouteHandler[] getRoutes = new RouteHandler[0], postRoutes = new RouteHandler[0],
                putRoutes = new RouteHandler[0], deleteRoutes = new RouteHandler[0];

        WebAppInstance(String id, String[] prefixes) {
            this.id = id;
            this.prefixes = prefixes;
        }

        String id() {
            return id;
        }

        void init() {
            lock = new Semaphore(threads);
            lock.acquireUninterruptibly(threads);
        }

        boolean processRequest(Request request, Response response) throws IOException, InterruptedException {
            // Check if the path prefix (if any) matches.
            String[] path = request.getPath().getSegments();
            if (path.length < prefixes.length)
                return false;
            for (int i = 0; i < prefixes.length; i++) {
                if (!path[i].equals(prefixes[i]))
                    return false;
            }
            if (prefixes.length > 0) {
                final String[] restOfPath = new String[path.length - prefixes.length];
                System.arraycopy(path, prefixes.length, restOfPath, 0, restOfPath.length);
                path = restOfPath;
            }

            // Get a semaphore permit.
            lock.acquire();
            try {
                // Get the list of routes, based on the request method.
                final RouteHandler[] routes;
                switch (request.getMethod()) {
                case Method.GET:
                case Method.HEAD:
                    routes = getRoutes;
                    break;
                case Method.POST:
                    routes = postRoutes;
                    break;
                case Method.PUT:
                    routes = putRoutes;
                    break;
                case Method.DELETE:
                    routes = deleteRoutes;
                    break;
                default:
                    // If the method is not valid, respond with a 405 error.
                    response.setStatus(Status.METHOD_NOT_ALLOWED);
                    response.setContentType("text.plain");
                    final String msg = "Unsupported HTTP method : " + request.getMethod();
                    response.setContentLength(msg.getBytes().length);
                    response.setDate(HttpHeaders.DATE, System.currentTimeMillis());
                    try (PrintStream out = response.getPrintStream()) {
                        out.print(msg);
                    }
                    return true;
                }

                request.getAttributes().put(WebApp.class, app.getClass());

                // Look for a route that matches the request.
                for (RouteHandler route : routes) {
                    if (route.handle(path, request, response))
                        return true;
                }
                return false;
            } finally {
                lock.release();
            }
        }

        synchronized void app(WebApp app) {
            if (active)
                throw new IllegalStateException("Cannot replace a running app.");
            this.app = app;
            final Routes routes = app.routes();
            final List<RouteHandler> handlers = routes.compile(Optional.<RouteBuilder<Nil>>absent(),
                    Options.DEFAULTS), get = new ArrayList<>(), post = new ArrayList<>(), put = new ArrayList<>(),
                    delete = new ArrayList<>();
            for (RouteHandler handler : handlers) {
                switch (handler.method()) {
                case Method.GET:
                    get.add(handler);
                    break;
                case Method.POST:
                    post.add(handler);
                    break;
                case Method.PUT:
                    put.add(handler);
                    break;
                case Method.DELETE:
                    delete.add(handler);
                    break;
                default:
                    throw new RouteConfigurationException("Unsupported HTTP method: " + handler.method());
                }
            }
            getRoutes = get.toArray(new RouteHandler[get.size()]);
            postRoutes = post.toArray(new RouteHandler[post.size()]);
            putRoutes = put.toArray(new RouteHandler[put.size()]);
            deleteRoutes = delete.toArray(new RouteHandler[delete.size()]);
        }

        synchronized void kill() {
            active = false;
            app = null;
            getRoutes = new RouteHandler[0];
            postRoutes = new RouteHandler[0];
            putRoutes = new RouteHandler[0];
            deleteRoutes = new RouteHandler[0];
            WebApps.unregister(app.getClass());
            lock.release(threads);
        }

        synchronized boolean start() {
            if (app == null || active)
                return false;
            WebApps.register(app);
            Log.info(app.getClass().getSimpleName(), "Starting app...");
            try {
                app.start();
            } catch (Exception ex) {
                Log.error(app.getClass().getSimpleName(), "Exception occurred in start().", ex);
                return false;
            }
            active = true;
            lock.release(threads);
            return true;
        }

        synchronized void stop() throws InterruptedException {
            if (!active)
                return;
            Log.info(app.getClass().getSimpleName(), "Stopping app...");
            lock.acquire(threads);
            try {
                app.stop();
            } catch (Exception ex) {
                Log.error(app.getClass().getSimpleName(), "Exception occurred in stop().", ex);
            }
            active = false;
            WebApps.unregister(app.getClass());
        }
    }
}