
net.sf.eBus.client.EServer Maven / Gradle / Ivy
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later
// version.
//
// This library 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 Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General
// Public License along with this library; if not, write to the
//
// Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330,
// Boston, MA
// 02111-1307 USA
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright (C) 2001 - 2008, 2011, 2015, 2016. Charles W. Rapp.
// All Rights Reserved.
//
package net.sf.eBus.client;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.config.AddressFilter;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.net.AsyncServerSocket;
import net.sf.eBus.net.ServerSocketListener;
import net.sf.eBus.util.logging.StatusReport;
import net.sf.eBus.util.logging.StatusReporter;
/**
* Accepts new connections to this eBus application. If an
* address filter is set, verifies that the accepted connection
* passes the filter.
*
* Only one {@code EServer} instance may be open at a
* time.
*
* Applications wanting to be notified when {@code EServer}
* accepts a new remote eBus application connection need to
* implement the {@link ESubscriber} interface and subscribe
* for the message key
* {@link ServerMessage#MESSAGE_KEY net.sf.eBus.client.ServerMessage:/eBus}.
* {@link ServerMessage} contains the remote address of the
* newly accepted TCP connection. Note: this is
* a local-only feed and cannot be remote accessed.
*
* @author Charles Rapp
*/
public final class EServer
implements ServerSocketListener,
EPublisher,
StatusReporter
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* TCP ports must be >= to 1.
*/
public static final int MIN_PORT = 1;
/**
* TCP ports must be <= 65,535.
*/
public static final int MAX_PORT = 65535;
/**
* Newly accepted eBus connections are reported using
* {@link ServerMessage} and the subject
* {@link AbstractEBusMessage#EBUS_SUBJECT "/eBus"}.
*/
public static final EMessageKey NEW_CONNECTION_KEY =
new EMessageKey(ServerMessage.class,
AbstractEBusMessage.EBUS_SUBJECT);
//-----------------------------------------------------------
// Statics.
//
/**
* eBus may have at most two server sockets open: one for
* accepting binary connections and one for XML connections.
*/
private static final ConcurrentMap sServers =
new ConcurrentHashMap<>();
/**
* The logging subsystem interface.
*/
private static final Logger sLogger =
Logger.getLogger(EServer.class.getName());
//-----------------------------------------------------------
// Locals.
//
/**
* The service is open on this TCP/IP port.
*/
private int mPort;
/**
* Accept connections only if there are in this filter.
* If null, then accept all connections.
*/
private final AddressFilter mPositiveFilter;
/**
* Contains the configuration information used for accepted
* socket connections.
*/
private final EConfigure.Service mConfiguration;
/**
* Set to true when the service has been opened.
*/
private boolean mIsOpen;
/**
* The actual service socket.
*/
private AsyncServerSocket mAsyncServer;
/**
* The instance creation timestamp.
*/
private final Date mCreated;
/**
* Tally up the number of connections accepted during the
* current status report period.
*/
private int mAcceptCount;
/**
* Publish new connection notifications on this feed.
*/
private IEPublishFeed mNewConnectionFeed;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a server socket that will listen on the specified
* port and create {@link ERemoteApp} objects. The parameters
* are used to configure the accept remote connections.
* @param config eBus service configuration.
*/
private EServer(final EConfigure.Service config)
{
mPort = -1;
mPositiveFilter = config.addressFilter();
mConfiguration = config;
mIsOpen = false;
mAsyncServer = null;
mCreated = new Date();
mAcceptCount = 0;
} // end of EServer(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// ServerSocketListener Interface Implementation
//
/**
* Create a {@link ERemoteApp} to handle this new
* connection if the connection passes the positive filter.
* If not, the accepted client connection is immediately
* closed.
* @param socket a {@code Socket} connection
* @param aserver an
* {@link net.sf.eBus.net.AsyncServerSocket}
*/
@Override
public void handleAccept(final SocketChannel socket,
final AsyncServerSocket aserver)
{
final InetSocketAddress iAddress =
(InetSocketAddress)
(socket.socket()).getRemoteSocketAddress();
++mAcceptCount;
// Accept the client connection if:
// 1. There is no filter in place.
// 2. The filter contains the client's inetAddress and port.
// 3. The filter contains the client's inetAddress and
// accepts any port.
if (mPositiveFilter != null &&
!mPositiveFilter.passes(iAddress))
{
sLogger.info(
String.format(
"Accepted unknown client connection %s; disconnecting.",
iAddress));
try
{
socket.close();
}
catch (IOException ioex)
{
// Ignore exception when closing.
}
}
else
{
sLogger.info(
String.format(
"Accepted client from %s.", iAddress));
// Tell the listeners about this new remote
// application connection.
if (mNewConnectionFeed.isFeedUp())
{
mNewConnectionFeed.publish(
new ServerMessage(iAddress, mPort));
}
ERemoteApp.openConnection(mPort,
socket,
mConfiguration);
}
return;
} // end of handleAccept(SocketChannel, AsyncServerSocket)
/**
* The eBus service has unexpectedly closed. Re-open it.
* @param jex an {@code Exception} value
* @param aserver an
* {@link net.sf.eBus.net.AsyncServerSocket}
*/
@Override
public void handleClose(final Throwable jex,
final AsyncServerSocket aserver)
{
final String message = jex.getMessage();
// Before doing anything else, drop the server socket
// reference and close the notification feed.
mAsyncServer = null;
mIsOpen = false;
mNewConnectionFeed.close();
sLogger.log(
Level.WARNING,
String.format(
"Service on port %d unexpectedly closed, %s.",
mPort,
((message == null || message.length() == 0) ?
"no reason given." :
message)),
jex);
return;
} // end of handleClose(Throwable, AsyncServerSocket)
//
// ServerSocketListener Interface Implementation
//-----------------------------------------------------------
//-----------------------------------------------------------
// StatusReporter Interface Implementation
//
/**
* Adds the eBus server connection status to the status
* report.
* @param report the logged status report.
*/
@Override
public void reportStatus(final PrintWriter report)
{
int acceptCount = mAcceptCount;
mAcceptCount = 0;
report.format(
"The eBus service is open on port %d.%n", mPort);
report.format(
" created on %1$tY-%1$tm-%1$td @ %1$tH:%1$tM:%1$tS.%1$tL%n",
mCreated);
report.format(" accepted %,d %s.%n",
acceptCount,
(acceptCount == 1 ?
"connection" :
"connections"));
return;
} // end of reportStatus(PrintWriter)
//
// end of StatusReporter Interface Implementation
//-----------------------------------------------------------
//-----------------------------------------------------------
// EPublisher Interface Implementation.
//
/**
* Updates the feed state. This method does nothing since the
* feed state is stored in the publish feed.
* @param pubState the new publisher feed state
* @param feed the feed state applies to this feed.
*/
@Override
public void publishStatus(final EFeedState pubState,
final IEPublishFeed feed)
{} // end of publishStatus(EFeedState, IEPublishFeed)
//
// end of EPublisher Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get methods.
//
/**
* Returns the TCP port on which this service is accepting
* connections.
* @return the TCP port on which this service is accepting
* connections.
*/
public int port()
{
return (mPort);
} // end of port()
/**
* Returns the eBus service configuration.
* @return eBus service configuration.
*/
public EConfigure.Service configuration()
{
return (mConfiguration);
} // end of configuration()
/**
* Returns {@code true} if this server is open and
* {@code false} otherwise.
* @return {@code true} if this server is open.
*/
public boolean isOpen()
{
return (mIsOpen);
} // end of isOpen()
/**
* Returns {@code true} if the singleton eBus service exists
* and is open; otherwise, returns {@code false}.
* @param port check if this service port is open.
* @return {@code true} if this eBus application has an open
* service.
*/
public static boolean isServiceOpen(final int port)
{
final EServer server = sServers.get(port);
return (server != null && server.mIsOpen);
} // end of isServiceOpen(int)
/**
* Returns the number of currently open eBus services.
* @return number of open eBus services.
*/
public static int serviceCount()
{
return (sServers.size());
} // end of serviceCount()
/**
* Returns a copy of the existing eBus service ports.
* @return open eBus service ports.
*/
public static Collection services()
{
final Collection retval = new ArrayList<>();
retval.addAll(sServers.keySet());
return (retval);
} // end of services()
/**
* Returns the eBus server instance associated with the given
TCP serverPort. May return {@code null} if there is no eBus
* server for {@code serverPort}.
* @param port TCP service serverPort.
* @return eBus server instance.
*/
public static EServer server(final int port)
{
return (sServers.get(port));
} // end of server(int)
//
// end of Get methods.
//-----------------------------------------------------------
/**
* Opens a service socket on the given port and accepting
* connections of the given type and from the specified
* hosts and ports. The accepted connections
* are configured as per the given service configuration.
* Returns the opened {@code EServer} instance.
*
* If
* {@link EConfigure.Service#addressFilter() address filter}
* contains an entry with a socket address but a port set to
* zero, that means the client connection may be bound to any
* port. If the port is > zero, then the client connection
* must be bound to the specific port. If the address filter
* is {@code null}, then all connections are accepted.
*
*
* Negative filters are not supported. A negative filter
* would accept all connections except those listed.
*
* @param config eBus service configuration.
* @return the opened server instance.
* @throws IllegalArgumentException if any of the given
* parameters is invalid.
* @exception IllegalStateException
* if service port is already open.
*/
public static EServer openServer(final EConfigure.Service config)
{
Objects.requireNonNull(config, "config is null");
final int port = config.port();
if (sServers.containsKey(port))
{
throw (
new IllegalStateException(
"service already open"));
}
final EServer retval = new EServer(config);
sServers.put(port, retval);
(StatusReport.getInstance()).register(retval);
if (!retval.open(port))
{
retval.close();
throw (
new IllegalStateException(
"service failed to open"));
}
return (retval);
} // end of openServer(Service)
/**
* Closes the specified service socket if open.
* @param port close the service on this service port.
*/
public static void closeServer(final int port)
{
if (sServers.containsKey(port))
{
final EServer server = sServers.remove(port);
(StatusReport.getInstance()).deregister(server);
server.close();
}
return;
} // end of closeServer(int)
/**
* Closes are currently open eBus servers.
*/
public static void closeAllServers()
{
final StatusReport report = StatusReport.getInstance();
sServers.values()
.stream()
.map((server) ->
{
report.deregister(server);
return server;
})
.forEachOrdered((server) ->
{
server.close();
});
sServers.clear();
return;
} // end of closeAllServers()
/**
* Creates and opens an eBus service for this application as
* per the {@link EConfigure eBus configuration}.
* @param config the eBus configuration.
* @throws IOException
* if the configured eBus service failed to open.
*/
public static void configure(final EConfigure config)
throws IOException
{
(config.services()).values()
.forEach(EServer::openServer);
return;
} // end of configure(EConfigure)
/**
* Performs the actual work of instantiating the eBus server
* and opening it on the specified port. Returns {@code true}
* if the eBus service is successfully opened.
* @param port a TCP service port.
* @return {@code true} if the eBus service is open.
*/
private boolean open(final int port)
{
// Open the service only if this service is
// supposed to be open.
if (mAsyncServer == null ||
!mAsyncServer.isOpen())
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"Opening service on port %d.",
port));
}
try
{
// No. Establish the service.
if (mAsyncServer == null)
{
final net.sf.eBus.net.AsyncServerSocket.ServerBuilder builder =
net.sf.eBus.net.AsyncServerSocket.builder();
mAsyncServer =
builder.selector(mConfiguration.serviceSelector())
.listener(this)
.build();
}
mAsyncServer.open(port);
mPort = port;
mIsOpen = true;
// Tell the world that this service is open.
if (sLogger.isLoggable(Level.INFO))
{
sLogger.info(
String.format(
"Service open on port %d.", port));
}
// Open the new connection notification feed.
// Open the new connection notification feed
// This is a local client/local-only feed.
mNewConnectionFeed =
EPublishFeed.open(this,
NEW_CONNECTION_KEY,
FeedScope.LOCAL_ONLY);
mNewConnectionFeed.advertise();
mNewConnectionFeed.updateFeedState(
EFeedState.UP);
}
catch (IOException ioex)
{
final String message = ioex.getMessage();
sLogger.log(
Level.WARNING,
String.format(
"Failed to open eBus service on port %d, %s.",
port,
((message == null || message.length() == 0) ?
"no reason given." :
message)),
ioex);
}
}
return (mIsOpen);
} // end of open(int, String)
/**
* Performs the actual work of closing the open eBus service.
*/
private void close()
{
final AsyncServerSocket asyncServer = mAsyncServer;
// Close the service only if this service is open.
if (mIsOpen &&
asyncServer != null &&
asyncServer.isOpen())
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"Closing service on port %d.", mPort));
}
mIsOpen = false;
mAsyncServer = null;
asyncServer.close();
// Close the new connection feed as well.
mNewConnectionFeed.close();
if (sLogger.isLoggable(Level.INFO))
{
sLogger.info(
String.format(
"Service closed on port %d.", mPort));
}
}
return;
} // end of close()
} // end of class EServer
//
// CHANGE LOG
// $Log: EServer.java,v $
// Revision 1.5 2008/05/08 23:28:18 charlesr
// Corrected for compiler warnings.
//
// Revision 1.4 2007/11/15 14:07:44 charlesr
// Updated for new message header.
//
// Revision 1.3 2006/10/01 18:10:33 charlesr
// Wait for client thread to start.
//
// Revision 1.2 2006/07/09 18:56:41 charlesr
// Moved from multi-threaded sockets to select-based sockets.
//
// Revision 1.1 2006/01/01 22:02:03 charlesr
// Initial revision
//