package er.extensions.remoteSynchronizer; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.lang.reflect.Constructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.eoaccess.EOEntityClassDescription; import com.webobjects.eocontrol.EOGlobalID; import com.webobjects.eocontrol.EOKeyGlobalID; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSMutableSet; import com.webobjects.foundation.NSSet; import er.extensions.eof.ERXDatabase; import er.extensions.eof.ERXObjectStoreCoordinatorSynchronizer.IChangeListener; import er.extensions.eof.ERXObjectStoreCoordinatorSynchronizer.RemoteChange; import er.extensions.foundation.ERXProperties; /** * The superclass of all remote EOF synchronizers. * * @property er.extensions.remoteSynchronizer.enabled if true, remote synchronization is enabled * @property er.extensions.remoteSynchronizer the class name of the remote synchronizer to use (default to ERXSimpleMulticastSynchronizer) * @property er.extensions.remoteSynchronizer.includeEntities * @property er.extensions.remoteSynchronizer.excludeEntities * * @author mschrag */ public abstract class ERXRemoteSynchronizer { private static final Logger log = LoggerFactory.getLogger(ERXRemoteSynchronizer.class); public static boolean remoteSynchronizerEnabled() { return ERXProperties.booleanForKeyWithDefault("er.extensions.remoteSynchronizer.enabled", false); } public static ERXRemoteSynchronizer newRemoteSynchronizer(IChangeListener changeListener) throws Throwable { String remoteSynchronizerClassName = ERXProperties.stringForKey("er.extensions.remoteSynchronizer"); ERXRemoteSynchronizer remoteSynchronizer; if (remoteSynchronizerClassName == null) { remoteSynchronizer = new ERXSimpleMulticastSynchronizer(changeListener); } else { Class remoteSynchronizerClass = Class.forName(remoteSynchronizerClassName); Constructor remoteSynchronizerConstructor = remoteSynchronizerClass.getConstructor(new Class[] { IChangeListener.class }); remoteSynchronizer = (ERXRemoteSynchronizer) remoteSynchronizerConstructor.newInstance(new Object[] { changeListener }); } return remoteSynchronizer; } private static final int INSERT = 3; private static final int UPDATE = 4; private static final int DELETE = 5; private static final int TO_MANY_UPDATE = 6; private static final int INVALIDATE = 7; private static final int BYTE_TYPE = 1; private static final int SHORT_TYPE = 2; private static final int INT_TYPE = 3; private static final int LONG_TYPE = 4; private static final int DATA_TYPE = 5; private static final int STRING_TYPE = 6; private IChangeListener _listener; private NSSet<String> _includeEntityNames; private NSSet<String> _excludeEntityNames; public ERXRemoteSynchronizer(IChangeListener listener) { _listener = listener; String includeEntityNames = ERXProperties.stringForKey("er.extensions.remoteSynchronizer.includeEntities"); if (includeEntityNames != null) { _includeEntityNames = new NSSet<>(NSArray.componentsSeparatedByString(includeEntityNames, ",")); } String excludeEntityNames = ERXProperties.stringForKey("er.extensions.remoteSynchronizer.excludeEntities"); if (excludeEntityNames != null) { _excludeEntityNames = new NSSet<>(NSArray.componentsSeparatedByString(excludeEntityNames, ",")); } } protected void _readCacheChange(RemoteChange remoteChange, DataInputStream dis) throws IOException { int messageType = dis.readByte(); if (messageType == ERXRemoteSynchronizer.INSERT) { EOGlobalID gid = readGID(dis); ERXDatabase.SnapshotInserted change = new ERXDatabase.SnapshotInserted(gid, NSDictionary.EmptyDictionary); log.info("Remote instance ({}) inserted {}", remoteChange.identifier(), change); remoteChange.addRemoteCacheChange(change); } else if (messageType == ERXRemoteSynchronizer.UPDATE) { EOGlobalID gid = readGID(dis); ERXDatabase.SnapshotUpdated change = new ERXDatabase.SnapshotUpdated(gid, NSDictionary.EmptyDictionary); log.info("Remote instance ({}) updated {}", remoteChange.identifier(), change); remoteChange.addRemoteCacheChange(change); } else if (messageType == ERXRemoteSynchronizer.DELETE) { EOGlobalID gid = readGID(dis); ERXDatabase.SnapshotDeleted change = new ERXDatabase.SnapshotDeleted(gid, NSDictionary.EmptyDictionary); log.info("Remote instance ({}) deleted {}", remoteChange.identifier(), change); remoteChange.addRemoteCacheChange(change); } else if (messageType == ERXRemoteSynchronizer.TO_MANY_UPDATE) { EOGlobalID sourceGID = readGID(dis); String name = dis.readUTF(); NSArray<EOGlobalID> addedGIDs = readGIDs(dis); NSArray<EOGlobalID> removedGIDs = readGIDs(dis); boolean removeAll = dis.readBoolean(); ERXDatabase.ToManySnapshotUpdated change = new ERXDatabase.ToManySnapshotUpdated(sourceGID, name, addedGIDs, removedGIDs, removeAll); log.info("Remote instance ({}) update to-many {}", remoteChange.identifier(), change); remoteChange.addRemoteCacheChange(change); } else if (!handleMessageType(messageType, remoteChange, dis)) { throw new IllegalArgumentException("Unknown remote message type #" + messageType + "."); } } protected boolean handleMessageType(int messageType, RemoteChange remoteChange, DataInputStream dis) { return false; } protected void _writeCacheChange(DataOutputStream dos, ERXDatabase.CacheChange cacheChange) throws IOException { if (cacheChange instanceof ERXDatabase.SnapshotInserted) { dos.writeByte(ERXRemoteSynchronizer.INSERT); writeSnapshotCacheChange(dos, cacheChange); } else if (cacheChange instanceof ERXDatabase.SnapshotUpdated) { dos.writeByte(ERXRemoteSynchronizer.UPDATE); writeSnapshotCacheChange(dos, cacheChange); } else if (cacheChange instanceof ERXDatabase.SnapshotDeleted) { dos.writeByte(ERXRemoteSynchronizer.DELETE); writeSnapshotCacheChange(dos, cacheChange); } else if (cacheChange instanceof ERXDatabase.ToManySnapshotUpdated) { dos.writeByte(ERXRemoteSynchronizer.TO_MANY_UPDATE); ERXDatabase.ToManySnapshotUpdated toManyChange = (ERXDatabase.ToManySnapshotUpdated) cacheChange; NSArray<EOGlobalID> addedGIDs = toManyChange.addedGIDs(); NSArray<EOGlobalID> removedGIDs = toManyChange.removedGIDs(); writeGID(dos, toManyChange.gid()); dos.writeUTF(toManyChange.name()); writeGIDs(dos, addedGIDs); if (toManyChange.removeAll()) { writeGIDs(dos, null); dos.writeBoolean(true); } else { writeGIDs(dos, removedGIDs); dos.writeBoolean(false); } } } protected void writeSnapshotCacheChange(DataOutputStream dos, ERXDatabase.CacheChange cacheChange) throws IOException { writeGID(dos, cacheChange.gid()); } protected void writeGIDs(DataOutputStream dos, NSArray<EOGlobalID> gids) throws IOException { int count = (gids == null) ? 0 : gids.count(); dos.writeByte(count); if (count > 0) { for (EOGlobalID gid : gids) { writeGID(dos, gid); } } } protected void writeGID(DataOutputStream dos, EOGlobalID gid) throws IOException { EOKeyGlobalID keyGID = (EOKeyGlobalID) gid; String entityName = keyGID.entityName(); dos.writeUTF(entityName); writeGIDKeys(dos, keyGID); } protected void writeGIDKeys(DataOutputStream dos, EOKeyGlobalID gid) throws IOException { Object[] values = gid._keyValuesNoCopy(); dos.writeByte(values.length); for (int keyNum = 0; keyNum < values.length; keyNum++) { writeKey(dos, values[keyNum]); } } protected void writeKey(DataOutputStream dos, Object key) throws IOException { if (key instanceof Byte) { dos.writeByte(ERXRemoteSynchronizer.BYTE_TYPE); dos.writeShort(((Byte) key).byteValue()); } else if (key instanceof Short) { dos.writeByte(ERXRemoteSynchronizer.SHORT_TYPE); dos.writeShort(((Short) key).shortValue()); } else if (key instanceof Integer) { dos.writeByte(ERXRemoteSynchronizer.INT_TYPE); dos.writeInt(((Integer) key).intValue()); } else if (key instanceof Long) { dos.writeByte(ERXRemoteSynchronizer.LONG_TYPE); dos.writeLong(((Long) key).longValue()); } else if (key instanceof NSData) { NSData data = (NSData) key; dos.writeByte(ERXRemoteSynchronizer.DATA_TYPE); dos.writeByte(data.length()); data.writeToStream(dos); } else if (key instanceof String) { String str = (String)key; dos.writeByte(ERXRemoteSynchronizer.STRING_TYPE); dos.writeUTF(str); } else { throw new IllegalArgumentException("RemoteSynchronizer can't handle key '" + key + "'."); } } protected NSArray<EOGlobalID> readGIDs(DataInputStream dis) throws IOException { NSMutableArray<EOGlobalID> gids = new NSMutableArray<>(); int gidCount = dis.readByte(); for (int gidNum = 0; gidNum < gidCount; gidNum++) { EOGlobalID gid = readGID(dis); gids.addObject(gid); } return gids; } protected EOGlobalID readGID(DataInputStream dis) throws IOException { String entityName = dis.readUTF(); EOEntityClassDescription classDescription = (EOEntityClassDescription) EOEntityClassDescription.classDescriptionForEntityName(entityName); return _readGID(classDescription, entityName, dis); } protected EOGlobalID _readGID(EOEntityClassDescription classDescription, String entityName, DataInputStream dis) throws IOException { EOKeyGlobalID gid; int keyCount = dis.readByte(); if (keyCount == -1) { gid = null; } else { Object[] keys = new Object[keyCount]; for (int i = 0; i < keyCount; i++) { keys[i] = readKey(dis); } gid = classDescription._globalIDWithEntityName(entityName, keys); } return gid; } protected Object readKey(DataInputStream dis) throws IOException { Object obj; int keyType = dis.readByte(); if (keyType == ERXRemoteSynchronizer.BYTE_TYPE) { obj = Byte.valueOf(dis.readByte()); } else if (keyType == ERXRemoteSynchronizer.SHORT_TYPE) { obj = Short.valueOf(dis.readShort()); } else if (keyType == ERXRemoteSynchronizer.INT_TYPE) { obj = Integer.valueOf(dis.readInt()); } else if (keyType == ERXRemoteSynchronizer.LONG_TYPE) { obj = Long.valueOf(dis.readLong()); } else if (keyType == ERXRemoteSynchronizer.DATA_TYPE) { int size = dis.readByte(); byte[] data = new byte[size]; dis.readFully(data); obj = new NSData(data); } else if (keyType == ERXRemoteSynchronizer.STRING_TYPE) { obj = dis.readUTF(); } else { throw new IllegalArgumentException("Unknown key type #" + keyType + "."); } return obj; } public boolean shouldSynchronizeEntity(String entityName) { boolean shouldSynchronizeEntity = true; if (_includeEntityNames != null) { shouldSynchronizeEntity = _includeEntityNames.containsObject(entityName); } if (shouldSynchronizeEntity && _excludeEntityNames != null) { shouldSynchronizeEntity = !_excludeEntityNames.containsObject(entityName); } return shouldSynchronizeEntity; } public NSDictionary<String, NSSet<EOGlobalID>> globalIDsGroupedByEntity(NSArray<EOGlobalID> gids) { if (gids == null) { return NSDictionary.EmptyDictionary; } NSMutableDictionary<String, NSSet<EOGlobalID>> result = new NSMutableDictionary<String, NSSet<EOGlobalID>>(); for (EOGlobalID gid : gids) { if (gid instanceof EOKeyGlobalID) { EOKeyGlobalID keyGID = (EOKeyGlobalID)gid; String entityName = keyGID.entityName(); if (shouldSynchronizeEntity(entityName)) { NSMutableSet<EOGlobalID> globalIDsForEntity = (NSMutableSet<EOGlobalID>) result.objectForKey(entityName); if (globalIDsForEntity == null) { globalIDsForEntity = new NSMutableSet<>(); result.setObjectForKey(globalIDsForEntity, entityName); } globalIDsForEntity.addObject(keyGID); } } } return result.immutableClone(); } protected void addChange(RemoteChange remoteChange) { _listener.addChange(remoteChange); } protected NSArray<ERXDatabase.CacheChange> filteredCacheChanges(NSArray<ERXDatabase.CacheChange> cacheChanges) { NSArray<ERXDatabase.CacheChange> filteredCacheChanges; if (_includeEntityNames == null && (_excludeEntityNames == null || _excludeEntityNames.count() == 0)) { filteredCacheChanges = cacheChanges; } else { NSMutableArray<ERXDatabase.CacheChange> mutableFilteredCacheChanges = new NSMutableArray<>(); for (ERXDatabase.CacheChange cacheChange : cacheChanges) { EOGlobalID gid = cacheChange.gid(); if (gid instanceof EOKeyGlobalID) { EOKeyGlobalID keyGID = (EOKeyGlobalID)gid; String entityName = keyGID.entityName(); if (shouldSynchronizeEntity(entityName)) { mutableFilteredCacheChanges.addObject(cacheChange); } } } filteredCacheChanges = mutableFilteredCacheChanges; } return filteredCacheChanges; } public abstract void join() throws Throwable; public abstract void leave() throws Throwable; public abstract void listen() throws Throwable; public void writeCacheChanges(int transactionID, NSArray<ERXDatabase.CacheChange> cacheChanges) throws Throwable { _writeCacheChanges(transactionID, filteredCacheChanges(cacheChanges)); } protected abstract void _writeCacheChanges(int transactionID, NSArray<ERXDatabase.CacheChange> cacheChanges) throws Throwable; public static class RefByteArrayOutputStream extends ByteArrayOutputStream { public byte[] buffer() { return buf; } } }