Package org.jboss.errai.bus.client.framework

Source Code of org.jboss.errai.bus.client.framework.ClientMessageBusImpl

/*
* Copyright 2011 JBoss, by Red Hat, Inc
*
* 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.common.client.util.LogUtil;
import org.jboss.errai.marshalling.client.api.MarshallerFramework;

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<SubscribeListener>();
  private final List<UnsubscribeListener> onUnsubscribeHooks = new ArrayList<UnsubscribeListener>();

  /**
   * 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);
    }
  };

  /**
   * Puts every message received onto this message bus. This is the mechanism by
   * which messages received from the remote server bus are delivered to local
   * message subscribers.
   */
  private final MessageCallback transportToBusCallback = new MessageCallback() {
    @Override
    public void callback(final Message message) {
      processMessageFromTransportLayer(message.getSubject(), message);
    }
  };

  private Map<String, TransportHandler> availableHandlers;

  private final TransportHandler BOOTSTRAP_HANDLER
      = HttpPollingHandler.newNoPollingInstance(transportToBusCallback, 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<String, List<MessageCallback>>();
  private final Map<String, List<MessageCallback>> localSubscriptions = new HashMap<String, List<MessageCallback>>();
  private final Map<String, List<MessageCallback>> shadowSubscriptions = new HashMap<String, List<MessageCallback>>();

  private final Map<String, MessageCallback> remotes = new HashMap<String, MessageCallback>();

  private final List<TransportErrorHandler> transportErrorHandlers = new ArrayList<TransportErrorHandler>();

  private final List<Runnable> deferredSubscriptions = new ArrayList<Runnable>();
  private final List<Message> deferredMessages = new ArrayList<Message>();

  private final List<BusLifecycleListener> lifecycleListeners = new ArrayList<BusLifecycleListener>();

  private BusState state = BusState.UNINITIALIZED;

  private final ManagementConsole managementConsole;

  private final Map<String, String> properties = new HashMap<String, String>();

  private Timer initialConnectTimer;

  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;

    setupDefaultHandlers();
  }

  private void setupDefaultHandlers() {
    if (availableHandlers != null) {
      for (TransportHandler handler : availableHandlers.values()) {
        handler.close();
      }
    }

    Map<String, TransportHandler> m = new LinkedHashMap<String, TransportHandler>();
    m.put(Capabilities.WebSockets.name(), new WebsocketHandler(transportToBusCallback, ClientMessageBusImpl.this));
    m.put(Capabilities.SSE.name(), new SSEHandler(transportToBusCallback, ClientMessageBusImpl.this));
    m.put(Capabilities.LongPolling.name(),
            HttpPollingHandler.newLongPollingInstance(transportToBusCallback, ClientMessageBusImpl.this));
    m.put(Capabilities.ShortPolling.name(),
            HttpPollingHandler.newShortPollingInstance(transportToBusCallback, 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.
       */
      InitVotes.voteFor(ClientMessageBus.class);
      return;
    }

    LogUtil.log("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, new MessageCallback() {
        @Override
        public void callback(final Message message) {
          final String errorTo = message.get(String.class, MessageParts.ErrorTo);
          if (errorTo == null) {
            managementConsole.displayError(message.get(String.class, MessageParts.ErrorMessage),
                message.get(String.class, MessageParts.AdditionalDetails), null);
          }
          else {
            message.toSubject(errorTo);
            message.sendNowWith(ClientMessageBusImpl.this);
          }
        }
      }, false);
    }

    if (!isSubscribed(BuiltInServices.ClientBus.name())) {
      directSubscribe(BuiltInServices.ClientBus.name(), new MessageCallback() {
        @Override
        @SuppressWarnings({"unchecked"})
        public void callback(final Message message) {
          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)) {
                LogUtil.log("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);
              LogUtil.log("my queue session id: " + sessionId);

              loadRpcProxies();
              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);
              LogUtil.log("bus federated and running.");
              break;

            case SessionExpired:
              LogUtil.log("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;
          }
        }
      }, 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() {
        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()) {
      LogUtil.log("initializing client bus in offline mode (erraiBusRemoteCommunicationEnabled was set to false)");
      InitVotes.voteFor(ClientMessageBus.class);
      setState(BusState.LOCAL_ONLY);
      return;
    }

    if (!getState().isStartableState()) {
      LogUtil.log("aborting startup. bus is not in correct state. (current state: " + getState() + ")");
      return;
    }

    setState(BusState.CONNECTING);

    LogUtil.log("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<String, String>();
      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) {
        LogUtil.log("warning: 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) {
    LogUtil.log("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;
  }

  public String getClientId() {
    return clientId;
  }

  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<MessageCallback>());
    }
    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 (Exception e) {
          managementConsole
              .displayError("receiver '" + subject + "' threw an exception", decodeCommandMessage(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();

  //  LogUtil.log("[bus] send(" + message.getParts() + ")");

    try {
      boolean deferred = 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);
            deferred = true;
          }
          else if (getState() != BusState.CONNECTED) {
          //  LogUtil.log("[bus] deferred: " + message);
            deferredMessages.add(message);
            deferred = true;
          }
        }

        boolean routedToRemote = false;

        if (!localOnly && remotes.containsKey(subject)) {
        //  LogUtil.log("[bus] sent to remote: " + message);
          remotes.get(subject).callback(message);
          routedToRemote = true;
        }

        if (subscriptions.containsKey(subject)) {
          deliverToSubscriptions(subscriptions, subject, message);
        }
        else if (localSubscriptions.containsKey(subject)) {
          deliverToSubscriptions(localSubscriptions, subject, message);
        }
        else if (!deferred && !routedToRemote) {
          throw new NoSubscribersToDeliverTo(subject);
        }
      }
      else {
        throw new RuntimeException("Cannot send message using this method"
            + " if the message does not contain a ToSubject field.");
      }
    }
    catch (RuntimeException e) {
      boolean defaultErrorHandling = callErrorHandler(message, e);

      if (defaultErrorHandling)
        throw e;
    }
  }

  private void processMessageFromTransportLayer(final String subject, final Message msg) {
    if (subscriptions.containsKey(subject)) {
      final ArrayList<MessageCallback> messageCallbacks = new ArrayList<MessageCallback>(subscriptions.get(subject));
      for (final MessageCallback cb : 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) {
      managementConsole.displayError(t.getMessage(), "none", t);
    }

    return defaultErrorHandling;
  }

  public void encodeAndTransmit(final Message message) {
  //  LogUtil.log("[bus] encodeAndTransmit(" + message.getParts() + ")");
    if (getState() == BusState.LOCAL_ONLY) {
    //  LogUtil.log("[bus] encodeAndTransmit(" + message.getParts() + ") NOT ROUTED - LOCAL ONLY");
      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<Message>(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())
      LogUtil.log("transmitting deferred messages now ...");

    final List<Message> highPriority = new ArrayList<Message>();
    for (final Message message : new ArrayList<Message>(deferredMessages)) {
      if (message.hasPart(MessageParts.PriorityProcessing)) {
        highPriority.add(message);
        deferredMessages.remove(message);
      }
    }

    final List<Message> lowPriority = new ArrayList<Message>();
    do {
      for (final Message message : new ArrayList<Message>(deferredMessages)) {
        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) {
        LogUtil.log("got a transport error while in the " + state + " state");
      }
    }

    return transportError.isStopDefaultErrorHandler();
  }

  /**
   * Initializes the message bus by setting up the <tt>recvBuilder</tt> to
   * accept responses. Also, initializes the incoming timer to ensure the
   * client's polling with the server is active.
   */
  private void loadRpcProxies() {
    ((RpcProxyLoader) GWT.create(RpcProxyLoader.class)).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);
  }

  private static String decodeCommandMessage(final Message msg) {
    final StringBuilder decode = new StringBuilder(
        "<table><thead style='font-weight:bold;'><tr><td>Field</td><td>Value</td></tr></thead><tbody>");

    for (final Map.Entry<String, Object> entry : msg.getParts().entrySet()) {
      decode.append("<tr><td>").append(entry.getKey()).append("</td><td>").append(entry.getValue())
          .append("</td></tr>");
    }

    return decode.append("</tbody></table>").toString();
  }

  /**
   * 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) {
      LogUtil.log("no available transports! stopping bus!");
      stop(false);
    }
    else if (newHandler != transportHandler) {
      LogUtil.log("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);
  }

  public BusState getState() {
    return state;
  }

  public Set<String> getRemoteServices() {
    return new HashSet<String>(remotes.keySet());
  }

  public Set<String> getLocalServices() {
    return new HashSet<String>(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 from " + state + " ");
      return;
    }

    final List<BusEventType> events = new ArrayList<BusEventType>();

    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) {
          LogUtil.log("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) {
      LogUtil.log("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 (Throwable t) {
          LogUtil.log("listener threw exception: " + t);
          t.printStackTrace();
        }
      }
    }
  }
}
TOP

Related Classes of org.jboss.errai.bus.client.framework.ClientMessageBusImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.