Java tutorial
/* * Leaves Framework * Copyright (C) 2015 Hugo Miard * * This program 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 2 of the License, or * (at your option) any later version. * * This program 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 program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package com.hmiard.leaves.webserver.Server; import com.hmiard.leaves.framework.IO.Files.RootWriter; import com.hmiard.leaves.framework.IO.ResourceHolder; import com.hmiard.leaves.framework.LeavesApplication; import com.hmiard.leaves.framework.Objects.Application.Leaf; import com.hmiard.leaves.framework.Objects.Application.LeavesConfiguration; import com.hmiard.leaves.framework.Objects.Exceptions.BadUriException; import com.hmiard.leaves.framework.Objects.Exceptions.MissingBudException; import com.hmiard.leaves.framework.Objects.Exceptions.UriMissmatchException; import com.hmiard.leaves.framework.Objects.ObjectModel; import com.hmiard.leaves.framework.Utils.Html.HtmlGenerator; import com.hmiard.leaves.framework.Utils.Persistence.HibernateUtils; import com.hmiard.leaves.webserver.Http.Exceptions.ReservedRouteException; import com.hmiard.leaves.webserver.Http.GenericSession; import com.hmiard.leaves.webserver.Http.LeavesResponse; import com.hmiard.leaves.webserver.Http.LeavesSession; import com.hmiard.leaves.webserver.IO.TempFileManager; import com.hmiard.leaves.webserver.Leaves; import com.hmiard.leaves.webserver.Server.Routing.LRoute; import com.hmiard.leaves.webserver.Server.Routing.Router; import com.hmiard.leaves.webserver.Server.Threading.LeavesMainThread; import com.hmiard.leaves.webserver.Server.Threading.LeavesThreadRunner; import com.hmiard.leaves.webserver.Utils.FileUtils; import com.hmiard.leaves.webserver.Utils.UriUtils; import org.joda.time.DateTime; import org.w3c.dom.Element; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.HashSet; /** * Encapsulates the Leaves server and threading strategy. */ public abstract class LeavesServer { /** * HTTP Keep-alive timeout (in milliseconds). */ public static final int SOCKET_READ_TIMEOUT = 5000; /** * Leaves session storage timeout, in seconds. */ public static long SESSION_TIMEOUT = 10000; /** * Path of the directory that wraps resources. */ protected final String resourcesDir; protected final String hostname; protected final int port; protected HashSet<Socket> connections = new HashSet<>(); public LeavesConfiguration configuration; public ResourceHolder resourceHolder; public TempFileManager fileManager; public LeavesThreadRunner threadRunner; public ServerSocket serverSocket; public LeavesWebSocketServer webSocketServer = null; public Router router; public ObjectModel objectModel; private LeavesMainThread mainThread = null; private boolean redirect; private String redirectionUri; public LeavesServer(LeavesConfiguration configuration) { this(configuration, ""); } public LeavesServer(LeavesConfiguration configuration, String hostname) { LeavesApplication.logo(); this.objectModel = new ObjectModel(); this.configuration = configuration; this.resourcesDir = configuration.RESOURCES_DIRECTORY.getPath(); this.hostname = hostname; this.port = configuration.SERVER_PORT; this.resourceHolder = new ResourceHolder(this); if (this.resourceHolder.isBloated()) this.stop(); this.fileManager = new TempFileManager(); this.threadRunner = new LeavesThreadRunner(this); this.router = new Router(this); this.resetRedirection(); } /** * Core server logic. * * @param session LeavesSession * @return LeavesResponse */ public LeavesResponse serve(LeavesSession session) { GenericSession.Method method = session.getMethod(); HashMap<String, String> files = new HashMap<>(); LeavesResponse response; if (method.equals(GenericSession.Method.GET) || method.equals(GenericSession.Method.POST)) { try { session.parseBody(files); } catch (LeavesResponse.ResponseException e) { Leaves.say("ERROR : " + e.getMessage()); e.printStackTrace(); return new LeavesResponse(e.getStatus(), LeavesResponse.CommonMimeTypes.PLAINTEXT, e.getMessage()); } catch (IOException e) { Leaves.say("ERROR : " + e.getMessage()); e.printStackTrace(); return new LeavesResponse(LeavesResponse.Status.INTERNAL_ERROR, LeavesResponse.CommonMimeTypes.PLAINTEXT, "SERVER INTERNAL ERROR : " + e.getMessage()); } } try { response = this.router.resolve(session); session.printRequestResult(response); } catch (Exception e) { Leaves.say("ERROR : " + e.getMessage()); if (e instanceof MissingBudException || e instanceof BadUriException || e instanceof Router.RouteNotFoundException) response = new LeavesResponse(LeavesResponse.Status.NOT_FOUND, LeavesResponse.CommonMimeTypes.HTML, get404Leaf(session.getUri())); else response = new LeavesResponse(LeavesResponse.Status.INTERNAL_ERROR, LeavesResponse.CommonMimeTypes.HTML, getExceptionLeaf(session.getUri(), e)); } if (redirect) { response.setAs301Redirection(redirectionUri); resetRedirection(); } return response; } /** * Starting the server. * If the socket is already in use, this will throw and IOExeception. * * @throws IOException */ protected void start() throws IOException { this.serverSocket = new ServerSocket(); this.serverSocket .bind((hostname.equals("")) ? new InetSocketAddress(port) : new InetSocketAddress(hostname, port)); this.mainThread = new LeavesMainThread(this); this.mainThread.start(); if (configuration.WEBSOCKETS_ACTIVE) { this.webSocketServer = new LeavesWebSocketServer(configuration.WEBSOCKETS_PORT, this.objectModel); this.webSocketServer.start(); } } /** * Stopping the server. */ public void stop() { try { if (this.serverSocket != null && !this.serverSocket.isClosed()) FileUtils.close(this.serverSocket); closeAllConnections(); HibernateUtils.shutdown(); if (this.mainThread != null) this.mainThread.join(); if (this.webSocketServer != null) this.webSocketServer.stop(); } catch (InterruptedException | IOException e) { e.printStackTrace(); } } /** * Injecting routes and static resources in the current server, * from another one. * * @param newServer the server that will be copied. */ protected void inject(LeavesServer newServer) { HashMap<String, LRoute> routes = newServer.router.allRoutes(); try { for (String k : routes.keySet()) this.router.registerRoutes(routes.get(k)); for (String uri : newServer.resourceHolder.getResourcesUris()) { ByteArrayInputStream res = newServer.resourceHolder.getResource(uri); this.resourceHolder.pushResource(res, uri); // Closing the old stream, since we've created a new one. newServer.resourceHolder.close(uri); } } catch (ReservedRouteException | IOException e) { e.printStackTrace(); } } /** * Preparing a redirection. * * @param to String */ public final void redirect(String to) { this.redirect = true; this.redirectionUri = (to.startsWith("/")) ? to : "/" + to; } private void resetRedirection() { this.redirect = false; this.redirectionUri = "/"; } /** * Utility debug. * * @param something String to debug. */ public static void say(String something) { DateTime date = new DateTime(); System.out.println(date.toString() + " > Leaves Server : " + something); } /** * Pushing a new connection to the connections set. * * @param connection Socket */ public synchronized void pushConnection(Socket connection) { this.connections.add(connection); } /** * Closing and removing an active connection from the set. * * @param connection Socket */ public synchronized void removeConnection(Socket connection) { if (!connection.isClosed()) FileUtils.close(connection); this.connections.remove(connection); } public synchronized void closeAllConnections() { try { this.connections.forEach(this::removeConnection); } catch (ConcurrentModificationException ignored) { } } /** * Trying to find a leaf named "404" for 404 page customisation. * If no such leaf is found, a default page is returned. * * @param uri Session uri * @return String */ public String get404Leaf(String uri) { try { String pre = UriUtils.getPrefixBranch(uri); Leaf leaf404 = (pre.isEmpty()) ? objectModel.findLeaf("lll404") : objectModel.findLeaf(pre + ".lll404"); return leaf404.export(); } catch (Exception e) { return RootWriter.convert(HtmlGenerator.generateDefault404Page("ERROR : 404, page not found.")); } } /** * Trying to find a leaf named "exception" to display an exception. * If no such leaf is found, a default page is returned. * * @param uri Session uri * @param ex Exception * @return String */ public String getExceptionLeaf(String uri, Exception ex) { try { Leaf exceptionLeaf = objectModel.findLeaf("exception"); Element e = exceptionLeaf.getElementById("error_title"); if (e != null) e.setTextContent("Whoops ! " + ex.getClass().getSimpleName()); e = exceptionLeaf.getElementById("error_message"); if (e != null) e.setTextContent(ex.getMessage()); return exceptionLeaf.export(); } catch (Exception e) { return RootWriter.convert(HtmlGenerator.generateDefaultExceptionPage(ex)); } } public String getResourcesDir() { return resourcesDir; } public String getHostname() { return hostname; } public int getPort() { return port; } }