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