/* * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates. * * 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 org.jboss.errai.bus.client.framework; import static org.jboss.errai.bus.client.protocols.BusCommand.RemoteSubscribe; import static org.jboss.errai.bus.client.protocols.BusCommand.RemoteUnsubscribe; import static org.jboss.errai.bus.client.util.BusToolsCli.isRemoteCommunicationEnabled; import static org.jboss.errai.common.client.protocols.MessageParts.PriorityProcessing; import static org.jboss.errai.common.client.protocols.MessageParts.Subject; import static org.jboss.errai.common.client.protocols.MessageParts.ToSubject; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.jboss.errai.bus.client.api.BusLifecycleEvent; import org.jboss.errai.bus.client.api.BusLifecycleListener; import org.jboss.errai.bus.client.api.BusMonitor; import org.jboss.errai.bus.client.api.ClientMessageBus; import org.jboss.errai.bus.client.api.RoutingFlag; import org.jboss.errai.bus.client.api.SubscribeListener; import org.jboss.errai.bus.client.api.Subscription; import org.jboss.errai.bus.client.api.TransportError; import org.jboss.errai.bus.client.api.TransportErrorHandler; import org.jboss.errai.bus.client.api.UnsubscribeListener; import org.jboss.errai.bus.client.api.base.Capabilities; import org.jboss.errai.bus.client.api.base.CommandMessage; import org.jboss.errai.bus.client.api.base.DefaultErrorCallback; import org.jboss.errai.bus.client.api.base.NoSubscribersToDeliverTo; import org.jboss.errai.bus.client.api.messaging.Message; import org.jboss.errai.bus.client.api.messaging.MessageCallback; import org.jboss.errai.bus.client.api.messaging.RequestDispatcher; import org.jboss.errai.bus.client.framework.transports.BusTransportError; import org.jboss.errai.bus.client.framework.transports.HttpPollingHandler; import org.jboss.errai.bus.client.framework.transports.SSEHandler; import org.jboss.errai.bus.client.framework.transports.TransportHandler; import org.jboss.errai.bus.client.framework.transports.WebsocketHandler; import org.jboss.errai.bus.client.protocols.BusCommand; import org.jboss.errai.bus.client.util.BusToolsCli; import org.jboss.errai.bus.client.util.ManagementConsole; import org.jboss.errai.common.client.api.Assert; import org.jboss.errai.common.client.api.extension.InitVotes; import org.jboss.errai.common.client.protocols.MessageParts; import org.jboss.errai.marshalling.client.api.MarshallerFramework; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gwt.core.client.GWT; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; /** * The default client <tt>MessageBus</tt> implementation. This bus runs in the browser and automatically federates * with the server immediately upon initialization. * * @author Mike Brock */ public class ClientMessageBusImpl implements ClientMessageBus { static { MarshallerFramework.initializeDefaultSessionProvider(); } String OUT_SERVICE_ENTRY_POINT; String IN_SERVICE_ENTRY_POINT; private final String clientId; private String sessionId; private final List<SubscribeListener> onSubscribeHooks = new ArrayList<>(); private final List<UnsubscribeListener> onUnsubscribeHooks = new ArrayList<>(); /** * Forwards every message received across the communication link to the remote * server bus. This is the mechanism by which local messages are routed to the * server bus. * <p> * One instance of this callback can be subscribed to any number of subjects * simultaneously. */ public final MessageCallback serverForwarder = new MessageCallback() { @Override public void callback(final Message message) { encodeAndTransmit(message); } }; /** * This callback processes all messages sent to the * {@link DefaultErrorCallback#CLIENT_ERROR_SUBJECT} on this bus. */ private final class ErrorProcessor implements MessageCallback { @Override public void callback(final Message message) { final String errorTo = message.get(String.class, MessageParts.ErrorTo); if (errorTo == null || DefaultErrorCallback.CLIENT_ERROR_SUBJECT.equals(errorTo)) { final Throwable t = message.get(Throwable.class, MessageParts.Throwable); if (GWT.getUncaughtExceptionHandler() != null) { GWT.getUncaughtExceptionHandler().onUncaughtException(t); } else { managementConsole.displayError(message.get(String.class, MessageParts.ErrorMessage), message.get(String.class, MessageParts.AdditionalDetails), null); } } else { message.toSubject(errorTo); message.set(MessageParts.ErrorTo, null); message.sendNowWith(ClientMessageBusImpl.this); } } } private final ErrorProcessor clientBusErrorsCallback = new ErrorProcessor(); /** * Processes bus protocol commands and passes protocol extensions to the * underlying transport handlers. Protocol commands include RemoteSubscribe, * SessionExpired, Disconnect, and so on. The complete set lives in the * {@link BusCommand} enum. */ private final class ProtocolCommandProcessor implements MessageCallback { @Override @SuppressWarnings({"unchecked"}) public void callback(final Message message) { final Logger logger = LoggerFactory.getLogger(getClass()); BusCommand busCommand; if (message.getCommandType() == null) { busCommand = BusCommand.Unknown; } else { busCommand = BusCommand.valueOf(message.getCommandType()); } if (busCommand == null) { busCommand = BusCommand.Unknown; } switch (busCommand) { case RemoteSubscribe: if (message.hasPart(MessageParts.SubjectsList)) { logger.info("remote services available: " + message.get(List.class, MessageParts.SubjectsList)); for (final String subject : (List<String>) message.get(List.class, MessageParts.SubjectsList)) { remoteSubscribe(subject); } } else { remoteSubscribe(message.get(String.class, Subject)); } break; case RemoteUnsubscribe: unsubscribeAll(message.get(String.class, Subject)); break; case FinishAssociation: sessionId = message.get(String.class, MessageParts.ConnectionSessionKey); logger.info("my queue session id: " + sessionId); processCapabilities(message); for (final String svc : message.get(String.class, MessageParts.RemoteServices).split(",")) { remoteSubscribe(svc); } remoteSubscribe(BuiltInServices.ServerBus.name()); if (!deferredSubscriptions.isEmpty()) { for (final Runnable deferredSubscription : deferredSubscriptions) { deferredSubscription.run(); } deferredSubscriptions.clear(); encodeAndTransmit(CommandMessage.create() .toSubject(BuiltInServices.ServerBus.name()).command(BusCommand.RemoteSubscribe) .set(PriorityProcessing, "1") .set(MessageParts.RemoteServices, getAdvertisableSubjects())); } // We don't want to declare the subscription listeners until after we've sent our initial state // to the bus. declareSubscriptionListeners(); setState(BusState.CONNECTED); sendAllDeferred(); InitVotes.voteFor(ClientMessageBus.class); logger.info("bus federated and running."); break; case SessionExpired: logger.info("session expired while in state " + getState() + ": attempting to reset ..."); // try to reconnect InitVotes.reset(); stop(false); init(); break; case Disconnect: stop(false); if (message.hasPart(MessageParts.Reason)) { managementConsole .displayError("The bus was disconnected by the server", "Reason: " + message.get(String.class, "Reason"), null); } break; case Heartbeat: case Resend: break; case Unknown: default: transportHandler.handleProtocolExtension(message); break; } } } private final ProtocolCommandProcessor protocolCommandCallback = new ProtocolCommandProcessor(); private Map<String, TransportHandler> availableHandlers; private final TransportHandler BOOTSTRAP_HANDLER = HttpPollingHandler.newNoPollingInstance(ClientMessageBusImpl.this); /** * The current transport handler that's in use. This field is never null; it bottoms out at the No-polling version * of HttpPollingHandler. */ private TransportHandler transportHandler = BOOTSTRAP_HANDLER; private final Map<String, List<MessageCallback>> subscriptions = new HashMap<>(); private final Map<String, List<MessageCallback>> localSubscriptions = new HashMap<>(); private final Map<String, List<MessageCallback>> shadowSubscriptions = new HashMap<>(); private final Map<String, MessageCallback> remotes = new HashMap<>(); private final List<TransportErrorHandler> transportErrorHandlers = new ArrayList<>(); private final List<Runnable> deferredSubscriptions = new ArrayList<>(); private final List<Message> deferredMessages = new ArrayList<>(); private final List<BusLifecycleListener> lifecycleListeners = new ArrayList<>(); private BusState state = BusState.UNINITIALIZED; private final ManagementConsole managementConsole; private final Map<String, String> properties = new HashMap<>(); private Timer initialConnectTimer; private static final Logger logger = LoggerFactory.getLogger(ClientMessageBus.class); public ClientMessageBusImpl() { setBusToInitializableState(); managementConsole = new ManagementConsole(this); clientId = String.valueOf(com.google.gwt.user.client.Random.nextInt(99999)) + "-" + (System.currentTimeMillis() % (com.google.gwt.user.client.Random.nextInt(99999) + 1)); IN_SERVICE_ENTRY_POINT = "in." + getClientId() + ".erraiBus"; OUT_SERVICE_ENTRY_POINT = "out." + getClientId() + ".erraiBus"; // when the window is closing, we want to stop the bus without causing any // errors (unless the server is unavailable of course) (see ERRAI-225) Window.addCloseHandler(new CloseHandler<Window>() { @Override public void onClose(final CloseEvent<Window> event) { if (state != BusState.LOCAL_ONLY) { stop(true); } } }); } private void setBusToInitializableState() { this.remotes.clear(); this.onSubscribeHooks.clear(); this.onUnsubscribeHooks.clear(); this.transportHandler = BOOTSTRAP_HANDLER; removeRpcResponseSubscriptions(); setupDefaultHandlers(); } private void removeRpcResponseSubscriptions() { final Iterator<String> iter = subscriptions.keySet().iterator(); while (iter.hasNext()) { final String topic = iter.next(); if (topic.endsWith(":RespondTo:RPC") || topic.endsWith(":Errors:RPC")) { iter.remove(); } } } private void setupDefaultHandlers() { if (availableHandlers != null) { for (final TransportHandler handler : availableHandlers.values()) { handler.close(); } } final Map<String, TransportHandler> m = new LinkedHashMap<>(); m.put(Capabilities.WebSockets.name(), new WebsocketHandler(ClientMessageBusImpl.this)); m.put(Capabilities.SSE.name(), new SSEHandler(ClientMessageBusImpl.this)); m.put(Capabilities.LongPolling.name(), HttpPollingHandler.newLongPollingInstance(ClientMessageBusImpl.this)); m.put(Capabilities.ShortPolling.name(), HttpPollingHandler.newShortPollingInstance(ClientMessageBusImpl.this)); availableHandlers = Collections.unmodifiableMap(m); } /** * Takes this message bus from the LOCAL_ONLY state into the CONNECTING state, * as long as remote communication is enabled. * <p/> * If this bus is not in the LOCAL_ONLY state when this method is called, this * method has no effect. * * @see org.jboss.errai.bus.client.util.BusToolsCli#isRemoteCommunicationEnabled() * @see BusLifecycleListener */ @Override public void init() { if (getState() == BusState.CONNECTED) { /** * This is an optimization to improve unit testing speed. If a test case * does not tear down the bus after each test, calling this will ensure * that any services dependent on the bus will still be loaded. * * It's very important that we call waitFor first because InitVotes is * reset between most tests. Calling voteFor has not effect without a * prior waitFor. */ InitVotes.waitFor(ClientMessageBus.class); InitVotes.voteFor(ClientMessageBus.class); return; } logger.info("bus initialization started ..."); setBusToInitializableState(); InitVotes.waitFor(ClientMessageBus.class); if (isRemoteCommunicationEnabled()) { remoteSubscribe(BuiltInServices.ServerEchoService.name()); } if (!isSubscribed(DefaultErrorCallback.CLIENT_ERROR_SUBJECT)) { directSubscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, clientBusErrorsCallback, false); } if (!isSubscribed(BuiltInServices.ClientBus.name())) { directSubscribe(BuiltInServices.ClientBus.name(), protocolCommandCallback, false); } // The purpose of this timer is to let the bus yield and give other modules a chance to register // services before we send our state synchronization message. This is not strictly necessary // but significantly decreases network chattiness since more (if not all known services) // can then be listed in the initial handshake message. initialConnectTimer = new Timer() { @Override public void run() { loadRpcProxies(); sendInitialMessage(); } }; initialConnectTimer.schedule(50); } /** * Sends the initial message to connect to the queue, to establish an HTTP * session. Otherwise, concurrent requests will result in multiple sessions * being created. */ private void sendInitialMessage() { if (!isRemoteCommunicationEnabled()) { logger.info("initializing client bus in offline mode (erraiBusRemoteCommunicationEnabled was set to false)"); InitVotes.voteFor(ClientMessageBus.class); setState(BusState.LOCAL_ONLY); return; } if (!getState().isStartableState()) { logger.warn("aborting startup. bus is not in correct state. (current state: " + getState() + ")"); return; } setState(BusState.CONNECTING); logger.info("sending handshake message to remote bus"); for (final Runnable deferredSubscription : deferredSubscriptions) { deferredSubscription.run(); } deferredSubscriptions.clear(); if (!isProperty(ChaosMonkey.DONT_REALLY_CONNECT, "true")) { final Map<String, String> properties = new HashMap<>(); properties.put("phase", "connection"); properties.put("wait", "1"); transportHandler.transmit(Collections.singletonList(CommandMessage.create() .command(BusCommand.Associate) .set(ToSubject, "ServerBus") .set(PriorityProcessing, "1") .set(MessageParts.RemoteServices, getAdvertisableSubjects()) .setResource(TransportHandler.EXTRA_URI_PARMS_RESOURCE, properties))); transportHandler.start(); } else { final String failOnConnectAfterMs = properties.get(ChaosMonkey.FAIL_ON_CONNECT_AFTER_MS); if (failOnConnectAfterMs != null) { final int ms = Integer.parseInt(failOnConnectAfterMs); new Timer() { @Override public void run() { setState(BusState.CONNECTION_INTERRUPTED); } }.schedule(ms); } } } private void processCapabilities(final Message message) { for (final String capability : message.get(String.class, MessageParts.CapabilitiesFlags).split(",")) { final TransportHandler handler = availableHandlers.get(capability); if (handler == null) { logger.warn("could not find handler for capability type: " + capability); continue; } handler.configure(message); } reconsiderTransport(); } private void declareSubscriptionListeners() { addUnsubscribeListener(new UnsubscribeListener() { @Override public void onUnsubscribe(final SubscriptionEvent event) { final String subject = event.getSubject(); if (subject.endsWith(":RespondTo:RPC") || subject.endsWith(":Errors:RPC")) { return; } encodeAndTransmit(CommandMessage.create() .toSubject(BuiltInServices.ServerBus.name()).command(RemoteUnsubscribe) .set(Subject, subject).set(PriorityProcessing, "1")); } }); addSubscribeListener(new SubscribeListener() { @Override public void onSubscribe(final SubscriptionEvent event) { final String subject = event.getSubject(); if (event.isLocalOnly() || subject.startsWith("local:") || remotes.containsKey(subject)) { return; } if (subject.endsWith(":RespondTo:RPC") || subject.endsWith(":Errors:RPC")) { return; } if (event.isNew()) { encodeAndTransmit(CommandMessage.create() .toSubject(BuiltInServices.ServerBus.name()).command(RemoteSubscribe) .set(Subject, subject).set(PriorityProcessing, "1")); } } }); } @Override public void stop(final boolean sendDisconnect) { stop(sendDisconnect, null); } private void stop(final boolean sendDisconnect, final TransportError reason) { logger.info("stopping bus ..."); if (initialConnectTimer != null) { initialConnectTimer.cancel(); } if (degradeToUnitialized()) { setState(BusState.UNINITIALIZED); deferredMessages.clear(); remotes.clear(); deferredSubscriptions.clear(); } else if (state != BusState.LOCAL_ONLY) { setState(BusState.LOCAL_ONLY, reason); } // Optionally tell the server we're going away (this causes two POST requests) if (sendDisconnect && isRemoteCommunicationEnabled()) { encodeAndTransmit(CommandMessage.create() .toSubject(BuiltInServices.ServerBus.name()).command(BusCommand.Disconnect) .set(MessageParts.PriorityProcessing, "1")); } deferredMessages.addAll(transportHandler.stop(true)); } private String getAdvertisableSubjects() { String subjects = ""; for (final String s : subscriptions.keySet()) { if (s.startsWith("local:")) continue; if (!remotes.containsKey(s)) { if (subjects.length() != 0) { subjects += ","; } subjects += s; } } return subjects; } @Override public String getClientId() { return clientId; } @Override public String getSessionId() { return sessionId; } /** * Removes all subscriptions attached to the specified subject * * @param subject * - the subject to have all it's subscriptions removed */ @Override public void unsubscribeAll(final String subject) { fireAllUnSubscribeListeners(subject); removeSubscriptionTopic(subject); } /** * Add a subscription for the specified subject * * @param subject * - the subject to add a subscription for * @param callback * - function called when the message is dispatched */ @Override public Subscription subscribe(final String subject, final MessageCallback callback) { return _subscribe(subject, callback, false); } @Override public Subscription subscribeLocal(final String subject, final MessageCallback callback) { return _subscribe(subject, callback, true); } @Override public Subscription subscribeShadow(final String subject, final MessageCallback callback) { List<MessageCallback> messageCallbacks = shadowSubscriptions.get(subject); if (messageCallbacks == null) { shadowSubscriptions.put(subject, messageCallbacks = new ArrayList<>()); } messageCallbacks.add(callback); final List<MessageCallback> _messageCallbacks = messageCallbacks; return new Subscription() { @Override public void remove() { _messageCallbacks.remove(callback); } }; } private Subscription _subscribe(final String subject, final MessageCallback callback, final boolean local) { if (getState() == BusState.CONNECTING) { return _subscribeDeferred(subject, callback, local); } else { return _subscribeNow(subject, callback, local); } } private Subscription _subscribeDeferred(final String subject, final MessageCallback callback, final boolean local) { final DeferredSubscription deferredSubscription = new DeferredSubscription(); deferredSubscriptions.add(new Runnable() { @Override public void run() { deferredSubscription.attachSubscription(_subscribeNow(subject, callback, local)); } @Override public String toString() { return "DeferredSubscribe:" + subject; } }); return deferredSubscription; } private Subscription _subscribeNow(final String subject, final MessageCallback callback, final boolean local) { if (BuiltInServices.ServerBus.name().equals(subject) && subscriptions.containsKey(BuiltInServices.ServerBus.name())) return null; final WrappedCallbackHolder wrappedCallbackHolder = new WrappedCallbackHolder(callback); fireAllSubscribeListeners(subject, local, directSubscribe(subject, callback, local, wrappedCallbackHolder)); return new Subscription() { @Override public void remove() { final List<MessageCallback> cbs = local ? localSubscriptions.get(subject) : subscriptions.get(subject); if (cbs != null) { cbs.remove(wrappedCallbackHolder.getWrappedCallback()); if (cbs.isEmpty()) { unsubscribeAll(subject); } } } }; } private boolean directSubscribe(final String subject, final MessageCallback callback, final boolean local) { return directSubscribe(subject, callback, local, new WrappedCallbackHolder(null)); } private boolean directSubscribe(final String subject, final MessageCallback callback, final boolean local, final WrappedCallbackHolder callbackHolder) { final boolean isNew = !isSubscribed(subject); final MessageCallback cb = new MessageCallback() { @Override public void callback(final Message message) { try { callback.callback(message); } catch (final Exception e) { handleCallbackError(message, e); } } }; callbackHolder.setWrappedCallback(cb); if (local) { addLocalSubscriptionEntry(subject, cb); } else { addSubscriptionEntry(subject, cb); } return isNew; } /** * Fire listeners to notify that a new subscription has been registered on the * bus. * * @param subject * - new subscription registered * @param local * - * @param isNew * - */ private void fireAllSubscribeListeners(final String subject, final boolean local, final boolean isNew) { final Iterator<SubscribeListener> iterator = onSubscribeHooks.iterator(); final SubscriptionEvent evt = new SubscriptionEvent(false, false, local, isNew, 1, "InBrowser", subject); while (iterator.hasNext()) { iterator.next().onSubscribe(evt); if (evt.isDisposeListener()) { iterator.remove(); evt.setDisposeListener(false); } } } /** * Fire listeners to notify that a subscription has been unregistered from the * bus * * @param subject * - subscription unregistered */ private void fireAllUnSubscribeListeners(final String subject) { final Iterator<UnsubscribeListener> iterator = onUnsubscribeHooks.iterator(); final SubscriptionEvent evt = new SubscriptionEvent(false, "InBrowser", 0, false, subject); while (iterator.hasNext()) { iterator.next().onUnsubscribe(evt); if (evt.isDisposeListener()) { iterator.remove(); evt.setDisposeListener(false); } } } /** * Globally send message to all receivers. * * @param message * - The message to be sent. */ @Override public void sendGlobal(final Message message) { send(message); } /** * Sends the specified message, and notifies the listeners. * * @param message * - the message to be sent * @param fireListeners * - true if the appropriate listeners should be fired */ @Override public void send(final Message message, final boolean fireListeners) { // TODO: fire listeners? send(message); } /** * Sends the message using it's encoded subject. If the bus has not been initialized, it will be added to * <tt>postInitTasks</tt>. * * @param message * - * * @throws RuntimeException * - if message does not contain a ToSubject field or if the * message's callback throws an error. */ @Override public void send(final Message message) { message.setResource(RequestDispatcher.class.getName(), BusToolsCli.getRequestDispatcherProvider()) .setResource("Session", BusToolsCli.getClientSession()).commit(); logger.debug("send({})", message.getParts()); try { boolean delivered = false; final boolean localOnly = message.isFlagSet(RoutingFlag.DeliverLocalOnly); final String subject = message.getSubject(); if (message.hasPart(MessageParts.ToSubject)) { if (isRemoteCommunicationEnabled() && !localOnly) { if (getState().isShadowDeliverable() && shadowSubscriptions.containsKey(subject)) { deliverToSubscriptions(shadowSubscriptions, subject, message); delivered = true; } else if (getState() != BusState.CONNECTED) { logger.debug("deferred: {}", message); deferredMessages.add(message); delivered = true; } else if (remotes.containsKey(subject)) { logger.debug("sent to remote: {}", message); remotes.get(subject).callback(message); delivered = true; } } if (subscriptions.containsKey(subject)) { deliverToSubscriptions(subscriptions, subject, message); } else if (localSubscriptions.containsKey(subject)) { deliverToSubscriptions(localSubscriptions, subject, message); } else if (!delivered) { if (shadowSubscriptions.containsKey(subject)) { deliverToSubscriptions(shadowSubscriptions, subject, message); } else { throw new NoSubscribersToDeliverTo(subject); } } } else { throw new RuntimeException("Cannot send message using this method" + " if the message does not contain a ToSubject field."); } } catch (final RuntimeException e) { callErrorHandler(message, e); } } @Override public void sendLocal(final Message msg) { final String subject = msg.getSubject(); final List<MessageCallback> messageCallbacks = subscriptions.get(subject); if (messageCallbacks != null) { // iterating over a copy of the list in case a subscriber unsubscribes during callback for (final MessageCallback cb : new ArrayList<>(messageCallbacks)) { cb.callback(msg); } } } public boolean callErrorHandler(final Message message, final Throwable t) { boolean defaultErrorHandling = true; if (message.getErrorCallback() != null) { defaultErrorHandling = message.getErrorCallback().error(message, t); } if (defaultErrorHandling) { DefaultErrorCallback.INSTANCE.error(message, t); } return defaultErrorHandling; } public void encodeAndTransmit(final Message message) { logger.debug("encodeAndTransmit({})", message.getParts()); if (getState() == BusState.LOCAL_ONLY) { logger.debug("encodeAndTransmit({}) NOT ROUTED - LOCAL ONLY", message.getParts()); return; } transportHandler.transmit(Collections.singletonList(message)); } private void addSubscriptionEntry(final String subject, final MessageCallback reference) { _addCallbackEntry(subscriptions, subject, reference); } private void addLocalSubscriptionEntry(final String subject, final MessageCallback reference) { _addCallbackEntry(localSubscriptions, subject, reference); } private static void _addCallbackEntry(final Map<String, List<MessageCallback>> subscriptions, final String subject, final MessageCallback reference) { if (!subscriptions.containsKey(subject)) { subscriptions.put(subject, new ArrayList<MessageCallback>()); } if (!subscriptions.get(subject).contains(reference)) { subscriptions.get(subject).add(reference); } } private void removeSubscriptionTopic(final String subject) { subscriptions.remove(subject); } private static void deliverToSubscriptions(final Map<String, List<MessageCallback>> subscriptions, final String subject, final Message message) { for (final MessageCallback cb : subscriptions.get(subject)) { cb.callback(message); } } /** * Checks if subject is already listed in the subscriptions map * * @param subject * subject to look for * * @return true if the subject is already subscribed */ @Override public boolean isSubscribed(final String subject) { return subscriptions.containsKey(subject); } /** * Arranges for messages to the given subject to be forwarded to the server. * * @param subject the bus subject for messages that should be forwarded to the server. */ private void remoteSubscribe(final String subject) { remotes.put(subject, serverForwarder); } Set<String> getRemoteSubscriptions() { return remotes.keySet(); } private void sendDeferredToShadow() { if (!deferredMessages.isEmpty() && !shadowSubscriptions.isEmpty()) { boolean deliveredMessages; do { deliveredMessages = false; for (final Message message : new ArrayList<>(deferredMessages)) { if (shadowSubscriptions.containsKey(message.getSubject())) { deferredMessages.remove(message); deliveredMessages = true; deliverToSubscriptions(shadowSubscriptions, message.getSubject(), message); } } } while (!deferredMessages.isEmpty() && deliveredMessages); } } private void sendAllDeferred() { if (!deferredMessages.isEmpty()) logger.info("transmitting deferred messages now ..."); final List<Message> highPriority = new ArrayList<>(); for (final Message message : new ArrayList<>(deferredMessages)) { if (message.hasPart(MessageParts.PriorityProcessing)) { if (remotes.containsKey(message.getSubject())) highPriority.add(message); deferredMessages.remove(message); } } final List<Message> lowPriority = new ArrayList<>(); do { for (final Message message : new ArrayList<>(deferredMessages)) { if (remotes.containsKey(message.getSubject())) lowPriority.add(message); deferredMessages.remove(message); } } while (!deferredMessages.isEmpty()); transportHandler.transmit(highPriority); transportHandler.transmit(lowPriority); deferredMessages.clear(); } public boolean handleTransportError(final BusTransportError transportError) { for (final TransportErrorHandler handler : transportErrorHandlers) { handler.onError(transportError); } if (!transportError.isStopDefaultErrorHandler()) { if (state == BusState.CONNECTED) { setState(BusState.CONNECTION_INTERRUPTED, transportError); } else if (state != BusState.CONNECTING && state != BusState.CONNECTION_INTERRUPTED) { logger.error("got a transport error while in the " + state + " state"); } } return transportError.isStopDefaultErrorHandler(); } private void handleCallbackError(final Message message, final Throwable t) { boolean defaultErrorHandling = true; if (message.getErrorCallback() != null) { try { defaultErrorHandling = message.getErrorCallback().error(message, t); } catch (final Throwable secondaryError) { logger.error("Encountered an error while calling error callback for message to " + message.getSubject(), secondaryError); } } if (defaultErrorHandling) { DefaultErrorCallback.INSTANCE.error(message, t); } } private void loadRpcProxies() { final RpcProxyLoader proxyLoader = ((RpcProxyLoader) GWT.create(RpcProxyLoader.class)); proxyLoader.loadProxies(ClientMessageBusImpl.this); } /** * Adds a subscription listener, so it is possible to add subscriptions to the * client. * * @param listener * subscription listener */ @Override public void addSubscribeListener(final SubscribeListener listener) { this.onSubscribeHooks.add(Assert.notNull(listener)); } /** * Adds an unsubscribe listener, so it is possible for applications to remove * subscriptions from the client * * @param listener * - unsubscribe listener */ @Override public void addUnsubscribeListener(final UnsubscribeListener listener) { this.onUnsubscribeHooks.add(listener); } /** * When called, the MessageBus assumes that the currently active transport is no longer capable of operating. The * MessageBus then find the best remaining handler and activates it. */ public void reconsiderTransport() { TransportHandler newHandler = null; for (final TransportHandler handler : availableHandlers.values()) { if (handler.isUsable()) { newHandler = handler; break; } } if (newHandler == null) { logger.error("no available transports! stopping bus!"); stop(false); } else if (newHandler != transportHandler) { logger.info("transitioning to new handler: " + newHandler); transportHandler.stop(false); transportHandler = newHandler; transportHandler.start(); } // 3rd case: we're already using the best available handler. Do nothing. } @Override public void attachMonitor(final BusMonitor monitor) { // only supported server-side right now. } @Override public Set<String> getAllRegisteredSubjects() { return Collections.unmodifiableSet(subscriptions.keySet()); } @Override public void addTransportErrorHandler(final TransportErrorHandler errorHandler) { transportErrorHandlers.add(errorHandler); } @Override public void removeTransportErrorHandler(final TransportErrorHandler errorHandler) { transportErrorHandlers.remove(errorHandler); } public BusState getState() { return state; } public Set<String> getRemoteServices() { return new HashSet<>(remotes.keySet()); } public Set<String> getLocalServices() { return new HashSet<>(subscriptions.keySet()); } public String getApplicationLocation(final String serviceEntryPoint) { final Configuration configuration = GWT.create(Configuration.class); if (configuration instanceof Configuration.NotSpecified) { return BusToolsCli.getApplicationRoot() + serviceEntryPoint; } return configuration.getRemoteLocation() + serviceEntryPoint; } public String getOutServiceEntryPoint() { return OUT_SERVICE_ENTRY_POINT; } public String getInServiceEntryPoint() { return IN_SERVICE_ENTRY_POINT; } @Override public void addLifecycleListener(final BusLifecycleListener l) { lifecycleListeners.add(Assert.notNull(l)); } @Override public void removeLifecycleListener(final BusLifecycleListener l) { lifecycleListeners.remove(l); } public TransportHandler getTransportHandler() { return transportHandler; } public Collection<TransportHandler> getAllAvailableHandlers() { return availableHandlers.values(); } @Override public void setProperty(final String name, final String value) { properties.put(name, value); } @Override public void clearProperties() { properties.clear(); } private boolean isProperty(final String name, final String value) { return properties.containsKey(name) && properties.get(name).equals(value); } private boolean degradeToUnitialized() { return isProperty(ChaosMonkey.DEGRADE_TO_UNINITIALIZED_ON_STOP, "true"); } /** * Puts the bus in the given state, firing all necessary transition events with no <tt>reason</tt> field. */ public void setState(final BusState newState) { setState(newState, null); } /** * Puts the bus in the given state, firing all necessary transition events with the given reason. * * @param reason * The error that led to this state transition, if any. Null is permitted. */ private void setState(final BusState newState, final TransportError reason) { if (state == newState) { GWT.log("bus tried to transition to " + state + ", but it already is"); return; } final List<BusEventType> events = new ArrayList<>(); switch (state) { case UNINITIALIZED: case LOCAL_ONLY: if (newState == BusState.CONNECTING) { events.add(BusEventType.ASSOCIATING); } else if (newState == BusState.CONNECTED) { events.add(BusEventType.ASSOCIATING); events.add(BusEventType.ONLINE); } break; case CONNECTION_INTERRUPTED: if (newState == BusState.CONNECTED) { logger.info("the connection has resumed."); } case CONNECTING: if (newState == BusState.LOCAL_ONLY) { events.add(BusEventType.DISASSOCIATING); } else if (newState == BusState.CONNECTED) { events.add(BusEventType.ONLINE); } break; case CONNECTED: if (newState == BusState.CONNECTING || newState == BusState.CONNECTION_INTERRUPTED) { events.add(BusEventType.OFFLINE); } else if (newState == BusState.LOCAL_ONLY) { events.add(BusEventType.OFFLINE); events.add(BusEventType.DISASSOCIATING); } break; default: throw new IllegalStateException("Bus is in unknown state: " + state); } state = newState; if (newState == BusState.CONNECTION_INTERRUPTED) { logger.warn("the connection to the server has been interrupted ..."); } /* * If the new state is a state we deliver to shadow subscriptions, we send any deferred messages to * the shadow subscriptions now. */ if (newState.isShadowDeliverable()) { sendDeferredToShadow(); } for (final BusEventType et : events) { final BusLifecycleEvent e = new BusLifecycleEvent(this, reason); for (int i = lifecycleListeners.size() - 1; i >= 0; i--) { try { et.deliverTo(lifecycleListeners.get(i), e); } catch (final Throwable t) { logger.error("listener threw exception: " + t); t.printStackTrace(); } } } } }