All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neogroup.httpserver.HttpServer Maven / Gradle / Ivy

The newest version!

package org.neogroup.httpserver;

import org.neogroup.httpserver.contexts.HttpContext;
import org.neogroup.util.MimeUtils;

import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Http Server
 */
public class HttpServer {

    public static final String SERVER_NAME_PROPERTY_NAME = "serverName";
    public static final String PORT_PROPERTY_NAME = "port";
    public static final String LOGGING_ENABLED_PROPERTY_NAME = "loggingEnabled";
    public static final String CONNECTION_CHECKOUT_INTERVAL_PROPERTY_NAME = "connectionCheckoutInterval";
    public static final String CONNECTION_MAX_INACTIVE_INTERVAL_PROPERTY_NAME = "connectionMaxInactiveInterval";
    public static final String SESSION_NAME_PROPERTY_NAME = "sessionName";
    public static final String SESSION_USE_COOKIES_PROPERTY_NAME = "sessionUseCookies";
    public static final String SESSION_MAX_INACTIVE_INTERVAL_PROPERTY_NAME = "sessionMaxInactiveInterval";
    public static final String SESSION_CHECKOUT_INTERVAL_PROPERTY_NAME = "sessionCheckoutInterval";

    public static final int DEFAULT_PORT = 80;
    public static final boolean DEFAULT_LOGGING_ENABLED = false;
    public static final int DEFAULT_CONNECTION_MAX_INACTIVE_INTERVAL = 5000;
    public static final int DEFAULT_CONNECTION_CHECKOUT_INTERVAL = 12000;
    public static final String DEFAULT_SERVER_NAME = "NeoGroup-HttpServer";
    public static final String DEFAULT_SESSION_NAME = "sessionId";
    public static final int DEFAULT_SESSION_MAX_INACTIVE_INTERVAL = 300000;
    public static final int DEFAULT_SESSION_CHECKOUT_INTERVAL = 60000;
    public static final boolean DEFAULT_SESSION_USE_COOKIES = true;

    private static final String CONNECTION_CREATED_MESSAGE = "Connection \"{0}\" created !!";
    private static final String CONNECTION_DESTROYED_MESSAGE = "Connection \"{0}\" destroyed !!";
    private static final String CONNECTION_REQUEST_RECEIVED_MESSAGE = "Connection \"{0}\" received request \"{1}\"";

    private static final Map threadConnections;
    static {
        threadConnections = new HashMap<>();
    }

    private Selector selector;
    private ServerSocketChannel serverChannel;
    private Executor executor;
    private ServerHandler serverHandler;
    private ScheduledExecutorService timer;
    private Logger logger;
    private Properties properties;
    private boolean running;
    private final Set contexts;
    private final Set idleConnections;
    private final Set readyConnections;
    private final Map sessions;

    /**
     * Constructor for the http server
     */
    public HttpServer() {
        running = false;
        properties = new Properties();
        logger = Logger.getAnonymousLogger();
        executor = new Executor() {
            @Override
            public void execute(Runnable task) {
                task.run();
            }
        };
        serverHandler = new ServerHandler();
        timer = Executors.newSingleThreadScheduledExecutor();
        contexts = Collections.synchronizedSet(new HashSet());
        idleConnections = Collections.synchronizedSet (new HashSet());
        readyConnections = Collections.synchronizedSet (new HashSet());
        sessions = Collections.synchronizedMap(new HashMap());
    }

    /**
     * Get the http server properties
     * @return properties
     */
    public Properties getProperties() {
        return properties;
    }

    /**
     * Set the http server properties
     * @param properties
     */
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    /**
     * Set a property value
     * @param key name of property
     * @param value value of property
     */
    public void setProperty(String key, Object value) {
        properties.put(key, value);
    }

    /**
     * Get the value of a property
     * @param key name of property
     * @param  type of response
     * @return casted response
     */
    public  R getProperty (String key) {
        return (R)properties.get(key);
    }

    /**
     * Get the value of a property diven a default value
     * @param key name of property
     * @param defaultValue default value
     * @param  type of response
     * @return casted response
     */
    public  R getProperty (String key, R defaultValue) {
        R value = (R)properties.get(key);
        if (value == null) {
            value = defaultValue;
        }
        return value;
    }

    /**
     * Retrieves the logger of the server
     * @return Logger
     */
    public Logger getLogger() {
        return logger;
    }

    /**
     * Sets the logger of the server
     * @param logger logger
     */
    public void setLogger(Logger logger) {
        this.logger = logger;
    }

    /**
     * Log message of the server
     * @param level level
     * @param message message
     * @param arguments arguments
     */
    private void log (Level level, String message, Object ... arguments) {
        if (logger != null && getProperty(LOGGING_ENABLED_PROPERTY_NAME, DEFAULT_LOGGING_ENABLED)) {
            logger.log(level, MessageFormat.format(message, arguments));
        }
    }

    /**
     * Retrieves the thread executor for the http server
     * @return Thread executor
     */
    public Executor getExecutor() {
        return executor;
    }

    /**
     * Sets the thread executor for the http server
     * @param executor Thread executor
     */
    public void setExecutor(Executor executor) {
        this.executor = executor;
    }

    /**
     * Adds a new Http Context
     * @param context Context to add
     */
    public void addContext (HttpContext context) {
        contexts.add(context);
    }

    /**
     * Removes an http context
     * @param context Context to remove
     */
    public void removeContext (HttpContext context) {
        contexts.remove(context);
    }

    /**
     * Finds a context for the current request
     * @param request current reques
     * @return http context
     */
    public HttpContext findContext (HttpRequest request) {
        HttpContext matchContext = null;
        for (HttpContext context : contexts) {
            if (request.getPath().startsWith(context.getPath())) {
                matchContext = context;
                break;
            }
        }
        return matchContext;
    }

    /**
     * Gets the current thread active connection
     * @return The connection for the current thread
     */
    protected static HttpConnection getCurrentThreadConnection () {
        return threadConnections.get(Thread.currentThread().getId());
    }

    /**
     * Starts the http server
     */
    public void start() {

        try {
            selector = Selector.open();
            serverChannel = ServerSocketChannel.open();
            serverChannel.socket().bind(new InetSocketAddress(getProperty(PORT_PROPERTY_NAME, DEFAULT_PORT)));
            serverChannel.configureBlocking(false);
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);

            int connectionCheckoutInterval = getProperty(CONNECTION_CHECKOUT_INTERVAL_PROPERTY_NAME, DEFAULT_CONNECTION_CHECKOUT_INTERVAL);
            int sessionCheckoutInterval = getProperty(SESSION_CHECKOUT_INTERVAL_PROPERTY_NAME, DEFAULT_SESSION_CHECKOUT_INTERVAL);
            timer.scheduleAtFixedRate(new ConnectionsHandler(),connectionCheckoutInterval,connectionCheckoutInterval,TimeUnit.MILLISECONDS);
            timer.scheduleAtFixedRate(new SessionsHandler(),sessionCheckoutInterval,sessionCheckoutInterval,TimeUnit.MILLISECONDS);

        } catch (Exception ex) {
            throw new HttpException("Error creating server socket", ex);
        }

        Thread dispatcherThread = new Thread(serverHandler);
        running = true;
        dispatcherThread.start();
    }

    /**
     * Stops the http server
     */
    public void stop() {

        running = false;
        try {
            selector.close();
        } catch (Exception ex) {
        }
        try {
            serverChannel.close();
        } catch (Exception ex) {
        }
        timer.shutdownNow();
        selector = null;
        serverChannel = null;
    }

    /**
     * Creates a session for the given connection
     * @param connection connection
     * @return created http session
     */
    protected HttpSession createSession(HttpConnection connection) {
        HttpSession session = new HttpSession();
        session.setMaxInactiveInterval(getProperty(SESSION_MAX_INACTIVE_INTERVAL_PROPERTY_NAME, DEFAULT_SESSION_MAX_INACTIVE_INTERVAL));
        sessions.put(session.getId(), session);
        if (getProperty(SESSION_USE_COOKIES_PROPERTY_NAME, DEFAULT_SESSION_USE_COOKIES)) {
            HttpCookie cookie = new HttpCookie(getProperty(SESSION_NAME_PROPERTY_NAME, DEFAULT_SESSION_NAME), session.getId().toString());
            cookie.setPath("/");
            connection.getExchange().addCookie(cookie);
        }
        return session;
    }

    /**
     * Get a session from the given connection
     * @param connection connection
     * @return http session
     */
    protected HttpSession getSession(HttpConnection connection) {
        HttpSession session = null;
        UUID sessionId = getSessionId(connection);
        if (sessionId != null) {
            session = sessions.get(sessionId);
            if (session != null && session.isValid()) {
                long time = System.currentTimeMillis();
                if ((time - session.getLastActivityTimestamp()) > session.getMaxInactiveInterval()) {
                    session = null;
                }
                else {
                    session.checkSession();
                }
            }
        }
        return session;
    }

    /**
     * Obtains the session id from the connection
     * @param connection connection
     * @return id of session
     */
    protected UUID getSessionId (HttpConnection connection) {
        UUID sessionId = null;
        String sessionName = getProperty(SESSION_NAME_PROPERTY_NAME, DEFAULT_SESSION_NAME);
        if (getProperty(SESSION_USE_COOKIES_PROPERTY_NAME, DEFAULT_SESSION_USE_COOKIES)) {
            HttpCookie sessionCookie = connection.getExchange().getCookie(sessionName);
            if (sessionCookie != null && !sessionCookie.getValue().isEmpty()) {
                sessionId = UUID.fromString(sessionCookie.getValue());
            }
        }
        else {
            String sessionIdString = connection.getExchange().getRequestParameter(sessionName);
            if (sessionIdString != null && !sessionIdString.isEmpty()) {
                sessionId = UUID.fromString(sessionIdString);
            }
        }
        return sessionId;
    }

    /**
     * Server handler
     */
    private class ServerHandler implements Runnable {

        @Override
        public void run() {
            while (running) {
                try {
                    //Reconnect ready connections
                    synchronized (readyConnections) {
                        Iterator iterator = readyConnections.iterator();
                        while (iterator.hasNext()) {
                            HttpConnection connection = iterator.next();
                            try {
                                SocketChannel clientChannel = connection.getChannel();
                                SelectionKey clientReadKey = clientChannel.register(selector, SelectionKey.OP_READ);
                                clientReadKey.attach(connection);
                                iterator.remove();
                                idleConnections.add(connection);
                            }
                            catch (Exception ex) {}
                        }
                    }

                    selector.select(1000);
                    Iterator selectorIterator = selector.selectedKeys().iterator();
                    while (selectorIterator.hasNext()) {
                        SelectionKey key = selectorIterator.next();
                        selectorIterator.remove();
                        if (key.isValid()) {
                            try {
                                if (key.isAcceptable()) {
                                    SocketChannel clientChannel = serverChannel.accept();
                                    clientChannel.configureBlocking(false);
                                    SelectionKey clientReadKey = clientChannel.register(selector, SelectionKey.OP_READ);
                                    HttpConnection connection = new HttpConnection(HttpServer.this, clientChannel);
                                    clientReadKey.attach(connection);
                                    idleConnections.add(connection);
                                    log(Level.FINE, CONNECTION_CREATED_MESSAGE, connection);
                                }
                                else if (key.isReadable()) {
                                    HttpConnection connection = (HttpConnection) key.attachment();
                                    key.cancel();
                                    idleConnections.remove(connection);
                                    executor.execute(new ClientHandler(connection));
                                }
                            } catch (Exception ex) {}
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    /**
     * Client handler
     */
    private class ClientHandler implements Runnable {

        private final HttpConnection connection;

        public ClientHandler(HttpConnection connection) {
            this.connection = connection;
        }

        @Override
        public void run() {

            boolean closeConnection = true;

            threadConnections.put(Thread.currentThread().getId(), connection);
            try {
                try {
                    //Starts the http exchange
                    HttpExchange exchange = connection.createExchange();
                    log(Level.FINE, CONNECTION_REQUEST_RECEIVED_MESSAGE, connection, exchange.getRequestPath());

                    //Add general response headers
                    exchange.addResponseHeader(HttpHeader.SERVER, getProperty(SERVER_NAME_PROPERTY_NAME, DEFAULT_SERVER_NAME));
                    exchange.addResponseHeader(HttpHeader.DATE, HttpServerUtils.formatDate(new Date()));
                    String connectionHeader = exchange.getRequestHeader(HttpHeader.CONNECTION);
                    if (connectionHeader == null || connectionHeader.equals(HttpHeader.KEEP_ALIVE)) {
                        exchange.addResponseHeader(HttpHeader.CONNECTION, (HttpHeader.KEEP_ALIVE));
                        closeConnection = false;
                    } else {
                        exchange.addResponseHeader(HttpHeader.CONNECTION, (HttpHeader.CLOSE));
                    }

                    //Execute the context that matches the request
                    HttpRequest request = new HttpRequest(connection);
                    HttpContext matchContext = findContext(request);
                    if (matchContext != null) {
                        HttpResponse response = matchContext.onContext(request);
                        response.flush();
                    } else {
                        HttpResponse response = new HttpResponse(connection);
                        response.setResponseCode(HttpResponseCode.HTTP_NOT_FOUND);
                        response.addHeader(HttpHeader.CONTENT_TYPE, MimeUtils.TEXT_PLAIN);
                        response.setBody("No context found for request path \"" + request.getPath() + "\" !!");
                        response.flush();
                    }
                }
                catch (HttpBadRequestException badRequestException) {
                    HttpResponse response = new HttpResponse(connection);
                    response.setResponseCode(HttpResponseCode.HTTP_BAD_REQUEST);
                    response.addHeader(HttpHeader.CONTENT_TYPE, MimeUtils.TEXT_PLAIN);
                    response.setBody("Bad request !!");
                    response.flush();
                    closeConnection = true;
                }
                catch (HttpException httpException) {
                    HttpResponse response = new HttpResponse(connection);
                    response.setResponseCode(HttpResponseCode.HTTP_INTERNAL_ERROR);
                    response.addHeader(HttpHeader.CONTENT_TYPE, MimeUtils.TEXT_PLAIN);
                    response.setBody("Connection error !!");
                    response.flush();
                    closeConnection = true;
                }
                catch (Throwable exception) {
                    HttpResponse response = new HttpResponse(connection);
                    response.setResponseCode(HttpResponseCode.HTTP_INTERNAL_ERROR);
                    response.addHeader(HttpHeader.CONTENT_TYPE, MimeUtils.TEXT_PLAIN);
                    response.setBody("Internal error !!");
                    response.flush();
                }
            }
            catch (Throwable ex) {
                closeConnection = true;
            }
            finally {
                threadConnections.remove(Thread.currentThread().getId());
            }

            //Close a connection
            if (!connection.isClosed()) {
                if (closeConnection) {
                    connection.close();
                } else {
                    readyConnections.add(connection);
                    selector.wakeup();
                }
            }
        }
    }

    /**
     * Handler that manages all connections
     * Removes connections that are inactive
     */
    private class ConnectionsHandler implements Runnable {
        @Override
        public void run() {
            long time = System.currentTimeMillis();
            int maxIdleConnectionInterval = getProperty(CONNECTION_MAX_INACTIVE_INTERVAL_PROPERTY_NAME, DEFAULT_CONNECTION_MAX_INACTIVE_INTERVAL);
            synchronized (idleConnections) {
                Iterator iterator = idleConnections.iterator();
                while (iterator.hasNext()) {
                    HttpConnection connection = iterator.next();
                    if ((time - connection.getLastActivityTimestamp()) > maxIdleConnectionInterval) {
                        connection.close();
                        iterator.remove();
                        log(Level.FINE, CONNECTION_DESTROYED_MESSAGE, connection);
                    }
                }
            }
        }
    }

    /**
     * Handler that manages inactive sessions
     * Removes all session that are inactive
     */
    private class SessionsHandler implements Runnable {
        @Override
        public void run() {
            long time = System.currentTimeMillis();
            synchronized (sessions) {
                Iterator> iterator = sessions.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.Entry entry = iterator.next();
                    HttpSession session = entry.getValue();
                    if (!session.isValid() || (time - session.getLastActivityTimestamp()) > session.getMaxInactiveInterval()) {
                        iterator.remove();
                    }
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy