/** * Copyright (c) 2011-2014, OpenIoT * * This file is part of OpenIoT. * * OpenIoT is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, version 3 of the License. * * OpenIoT is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with OpenIoT. If not, see <http://www.gnu.org/licenses/>. * * Contact: OpenIoT mailto: info@openiot.eu */ package org.openiot.cupus.entity.broker; import com.google.android.gcm.server.Sender; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.Socket; import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.UUID; import org.openiot.cupus.artefact.ActivePublication; import org.openiot.cupus.artefact.ActiveSubscription; import org.openiot.cupus.artefact.HashtablePublication; import org.openiot.cupus.artefact.MemorySubscription; import org.openiot.cupus.artefact.Publication; import org.openiot.cupus.artefact.Subscription; import org.openiot.cupus.artefact.TopKWSubscription; import org.openiot.cupus.common.UniqueObject; import org.openiot.cupus.entity.NetworkEntity; import org.openiot.cupus.message.InternalMessage; import org.openiot.cupus.message.Message; import org.openiot.cupus.message.external.AnnounceMessage; import org.openiot.cupus.message.external.MobileBrokerDisconnectMessage; import org.openiot.cupus.message.external.MobileBrokerRegisterGCMMessage; import org.openiot.cupus.message.external.MobileBrokerRegisterMessage; import org.openiot.cupus.message.external.NotifyMessage; import org.openiot.cupus.message.external.NotifySubscriptionMessage; import org.openiot.cupus.message.external.PublishMessage; import org.openiot.cupus.message.external.SubscribeMessage; import org.openiot.cupus.message.external.SubscriberDisconnectMessage; import org.openiot.cupus.message.external.SubscriberRegisterMessage; import org.openiot.cupus.message.external.SubscriberUnregisterMessage; import org.openiot.cupus.message.internal.ErrorMessage; import org.openiot.cupus.message.internal.InfoMessage; import org.openiot.cupus.message.internal.InitialAnnouncementMatchesMessage; import org.openiot.cupus.message.internal.InitialMatchesMessage; import org.openiot.cupus.message.internal.StartComponentMessage; import org.openiot.cupus.util.LogWriter; /** * DeliveryService is a component of a cloud-broker whose job is to keep and * maintain a directory of subscriber queues and to receive (UDP) pulish * messages from Matcher processes and put them in their respectivy queues. * * @author Eugen Rozic * */ public class DeliveryService extends NetworkEntity { private InternalCommunicationsThread intercomm = null; private HashMap<UUID, SubscriberQueue> queueDirectory = null; private int queueCapacity = -1; private boolean testing = false; @SuppressWarnings("unused") private boolean logWriting = false; private LogWriter log = null; protected boolean isRunning = false; private Sender gcmSender; private String APIkey = "AIzaSyADsn6N5Rq6an73KnSaEt3geI-BHy5nBdY"; //testing variables private UUID mb; private UUID sub; /** * Constructs the delivery service and starts it's internal communication * thread. * * @param UDPport The port on which this will listen and wait for * connections */ private DeliveryService(String brokerName, String brokerIP, int UDPport, int queueCapacity, boolean testing, boolean logWriting, String ApiKey, ObjectInputStream in, ObjectOutputStream out) { super(brokerName, brokerIP, UDPport); queueDirectory = new HashMap<UUID, SubscriberQueue>(); this.queueCapacity = queueCapacity; this.testing = testing; this.logWriting = logWriting; log = new LogWriter(brokerName + "_deliveryService.log", logWriting, false); intercomm = new InternalCommunicationsThread(in, out); new Thread(intercomm).start(); this.APIkey = ApiKey; if (this.APIkey != null && !this.APIkey.isEmpty()) { this.gcmSender = new Sender(APIkey); } } /** * Creates a UDP socket a thread that manages all incoming packets to it. * All notifications from all Matchers about matched subscribers are sent * through it. */ private void start() { new Thread(new NotifyReceiverThread()).start(); isRunning = true; informBroker("DeliveryService started!", false); } /** * Kills the (sub)process (which will automatically close all the * connections to the pubs and subs and close the stream to the CloudBroker * whose input stream will in turn throw an EOFException that will start the * shutdown of the CloudBroker and all it's children processes. */ private void shutdown() { isRunning = false; //not really necessary System.exit(-1); } /** * Puts a NotifyMessage with a publication in each of the subscribers queues * (a check for duplicates is done before adding to the queue). */ private void notifySubscribers(PublishMessage pubMsg, Set<UUID> subscribers) { Publication pub = null; if (pubMsg.getPublication() instanceof ActivePublication) { pub = ((ActivePublication) pubMsg.getPublication()).getPublication(); } else if (pubMsg.getPublication() instanceof HashtablePublication) { pub = pubMsg.getPublication(); } for (UUID subscriber : subscribers) { sub = subscriber; SubscriberQueue queue = queueDirectory.get(subscriber); NotifyMessage msg = new NotifyMessage(pub, false); if (pubMsg.isUnpublish()) { boolean found = queue.remove(msg); if (!found) { //if not found locally must have already been sent to subscriber, in which case he should be notified that the publication was unpublished... msg.setUnpublish(true); queue.put(msg); } } else { queue.put(msg); } } } /** * Puts a NotifyMessage in the subscriber's queue (the one identified by the * given UUID) for each of the publications in the pubs list.<br> * Used for notifing a subscriber about the initial matches to a * subscription he just subscribed (the matches to the beforehand published * but still active publications). */ private void bulkNotify(UUID subscriber, List<Publication> pubs) { SubscriberQueue queue = queueDirectory.get(subscriber); for (Publication pub : pubs) { NotifyMessage msg = new NotifyMessage(pub, false); queue.put(msg); } } private void notifyMobileBroker(UUID mbID, Set<Subscription> subs) { mb = mbID; SubscriberQueue queue = queueDirectory.get(mbID); for (Subscription sub : subs) { NotifySubscriptionMessage msg = new NotifySubscriptionMessage(sub, false); queue.put(msg); } } private void notifyMobileBrokers(SubscribeMessage subscription, Set<UUID> mobileBrokerIDs) { Subscription subForSending = null; if (subscription.getSubscription() instanceof ActiveSubscription) { subForSending = ((ActiveSubscription)subscription.getSubscription()).getSubscription(); } else if (subscription.getSubscription() instanceof MemorySubscription) { subForSending = ((MemorySubscription)subscription.getSubscription()).getSubscription(); } else { subForSending = subscription.getSubscription(); } for (UUID mb : mobileBrokerIDs) { SubscriberQueue queue = queueDirectory.get(mb); NotifySubscriptionMessage msg = new NotifySubscriptionMessage(subForSending, false); if (subscription.isUnsubscribe()) { boolean found = queue.remove(msg); if (!found) { //if not found locally must have already been sent to subscriber, in which case he should be notified that the publication was unpublished... msg.setRevoke(true); queue.put(msg); } } else { queue.put(msg); } } } /** * Convinience method that sends an ErrorMessage or an InfoMessage to the * CloudBroker, depending if the reporting flag is set or not. It also logs * the message to this DeliveryService's log file (if logging is on). */ protected void informBroker(String msg, boolean error) { if (error) { log.writeToLog("ERROR: " + msg); if (testing) { sendInternalMessage(new ErrorMessage(msg)); } } else { log.writeToLog(msg); if (testing) { sendInternalMessage(new InfoMessage(msg)); } } } /** * Convinience method to send a message to the CloudBroker. It has to be * synchronized because multiple threads may want to send something at the * same time. */ synchronized protected void sendInternalMessage(Object msg) { try { intercomm.out.writeObject(msg); intercomm.out.flush(); } catch (IOException e) { //if component found terminated shut everything down... this.shutdown(); } } synchronized protected Sender getSender() { return gcmSender; } //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** * For communicating with the starting process (CloudBroker) */ private class InternalCommunicationsThread implements Runnable { ObjectInputStream in = null; ObjectOutputStream out = null; public InternalCommunicationsThread(ObjectInputStream in, ObjectOutputStream out) { this.in = in; this.out = out; } @Override public void run() { while (true) { Object objIn = null; try { objIn = in.readObject(); } catch (Exception e) { DeliveryService.this.shutdown(); } if (objIn instanceof InternalMessage) { if (objIn instanceof StartComponentMessage) { //DeliveryService.this.start(); } else if (objIn instanceof InitialMatchesMessage) { //notify the subscriber about all of the publications bulkNotify(((InitialMatchesMessage) objIn).getSubscriberID(), ((InitialMatchesMessage) objIn).getInitialMatches()); } else if (objIn instanceof InitialAnnouncementMatchesMessage) { //notify the subscriber about all of the publications notifyMobileBrokers(((InitialAnnouncementMatchesMessage) objIn).getSubscriptionMessage(), ((InitialAnnouncementMatchesMessage) objIn).getMobileBrokerIDs()); } else { informBroker("DeliveryService: Unexpected internal message received from CloudBroker - " + objIn.getClass().getName(), true); } } else if (objIn instanceof Message) { if (objIn instanceof SubscriberDisconnectMessage) { UUID subID = ((SubscriberDisconnectMessage) objIn).getEntityID(); SubscriberQueue queue = queueDirectory.get(subID); if (queue != null){ queue.terminateConnection(); } informBroker("Subscriber " + ((SubscriberDisconnectMessage) objIn).getEntityName() + " disconnected from broker!", false); } else if (objIn instanceof SubscriberUnregisterMessage) { UUID subID = ((SubscriberUnregisterMessage) objIn).getEntityID(); SubscriberQueue queue = queueDirectory.remove(subID); if (queue != null){ queue.terminateConnection(); } informBroker("Subscriber " + ((SubscriberUnregisterMessage) objIn).getEntityName() + " unregistered from broker!", false); } else if (objIn instanceof SubscriberRegisterMessage) { SubscriberRegisterMessage msg = (SubscriberRegisterMessage) objIn; SubscriberQueue queue = new SubscriberQueue(msg.getEntityID(), queueCapacity, DeliveryService.this, false); queueDirectory.put(msg.getEntityID(), queue); Socket socket = null; try { socket = new Socket(msg.getIP(), msg.getPort()); queue.setConnection(socket); } catch (Exception e) { //if not able to make connection... queue.terminateConnection(); //just in case sendInternalMessage(new SubscriberDisconnectMessage( msg.getEntityName(), msg.getEntityID())); continue; //go wait for next message } //if connection successfully made... new Thread(queue).start(); //start communication thread informBroker("Subscriber " + ((SubscriberRegisterMessage) objIn).getEntityName() + " connected to broker!", false); } else if (objIn instanceof MobileBrokerDisconnectMessage) { UUID mbID = ((MobileBrokerDisconnectMessage) objIn).getEntityID(); SubscriberQueue queue = queueDirectory.remove(mbID); if (queue != null){ queue.terminateConnection(); } informBroker("Mobile broker " + ((MobileBrokerDisconnectMessage) objIn).getEntityName() + " unregistered from broker!", false); } else if (objIn instanceof MobileBrokerRegisterMessage) { MobileBrokerRegisterMessage msg = (MobileBrokerRegisterMessage) objIn; SubscriberQueue queue = new SubscriberQueue(msg.getEntityID(), queueCapacity, DeliveryService.this, false); queueDirectory.put(msg.getEntityID(), queue); Socket socket = null; try { socket = new Socket(msg.getIP(), msg.getPort()); queue.setConnection(socket); } catch (Exception e) { //if not able to make connection... queue.terminateConnection(); //just in case sendInternalMessage(new MobileBrokerDisconnectMessage( msg.getEntityName(), msg.getEntityID())); continue; //go wait for next message } //if connection successfully made... new Thread(queue).start(); //start communication thread informBroker("Mobile broker " + ((MobileBrokerRegisterMessage) objIn).getEntityName() + " connected to broker!", false); } else if (objIn instanceof MobileBrokerRegisterGCMMessage) { MobileBrokerRegisterGCMMessage msg = (MobileBrokerRegisterGCMMessage) objIn; SubscriberQueue queue = new SubscriberQueue(msg.getEntityID(), queueCapacity, DeliveryService.this, true); queueDirectory.put(msg.getEntityID(), queue); if (msg.getRegistrationID() != null && !msg.getRegistrationID().isEmpty()) { //informBroker("User RegId="+msg.getRegistrationID(), false); queue.setGCMId(msg.getRegistrationID()); } else { if (msg.getRegistrationID() == null || msg.getRegistrationID().isEmpty()) { informBroker("GCM user " + msg.getEntityName() + " did not provide GCM registration ID", false); } //if a user did not send a registration key queue.terminateConnection(); //just in case sendInternalMessage(new MobileBrokerDisconnectMessage( msg.getEntityName(), msg.getEntityID())); continue; //go wait for next message } //if connection successfully made... new Thread(queue).start(); //start communication thread informBroker("Mobile broker " + ((MobileBrokerRegisterGCMMessage) objIn).getEntityName() + " connected to broker!", false); } else { informBroker("DeliveryService: Unexpected external message received from CloudBroker - " + objIn.getClass().getName(), true); } } else { String errMsg = "DeliveryService: Received message is not of class Message" + " or InternalMessage! (" + objIn.getClass() + " instead). Ignoring..."; informBroker(errMsg, true); } } } } /** * For receiving UDP messages from Matchers that carry a publication and a * list of subscriberID's to which the publication needs to be sent. */ private class NotifyReceiverThread implements Runnable { private DatagramSocket UDPsocket = null; public NotifyReceiverThread() { try { UDPsocket = new DatagramSocket(DeliveryService.this.myPort); } catch (Exception e) { DeliveryService.this.shutdown(); } } @SuppressWarnings("unchecked") @Override public void run() { byte[] data = new byte[64 * 1000]; //64 KB, max IP packet DatagramPacket packet = new DatagramPacket(data, data.length); ByteArrayInputStream bais = new ByteArrayInputStream(data); while (true) { try { //read the next packet... UDPsocket.receive(packet); } catch (Exception e) { if (UDPsocket.isClosed()) { informBroker("DeliveryService crashed because the UDPSocket got closed (somehow, unexpectedly).", true); DeliveryService.this.shutdown(); } else { continue; } } bais.reset(); //sets the stream to start reading from 0 try { ObjectInputStream ois = new ObjectInputStream(bais); //has to read the stream header each time Object messageType = ois.readObject(); if (messageType instanceof PublishMessage) { PublishMessage pubMsg = (PublishMessage) messageType; Set<UUID> subscriberIDs = (Set<UUID>) ois.readObject(); notifySubscribers(pubMsg, subscriberIDs); } else if (messageType instanceof AnnounceMessage) { Set<Subscription> subscriptions = (Set<Subscription>) ois.readObject(); UUID mbID = (UUID) ois.readObject(); notifyMobileBroker(mbID, subscriptions); } else { //TODO send some sort of NACK } } catch (Exception e) { //TODO send some sort of NACK continue; } } } } //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** * Used for starting of the component. It creates an instance with the * received arguments and starts a thread that listens on the input stream. */ public static void main(String[] args) { ObjectInputStream in = null; ObjectOutputStream out = null; try { out = new ObjectOutputStream(System.out); out.flush(); in = new ObjectInputStream(System.in); } catch (Exception e) { //don't write anything, just kill the (sub)process. System.exit(-1); } String brokerName = null; String brokerIP = null; int port = -1; int queueCapacity = -1; boolean testing = false; boolean logWriting = false; String api=""; try { brokerName = args[0]; brokerIP = args[1]; if (!brokerIP.equals(UniqueObject.getLocalIP())) { throw new Exception("IP's don't match!"); } port = Integer.parseInt(args[2]); if (port <= 1024 || port > 49151) { throw new NumberFormatException("Given port number is <1024 or >49151 !"); } queueCapacity = Integer.parseInt(args[3]); if (queueCapacity <= 0) { throw new NumberFormatException("Given queue capacity is <=0 !"); } testing = Boolean.parseBoolean(args[4]); logWriting = Boolean.parseBoolean(args[5]); api = args[6]; } catch (IndexOutOfBoundsException e) { String errMsg = e.getMessage() + " Not enough argument sent when starting DeliveryService! (7 needed)"; sendObject(new ErrorMessage(errMsg), out); System.exit(-1); } catch (NumberFormatException e) { String errMsg = e.getMessage(); sendObject(new ErrorMessage(errMsg), out); System.exit(-1); } catch (Exception e) { sendObject(new ErrorMessage(e.getMessage()), out); System.exit(-1); } //Sender sender = new Sender(api); //create a new DeliveryService, a thread listening on System.in is //automatically started and keeps the process alive DeliveryService ds = new DeliveryService(brokerName, brokerIP, port, queueCapacity, testing, logWriting, api, in, out); ds.start(); sendObject(new InfoMessage("DeliveryService created!"), out); } /** * convinience method for sending object and terminating process if not * successfull. */ private static void sendObject(Object o, ObjectOutputStream out) { try { out.writeObject(o); out.flush(); } catch (Exception e) { System.exit(-1); } } }