//
// ERXEC.java
// ERExtensions
//
// Created by Max Muller on Sun Feb 23 2003.
//
package er.extensions.eof;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.WeakHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.misc.Signal;
import sun.misc.SignalHandler;
import com.webobjects.eoaccess.EOGeneralAdaptorException;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.eocontrol.EOFetchSpecification;
import com.webobjects.eocontrol.EOGlobalID;
import com.webobjects.eocontrol.EOObjectStore;
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.NSNotification;
import com.webobjects.foundation.NSNotificationCenter;
import com.webobjects.foundation.NSSelector;
import com.webobjects.foundation._NSDelegate;
import er.extensions.appserver.ERXApplication;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXSelectorUtilities;
import er.extensions.foundation.ERXSignalHandler;
import er.extensions.foundation.ERXUtilities;
import er.extensions.foundation.ERXValueUtilities;
/**
* Subclass of {@link com.webobjects.eocontrol.EOEditingContext} that has every
* public method overridden to support automatic lock/unlock handling for you.
* This is very useful, as it is potentially very dangerous to rely on EOFs automatic
* lock handling - it will invariably lead into deadlocks. As you will need to use
* this class and its subclasses exclusively as your ECs, it also contains a factory
* instances. The Factory also sets a default delegate for you and is used
* everywhere in ERExtensions and ERDirectToWeb. The Factory is actually an
* interface and you would create a new EC by using:
* <code>ERXEC.newEditingContext()</code> You can also install your own
* Factory classes. It is recommended to subclass ERXEC.DefaultFactory and
* override <code>_createEditingContext()</code> or use the
* er.extensions.ERXEC.editingContextClassName property to specify the name of the
* editing context class to be instantiated by ERXEC.DefaultFactory's
* _createEditingContext() core method.
*
* @property er.extensions.ERXEC.useSharedEditingContext
* @property er.extensions.ERXEC.markOpenLocks
* @property er.extensions.ERXEC.traceOpenLocks
* @property er.extensions.ERXEC.useUnlocker
* @property er.extensions.ERXEC.denyMerges
* @property er.extensions.ERXEC.defaultAutomaticLockUnlock
* @property er.extensions.ERXEC.defaultCoalesceAutoLocks
* @property er.extensions.ERXEC.safeLocking
* @property er.extensions.ERXEC.editingContextClassName
*/
public class ERXEC extends EOEditingContext {
/**
* 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 static final Logger log = LoggerFactory.getLogger(ERXEC.class);
/**
* Logs a message when set to DEBUG and an EC is locked/unlocked. Logs a message with
* trace if {@link #lockTrace} is also set to DEBUG.
*/
public static final Logger lockLogger = LoggerFactory.getLogger("er.extensions.ERXEC.LockLogger");
/**
* Logs a message with trace when set to DEBUG if autoLocking is enabled and an EC is
* used without a lock. Adds traces to messages by {@link #lockLogger} if it is also set to DEBUG.
*/
public static final Logger lockTrace = LoggerFactory.getLogger("er.extensions.ERXEC.LockTrace");
/** Name of the notification that is posted after editing context is created. */
public static final String EditingContextDidCreateNotification = "EOEditingContextDidCreate";
/**
* Name of the notification that is posted before an editing context is
* saved.
*/
public static final String EditingContextWillSaveChangesNotification = "EOEditingContextWillSaveChanges";
/**
* Name of the notification that is posted when an editing context is
* reverted.
*/
public static final String EditingContextDidRevertChanges = "EOEditingContextDidRevertChanges";
/**
* Name of the notification that is posted when an editing context has
* failed to save changes.
*/
public static final String EditingContextFailedToSaveChanges = "EOEditingContextFailedToSaveChanges";
/**
* if traceOpenEditingContextLocks is true, this contains the stack trace
* from this EC's call to lock
*/
private Exception creationTrace;
NSMutableDictionary<Thread, NSMutableArray<Exception>> openLockTraces = new NSMutableDictionary<Thread, NSMutableArray<Exception>>();
/**
* if traceOpenEditingContextLocks is true, this will contain
* the locking thread
*/
transient Thread lockingThread;
/**
* And, as the name might change, also the name of the locking thread (might contain session ID or other info)
*/
String lockingThreadName;
/** decides whether to lock/unlock automatically when used without a lock. */
private Boolean useAutolock;
/**
* if true, then autolocks are left open inside of a request to be cleaned
* up at the end
*/
private Boolean coalesceAutoLocks;
/** if > 0, there is an autolock on this editingContext */
private int autoLocked;
/**
* holds a flag if the EC is in finalize(). This is needed because we can't
* autolock then.
*/
private boolean isFinalizing;
/**
* holds a flag if locked ECs should be unlocked after the request-response
* loop.
*/
private static volatile Boolean useUnlocker;
/** holds a flag if editing context locks should be traced */
private static volatile Boolean traceOpenLocks;
/** holds a flag if editing context locks should be marked */
private static volatile Boolean markOpenLocks;
// /** key for the thread storage used by the unlocker. */
// private static final String LockedContextsForCurrentThreadKey = "ERXEC.lockedContextsForCurrentThread";
private static final NSSelector EditingContextWillRevertObjectsDelegateSelector = new NSSelector("editingContextWillRevertObjects", new Class[] { EOEditingContext.class, NSArray.class, NSArray.class, NSArray.class });
private static final NSSelector EditingContextDidRevertObjectsDelegateSelector = new NSSelector("editingContextDidRevertObjects", new Class[] { EOEditingContext.class, NSArray.class, NSArray.class, NSArray.class });
private static final NSSelector EditingContextDidFailSaveChangesDelegateSelector = new NSSelector("editingContextDidFailSaveChanges", new Class[] { EOEditingContext.class, EOGeneralAdaptorException.class });
private static final String ERXECProcessQueuedNotificationsNotification = "processQueuedNotifications";
public static final NSSelector ERXECProcessQueuedNotificationsSelector = ERXSelectorUtilities.notificationSelector("processQueuedNotificationsNotification");
/**
* @return the value of the <code>er.extensions.ERXEC.editingContextClassName</code> property, which
* is the name of the editing context class instantiated by ERXEC.DefaultFactory's _createEditingContext()
* core method. It defaults to the name of the ERXEC class.
*
*/
public static String editingContextClassName() {
return ERXProperties.stringForKeyWithDefault("er.extensions.ERXEC.editingContextClassName", ERXEC.class.getName());
}
/**
* Returns the value of the <code>er.extensions.ERXEC.safeLocking</code> property, which is the
* new catch-all setting that turns on all of the recommended locking settings.
* @return the value of the <code>er.extensions.ERXEC.safeLocking</code> property
*/
public static boolean safeLocking() {
return ERXProperties.booleanForKeyWithDefault("er.extensions.ERXEC.safeLocking", false);
}
/**
* Returns the value of the <code>er.extensions.ERXEC.defaultAutomaticLockUnlock</code> property, which
* turns on auto-locking by default. Also returns true if <code>safeLocking</code> is true.
* @return the value of the <code>er.extensions.ERXEC.defaultAutomaticLockUnlock</code> property
*/
public static boolean defaultAutomaticLockUnlock() {
return ERXProperties.booleanForKey("er.extensions.ERXEC.defaultAutomaticLockUnlock") || ERXEC.safeLocking();
}
/**
* Returns the value of the <code>er.extensions.ERXEC.defaultCoalesceAutoLocks</code> property, which
* turns on coalescing auto-locks, meaning that the EC gets locked once and unlocked at the end of the RR loop.
* Also returns true if <code>safeLocking</code> is true.
* @return the value of the <code>er.extensions.ERXEC.defaultCoalesceAutoLocks</code> property
*/
public static boolean defaultCoalesceAutoLocks() {
return ERXProperties.booleanForKey("er.extensions.ERXEC.defaultCoalesceAutoLocks") || ERXEC.safeLocking();
}
/**
* Returns the value of the <code>er.extensions.ERXEC.useUnlocker</code> property, which
* turns on unlocking at the end of the RR loop.
* Also returns true if <code>safeLocking</code> is true.
* @return the value of the <code>er.extensions.ERXEC.useUnlocker</code> property
*/
public static boolean useUnlocker() {
if (useUnlocker == null) {
synchronized (ERXEC.class) {
if(useUnlocker == null) {
useUnlocker = Boolean.valueOf(ERXProperties.booleanForKey("er.extensions.ERXEC.useUnlocker") || ERXEC.safeLocking());
log.debug("setting useUnlocker to {}", useUnlocker);
}
}
}
return useUnlocker.booleanValue();
}
public static void setUseUnlocker(boolean value) {
useUnlocker = Boolean.valueOf(value);
}
/**
* Returns the value of the <code>er.extensions.ERXEC.traceOpenLocks</code>
* property, which turns on tracing of locks. You can see the trace either by
* <code>kill -HUP</code>, by the
* <code>ERXDirectAction/showOpenEditingContextLockTraces</code> action or
* by setting your App's statistic store to <code>ERXStatisticStore</code>.
* @return the value of the <code>er.extensions.ERXEC.traceOpenLocks</code> property
*/
public static boolean traceOpenLocks() {
if (traceOpenLocks == null) {
synchronized (ERXEC.class) {
if(traceOpenLocks == null) {
traceOpenLocks = Boolean.valueOf(ERXProperties.booleanForKeyWithDefault("er.extensions.ERXEC.traceOpenLocks", false));
log.debug("setting traceOpenLocks to {}", traceOpenLocks);
}
}
}
return traceOpenLocks.booleanValue();
}
/**
* Returns the value of the <code>er.extensions.ERXEC.markOpenLocks</code>
* property, which turns on marking of locks. You can see the threads that hold the lock either by
* <code>kill -HUP</code>, by the
* <code>ERXDirectAction/showOpenEditingContextLockTraces</code> action or
* by setting your App's statistic store to <code>ERXStatisticStore</code>.
* Also returns true if <code>traceOpenLocks</code> is true.
* @return the value of the <code>er.extensions.ERXEC.markOpenLocks</code> property
*/
public static boolean markOpenLocks() {
if (markOpenLocks == null) {
synchronized (ERXEC.class) {
if(markOpenLocks == null) {
markOpenLocks = Boolean.valueOf(ERXProperties.booleanForKeyWithDefault("er.extensions.ERXEC.markOpenLocks", false));
log.debug("setting markOpenLocks to {}", markOpenLocks);
}
}
}
return markOpenLocks.booleanValue() || traceOpenLocks();
}
/**
* Sets whether or not open editing context lock tracing is enabled.
* @param value if open locks should be traced
*/
public static void setTraceOpenLocks(boolean value) {
traceOpenLocks = Boolean.valueOf(value);
}
/**
* Sets whether or not open editing context lock marking is enabled.
* @param value if open locks should be marked
*/
public static void setMarkOpenLocks(boolean value) {
markOpenLocks = Boolean.valueOf(value);
}
private static ThreadLocal<List> locks = new ThreadLocal() {
@Override
protected Object initialValue() {
return new Vector();
}
};
/**
* Pushes the given EC to the array of locked ECs in the current thread. The
* ECs left over after the RR-loop will be automagically unlocked.
*
* @param ec
* locked EOEditingContext
*/
public static void pushLockedContextForCurrentThread(EOEditingContext ec) {
if (useUnlocker() && ec != null) {
List ecs = locks.get();
ecs.add(ec);
log.debug("After pushing: {}", ecs);
}
}
/**
* Pops the given EC from the array of contexts to unlock. The ECs left over
* after the RR-loop will be automagically unlocked.
*
* @param ec
* unlocked EOEditingContext
*/
public static void popLockedContextForCurrentThread(EOEditingContext ec) {
if (useUnlocker() && ec != null) {
List ecs = locks.get();
if (ecs != null) {
int index = ecs.lastIndexOf(ec);
if (index >= 0) {
ecs.remove(index);
}
else {
log.error("Should pop, but ec not found in Vector! {}, ec: {}, ecs: {}", Thread.currentThread().getName(), ec, ecs);
}
}
log.debug("After popping: {}", ecs);
}
}
/**
* Unlocks all remaining locked contexts in the current thread. You
* shouldn't call this yourself, but let the Unlocker handle it for you.
*/
public static void unlockAllContextsForCurrentThread() {
List ecs = locks.get();
if (useUnlocker() && ecs != null && ecs.size() > 0) {
log.debug("Unlock remaining: {}", ecs);
// we can't use an iterator, because calling unlock() will remove
// the EC from end of the vector
for (int i = ecs.size() - 1; i >= 0; i--) {
EOEditingContext ec = (EOEditingContext) ecs.get(i);
boolean openAutoLocks = (ec instanceof ERXEC && ((ERXEC) ec).isAutoLocked());
if (openAutoLocks) {
log.debug("Unlocking autolocked editing context: {}", ec);
((ERXEC) ec).autoLocked--;
}
else {
log.warn("Unlocking context that wasn't unlocked in RR-Loop: {}", ec);
}
try {
ec.unlock();
}
catch (IllegalStateException ex) {
log.error("Could not unlock EC: {}", ec, ex);
}
}
}
}
/**
* Extensions for the EOEditingContext.Delegate interface.
*/
public static interface Delegate extends EOEditingContext.Delegate {
/**
* If the delegate implements this method, this method is invoked if a
* <code>EOGeneralAdaptorException</code> is thrown.
*
* @param ec
* the editing context that tried to save changes.
* @param exception
* the exception thrown during the operation
*/
public void editingContextDidFailSaveChanges(EOEditingContext ec, EOGeneralAdaptorException exception);
/**
* If the delegate implements this method, this method is invoked before
* a revert of an editing context. We pass the objects that are marked
* as inserted, updated and deleted.
*
* @param ec
* the editing context that just reverted.
* @param insertedObjects
* objects that were marked as inserted in the editing
* context before the revert took place.
* @param updatedObjects
* objects that were marked as updated in the editing context
* before the revert took place.
* @param deletedObjects
* objects that were marked as deleted in the editing context
* before the revert took place.
*/
public void editingContextWillRevertObjects(EOEditingContext ec, NSArray insertedObjects, NSArray updatedObjects, NSArray deletedObjects);
/**
* If the delegate implements this method, this method is invoked
* following a revert of an editing context. We pass the objects that
* were marked as inserted, updated and deleted before the revert took
* place.
*
* @param ec
* the editing context that just reverted.
* @param insertedObjects
* objects that were marked as inserted in the editing
* context before the revert took place.
* @param updatedObjects
* objects that were marked as updated in the editing context
* before the revert took place.
* @param deletedObjects
* objects that were marked as deleted in the editing context
* before the revert took place.
*/
public void editingContextDidRevertObjects(EOEditingContext ec, NSArray insertedObjects, NSArray updatedObjects, NSArray deletedObjects);
}
public static interface Factory {
public Object defaultEditingContextDelegate();
public void setDefaultEditingContextDelegate(Object delegate);
public Object defaultNoValidationDelegate();
public void setDefaultNoValidationDelegate(Object delegate);
public void setDefaultDelegateOnEditingContext(EOEditingContext ec);
public void setDefaultDelegateOnEditingContext(EOEditingContext ec, boolean validation);
public boolean useSharedEditingContext();
public void setUseSharedEditingContext(boolean value);
public EOEditingContext _newEditingContext();
public EOEditingContext _newEditingContext(boolean validationEnabled);
public EOEditingContext _newEditingContext(EOObjectStore objectStore);
public EOEditingContext _newEditingContext(EOObjectStore objectStore, boolean validationEnabled);
}
public ERXEC() {
this(defaultParentObjectStore());
}
public ERXEC(EOObjectStore os) {
super(os);
//super._initWithParentObjectStore(os);
ERXEnterpriseObject.Observer.install();
if (ERXEC.markOpenLocks()) {
creationTrace = new Exception("Creation");
creationTrace.fillInStackTrace();
activeEditingContexts.put(this, Thread.currentThread().getName());
}
}
// protected void _initWithParentObjectStore(EOObjectStore parent) {
// /* NOTE: This method is called from EOEditingContext's constructor. Doing nothing here to avoid the following race condition:
// * - new ERXEC() is called, it starts with invoking EOEditingContext constructor
// * - EOEditingContext constructor registers notifications' handlers
// * - One of the handlers gets triggered in another thread
// * - As some handlers are overriden in ERXEC, the ERXEC's handler is called - which means that ERXEC's method is invoked when ERXEC is still
// * under construction. This leads to disaster as all instance variables are not initialized at the time of the method call.
// */
// }
/**
* Utility to delete a bunch of objects.
* @param objects objects to delete
*/
public void deleteObjects(NSArray objects) {
for (int i = objects.count(); i-- > 0;) {
Object o = objects.objectAtIndex(i);
if (o instanceof EOEnterpriseObject) {
EOEnterpriseObject eo = (EOEnterpriseObject) o;
if (eo.editingContext() != null) {
eo.editingContext().deleteObject(eo);
}
}
}
}
/**
* Decides on a per-EC-level if autoLocking should be used.
* @return true if autoLocking should be used
*/
public boolean useAutoLock() {
if (useAutolock == null) {
useAutolock = Boolean.valueOf(defaultAutomaticLockUnlock());
}
return useAutolock.booleanValue();
}
/**
* Sets whether to use autoLocking on this EC.
* @param value if autolocking should be used
*/
public void setUseAutoLock(boolean value) {
useAutolock = Boolean.valueOf(value);
}
/**
* If you just use autolocking, you will end up churning locks constantly.
* Additionally, you can still end up with race conditions since you're not
* actually locking across your entire request. Coalescing auto locks
* attempts to solve this problem by leaving your auto lock open after the
* first use. This "hanging lock" will be cleaned up at the end of the RR
* loop by the unlocker.
* @return true if coalesceAutoLocks is enabled
*/
public boolean coalesceAutoLocks() {
if (coalesceAutoLocks == null) {
coalesceAutoLocks = Boolean.valueOf(defaultCoalesceAutoLocks());
if (coalesceAutoLocks.booleanValue() && !useUnlocker()) {
throw new IllegalStateException("You must enable the EC unlocker if you want to coalesce autolocks.");
}
}
return coalesceAutoLocks.booleanValue() && ERXApplication.isInRequest();
}
/**
* Sets whether or not coalescing auto locks should be enabled.
* @param value if coalescing auto locks should be enabled
*/
public void setCoalesceAutoLocks(boolean value) {
coalesceAutoLocks = Boolean.valueOf(value);
}
/**
* If traceOpenEditingContextLocks is true, returns the stack trace from
* when this EC was created
* @return the stack trace from when this EC was created
*/
public Exception creationTrace() {
return creationTrace;
}
/**
* @return If traceOpenEditingContextLocks is true, returns the stack trace from
* when this EC was locked
*/
public synchronized NSDictionary<Thread, NSMutableArray<Exception>> openLockTraces() {
return openLockTraces;
}
/**
* Overridden to emit log messages and push this instance to the locked
* editing contexts in this thread.
*/
@Override
public void lock() {
if (markOpenLocks()) {
traceLock();
}
lockAttempts().set(lockAttempts().get().intValue()+1);
super.lock();
pushLockedContextForCurrentThread(this);
if (markOpenLocks()) {
synchronized(this) {
lockingThread = Thread.currentThread();
lockingThreadName = lockingThread.getName();
}
}
if (!isAutoLocked() && lockLogger.isDebugEnabled()) {
if (lockTrace.isDebugEnabled()) {
lockLogger.debug("locked {}", this, new Exception());
}
else {
lockLogger.debug("locked {}", this);
}
}
}
private static Exception defaultTrace = new Exception("DefaultTrace");
/**
* Adds the current stack trace to openLockTraces.
*/
private synchronized void traceLock() {
if(openLockTraces == null) {
openLockTraces = new NSMutableDictionary<Thread, NSMutableArray<Exception>>();
}
Exception openLockTrace = defaultTrace;
if(traceOpenLocks()) {
openLockTrace = new Exception("Locked");
}
Thread currentThread = Thread.currentThread();
NSMutableArray<Exception> currentTraces = openLockTraces.objectForKey(currentThread);
if(currentTraces == null) {
currentTraces = new NSMutableArray<>();
openLockTraces.setObjectForKey(currentTraces, currentThread);
}
currentTraces.addObject(openLockTrace);
// AK: disabled, because do we really need this? It's really annoying while debugging.
// if (!currentThread.equals(lockingThread) && false) {
// StringBuffer buf = new StringBuffer(1024);
// buf.append(System.identityHashCode(this) + " Attempting to lock editing context from " + currentThread.getName() + " that was previously locked in " + lockingThread.getName() + "\n");
// buf.append(" Current stack trace: " + ERXUtilities.stackTrace(openLockTrace) + "\n");
// buf.append(" Lock count: " + openLockTraces.count() + "\n");
// Enumeration openLockTracesEnum = openLockTraces.objectEnumerator();
// while (openLockTracesEnum.hasMoreElements()) {
// Exception existingOpenLockTrace = (Exception) openLockTracesEnum.nextElement();
// buf.append(" Existing lock: " + ERXUtilities.stackTrace(existingOpenLockTrace));
// }
// log.info(buf);
// }
}
private synchronized void traceUnlock() {
if (openLockTraces != null) {
NSMutableArray<Exception> traces = openLockTraces.objectForKey(lockingThread);
if(traces != null) {
traces.removeLastObject();
if (traces.count() == 0) {
openLockTraces.removeObjectForKey(lockingThread);
}
} else {
log.error("Missing lock: {}", lockingThread);
}
if (openLockTraces.count() == 0) {
openLockTraces = null;
}
}
if(!isLockedInThread()) {
lockingThread = null;
lockingThreadName = null;
}
}
/**
* Overridden to emit log messages and pull this instance from the locked
* editing contexts in this thread.
*/
@Override
public void unlock() {
popLockedContextForCurrentThread(this);
if (markOpenLocks()) {
traceUnlock();
}
if (!isAutoLocked() && lockLogger.isDebugEnabled()) {
if (lockTrace.isDebugEnabled()) {
lockLogger.debug("unlocked {}", this, new Exception());
}
else {
lockLogger.debug("unlocked {}", this);
}
}
super.unlock();
lockAttempts().set(lockAttempts().get().intValue()-1);
}
private boolean isLockedInThread() {
return locks.get().contains(this);
}
private transient ThreadLocal<Integer> lockAttempts;
private ThreadLocal<Integer> lockAttempts() {
if(lockAttempts == null) {
lockAttempts = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return Integer.valueOf(0);
}
};
}
return lockAttempts;
}
/**
* Utility to actually emit the log messages and do the locking, based on
* the result of {@link #useAutoLock()}.
*
* @param method
* method name which to prepend to log message
* @return whether we did lock automatically
*/
protected boolean autoLock(String method) {
if (!useAutoLock() || isFinalizing || isLockedInThread() || lockAttempts().get().intValue() > 0)
return false;
boolean wasAutoLocked = false;
if (!isAutoLocked() || !coalesceAutoLocks()) {
wasAutoLocked = true;
lock();
autoLocked++;
if (!isFinalizing) {
if (lockTrace.isDebugEnabled()) {
lockTrace.debug("called method {} without a lock, ec={}", method, this, new Exception());
}
else {
//lockLogger.warn("called method {} without a lock, ec={}", method, this);
}
}
}
return wasAutoLocked;
}
/**
* Utility to unlock the EC is it was locked in the previous invocation.
*
* @param wasAutoLocked
* true if the EC was autolocked
*/
protected void autoUnlock(boolean wasAutoLocked) {
if (wasAutoLocked) {
// MS: Coalescing autolocks leaves the last autolock open to be
// closed by the request.
if (!coalesceAutoLocks()) {
autoLocked--;
unlock();
}
}
}
/**
* Returns whether we did autolock this instance.
*
* @return true if we were autolocked.
*/
public boolean isAutoLocked() {
return autoLocked > 0;
}
protected synchronized void _checkOpenLockTraces() {
NSMutableDictionary<Thread, NSMutableArray<Exception>> traces = openLockTraces;
if (traces != null && traces.count() != 0) {
String instance = getClass().getSimpleName() + "@" + System.identityHashCode(this);
log.error("{} Disposed with {} locks (finalizing = {})", instance, traces.count(), isFinalizing);
for (NSMutableArray<Exception> actual : traces.values()) {
for (Exception existingOpenLockTrace : actual) {
log.error("{} Existing lock.", instance, existingOpenLockTrace);
}
}
log.error("{} Created.", instance, creationTrace);
}
}
@Override
public void dispose() {
if (markOpenLocks()) {
_checkOpenLockTraces();
}
super.dispose();
}
@Override
public void finalize() throws Throwable {
isFinalizing = true;
try {
if (markOpenLocks()) {
// log.info("Finalize {}@{}", getClass(), System.identityHashCode(this));
_checkOpenLockTraces();
}
} catch(Throwable ex) {
// we *must* not fail in a finalizer
log.error("Error finalizing.", ex);
} finally {
super.finalize();
}
}
/**
* Technically, the OSC is public API and as such should also get
* auto-locked. In practice, it's called too often to warrant it. If you
* want, you can turn it on on a case by case basis.
*/
public static boolean _shouldLockOnLockObjectStore = false;
@Override
public void lockObjectStore() {
if(!_shouldLockOnLockObjectStore || (parentObjectStore() instanceof EOEditingContext)) {
super.lockObjectStore();
} else {
boolean wasAutoLocked = autoLock("lockObjectStore");
try {
super.lockObjectStore();
}
finally {
autoUnlock(wasAutoLocked);
}
}
}
@Override
public void unlockObjectStore() {
if(!_shouldLockOnLockObjectStore || (parentObjectStore() instanceof EOEditingContext)) {
super.unlockObjectStore();
} else {
boolean wasAutoLocked = autoLock("unlockObjectStore");
try {
super.unlockObjectStore();
}
finally {
autoUnlock(wasAutoLocked);
}
}
}
@Override
public void reset() {
boolean wasAutoLocked = autoLock("reset");
try {
super.reset();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void recordObject(EOEnterpriseObject eoenterpriseobject, EOGlobalID eoglobalid) {
boolean wasAutoLocked = autoLock("recordObject");
try {
super.recordObject(eoenterpriseobject, eoglobalid);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void forgetObject(EOEnterpriseObject eoenterpriseobject) {
boolean wasAutoLocked = autoLock("forgetObject");
try {
super.forgetObject(eoenterpriseobject);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void processRecentChanges() {
boolean wasAutoLocked = autoLock("processRecentChanges");
try {
super.processRecentChanges();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public NSArray<EOEnterpriseObject> updatedObjects() {
boolean wasAutoLocked = autoLock("updatedObjects");
try {
return super.updatedObjects();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public NSArray registeredObjects() {
boolean wasAutoLocked = autoLock("registeredObjects");
try {
return super.registeredObjects();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public NSArray<EOEnterpriseObject> insertedObjects() {
boolean wasAutoLocked = autoLock("insertedObjects");
try {
return super.insertedObjects();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public NSArray<EOEnterpriseObject> deletedObjects() {
boolean wasAutoLocked = autoLock("deletedObjects");
try {
return super.deletedObjects();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void setSharedEditingContext(EOSharedEditingContext eosharededitingcontext) {
boolean wasAutoLocked = autoLock("setSharedEditingContext");
try {
super.setSharedEditingContext(eosharededitingcontext);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public EOEnterpriseObject objectForGlobalID(EOGlobalID eoglobalid) {
boolean wasAutoLocked = autoLock("objectForGlobalID");
try {
return super.objectForGlobalID(eoglobalid);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public EOGlobalID globalIDForObject(EOEnterpriseObject eoenterpriseobject) {
boolean wasAutoLocked = autoLock("globalIDForObject");
try {
return super.globalIDForObject(eoenterpriseobject);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public NSDictionary committedSnapshotForObject(EOEnterpriseObject eoenterpriseobject) {
boolean wasAutoLocked = autoLock("committedSnapshotForObject");
try {
return super.committedSnapshotForObject(eoenterpriseobject);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public NSDictionary currentEventSnapshotForObject(EOEnterpriseObject eoenterpriseobject) {
boolean wasAutoLocked = autoLock("currentEventSnapshotForObject");
try {
return super.currentEventSnapshotForObject(eoenterpriseobject);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void objectWillChange(Object obj) {
boolean wasAutoLocked = autoLock("objectWillChange");
try {
super.objectWillChange(obj);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void insertObjectWithGlobalID(EOEnterpriseObject eoenterpriseobject, EOGlobalID eoglobalid) {
boolean wasAutoLocked = autoLock("insertObjectWithGlobalID");
try {
super.insertObjectWithGlobalID(eoenterpriseobject, eoglobalid);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void insertObject(EOEnterpriseObject eoenterpriseobject) {
boolean wasAutoLocked = autoLock("insertObject");
try {
super.insertObject(eoenterpriseobject);
}
finally {
autoUnlock(wasAutoLocked);
}
}
/**
* Overridden to support autoLocking and to call mightDelete() on subclasses
* of ERXEnterpriseObject.
*/
@Override
public void deleteObject(EOEnterpriseObject eo) {
boolean wasAutoLocked = autoLock("deleteObject");
try {
if (eo instanceof ERXEnterpriseObject) {
ERXEnterpriseObject erxeo = (ERXEnterpriseObject) eo;
erxeo.mightDelete();
}
super.deleteObject(eo);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public boolean hasChanges() {
boolean wasAutoLocked = autoLock("hasChanges");
try {
return super.hasChanges();
}
finally {
autoUnlock(wasAutoLocked);
}
}
protected void willSaveChanges(NSArray insertedObjects, NSArray updatedObjects, NSArray deletedObjects) {
}
protected void didSaveChanges(NSArray insertedObjects, NSArray updatedObjects, NSArray deletedObjects) {
}
/**
* Smarter version of normal <code>saveChanges()</code> method. Overridden
* to support autoLocking and a bugfix from Lenny Marks, calls up Will/Did
* methods on ERXEnterpriseObjects and corrects issues with
* <code>flushCaches()</code> needing to be called on objects in the
* parent context when committing the child context to the parent. If the
* editing context is a child of the object-store coordinator---that is,
* it's not a nested context---this method behaves exactly the same as
* <code>EOEditingContext.saveChanges()</code>. Otherwise, this method
* looks over the changed objects in <code>ec</code> (<code>updatedObjects()</code>,
* <code>insertedObjects()</code> and <code>deletedObjects()</code>).
* The changed objects lists are filtered for instances of
* <code>ERXGenericRecord</code>. The order of operations then becomes:
*
* <ol>
* <li> Call <code>processRecentChanges()</code> on the child context to
* propagate changes.
* <li> Lock the parent editing context.
* <li> On the deleted objects list in the child editing context, call
* <code>flushCaches()</code> on each corresponding EO in the parent
* context.
* <li> Unlock the parent editing context.
* <li> Call <code>saveChanges()</code> on the child, committing the child
* changes to the parent editing context.
* <li> Lock the parent editing context.
* <li> On the objects that were updated or inserted in the child, call
* <code>flushCaches()</code> on each corresponding EO in the parent
* context.
* <li> Unlock the parent editing context.
* </ol>
*
* <p>
*
* The order of operations is a bit peculiar: flush deletes, save, flush
* inserts and updates. This is done because deletes must be flushed because
* there may be dependent computed state that needs to be reset. But
* following the delete being committed, the relationships to other objects
* cannot be relied upon so it isn't reliable to call flushCaches after the
* commit. It's not entirely correct to flush the deletes like this, but
* it's the best we can do.
*
* <p>
*
* This works around an issue in EOF that we don't get a merge notification
* when a child EC saves to its parent. Because there's no merge
* notification, <code>flushCaches()</code> isn't called by the EC
* delegate and we're essentially screwed vis-a-vis resetting computed
* state.
*
* <p>
*
* This method assumes that the <code>ec</code> is locked before this
* method is invoked, but this method will take the lock on the parent
* editing context if the <code>ec</code> is a nested context before and
* after the save in order to get the objects and to flush caches on them.
*/
@Override
public void saveChanges() {
boolean wasAutoLocked = autoLock("saveChanges");
_EOAssertSafeMultiThreadedAccess("saveChanges()");
savingChanges = true;
try {
NSArray insertedObjects = insertedObjects().immutableClone();
NSArray updatedObjects = updatedObjects().immutableClone();
NSArray deletedObjects = deletedObjects().immutableClone();
willSaveChanges(insertedObjects, updatedObjects, deletedObjects);
NSNotificationCenter.defaultCenter().postNotification(EditingContextWillSaveChangesNotification, this);
_saveChanges();
didSaveChanges(insertedObjects, updatedObjects, deletedObjects);
}
catch (com.webobjects.eoaccess.EOGeneralAdaptorException e) {
NSNotificationCenter.defaultCenter().postNotification(EditingContextFailedToSaveChanges, this);
Object delegate = delegate();
boolean delegateImplementsDidSaveFailed = delegate != null && EditingContextDidFailSaveChangesDelegateSelector.implementedByObject(delegate);
if (delegateImplementsDidSaveFailed) {
final Object[] parameters = new Object[] { this, e };
RuntimeException ex = (RuntimeException) ERXSelectorUtilities.invoke(EditingContextDidFailSaveChangesDelegateSelector, delegate, parameters);
if (ex != null) {
throw ex;
}
}
else {
throw e;
}
}
finally {
autoUnlock(wasAutoLocked);
savingChanges = false;
processQueuedNotifications();
}
}
/**
* Saves changes and tries to recover from optimistic locking exceptions by
* refaulting the object in question, optionally merging the changed values
* and optionally retrying the save.
*
* @param doesRetry
* when true, saves again after resolving. when false, throws the
* optimistic locking after resolving
* @param mergesChanges
*/
public void saveChangesTolerantly(boolean doesRetry, boolean mergesChanges) {
_EOAssertSafeMultiThreadedAccess("saveChangesTolerantly()");
boolean recover = _recoversFromException;
boolean retry = _doesRetry;
boolean merge = _mergesChanges;
setOptions(true, doesRetry, mergesChanges);
saveChanges();
setOptions(recover, retry, merge);
}
public void saveChangesTolerantly() {
saveChangesTolerantly(true);
}
public void saveChangesTolerantly(boolean doesRetry) {
saveChangesTolerantly(doesRetry, true);
}
private boolean _recoversFromException;
private boolean _doesRetry;
private boolean _mergesChanges;
/**
* Set the options for the saveChanges() operation.
*
* @param recoversFromException
* @param doesRetry
* when true, saves again after resolving. when false, throws the
* optimistic locking after resolving
* @param mergesChanges
*/
public void setOptions(boolean recoversFromException, boolean doesRetry, boolean mergesChanges) {
_EOAssertSafeMultiThreadedAccess("setOptions()");
_recoversFromException = recoversFromException;
_doesRetry = doesRetry;
_mergesChanges = mergesChanges;
}
protected void _saveChanges() {
boolean saved = true;
try {
super.saveChanges();
}
catch (EOGeneralAdaptorException e) {
saved = false;
if (_recoversFromException) {
log.warn("_saveChangesTolerantly: Exception occurred", e);
if (ERXEOAccessUtilities.isOptimisticLockingFailure(e)) {
EOEnterpriseObject eo = ERXEOAccessUtilities.refetchFailedObject(this, e);
if (_mergesChanges) {
ERXEOAccessUtilities.reapplyChanges(eo, e);
}
if (_doesRetry) {
_saveChanges();
saved = true;
}
}
}
else {
// log.warn("_saveChangesTolerantly: Exception occurred: "+ e +
// NSPropertyListSerialization.stringFromPropertyList(e.userInfo()),
// e);
}
if (!saved) {
throw e;
}
}
}
@Override
public EOEnterpriseObject faultForGlobalID(EOGlobalID eoglobalid, EOEditingContext eoeditingcontext) {
boolean wasAutoLocked = autoLock("faultForGlobalID");
try {
return super.faultForGlobalID(eoglobalid, eoeditingcontext);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public NSArray arrayFaultWithSourceGlobalID(EOGlobalID eoglobalid, String s, EOEditingContext eoeditingcontext) {
boolean wasAutoLocked = autoLock("arrayFaultWithSourceGlobalID");
try {
return super.arrayFaultWithSourceGlobalID(eoglobalid, s, eoeditingcontext);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void initializeObject(EOEnterpriseObject eoenterpriseobject, EOGlobalID eoglobalid, EOEditingContext eoeditingcontext) {
boolean wasAutoLocked = autoLock("initializeObject");
try {
if (eoenterpriseobject instanceof ERXGenericRecord) {
ERXGenericRecord eo = (ERXGenericRecord) eoenterpriseobject;
// gross hack to not trigger the two-way relationships
boolean old = eo._setUpdateInverseRelationships(false);
try {
super.initializeObject(eoenterpriseobject, eoglobalid, eoeditingcontext);
}
finally {
eo._setUpdateInverseRelationships(old);
}
}
else {
super.initializeObject(eoenterpriseobject, eoglobalid, eoeditingcontext);
}
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void editingContextDidForgetObjectWithGlobalID(EOEditingContext eoeditingcontext, EOGlobalID eoglobalid) {
boolean wasAutoLocked = autoLock("editingContextDidForgetObjectWithGlobalID");
try {
super.editingContextDidForgetObjectWithGlobalID(eoeditingcontext, eoglobalid);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public NSArray objectsForSourceGlobalID(EOGlobalID eoglobalid, String s, EOEditingContext eoeditingcontext) {
boolean wasAutoLocked = autoLock("objectsForSourceGlobalID");
try {
return super.objectsForSourceGlobalID(eoglobalid, s, eoeditingcontext);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void refaultObject(EOEnterpriseObject eoenterpriseobject) {
boolean wasAutoLocked = autoLock("refaultObject");
try {
super.refaultObject(eoenterpriseobject);
}
finally {
autoUnlock(wasAutoLocked);
}
}
/**
* Overridden to support autoLocking and to flush the cache of all
* ERXEnterpriseObjects.
*/
@Override
public void refaultObject(EOEnterpriseObject eoenterpriseobject, EOGlobalID eoglobalid, EOEditingContext eoeditingcontext) {
boolean wasAutoLocked = autoLock("refaultObject");
try {
// ak: need to flush caches
ERXEnterpriseObject.FlushCachesProcessor.perform(this, eoenterpriseobject);
super.refaultObject(eoenterpriseobject, eoglobalid, eoeditingcontext);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public NSArray objectsWithFetchSpecification(EOFetchSpecification fs, EOEditingContext eoeditingcontext) {
boolean wasAutoLocked = autoLock("objectsWithFetchSpecification");
try {
NSArray objects = super.objectsWithFetchSpecification(fs, eoeditingcontext);
if (fs instanceof ERXFetchSpecification && ((ERXFetchSpecification)fs).includeEditingContextChanges()) {
objects = ERXEOControlUtilities.filteredObjectsWithQualifier(this, objects, fs.entityName(), fs.qualifier(), fs.sortOrderings(), fs.usesDistinct(), fs.isDeep(), true, true, true, true);
}
return objects;
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void saveChangesInEditingContext(EOEditingContext eoeditingcontext) {
boolean wasAutoLocked = autoLock("saveChangesInEditingContext");
try {
super.saveChangesInEditingContext(eoeditingcontext);
NSArray<EOEnterpriseObject> childInsertedObjects = eoeditingcontext.insertedObjects();
for (EOEnterpriseObject childInsertedObject : childInsertedObjects) {
EOEnterpriseObject parentInsertedObject = objectForGlobalID(eoeditingcontext.globalIDForObject(childInsertedObject));
if (parentInsertedObject instanceof ERXGenericRecord && childInsertedObject instanceof ERXGenericRecord) {
((ERXGenericRecord)parentInsertedObject).didCopyFromChildInEditingContext((ERXGenericRecord)childInsertedObject, eoeditingcontext);
}
}
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void refaultAllObjects() {
boolean wasAutoLocked = autoLock("refaultAllObjects");
try {
super.refaultAllObjects();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void invalidateObjectsWithGlobalIDs(NSArray nsarray) {
boolean wasAutoLocked = autoLock("invalidateObjectsWithGlobalIDs");
try {
super.invalidateObjectsWithGlobalIDs(nsarray);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void invalidateAllObjects() {
boolean wasAutoLocked = autoLock("invalidateAllObjects");
try {
super.invalidateAllObjects();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void lockObject(EOEnterpriseObject eoenterpriseobject) {
boolean wasAutoLocked = autoLock("lockObject");
try {
super.lockObject(eoenterpriseobject);
}
finally {
autoUnlock(wasAutoLocked);
}
}
/** Overridden to support autoLocking and will/did revert delegate methods. */
@Override
public void revert() {
boolean wasAutoLocked = autoLock("revert");
try {
final NSArray insertedObjects = insertedObjects().immutableClone();
final NSArray updatedObjects = updatedObjects().immutableClone();
final NSArray deletedObjects = deletedObjects().immutableClone();
ERXEnterpriseObject.WillRevertProcessor.perform(this, insertedObjects);
ERXEnterpriseObject.WillRevertProcessor.perform(this, updatedObjects);
ERXEnterpriseObject.WillRevertProcessor.perform(this, deletedObjects);
final Object delegate = delegate();
final boolean delegateImplementsWillRevert = delegate != null && EditingContextWillRevertObjectsDelegateSelector.implementedByObject(delegate);
final boolean delegateImplementsDidRevert = delegate != null && EditingContextDidRevertObjectsDelegateSelector.implementedByObject(delegate);
final boolean needToCallDelegate = delegateImplementsWillRevert || delegateImplementsDidRevert;
final Object[] parameters = needToCallDelegate ? new Object[] { this, insertedObjects, updatedObjects, deletedObjects } : null;
if (delegateImplementsWillRevert)
ERXSelectorUtilities.invoke(EditingContextWillRevertObjectsDelegateSelector, delegate, parameters);
super.revert();
if (delegateImplementsDidRevert)
ERXSelectorUtilities.invoke(EditingContextDidRevertObjectsDelegateSelector, delegate, parameters);
ERXEnterpriseObject.DidRevertProcessor.perform(this, insertedObjects);
ERXEnterpriseObject.DidRevertProcessor.perform(this, updatedObjects);
ERXEnterpriseObject.DidRevertProcessor.perform(this, deletedObjects);
NSNotificationCenter.defaultCenter().postNotification(EditingContextDidRevertChanges, this);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void refreshObject(EOEnterpriseObject eoenterpriseobject) {
boolean wasAutoLocked = autoLock("refreshObject");
try {
super.refreshObject(eoenterpriseobject);
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void undo() {
boolean wasAutoLocked = autoLock("undo");
try {
super.undo();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public void redo() {
boolean wasAutoLocked = autoLock("redo");
try {
super.redo();
}
finally {
autoUnlock(wasAutoLocked);
}
}
@Override
public Object invokeRemoteMethod(EOEditingContext eoeditingcontext, EOGlobalID eoglobalid, String s, Class aclass[], Object aobj[]) {
boolean wasAutoLocked = autoLock("invokeRemoteMethod");
try {
return super.invokeRemoteMethod(eoeditingcontext, eoglobalid, s, aclass, aobj);
}
finally {
autoUnlock(wasAutoLocked);
}
}
private boolean savingChanges;
private NSMutableArray<NSNotification> queuedNotifications = new NSMutableArray<>();
protected static Map<ERXEC, String> activeEditingContexts = Collections.synchronizedMap(new WeakHashMap());
/**
* Overridden so add a bugfix from Lenny Marks
*/
@Override
public void _objectsChangedInStore(NSNotification nsnotification) {
ERXEnterpriseObject.FlushCachesProcessor.perform(this, (NSArray) nsnotification.userInfo().objectForKey("objects"));
/*
* Check to see if this context, or any parent context, is saving changes.
* If so, queue up the notifications.
*/
boolean isSavingChanges = savingChanges;
EOObjectStore parent = parentObjectStore();
while(!isSavingChanges && parent instanceof ERXEC) {
ERXEC parentEc = (ERXEC) parent;
isSavingChanges = parentEc.savingChanges;
parent = parentEc.parentObjectStore();
}
if (isSavingChanges) {
synchronized (queuedNotifications) {
queuedNotifications.addObject(nsnotification);
}
}
else {
super._objectsChangedInStore(nsnotification);
}
}
/**
* Temp EC delegate that prevents merging of changes to objects that already have changes.
*
* @author ak
*/
public static class _DenyMergeDelegate {
private _NSDelegate delegate;
public _DenyMergeDelegate(Object original) {
if (original != null) {
delegate = new _NSDelegate(original.getClass(), original);
}
}
public boolean editingContextShouldMergeChangesForObject(EOEditingContext ec, EOEnterpriseObject eo) {
if(delegate != null && delegate.respondsTo("editingContextShouldMergeChangesForObject")) {
return delegate.booleanPerform("editingContextShouldMergeChangesForObject", ec, eo);
}
NSDictionary committedSnapshotForObject = ec.committedSnapshotForObject(eo);
if(committedSnapshotForObject != null) {
return eo.changesFromSnapshot(committedSnapshotForObject).count() == 0;
}
//AK: this is when the object is simply registered, not sure if true or false would be better... setting?
return true;
}
public void editingContextDidMergeChanges(EOEditingContext ec) {
if(delegate != null && delegate.respondsTo("editingContextShouldMergeChangesForObject")) {
delegate.perform("editingContextDidMergeChanges", ec);
}
}
}
private _NSDelegate _denyDelegate;
/**
* Quick and experimental hack to hook into the merge-changes system. If
* it's not useful, it should be removed again.
*/
@Override
public void _processObjectStoreChanges(NSDictionary info) {
if (ERXProperties.booleanForKey("er.extensions.ERXEC.denyMerges")) {
Object original = delegate();
try {
if (_denyDelegate == null) {
_denyDelegate = new _NSDelegate(_DenyMergeDelegate.class, new _DenyMergeDelegate(original));
}
setDelegate(_denyDelegate);
super._processObjectStoreChanges(info);
}
finally {
setDelegate(original);
}
}
else {
super._processObjectStoreChanges(info);
}
}
/**
* Overridden so add a bugfix from Lenny Marks
*
*/
private void processQueuedNotifications() {
NSMutableArray<NSNotification> queuedNotificationsClone;
synchronized (queuedNotifications) {
queuedNotificationsClone = new NSMutableArray<>(queuedNotifications);
queuedNotifications.removeAllObjects();
}
for (NSNotification notification : queuedNotificationsClone) {
_objectsChangedInStore(notification);
}
NSNotificationCenter.defaultCenter().postNotification(ERXECProcessQueuedNotificationsNotification, this);
}
public void processQueuedNotificationsNotification(NSNotification n) {
processQueuedNotifications();
}
/**
* Sets the delegate for this context.
*/
@Override
public void setDelegate(Object d) {
if (log.isDebugEnabled()) {
log.debug("setting delegate to {}", d);
log.debug(ERXUtilities.stackTrace());
}
super.setDelegate(d);
}
/** Default implementation of the Factory interface. */
public static class DefaultFactory implements Factory {
private static final Logger log = LoggerFactory.getLogger(DefaultFactory.class);
/** holds a reference to the default ec delegate */
protected Object defaultEditingContextDelegate;
/** holds a reference to the default no validation delegate */
protected Object defaultNoValidationDelegate;
/**
* holds whether to newly created instances use the shared editing
* context.
*/
protected Boolean useSharedEditingContext = null;
public DefaultFactory() {
// Initing defaultEditingContext delegates
defaultEditingContextDelegate = new ERXDefaultEditingContextDelegate();
defaultNoValidationDelegate = new ERXECNoValidationDelegate();
}
/**
* Returns the default editing context delegate. This delegate is used
* by default for all editing contexts that are created.
*
* @return the default editing context delegate
*/
public Object defaultEditingContextDelegate() {
return defaultEditingContextDelegate;
}
/**
* Sets the default editing context delegate to be used for editing
* context creation.
*
* @param delegate
* to be set on every created editing context by default.
*/
public void setDefaultEditingContextDelegate(Object delegate) {
defaultEditingContextDelegate = delegate;
log.debug("setting defaultEditingContextDelegate to {}", delegate);
}
/**
* Default delegate that does not perform validation. Not performing
* validation can be a good thing when using nested editing contexts as
* sometimes you only want to validate one object, not all the
* objects.
*
* @return default delegate that doesn't perform validation
*/
public Object defaultNoValidationDelegate() {
return defaultNoValidationDelegate;
}
/**
* Sets the default editing context delegate to be used for editing
* context creation that does not allow validation.
*
* @param delegate
* to be set on every created editing context that doesn't
* allow validation.
*/
public void setDefaultNoValidationDelegate(Object delegate) {
defaultNoValidationDelegate = delegate;
}
/**
* Sets either the default editing context delegate that does or does
* not allow validation based on the validation flag passed in on the
* given editing context.
*
* @param ec
* editing context to have it's delegate set.
* @param validation
* flag that determines if the editing context should perform
* validation on objects being saved.
*/
public void setDefaultDelegateOnEditingContext(EOEditingContext ec, boolean validation) {
log.debug("Setting default delegate on editing context: {} allows validation: {}", ec, validation);
if (ec != null) {
if (validation) {
ec.setDelegate(defaultEditingContextDelegate());
}
else {
ec.setDelegate(defaultNoValidationDelegate());
}
}
else {
log.warn("Attempting to set a default delegate on a null ec!");
}
}
/**
* Sets the default editing context delegate on the given editing
* context.
*
* @param ec
* editing context to have it's delegate set.
*/
public void setDefaultDelegateOnEditingContext(EOEditingContext ec) {
setDefaultDelegateOnEditingContext(ec, true);
}
/**
* See static method for documentation.
*/
public EOEditingContext _newEditingContext() {
return _newEditingContext(EOEditingContext.defaultParentObjectStore(), true);
}
/**
* See static method for documentation.
*/
public EOEditingContext _newEditingContext(boolean validationEnabled) {
return _newEditingContext(EOEditingContext.defaultParentObjectStore(), validationEnabled);
}
/**
* See static method for documentation.
*/
public EOEditingContext _newEditingContext(EOObjectStore objectStore) {
return _newEditingContext(objectStore, true);
}
/**
* See static method for documentation.
*/
public EOEditingContext _newEditingContext(EOObjectStore objectStore, boolean validationEnabled) {
EOEditingContext ec = _createEditingContext(objectStore);
int levelsOfUndo = ERXValueUtilities.intValueWithDefault(System.getProperty("WODefaultUndoStackLimit"), 10);
if (levelsOfUndo == 0) {
ec.setUndoManager(null);
}
else {
ec.undoManager().setLevelsOfUndo(levelsOfUndo < 0 ? 10 : levelsOfUndo);
}
setDefaultDelegateOnEditingContext(ec, validationEnabled);
if (!useSharedEditingContext()) {
ec.lock();
ec.setSharedEditingContext(null);
ec.unlock();
}
NSNotificationCenter.defaultCenter().postNotification(EditingContextDidCreateNotification, ec);
if(objectStore instanceof ERXEC) {
ERXEC parent = (ERXEC)objectStore;
NSNotificationCenter.defaultCenter().addObserver(ec, ERXECProcessQueuedNotificationsSelector, ERXECProcessQueuedNotificationsNotification, parent);
}
return ec;
}
private Constructor _editingContextConstructor;
/**
* @return The Constructor used by _createEditingContext() to create a new editing context by instantiating
* the class named ERXEC.editingContextClassName().
*/
protected Constructor editingContextConstructor() {
if (_editingContextConstructor == null) {
Class editingContextClass;
try {
editingContextClass = Class.forName(ERXEC.editingContextClassName());
}
catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
Class[] parameterTypes = new Class[] { EOObjectStore.class };
try {
_editingContextConstructor = editingContextClass.getConstructor(parameterTypes);
}
catch (SecurityException e) {
throw new RuntimeException(e);
}
catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
return _editingContextConstructor;
}
/**
* Actual EC creation bottleneck. Override this to return other
* subclasses or use the <code>er.extensions.ERXEC.editingContextClassName</code> property to
* specify the name of your editing context class to be instantiated and returned by this method.
*
* @param parent The parent EOObjectStore for the editing context created and returned by this method
* @return The editing context created by this method
*/
protected EOEditingContext _createEditingContext(EOObjectStore parent) {
EOObjectStore arg = (parent == null ? EOEditingContext.defaultParentObjectStore() : parent);
EOEditingContext editingContext;
try {
editingContext = (EOEditingContext) editingContextConstructor().newInstance(arg);
}
catch (IllegalArgumentException e) {
throw new RuntimeException(e);
}
catch (InstantiationException e) {
throw new RuntimeException(e);
}
catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
return editingContext;
}
public boolean useSharedEditingContext() {
if (useSharedEditingContext == null) {
useSharedEditingContext = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXEC.useSharedEditingContext", true) ? Boolean.TRUE : Boolean.FALSE;
log.debug("setting useSharedEditingContext to {}", useSharedEditingContext);
}
return useSharedEditingContext.booleanValue();
}
public void setUseSharedEditingContext(boolean value) {
useSharedEditingContext = value ? Boolean.TRUE : Boolean.FALSE;
}
}
/** holds a reference to the factory used to create editing contexts */
protected static volatile Factory factory;
/**
* Gets the factory used to create editing contexts
*
* @return editing context factory
*/
public static Factory _factory() {
if (factory == null) {
synchronized(ERXEC.class) {
if(factory == null) {
factory = new DefaultFactory();
}
}
}
return factory;
}
/**
* Sets the default editing context factory
*
* @param aFactory
* factory used to create editing contexts
*/
public static void setFactory(Factory aFactory) {
factory = aFactory;
}
/**
* Factory method to create a new editing context. Sets the current default
* delegate on the newly created editing context.
*
* @return a newly created editing context with the default delegate set.
*/
public static EOEditingContext newEditingContext() {
return _factory()._newEditingContext();
}
/**
* Factory method to create a new tolerant editing context.
* @param parent the parent object store
* @param retry true if the ec should retry on failed save
* @param merge true if ec changes should be merged
* @return a new ec
*/
public static EOEditingContext newTolerantEditingContext(EOObjectStore parent, boolean retry, boolean merge) {
ERXEC ec = (ERXEC) newEditingContext(parent);
ec.lock();
try {
ec.setOptions(true, retry, merge);
}
finally {
ec.unlock();
}
return ec;
}
public static EOEditingContext newTolerantEditingContext() {
return newTolerantEditingContext(null, true, true);
}
public static EOEditingContext newTolerantEditingContext(EOObjectStore osc) {
return newTolerantEditingContext(osc, true, true);
}
public static void saveChangesTolerantly(EOEditingContext ec, boolean doesRetry, boolean mergesChanges) {
if (ec instanceof ERXEC) {
ERXEC erxec = (ERXEC) ec;
erxec.saveChangesTolerantly(doesRetry, mergesChanges);
}
}
public static void saveChangesTolerantly(EOEditingContext ec) {
saveChangesTolerantly(ec, true, true);
}
/**
* Creates a new editing context with the specified object store as the
* parent object store and with validation turned on or off depending on the
* flag passed in. This method is useful when creating nested editing
* contexts. After creating the editing context the default delegate is set
* on the editing context if validation is enabled or the default no
* validation delegate is set if validation is disabled.
* <p>
* Note:
* an {@link com.webobjects.eocontrol.EOEditingContext EOEditingContext} is
* a subclass of EOObjectStore so passing in another editing context to this
* method is completely kosher.
*
* @param parent
* object store for the newly created editing context.
* @param validationEnabled
* determines if the editing context should perform validation
* @return new editing context with the given parent object store and the
* delegate corresponding to the validation flag
*/
public static EOEditingContext newEditingContext(EOObjectStore parent, boolean validationEnabled) {
return _factory()._newEditingContext(parent, validationEnabled);
}
/**
* Factory method to create a new editing context with validation disabled.
* Sets the default no validation delegate on the editing context. Be careful -
* an editing context that does not perform validation means that none of
* the usual validation methods are called on the enterprise objects before
* they are saved to the database.
*
* @param validation
* flag that determines if validation should or should not be
* enabled.
* @return a newly created editing context with a delegate set that has
* disabled validation.
*/
public static EOEditingContext newEditingContext(boolean validation) {
return _factory()._newEditingContext(validation);
}
/**
* Creates a new editing context with the specified object store as the
* parent object store. This method is useful when creating nested editing
* contexts. After creating the editing context the default delegate is set
* on the editing context.
* <p>
* Note: an {@link EOEditingContext} is
* a subclass of EOObjectStore so passing in another editing context to this
* method is completely kosher.
*
* @param objectStore
* parent object store for the newly created editing context.
* @return new editing context with the given parent object store
*/
public static EOEditingContext newEditingContext(EOObjectStore objectStore) {
return _factory()._newEditingContext(objectStore);
}
/**
* Register the OpenEditingContextLockSignalHandler signal handle on the HUP
* signal.
*/
public static void registerOpenEditingContextLockSignalHandler() {
try {
ERXEC.registerOpenEditingContextLockSignalHandler("HUP");
}
catch (IllegalArgumentException e) {
log.warn("ERXEC's HUP signal handler was not registered, probably because your operating system does not support this signal.");
}
}
/**
* Register the OpenEditingContextLockSignalHandler signal handle on the
* named signal.
*
* @param signalName
* the name of the signal to handle
*/
public static void registerOpenEditingContextLockSignalHandler(String signalName) {
ERXSignalHandler.register(signalName, new ERXEC.DumpLocksSignalHandler());
ERXSignalHandler.register(signalName, new ERXObjectStoreCoordinator.DumpLocksSignalHandler());
}
/**
* Returns a string describing outstanding locks for the created ECs
* @return description of locks
*/
public static String outstandingLockDescription() {
try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
boolean hadLocks = false;
pw.print("Currently " + activeEditingContexts.size() + " active ECs : "+ activeEditingContexts + ")");
for (ERXEC ec : ERXEC.activeEditingContexts.keySet()) {
synchronized (ec) {
NSMutableDictionary<Thread, NSMutableArray<Exception>> traces = ec.openLockTraces;
if (traces != null && traces.count() > 0) {
hadLocks = true;
pw.println("\n------------------------");
pw.println("Editing Context: " + ec + " Locking thread: " + ec.lockingThreadName + "->" + ec.lockingThread);
if(ec.creationTrace != null) {
ec.creationTrace.printStackTrace(pw);
}
if(!ERXEC.traceOpenLocks()) {
pw.println("Stack tracing is disabled");
} else {
for (Thread thread : traces.keySet()) {
pw.println("Outstanding at @" + thread);
for(Exception ex: traces.objectForKey(thread)) {
ex.printStackTrace(pw);
}
}
}
} else {
// pw.println("\n------------------------");
// pw.println("Editing Context: " + ec + " unlocked");
}
}
}
if(!hadLocks) {
pw.print("No open editing contexts (of " + activeEditingContexts.size() + ")");
}
return sw.toString();
}
catch (IOException e) {
// ignore
}
return null;
}
/**
* OpenEditingContextLockSignalHandler provides a signal handler that prints
* out open editing context locks. By default, the handler attaches to
* SIGHUP.
* <p>
* Call ERXEC.registerOpenEditingContextLockSignalHandler() to attach it.
*/
public static class DumpLocksSignalHandler implements SignalHandler {
public void handle(Signal signal) {
log.info(outstandingLockDescription());
}
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
/*
* The superclass serialization methods register for notifications during
* readObject, but do not unregister during writeObject. Since the
* NSNotificationCenter hangs on to the ec until it is gc'ed, then it is
* possible to have an ec containing insertedObjects which were later
* saved in a different deserialized ec. If one of these objects is then
* deleted, the notification center broadcasts an invalidate objects
* notification to the ec which still contains the eo in insertedObjects.
*
* When this happens, the ec throws an exception. To prevent this, the ec
* is removed here as an observer for notifications when it is serialized.
*/
NSNotificationCenter nc = NSNotificationCenter.defaultCenter();
nc.removeObserver(this);
}
}