package er.extensions.foundation; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MulticastSocket; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOApplication; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; /** * NSNotificationCenter that can post simple notifications to other * applications. Currently just posts the name, no object and the userInfo as a * dictionary of strings. You must specifically register observers and post notifications here, not at * <code>NSNotificationCenter.defaultCenter()</code>. * <p> * If you don't link ERJGroupsSynchronizer, it will create a simple implementation, which posts via * multicast - and is thus not really reliable and can't handle larger useInfo dict * (of which keys and values must be strings). Also, you need to explicitely send the * notification locally if you want to receive it in the sending application. * @property er.extensions.ERXRemoteNotificationCenter.localBindAddress the local address to bind to * @property er.extensions.ERXRemoteNotificationCenter.group the multicast address to send to (230.0.0.1) * @property er.extensions.ERXRemoteNotificationCenter.port the multicast port to send to (9754) * @property er.extensions.ERXRemoteNotificationCenter.maxPacketSize the maximum multicast packet size (1024) * @property er.extensions.ERXRemoteNotificationCenter.identifier the unique identifier for this host (autogenerated by default) * * @author ak */ // TODO subclass of NSNotification that custom-serialize itself to a sparser // format public abstract class ERXRemoteNotificationCenter extends NSNotificationCenter { private static ERXRemoteNotificationCenter _sharedInstance; private static class SimpleCenter extends ERXRemoteNotificationCenter { private static final Logger log = LoggerFactory.getLogger(ERXRemoteNotificationCenter.class); public static final int IDENTIFIER_LENGTH = 6; private static final int JOIN = 1; private static final int LEAVE = 2; private static final int POST = 3; private boolean _postLocal; private byte[] _identifier; private InetAddress _localBindAddress; private NetworkInterface _localNetworkInterface; private InetSocketAddress _multicastGroup; private int _multicastPort; private MulticastSocket _multicastSocket; private boolean _listening; private int _maxPacketSize; protected SimpleCenter() throws IOException { init(); } protected void init() throws UnknownHostException, SocketException, IOException { String localBindAddressStr = ERXProperties.stringForKey("er.extensions.ERXRemoteNotificationsCenter.localBindAddress"); if (localBindAddressStr == null) { _localBindAddress = WOApplication.application().hostAddress(); } else { _localBindAddress = InetAddress.getByName(localBindAddressStr); } String multicastGroup = ERXProperties.stringForKeyWithDefault("er.extensions.ERXRemoteNotificationsCenter.group", "230.0.0.1"); _multicastPort = ERXProperties.intForKeyWithDefault("er.extensions.ERXRemoteNotificationsCenter.port", 9754); int maxPacketSize = ERXProperties.intForKeyWithDefault("er.extensions.ERXRemoteNotificationsCenter.maxPacketSize", 1024); _maxPacketSize = 2 * maxPacketSize; String multicastIdentifierStr = ERXProperties.stringForKey("er.extensions.ERXRemoteNotificationsCenter.identifier"); if (multicastIdentifierStr == null) { _identifier = new byte[IDENTIFIER_LENGTH]; byte[] hostAddressBytes = _localBindAddress.getAddress(); System.arraycopy(hostAddressBytes, 0, _identifier, 0, hostAddressBytes.length); int multicastInstance = WOApplication.application().port().shortValue(); _identifier[4] = (byte) (multicastInstance & 0xff); _identifier[5] = (byte) ((multicastInstance >>> 8) & 0xff); } else { _identifier = ERXStringUtilities.hexStringToByteArray(multicastIdentifierStr); } _localNetworkInterface = NetworkInterface.getByInetAddress(_localBindAddress); _multicastGroup = new InetSocketAddress(InetAddress.getByName(multicastGroup), _multicastPort); _multicastSocket = new MulticastSocket(null); _multicastSocket.setInterface(_localBindAddress); _multicastSocket.setTimeToLive(4); _multicastSocket.setReuseAddress(true); _multicastSocket.bind(new InetSocketAddress(_multicastPort)); join(); } public void join() throws IOException { if (log.isInfoEnabled()) { log.info("Multicast instance {} joining.", ERXStringUtilities.byteArrayToHexString(_identifier)); } _multicastSocket.joinGroup(_multicastGroup, _localNetworkInterface); try (MulticastByteArrayOutputStream baos = new MulticastByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) { dos.write(_identifier); dos.writeByte(JOIN); _multicastSocket.send(baos.createDatagramPacket()); } listen(); } public void leave() throws IOException { if (log.isInfoEnabled()) { log.info("Multicast instance {} leaving.", ERXStringUtilities.byteArrayToHexString(_identifier)); } try (MulticastByteArrayOutputStream baos = new MulticastByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) { dos.write(_identifier); dos.writeByte(LEAVE); _multicastSocket.send(baos.createDatagramPacket()); } _multicastSocket.leaveGroup(_multicastGroup, _localNetworkInterface); _listening = false; } @Override protected void postRemoteNotification(NSNotification notification) { try (MulticastByteArrayOutputStream baos = new MulticastByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) { dos.write(_identifier); dos.writeByte(POST); writeNotification(notification, dos); _multicastSocket.send(baos.createDatagramPacket()); if (log.isDebugEnabled()) { log.info("Multicast instance {}: Writing {}", ERXStringUtilities.byteArrayToHexString(_identifier), notification); } if (_postLocal) { postLocalNotification(notification); } } catch (Exception e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } public void listen() throws IOException { Thread listenThread = new Thread(new Runnable() { public void run() { _listening = true; byte[] buffer = new byte[_maxPacketSize]; while (_listening) { DatagramPacket receivePacket = new DatagramPacket(buffer, 0, buffer.length); try { _multicastSocket.receive(receivePacket); handlePacket(receivePacket); } catch (Throwable t) { log.error("Failed to read multicast notification.", t); } } } private void handlePacket(DatagramPacket receivePacket) throws IOException { ByteArrayInputStream bais = new ByteArrayInputStream(receivePacket.getData(), 0, receivePacket.getLength()); DataInputStream dis = new DataInputStream(bais); byte[] identifier = new byte[IDENTIFIER_LENGTH]; dis.readFully(identifier); String remote = ERXStringUtilities.byteArrayToHexString(identifier); byte code = dis.readByte(); if (code == JOIN) { log.info("Received JOIN from {}", remote); } else if (code == LEAVE) { log.info("Received LEAVE from {}", remote); } else if (code == POST) { String self = ERXStringUtilities.byteArrayToHexString(_identifier); log.info("Received POST from {}", remote); if (log.isDebugEnabled()) { log.debug("Received POST from {}", ERXStringUtilities.byteArrayToHexString(identifier)); } if(!self.equals(remote)) { NSNotification notification = readNotification(dis); if (log.isDebugEnabled()) { log.debug("Received notification: {}", notification); } else if (log.isInfoEnabled()) { log.info("Received {} notification from {}", notification.name(), remote); } postLocalNotification(notification); } } } }); listenThread.start(); } protected class MulticastByteArrayOutputStream extends ByteArrayOutputStream { public byte[] buffer() { return buf; } public DatagramPacket createDatagramPacket() throws SocketException { return new DatagramPacket(buf, 0, count, _multicastGroup); } } private NSNotification readNotification(DataInputStream dis) throws IOException { short nameLen = dis.readShort(); byte[] nameBytes = new byte[nameLen]; dis.readFully(nameBytes); short objectLen = dis.readShort(); byte[] objectBytes = new byte[objectLen]; dis.readFully(objectBytes); short userInfoLen = dis.readShort(); NSMutableDictionary userInfo = new NSMutableDictionary(); for (int i = 0; i < userInfoLen; i++) { short keyLen = dis.readShort(); byte[] keyBytes = new byte[keyLen]; dis.readFully(keyBytes); short valueLen = dis.readShort(); byte[] valueBytes = new byte[valueLen]; dis.readFully(valueBytes); userInfo.setObjectForKey(new String(valueBytes), new String(keyBytes)); } NSNotification notification = new NSNotification(new String(nameBytes), null, userInfo); return notification; } private void writeNotification(NSNotification notification, DataOutputStream dos) throws IOException { byte[] name = notification.name().getBytes(); dos.writeShort(name.length); dos.write(name); byte[] object = new byte[0]; dos.writeShort(object.length); dos.write(object); NSDictionary userInfo = notification.userInfo(); if (userInfo == null) { userInfo = NSDictionary.EmptyDictionary; } dos.writeShort(userInfo.count()); for (Object key : userInfo.allKeys()) { byte[] keyBytes = key.toString().getBytes(); byte[] valueBytes = userInfo.objectForKey(key).toString().getBytes(); dos.writeShort(keyBytes.length); dos.write(keyBytes); dos.writeShort(valueBytes.length); dos.write(valueBytes); } dos.flush(); if (dos.size() > _maxPacketSize) { throw new IllegalArgumentException("More than " + _maxPacketSize + " bytes"); } } } public static ERXRemoteNotificationCenter defaultCenter() { if (_sharedInstance == null) { synchronized (ERXRemoteNotificationCenter.class) { if (_sharedInstance == null) { try { _sharedInstance = new SimpleCenter(); } catch (IOException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } } } return _sharedInstance; } /** * Set the default center * @param center the notification center to use as default */ public static void setDefaultCenter(ERXRemoteNotificationCenter center) { _sharedInstance = center; } /** * Post a notification to the local app only. * @param notification the notification */ public void postLocalNotification(NSNotification notification) { super.postNotification(notification); } /** * Post a notification to the remote listeners. * @param notification the notification */ protected abstract void postRemoteNotification(NSNotification notification); /** * Overridden to call {@link #postRemoteNotification(NSNotification)}. */ @Override public void postNotification(NSNotification notification) { postRemoteNotification(notification); } }