/*
* Copyright (C) NetStruxr, Inc. All rights reserved.
*
* This software is published under the terms of the NetStruxr
* Public Software License version 0.5, a copy of which has been
* included with this distribution in the LICENSE.NPL file. */
package er.extensions.eof;
import java.util.Enumeration;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.log4j.Logger;
import com.webobjects.eoaccess.EOAccessArrayFaultHandler;
import com.webobjects.eoaccess.EOAdaptor;
import com.webobjects.eoaccess.EOAdaptorChannel;
import com.webobjects.eoaccess.EOAdaptorOperation;
import com.webobjects.eoaccess.EOAttribute;
import com.webobjects.eoaccess.EODatabaseChannel;
import com.webobjects.eoaccess.EODatabaseContext;
import com.webobjects.eoaccess.EODatabaseOperation;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EOEntityClassDescription;
import com.webobjects.eoaccess.EOGeneralAdaptorException;
import com.webobjects.eoaccess.EOModel;
import com.webobjects.eoaccess.EOObjectNotAvailableException;
import com.webobjects.eoaccess.EORelationship;
import com.webobjects.eoaccess.EOSQLExpression;
import com.webobjects.eoaccess.EOSQLExpressionFactory;
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.EOSharedEditingContext;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSMutableSet;
import com.webobjects.foundation.NSNotificationCenter;
import er.extensions.foundation.ERXArrayUtilities;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXThreadStorage;
import er.extensions.foundation.ERXUtilities;
import er.extensions.jdbc.ERXJDBCConnectionAnalyzer;
import er.extensions.jdbc.ERXSQLHelper;
import er.extensions.logging.ERXPatternLayout;
import er.extensions.statistics.ERXStats;
import er.extensions.statistics.ERXStats.Group;
/**
* This delegate implements several methods from the formal interface
* {@link com.webobjects.eoaccess.EODatabaseContext.Delegate EODatabaseContext.Delegate}.
* Of special note this class adds the ability
* for enterprise objects to generate their own primary keys, correctly throws an
* exception when a toOne relationship object is not found in the database and adds
* debugging abilities to tracking down when faults are fired. It also supports a cache for
* array fault that is checked before they are fetched from the database.
*
* @property er.extensions.ERXDatabaseContextDelegate.Exceptions.regex regular expression to
* check the exception test for if database connection should be reopened
*/
public class ERXDatabaseContextDelegate {
public static final String DatabaseContextFailedToFetchObject = "DatabaseContextFailedToFetchObject";
public static final String ERX_ADAPTOR_EXCEPTIONS_REGEX = "er.extensions.ERXDatabaseContextDelegate.Exceptions.regex";
public static final String ERX_ADAPTOR_EXCEPTIONS_REGEX_DEFAULT = ".*_obtainOpenChannel.*|.*Closed Connection.*|.*No more data to read from socket.*";
public static class ObjectNotAvailableException extends EOObjectNotAvailableException {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
private EOGlobalID globalID;
public ObjectNotAvailableException(String message) {
this(message, null);
}
public ObjectNotAvailableException(String message, EOGlobalID gid) {
super(message);
globalID = gid;
}
public EOGlobalID globalID() {
return globalID;
}
}
/** Basic logging support */
public final static Logger log = Logger.getLogger(ERXDatabaseContextDelegate.class);
/** Faulting logging support, logging category: <b>er.transaction.adaptor.FaultFiring</b> */
public final static Logger dbLog = Logger.getLogger("er.transaction.adaptor.FaultFiring");
/** Faulting logging support, logging category: <b>er.transaction.adaptor.Exceptions</b> */
public final static Logger exLog = Logger.getLogger("er.transaction.adaptor.Exceptions");
/** Faulting logging support, logging category: <b>er.transaction.adaptor.Batching</b> */
public final static Logger batchLog = Logger.getLogger("er.transaction.adaptor.Batching");
/** Holds onto the singleton of the default delegate */
private static ERXDatabaseContextDelegate _defaultDelegate = new ERXDatabaseContextDelegate();
private ERXArrayFaultCache _arrayFaultCache = null;
private ERXFetchResultCache _fetchResultCache = null;
/** Returns the singleton of the database context delegate */
public static ERXDatabaseContextDelegate defaultDelegate() {
return _defaultDelegate;
}
/**
* @param delegate - the singleton database context delegate to set
*/
public static void setDefaultDelegate(ERXDatabaseContextDelegate delegate) {
_defaultDelegate = delegate;
}
public ERXArrayFaultCache arrayFaultCache() {
return _arrayFaultCache;
}
public void setArrayFaultCache(ERXArrayFaultCache value) {
_arrayFaultCache = value;
}
public ERXFetchResultCache fetchResultCache() {
return _fetchResultCache;
}
public void setFetchResultCache(ERXFetchResultCache value) {
_fetchResultCache = value;
}
/**
* Returns an array of already fetched objects or null if they were not already fetched.
* @param dbc
* @param fs
* @param ec
*/
public NSArray databaseContextShouldFetchObjects(EODatabaseContext dbc, EOFetchSpecification fs, EOEditingContext ec) {
NSArray result = null;
ERXFetchResultCache fetchResultCache = fetchResultCache();
if (fetchResultCache != null) {
result = fetchResultCache.objectsForFetchSpecification(dbc, ec, fs);
}
return result;
}
/**
* Sets the cache entry for the fetched objects and refreshes the timestamps
* for fetched objects if batch faulting is enabled.
*
* @param dbc
* @param eos
* @param fs
* @param ec
*/
public void databaseContextDidFetchObjects(EODatabaseContext dbc, NSArray eos, EOFetchSpecification fs, EOEditingContext ec) {
ERXFetchResultCache fetchResultCache = fetchResultCache();
if (fetchResultCache != null) {
fetchResultCache.setObjectsForFetchSpecification(dbc, ec, eos, fs);
}
if(autoBatchFetchSize() > 0 && eos.count() > 0) {
//log.info("Freshen: " + fs.entityName() + " " + eos.count());
freshenFetchTimestamps(eos, ec.fetchTimestamp());
}
}
/**
* Provides for a hook to get at the original exceptions from the JDBC
* driver, as opposed to the cooked EOGeneralAdaptorException you get from
* EOF. To see the exceptions trace, set the logger
* er.transaction.adaptor.Exceptions to DEBUG.
*
* @param databaseContext the current database context
* @param throwable the original exception
* @return <code>true</code> if the exception has been handled already
*/
public boolean databaseContextShouldHandleDatabaseException(EODatabaseContext databaseContext, Throwable throwable) {
if(!reportingError.canEnter(databaseContext)) return true;
try {
if(exLog.isDebugEnabled()) {
exLog.debug("Database Exception occured: " + throwable, throwable);
} else if(exLog.isInfoEnabled()) {
exLog.info("Database Exception occured: " + throwable);
}
boolean handled = false;
try {
handled = ERXSQLHelper.newSQLHelper(databaseContext).handleDatabaseException(databaseContext, throwable);
} catch(RuntimeException e) {
databaseContext.rollbackChanges();
throw e;
}
String exceptionsRegex = ERXProperties.stringForKeyWithDefault(ERX_ADAPTOR_EXCEPTIONS_REGEX, ERX_ADAPTOR_EXCEPTIONS_REGEX_DEFAULT);
if(!handled && throwable.getMessage() != null && throwable.getMessage().matches(exceptionsRegex)) {
NSArray models = databaseContext.database().models();
for(Enumeration e = models.objectEnumerator(); e.hasMoreElements(); ) {
EOModel model = (EOModel)e.nextElement();
NSDictionary connectionDictionary = model.connectionDictionary();
if (connectionDictionary != null) {
NSMutableDictionary mutableConnectionDictionary = connectionDictionary.mutableClone();
mutableConnectionDictionary.setObjectForKey("<password deleted for log>", "password");
connectionDictionary = mutableConnectionDictionary;
}
log.info(model.name() + ": " + (connectionDictionary == null ? "No connection dictionary!" : connectionDictionary.toString()));
}
if ("JDBC".equals(databaseContext.adaptorContext().adaptor().name())) {
new ERXJDBCConnectionAnalyzer(databaseContext.database().adaptor().connectionDictionary());
}
}
//EOEditingContext ec = ERXEC.newEditingContext();
//log.info(NSPropertyListSerialization.stringFromPropertyList(EOUtilities.modelGroup(ec).models().valueForKey("connectionDictionary")));
return !handled;
} finally {
reportingError.leave(databaseContext);
}
}
/**
* Provides the ability for new enterprise objects that implement the interface {@link ERXGeneratesPrimaryKeyInterface}
* to provide their own primary key dictionary. If the enterprise object implements the above interface then the
* method <code>primaryKeyDictionary(true)</code> will be called on the object. If the object returns null then a
* primary key will be generated for the object in the usual fashion.
* @param databaseContext databasecontext
* @param object the new enterprise object
* @param entity the entity of the object
* @return primary key dictionary to be used or null if a primary key should be generated for the object.
*/
public NSDictionary<String, Object> databaseContextNewPrimaryKey(EODatabaseContext databaseContext, Object object, EOEntity entity) {
return object instanceof ERXGeneratesPrimaryKeyInterface ? ((ERXGeneratesPrimaryKeyInterface)object).rawPrimaryKeyDictionary(true) : null;
}
/**
* Allows custom handling of dropped connection exceptions. This was needed in WebObjects 4.5 because the
* OracleEOAdaptor wouldn't correctly handle all exceptions of dropped connections. This may not be needed
* now.
* @param dbc current database context
* @param e throw exception
* @return if the exception is one of the bad ones that isn't handled then the method <code>handleDroppedConnection</code>
* is called directly on the database object of the context and <code>false</code> is returned otherwise <code>true</code>.
*/
// CHECKME: Is this still needed now?
public boolean databaseContextShouldHandleDatabaseException(EODatabaseContext dbc, Exception e) throws Throwable {
if(!reportingError.canEnter(dbc)) return true;
try {
EOAdaptor adaptor=dbc.adaptorContext().adaptor();
boolean shouldHandleConnection = false;
if(e instanceof EOGeneralAdaptorException)
log.error(((EOGeneralAdaptorException)e).userInfo());
else
log.error(e);
if (adaptor.isDroppedConnectionException(e))
shouldHandleConnection = true;
// FIXME: Should provide api to extend the list of bad exceptions.
else if (e.toString().indexOf("ORA-01041")!=-1) {
// just returning true here does not seem to do the trick. why !?!?
log.error("ORA-01041 detecting -- forcing reconnect");
dbc.database().handleDroppedConnection();
shouldHandleConnection = false;
} else {
if(e instanceof EOGeneralAdaptorException)
log.info(((EOGeneralAdaptorException)e).userInfo());
throw e;
}
return shouldHandleConnection;
} finally {
reportingError.leave(dbc);
}
}
/**
* This is Kelly Hawks' fix for the missing to one relationship.
* Delegate on EODatabaseContext that gets called when a to-one fault cannot find its data in
* the database. The object that is returned is a cleared fault.
* We raise here to restore the functionality that existed prior to WebObjects 4.5.
* Whenever a fault fails for a globalID (i.e. the object is NOT found in the database), we raise
* an {@link com.webobjects.eoaccess.EOObjectNotAvailableException EOObjectNotAvailableException}. <br>
* If you have entities you don't really care about, you can set the system property
* <code>er.extensions.ERXDatabaseContextDelegate.tolerantEntityPattern</code> to a regular expression
* that will be tested against the GID entity name. If it matches, then only an error will be logged
* but no exception will be thrown.
*
* @param context database context
* @param object object that is firing the fault for a given to-one relationship
* @param gid global id that wasn't found in the database.
*/
public boolean databaseContextFailedToFetchObject(EODatabaseContext context, Object object, EOGlobalID gid) {
String tolerantEntityPattern = ERXProperties.stringForKey("er.extensions.ERXDatabaseContextDelegate.tolerantEntityPattern");
boolean raiseException = true;
if(tolerantEntityPattern != null && (gid instanceof EOKeyGlobalID)) {
if(((EOKeyGlobalID)gid).entityName().matches(tolerantEntityPattern)) {
raiseException = false;
}
}
if (object!=null) {
EOEditingContext ec = ((EOEnterpriseObject)object).editingContext();
// we need to refault the object before raising, otherwise, if the caller traps
// the exception, it will be a successful lookup the next time a fault with the
// same global id fires. NOTE: refaulting in a sharedEditingContext is illegal,
// so we specifically check for that special case.
if (!(ec instanceof EOSharedEditingContext) && raiseException) {
context.refaultObject((EOEnterpriseObject)object, gid, ec);
}
}
String gidString;
if(gid instanceof EOKeyGlobalID) {
// ak: when you use 24 byte PKs, the output is unreadable otherwise
EOKeyGlobalID kgid = (EOKeyGlobalID)gid;
gidString = "<" + kgid.entityName() + ": [" ;
EOEntity entity = ERXEOAccessUtilities.entityNamed(null, kgid.entityName());
NSArray pks = entity.primaryKeyAttributes();
NSArray values = kgid.keyValuesArray();
EOSQLExpressionFactory expressionFactory = context.database().adaptor().expressionFactory();
EOSQLExpression expression = null;
if (expressionFactory != null) {
expression = expressionFactory.expressionForEntity(entity);
}
for(int i = 0; i < pks.count(); i++) {
Object value = values.objectAtIndex(i);
EOAttribute attribute = (EOAttribute) pks.objectAtIndex(i);
// ak: only Postgres seems to return reasonable values here...
String stringValue = "" + value;
if (expression != null) {
stringValue = expression.formatValueForAttribute(value, attribute);
}
if("NULL".equals(stringValue)) {
stringValue = "" + value;
}
gidString += attribute.name() + ": \'" + stringValue + "\'"
+ (i == pks.count() - 1 ? "" : ", ");
}
gidString += "] >";
} else {
gidString = gid.toString();
}
NSNotificationCenter.defaultCenter().postNotification(DatabaseContextFailedToFetchObject, object);
if(raiseException) {
throw new ObjectNotAvailableException("No " + (object!=null ? object.getClass().getName() : "N/A") + " found with globalID: " + gidString, gid);
} else if (ERXProperties.booleanForKeyWithDefault("er.extensions.ERXDatabaseContextDelegate.logTolerantEntityNotAvailable", true)) {
log.error("No " + (object!=null ? object.getClass().getName() : "N/A") + " found with globalID: " + gidString + "\n" + ERXUtilities.stackTrace());
}
return false;
}
/**
* This delegate method is called every time a fault is fired that needs
* to go to the database. All we have added is logging statement of the
* debug priority. This way during runtime a developer can toggle the
* logger priority settting on and off to see what faults are firing. Also
* note that when using {@link ERXPatternLayout} one can set the option to
* see full backtraces to the calling method. With this option specified
* a developer can see exactly which methods are firing faults.
* @param dc the databasecontext
* @param fs the fetchspecification
* @param channel the databasechannel
*/
public void databaseContextDidSelectObjects(EODatabaseContext dc,
EOFetchSpecification fs,
EODatabaseChannel channel) {
if (dbLog.isDebugEnabled()) {
dbLog.debug("databaseContextDidSelectObjects " + fs, new Exception());
}
}
/**
* This delegate method first checks the arrayFaultCache if it is set before
* trying to resolve the fault from the DB. It can be a severe performance
* optimization depending on your setup. Also, it support batch fetching of
* to-many relationships of EOs that were fetched at the "same" time.
*
* @param dbc
* @param obj
*/
public boolean databaseContextShouldFetchArrayFault(EODatabaseContext dbc, Object obj) {
if(_arrayFaultCache != null) {
_arrayFaultCache.clearFault(obj);
if(!EOFaultHandler.isFault(obj)) {
return false;
}
}
if(autoBatchFetchSize() > 0) {
return batchFetchToManyFault(dbc, obj);
}
return true;
}
/**
* Batch fetches to one relationships if enabled.
* @param dbc
* @param obj
* @return true if the fault should get fetched
*/
public boolean databaseContextShouldFetchObjectFault(EODatabaseContext dbc, Object obj) {
if(autoBatchFetchSize() > 0 && obj instanceof AutoBatchFaultingEnterpriseObject) {
return batchFetchToOneFault(dbc, (AutoBatchFaultingEnterpriseObject)obj);
}
return true;
}
/**
* Overridden to remove inserts and deletes of the "same" row. When you
* delete from a join table and then re-add the same object, then the
* order of operations would be insert, then delete and you will get
* an error because the delete would try to also delete the newly inserted
* row. Here we just check every insert and see if the deleted contains the same
* object. If they do, we just skip both operations,
* @author chello team!
* @param dbCtxt
* @param adaptorOps
* @param adChannel
*/
public NSArray databaseContextWillPerformAdaptorOperations(EODatabaseContext dbCtxt,
NSArray adaptorOps, EOAdaptorChannel adChannel) {
NSMutableArray result = new NSMutableArray();
NSDictionary groupedOps = ERXArrayUtilities.arrayGroupedByKeyPath(adaptorOps, "adaptorOperator");
Integer insertKey = ERXConstant.integerForInt(EODatabaseOperation.AdaptorInsertOperator);
NSArray insertOps = (NSArray) groupedOps.objectForKey(insertKey);
Integer deleteKey = ERXConstant.integerForInt(EODatabaseOperation.AdaptorDeleteOperator);
NSArray deleteOps = (NSArray) groupedOps.objectForKey(deleteKey);
if (insertOps!=null && deleteOps!=null) {
NSMutableSet skippedOps = new NSMutableSet();
for(Enumeration e = insertOps.objectEnumerator(); e.hasMoreElements();) {
EOAdaptorOperation insertOp = (EOAdaptorOperation)e.nextElement();
for(Enumeration e1 = deleteOps.objectEnumerator(); e1.hasMoreElements();) {
EOAdaptorOperation deleteOp = (EOAdaptorOperation)e1.nextElement();
if(!skippedOps.containsObject(deleteOp)) {
if(insertOp.entity() == deleteOp.entity()) {
if(deleteOp.qualifier().evaluateWithObject(insertOp.changedValues())) {
if(false) {
// here we remove both the delete and the
// insert. this might fail if we didn't lock on all rows
// FIXME: check the current snapshot in the database and
// see if it is the same as the new insert
skippedOps.addObject(deleteOp);
skippedOps.addObject(insertOp);
} else {
// here we put the delete up front, this might fail if
// we have cascading delete rules in the database
result.addObject(deleteOp);
skippedOps.addObject(deleteOp);
}
log.warn("Skipped: " + insertOp + "\n" + deleteOp);
}
}
}
}
}
for(Enumeration e = adaptorOps.objectEnumerator(); e.hasMoreElements();) {
EOAdaptorOperation op = (EOAdaptorOperation)e.nextElement();
if(!skippedOps.containsObject(op)) {
result.addObject(op);
}
}
} else {
result.addObjectsFromArray(adaptorOps);
}
return result;
}
/**
* The delegate is not reentrant, so this marks whether we are already reporting an error (most probably due to a logging pattern)
*/
private ReentranceProtector reportingError = new ReentranceProtector();
/**
* The delegate is not reentrant, so this marks whether we are already batch faulting a to-many relationship.
*/
private ReentranceProtector fetchingToMany = new ReentranceProtector();
/**
* The delegate is not reentrant, so this marks whether we are already batch faulting a to-one relationship.
*/
private ReentranceProtector fetchingToOne = new ReentranceProtector();
/**
* Holds the auto batch fetch size.
*/
public static int autoBatchFetchSize = -1;
/**
* Batching thread key
*/
public static final String THREAD_KEY = "ERXBatching";
/**
* Interface to provide auto-magic batch fetching. For an implementation (and the hack needed for to-one handling), see {@link ERXGenericRecord}.
*/
public interface AutoBatchFaultingEnterpriseObject extends EOEnterpriseObject {
/**
* Last time fetched.
* @return millis of last fetch.
*/
public long batchFaultingTimeStamp();
/**
* Updates the last time this object was fetched.
* @param fetchTimestamp
*/
public void setBatchFaultingTimestamp(long fetchTimestamp);
/**
* Marks the object as touched from the specified source with the specified key.
* @param toucher
* @param relationship name of relationship
*/
public void touchFromBatchFaultingSource(AutoBatchFaultingEnterpriseObject toucher, String relationship);
/**
* The GID of the object that last touched us, or null.
* @return gid of touching object.
*/
public EOGlobalID batchFaultingSourceGlobalID();
/**
* The key through which we were last accessed.
* @return relationship name or null
*/
public String batchFaultingRelationshipName();
}
private class ReentranceProtector {
public ReentranceProtector() {}
private CopyOnWriteArrayList<EODatabaseContext> _accessing = new CopyOnWriteArrayList<EODatabaseContext>();
public boolean canEnter(EODatabaseContext dbc) {
if(_accessing.contains(dbc)) {
return false;
}
_accessing.add(dbc);
return true;
}
public void leave(EODatabaseContext dbc) {
_accessing.remove(dbc);
}
}
public interface BatchHandler {
/**
* Override this to skip fetching for the given ec and relationship. The
* default is to skip abstract destination entities of to-many
* relationships. You might want to exclude very large destinations or
* sources and so on.
*
* @param ec
* @param rel
* @return int batch size (0 for not batch fetch)
*/
public int batchSizeForRelationship(EOEditingContext ec, EORelationship rel);
}
/**
* Refreshes the fetch timestamp for the fetched objects.
* @param eos
* @param timestamp
*/
private void freshenFetchTimestamps(NSArray eos, long timestamp) {
for(Object eo: eos) {
if (eo instanceof AutoBatchFaultingEnterpriseObject) {
AutoBatchFaultingEnterpriseObject ft = (AutoBatchFaultingEnterpriseObject) eo;
ft.setBatchFaultingTimestamp(timestamp);
}
}
}
private void markStart(String type, EOEnterpriseObject eo, String key) {
if(ERXStats.isTrackingStatistics()) {
ERXStats.markStart(Group.Batching, type + "." + eo.entityName()+"."+key);
}
}
private void markEnd(String type, EOEnterpriseObject eo, String key) {
if(ERXStats.isTrackingStatistics()) {
ERXStats.markEnd(Group.Batching, type + "." + eo.entityName()+"."+key);
}
}
/**
* Returns the batch size for automatic batch faulting from the System property <code>er.extensions.ERXDatabaseContextDelegate.autoBatchFetchSize</code>.
* Default is 0, meaning it's disabled.
* @return batch size
*/
public static int autoBatchFetchSize() {
if(autoBatchFetchSize == -1) {
autoBatchFetchSize = ERXProperties.intForKeyWithDefault("er.extensions.ERXDatabaseContextDelegate.autoBatchFetchSize", 0);
}
//if(true) return 50;
return autoBatchFetchSize;
}
/**
* Fetches the to-many fault and the faults of all other objects in the EC
* that have the same relationship, the fetch timestamp and the same class
* description.<br>
*
* @param dbc
* database context
* @param obj
* to-many fault
* @return true if it's still a fault.
*/
private boolean batchFetchToManyFault(EODatabaseContext dbc, Object obj) {
if (fetchingToMany.canEnter(dbc)) {
try {
EOAccessArrayFaultHandler handler = (EOAccessArrayFaultHandler) EOFaultHandler.handlerForFault(obj);
EOEditingContext ec = handler.editingContext();
EOEnterpriseObject source = ec.faultForGlobalID(handler.sourceGlobalID(), ec);
if (source instanceof AutoBatchFaultingEnterpriseObject) {
String key = handler.relationshipName();
EOEntityClassDescription cd = (EOEntityClassDescription) source.classDescription();
EORelationship relationship = cd.entity().relationshipNamed(key);
if (_handler.batchSizeForRelationship(ec, relationship) > 0) {
markStart("ToMany.Calculation", source, key);
NSArray<EOEnterpriseObject> candidates = null;
NSArray currentObjects = (NSArray) ERXThreadStorage.valueForKey(THREAD_KEY);
boolean fromThreadStorage = false;
if (currentObjects != null) {
NSMutableArray<EOEnterpriseObject> tmpList = new NSMutableArray<>();
for (Object tmpItem : currentObjects) {
if (tmpItem instanceof AutoBatchFaultingEnterpriseObject) {
tmpList.add((EOEnterpriseObject) tmpItem);
}
}
if (tmpList.count() > 0) {
candidates = tmpList;
fromThreadStorage = true;
}
}
if (candidates == null) {
candidates = ec.registeredObjects();
}
long timestamp = ((AutoBatchFaultingEnterpriseObject) source).batchFaultingTimeStamp();
NSMutableArray<EOEnterpriseObject> eos = new NSMutableArray<>();
NSMutableArray faults = new NSMutableArray();
for (EOEnterpriseObject current : candidates) {
if (current instanceof AutoBatchFaultingEnterpriseObject) {
AutoBatchFaultingEnterpriseObject currentEO = (AutoBatchFaultingEnterpriseObject) current;
if (currentEO.batchFaultingTimeStamp() == timestamp || fromThreadStorage) {
if (!EOFaultHandler.isFault(currentEO) && currentEO.classDescription() == source.classDescription()) {
Object fault = currentEO.storedValueForKey(key);
if (EOFaultHandler.isFault(fault)) {
faults.addObject(fault);
eos.addObject(currentEO);
if (eos.count() == autoBatchFetchSize()) {
break;
}
}
}
}
}
}
markEnd("ToMany.Calculation", source, key);
if (eos.count() > 1) {
markStart("ToMany.Fetching", source, key);
doFetch(dbc, ec, relationship, eos);
int cnt = 0;
for(Object fault: faults) {
if(!EOFaultHandler.isFault(fault)) {
NSArray array = (NSArray)fault;
freshenFetchTimestamps(array, timestamp);
cnt += array.count();
}
}
markEnd("ToMany.Fetching", source, key);
if(batchLog.isDebugEnabled()) {
batchLog.debug("Fetched " + cnt + " to-many " + relationship.destinationEntity().name() + " from " + eos.count() + " " + source.entityName() + " for " + key);
}
return EOFaultHandler.isFault(obj);
}
}
}
}
finally {
fetchingToMany.leave(dbc);
}
}
return true;
}
/**
* Fetches the to-one fault and the faults of all other objects in the EC
* that have the same relationship, the fetch timestamp and the same class
* description.<br>
*
* @param dbc
* database context
* @param eo
* to-one fault
* @return true if it's still a fault.
*/
private synchronized boolean batchFetchToOneFault(EODatabaseContext dbc, AutoBatchFaultingEnterpriseObject eo) {
if(fetchingToOne.canEnter(dbc)) {
try {
EOGlobalID sourceGID = eo.batchFaultingSourceGlobalID();
String key = eo.batchFaultingRelationshipName();
if(sourceGID != null && key != null) {
EOEditingContext ec = eo.editingContext();
AutoBatchFaultingEnterpriseObject source = (AutoBatchFaultingEnterpriseObject) ec.faultForGlobalID(sourceGID, ec);
EOEntityClassDescription cd = (EOEntityClassDescription)source.classDescription();
EORelationship relationship = cd.entity().relationshipNamed(key);
if(_handler.batchSizeForRelationship(ec, relationship) > 0 && !relationship.isToMany()) {
markStart("ToOne.Calculation", source, key);
long timestamp = source.batchFaultingTimeStamp();
boolean fromThreadStorage = false;
NSMutableArray<EOEnterpriseObject> eos = new NSMutableArray<>();
NSMutableSet faults = new NSMutableSet();
NSArray<EOEnterpriseObject> candidates = null;
NSArray currentObjects = (NSArray) ERXThreadStorage.valueForKey(THREAD_KEY);
if (currentObjects != null) {
NSMutableArray<EOEnterpriseObject> tmpList = new NSMutableArray<>();
for (Object tmpItem : currentObjects) {
if (tmpItem instanceof AutoBatchFaultingEnterpriseObject) {
tmpList.add((EOEnterpriseObject) tmpItem);
}
}
if (tmpList.count() > 0) {
candidates = tmpList;
fromThreadStorage = true;
}
}
if (candidates == null) {
candidates = ec.registeredObjects();
}
for (EOEnterpriseObject current : candidates) {
if (current instanceof AutoBatchFaultingEnterpriseObject) {
AutoBatchFaultingEnterpriseObject currentEO = (AutoBatchFaultingEnterpriseObject) current;
if(currentEO.batchFaultingTimeStamp() == timestamp || fromThreadStorage) {
if(source.classDescription() == currentEO.classDescription()) {
if(!EOFaultHandler.isFault(currentEO)) {
Object fault = currentEO.storedValueForKey(key);
if(EOFaultHandler.isFault(fault)) {
faults.addObject(fault);
eos.addObject(currentEO);
if(eos.count() == autoBatchFetchSize()) {
break;
}
}
}
}
}
}
}
markEnd("ToOne.Calculation", source, key);
if(eos.count() > 1) {
markStart("ToOne.Fetching", source, key);
doFetch(dbc, ec, relationship, eos);
freshenFetchTimestamps(faults.allObjects(), timestamp);
markEnd("ToOne.Fetching", source, key);
if(batchLog.isDebugEnabled()) {
batchLog.debug("Fetched " + faults.count() + " to-one " + relationship.destinationEntity().name() + " from " + eos.count() + " " + source.entityName() + " for " + key);
}
return EOFaultHandler.isFault(eo);
}
}
}
} finally {
fetchingToOne.leave(dbc);
}
}
return true;
}
private void doFetch(EODatabaseContext dbc, EOEditingContext ec, EORelationship relationship, NSArray eos) {
// dbc.batchFetchRelationship(relationship, eos, ec);
ERXEOAccessUtilities.batchFetchRelationship(dbc, relationship, eos, ec, true);
//ERXBatchFetchUtilities.batchFetch(eos, relationship.name());
}
private BatchHandler DEFAULT = new BatchHandler() {
public int batchSizeForRelationship(EOEditingContext ec, EORelationship relationship) {
return autoBatchFetchSize();
}
};
private BatchHandler _handler = DEFAULT;
/**
* Sets the batch handler.
* @param handler
*/
public void setBatchHandler(BatchHandler handler) {
if(handler == null) {
handler = DEFAULT;
}
_handler = handler;
}
public static void setCurrentBatchObjects(NSArray arr) {
if(ERXDatabaseContextDelegate.autoBatchFetchSize() > 0) {
if(arr == null || arr.lastObject() instanceof EOEnterpriseObject) {
ERXThreadStorage.takeValueForKey(arr, ERXDatabaseContextDelegate.THREAD_KEY);
}
}
}
}