Java tutorial
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ // //// /// /// /// ////// //// // //// //// /// //// //// // //// //// /// //// ///// // /// /// //// ///// // //// ////// // /// ///// // //// //// // //// ///// // //// //// ///////////// //// //// //////////// /// /// ///// ///// //// //// ///// ///// //// ///// // //// //// /// ///// // ///// ///// //////////// //// //// //// //// // 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()); } } }