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

net.sf.eBus.client.EMulticastConnection Maven / Gradle / Ivy

The newest version!
//
// Copyright 2021 Charles W. Rapp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package net.sf.eBus.client;

import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ProtocolFamily;
import java.net.SocketAddress;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.MembershipKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import static net.sf.eBus.client.EAbstractConnection.MAX_MESSAGE_SIZE;
import static net.sf.eBus.client.EAbstractConnection.MESSAGE_HEADER_SIZE;
import static net.sf.eBus.client.EAbstractConnection.MESSAGE_SIZE_SIZE;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.MulticastMessage.MulticastState;
import net.sf.eBus.client.sysmessages.McastKeyMessage;
import net.sf.eBus.client.sysmessages.McastSubscribeMessage;
import net.sf.eBus.client.sysmessages.SystemMessageType;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.config.EConfigure.McastNotifyConfig;
import net.sf.eBus.config.EConfigure.MulticastConnection;
import net.sf.eBus.config.EConfigure.MulticastRole;
import net.sf.eBus.config.EConfigure.MultifeedType;
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.ESystemMessage;
import net.sf.eBus.messages.InvalidMessageException;
import net.sf.eBus.messages.UnknownMessageException;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.messages.type.MessageType;
import net.sf.eBus.net.AbstractAsyncDatagramSocket;
import net.sf.eBus.net.AsyncMulticastSocket;
import net.sf.eBus.net.AsyncMulticastSocket.MulticastBuilder;
import net.sf.eBus.net.DatagramBufferWriter;
import net.sf.eBus.net.DatagramListener;
import net.sf.eBus.util.HexDump;
import net.sf.eBus.util.LazyString;
import net.sf.eBus.util.MultiKey2;
import net.sf.eBus.util.logging.StatusReport;
import net.sf.eBus.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@code EMulticastConnection} is the API gateway to eBus
 * multicast communication. This interface does not
 * connect two or more eBus applications together in the same
 * manner as {@link ERemoteApp}, exchanging notification and
 * request advertisements with remote {@link FeedScope}. Instead
 * {@code EMulticastConnections} are configured to either send
 * or receive specific {@link ENotificationMessage} message keys
 * via a multicast group.
 * 

* {@link EMulticastPublisher} uses a * {@link EMultiSubscribeFeed} to receive remotely-scope * notification messages matching the configured notification * message keys. These message are then sent on the multicast * group. {@link EMulticastSubscriber} receive these multicast * notifications and, if the notification message key is one of * the multicast keys, posts the inbound message to the * {@link EMultiPublishFeed}. *

*

* Both multicast publish and subscriber use * {@link net.sf.eBus.config.EConfigure.McastNotifyConfig} to * define the multi-feed key(s) used. Only those multi-feed * notification feeds in the list will be send by the multicast * publisher or received by the multicast subscriber. For more * information on multi-feeds see {@link EMultiFeed}, * {@link EMultiPublishFeed}, and {@link EMultiSubscribeFeed}. *

*

* Multicast publishers and subscribers coordinate message key * feed state using the {@link McastSubscribeMessage} and * {@link McastKeyMessage} system messages. A subscriber * multicasts a {@code McastSubscribeMessage} which causes all * publishers joined to the group to respond by multicasting * their {@code McastKeyMessage}s. This informs the multicast * subscriber as to what notifications are available and their * current feed state. Multicast publishers post the multicast * key state on demand, on start up, and on feed status update. *

*

* Aside: since request/reply messages are exchanged directly * between the requester and replier it makes no sense to * multicast such messages. *

*

Opening a Multicast Connection

* A multicast connection is opened by first configuring the * connection and then using that configuration to open the * desired connection type. Multicast connection configuration is * done using an * {@code EConfigure.MulticastConnection} instance which is * acquired using * {@link net.sf.eBus.config.EConfigure#multicastBuilder()}. See * {@link net.sf.eBus.config.EConfigure.MulticastConnection} for * detailed explanation of how to configure a multicast * connection. *

* The following example shows how to configure and then open an * eBus multicast connection. *

*
final EConfigure.MulticastBuilder builder = EConfigure.multicastBuilder();
final InetAddress group = InetAddress.getByName("230.0.0.0");
final NetworkInterface netIF = NetworkInterface.getByName("en8");
final String messageClass = com.stockreport.notification.LatestTrade.class.getCanonicalName();
final List<String> messageSubjects = com.google.common.collect.ImmutableList<>.of("ALPH", "BETA", "GMMA", "DLTA");
final EConfigure.McastNotifyConfig mcastNotification =
    (EConfigure.notificationBuilder()).feedType(EConfigure.MultifeedType.LIST)
                                      .messageClass(messageClass)
                                      .subjectList(messageSubjects)
                                      .build();
final List<MulticastKey> mcastFeeds = com.google.common.collect.ImmutableList<>.of(mcastNotification);
final EConfigure.MulticastConnection mcastConfig =
    builder.name("McastConn_0")
           .role(EConfigure.MulticastRole.PUBLISHER)
           .group(group)
           .targetPort(5000)
           .networkInterface(netIF)
           .bindPort(5001)
           .protocolFamily(StandardProtocolFamily.INET)
           .byteOrder(ByteOrder.LITTLE_ENDIAN)
           .inputBufferSize(1_024)
           .outputBufferSize(1_024)
           .notifications(mcastFeeds)
           .build();

EMulticastConnection.openConnection(mcastConfig);
*

Monitoring Multicast Connection State

* If there is a need to monitor multicast connection state, * then subscribe to {@link MulticastMessage#MESSAGE_KEY}. This * can be done as follows: *
ESubsubscribeFeed.open(this,
                       MulticastMessage.MESSAGE_KEY,
                       FeedScope.LOCAL_ONLY,
                       null);
* * @author Charles W. Rapp */ public abstract class EMulticastConnection implements DatagramListener, EObject { //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Constants. // /** * The unique identifier for this JVM. */ protected static final UUID MCAST_ID = UUID.randomUUID(); //----------------------------------------------------------- // Statics. // /** * Logging subsystem interface. */ private static final Logger sLogger = LoggerFactory.getLogger(EMulticastConnection.class); /** * Existing multicast connections. */ private static final Map, EMulticastConnection> sConnections = new HashMap<>(); /** * Singleton responsible for publishing * {@link ConnectionMessage} to subscribers. */ private static final ConnectionPublisher sConnPublisher; // Class static initialization. static { // Register the multicast connection status reporter. (StatusReport.getsInstance()).register( EMulticastConnection::reportMulticastStatus); sConnPublisher = new ConnectionPublisher(); EFeed.register(sConnPublisher); EFeed.startup(sConnPublisher); } // end of class static initialization. //----------------------------------------------------------- // Locals. // /** * Multicast connection configuration. */ protected final MulticastConnection mConfig; /** * Multicast connection name. */ protected final String mName; /** * Join this multicast group. */ protected final InetAddress mGroup; /** * Post messages to this multicast group and port. */ protected final InetSocketAddress mTarget; /** * eBus client reference to this object. Used to post tasks * to the dispatcher thread. */ protected EClient mEClient; /** * Message key, subject pattern pair used to create the * multi-feed publisher or subscriber feed. Maps the * canonical message class name to the multicast key. */ protected final Map mMulticastKeys; /** * Maps a (JVM identifier, key identifier) pair to its * associated message key. When a multicast * packet is received, the JVM and key identifiers * (found in message header) are used to look up the * message key. In turn the message key used to decode the * eBus message. */ protected final Map, EMessageKey> mKeys; /** * Maps message key to its associated information. */ protected final Map mKeyIds; /** * This instance creation timestamp. */ private final Date mCreated; /** * Multicast key used in {@link #sConnections} map. */ private final MultiKey2 mMultiKey; /** * {@code MessageWriter} is responsible for encoding outbound * messages to the multicast group. */ private final MessageWriter mWriter; /** * Multicast group membership keys. There is one for each * publisher source. If no sources are specified then this * set will contain the single unsourced key. */ private final List mMcastKeys; /** * Set to {@code true} when joined to a multicast group. */ private boolean mIsJoined; /** * Set to {@code true} if at least one of the multicast * notification keys is dynamic. */ protected boolean mIsDynamic; /** * Asynchronous multicast socket. */ private AsyncMulticastSocket mMcastSocket; /** * Count up the number of attempted multicast transmit * attempts. */ private int mTransmitAttemptCount; /** * Number of successful multicast transmits. */ private int mTransmitSuccessCount; /** * Number of failed multicast transmits. */ private int mTransmitFailCount; /** * Number of datagram messages received. */ private int mReceiveCount; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * Creates a new multicast connection instance based on the * given configuration. * @param config multicast connection configuration. * @throws IllegalArgumentException * if {@code config} contains an invalid notification * message class. */ protected EMulticastConnection(final MulticastConnection config) { mConfig = config; mName = config.name(); mGroup = config.group(); mTarget = config.groupAddress(); mCreated = new Date(); mMultiKey = new MultiKey2<>(config.role(), config.group()); mKeys = new HashMap<>(); mKeyIds = new HashMap<>(); mWriter = new MessageWriter(); mMulticastKeys = loadKeys(config.notifications()); mIsJoined = false; mIsDynamic = false; mMcastKeys = new ArrayList<>(); mTransmitAttemptCount = 0; mTransmitSuccessCount = 0; mTransmitFailCount = 0; mReceiveCount = 0; } // end of EMulticastConnection(MulticastConnection) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // Abstract Method Declarations. // /** * Called when the multicast connection is opened * but not yet joined to multicast group. Subclass should * open multi-feeds at this time but not advertise or * subscribe. */ protected abstract void onOpen(); /** * Called when multicast group is successfully joined. * Subclass should advertise or subscribe its multi-feeds. */ protected abstract void onConnect(); /** * Called prior to disconnecting multicast group. * Allows subclass to report updated feed state. */ protected abstract void onDisconnect(); /** * Called when a message is received from the given source * address. Note that the message may be either a * notification or a system message. * @param msg inbound eBus message. * @param source inbound message source. */ protected abstract void onMessage(final EMessage msg, final InetSocketAddress source); /** * Called when multicast connection is disconnected from the * multicast group and closed. Subclass should close its * multi-feeds at this time. Subclass should not * attempt to send any messages since the multicast * connection is now closed. */ protected abstract void onClose(); // // end of Abstract Method Declarations. //----------------------------------------------------------- //----------------------------------------------------------- // EObject Interface Implementation. // /** * Initializes system message decoders and then begins * joining multicast group. */ @Override public void startup() { sLogger.info("{}: starting multicast connection.", mName); initializeReaders(); connect(mConfig); } // end of startup() /** * Disconnects from multicast group and closes multicast * socket. */ @Override public void shutdown() { sLogger.info("{}: stopping multicast connection.", mName); close(); } // end of shutdown() // // end of EObject Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // DatagramListener Interface Implementation. // /** * Decodes the given input into an eBus message and forwards * message to {@link #onMessage(EMessage, InetSocketAddress)}. * Does nothing if this multicast connection is no longer * joined to the multicast group or {@code buffer} contains * a message not supported by this multicast connection. In * either case {@code buffer} is emptied before returning. *

* Do not call this method. This method is * part of the {@link AsyncMulticastSocket} listener API and * meant to be called in that context. *

* @param buffer contains encoded eBus message. * @param address {@code buffer} contents were transmitted * from this source. * @param socket input received by this datagram socket. */ @Override public void handleInput(final ByteBuffer buffer, final InetSocketAddress address, final AbstractAsyncDatagramSocket socket) { final int limit = buffer.limit(); sLogger.trace( "{}: received {} bytes from {}:{}, state={}.", mName, buffer.limit(), address.getHostString(), address.getPort(), (mIsJoined ? "joined" : "not joined")); // Are we still joined to the multicast group? if (mIsJoined) { // Yes. Decode the inbound message and have the // subclass handle it. Pass along the source address // since that is needed for decoding. processInput(buffer, address); } // Not joined to the multicast group. // Clear the buffer by setting the position to its limit. buffer.position(limit); } // end of handleInput(... /** * Logs the multicast socket exception. * @param t log this exception. * @param socket error occurred on this multicast socket. */ @Override public void handleError(final Throwable t, final AbstractAsyncDatagramSocket socket) { sLogger.warn("{}: multicast socket error.", mName, t); } // end of handleError(...) // // end of DatagramListener Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // Get Methods. // /** * Returns unique multicast name. * @return multicast name. */ @Override public final String name() { return (mName); } // end of name() /** * Returns {@code true} if this multicast connection is * currently joined to its target multicast group and * {@code false} if not joined. * @return {@code true} if joined to multicast connection. */ public final boolean isJoined() { return (mIsJoined); } // end of isJoined() // // end of Get Methods. //----------------------------------------------------------- /** * Returns a multicast connection according to the given * multicast connection configuration. If the multicast * role is {@link MulticastRole#PUBLISHER} then creates a * {@link EMulticastPublisher} instance; otherwise creates * {@link EMulticastSubscriber}. The actual multicast group * join is performed asynchronously. It recommended that * an application subscribe to the message key * {@code net.sf.eBus.client.MulticastMessage:/eBus/multicast} * (see {@link MulticastMessage#MESSAGE_KEY}) in order to * receive updates on multicast join state. * @param config multicast connection configuration. * @return publisher or subscriber multicast connection. * @throws NullPointerException * if {@code config} is {@code null}. * @throws IllegalStateException * if there is already a multicast connection for the given * role and group. */ public static EMulticastConnection openConnection(final MulticastConnection config) { Objects.requireNonNull(config, "config is null"); final MulticastRole role = config.role(); final InetAddress group = config.group(); final MultiKey2 key = new MultiKey2<>(role, group); final EMulticastConnection retval; if (sConnections.containsKey(key)) { throw ( new IllegalStateException( String.format( "already connected to %s as %s", group.getHostAddress(), role))); } sLogger.debug( "Opening multicast connection {} to {} (net I/F {}) as {}.", config.name(), config.groupAddress(), config.networkInterface(), role); if (config.role() == MulticastRole.PUBLISHER) { retval = new EMulticastPublisher(config); } else { retval = new EMulticastSubscriber(config); } sConnections.put(key, retval); // Multicast connections are remote clients. retval.mEClient = EClient.findOrCreateClient( retval, ClientLocation.REMOTE); (retval.mEClient).dispatch(retval::startup); return (retval); } // end of openConnection(MulticastConnection) /** * Disconnects from the multicast group, closes the * datagram socket, and removes this multicast connection * from the connections map. This action permanently close * this multicast connection. If the connection is needed * again then * {@link #openConnection(net.sf.eBus.config.EConfigure.MulticastConnection)} * must be called again to re-create this multicast * connection. * * @see #openConnection(net.sf.eBus.config.EConfigure.MulticastConnection) */ public final void close() { final AsyncMulticastSocket mcastSocket = mMcastSocket; sLogger.debug( "Closing multicast connection {} to {} (net I/F {}) as {}.", mName, mTarget, mConfig.networkInterface(), mConfig.role()); // Is this multicast connection open? if (mcastSocket != null) { // Yes. Is it joined to any groups? if (mIsJoined) { final List keys = ImmutableList.copyOf(mMcastKeys); // Yes. Give subclass chance to clean up before // dropping the membership. onDisconnect(); // Drop the group memberships. mMcastKeys.clear(); keys.forEach(MembershipKey::drop); mIsJoined = false; } mcastSocket.closeNow(); mMcastSocket = null; sConnPublisher.publish( mName, mGroup, MulticastState.DISCONNECTED); // Give subclass a chance now to clean up. onClose(); } sConnections.remove(mMultiKey); } // end of doClose() /** * Posts eBus header and message to the joined multicast * group. * @param header send this eBus header and message. * @param target send encoded message to this address. May * be either a multicast group and port and a unicast * datagram address and port. * @throws IllegalStateException * if not currently joined to multicast group. */ protected void send(final EMessageHeader header, final InetSocketAddress target) { if (mMcastSocket == null) { throw ( new IllegalStateException( "multicast socket closed")); } if (!mIsJoined) { throw ( new IllegalStateException( "not joined to multicast group")); } if (sLogger.isTraceEnabled()) { sLogger.trace( "{}: sending message to {}:{}:\n{}", mName, target.getHostString(), target.getPort(), header.message()); } else { sLogger.debug( "{}: sending {} message to {}:{}.", mName, header.messageKey(), target.getHostString(), target.getPort()); } try { ++mTransmitAttemptCount; mWriter.post(header, target); mMcastSocket.send(mWriter); ++mTransmitSuccessCount; } catch (BufferOverflowException | IOException jex) { sLogger.warn( "{}: failed to send {} to {}.", mName, (header.messageClass()).getCanonicalName(), target, jex); ++mTransmitFailCount; } } // end of send(EMessageHeader) /** * Opens a multicast connect as per * {@link EConfigure#multicastConnections() }. * @param config contains multicast connection configuration. */ /* package */ static void configure(final EConfigure config) { final Collection conns = (config.multicastConnections()).values(); conns.forEach(EMulticastConnection::openConnection); } // end of configure(EConfigure) /** * Converts the multicast key configurations to * {@link MulticastKey} instances. * @param config contains multicast key configuration. * @return multicast key set. * @throws IllegalArgumentException * if {@code config} contains an invalid notification * message class. */ @SuppressWarnings ("unchecked") private static Map loadKeys(final List config) { String className; final Map retval = new HashMap<>(); for (McastNotifyConfig mnc : config) { className = mnc.messageClass(); try { retval.put(className, new MulticastKey(mnc)); } catch (ClassNotFoundException classex) { throw ( new IllegalArgumentException( className + " is not a known class", classex)); } catch (ClassCastException castex) { throw ( new IllegalArgumentException( className + " is not a notification message class", castex)); } } return (retval); } // end of loadKeys(List<>) /** * Initializes {@link #mKeys} and {@link #mKeyIds} maps to * the multicast system messages. Application notification * messages are added only by {@code EMulticastSubscriber} * when {@code McastKeyMessage} is received. */ private void initializeReaders() { final SystemMessageType[] types = SystemMessageType.values(); final int size = types.length; int keyId; int index; Class mc; MessageType mt; MultiKey2 mapKey; EMessageKey msgKey; sLogger.debug("{}: initializing message readers.", mName); // Add multicast system message input readers to map. for (index = 0; index < size; ++index) { if (types[index].isMulticast()) { keyId = types[index].keyId(); mc = types[index].messageClass(); mt = (MessageType) DataType.findType(mc); mapKey = new MultiKey2<>(null, keyId); msgKey = new EMessageKey( mc, ESystemMessage.SYSTEM_SUBJECT); mKeys.put(mapKey, msgKey); mKeyIds.put(msgKey, new KeyInfo( msgKey, null, MCAST_ID, keyId, // System message feeds are always up. EFeedState.UP, new MessageReader( keyId, ESystemMessage.SYSTEM_SUBJECT, mt))); } } } // end of initializeReaders() /** * Asynchronously performs multicast group join. Creates * {@link AsyncMulticastSocket}, configured according to the * given multicast connection configuration. First opens the * multicast datagram channel and performs role-specific * work. Then joins the multicast group or groups depending * on whether source addresses where provided. Role-specific * post-join actions are then taken. *

* After successfully joining a {@link MulticastMessage} is * published to {@link MulticastMessage#MESSAGE_KEY} with * a multicast connection state set to * {@code JOINED}. *

* @param config multicast connection configuration. */ private void connect(final MulticastConnection config) { final InetSocketAddress bindAddr = new InetSocketAddress( config.bindAddress(), config.bindPort()); final ProtocolFamily family = config.protocolFamily(); final NetworkInterface netIf = config.networkInterface(); final MulticastBuilder builder = AsyncMulticastSocket.builder(); sLogger.debug( "{}: creating multicast socket:\n{}", mName, config); mMcastSocket = builder.byteOrder(config.byteOrder()) .inputBufferSize(config.inputBufferSize()) .outputBufferSize(config.outputBufferSize()) .selector(config.selector()) .listener(this) .build(); try { final InetAddress group = config.group(); final List sources = config.sources(); sLogger.info( "{}: connecting to multicast group {} on network I/F {}.", mName, group.getHostName(), netIf.getDisplayName()); mMcastSocket.open(bindAddr, family, netIf); // Successfully opened multicast socket. onOpen(); sLogger.info( "{}: joining multicast group {} on network I/F {}.", mName, group.getHostName(), netIf.getDisplayName()); // Are there any sources for this multicast // subscriber? if (sources == null || sources.isEmpty()) { // No, join the multicast group without a source. mMcastKeys.add(mMcastSocket.join(group, netIf)); } // Yes, there are sources. Create a membership key // for each source. else { for (InetAddress source : sources) { mMcastKeys.add( mMcastSocket.join(group, netIf, source)); } } mIsJoined = true; sLogger.debug( "{}: joined to multicast group {} on network I/F {}.", mName, group.getHostName(), netIf.getDisplayName()); sConnPublisher.publish( mName, mGroup, MulticastState.JOINED); // Successfully joined multicast group. onConnect(); } catch (IOException | UnsupportedOperationException | IllegalArgumentException jex) { sLogger.warn( "{}: failed to open multicast socket (bind address={}, family={}, net I/F={})", config.name(), bindAddr, family, netIf.getDisplayName(), jex); } } // end of connect(MulticastConnection) /** * Decodes eBus message contained in {@code buffer} and * forwards that message to role-specific method * {@link #onMessage(EMessage, InetSocketAddress)}. * @param buffer buffer containing eBus message. * @param source message source address. */ private void processInput(final ByteBuffer buffer, final InetSocketAddress source) { if (sLogger.isTraceEnabled()) { final int rem = buffer.remaining(); final int pos = buffer.position(); final int ms = buffer.getInt(pos); final byte[] data = new byte[rem]; buffer.mark(); buffer.get(data); buffer.reset(); sLogger.trace( "{}:{}: {} bytes available (start={}, msg size={}):\n{}", source.getHostString(), source.getPort(), rem, pos, ms, new LazyString( () -> HexDump.dump(data, 0, rem, " "))); } else { sLogger.debug("{}: {} bytes available.", source, buffer.remaining()); } final int messageSize = buffer.getInt(); // The message size must be at least big enough for // the message header or is within the maximum // allowed message size. if (messageSize < MESSAGE_HEADER_SIZE || messageSize > MAX_MESSAGE_SIZE) { sLogger.warn("{}: invalid message size ({} bytes).", mName, messageSize); } else { decode(buffer, source); } } // end of processInput(ByteBuffer, InetSocketAddress) /** * Decodes eBus message contained in {@code buffer} and then * forwards decoded message to abstract method * {@link #onMessage(EMessage, InetSocketAddress)}. Does * nothing if {@code buffer} contains a message that is not * supported by this multicast connection. * @param buffer contains latest eBus message. * @param source message posted from this network location. */ private void decode(final ByteBuffer buffer, final InetSocketAddress source) { final UUID mcastId = new UUID(buffer.getLong(), buffer.getLong()); final int keyId = buffer.getInt(); // If the keyId is < 0 that means this is a system // message. In that case set the source to null. final MultiKey2 infoKey = new MultiKey2<>((keyId < 0 ? null : mcastId), keyId); final EMessageKey msgKey = mKeys.get(infoKey); final KeyInfo keyInfo = mKeyIds.get(msgKey); sLogger.trace( "{}: info key {} -> message key {}, keyInfo {}.", mName, infoKey, msgKey, keyInfo); // Is this a supported message type? if (keyInfo != null) { // Yes. Decode the inbound message and pass on to the // publisher/subscriber. try { final EMessageHeader header = (keyInfo.reader()).extractMessage( buffer, source); ++mReceiveCount; onMessage(header.message(), source); } catch (BufferUnderflowException | InvalidMessageException | UnknownMessageException jex) { sLogger.warn("{}: failed to decode {}.", mName, msgKey, jex); } } } // end of decode(ByteBuffer, InetSocketAddress) /** * Appends the status of all multicast connections to status * report. * @param report append multicast connection status to * this report. */ private static void reportMulticastStatus(final PrintWriter report) { final int count = sConnections.size(); report.print("Multicast connections: "); if (count == 0) { report.println("no multicast connections."); } else { final Collection connections = new ArrayList<>(sConnections.values()); int index = 0; report.format( "there %s %,d multicast %s.%n", (count == 1 ? "is" : "are"), count, (count == 1 ? "connection" : "connections")); for (EMulticastConnection conn : connections) { conn.reportStatus(report, index); report.println(); ++index; } } } // end of reportMulticastStatus(PrintWriter) /** * Appends this multicast connection's status to the status * report. * @param report append status to this report. * @param index connection map index. */ @SuppressWarnings({"java:S3398"}) private void reportStatus(final PrintWriter report, final int index) { int i = 0; report.format(" [%,d] address: %s%n", index, mTarget) .format( " created on %1$tY-%1$tm-%1$td @ %1$tH:%1$tM:%1$tS.%1$tL%n", mCreated) .format(" joined: %b%n", mIsJoined) .format("attempted xmits: %,d%n", mTransmitAttemptCount) .format("completed xmits: %,d%n", mTransmitSuccessCount) .format(" failed xmits: %,d%n", mTransmitFailCount) .format(" received: %,d%n", mReceiveCount) .format(" subscriptions:%n"); for (KeyInfo info : mKeyIds.values()) { report.format(" [%2d] %s%n", i, info); ++i; } } // end of reportStatus(PrintWriter, int) //--------------------------------------------------------------- // Inner classes. // /** * Contains multi-feed notification key. The difference * between this class and {@link McastNotifyConfig} is that * the message class is stored here as a {@code Class} * instance while {@code McastNotifyConfig} stores it as * text. Also, this class provides the * {@link #matches(String)} method. */ protected static final class MulticastKey implements Comparable { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * Either a list or query notification multi-feed. */ private final MultifeedType mType; /** * Notification message class used to create a * {@link EMessageKey}. */ private final Class mMessageClass; /** * Multi-feed subject list. */ private final List mSubjectList; /** * Pattern used to match patterns. */ private final Pattern mSubjectQuery; /** * Set to {@code true} if this is a dynamic,query-based * multi-feed. */ private final boolean mIsDynamic; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a multicast multi-feed keys based on given * configuration. * @param config multicast multi-feed configuration. * @throws ClassNotFoundException * if {@code config} contains an unknown message class. * @throws ClassCastException * if {@code config} contains a non-notification message * class. */ @SuppressWarnings ("unchecked") private MulticastKey(final McastNotifyConfig config) throws ClassNotFoundException { mType = config.feedType(); mMessageClass = (Class) Class.forName(config.messageClass()); mSubjectList = config.subjectList(); mSubjectQuery = config.subjectQuery(); mIsDynamic = config.isDynamic(); } // end of MulticastKey(...) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Comparable Interface Implementation. // /** * Returns an integer value < 0, zero, or > 0 * if {@code this MulticastKey} is <, equal to, or * > than the given key. Comparison is based on the * message key class name only. * @param o comparison multicast key. * @return integer value. */ @Override public int compareTo(final MulticastKey o) { final String name0 = mMessageClass.getCanonicalName(); final String name1 = (o.mMessageClass).getCanonicalName(); return (name0.compareTo(name1)); } // end of compareTo(MulticastKey) // // end of Comparable Interface Implementation. //------------------------------------------------------- //------------------------------------------------------- // Object Method Overrides. // /** * Returns text containing this multicast key's values. * @return multicast key as text. */ @Override public String toString() { final StringBuilder retval = new StringBuilder(); retval.append("[type=").append(mType) .append(", class=") .append(mMessageClass.getCanonicalName()); if (mType == MultifeedType.QUERY) { retval.append(", query=") .append(mSubjectQuery.pattern()) .append(", is dynamic=") .append(mIsDynamic); } else { String sep = ""; retval.append(", list={"); for (String subject : mSubjectList) { retval.append(sep) .append("\"") .append(subject) .append("\""); sep = ", "; } retval.append("}"); } return (retval.append("]").toString()); } // end of toString() /** * Returns {@code true} if this multicast key is equal * to the given object. Equality is based on message * class name only. * @param o check for equality with this object. * @return {@code true} if {@code this MulticastKey} * equals {@code o}. */ @Override public boolean equals(final Object o) { boolean retcode = (this == o); if (!retcode && o instanceof MulticastKey) { final MulticastKey key = (MulticastKey) o; retcode = mMessageClass.equals(key.mMessageClass); } return (retcode); } // end of equals(Object) /** * Returns hash code based on the message class. * @return message class hash code. */ @Override public int hashCode() { return (mMessageClass.hashCode()); } // end of hashCode() // // end of Object Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Get Methods. // /** * Returns multi-feed type. * @return multi-feed type. */ public MultifeedType feedType() { return (mType); } // end of feedType() /** * Returns notification message class. * @return notification message class. */ public Class messageClass() { return (mMessageClass); } // end of messageClass() /** * Returns multi-feed subject list. If * {@link #feedType()} is not {@link MultifeedType#LIST} * then returns {@code null}. * @return multi-feed subject list. * * @see #subjectQuery() */ public List subjectList() { return (mSubjectList); } // end of subjectList() /** * Returns multi-feed subject query. If * {@link #feedType()} is not {@link MultifeedType#QUERY} * then returns {@code null}. * @return multi-feed subject query. * * @see #subjectList() * @see #isDynamic() */ public Pattern subjectQuery() { return (mSubjectQuery); } // end of subjectQuery() /** * Returns {@code true} if this multi-feed is a dynamic * query; otherwise returns {@code false}. * @return {@code true} if multi-feed is dynamic query. * * @see #subjectQuery() */ public boolean isDynamic() { return (mIsDynamic); } // end of isDynamic() // // end of Get Methods. //------------------------------------------------------- /** * Returns {@code true} if this notification multi-feed * is: *
    *
  1. * a {@link MultifeedType#QUERY} and *
  2. *
  3. * this feed is dynamic and *
  4. *
  5. * {@code subject} matches * {@code this.subjectQuery()} *
  6. *
* Otherwise returns {@code false}. * @param subject check if this message subject matches * the subject query. * @return {@code true} if {@code subject} matches this * multicast key. */ public boolean matches(final String subject) { return (mIsDynamic && mType == MultifeedType.QUERY && mSubjectQuery.matches(subject)); } // end of matches(EMessageKey) } // end of class MulticastKey /** * Contains a message keys's unique integer identifier, * source address and its current feed state. */ protected static final class KeyInfo { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * Information is for this message key. */ private final EMessageKey mKey; /** * Source address associated with the message key. Will * be {@code null} if message key is a system message. */ private final InetSocketAddress mSource; /** * Message key identifier applies to this JVM. */ private final UUID mMcastId; /** * Unique message key identifier. */ private final int mKeyId; /** * Calculate the hash code once. */ private final int mHashCode; /** * Associated message decoder. Set for inbound messages * only. */ private final MessageReader mReader; /** * Multicast publishers currently feeding this message * key. Used by multicast subscriber to keep track of * a message key feed state. As long as there is one * multicast publisher with an up feed state, then * {@link #mFeedState} is set to up. *

* This data member is not used by * {@code EMulticastPublisher}. *

*/ private final List mPublishers; /** * Current message feed state. Initialized to * {@code UNKNOWN}. */ private EFeedState mFeedState; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a multicast key information instance for the * given message key, associated source address, unique * message key identifier, and feed state. * @param key multicast notification message key. * @param source publisher source address. * @param mcastId message key info is from this JVM. * @param id {@code key}'s unique integer identifier. * @param state current notification feed state. */ protected KeyInfo(final EMessageKey key, final InetSocketAddress source, final UUID mcastId, final int id, final EFeedState state) { this (key, source, mcastId, id, state, null); } // end of KeyInfo(...) /** * Creates a multicast key information instance for the * given message key, associated source address, unique * message key identifier, and feed state. * @param key multicast notification message key. * @param source publisher source address. * @param mcastId message key info is from this JVM. * @param id {@code key}'s unique integer identifier. * @param state current notification feed state. * @param reader inbound message decoder. */ protected KeyInfo(final EMessageKey key, final InetSocketAddress source, final UUID mcastId, final int id, final EFeedState state, final MessageReader reader) { mKey = key; mSource = source; mMcastId = mcastId; mKeyId = id; mHashCode = Objects.hash(source, mcastId, id); mReader = reader; mPublishers = new ArrayList<>(); mFeedState = state; } // end of KeyInfo(...) /** * Creates a temporary key info instance used to report * that the message key feed is down on the multicast * group. * @param keyInfo copy information from this key. * @param state copy key state. */ protected KeyInfo(final KeyInfo keyInfo, final EFeedState state) { mKey = keyInfo.mKey; mSource = keyInfo.mSource; mMcastId = keyInfo.mMcastId; mKeyId = keyInfo.mKeyId; mHashCode = keyInfo.mHashCode; mReader = keyInfo.mReader; mPublishers = null; mFeedState = state; } // end of KeyInfo(KeyInfo, EFeedState) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Object Method Overrides. // /** * Returns text describing this multicast notification * message key. * @return {@code KeyInfo} described as text. */ @Override public String toString() { final StringBuilder retval = new StringBuilder(); return (retval.append("[key=").append(mKey) .append(", source=") .append(mSource == null ? "" : mSource) .append(", id=").append(mKeyId) .append(", state=").append(mFeedState) .append("]") .toString()); } // end of toString() /** * Returns {@code true} if {@code o} is a * non-{@code null KeyInfo} instance with equal source * address, JVM and key identifiers; otherwise returns * {@code false}. * @param o comparison object. * @return {@code true} if the {@code this KeyInfo} * instance equals {@code o}. */ @Override public boolean equals(final Object o) { boolean retcode = (this == o); if (!retcode && o instanceof KeyInfo) { final KeyInfo info = (KeyInfo) o; retcode = (Objects.equals(mSource, info.mSource) && Objects.equals(mMcastId, info.mMcastId) && mKeyId == info.mKeyId); } return (retcode); } // end of equals(Object) /** * Returns hash code based on source address and unique * key identifier. * @return {@code KeyInfo} instance hash code. */ @Override public int hashCode() { return (mHashCode); } // end of hashCode() // // end of Object Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Get Methods. // /** * Returns notification message key. * @return notification message key. */ public EMessageKey key() { return (mKey); } // end of key() /** * Returns message source address. If message key is for * a system message then returns {@code null}. * @return message source address. */ public InetSocketAddress source() { return (mSource); } // end of source() /** * Returns unique multicast identifier. * @return multicast identifier. */ public UUID mcastId() { return (mMcastId); } // end of mcastId() /** * Returns integer identifier for message key. This * identifier is unique for the given source address. * It is possible that difference sources will assign the * same identifier for the same message key. * @return message key identifier. */ public int keyId() { return (mKeyId); } // end of keyId() /** * Returns the current feed state for message key. * @return message key feed state. */ public EFeedState state() { return (mFeedState); } // end of state() /** * Returns the inbound message decoder. Returns * {@code null} if this {@code KeyInfo} instance is for * outbound messages only. * @return inbound message decoder. */ public MessageReader reader() { return (mReader); } // end of reader() // // end of Get Methods. //------------------------------------------------------- //------------------------------------------------------- // Set Methods. // /** * Updates the message key feed state to the given value. * @param state latest message key feed state. */ public void state(final EFeedState state) { mFeedState = state; } // end of state(EFeedState) /** * Updates publishers list by adding source address if * {@code state} is up and removing address if down. * Returns {@code true} if this changes the multicast * feed state and {@code false} if feed state remains * unchanged. * @param state multicast source feed state. * @param source multicast source. * @return {@code true} if feed state has changed. */ public boolean update(final EFeedState state, final InetSocketAddress source) { final EFeedState initialState = mFeedState; // Add or remove publisher? // Add only if source is not already a listed // publisher. if (state == EFeedState.UP && !mPublishers.contains(source)) { // Add. mPublishers.add(source); } // Is the feed state down or unknown? else if (state != EFeedState.UP) { // Remove. mPublishers.remove(source); } mFeedState = (mPublishers.isEmpty() ? EFeedState.DOWN : EFeedState.UP); sLogger.trace("{}: {} {}, # publishers is {}.", mKey, (state == EFeedState.UP ? "added" : "removed"), source, mPublishers.size()); return (mFeedState != initialState); } // end of update(EFeedState, InetSocketAddress) // // end of Set Methods. //------------------------------------------------------- } // end of class KeyInfo /** * This class is responsible for decoding the eBus header and * message from an {@link AsyncMulticastSocket}'s underlying * inbound buffer. */ protected static final class MessageReader { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * A unique message key identifier received from the * remote eBus application. */ private final int mKeyId; /** * The message key subject. Needed to construct inbound * messages. */ private final String mSubject; /** * The message type for deserializing the message class. */ private final MessageType mMessageType; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new multicast message reader for the given * message key identifier, notification subject, and * message data type. * @param keyId associated message key identifier. * @param subject associated notification subject. * @param mt message data type containing decoder. */ protected MessageReader(final int keyId, final String subject, final MessageType mt) { mKeyId = keyId; mSubject = subject; mMessageType = mt; } // end of MessageReader(int, String, MessageType) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Get Methods. // /** * Returns the message key identifier. * @return message key identifier. */ public int keyId() { return (mKeyId); } // end of keyId() public Class messageClass() { return (mMessageType.dataClass()); } // end of messageClass() /** * Returns message key subject. * @return message key subject. */ public String subject() { return (mSubject); } // end of subject() // // end of Get Methods. //------------------------------------------------------- /** * Returns the message header extracted from the given * buffer starting at the buffer's current position. * Note that the message size and message key identifier * have already been extracted from {@code buffer}. * @param buffer extract the message from this buffer. * @param source data came from this source address. * @return the extracted message header and message. * @throws BufferUnderflowException * if {@code buffer} contains an incomplete message. * @throws UnknownMessageException * if the de-serialized class identifier references an * unknown message class. * @throws InvalidMessageException * if the message construction fails. */ private EMessageHeader extractMessage(final ByteBuffer buffer, final InetSocketAddress source) { // Note: JVM and class identifiers already extracted // by EMulticastConnect.decode(). final int fromFeedId = buffer.getInt(); final int toFeedId = buffer.getInt(); // Need to pass in the message subject explicitly // because it is not longer serialized. Rather the // subject is inferred from the message key // identifier. final EMessage msg = (EMessage) mMessageType.deserialize(mSubject, buffer); return ( new EMessageHeader( mKeyId, fromFeedId, toFeedId, source, msg)); } // end of extractMessage(ByteBuffer, InetSocketAddress) } // end of class MessageReader /** * This class is responsible for serializing eBus message * header and message directly to the underlying * {@link AsyncMulticastSocket} outbound buffer. */ private static final class MessageWriter implements DatagramBufferWriter { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * Post outbound messages to this multicast group and * port. */ private InetSocketAddress mTarget; /** * Store next outbound message here. */ private final Queue mTransmitQueue; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new outbound message writer for the parent * multicast connection. */ private MessageWriter() { mTransmitQueue = new ConcurrentLinkedQueue<>(); } // end of MessageWriter() // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // DatagramBufferWriter Interface Implementation. // /** * Encodes the currently set eBus message to the given * buffer. * @param buffer place encoded eBus message into this * location. * @return target multicast group address. */ @Override public SocketAddress fill(ByteBuffer buffer) { final EMessageHeader header = mTransmitQueue.poll(); final DataType msgType = header.dataType(); final int sizePosition = buffer.position(); final int size; // Move past the message size bytes and fill in the // rest of the header and message. buffer.position(sizePosition + MESSAGE_SIZE_SIZE); // Multicast messages start with the multicast // identifier. buffer.putLong(MCAST_ID.getMostSignificantBits()) .putLong(MCAST_ID.getLeastSignificantBits()) .putInt(header.classId()) .putInt(header.fromFeedId()) .putInt(header.toFeedId()); msgType.serialize(header.message(), null, buffer); // Now write out the message size at the initial // position. size = (buffer.position() - sizePosition); buffer.putInt(sizePosition, size); return (mTarget); } // end of fill(ByteBuffer) // // end of DatagramBufferWriter Interface Implementation. //------------------------------------------------------- /** * Store this next outbound eBus message header. It is * expected that * {@link AsyncMulticastSocket#send(DatagramBufferWriter)} * will be called immediately after calling this method. * @param header eBus message header. * @param target send message to this target address. */ private void post(final EMessageHeader header, final InetSocketAddress target) { mTransmitQueue.add(header); mTarget = target; } // end of post(EMessageHeader) } // end of class MessageWriter /** * This singleton is responsible for locally publishing * multicast connection state updates to subscribers. This * class is necessary since multicast connections are a * remote client by definition and {@link ConnectionMessage} * has local scope. */ private static final class ConnectionPublisher implements EPublisher { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Constants. // /** * eBus object name is {@value}. */ public static final String OBJECT_NAME = "MulticastConnectionPublisher"; //------------------------------------------------------- // Locals. // /** * The {@link ConnectionMessage} notification feed. */ private EPublishFeed mStateFeed; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * {@code private} default constructor to prevent * instantiation outside {@code EMulticastConnection}. */ private ConnectionPublisher() {} // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // EObject Interface Implementation. // /** * Returns {@link #OBJECT_NAME} as eBus object name. * @return eBus object name. */ @Override public String name() { return (OBJECT_NAME); } // end of name() /** * Open {@link MulticastMessage} notification feed. */ @Override public void startup() { final EPublishFeed.Builder builder = EPublishFeed.builder(); // Open the connect state feed and put its // advertisement in place. mStateFeed = builder.target(this) .messageKey(MulticastMessage.MESSAGE_KEY) .scope(FeedScope.LOCAL_ONLY) .build(); mStateFeed.advertise(); mStateFeed.updateFeedState(EFeedState.UP); } // 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 * multicast connection. * @param feedState either up or down. * @param feed the connection state feed. */ @Override public void publishStatus(final EFeedState feedState, final IEPublishFeed feed) { sLogger.debug("{} feed is {}.", 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. final MulticastMessage.Builder builder = MulticastMessage.builder(); sConnections.values() .forEach(conn -> mStateFeed.publish(builder.subject(mStateFeed.messageSubject()) .connectionName(conn.mName) .multicastGroup(conn.mGroup) .state(conn.mIsJoined ? MulticastState.JOINED : MulticastState.DISCONNECTED) .build())); } // The feed is down. Nothing to send. } // end of publishStatus(EFeedState, IEPublishFeed) // // end of EPublisher Interface Implementation. //------------------------------------------------------- /** * Publishes a {@link MulticastMessage} with the given * information if the feed is up. * @param name multicast connection name. * @param group connection state change applies to this * multicast group. * @param state new multicast connection state. */ private void publish(final String name, final InetAddress group, final MulticastState state) { if (mStateFeed.isFeedUp()) { final MulticastMessage.Builder builder = MulticastMessage.builder(); mStateFeed.publish( builder.subject(mStateFeed.messageSubject()) .connectionName(name) .multicastGroup(group) .state(state) .build()); } } // end of publish(...) } // end of class ConnectionPublisher } // end of class EMulticastConnection




© 2015 - 2024 Weber Informatics LLC | Privacy Policy