package er.extensions.eof;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import com.webobjects.eoaccess.EODatabaseContext;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EORelationship;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.eocontrol.EOFaultHandler;
import com.webobjects.eocontrol.EOFetchSpecification;
import com.webobjects.eocontrol.EOGlobalID;
import com.webobjects.eocontrol.EOKeyGlobalID;
import com.webobjects.eocontrol.EOObjectStoreCoordinator;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOTemporaryGlobalID;
import com.webobjects.eocontrol._EOIntegralKeyGlobalID;
import com.webobjects.eocontrol._EOVectorKeyGlobalID;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSData;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSTimestamp;
import er.extensions.foundation.ERXArrayUtilities;
/**
* Utilities that help with batch loading sets of global IDs.
*
* @author ak
*/
public class ERXEOGlobalIDUtilities {
/**
* Decrypts the byte array of NSData PKs so you get the process number or port,
* host and timestamp.
*
* @author ak
*/
public static class Info {
private byte _data[];
private static final int _HostIdentificationStartIndex = 0;
private static final int _ProcessIdentificationStartIndex = 6;
private static final int _CounterStartIndex = 10;
private static final int _TimestampStartIndex = 12;
private static final int _RandomStartIndex = 20;
public Info(EOGlobalID gid) {
if(gid instanceof EOTemporaryGlobalID) {
_data = ((EOTemporaryGlobalID)gid)._rawBytes();
} else if (gid instanceof EOKeyGlobalID) {
EOKeyGlobalID keyGid = (EOKeyGlobalID)gid;
Object value = keyGid.keyValues()[0];
if(value instanceof NSData && keyGid.keyValues().length == 1) {
_data = ((NSData)value)._bytesNoCopy();
}
}
if(_data == null) {
throw new IllegalArgumentException("This class only works with EOTemporaryGlobalID or EOKeyGlobalID with a single 24-byte data PK");
}
}
private byte extractByte(int offset) {
return _data[offset + 0];
}
@SuppressWarnings("cast")
private short extractShort(int offset) {
short result = 0;
result |= ((int)extractByte(offset + 0)) & 255;
result <<= 8;
result |= ((int)extractByte(offset + 1)) & 255;
return result;
}
private int extractInt(int offset) {
int result = 0;
result |= extractShort(offset + 0) & 65535;
result <<= 16;
result |= extractShort(offset + 2) & 65535;
return result;
}
private long extractLong(int offset) {
long result = 0;
result |= extractInt(offset + 0) & 4294967295L;
result <<= 32;
result |= extractInt(offset + 4) & 4294967295L;
return result;
}
public NSTimestamp timestamp() {
return new NSTimestamp(milliseconds());
}
public long milliseconds() {
return extractLong(_TimestampStartIndex);
}
public InetAddress host() {
try {
byte data[] = new byte[4];
System.arraycopy(_data, _HostIdentificationStartIndex + 2, data, 0, 4);
return InetAddress.getByAddress(data);
} catch (UnknownHostException e) {
return null;
}
}
public long port() {
return extractInt(_ProcessIdentificationStartIndex);
}
public short counter() {
return extractShort(_CounterStartIndex);
}
public long random() {
return extractLong(_RandomStartIndex);
}
@Override
public String toString() {
return host().getHostAddress() + ":" + port() + " " + counter() + "@" + timestamp();
}
}
/**
* Groups an array of global IDs by their entity name.
*
* @param <T> subclass of EOGlobalID
* @param globalIDs list of global IDs
* @return dictionary with global IDs grouped by their entity name
*/
public static <T extends EOGlobalID> NSDictionary<String, NSArray<T>> globalIDsGroupedByEntityName(Collection<T> globalIDs) {
return ERXArrayUtilities.arrayGroupedByKeyPath(globalIDs, "entityName");
}
/**
* Translates an array of {@link EOGlobalID} to primary key values. Throws an exception if the given
* global IDs are not EOKeyGlobalIDs and the primary keys are not single values.
*
* @param <T> subclass of EOGlobalID
* @param globalIDs list of global IDs
* @return list of primary key values
*/
public static <T extends EOGlobalID> NSArray<Object> primaryKeyValuesWithGlobalIDs(Collection<T> globalIDs) {
if (globalIDs == null || globalIDs.isEmpty()) {
return NSArray.emptyArray();
}
NSMutableArray<Object> result = new NSMutableArray<>(globalIDs.size());
NSDictionary<String, NSArray<T>> gidsByEntity = globalIDsGroupedByEntityName(globalIDs);
for (String entityName : gidsByEntity.keySet()) {
NSArray<T> gidsForEntity = gidsByEntity.objectForKey(entityName);
for (T gid : gidsForEntity) {
if (gid instanceof EOKeyGlobalID) {
EOKeyGlobalID keyGID = (EOKeyGlobalID) gid;
if (keyGID.keyCount() == 1) {
result.addObject(keyGID.keyValues()[0]);
} else {
throw new IllegalArgumentException("GID has more than one key: " + keyGID);
}
} else {
throw new IllegalArgumentException("GID is not an EOKeyGlobalID: " + gid);
}
}
}
return result;
}
/**
* Translates an array of single-value raw primary key values to EOGlobalIDs.
*
* @param entityName the entity name of the raw primary values
* @param values list of raw primary key values
* @return list of global IDs
*/
public static NSArray<EOGlobalID> globalIDsWithPrimaryKeyValues(String entityName, Collection<Object> values) {
if (values == null || values.isEmpty()) {
return NSArray.emptyArray();
}
NSMutableArray<EOGlobalID> result = new NSMutableArray<>(values.size());
for (Object value : values) {
EOKeyGlobalID gid = EOKeyGlobalID.globalIDWithEntityName(entityName, new Object[] {value});
result.addObject(gid);
}
return result;
}
/**
* Fetches an object defined by gid without refreshing refetched objects.
*
* @param ec the editing context to fetch within
* @param gid the global ID to fetch
* @return the fetched EO
*/
public static EOEnterpriseObject fetchObjectWithGlobalID(EOEditingContext ec, EOGlobalID gid) {
NSArray<EOEnterpriseObject> results = fetchObjectsWithGlobalIDs(ec, new NSArray<>(gid));
return ERXArrayUtilities.firstObject(results);
}
/**
* Fetches an array of objects defined by the globalIDs in a single fetch per entity without
* refreshing refetched objects.
*
* @param <T> subclass of EOGlobalID
* @param ec the editing context to fetch within
* @param globalIDs the global IDs to fetch
* @return the fetched EOs
*/
public static <T extends EOGlobalID> NSMutableArray<EOEnterpriseObject> fetchObjectsWithGlobalIDs(EOEditingContext ec, Collection<T> globalIDs) {
return fetchObjectsWithGlobalIDs(ec, globalIDs, false);
}
/**
* Fetches an array of objects defined by the globalIDs in a single fetch per entity.
*
* @param <T> subclass of EOGlobalID
* @param ec the editing context to fetch within
* @param globalIDs the global IDs to fetch
* @param refreshesRefetchedObjects whether or not to refresh refetched objects
* @return the fetched EOs
*/
public static <T extends EOGlobalID> NSMutableArray<EOEnterpriseObject> fetchObjectsWithGlobalIDs(EOEditingContext ec, Collection<T> globalIDs, boolean refreshesRefetchedObjects) {
NSMutableArray<EOEnterpriseObject> result = new NSMutableArray<>();
ec.lock();
ec.rootObjectStore().lock();
try {
NSDictionary<String, NSArray<T>> gidsByEntity = globalIDsGroupedByEntityName(globalIDs);
for(String entityName : gidsByEntity.keySet()) {
NSArray<T> gidsForEntity = gidsByEntity.objectForKey(entityName);
NSMutableArray<EOQualifier> qualifiers = new NSMutableArray<>();
EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, entityName);
for (T g : gidsForEntity) {
boolean fetch = refreshesRefetchedObjects;
if (!fetch) {
EOEnterpriseObject eo;
eo = ec.objectForGlobalID(g);
if (eo != null && !EOFaultHandler.isFault(eo)) {
result.addObject(eo);
}
else {
NSDictionary row;
EODatabaseContext databaseContext = (EODatabaseContext) ((EOObjectStoreCoordinator) ec.rootObjectStore()).objectStoreForGlobalID(g);
databaseContext.lock();
try {
row = databaseContext.snapshotForGlobalID(g, ec.fetchTimestamp());
}
finally {
databaseContext.unlock();
}
if (row == null) {
fetch = true;
}
else {
eo = ec.faultForGlobalID(g, ec);
result.addObject(eo);
}
}
}
if (fetch) {
EOQualifier qualifier = entity.qualifierForPrimaryKey(entity.primaryKeyForGlobalID(g));
qualifiers.addObject(qualifier);
}
}
if (!qualifiers.isEmpty()) {
EOQualifier qualifier = qualifiers.size() > 1 ? ERXQ.or(qualifiers) : qualifiers.lastObject();
EOFetchSpecification fetchSpec = new EOFetchSpecification(entityName, qualifier, null);
fetchSpec.setRefreshesRefetchedObjects(refreshesRefetchedObjects);
NSArray<EOEnterpriseObject> details = ec.objectsWithFetchSpecification(fetchSpec);
result.addObjectsFromArray(details);
}
}
}
finally {
ec.rootObjectStore().unlock();
ec.unlock();
}
return result;
}
/**
* Fires all faults in the given global IDs on one batch together with their relationships.
* This is much more efficient than triggering the individual faults and relationships later on.
* The method should use only 1 fetch for all objects per entity
* and then one for each relationship per entity.
*
* @param <T> subclass of EOGlobalID
* @param ec editing context to fault into
* @param globalIDs the global IDs to fault
* @param prefetchingKeypaths list of key paths to prefetch or null
* @return faulted objects
*/
public static <T extends EOGlobalID> NSArray<EOEnterpriseObject> fireFaultsForGlobalIDs(EOEditingContext ec, Collection<T> globalIDs, Collection<String> prefetchingKeypaths) {
if (globalIDs == null || globalIDs.isEmpty()) {
return NSArray.emptyArray();
}
NSMutableArray<EOEnterpriseObject> result = new NSMutableArray<>(globalIDs.size());
NSMutableArray<T> faults = new NSMutableArray<>(globalIDs.size());
for (T gid : globalIDs) {
EOEnterpriseObject eo = ec.faultForGlobalID(gid, ec);
if (EOFaultHandler.isFault(eo)) {
faults.addObject(gid);
} else {
result.addObject(eo);
}
}
NSArray<EOEnterpriseObject> loadedObjects = fetchObjectsWithGlobalIDs(ec, faults);
result.addObjectsFromArray(loadedObjects);
if (prefetchingKeypaths != null && !prefetchingKeypaths.isEmpty()) {
NSDictionary<String, NSArray<EOEnterpriseObject>> objectsByEntity = ERXArrayUtilities.arrayGroupedByKeyPath(result, "entityName");
for (String entityName : objectsByEntity.keySet()) {
NSArray<EOEnterpriseObject> objects = objectsByEntity.objectForKey(entityName);
EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, entityName);
for (String keypath : prefetchingKeypaths) {
EORelationship relationship = entity.relationshipNamed(keypath);
EODatabaseContext dbc = ERXEOAccessUtilities.databaseContextForEntityNamed((EOObjectStoreCoordinator) ec.rootObjectStore(), entityName);
dbc.lock();
try {
dbc.batchFetchRelationship(relationship, objects, ec);
} finally {
dbc.unlock();
}
}
}
}
return result;
}
/**
* Fires all faults in the given global IDs on one batch. This is much more efficient than
* triggering the individual faults later on. The method should use only 1 fetch for all objects per entity.
*
* @param <T> subclass of EOGlobalID
* @param ec editing context to fault in
* @param globalIDs the global IDs to fault
* @return list of faulted objects
*/
public static <T extends EOGlobalID> NSArray<EOEnterpriseObject> fireFaultsForGlobalIDs(EOEditingContext ec, Collection<T> globalIDs) {
return fireFaultsForGlobalIDs(ec, globalIDs, NSArray.emptyArray());
}
/**
* Creates a global ID for the given entity name and its primary key value(s).
*
* @param entityName the entity name
* @param values one or more primary key values
* @return global ID object
*/
public static EOKeyGlobalID createGlobalID(String entityName, Object[] values) {
if (values != null && values.length == 1) {
Object primaryKey = values[0];
if (primaryKey instanceof Number) {
return new _EOIntegralKeyGlobalID(entityName, (Number) primaryKey);
}
}
return new _EOVectorKeyGlobalID(entityName, values);
}
}