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

net.sf.eBus.client.ERemoteApp 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 2011 - 2016. Charles W. Rapp
// All Rights Reserved.
//

package net.sf.eBus.client;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.nio.BufferOverflowException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Formatter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.ConnectionMessage.ConnectionState;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.ERequestFeed.RequestState;
import net.sf.eBus.client.sysmessages.AdMessage;
import net.sf.eBus.client.sysmessages.CancelRequest;
import net.sf.eBus.client.sysmessages.FeedStatusMessage;
import net.sf.eBus.client.sysmessages.KeyMessage;
import net.sf.eBus.client.sysmessages.LogoffMessage;
import net.sf.eBus.client.sysmessages.LogonCompleteMessage;
import net.sf.eBus.client.sysmessages.LogonMessage;
import net.sf.eBus.client.sysmessages.LogonReply;
import net.sf.eBus.client.sysmessages.RemoteAck;
import net.sf.eBus.client.sysmessages.SubscribeMessage;
import net.sf.eBus.client.sysmessages.SystemMessageType;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageHeader;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.EReplyMessage.ReplyStatus;
import net.sf.eBus.messages.ERequestMessage;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.messages.type.MessageType;
import net.sf.eBus.util.logging.StatusReport;
import net.sf.eBus.util.logging.StatusReporter;

/**
 * This class encapsulates a connection between eBus
 * applications. A {@code ERemoteApp} instance is created by
 * either:
 * 
    *
  • * {@link #openConnection(EConfigure.RemoteConnection) openConnection}: * Establishes a connection from this process to a remote * application at the specified Internet address. *
  • *
  • * {@link EServer}: Accepts a connection from a remote * application and encapsulates the accepted * {@link java.nio.channels.SocketChannel socket}. *
  • *
* The {@code ERemoteApp} instance is responsible for forwarding * advertisements, subscriptions, requests and replies to the * remote application. *

* An application can track {@code ERemoteApp} events for all * connections by subscribing to the message key * {@link #CONNECTION_UPDATE_KEY net.sf.eBus.client.ConnectionMessage:/eBus}. * The {@link ConnectionMessage} contains the remote eBus * {@link java.net.InetSocketAddress socket address} and whether * the remote eBus is * {@link ConnectionMessage.ConnectionState#LOGGED_ON logged on} * or * {@link ConnectionMessage.ConnectionState#LOGGED_OFF logged off}. * If logged off, an optional reason is provided for why the * eBus logged off. Unsubscribe to stop receiving these updates. * Note: this message key is published locally and cannot be * accessed by remote eBus applications. *

* Only one connection may exist between any two eBus processes, * independent of which process established the first connection. * This means that an eBus process may not connect to * itself. * * @author Charles Rapp */ public final class ERemoteApp implements EPublisher, ESubscriber, EReplier, ERequestor { //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Constants. // /** * Used to specify that the feed identifier is not set. */ public static final int NO_ID = -1; /** * eBus connection updates are reported using * {@link ConnectionMessage} and the subject * {@link AbstractEBusMessage#EBUS_SUBJECT "/eBus"}. */ public static final EMessageKey CONNECTION_UPDATE_KEY = ConnectionMessage.MESSAGE_KEY; /** * When a remote eBus disconnects by sending a * {@link net.sf.eBus.client.sysmessages.LogoffMessage logoff message}, * then the * {@link ConnectionMessage#reason reason} is set to * "logged off". */ public static final String NORMAL_LOGOFF = "logged off"; /** * The connection is down. */ /* package */ static final int CONNECT_DOWN = 0; /** * The remote TCP connection completed synchronously. */ /* package */ static final int CONNECT_COMPLETE = 1; /** * The remote TCP session is in progress. */ /* package */ static final int CONNECT_INCOMPLETE = 2; /** * The remote TCP connect attempt failed and will never * complete. */ /* package */ static final int CONNECT_FAILED = 3; //----------------------------------------------------------- // Statics. // /** * Tracks all existing remote eBus connections by mapping * the host and port to the remote eBus instance. */ private static final ConcurrentMap sConnections = new ConcurrentHashMap<>(); /** * Stores the accepted logon identifiers. */ private static final Set sLogonIds = new TreeSet<>(); /** * Protects the connection map, listener collection, and * logon ID set. */ private static final Lock sConnectionMutex = new ReentrantLock(true); /** * Singleton responsible for publishing * {@link ConnectionMessage} to subscribers. */ private static final ConnectionPublisher sConnPublisher; /** * The unique identifier for this JVM. */ private static final String sJvmId; /** * Java logging access point. */ private static final Logger sLogger = Logger.getLogger(ERemoteApp.class.getName()); //----------------------------------------------------------- // Locals. // /** * The connection finite state machine. */ private final ERemoteAppContext mFSM; /** * This is the eBus client for this remote connection. The * client contains the executor used to run eBus tasks meant * for this object. */ private EClient mEClient; /** * This connection's endpoint. */ private InetSocketAddress mAddress; /** * If this connection was accepted by an * {@link EServer eBus service}, then this is the TCP service * port. If not so accepted, then this value is zero. */ private int mServerPort; /** * The actual eBus connection to the remote engine. */ private EAbstractConnection mConnection; /** * The connect attempt status. */ private int mConnectStatus; /** * Set to {@code true} when successfully logged on and * {@code false} otherwise. */ private volatile boolean mLoggedOn; /** * This instance creation timestamp. */ private final Date mCreated; /** * The remote application identifier. */ private String mRemoteId; /** * The optional exception behind a logoff. Will be * {@code null} if the remote eBus correctly logged off. */ private Throwable mLogoffException; /** * Maps a message key to the feed. This map is used to find * to a feed referenced by a message key. */ private final Map mKeys; /** * Maps the local feed identifier to the local feed instance. */ private final Map mFeeds; /** * Maps a locally-generated request identifier to the request * instance. This request identifier is used instead of the * feed identifier for request/reply. */ private final Map mLocalRequests; /** * When a remote request is received, a local request feed is * needed to create a local request feed. This map tracks the * extant request feeds used for the purpose of making local * requests. There is one request feed per message key, all * having a {@link FeedScope#LOCAL_ONLY} scope. */ private final Map mRequestFeeds; /** * Tracks the active, remote requests. Used to terminate * requests in case of disconnect. Maps the remote feed * identifier to the local request. */ private final Map mRemoteRequests; /** * Links the local to remote feed identifier. The key is the * local feed identifier and the value is the remote feed * identifier. */ private final Map mToFromMap; /** * Obtain unique message class identifiers from here. */ private final MessageKeyStore mKeyStore; /** * Store the advertisements received during logon here and * process them all once logon is successfully completed. */ private List mLogonAds; // // StatusReport statistics. // Not meant to be strictly accurate. // /** * Track the number of subscriber feeds. */ private int mSubCount; /** * Track the number of publisher feeds. */ private int mPubCount; /** * Track the number of replier feeds. */ private int mReplierCount; /** * Track the number of request feeds. */ private int mRequestorCount; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * Creates a new ERemoteApp instance for the specified * remote eBus engine. */ private ERemoteApp() { mAddress = null; mServerPort = 0; mRemoteId = null; mConnection = null; mConnectStatus = CONNECT_DOWN; mLoggedOn = false; mCreated = new Date(); mFSM = new ERemoteAppContext(this); mFeeds = new HashMap<>(); mKeys = new HashMap<>(); mLocalRequests = new HashMap<>(); mRequestFeeds = new HashMap<>(); mRemoteRequests = new HashMap<>(); mToFromMap = new HashMap<>(); mKeyStore = new MessageKeyStore(this); mFSM.setDebugFlag(sLogger.isLoggable(Level.FINEST)); } // end of ERemoteApp(...) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // EConnection Callback Methods. // /** * The connection is successfully established. * @param c the now connected {@link EAbstractConnection} * source. */ /* package */ void handleOpen(final EAbstractConnection c) { if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format( "%s: connected to remote eBus.", mAddress)); } mEClient.dispatch(mFSM::connected); return; } // end of handleOpen(EAbstractConnection) /** * The {@link EAbstractConnection} is now closed. * If an exception caused this closure, it is forwarded to * the listener via {@code ex}. * If {@link EAbstractConnection} is set to do automatically * reconnect, then {@link #handleOpen(EAbstractConnection)} * will be called when the connection has been * re-established. * @param c this connection is the now closed. */ /* package */ void handleClose(final EAbstractConnection c) { sLogger.info( String.format("%s: disconnected.", mAddress)); mEClient.dispatch(mFSM::disconnected); return; } // end of handleClose(EAbstractConnection) // // end of EConnection Callback Methods. //----------------------------------------------------------- //----------------------------------------------------------- // EPublisher Interface Implementation. // /** * This callback means that there is or is not a local * subscriber for the remote publisher. Convert this into * a remote subscription message, passing along the given * feed state. * @param feedState subscribe to or unsubscribe from the * remote publisher. * @param feed the publish feed. */ @Override public void publishStatus(final EFeedState feedState, final IEPublishFeed feed) { final int fromFeedId = feed.feedId(); final int toFeedId = (!mToFromMap.containsKey(fromFeedId) ? NO_ID : mToFromMap.get(fromFeedId)); if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format( "%s: %s publish status is %s.", mAddress, feed, feedState)); } // Is the feed still around? It might have been // retracted just prior to this method call. // Did the feed state change? // The feed state is changed if: // + the feed state is UP and to feed ID == NO_ID or // + the feed state is DOWN and to feed ID != NO_ID. if (mFeeds.containsKey(fromFeedId) && ((feedState == EFeedState.UP && toFeedId == NO_ID) || (feedState == EFeedState.DOWN && toFeedId != NO_ID))) { // Yes. Forward the corresponding subscription // message to the remote eBus app. // If this is an unsubscribe, then mark the feed as // down and clear out the from->to map entry. if (feedState == EFeedState.DOWN) { ((EPublishFeed) feed).clearFeedState(); mToFromMap.remove(fromFeedId); } send( new EMessageHeader( (SystemMessageType.SUBSCRIBE).keyId(), fromFeedId, toFeedId, new SubscribeMessage( feed.key(), feedState))); } return; } // end of publishStatus(EFeedState, IEPublishFeed) // // end of EPublisher Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // ESubscriber Interface Implementation. // /** * Forwards the subscribe feed state to the remote eBus * application if the feed is still in place. * @param feedState the updated subscription state. * @param feed the state change applies to this subscribe * feed. This is an {@link ESubscribeFeed} instance since * multi-key feeds cannot be transmitted to remote * applications. */ @Override public void feedStatus(final EFeedState feedState, final IESubscribeFeed feed) { final int feedId = feed.feedId(); if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format( "%s: %s feed status is %s.", mAddress, feed, feedState)); } // Is the feed still around? if (mFeeds.containsKey(feedId)) { // Yes. Forward the feed state to the remote eBus. // If putting the subscription is place, then there // is no remote feed yet. send( new EMessageHeader( (SystemMessageType.FEED_STATUS).keyId(), feedId, mToFromMap.get(feedId), new FeedStatusMessage(feedState))); } return; } // end of feedStatus(EFeedState, ESubscribeFeed) /** * Forwards the message to the remote eBus application if the * subscription is still in place. * @param msg forward this message. * @param feed subscription feed posting this callback. This * is an {@link ESubscribeFeed} instance since multi-key * feeds cannot be transmitted to remote applications. */ @Override public void notify(final ENotificationMessage msg, final IESubscribeFeed feed) { final int feedId = feed.feedId(); // Is the feed still around? It might have been retracted // just prior to this method call. if (mFeeds.containsKey(feedId)) { if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format( "%s: forwarding message%n%s", mAddress, msg)); } // Yes, still here. Forward the notification message // to the remote eBus. send( new EMessageHeader( mKeyStore.findOrCreate(msg.key()), feedId, mToFromMap.get(feedId), msg)); } return; } // end of notify(ENotification, ESubscribeFeed) // // end of ESubscriber Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // EReplier Interface Implementation. // /** * Assigns a locally unique identifier to this request and * forwards the request to the remote eBus application. The * remote eBus application uses the assigned request * identifier to match this request to the remote request * feed. * @param request the request instance. */ @Override public void request(final EReplyFeed.ERequest request) { final ERequestMessage msg = request.request(); final int keyId = mKeyStore.findOrCreate(msg.key()); final int requestId = request.feedId(); // Map the request identifier to the request. mLocalRequests.put(requestId, request); // Forward the request message using the request // identifier as the from feed ID. send(new EMessageHeader(keyId, requestId, NO_ID, msg)); return; } // end of request(ERequest) /** * Forward this cancel request to the remote eBus application * if that remote application acknowledged the request. If * not yet acknowledged, then sends the cancel request * message upon acknowledgment. * @param request cancel this request. */ @Override public void cancelRequest(final EReplyFeed.ERequest request) { final int feedId = request.feedId(); // Is the request acknowledged? if (mToFromMap.containsKey(feedId)) { // Yes. Forward the cancel request to the remote // eBus application. send( new EMessageHeader( (SystemMessageType.CANCEL_REQUEST).keyId(), feedId, mToFromMap.get(feedId), new CancelRequest())); // Remove from the request map. mLocalRequests.remove(feedId); } // No, not yet acknowledged. Send the cancel request // when the ack arrives. return; } // end of cancelRequest(ERequest) // // end of EReplier Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // ERequestor Interface Implementation. // /** * Ignores request feed state changes. * @param feedState the latest request feed state. * @param feed feed state applies to this feed. */ @Override public void feedStatus(final EFeedState feedState, final ERequestFeed feed) {} /** * Forwards the remaining replier count and reply message to * the remote eBus application. *

* This method and {@link #remoteRequest(EMessageHeader)} are * synchronized to protect against the reply delivery prior * to storing the request information. If not synchronized, * the reply would appear to be for an unknown request. *

* @param remaining number of repliers still actively * replying. * @param reply the reply message. * @param request reply is for this request. */ @Override public synchronized void reply(final int remaining, final EReplyMessage reply, final ERequestFeed.ERequest request) { final int fromFeedId = request.feedId(); // Is the request still around? if (mToFromMap.containsKey(fromFeedId)) { final int toFeedId = mToFromMap.get(fromFeedId); // Yes, the request is still around. // Is this a final reply? if (reply.isFinal()) { // Yes. That means the remaining count changed. // Send a new request ack with the updated // remaining count. if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format( "%s: request step 4: %d -> %d has %d remaining replies.", mAddress, fromFeedId, toFeedId, remaining)); } send( new EMessageHeader( (SystemMessageType.REMOTE_ACK).keyId(), fromFeedId, toFeedId, new RemoteAck(remaining))); } // Now forward teh reply to the remote eBus // application. send( new EMessageHeader( mKeyStore.findOrCreate(reply.key()), fromFeedId, toFeedId, reply)); // Is this remote request finished? if (remaining == 0) { // Yes. Clean up the request. mToFromMap.remove(fromFeedId); mRemoteRequests.remove(request.feedId()); } } return; } // end of reply(int, EReplyMessage, ERequestFeed.ERequest) // // end of ERequestor Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // Object Method Overrides. // @Override public String toString() { return ("ERemoteApp " + mAddress); } // end of toString() // // end of Object Method Overrides. //----------------------------------------------------------- //----------------------------------------------------------- // From Local JVM to Remote JVM. // /** * Forwards the given message to all {@code ERemoteApp} * instances by wrapping message in a {@link SendTask} and * posting it to the {@link EClient} task queue. * @param h forward this message to remote eBus * instances. */ /* package */ static void forwardAll(final EMessageHeader h) { sConnections.values() .stream() .forEach((conn) -> { (conn.mEClient).dispatch(() -> conn.send(h)); }); return; } // end of forwardAll(AdMessage) // // end of From Local JVM to Remote JVM. //----------------------------------------------------------- //----------------------------------------------------------- // From Remote JVM to Local JVM // /** * Passes the logon message to the finite state machine. * @param header contains the logon message. */ /* package */ void remoteLogon(final EMessageHeader header) { mFSM.logon((LogonMessage) header.message()); return; } // end of remoteLogon(EMessageHeader) /** * Passes the logon reply to the finite state machine. * @param header contains the logon reply message. */ /* package */ void remoteLogonReply(final EMessageHeader header) { mFSM.logonReply((LogonReply) header.message()); return; } // end of remoteLogonReply(EMessageHeader) /** * Passes the logon complete message to the finite state * machine. * @param header contains the logon complete message. */ /* package */ void remoteLogonComplete(final EMessageHeader header) { mFSM.logonComplete( (LogonCompleteMessage) header.message()); return; } // end of remoteLogonComplete(EMessageHeader) /** * Passes the logoff message to the finite state machine. * @param header contains the logoff message. */ /* package */ void remoteLogoff(final EMessageHeader header) { mFSM.logoff((LogoffMessage) header.message()); return; } // end of remoteLogoff(EMessageHeader) /** * Turns around and passes this message right back to * {@link EAbstractConnection#keyUpdate(KeyMessage)}. * @param header contains the class update message. */ /* package */ void remoteClassUpdate(final EMessageHeader header) { mConnection.keyUpdate((KeyMessage) header.message()); return; } // end of remoteClassUpdate(EMessageHeader) /** * Routes the advertisement message through the state * machine. If the connection is open, then processes the * advertisement immediately. If logging on, then stores the * advertisement for processing when logon is completed. * @param header contains the advertise message. */ /* package */ void remoteAd(final EMessageHeader header) { mFSM.adMessage((AdMessage) header.message()); return; } // end of remoteAd(EMessageHeader) /** * Either adds are removes a local subscribe feed based on * the remote eBus feed state. * @param header contains the subscribe message. */ /* package */ void remoteSubscribe(final EMessageHeader header) { final SubscribeMessage subMsg = (SubscribeMessage) header.message(); try { @SuppressWarnings ("unchecked") final Class mc = (Class) Class.forName(subMsg.messageClass); final EMessageKey key = new EMessageKey(mc, subMsg.messageSubject); int toFeedId = header.toFeedId(); final ESubscribeFeed feed; // Adding or removing. if (subMsg.feedState == EFeedState.UP) { // Adding. feed = ESubscribeFeed.open(this, key, FeedScope.LOCAL_ONLY, null, ClientLocation.REMOTE, false); toFeedId = feed.feedId(); // Map the message key and feed identifier to the // feed instance. mKeys.put(key, feed); mFeeds.put(toFeedId, feed); // Link the remote, publish feed to the local // subscribe feed. mToFromMap.put(toFeedId, header.fromFeedId()); if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format( "%s: subscribing to feed %s.", mAddress, feed)); } // Now put the feed in place. feed.subscribe(); } // Removing. else if ((feed = (ESubscribeFeed) findFeed( toFeedId, key)) != null) { // Retract the feed and remove from the feeds // map. feed.unsubscribe(); mKeys.remove(key); mFeeds.remove(toFeedId); // Disconnect the local and remote feeds. mToFromMap.remove(toFeedId); if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format( "%s: unsubscribing from feed %s.", mAddress, feed)); } } } catch (ClassNotFoundException classex) { if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format( "%s: subscribe message %s unknown class %s, ignored.", mAddress, subMsg.feedState, subMsg.messageClass)); } } catch (IllegalArgumentException jex) { sLogger.log( Level.WARNING, String.format( "%s: ad message to %s %s:%s failed.", mAddress, subMsg.feedState, subMsg.messageClass, subMsg.messageSubject), jex); } return; } // end of remoteSubscribe(EMessageHeader) /** * Converts the remote subscribe feed state to a local * publish feed state if the publish feed is still active. * @param header contains the subscribe feed state message. */ /* package */ void remoteFeedStatus(final EMessageHeader header) { final int toFeedId = header.toFeedId(); final int fromFeedId = header.fromFeedId(); final FeedStatusMessage fsMsg = (FeedStatusMessage) header.message(); final EFeed feed = mFeeds.get(toFeedId); if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format( "%s: from=%d, to=%d, status=%s, feed=%s.", mAddress, fromFeedId, toFeedId, fsMsg.feedState, (feed == null ? "(unknown)" : feed))); } // Is the feed still around? // Is it still active? if (feed != null && feed.isActive()) { // Yes and yes. Convert the feed state to a publish // state. if (feed instanceof EPublishFeed) { ((IEPublishFeed) feed).updateFeedState( fsMsg.feedState); } else { ((IEReplyFeed) feed).updateFeedState( fsMsg.feedState); } // Link this publish feed to the remote subscribe // feed. mToFromMap.put(toFeedId, fromFeedId); } return; } // end of remoteFeedStatus(EMessageHeader) /** * Forwards a notification from a remote publisher to local * subscribers. * @param header contains the local publish feed identifier * and notification message. */ /* package */ void remoteNotify(final EMessageHeader header) { final int toFeedId = header.toFeedId(); final EPublishFeed feed = (EPublishFeed) mFeeds.get(toFeedId); final ENotificationMessage message = (ENotificationMessage) header.message(); if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format( "%s: feed %s received message:%n%s", mAddress, feed, message)); } try { feed.publish(message); } catch (IllegalArgumentException | IllegalStateException jex) { // Ignore. } return; } // end of remoteNotify(EMessageHeader) /** * Converts the remote request message into a local * {@link ERequestFeed}, posting the inbound request message * to that feed. Then sends a {@link RemoteAck} message * which contains the request feed identifier and the remote * request identifier. This allows the far-end which sent the * request to link the feeds together. *

* This method and * {@link #reply(int, EReplyMessage, ERequestFeed.ERequest)} * are synchronized because replies may be delivered before * the request information is stored away. If not * synchronized, then the reply would appear to be for an * unknown request. *

* @param header contains the remote request message. */ /* package */ synchronized void remoteRequest(final EMessageHeader header) { final int fromFeedId = header.fromFeedId(); final ERequestMessage reqMsg = (ERequestMessage) header.message(); final EMessageKey key = reqMsg.key(); final ERequestFeed reqFeed = findRequestFeed(key); final ERequestFeed.ERequest request = reqFeed.request(reqMsg); final int toFeedId = request.feedId(); try { // Create a new local request and store it away using // the remote feed identifier. mRemoteRequests.put(fromFeedId, request); // Link the two feeds together ... mToFromMap.put(toFeedId, fromFeedId); mFeeds.put(toFeedId, request); // ... and send an acknowledgement back to the remote // application which contains the local feed // identifier. But do this asynchronously since the // remote connection should be accessed from a // dispatcher thread only. mEClient.dispatch( () -> send( new EMessageHeader( (SystemMessageType.REMOTE_ACK).keyId(), toFeedId, fromFeedId, new RemoteAck( request.repliersRemaining())))); } catch (IllegalArgumentException | IllegalStateException jex) { final EMessageKey replyKey = new EMessageKey( EReplyMessage.class, key.subject()); final int keyId = mKeyStore.findOrCreate(replyKey); // If the request failed, then close the request and // clear the maps. request.close(); mToFromMap.remove(toFeedId); mFeeds.remove(toFeedId); // Send the reply first and then the updated request // acknowledgement. mEClient.dispatch( () -> send( new EMessageHeader( keyId, toFeedId, fromFeedId, new EReplyMessage( key.subject(), ReplyStatus.ERROR, jex.getMessage())))); // Send an acknowledgement stating that there will be // no replies except the failure reply. mEClient.dispatch( () -> send( new EMessageHeader( (SystemMessageType.REMOTE_ACK).keyId(), toFeedId, fromFeedId, new RemoteAck(0)))); } return; } // end of remoteRequest(EMessageHeader) /** * Forward the request cancellation to the replier if still * in place. * @param header contains the request cancel message. */ /* package */ void remoteCancelRequest(final EMessageHeader header) { final int toFeedId = header.toFeedId(); final ERequestFeed.ERequest request = (ERequestFeed.ERequest) mFeeds.get(toFeedId); if (sLogger.isLoggable(Level.FINER)) { final Formatter output = new Formatter(); output.format("%s: feed %d remote cancel: ", mAddress, toFeedId); if (request == null) { output.format("unknown request feed"); } else { output.format("%s is %s.", request.key(), request.requestState()); } sLogger.finer(output.toString()); } // Is the request still around? // Is the request still active? if (request != null && request.requestState() == RequestState.ACTIVE) { // No. Clean up the defunct request. request.close(); // Clean up the dead request. mToFromMap.remove(toFeedId); mRemoteRequests.remove(toFeedId); } // The request is either done, terminated, or a cancel is // already in progress. return; } // end of remoteCancelRequest(EMessageHeader) /** * The inbound message contains the remote feed identifier * associated with the request, linking the local feed with * the remote. Allows the request to be canceled before the * first reply is received. * @param header contains the inbound request acknowledgment. */ /* package */ void remoteRequestAck(final EMessageHeader header) { final int fromFeedId = header.fromFeedId(); final int toFeedId = header.toFeedId(); final RemoteAck msg = (RemoteAck) header.message(); final EReplyFeed.ERequest request = mLocalRequests.get(toFeedId); final RequestState reqState = request.state(); if (!mToFromMap.containsKey(toFeedId)) { mToFromMap.put(toFeedId, fromFeedId); } request.remoteRemaining(msg.remaining); // Was a cancel attempt made before this acknowledgment // was received? if (reqState == RequestState.CANCELED) { // Yes. Send the cancel request message now. send(new EMessageHeader( (SystemMessageType.CANCEL_REQUEST).keyId(), toFeedId, fromFeedId, new CancelRequest())); // Remove from the request map. mLocalRequests.remove(toFeedId); } return; } // end of remoteRequestAck(EMessageHeader) /** * Handles a {@link RemoteAck} message which is used to * link local and remote requests together. If the request * was canceled prior to this message receipt, then a * {@link CancelRequest} message is immediately sent back to * the remote eBus application. * @param header contains the system remote reply message. */ /* package */ void remoteReply(final EMessageHeader header) { final int toFeedId = header.toFeedId(); final int fromFeedId = header.fromFeedId(); final EReplyFeed.ERequest request = mLocalRequests.get(toFeedId); // Is the request still around? if (request == null) { // No. Ignore this message. } // Yes, the request is still here. But is it still // breathing? else if (request.state() == RequestState.CANCELED) { // Nope, its dead. Tell the other side to terminate // its request. send(new EMessageHeader( (SystemMessageType.CANCEL_REQUEST).keyId(), fromFeedId, toFeedId, new CancelRequest())); // Remove from the request map as well. mLocalRequests.remove(toFeedId); } // The request is both known and alive. // Its alive, I tell you! Alive! else { final EReplyMessage replyMsg = (EReplyMessage) header.message(); final RequestState reqState = request.state(); // So link the request and remote feed by the hip. mToFromMap.put(toFeedId, fromFeedId); // Forward the reply to the requester. request.remoteReply(replyMsg); // Is a request cancellation in progress or // terminated? if (reqState == RequestState.CANCELED) { // Then send the cancel request to the far end. send(new EMessageHeader( (SystemMessageType.CANCEL_REQUEST).keyId(), fromFeedId, toFeedId, new CancelRequest())); } } return; } // end of remoteReply(EMessageHeader) // // end of EPublisher Method Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // Get methods // /** * Returns the host and port of the remote eBus engine to * which this client is connected. * @return the host and port of the remote eBus engine to * which this client is connected. */ public InetSocketAddress address() { return (mAddress); } // end of address() /** * Returns {@code true} if the connection is automatically * re-established when lost; {@code false} otherwise. * @return {@code true} if the connection is automatically * re-established when lost; {@code false} otherwise. */ public boolean willReconnect() { return (mConnection.willReconnect()); } // end of willReconnect() /** * Returns {@code true} if connected and logged on to the * remote eBus application and {@code false} otherwise. * @return {@code true} if connected and logged on to the * remote eBus application and {@code false} otherwise. */ public boolean isConnected() { return (mLoggedOn); } // end of isConnected() /** * Returns the current connect attempt status. * @return connect attempt status. */ /* package */ int connectStatus() { return (mConnectStatus); } // end of connectStatus() /** * Returns the number of remote connections. * @return the number of remote connections. */ public static int connectionCount() { return (sConnections.size()); } // end of connectionCount() /** * Returns a copy of the existing remote eBus application * connections. * @return a copy of the existing remote eBus application * connections. */ public static Collection connections() { final Collection retval = new ArrayList<>(); retval.addAll(sConnections.keySet()); return (retval); } // end of connections() /** * Returns {@code true} if there is a connection to the * remote eBus application at the given Internet address * and {@code false} otherwise. * @param a find a connection to this Internet address. * @return {@code true} if there is a connection to the * remote eBus application at the given Internet address * and {@code false} otherwise. */ public static boolean isConnected(final InetSocketAddress a) { final ERemoteApp remote = sConnections.get(a); return (remote == null ? false : remote.isConnected()); } // end of isConnected(InetSocketAddress) /** * Returns the eBus remote application instance for the given * socket address. May return {@code null}. * @param a the remote Internet address. * @return an eBus remote application instance. */ /* package */ static ERemoteApp connection(final InetSocketAddress a) { return (sConnections.get(a)); } // end of connection(InetSocketAddress) /** * Returns the request feed for the given message key. If no * such request feed exists, then a new request feed is * opened and stored into the request feeds map. * @param key request message key. * @return request feed for the given key. */ private ERequestFeed findRequestFeed(final EMessageKey key) { ERequestFeed retval = mRequestFeeds.get(key); // Is there a request feed for this message key? if (retval == null) { // No. Create one now and store it away. retval = ERequestFeed.open(this, key, FeedScope.LOCAL_ONLY, ClientLocation.REMOTE, false); retval.subscribe(); mRequestFeeds.put(key, retval); } return (retval); } // end of findRequestFeed(EMessageKey) // // end of Get methods. //----------------------------------------------------------- /** * Opens a client socket connection to the remote eBus at * the specified * {@link EConfigure.RemoteConnection#address() host and port} * and binding the local Internet address to * {@link EConfigure.RemoteConnection#bindPort() local port}. * If a connection to the given host and type already exists * then throws an {@code IllegalStateException} and does * nothing else. *

* If the bind port is zero, then the local Internet address * is bound to any available port. *

*

* Only one connection is allowed between eBus engines * regardless of which engine initiated the connection. * An eBus engine is also precluded from connecting to * itself. *

* @param config remote connection configuration. * @return the remote eBus connection instance. * @throws NullPointerException * if {@code config} is {@code null}. */ public static ERemoteApp openConnection(final EConfigure.RemoteConnection config) { final InetSocketAddress inetAddress = config.address(); final ERemoteApp retval; Objects.requireNonNull(config, "config is null"); retval = new ERemoteApp(); // If a non-null value is returned, that this remote // connection was previously establised. if (sConnections.putIfAbsent(inetAddress, retval) != null) { throw ( new IllegalStateException( String.format( "already connected to %s", inetAddress))); } // This is a new remote connection. Continue // processing. else { if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format( "Opening connection to %s:%n%s", inetAddress, config)); } retval.open(config); } return (retval); } // end of openConnection(RemoteConnection) /** * Closes a currently open remote eBus client referenced by * the Internet address. Does nothing if no such client * currently exists. * @param address close the connection to this eBus host and serverPort. */ public static void closeConnection(final InetSocketAddress address) { final ERemoteApp connection = sConnections.remove(address); if (connection != null) { connection.close(); } return; } // end of closeConnection(InetSocketAddress) /** * Closes all currently open connections. */ public static void closeAllConnections() { sConnections.values().stream(). forEach((conn) -> { conn.close(); }); return; } // end of closeAllConnections() /** * Opens the remote eBus connections as per * {@link EConfigure#remoteConnections()}. * @param config contains the remote eBus connection * configuration. */ public static void configure(final EConfigure config) { (config.remoteConnections()).values() .forEach(ERemoteApp::openConnection); return; } // end of configure(EConfigure) /** * Returns a {@code ERemoteApp} instance for the newly * accepted TCP socket. * @param serverPort socket accepted on this service port. * @param channel the accepted socket. * @param config accepted socket configuration. * @return the eBus remote application instance encapsulating * {@code socket}. */ /* package */ static ERemoteApp openConnection(final int serverPort, final SelectableChannel channel, final EConfigure.Service config) { final SocketChannel socket = (SocketChannel) channel; final InetSocketAddress address = (InetSocketAddress) (socket.socket()).getRemoteSocketAddress(); ERemoteApp retval = new ERemoteApp(); sConnections.put(address, retval); retval.open(address, serverPort, channel, config); return (retval); } // end of openConnection(int, SelectableChannel, Service) /** * Opens the connection to the remote eBus application as per * the given connection configuration. * @param config remote connection configuration. */ private void open(final EConfigure.RemoteConnection config) { // Create the eBus client instance for this remote // connection and then post the open task to it. mAddress = config.address(); mEClient = EClient.findOrCreateClient( this, ClientLocation.REMOTE); mEClient.dispatch(() -> mFSM.open(config)); return; } // end of open(RemoteConnection) /** * Opens the connection to the remote eBus application as per * the given parameters. Note that accepted socket * connections are not reconnected when lost. * @param addr remote application address. * @param serverPort connection accepted on this service * port. * @param channel the accepted socket. * @param config configure the channel as per these settings. */ private void open(final InetSocketAddress addr, final int serverPort, final SelectableChannel channel, final EConfigure.Service config) { // Create the eBus client instance for this remote // connection and then post the open task to it. mAddress = addr; mEClient = EClient.findOrCreateClient( this, ClientLocation.REMOTE); mEClient.dispatch(() -> mFSM.open(addr, serverPort, channel, config)); return; } // end of open(...) /** * Closes an open remote connection. */ private void close() { mEClient.dispatch(mFSM::close); return; } // end of close() /** * Returns the feed instance by first looking for it using * the unique feed identifier and, if not found, then the * message key. May return {@code null}. * @param feedId unique feed identifier. * @param key message class and subject key. * @return feed instance for the given identifier and message * key. */ private EFeed findFeed(final int feedId, final EMessageKey key) { EFeed retval = mFeeds.get(feedId); if (retval == null) { retval = mKeys.get(key); } return (retval); } // end of findFeed(int, EMessageKey) //----------------------------------------------------------- // FSM Conditions. // /** * Returns {@code true} if the given logon identifier is * already logged on and {@code false} otherwise. * @param logonId check if this logon identifier is already * logged on. * @return {@code true} if the given logon identifier is * already logged on and {@code false} otherwise. */ /* package */ boolean isLoggedOn(final String logonId) { boolean retcode = false; sConnectionMutex.lock(); try { retcode = sLogonIds.contains(logonId); } finally { sConnectionMutex.unlock(); } return (retcode); } // end of isLoggedOn(String) // // end of FSM Conditions. //----------------------------------------------------------- //----------------------------------------------------------- // FSM Actions. // /** * Establishes a connection the remote eBus application at * the given IP address. * @param config the remote connection configuration. * @return {@code true} if the remote eBus connection was * successfully established and {@code false} otherwise. * Note: {@code true} does not mean the connection * successfully logged on. */ /* package */ int connect(final EConfigure.RemoteConnection config) { if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format("%s: connecting to remote eBus.", config.address())); } mConnection = ETCPConnection.create(config, this); try { if (mConnection.open(config)) { mConnectStatus = CONNECT_COMPLETE; } else { mConnectStatus = CONNECT_INCOMPLETE; } } catch (IOException ioex) { mConnectStatus = CONNECT_FAILED; } return (mConnectStatus); } // end of connect(...) /** * Creates a remote application for the accepted connection. * Note that accepted connections are not reconnected by the * server. If the connection is lost, it is up to the client * to reconnect. * @param address the remote application address and port. * @param bindPort socket accepted on this service port. * @param channel the accepted socket channel. * @param config accepted socket connection configuration. */ /* package */ void connect(final InetSocketAddress address, final int bindPort, final SelectableChannel channel, final EConfigure.Service config) { mAddress = address; mServerPort = bindPort; mConnection = ETCPConnection.create(config, this); if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format( "%s: remote eBus connected, max queue size: %,d, current queue size: %,d.", mAddress, mConnection.maxMessageQueueSize(), mConnection.messageQueueSize())); } try { mConnection.open(channel, config); } catch (IOException ioex) { sLogger.log(Level.WARNING, String.format( "%s: connection open failed.", address), ioex); } return; } // end of connect(...) /** * Disconnects the remote eBus application connection if * open. All queued messages not yet transmitted are * discarded. */ /* package */ void disconnect() { if (mConnection != null && mConnection.isOpen()) { mConnection.closeNow(); } return; } // end of disconnect() /** * Sends a logon message to the remote eBus application. This * message is sent directly, by-passing the transmit queue. */ /* package */ void logon() { final EMessage logon = new LogonMessage(sJvmId); if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format("%s: sending logon:%n%s", mAddress, logon)); } send( new EMessageHeader( (SystemMessageType.LOGON).keyId(), NO_ID, NO_ID, logon)); return; } // end of logon() /** * Sends a logon reply message to the remote eBus * application. * @param status {@code true} if the remote eBus application * successfully logged on and {@code false} otherwise. * @param reason the reason for a logon failure. */ /* package */ void logonReply(final ReplyStatus status, final String reason) { final EMessage reply = new LogonReply(sJvmId, status, reason); if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format( "%s: sending logon reply:%n%s", mAddress, reply)); } send( new EMessageHeader( (SystemMessageType.LOGON_REPLY).keyId(), NO_ID, NO_ID, reply)); return; } // end of logonReply(boolean, String) /** * Sends a logoff message to the remote eBus application. */ /* package */ void logoff() { final EMessage logoff = new LogoffMessage(sJvmId); sLogger.info( String.format( "%s: logging off from remote eBus.", mAddress)); send( new EMessageHeader( (SystemMessageType.LOGOFF).keyId(), NO_ID, NO_ID, logoff)); return; } // end of logoff() /** * Stores the remote client logon identifier. * @param id the remote client logon identifier. */ /* package */ void storeRemoteId(final String id) { mRemoteId = id; sConnectionMutex.lock(); try { sLogonIds.add(id); } finally { sConnectionMutex.unlock(); } return; } // end of storeRemoteId(String) /** * Stores the advertisement message for later processing once * the logon process is complete. * @param msg store this advertisement message. */ /* package */ void storeAd(final AdMessage msg) { mLogonAds.add(msg); return; } // end of storeAd(AdMessage) /** * Removes this connection from the static connections list. */ /* package */ void removeConnection() { sConnections.remove(mAddress); return; } // end of removeConnection() /** * When the connection comes up, retrieve all local * advertisements and forward item to the far-end. */ /* package */ void sendAds() { // Store *received* ads here and process when logon // completes. mLogonAds = new LinkedList<>(); mEClient.dispatch( () -> { // Send the local client advertisements. (ESubject.localAds(AdMessage.AdStatus.ADD)) .stream() .forEach( msg -> { try { mConnection.send(msg); } catch (IOException ioex) { sLogger.log( Level.WARNING, "Failed to send advertisement", ioex); } }); }); return; } // end of sendAds() /** * Sends a logon complete message to the far-end. */ /* package */ void sendLogonComplete() { mEClient.dispatch( () -> { final LogonCompleteMessage logonMsg = new LogonCompleteMessage(sJvmId); // Do this after retrieving the local advertisements // to prevent localAd() from posting a redundant // advertisement. mLoggedOn = true; send(new EMessageHeader( (SystemMessageType.LOGON_COMPLETE).keyId(), NO_ID, NO_ID, logonMsg)); }); return; } // end of sendLogonComplete() /** * When the advertisement exchange is complete, inform the * connection subscribers that this eBus application is now * connected to the given remote application. */ /* package */ void remoteConnect() { // Inform listeners that this connection is up. sConnPublisher.publish(mAddress, mServerPort, ConnectionState.LOGGED_ON, null); return; } // end of remoteConnect() /** * Processes the advertisements received during logon. Once * completed, clears the stored advertisement list and drops * the reference to that list. */ /* package */ void processLogonAds() { mLogonAds.forEach( (msg) -> { processAd(msg); }); mLogonAds.clear(); mLogonAds = null; return; } // end of processLogonAds() /** * Either adds or removes a publish feed based on the message * advertise status. If the advertise message references an * unknown message class, then the advertise message is * ignored. * @param adMsg the advertisement message. */ /* package */ void processAd(final AdMessage adMsg) { try { @SuppressWarnings ("unchecked") final Class mc = (Class) Class.forName(adMsg.messageClass); final EMessageKey key = new EMessageKey(mc, adMsg.messageSubject); final EFeed feed; if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format( "%s: received ad message:%n%s", mAddress, adMsg)); } // Adding or removing? if (adMsg.adStatus == AdMessage.AdStatus.ADD) { // Adding. // But adding what? Publisher or replier? if (key.isNotification()) { // Publisher. feed = EPublishFeed.open(this, key, FeedScope.LOCAL_ONLY, ClientLocation.REMOTE, false); ++mPubCount; } else { // Replier. final MessageType dataType = (MessageType) DataType.findType(key.messageClass()); feed = EReplyFeed.open(this, key, FeedScope.LOCAL_ONLY, null, ClientLocation.REMOTE, dataType, false); ++mReplierCount; } // Map the message key and feed identifier to the // feed instance. mKeys.put(key, feed); mFeeds.put(feed.feedId(), feed); if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format( "%s: added %s feed %s (state: %s).", mAddress, (key.isNotification() ? "publish" : "reply"), key, adMsg.feedState)); } // Now put the feed in place. if (key.isNotification()) { ((IEPublishFeed) feed).advertise(); ((IEPublishFeed) feed).updateFeedState( adMsg.feedState); } else { ((IEReplyFeed) feed).advertise(); ((IEReplyFeed) feed).updateFeedState( adMsg.feedState); } } // Removing. else if ((feed = mKeys.remove(key)) != null) { // Retract the feed and remove from the feeds // map. mFeeds.remove(feed.feedId()); if (key.isNotification()) { ((IEPublishFeed) feed).unadvertise(); --mPubCount; } else { ((IEReplyFeed) feed).unadvertise(); --mReplierCount; } if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format( "%s: removed %s feed %s.", mAddress, (key.isNotification() ? "publish" : "reply"), key)); } } } catch (ClassNotFoundException classex) { if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format( "%s: ad message to %s unknown class %s, ignored.", mAddress, adMsg.adStatus, adMsg.messageClass)); } } catch (IllegalArgumentException | IllegalStateException jex) { sLogger.log( Level.WARNING, String.format( "%s: ad message to %s %s:%s failed.", mAddress, adMsg.adStatus, adMsg.messageClass, adMsg.messageSubject), jex); } return; } // end of processAd(AdMessage) /** * When the connection goes down, retract and dispose of all * remote feeds. This means retracting all notification and * reply advertisements, and failing all remote requests. */ /* package */ void remoteDisconnect() { if (mLoggedOn) { String reason; mLoggedOn = false; // Remove the remote ID and clear it. sConnectionMutex.lock(); try { sLogonIds.remove(mRemoteId); } finally { sConnectionMutex.unlock(); } mRemoteId = null; // Retract the remote ads. mFeeds.values() .stream() .filter(feed -> feed.inPlace()) // Close the feed rather than retract because // the feeds will be thrown away. .forEach(feed -> feed.close()); // Cancel the remote requests. mRemoteRequests.values() .stream() .forEach(request -> request.close()); // Clear out all the maps. mKeys.clear(); mFeeds.clear(); mLocalRequests.clear(); mRemoteRequests.clear(); mToFromMap.clear(); // Inform listeners that this connection is down. if (mLogoffException == null) { reason = NORMAL_LOGOFF; } else { reason = mLogoffException.getLocalizedMessage(); if (reason == null || reason.isEmpty()) { reason = (mLogoffException.getClass()).getName(); } } sConnPublisher.publish(mAddress, mServerPort, ConnectionState.LOGGED_OFF, reason); mLogoffException = null; } return; } // end of remoteDisconnect() /** * Log the connection shutdown. */ /* package */ void doShutdown() { if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format("%s: shutting down.", mAddress)); } return; } // end of doShutdown() // // end of FSM Actions. //----------------------------------------------------------- /** * Sends the message to the remote application. Takes the * appropriate action if the outgoing message queue * overflows. *

* If the message transmit fails for any reason, this * remote connection is dropped. *

* @param header send the message header and its encapsulated * message. */ /* package */ void send(final EMessageHeader header) { try { mConnection.send(header); } catch (IllegalStateException statex) { // Ignore if not connected. This will happen if the // far-end disconnects at the same time as this send. } catch (BufferOverflowException | IOException jex) { sLogger.log( Level.WARNING, String.format( "%s: failed to send %s, disconnecting", mAddress, (header.messageClass()).getName()), jex); mLogoffException = jex; // Note: it is safe to issue a transition now because // either this method is called from outside the FSM // or by the last action in an FSM state entry block. mFSM.disconnected(); } return; } // end of send(EMessage) /** * Appends this remote application connection status to the * report. * @param report append the status to this report. * @param index the connection index in the map. */ private void reportStatus(final PrintWriter report, final int index) { report.format(" [%,d] address: %s%n", index, mAddress); report.format( " created on %1$tY-%1$tm-%1$td @ %1$tH:%1$tM:%1$tS.%1$tL%n", mCreated); report.format(" logged in: %b%n", mLoggedOn); report.format(" subscribers: %,d%n", mSubCount); report.format(" publishers: %,d%n", mPubCount); report.format(" repliers: %,d%n", mReplierCount); report.format(" requestors: %,d%n", mRequestorCount); return; } // end of reportStatus(PrintWriter, int, int) //--------------------------------------------------------------- // Inner classes. // /** * This class adds the status of all remote application * connections to the status report. */ private static final class ERemoteStatusReporter implements StatusReporter { //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * This constructor has no member data to initialize. */ public ERemoteStatusReporter() {} // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // StatusReporter Interface Implementation. // /** * Add the remote application connection status to the * report. * @param report the logged status report. */ @Override public void reportStatus(final PrintWriter report) { final int appCount = sConnections.size(); report.print("ERemote: "); if (appCount == 0) { report.println( "there are no remote connections."); } else { final Collection apps = new ArrayList<>(sConnections.values()); int index = 0; report.format( "there is %,d remote application %s.%n", appCount, (appCount == 1 ? "connection" : "connections")); for (ERemoteApp remoteApp : apps) { remoteApp.reportStatus(report, index); report.println(); ++index; } apps.clear(); } return; } // end of reportStatus(PrintWriter) // // end of StatusReporter Interface Implementation. //------------------------------------------------------- //----------------------------------------------------------- // Member data. // } // end of class ERemoteStatusReporter /** * This singleton is responsible for locally publishing * {@link ERemoteApp} connection state updates to * subscribers. This must be done since {@code ERemoteApp} is * a remote client by definition and * {@link ConnectionMessage} has local scope. */ private static final class ConnectionPublisher implements EPublisher { //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new connection publisher instance. The * connection state feed is initially {@code null} and * is instantiated when {@link #startup()} is called. * @see #startup() */ ConnectionPublisher() { _stateFeed = null; } // end of ConnectionPublisher() // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // EObject Interface Implementation. // /** * Opens the {@link ConnectionMessage} notification feed. */ @Override public void startup() { // Open the connect state feed and put its // advertisement in place. _stateFeed = EPublishFeed.open(this, CONNECTION_UPDATE_KEY, FeedScope.LOCAL_ONLY); _stateFeed.advertise(); _stateFeed.updateFeedState(EFeedState.UP); return; } // end of startup() // // end of EObject Interface Implementation. //------------------------------------------------------- //------------------------------------------------------- // EPublisher Interface Implementation. // /** * If {@code feedState} is {@link EFeedState#UP}, then * sends the current connection state for each known * connection. * @param feedState either up or down. * @param feed the connection state feed. */ @Override public void publishStatus(final EFeedState feedState, final IEPublishFeed feed) { if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format( "%s feed is %s.", feed.key(), feedState)); } // Is the feed now up? if (feedState == EFeedState.UP) { // Yes. Send the current feed state for each of // the known remote eBus connections. sConnections.values() .forEach( (conn) -> { _stateFeed.publish( new ConnectionMessage( conn.mAddress, conn.mServerPort, (conn.mLoggedOn ? ConnectionState.LOGGED_ON : ConnectionState.LOGGED_OFF))); }); } // The feed is down. Nothing to send. return; } // end of publishStatus(EFeedState, IEPublishFeed) // // end of EPublisher Interface Implementation. //------------------------------------------------------- /** * Publishes the {@link ConnectionMessage} with the given * information, if the feed is up. * @param address the connection state change applies to * this address. * @param serverPort socket accepted on this service port. * @param state the new connection state. * @param reason human-readable text explaining why the * state changed. May be {@code null}. */ private void publish(final InetSocketAddress address, final int serverPort, final ConnectionState state, final String reason) { if (_stateFeed.isFeedUp()) { _stateFeed.publish( new ConnectionMessage( address, serverPort, state, reason)); } return; } // end of publish(...) //----------------------------------------------------------- // Member data. // /** * The {@link ConnectionMessage} notification feed. */ private EPublishFeed _stateFeed; } // end of class ConnectionPublisher // Static initialization block. static { sJvmId = (ManagementFactory.getRuntimeMXBean()).getName(); // Add this application's JVM identifier into the logon // IDs in order to prevent self connection. sLogonIds.add(sJvmId); // Create the system messages data types for better // performance. DataType.findType(AdMessage.class); DataType.findType(CancelRequest.class); DataType.findType(FeedStatusMessage.class); DataType.findType(KeyMessage.class); DataType.findType(LogoffMessage.class); DataType.findType(LogonMessage.class); DataType.findType(LogonMessage.class); DataType.findType(LogonReply.class); DataType.findType(SubscribeMessage.class); (StatusReport.getInstance()).register( new ERemoteStatusReporter()); sConnPublisher = new ConnectionPublisher(); EFeed.register(sConnPublisher); EFeed.startup(sConnPublisher); } // end of static initialization block. } // end of class ERemoteApp




© 2015 - 2025 Weber Informatics LLC | Privacy Policy