Java tutorial
/* $Id$ Copyright (C) 2009-2010 Virginia Tech. All rights reserved. SEE LICENSE FOR MORE INFORMATION Author: Middleware Services Email: middleware@vt.edu Version: $Revision$ Updated: $Date$ */ package edu.vt.middleware.gator.server; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import edu.vt.middleware.gator.ClientConfig; import edu.vt.middleware.gator.ConfigChangeListener; import edu.vt.middleware.gator.ConfigManager; import edu.vt.middleware.gator.ProjectConfig; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Log4j server that accepts connections from clients publishing log events via * the log4j SocketAppender. Based on the SocketServer class provided in the * log4j distribution, but does configuration via database instead of * filesystem. Additionally this class was designed with Spring configuration in * mind. * * @author Middleware Services * @version $Revision$ */ public class SocketServer implements ConfigChangeListener, SocketCloseListener, Runnable { /** Default port number to listen for connections on. */ public static final int DEFAULT_PORT = 8000; /** Default bind address is loopback address. */ public static final String DEFAULT_BIND_ADDRESS = "127.0.0.1"; /** Default maximum number of clients. */ public static final int DEFAULT_MAX_CLIENTS = 100; /** Number of ms to wait for async thread operations to stop. */ protected static final int STOP_TIMEOUT = 10000; /** Logger instance. */ protected final Log logger = LogFactory.getLog(getClass()); /** Project configuration manager. */ protected final ConfigManager configManager; /** Available logging engines. */ protected final Set<LoggingEngine> loggingEngines; /** Executor used to publish log events to registered listeners. */ protected final Executor eventExecutor; /** IP address/host name server will bind to. */ protected String bindAddress = DEFAULT_BIND_ADDRESS; /** Server bind/listing address. */ protected InetAddress inetBindAddress; /** Port server will listen on. */ protected int port = DEFAULT_PORT; /** Maximum number of clients that can connect to server. */ protected int maxClients = DEFAULT_MAX_CLIENTS; /** Flag indicating whether to start on initialization. */ protected boolean startOnInit; /** Accepts incoming socket connections. */ protected ServerSocket serverSocket; /** Runs the socket server. */ protected Thread socketServerThread; /** Defines behavior when clients are removed from projects. */ protected ClientRemovalPolicy clientRemovalPolicy = new NoopClientRemovalPolicy(); /** Executes logging event handlers for each connected client. */ protected LoggingEventHandlerExecutor handlerExecutor; /** Maps clients to the logging event handler that services its log events. */ private final Map<InetAddress, LoggingEventHandler> eventHandlerMap = new HashMap<InetAddress, LoggingEventHandler>(); /** This is initialized on application startup */ private final Date startTime = new Date(); /** * Creates a new socket server with the given required components. * * @param manager Configuration manager. * @param engines Set of available logging engines for handling log events. * @param executor Executor used to publish events to registered listeners. */ public SocketServer(final ConfigManager manager, final Set<LoggingEngine> engines, final Executor executor) { this.configManager = manager; this.loggingEngines = engines; this.eventExecutor = executor; } /** * Gets the address to which the server socket is bound. * * @return Bind address. */ public String getBindAddress() { return bindAddress; } /** * Sets the bind address on which incoming connections will be accepted. * * @param hostNameOrIP Host name or dotted IP address of bind address. */ public void setBindAddress(final String hostNameOrIP) { bindAddress = hostNameOrIP; } /** * Gets the port on which to listen for client connections. * * @return Listening port number. */ public int getPort() { return port; } /** * Sets the port on which to listen for client connections. * * @param n Listening port number. */ public void setPort(final int n) { port = n; } /** * Gets the maximum number of logging clients allowed by this server. * Default is {@link SocketServer#DEFAULT_MAX_CLIENTS}. * * @return Maximum number of allowed clients. */ public int getMaxClients() { return this.maxClients; } /** * Sets the maximum number of logging clients allowed by this server. * * @param max Maximum number of clients. */ public void setMaxClients(final int max) { this.maxClients = max; } /** * Gets the policy applied when clients are removed from a project. * Default is {@link NoopClientRemovalPolicy}. * * @return Client removal policy. */ public ClientRemovalPolicy getClientRemovalPolicy() { return clientRemovalPolicy; } /** * Sets the policy applied when clients are removed from a project. * * @param policy Client removal policy. */ public void setClientRemovalPolicy(final ClientRemovalPolicy policy) { clientRemovalPolicy = policy; } /** * Gets an immutable set of logging engines configured for this server. * * @return Immutable set of available logging engines. */ public Set<LoggingEngine> getLoggingEngines() { return Collections.unmodifiableSet(loggingEngines); } /** * Gets a collection of all registered logging event handlers. * * @return Immutable collection of logging event handlers. */ public Collection<LoggingEventHandler> getLoggingEventHandlers() { return Collections.unmodifiableCollection(eventHandlerMap.values()); } /** * Gets the logging event handler for the given client. * * @param hostNameOrIp Host name or IP address of client. * * @return Logging event handler for given client or null if no handler is * found for given client. */ public LoggingEventHandler getLoggingEventHandler(final String hostNameOrIp) { for (InetAddress address : eventHandlerMap.keySet()) { if (address.getHostName().equals(hostNameOrIp) || address.getHostAddress().equals(hostNameOrIp)) { return eventHandlerMap.get(address); } } return null; } /** * Gets first project to which the host possessing the given IP address is a * member. * * @param addr IP address. * * @return First project to which the client at the given IP address is a * member or null if client does not belong to project. */ public ProjectConfig getProject(final InetAddress addr) { ProjectConfig project = null; List<ProjectConfig> projects = configManager.findProjectsByClientName(addr.getHostName()); if (projects.size() > 0) { project = projects.get(0); } else { projects = configManager.findProjectsByClientName(addr.getHostAddress()); if (projects.size() > 0) { project = projects.get(0); } } return project; } /** * Set a flag indicating whether or not to start the server after * initialization via {@link #init()} is complete. * * @param init True to start after initialization, false otherwise. If * false, the server must be started by an explicit call to {@link #start()}. * The default is FALSE. */ public void setStartOnInit(boolean startOnInit) { this.startOnInit = startOnInit; } /** * @return Date/time of server startup. */ public Date getStartTime() { return this.startTime; } /** * Initializes the socket server so it can begin accepting connections from * remote hosts. If the {@link #startOnInit} flag is set, this method calls * {@link #start()}; otherwise the server must be started by calling {@link * #start()} explicitly. * * @throws Exception On initialization errors. */ public void init() throws Exception { try { inetBindAddress = InetAddress.getByName(bindAddress); } catch (UnknownHostException e) { throw new IllegalArgumentException("Unknow host " + bindAddress); } if (startOnInit) { start(); } } /** * Start the server and listen for incoming connections. * * @throws IOException On failure to bind to desired port. */ public void start() throws IOException { final String listenIP = inetBindAddress.getHostAddress(); logger.info("Starting SocketServer..."); logger.info(String.format("Listening on %s:%s", listenIP, port)); serverSocket = new ServerSocket(port, 0, inetBindAddress); socketServerThread = new Thread(this, String.format("gator-server-%s-%s", listenIP, port)); handlerExecutor = new LoggingEventHandlerExecutor(); socketServerThread.start(); logger.info("Socket server started successfully."); } /** * Stops the socket server from accepting incoming connections and cleans up * resources for handling logging events. */ public void stop() { // Multiple invocations of stop() are safe if (socketServerThread == null) { logger.info("Socket server is already stopped."); return; } logger.info("Stopping socket server..."); try { socketServerThread.join(STOP_TIMEOUT); } catch (InterruptedException e) { logger.warn("Times out waiting for socker server thread to finish."); } if (!serverSocket.isClosed()) { try { serverSocket.close(); } catch (IOException e) { logger.error("Error closing server socket.", e); } } handlerExecutor.shutdown(); eventHandlerMap.clear(); serverSocket = null; socketServerThread = null; logger.info("Socket server stopped."); } /** {@inheritDoc}. */ public void run() { while (serverSocket != null && serverSocket.isBound()) { logger.info("Waiting to accept a new client."); Socket socket = null; InetAddress inetAddress = null; try { socket = serverSocket.accept(); inetAddress = socket.getInetAddress(); // Validate newly-connected client if (eventHandlerMap.keySet().size() >= maxClients) { throw new UnauthorizedClientException(inetAddress, "Maximum number of clients exceeded."); } final ProjectConfig project = getProject(inetAddress); if (project == null) { throw new UnauthorizedClientException(inetAddress, "Client not registered with any projects."); } // Explicitly enable TCP keep alives to try to help reclaim resources // from dead clients socket.setKeepAlive(true); logger.info("Accepted connection from client " + inetAddress); logger.info("Configuring logger repository for " + inetAddress); final LoggingEngine engine = getLoggingEngine(project); if (engine != null) { engine.register(inetAddress, project); logger.info("Successfully configured " + engine); final LoggingEventHandler handler = new LoggingEventHandler(socket, engine, eventExecutor); handler.getSocketCloseListeners().add(this); eventHandlerMap.put(inetAddress, handler); handlerExecutor.execute(handler); } else { logger.warn(String.format("Logging engine not found for %s. Closing socket for %s.", project, inetAddress)); socket.close(); } } catch (UnauthorizedClientException e) { logger.warn(String.format("Unauthorized client %s rejected for reason: " + e.getMessage(), e.getClient())); if (socket != null && !socket.isClosed()) { logger.info("Closing socket for rejected host."); try { socket.close(); } catch (IOException ioex) { logger.error("Error closing client socket.", ioex); } } } catch (SocketException e) { // Check whether this is caused by a stop() invocation: // calling stop() closes server socket, which throws SocketException // from blocking accept() call if (serverSocket == null) { logger.info("Ignoring SocketException caused by stop() invocation."); } else { logger.error(e); } } catch (Exception e) { logger.error(e); } } } /** {@inheritDoc}. */ public synchronized void projectChanged(final Object sender, final ProjectConfig project) { logger.info(String.format("Got notice that %s has changed.", project)); try { logger.info("Updating configuration of " + project); getLoggingEngine(project).configure(project); } catch (ConfigurationException e) { logger.error("Error updating configuration of " + project, e); } } /** {@inheritDoc}. */ public synchronized void projectRemoved(final Object sender, final ProjectConfig project) { logger.info(String.format("Got notice that %s was removed.", project)); for (ClientConfig client : project.getClients()) { final LoggingEventHandler handler = getLoggingEventHandler(client.getName()); if (handler != null) { clientRemovalPolicy.clientRemoved(client.getName(), handler); } } } /** {@inheritDoc}. */ public synchronized void clientRemoved(final Object sender, final ProjectConfig project, final String clientName) { logger.info(String.format("Got notice that client %s was removed from %s.", clientName, project)); final LoggingEventHandler handler = getLoggingEventHandler(clientName); if (handler != null) { clientRemovalPolicy.clientRemoved(clientName, handler); } } /** {@inheritDoc}. */ public synchronized void socketClosed(final Object sender, final Socket socket) { logger.info("Got notification of closed socket " + socket); final InetAddress addr = socket.getInetAddress(); if (eventHandlerMap.containsKey(addr)) { logger.info(String.format("Cleaning up resources held by %s due to socket close.", addr)); eventHandlerMap.get(addr).shutdown(); eventHandlerMap.remove(addr); } } /** * Gets the {@link LoggingEngine} for the given project. * * @param project Project configuration. * * @return The {@link LoggingEngine} associated with the given project * or a new engine if none exists. */ protected LoggingEngine getLoggingEngine(final ProjectConfig project) { for (LoggingEngine e : loggingEngines) { if (e.getClass().getName().equals(project.getLoggingEngine())) { return e; } } return null; } }