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

joynr.messaging.routing.MessageRouter.js Maven / Gradle / Ivy

There is a newer version: 1.4.0
Show newest version
/*global JSON: true */
/*jslint es5: true */

/*
 * #%L
 * %%
 * Copyright (C) 2011 - 2017 BMW Car IT GmbH
 * %%
 * 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.
 * #L%
 */

define(
        "joynr/messaging/routing/MessageRouter",
        [
            "global/Promise",
            "joynr/messaging/util/MulticastWildcardRegexFactory",
            "joynr/system/DiagnosticTags",
            "joynr/system/LoggerFactory",
            "joynr/messaging/inprocess/InProcessAddress",
            "joynr/messaging/JoynrMessage",
            "joynr/messaging/MessageReplyToAddressCalculator",
            "joynr/exceptions/JoynrException",
            "joynr/exceptions/JoynrRuntimeException",
            "joynr/util/Typing",
            "joynr/util/UtilInternal",
            "joynr/util/JSONSerializer",
        ],
        function(Promise,
                MulticastWildcardRegexFactory,
                DiagnosticTags,
                LoggerFactory,
                InProcessAddress,
                JoynrMessage,
                MessageReplyToAddressCalculator,
                JoynrException,
                JoynrRuntimeException,
                Typing,
                Util,
                JSONSerializer) {

            /**
             * Message Router receives a message and forwards it to the correct endpoint, as looked up in the {@link RoutingTable}
             *
             * @constructor
             * @name MessageRouter
             *
             * @param {Object}
             *            settings the settings object holding dependencies
             * @param {RoutingTable}
             *            settings.routingTable
             * @param {MessagingStubFactory}
             *            settings.messagingStubFactory
             * @param {String}
             *            settings.myChannelId
             * @param {Address} settings.incomingAddress
             * @param {Address} settings.parentMessageRouterAddress
             * @param {LocalStorage} settings.persistency - LocalStorage or another object implementing the same interface
             * @param {TypeRegistry} settings.typeRegistry
             *
             * @classdesc The MessageRouter is a joynr internal interface. The Message
             * Router receives messages from Message Receivers, and forwards them along using to the
             * appropriate Address, either {@link ChannelAddress} for messages being
             * sent to a joynr channel via HTTP, {@link BrowserAddress} for messages
             * going to applications running within a seperate browser tab, or
             * {@link InProcessAddress} for messages going to a dispatcher running
             * within the same JavaScript scope as the MessageRouter.
             *
             * MessageRouter is part of the cluster controller, and is used for
             * internal messaging only.
             */
            function MessageRouter(settings) {
                // TODO remote provider participants are registered by capabilitiesDirectory on lookup
                // TODO local providers registered (using dispatcher as local endpoint) by capabilitiesDirectory on register
                // TODO local replyCallers are registered (using dispatcher as local endpoint) when they are created

                var that = this;
                var multicastWildcardRegexFactory = new MulticastWildcardRegexFactory();
                var log = LoggerFactory.getLogger("joynr/messaging/routing/MessageRouter");
                var listener, routingProxy, messagingStub;
                var queuedAddNextHopCalls = [], queuedRemoveNextHopCalls = [], queuedAddMulticastReceiverCalls = [], queuedRemoveMulticastReceiverCalls = [];
                var routingTable = settings.initialRoutingTable || {};
                var id = settings.joynrInstanceId;
                var persistency = settings.persistency;
                var incomingAddress = settings.incomingAddress;
                var parentMessageRouterAddress = settings.parentMessageRouterAddress;
                var typeRegistry = settings.typeRegistry;
                var multicastAddressCalculator = settings.multicastAddressCalculator;
                var messagingSkeletonFactory = settings.messagingSkeletonFactory;
                var multicastReceiversRegistry = {};
                var replyToAddress;
                var messageReplyToAddressCalculator = new MessageReplyToAddressCalculator({});
                var messagesWithoutReplyTo = [];

                // if (settings.routingTable === undefined) {
                // throw new Error("routing table is undefined");
                // }
                if (settings.parentMessageRouterAddress !== undefined
                    && settings.incomingAddress === undefined) {
                    throw new Error("incoming address is undefined");
                }
                if (settings.messagingStubFactory === undefined) {
                    throw new Error("messaging stub factory is undefined");
                }
                if (settings.messageQueue === undefined) {
                    throw new Error("messageQueue is undefined");
                }


                var started = true;

                function isReady() {
                    return started;
                }

                this.setReplyToAddress = function (newAddress) {
                    replyToAddress = newAddress;
                    messageReplyToAddressCalculator.setReplyToAddress(replyToAddress);
                    messagesWithoutReplyTo.forEach(function(msg) {
                        that.route(msg);
                    });
                };

                /**
                 * @function MessageRouter#getStorageKey
                 *
                 * @param {String} participantId
                 *
                 * @returns {String}
                 */
                this.getStorageKey = function getStorageKey(participantId) {
                    return id + "_" + participantId.replace("/", ".");
                };

                /**
                 * @function MessageRouter#removeNextHop
                 *
                 * @param {String} participantId
                 *
                 * @returns {Promise} promise
                 */
                this.removeNextHop = function removeNextHop(participantId) {
                    if (!isReady()) {
                        log.debug("removeNextHop: ignore call as message router is already shut down");
                        return Promise.reject(new Error("message router is already shut down"));
                    }

                    routingTable[participantId] = undefined;
                    persistency.removeItem(that.getStorageKey(participantId));
                    var promise;
                    if (routingProxy !== undefined) {
                        promise = routingProxy.removeNextHop({
                            participantId : participantId
                        });
                    } else if (parentMessageRouterAddress !== undefined) {
                        promise = new Promise(function(resolve, reject) {
                            queuedRemoveNextHopCalls[queuedRemoveNextHopCalls.length] = {
                                participantId : participantId,
                                resolve : resolve,
                                reject : reject
                            };
                        });
                    } else {
                        promise = Promise.resolve();
                    }
                    return promise;
                };

                /**
                 * @function MessageRouter#addNextHopToParentRoutingTable
                 *
                 * @param {String} participantId
                 * @param {boolean} isGloballyVisible
                 *
                 * @returns result
                 */
                this.addNextHopToParentRoutingTable =
                        function addNextHopToParentRoutingTable(participantId, isGloballyVisible) {
                            var result;
                            if (Typing.getObjectType(incomingAddress) === "BrowserAddress") {
                                result = routingProxy.addNextHop({
                                    participantId : participantId,
                                    browserAddress : incomingAddress,
                                    isGloballyVisible : isGloballyVisible
                                });
                            } else if (Typing.getObjectType(incomingAddress) === "ChannelAddress") {
                                result = routingProxy.addNextHop({
                                    participantId : participantId,
                                    channelAddress : incomingAddress,
                                    isGloballyVisible : isGloballyVisible
                                });
                            } else if (Typing.getObjectType(incomingAddress) === "WebSocketAddress") {
                                result = routingProxy.addNextHop({
                                    participantId : participantId,
                                    webSocketAddress : incomingAddress,
                                    isGloballyVisible : isGloballyVisible
                                });
                            } else if (Typing.getObjectType(incomingAddress) === "WebSocketClientAddress") {
                                result = routingProxy.addNextHop({
                                    participantId : participantId,
                                    webSocketClientAddress : incomingAddress,
                                    isGloballyVisible : isGloballyVisible
                                });
                            } else if (Typing.getObjectType(incomingAddress) === "CommonApiDbusAddress") {
                                result = routingProxy.addNextHop({
                                    participantId : participantId,
                                    commonApiDbusAddress : incomingAddress,
                                    isGloballyVisible : isGloballyVisible
                                });
                            }
                            return result;
                        };

                /**
                 * @function MessageRouter#setRoutingProxy
                 *
                 * @param {RoutingProxy} newRoutingproxy - the routing proxy to be set
                 */
                this.setRoutingProxy =
                        function setRoutingProxy(newRoutingProxy) {
                            var hop, participantId, errorFct, receiver, queuedCall;

                            routingProxy = newRoutingProxy;

                            var replyToAddressPromise = routingProxy.replyToAddress.get().then(function(replyToAddress) {
                                that.setReplyToAddress(replyToAddress);
                            }).catch(function(error) {
                                throw new Error("Failed to get replyToAddress from parent router: " + error
                                        + (error instanceof JoynrException ? " " + error.detailMessage : ""));
                            });

                            errorFct = function(error) {
                                if (!isReady()) {
                                    //in this case, the error is expected, e.g. during shut down
                                    log.debug("Adding routingProxy.proxyParticipantId " + routingProxy.proxyParticipantId
                                            + "failed while the message router is not ready. Error: " + error.message);
                                    return;
                                }
                                throw new Error(error);
                            };

                            if (routingProxy !== undefined) {
                                if (routingProxy.proxyParticipantId !== undefined) {
                                    // isGloballyVisible is false because the routing provider is local
                                    var isGloballyVisible = false;
                                    that.addNextHopToParentRoutingTable(
                                            routingProxy.proxyParticipantId, isGloballyVisible).catch(errorFct);
                                }
                                for (hop in queuedAddNextHopCalls) {
                                    if (queuedAddNextHopCalls.hasOwnProperty(hop)) {
                                        queuedCall = queuedAddNextHopCalls[hop];
                                        if (queuedCall.participantId !== routingProxy.proxyParticipantId) {
                                            that.addNextHopToParentRoutingTable(
                                                    queuedCall.participantId, queuedCall.isGloballyVisible).then(
                                                    queuedCall.resolve).catch(queuedCall.reject);
                                        }
                                    }
                                }
                                for (hop in queuedRemoveNextHopCalls) {
                                    if (queuedRemoveNextHopCalls.hasOwnProperty(hop)) {
                                        queuedCall = queuedRemoveNextHopCalls[hop];
                                        that.removeNextHop(queuedCall.participantId).then(
                                                queuedCall.resolve).catch(queuedCall.reject);
                                    }
                                }
                                for (receiver in queuedAddMulticastReceiverCalls) {
                                    if (queuedAddMulticastReceiverCalls.hasOwnProperty(receiver)) {
                                        queuedCall = queuedAddMulticastReceiverCalls[receiver];
                                        routingProxy.addMulticastReceiver(queuedCall.parameters).then(
                                                queuedCall.resolve).catch(queuedCall.reject);
                                    }
                                }
                                for (receiver in queuedRemoveMulticastReceiverCalls) {
                                    if (queuedRemoveMulticastReceiverCalls.hasOwnProperty(receiver)) {
                                        queuedCall = queuedRemoveMulticastReceiverCalls[receiver];
                                        routingProxy.removeMulticastReceiver(queuedCall.parameters).then(
                                                queuedCall.resolve).catch(queuedCall.reject);
                                    }
                                }
                            }
                            queuedAddNextHopCalls = undefined;
                            queuedRemoveNextHopCalls = undefined;
                            queuedAddMulticastReceiverCalls = undefined;
                            queuedRemoveMulticastReceiverCalls = undefined;
                            return replyToAddressPromise;
                        };

                /*
                 * this method is called when no address can be found in the local routing table 
                 */
                function resolveNextHopInternal(participantId) {
                    var address, addressString;

                    addressString = persistency.getItem(that.getStorageKey(participantId));
                    if ((addressString === undefined
                        || addressString === null || addressString === '{}')) {
                        persistency.removeItem(that.getStorageKey(participantId));
                    } else {
                        address = Typing.augmentTypes(JSON.parse(addressString), typeRegistry);
                        routingTable[participantId] = address;
                    }
                    if (address === undefined && routingProxy !== undefined) {
                        return routingProxy.resolveNextHop({
                                participantId : participantId
                            }).then(function(opArgs) {
                                if (opArgs.resolved) {
                                    routingTable[participantId] = parentMessageRouterAddress;
                                    return parentMessageRouterAddress;
                                }
                                throw new Error("nextHop cannot be resolved, as participant with id "
                                                + participantId
                                                + " is not reachable by parent routing table");
                            });
                    }
                    return Promise.resolve(address);
                }

                /**
                 * Looks up an Address for a given participantId (next hop)
                 *
                 * @name MessageRouter#resolveNextHop
                 * @function
                 *
                 * @param {String}
                 *            participantId
                 * @returns {InProcessAddress|BrowserAddress|ChannelAddress} the address of the next hop in the direction of the given
                 *          participantId, or undefined if not found
                 */
                this.resolveNextHop = function resolveNextHop(participantId) {
                    if (!isReady()) {
                        log.debug("resolveNextHop: ignore call as message router is already shut down");
                        return Promise.reject(new Error("message router is already shut down"));
                    }

                    var address = routingTable[participantId];

                    if (address === undefined) {
                        return resolveNextHopInternal(participantId);
                    }

                    return Promise.resolve(address);
                };

                /**
                 * Get the address to which the passed in message should be sent to.
                 * This is a multicast address calculated from the header content of the message.
                 *
                 * @param message the message for which we want to find an address to send it to.
                 * @return the address to send the message to. Will not be null, because if an address can't be determined an exception is thrown.
                 */
                function getAddressesForMulticast (joynrMessage) {
                    var i, result = [], address;
                    if (!joynrMessage.isReceivedFromGlobal) {
                        address = multicastAddressCalculator.calculate(joynrMessage);
                        if (address !== undefined) {
                            result.push(address);
                        }
                    }

                    function containsAddress(array, address) {
                        //each address class provides an equals method, e.g. InProcessAddress
                        var j;
                        if (array === undefined) {
                            return false;
                        }
                        for (j=0;j joynrMessage.expiryDate) {
                        var errorMsg = "Received expired message. Dropping the message. ID: " + joynrMessage.msgId;
                        log.warn(errorMsg);
                        throw new JoynrRuntimeException({detailMessage: errorMsg});
                    }

                    registerGlobalRoutingEntryIfRequired(joynrMessage);

                    function forwardToRouteInternal(address) {
                        return routeInternal(address, joynrMessage);
                    }
                    if (joynrMessage.type === JoynrMessage.JOYNRMESSAGE_TYPE_MULTICAST) {
                        return Promise.all(getAddressesForMulticast(joynrMessage).map(forwardToRouteInternal));
                    }

                    var participantId = joynrMessage.to;
                    var address = routingTable[participantId];
                    if (address !== undefined) {
                        return routeInternal(address, joynrMessage);
                    }

                    return resolveNextHopInternal(participantId).then(forwardToRouteInternal);
                };

                /**
                 * Registers the next hop with this specific participant Id
                 *
                 * @name RoutingTable#addNextHop
                 * @function
                 *
                 * @param {String}
                 *            participantId
                 * @param {InProcessAddress|BrowserAddress|ChannelAddress}
                 *            address the address to register
                 * @param {boolean}
                 *            isGloballyVisible
                 */
                this.addNextHop =
                        function addNextHop(participantId, address, isGloballyVisible) {
                            if (!isReady()) {
                                log.debug("addNextHop: ignore call as message router is already shut down");
                                return Promise.reject(new Error("message router is already shut down"));
                            }
                            // store the address of the participantId persistently
                            routingTable[participantId] = address;
                            var serializedAddress = JSONSerializer.stringify(address);
                            var promise;
                            if ((serializedAddress === undefined
                                || serializedAddress === null || serializedAddress === '{}')) {
                                log.info("addNextHop: HOP address "
                                    + serializedAddress
                                    + " will not be persisted for participant id: "
                                    + participantId);
                            } else {
                                persistency.setItem(
                                        that.getStorageKey(participantId),
                                        serializedAddress);
                            }

                            if (routingProxy !== undefined) {
                                // register remotely
                                promise = that.addNextHopToParentRoutingTable(participantId, isGloballyVisible);
                            } else {
                                if (parentMessageRouterAddress !== undefined) {
                                    promise = new Promise(function(resolve, reject){
                                        queuedAddNextHopCalls[queuedAddNextHopCalls.length] =
                                        {
                                            participantId : participantId,
                                            isGloballyVisible : isGloballyVisible,
                                            resolve : resolve,
                                            reject : reject
                                        };
                                    });
                                } else {
                                    promise = Promise.resolve();
                                }
                            }
                            that.participantRegistered(participantId);
                            return promise;
                        };

                /**
                 * Adds a new receiver for the identified multicasts.
                 *
                 * @name RoutingTable#addMulticastReceiver
                 * @function
                 *
                 * @param {Object}
                 *            parameters - object containing parameters
                 * @param {String}
                 *            parameters.multicastId
                 * @param {String}
                 *            parameters.subscriberParticipantId
                 * @param {String}
                 *            parameters.providerParticipantId
                 */
                this.addMulticastReceiver = function addMulticastReceiver(parameters) {
                    //1. handle call in local router
                    //1.a store receiver in multicastReceiverRegistry
                    var multicastIdPattern = multicastWildcardRegexFactory.createIdPattern(parameters.multicastId);
                    var providerAddress = routingTable[parameters.providerParticipantId];

                    if (multicastReceiversRegistry[multicastIdPattern] === undefined) {
                        multicastReceiversRegistry[multicastIdPattern] = [];

                        //1.b the first receiver for this multicastId -> inform MessagingSkeleton about receiver
                        var skeleton = messagingSkeletonFactory.getSkeleton(providerAddress);
                        if (skeleton !== undefined && skeleton.registerMulticastSubscription !== undefined) {
                            skeleton.registerMulticastSubscription(parameters.multicastId);
                        }
                    }

                    multicastReceiversRegistry[multicastIdPattern].push(parameters.subscriberParticipantId);

                    //2. forward call to parent router (if available)
                    if (parentMessageRouterAddress === undefined || providerAddress === undefined || providerAddress instanceof InProcessAddress) {
                        return Promise.resolve();
                    }
                    if (routingProxy !== undefined) {
                        return routingProxy.addMulticastReceiver(parameters);
                    }
                    return new Promise(function(resolve, reject){
                        queuedAddMulticastReceiverCalls[queuedAddMulticastReceiverCalls.length] =
                        {
                            parameters : parameters,
                            resolve : resolve,
                            reject : reject
                        };
                    });
                };

                /**
                 * Removes a receiver for the identified multicasts.
                 *
                 * @name RoutingTable#removeMulticastReceiver
                 * @function
                 *
                 * @param {Object}
                 *            parameters - object containing parameters
                 * @param {String}
                 *            parameters.multicastId
                 * @param {String}
                 *            parameters.subscriberParticipantId
                 * @param {String}
                 *            parameters.providerParticipantId
                 */
                this.removeMulticastReceiver = function removeMulticastReceiver(parameters) {
                    //1. handle call in local router
                    //1.a remove receiver from multicastReceiverRegistry
                    var multicastIdPattern = multicastWildcardRegexFactory.createIdPattern(parameters.multicastId);
                    var providerAddress = routingTable[parameters.providerParticipantId];
                    if (multicastReceiversRegistry[multicastIdPattern] !== undefined) {
                        var i, receivers = multicastReceiversRegistry[multicastIdPattern];
                        for (i = 0; i < receivers.length; i++) {
                            if (receivers[i] === parameters.subscriberParticipantId) {
                                receivers.splice(i, 1);
                                break;
                            }
                        }
                        if (receivers.length === 0) {
                            delete multicastReceiversRegistry[multicastIdPattern];

                            //1.b no receiver anymore for this multicastId -> inform MessagingSkeleton about removed receiver
                            var skeleton = messagingSkeletonFactory.getSkeleton(providerAddress);
                            if (skeleton !== undefined && skeleton.unregisterMulticastSubscription !== undefined) {
                                skeleton.unregisterMulticastSubscription(parameters.multicastId);
                            }

                        }
                    }


                    //2. forward call to parent router (if available)
                    if (parentMessageRouterAddress === undefined || providerAddress === undefined || providerAddress instanceof InProcessAddress) {
                        return Promise.resolve();
                    }
                    if (routingProxy !== undefined) {
                        return routingProxy.removeMulticastReceiver(parameters);
                    }
                    return new Promise(function(resolve, reject){
                        queuedRemoveMulticastReceiverCalls[queuedRemoveMulticastReceiverCalls.length] =
                        {
                            parameters : parameters,
                            resolve : resolve,
                            reject : reject
                        };
                    });
                };

                /**
                 * @function MessageRouter#participantRegistered
                 *
                 * @param {String} participantId
                 *
                 * @returns {Promise} a Promise object
                 */
                this.participantRegistered =
                        function participantRegistered(participantId) {
                            var i, msgContainer, messageQueue =
                                    settings.messageQueue.getAndRemoveMessages(participantId);

                            if (messageQueue !== undefined) {
                                i = messageQueue.length;
                                while (i--) {
                                    try {
                                        that.route(messageQueue[i]);
                                    } catch(error) {
                                        log.error("queued message could not be sent to " + participantId + ", error: " + error
                                                + (error instanceof JoynrException ? " " + error.detailMessage : ""));
                                    }
                                }
                            }
                        };

                /**
                 * Tell the message router that the given participantId is known. The message router
                 * checks internally if an address is already present in the routing table. If not,
                 * it adds the parentMessageRouterAddress to the routing table for this participantId.
                 * @function MessageRouter#setToKnown
                 *
                 * @param {String} participantId
                 *
                 * @returns void
                 */
                this.setToKnown = function setToKnown(participantId) {
                    if (!isReady()) {
                        log.debug("setToKnown: ignore call as message router is already shut down");
                        return;
                    }

                    //if not already set
                    if (routingTable[participantId] === undefined) {
                        if (parentMessageRouterAddress !== undefined) {
                            routingTable[participantId] = parentMessageRouterAddress;
                        }
                    }
                };

                this.hasMulticastReceivers = function() {
                    return Object.keys(multicastReceiversRegistry).length > 0;
                };

                /**
                 * Shutdown the message router
                 *
                 * @function
                 * @name MessageRouter#shutdown
                 */
                this.shutdown = function shutdown() {
                    if (queuedAddNextHopCalls !== undefined) {
                        queuedAddNextHopCalls.forEach(function(call) {
                            call.reject(new Error("Message Router has been shut down"));
                        });
                        queuedAddNextHopCalls = [];
                    }
                    if (queuedRemoveNextHopCalls !== undefined) {
                        queuedRemoveNextHopCalls.forEach(function(call) {
                            call.reject(new Error("Message Router has been shut down"));
                        });
                        queuedRemoveNextHopCalls = [];
                    }
                    if (queuedAddMulticastReceiverCalls !== undefined) {
                        queuedAddMulticastReceiverCalls.forEach(function(call) {
                            call.reject(new Error("Message Router has been shut down"));
                        });
                        queuedAddMulticastReceiverCalls = [];
                    }
                    if (queuedRemoveMulticastReceiverCalls !== undefined) {
                        queuedRemoveMulticastReceiverCalls.forEach(function(call) {
                            call.reject(new Error("Message Router has been shut down"));
                        });
                        queuedRemoveMulticastReceiverCalls = [];
                    }
                    started = false;
                    settings.messageQueue.shutdown();
                };
            }

            return MessageRouter;
        });




© 2015 - 2025 Weber Informatics LLC | Privacy Policy