This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit d8941d1a89a87ad8417831be2c021d3da758ad8f
Author: stariy95 <[email protected]>
AuthorDate: Tue Dec 5 14:58:54 2023 +0400

    CAY-2829 Merge `BaseContext` with `DataContext` and deprecate it
---
 RELEASE-NOTES.txt                                  |   1 +
 .../main/java/org/apache/cayenne/BaseContext.java  | 681 +--------------------
 .../org/apache/cayenne/access/DataContext.java     | 629 ++++++++++++++++++-
 .../{ => access}/ObjectContextDeleteAction.java    |   7 +-
 .../java/org/apache/cayenne/MockBaseContext.java   | 124 ----
 .../DataContextTest.java}                          | 191 +++---
 .../access/NestedDataContextLocalCacheIT.java      |   5 +-
 7 files changed, 720 insertions(+), 918 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index c4c13a70c..ce84893f8 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -50,6 +50,7 @@ CAY-2824 Rename CayenneServerModuleProvider to 
CayenneRuntimeModuleProvider
 CAY-2825 Rename package `o.a.c.configuration.server` to 
`o.a.c.configuration.runtime`
 CAY-2826 Rename `ServerModule` to `CoreModule`
 CAY-2828 Rename values in `org.apache.cayenne.configuration.Constants`
+CAY-2829 Merge `BaseContext` with `DataContext` and deprecate it
 
 Bug Fixes:
 
diff --git a/cayenne/src/main/java/org/apache/cayenne/BaseContext.java 
b/cayenne/src/main/java/org/apache/cayenne/BaseContext.java
index fd8a5ea4f..d308b4fef 100644
--- a/cayenne/src/main/java/org/apache/cayenne/BaseContext.java
+++ b/cayenne/src/main/java/org/apache/cayenne/BaseContext.java
@@ -18,689 +18,16 @@
  ****************************************************************/
 package org.apache.cayenne;
 
-import org.apache.cayenne.cache.NestedQueryCache;
-import org.apache.cayenne.cache.QueryCache;
-import org.apache.cayenne.runtime.CayenneRuntime;
-import org.apache.cayenne.di.Injector;
-import org.apache.cayenne.event.EventManager;
-import org.apache.cayenne.exp.ValueInjector;
-import org.apache.cayenne.graph.CompoundDiff;
-import org.apache.cayenne.graph.GraphDiff;
-import org.apache.cayenne.graph.GraphEvent;
-import org.apache.cayenne.graph.GraphManager;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.LifecycleEvent;
-import org.apache.cayenne.map.ObjEntity;
-import org.apache.cayenne.query.ObjectIdQuery;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.RefreshQuery;
-import org.apache.cayenne.query.Select;
-import org.apache.cayenne.reflect.AttributeProperty;
-import org.apache.cayenne.reflect.ClassDescriptor;
-import org.apache.cayenne.reflect.PropertyDescriptor;
-import org.apache.cayenne.reflect.PropertyVisitor;
-import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.util.ObjectContextGraphAction;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import org.apache.cayenne.access.DataContext;
 
 /**
  * A common base superclass for Cayenne ObjectContext implementors.
  * 
  * @since 3.0
+ * @deprecated since 5.0, use {@link DataContext} directly
  */
-public abstract class BaseContext implements ObjectContext {
-
-       /**
-        * A holder of a ObjectContext bound to the current thread.
-        * 
-        * @since 3.0
-        */
-       protected static final ThreadLocal<ObjectContext> threadObjectContext = 
new ThreadLocal<ObjectContext>();
-
-       /**
-        * Returns the ObjectContext bound to the current thread.
-        * 
-        * @since 3.0
-        * @return the ObjectContext associated with caller thread.
-        * @throws IllegalStateException
-        *             if there is no ObjectContext bound to the current thread.
-        */
-       public static ObjectContext getThreadObjectContext() throws 
IllegalStateException {
-               ObjectContext context = threadObjectContext.get();
-               if (context == null) {
-                       throw new IllegalStateException("Current thread has no 
bound ObjectContext.");
-               }
-
-               return context;
-       }
-
-       /**
-        * Binds a ObjectContext to the current thread. ObjectContext can later 
be
-        * retrieved by users in the same thread by calling
-        * {@link BaseContext#getThreadObjectContext}. Using null parameter will
-        * unbind currently bound ObjectContext.
-        * 
-        * @since 3.0
-        */
-       public static void bindThreadObjectContext(ObjectContext context) {
-               threadObjectContext.set(context);
-       }
-
-       // transient variables that should be reinitialized on deserialization 
from
-       // the
-       // registry
-       protected transient DataChannel channel;
-       protected transient QueryCache queryCache;
-       protected transient EntityResolver entityResolver;
-
-       protected boolean validatingObjectsOnCommit = true;
-
-       /**
-        * Graph action that handles property changes
-        * 
-        * @since 3.1
-        */
-       protected ObjectContextGraphAction graphAction;
-
-       /**
-        * Stores user defined properties associated with this DataContext.
-        * 
-        * @since 3.0
-        */
-       protected volatile Map<String, Object> userProperties;
-
+@Deprecated(since = "5.0", forRemoval = true)
+public abstract class BaseContext extends DataContext {
        protected BaseContext() {
-               graphAction = new ObjectContextGraphAction(this);
-       }
-
-       /**
-        * Checks whether this context is attached to Cayenne runtime stack and 
if
-        * not, attempts to attach itself to the runtime using Injector returned
-        * from the call to {@link CayenneRuntime#getThreadInjector()}. If 
thread
-        * Injector is not available and the context is not attached, throws
-        * CayenneRuntimeException.
-        * <p>
-        * This method is called internally by the context before access to
-        * transient variables to allow the context to attach to the stack 
lazily
-        * following deserialization.
-        * 
-        * @return true if the context successfully attached to the thread 
runtime,
-        *         false - if it was already attached.
-        * @since 3.1
-        */
-       protected boolean attachToRuntimeIfNeeded() {
-               if (channel != null) {
-                       return false;
-               }
-
-               Injector injector = CayenneRuntime.getThreadInjector();
-               if (injector == null) {
-                       throw new CayenneRuntimeException("Can't attach to 
Cayenne runtime. "
-                                       + "Null injector returned from 
CayenneRuntime.getThreadInjector()");
-               }
-
-               attachToRuntime(injector);
-               return true;
-       }
-
-       /**
-        * Attaches this context to the CayenneRuntime whose Injector is passed 
as
-        * an argument to this method.
-        * 
-        * @since 3.1
-        */
-       protected void attachToRuntime(Injector injector) {
-
-               // TODO: nested contexts handling??
-               attachToChannel(injector.getInstance(DataChannel.class));
-               setQueryCache(new 
NestedQueryCache(injector.getInstance(QueryCache.class)));
-       }
-
-       /**
-        * Attaches to a provided DataChannel.
-        * 
-        * @since 3.1
-        */
-       protected void attachToChannel(DataChannel channel) {
-               if (channel == null) {
-                       throw new NullPointerException("Null channel");
-               }
-
-               setChannel(channel);
-               setEntityResolver(channel.getEntityResolver());
-       }
-
-       @Override
-       public abstract void commitChanges();
-
-       @Override
-       public abstract void commitChangesToParent();
-
-       @Override
-       public void deleteObject(Object object) throws DeleteDenyException {
-               deleteObjects(object);
-       }
-
-       @Override
-       public abstract Collection<?> deletedObjects();
-
-       @Override
-       public DataChannel getChannel() {
-               attachToRuntimeIfNeeded();
-               return channel;
-       }
-
-       /**
-        * Sets a new DataChannel for this context.
-        * 
-        * @since 3.1
-        */
-       public void setChannel(DataChannel channel) {
-               this.channel = channel;
-       }
-
-       @Override
-       public EntityResolver getEntityResolver() {
-               attachToRuntimeIfNeeded();
-               return entityResolver;
-       }
-
-       /**
-        * @since 3.1
-        */
-       public void setEntityResolver(EntityResolver entityResolver) {
-               this.entityResolver = entityResolver;
-       }
-
-       /**
-        * Returns whether this ObjectContext performs object validation before
-        * commit is executed.
-        * 
-        * @since 1.1
-        */
-       public boolean isValidatingObjectsOnCommit() {
-               return validatingObjectsOnCommit;
-       }
-
-       /**
-        * Sets the property defining whether this ObjectContext should perform
-        * object validation before commit is executed.
-        * 
-        * @since 1.1
-        */
-       public void setValidatingObjectsOnCommit(boolean flag) {
-               this.validatingObjectsOnCommit = flag;
-       }
-
-       /**
-        * @since 3.1
-        */
-       @Override
-       public <T extends Persistent> T localObject(T objectFromAnotherContext) 
{
-
-               if (objectFromAnotherContext == null) {
-                       throw new NullPointerException("Null object argument");
-               }
-
-               ObjectId id = objectFromAnotherContext.getObjectId();
-
-               // first look for the ID in the local GraphManager
-               synchronized (getGraphManager()) {
-                       @SuppressWarnings("unchecked")
-                       T localObject = (T) getGraphManager().getNode(id);
-                       if (localObject != null) {
-                               return localObject;
-                       }
-
-                       // create a hollow object, optimistically assuming that 
the ID we got from
-                       // 'objectFromAnotherContext' is a valid ID either in 
the parent context or in the DB.
-                       // This essentially defers possible 
FaultFailureExceptions.
-
-                       ClassDescriptor descriptor = 
getEntityResolver().getClassDescriptor(id.getEntityName());
-                       @SuppressWarnings("unchecked")
-                       T persistent = (T) descriptor.createObject();
-
-                       persistent.setObjectContext(this);
-                       persistent.setObjectId(id);
-                       persistent.setPersistenceState(PersistenceState.HOLLOW);
-
-                       getGraphManager().registerNode(id, persistent);
-
-                       return persistent;
-               }
-       }
-
-       @Override
-       public abstract GraphManager getGraphManager();
-
-       @Override
-       public abstract Collection<?> modifiedObjects();
-
-       @Override
-       public abstract <T> T newObject(Class<T> persistentClass);
-
-       @Override
-       public abstract void registerNewObject(Object object);
-
-       @Override
-       public abstract Collection<?> newObjects();
-
-       @Override
-       public abstract QueryResponse performGenericQuery(Query query);
-
-       @Override
-       public abstract List performQuery(Query query);
-
-       /**
-        * @since 4.0
-        */
-       @SuppressWarnings("unchecked")
-       @Override
-       public <T> List<T> select(Select<T> query) {
-               return performQuery(query);
-       }
-
-       /**
-        * @since 4.0
-        */
-       @Override
-       public <T> T selectOne(Select<T> query) {
-               List<T> objects = select(query);
-
-               if (objects.size() == 0) {
-                       return null;
-               } else if (objects.size() > 1) {
-                       throw new CayenneRuntimeException("Expected zero or one 
object, instead query matched: %d", objects.size());
-               }
-
-               return objects.get(0);
-       }
-
-       /**
-        * @since 4.0
-        */
-       @Override
-       public <T> T selectFirst(Select<T> query) {
-               List<T> objects = select(query);
-
-               return (objects == null || objects.isEmpty()) ? null : 
objects.get(0);
-       }
-
-       /**
-        * @since 4.0
-        */
-       @Override
-       public <T> void iterate(Select<T> query, ResultIteratorCallback<T> 
callback) {
-
-               try (ResultIterator<T> it = iterator(query);) {
-                       for (T t : it) {
-                               callback.next(t);
-                       }
-               }
-       }
-
-       @Override
-       public abstract <T> ResultIterator<T> iterator(Select<T> query);
-
-       @Override
-       public <T> ResultBatchIterator<T> batchIterator(Select<T> query, int 
size) {
-               return new ResultBatchIterator<T>(iterator(query), size);
-       }
-
-       @Override
-       public void prepareForAccess(Persistent object, String property, 
boolean lazyFaulting) {
-               if (object.getPersistenceState() == PersistenceState.HOLLOW) {
-
-                       ObjectId oid = object.getObjectId();
-                       List<?> objects = performQuery(new ObjectIdQuery(oid, 
false, ObjectIdQuery.CACHE));
-
-                       if (objects.size() == 0) {
-                               throw new FaultFailureException(
-                                               "Error resolving fault, no 
matching row exists in the database for ObjectId: " + oid);
-                       } else if (objects.size() > 1) {
-                               throw new FaultFailureException(
-                                               "Error resolving fault, more 
than one row exists in the database for ObjectId: " + oid);
-                       }
-
-                       // 5/28/2013 - Commented out this block to allow for 
modifying
-                       // objects in the postLoad callback
-                       // sanity check...
-                       // if (object.getPersistenceState() != 
PersistenceState.COMMITTED) {
-                       //
-                       // String state =
-                       // 
PersistenceState.persistenceStateName(object.getPersistenceState());
-                       //
-                       // // TODO: andrus 4/13/2006, modified and deleted 
states are
-                       // // possible due to
-                       // // a race condition, should we handle them here?
-                       // throw new
-                       // FaultFailureException("Error resolving fault for 
ObjectId: " +
-                       // oid + " and state (" + state
-                       // +
-                       // "). Possible cause - matching row is missing from 
the database.");
-                       // }
-               }
-
-               // resolve relationship fault
-               if (lazyFaulting && property != null) {
-                       ClassDescriptor classDescriptor = 
getEntityResolver().getClassDescriptor(
-                                       object.getObjectId().getEntityName());
-                       PropertyDescriptor propertyDescriptor = 
classDescriptor.getProperty(property);
-
-                       // If we don't have a property descriptor, there's not 
much we can
-                       // do.
-                       // Let the caller know that the specified property 
could not be
-                       // found and list
-                       // all of the properties that could be so the caller 
knows what can
-                       // be used.
-                       if (propertyDescriptor == null) {
-                               final StringBuilder errorMessage = new 
StringBuilder();
-
-                               errorMessage.append(String.format("Property 
'%s' is not declared for entity '%s'.", property, object
-                                               
.getObjectId().getEntityName()));
-
-                               errorMessage.append(" Declared properties are: 
");
-
-                               // Grab each of the declared properties.
-                               final List<String> properties = new 
ArrayList<>();
-                               classDescriptor.visitProperties(new 
PropertyVisitor() {
-                                       @Override
-                                       public boolean visitAttribute(final 
AttributeProperty property) {
-                                               
properties.add(property.getName());
-
-                                               return true;
-                                       }
-
-                                       @Override
-                                       public boolean visitToOne(final 
ToOneProperty property) {
-                                               
properties.add(property.getName());
-
-                                               return true;
-                                       }
-
-                                       @Override
-                                       public boolean visitToMany(final 
ToManyProperty property) {
-                                               
properties.add(property.getName());
-
-                                               return true;
-                                       }
-                               });
-
-                               // Now add the declared property names to the 
error message.
-                               boolean first = true;
-                               for (String declaredProperty : properties) {
-                                       if (first) {
-                                               
errorMessage.append(String.format("'%s'", declaredProperty));
-
-                                               first = false;
-                                       } else {
-                                               
errorMessage.append(String.format(", '%s'", declaredProperty));
-                                       }
-                               }
-
-                               errorMessage.append(".");
-
-                               throw new 
CayenneRuntimeException(errorMessage.toString());
-                       }
-
-                       // this should trigger fault resolving
-                       propertyDescriptor.readProperty(object);
-               }
-       }
-
-       @Override
-       public void propertyChanged(Persistent object, String property, Object 
oldValue, Object newValue) {
-               graphAction.handlePropertyChange(object, property, oldValue, 
newValue);
-       }
-
-       @Override
-       public abstract void rollbackChanges();
-
-       @Override
-       public abstract void rollbackChangesLocally();
-
-       @Override
-       public abstract Collection<?> uncommittedObjects();
-
-       public QueryCache getQueryCache() {
-               attachToRuntimeIfNeeded();
-               return queryCache;
-       }
-
-       /**
-        * Sets a QueryCache to be used for storing cached query results.
-        */
-       public void setQueryCache(QueryCache queryCache) {
-               this.queryCache = queryCache;
-       }
-
-       /**
-        * Returns EventManager associated with the ObjectStore.
-        * 
-        * @since 1.2
-        */
-       @Override
-       public EventManager getEventManager() {
-               return channel != null ? channel.getEventManager() : null;
-       }
-
-       @Override
-       public GraphDiff onSync(ObjectContext originatingContext, GraphDiff 
changes, int syncType) {
-               switch (syncType) {
-               case DataChannel.ROLLBACK_CASCADE_SYNC:
-                       return onContextRollback(originatingContext);
-               case DataChannel.FLUSH_NOCASCADE_SYNC:
-                       return onContextFlush(originatingContext, changes, 
false);
-               case DataChannel.FLUSH_CASCADE_SYNC:
-                       return onContextFlush(originatingContext, changes, 
true);
-               default:
-                       throw new CayenneRuntimeException("Unrecognized 
SyncMessage type: %d", syncType);
-               }
-       }
-
-       GraphDiff onContextRollback(ObjectContext originatingContext) {
-               rollbackChanges();
-               return new CompoundDiff();
-       }
-
-       protected abstract GraphDiff onContextFlush(ObjectContext 
originatingContext, GraphDiff changes, boolean cascade);
-
-       /**
-        * @since 1.2
-        */
-       protected void fireDataChannelCommitted(Object postedBy, GraphDiff 
changes) {
-               EventManager manager = getEventManager();
-
-               if (manager != null) {
-                       GraphEvent e = new GraphEvent(this, postedBy, changes);
-                       manager.postEvent(e, DataChannel.GRAPH_FLUSHED_SUBJECT);
-               }
-       }
-
-       /**
-        * @since 1.2
-        */
-       protected void fireDataChannelRolledback(Object postedBy, GraphDiff 
changes) {
-               EventManager manager = getEventManager();
-
-               if (manager != null) {
-                       GraphEvent e = new GraphEvent(this, postedBy, changes);
-                       manager.postEvent(e, 
DataChannel.GRAPH_ROLLEDBACK_SUBJECT);
-               }
-       }
-
-       /**
-        * @since 1.2
-        */
-       protected void fireDataChannelChanged(Object postedBy, GraphDiff 
changes) {
-               EventManager manager = getEventManager();
-
-               if (manager != null) {
-                       GraphEvent e = new GraphEvent(this, postedBy, changes);
-                       manager.postEvent(e, DataChannel.GRAPH_CHANGED_SUBJECT);
-               }
-       }
-
-       @Override
-       public void invalidateObjects(Collection<?> objects) {
-
-               // don't allow null collections as a matter of coding discipline
-               if (objects == null) {
-                       throw new NullPointerException("Null collection of 
objects to invalidate");
-               }
-
-               if (!objects.isEmpty()) {
-                       performGenericQuery(new RefreshQuery(objects));
-               }
-       }
-
-       /**
-        * @since 3.1
-        */
-       @Override
-       public <T> void invalidateObjects(T... objects) {
-               if (objects != null && objects.length > 0) {
-                       performGenericQuery(new 
RefreshQuery(Arrays.asList(objects)));
-               }
-       }
-
-       /**
-        * Returns a map of user-defined properties associated with this
-        * DataContext.
-        * 
-        * @since 3.0
-        */
-       protected Map<String, Object> getUserProperties() {
-
-               // as not all users will take advantage of properties, creating 
the
-               // map on demand to keep the context lean...
-               if (userProperties == null) {
-                       synchronized (this) {
-                               if (userProperties == null) {
-                                       userProperties = new 
ConcurrentHashMap<>();
-                               }
-                       }
-               }
-
-               return userProperties;
-       }
-
-       /**
-        * Returns a user-defined property previously set via 'setUserProperty'.
-        * Note that it is a caller responsibility to synchronize access to
-        * properties.
-        * 
-        * @since 3.0
-        */
-       @Override
-       public Object getUserProperty(String key) {
-               return getUserProperties().get(key);
-       }
-
-       /**
-        * Sets a user-defined property. Note that it is a caller 
responsibility to
-        * synchronize access to properties.
-        * 
-        * @since 3.0
-        */
-       @Override
-       public void setUserProperty(String key, Object value) {
-               getUserProperties().put(key, value);
-       }
-
-       /**
-        * {@inheritDoc}
-        *
-        * @since 5.0
-        */
-       @Override
-       public void removeUserProperty(String key) {
-               getUserProperties().remove(key);
-       }
-
-       /**
-        * {@inheritDoc}
-        *
-        * @since 5.0
-        */
-       @Override
-       public void clearUserProperties() {
-               getUserProperties().clear();
-       }
-
-       /**
-        * If ObjEntity qualifier is set, asks it to inject initial value to an
-        * object. Also performs all Persistent initialization operations
-        */
-       protected void injectInitialValue(Object obj) {
-               // must follow this exact order of property initialization per 
CAY-653,
-               // i.e. have
-               // the id and the context in place BEFORE setPersistence is 
called
-
-               Persistent object = (Persistent) obj;
-
-               object.setObjectContext(this);
-               object.setPersistenceState(PersistenceState.NEW);
-
-               GraphManager graphManager = getGraphManager();
-               synchronized (graphManager) {
-                       graphManager.registerNode(object.getObjectId(), object);
-                       graphManager.nodeCreated(object.getObjectId());
-               }
-
-               ObjEntity entity;
-               try {
-                       entity = 
getEntityResolver().getObjEntity(object.getClass());
-               } catch (CayenneRuntimeException ex) {
-                       // ObjEntity cannot be fetched, ignored
-                       entity = null;
-               }
-
-               if (entity != null) {
-                       if (entity.getDeclaredQualifier() instanceof 
ValueInjector) {
-                               ((ValueInjector) 
entity.getDeclaredQualifier()).injectValue(object);
-                       }
-               }
-
-               // invoke callbacks
-               
getEntityResolver().getCallbackRegistry().performCallbacks(LifecycleEvent.POST_ADD,
 object);
-       }
-
-       /**
-        * @since 3.1
-        */
-       @Override
-       public <T> void deleteObjects(T... objects) throws DeleteDenyException {
-               if (objects == null || objects.length == 0) {
-                       return;
-               }
-
-               ObjectContextDeleteAction action = new 
ObjectContextDeleteAction(this);
-
-               for (Object object : objects) {
-                       action.performDelete((Persistent) object);
-               }
-       }
-
-       @Override
-       public void deleteObjects(Collection<?> objects) throws 
DeleteDenyException {
-               if (objects.isEmpty()) {
-                       return;
-               }
-
-               ObjectContextDeleteAction action = new 
ObjectContextDeleteAction(this);
-
-               // Make a copy to iterate over to avoid 
ConcurrentModificationException.
-               List<Object> copy = new ArrayList<>(objects);
-               for (Object object : copy) {
-                       action.performDelete((Persistent) object);
-               }
        }
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java 
b/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
index 4206edfd5..6cf895aa2 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
@@ -23,48 +23,59 @@ import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
-import org.apache.cayenne.BaseContext;
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.DataObject;
 import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DeleteDenyException;
 import org.apache.cayenne.Fault;
+import org.apache.cayenne.FaultFailureException;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.PersistenceState;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.ResultBatchIterator;
 import org.apache.cayenne.ResultIterator;
-import org.apache.cayenne.access.util.IteratedSelectObserver;
+import org.apache.cayenne.ResultIteratorCallback;
+import org.apache.cayenne.cache.NestedQueryCache;
+import org.apache.cayenne.cache.QueryCache;
 import org.apache.cayenne.di.Injector;
 import org.apache.cayenne.event.EventManager;
+import org.apache.cayenne.exp.ValueInjector;
 import org.apache.cayenne.graph.ArcId;
 import org.apache.cayenne.graph.ChildDiffLoader;
 import org.apache.cayenne.graph.CompoundDiff;
 import org.apache.cayenne.graph.GraphDiff;
+import org.apache.cayenne.graph.GraphEvent;
 import org.apache.cayenne.graph.GraphManager;
 import org.apache.cayenne.map.DbJoin;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.LifecycleEvent;
 import org.apache.cayenne.map.ObjAttribute;
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.map.ObjRelationship;
 import org.apache.cayenne.query.*;
 import org.apache.cayenne.reflect.AttributeProperty;
 import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyDescriptor;
 import org.apache.cayenne.reflect.PropertyVisitor;
 import org.apache.cayenne.reflect.ToManyProperty;
 import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.tx.BaseTransaction;
-import org.apache.cayenne.tx.Transaction;
+import org.apache.cayenne.runtime.CayenneRuntime;
 import org.apache.cayenne.tx.TransactionFactory;
 import org.apache.cayenne.util.EventUtil;
 import org.apache.cayenne.util.GenericResponse;
+import org.apache.cayenne.util.ObjectContextGraphAction;
 import org.apache.cayenne.util.Util;
 
 /**
@@ -73,12 +84,68 @@ import org.apache.cayenne.util.Util;
  * changes to persistent objects that are registered with the context, are not
  * visible to the users of other contexts.
  */
-public class DataContext extends BaseContext {
+public class DataContext implements ObjectContext {
+
+    /**
+     * A holder of a ObjectContext bound to the current thread.
+     *
+     * @since 3.0
+     */
+    protected static final ThreadLocal<ObjectContext> threadObjectContext = 
new ThreadLocal<ObjectContext>();
+
+    /**
+     * Returns the ObjectContext bound to the current thread.
+     *
+     * @since 3.0
+     * @return the ObjectContext associated with caller thread.
+     * @throws IllegalStateException
+     *             if there is no ObjectContext bound to the current thread.
+     */
+    public static ObjectContext getThreadObjectContext() throws 
IllegalStateException {
+        ObjectContext context = threadObjectContext.get();
+        if (context == null) {
+            throw new IllegalStateException("Current thread has no bound 
ObjectContext.");
+        }
+
+        return context;
+    }
+
+    /**
+     * Binds a ObjectContext to the current thread. ObjectContext can later be
+     * retrieved by users in the same thread by calling
+     * {@link #getThreadObjectContext}. Using null parameter will
+     * unbind currently bound ObjectContext.
+     *
+     * @since 3.0
+     */
+    public static void bindThreadObjectContext(ObjectContext context) {
+        threadObjectContext.set(context);
+    }
+
 
     private DataContextDelegate delegate;
-    protected boolean usingSharedSnaphsotCache;
+    protected boolean usingSharedSnapshotCache;
     protected ObjectStore objectStore;
 
+    /**
+     * Graph action that handles property changes
+     *
+     * @since 3.1
+     */
+    protected ObjectContextGraphAction graphAction;
+
+    /**
+     * Stores user defined properties associated with this DataContext.
+     *
+     * @since 3.0
+     */
+    protected volatile Map<String, Object> userProperties;
+
+    // transient variables that should be reinitialized on deserialization 
from the registry
+    protected transient DataChannel channel;
+    protected transient QueryCache queryCache;
+    protected transient EntityResolver entityResolver;
+
     /**
      * @deprecated since 4.0 used in a method that itself should be deprecated,
      *             so this is a temp code
@@ -88,6 +155,8 @@ public class DataContext extends BaseContext {
 
     protected transient DataContextMergeHandler mergeHandler;
 
+    protected boolean validatingObjectsOnCommit = true;
+
     /**
      * Creates a new DataContext that is not attached to the Cayenne stack.
      */
@@ -102,6 +171,8 @@ public class DataContext extends BaseContext {
      */
     public DataContext(DataChannel channel, ObjectStore objectStore) {
 
+        graphAction = new ObjectContextGraphAction(this);
+
         // inject self as parent context
         if (objectStore != null) {
             this.objectStore = objectStore;
@@ -114,24 +185,66 @@ public class DataContext extends BaseContext {
 
         if (objectStore != null) {
             DataDomain domain = getParentDataDomain();
-            this.usingSharedSnaphsotCache = domain != null
+            this.usingSharedSnapshotCache = domain != null
                     && objectStore.getDataRowCache() == 
domain.getSharedSnapshotCache();
         }
     }
 
-    @Override
+    /**
+     * Checks whether this context is attached to Cayenne runtime stack and if
+     * not, attempts to attach itself to the runtime using Injector returned
+     * from the call to {@link CayenneRuntime#getThreadInjector()}. If thread
+     * Injector is not available and the context is not attached, throws
+     * CayenneRuntimeException.
+     * <p>
+     * This method is called internally by the context before access to
+     * transient variables to allow the context to attach to the stack lazily
+     * following deserialization.
+     *
+     * @return true if the context successfully attached to the thread runtime,
+     *         false - if it was already attached.
+     * @since 3.1
+     */
+    protected boolean attachToRuntimeIfNeeded() {
+        if (channel != null) {
+            return false;
+        }
+
+        Injector injector = CayenneRuntime.getThreadInjector();
+        if (injector == null) {
+            throw new CayenneRuntimeException("Can't attach to Cayenne 
runtime. "
+                    + "Null injector returned from 
CayenneRuntime.getThreadInjector()");
+        }
+
+        attachToRuntime(injector);
+        return true;
+    }
+
+    /**
+     * Attaches this context to the CayenneRuntime whose Injector is passed as
+     * an argument to this method.
+     *
+     * @since 3.1
+     */
     protected void attachToRuntime(Injector injector) {
-        super.attachToRuntime(injector);
+        attachToChannel(injector.getInstance(DataChannel.class));
+        setQueryCache(new 
NestedQueryCache(injector.getInstance(QueryCache.class)));
         this.transactionFactory = 
injector.getInstance(TransactionFactory.class);
     }
 
     /**
+     * Attaches to a provided DataChannel.
+     *
      * @since 3.1
      */
-    @Override
     protected void attachToChannel(DataChannel channel) {
 
-        super.attachToChannel(channel);
+        if (channel == null) {
+            throw new NullPointerException("Null channel");
+        }
+
+        setChannel(channel);
+        setEntityResolver(channel.getEntityResolver());
 
         if (mergeHandler != null) {
             mergeHandler.setActive(false);
@@ -150,7 +263,7 @@ public class DataContext extends BaseContext {
             EventUtil.listenForChannelEvents(channel, mergeHandler);
         }
 
-        if (!usingSharedSnaphsotCache && getObjectStore() != null) {
+        if (!usingSharedSnapshotCache && getObjectStore() != null) {
             DataRowStore cache = getObjectStore().getDataRowCache();
 
             if (cache != null) {
@@ -159,6 +272,35 @@ public class DataContext extends BaseContext {
         }
     }
 
+    @Override
+    public DataChannel getChannel() {
+        attachToRuntimeIfNeeded();
+        return channel;
+    }
+
+    /**
+     * Sets a new DataChannel for this context.
+     *
+     * @since 3.1
+     */
+    public void setChannel(DataChannel channel) {
+        this.channel = channel;
+    }
+
+    @Override
+    public EntityResolver getEntityResolver() {
+        attachToRuntimeIfNeeded();
+        return entityResolver;
+    }
+
+    /**
+     * @since 3.1
+     */
+    public void setEntityResolver(EntityResolver entityResolver) {
+        this.entityResolver = entityResolver;
+    }
+
+
     /**
      * Returns a DataDomain used by this DataContext. DataDomain is looked up 
in
      * the DataChannel hierarchy. If a channel is not a DataDomain or a
@@ -251,6 +393,42 @@ public class DataContext extends BaseContext {
         return getObjectStore().objectsInState(PersistenceState.DELETED);
     }
 
+    @Override
+    public void deleteObject(Object object) throws DeleteDenyException {
+        deleteObjects(object);
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public <T> void deleteObjects(T... objects) throws DeleteDenyException {
+        if (objects == null || objects.length == 0) {
+            return;
+        }
+
+        ObjectContextDeleteAction action = new ObjectContextDeleteAction(this);
+
+        for (Object object : objects) {
+            action.performDelete((Persistent) object);
+        }
+    }
+
+    @Override
+    public void deleteObjects(Collection<?> objects) throws 
DeleteDenyException {
+        if (objects.isEmpty()) {
+            return;
+        }
+
+        ObjectContextDeleteAction action = new ObjectContextDeleteAction(this);
+
+        // Make a copy to iterate over to avoid 
ConcurrentModificationException.
+        List<Object> copy = new ArrayList<>(objects);
+        for (Object object : copy) {
+            action.performDelete((Persistent) object);
+        }
+    }
+
     /**
      * Returns a list of objects that are registered with this DataContext and
      * have a state PersistenceState.MODIFIED
@@ -290,6 +468,90 @@ public class DataContext extends BaseContext {
         return objects;
     }
 
+    public QueryCache getQueryCache() {
+        attachToRuntimeIfNeeded();
+        return queryCache;
+    }
+
+    /**
+     * Sets a QueryCache to be used for storing cached query results.
+     */
+    public void setQueryCache(QueryCache queryCache) {
+        this.queryCache = queryCache;
+    }
+
+    /**
+     * Returns EventManager associated with the ObjectStore.
+     *
+     * @since 1.2
+     */
+    @Override
+    public EventManager getEventManager() {
+        return channel != null ? channel.getEventManager() : null;
+    }
+
+    /**
+     * @since 1.2
+     */
+    protected void fireDataChannelCommitted(Object postedBy, GraphDiff 
changes) {
+        EventManager manager = getEventManager();
+
+        if (manager != null) {
+            GraphEvent e = new GraphEvent(this, postedBy, changes);
+            manager.postEvent(e, DataChannel.GRAPH_FLUSHED_SUBJECT);
+        }
+    }
+
+    /**
+     * @since 1.2
+     */
+    protected void fireDataChannelRolledback(Object postedBy, GraphDiff 
changes) {
+        EventManager manager = getEventManager();
+
+        if (manager != null) {
+            GraphEvent e = new GraphEvent(this, postedBy, changes);
+            manager.postEvent(e, DataChannel.GRAPH_ROLLEDBACK_SUBJECT);
+        }
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public <T extends Persistent> T localObject(T objectFromAnotherContext) {
+
+        if (objectFromAnotherContext == null) {
+            throw new NullPointerException("Null object argument");
+        }
+
+        ObjectId id = objectFromAnotherContext.getObjectId();
+
+        // first look for the ID in the local GraphManager
+        synchronized (getGraphManager()) {
+            @SuppressWarnings("unchecked")
+            T localObject = (T) getGraphManager().getNode(id);
+            if (localObject != null) {
+                return localObject;
+            }
+
+            // create a hollow object, optimistically assuming that the ID we 
got from
+            // 'objectFromAnotherContext' is a valid ID either in the parent 
context or in the DB.
+            // This essentially defers possible FaultFailureExceptions.
+
+            ClassDescriptor descriptor = 
getEntityResolver().getClassDescriptor(id.getEntityName());
+            @SuppressWarnings("unchecked")
+            T persistent = (T) descriptor.createObject();
+
+            persistent.setObjectContext(this);
+            persistent.setObjectId(id);
+            persistent.setPersistenceState(PersistenceState.HOLLOW);
+
+            getGraphManager().registerNode(id, persistent);
+
+            return persistent;
+        }
+    }
+
     /**
      * Returns a DataRow reflecting current, possibly uncommitted, object 
state.
      * <p>
@@ -399,6 +661,107 @@ public class DataContext extends BaseContext {
         return snapshot;
     }
 
+    @Override
+    public void prepareForAccess(Persistent object, String property, boolean 
lazyFaulting) {
+        if (object.getPersistenceState() == PersistenceState.HOLLOW) {
+
+            ObjectId oid = object.getObjectId();
+            List<?> objects = performQuery(new ObjectIdQuery(oid, false, 
ObjectIdQuery.CACHE));
+
+            if (objects.size() == 0) {
+                throw new FaultFailureException(
+                        "Error resolving fault, no matching row exists in the 
database for ObjectId: " + oid);
+            } else if (objects.size() > 1) {
+                throw new FaultFailureException(
+                        "Error resolving fault, more than one row exists in 
the database for ObjectId: " + oid);
+            }
+
+            // 5/28/2013 - Commented out this block to allow for modifying
+            // objects in the postLoad callback
+            // sanity check...
+            // if (object.getPersistenceState() != PersistenceState.COMMITTED) 
{
+            //
+            // String state =
+            // 
PersistenceState.persistenceStateName(object.getPersistenceState());
+            //
+            // // TODO: andrus 4/13/2006, modified and deleted states are
+            // // possible due to
+            // // a race condition, should we handle them here?
+            // throw new
+            // FaultFailureException("Error resolving fault for ObjectId: " +
+            // oid + " and state (" + state
+            // +
+            // "). Possible cause - matching row is missing from the 
database.");
+            // }
+        }
+
+        // resolve relationship fault
+        if (lazyFaulting && property != null) {
+            ClassDescriptor classDescriptor = 
getEntityResolver().getClassDescriptor(
+                    object.getObjectId().getEntityName());
+            PropertyDescriptor propertyDescriptor = 
classDescriptor.getProperty(property);
+
+            // If we don't have a property descriptor, there's not much we can
+            // do.
+            // Let the caller know that the specified property could not be
+            // found and list
+            // all of the properties that could be so the caller knows what can
+            // be used.
+            if (propertyDescriptor == null) {
+                final StringBuilder errorMessage = new StringBuilder();
+
+                errorMessage.append(String.format("Property '%s' is not 
declared for entity '%s'.", property, object
+                        .getObjectId().getEntityName()));
+
+                errorMessage.append(" Declared properties are: ");
+
+                // Grab each of the declared properties.
+                final List<String> properties = new ArrayList<>();
+                classDescriptor.visitProperties(new PropertyVisitor() {
+                    @Override
+                    public boolean visitAttribute(final AttributeProperty 
property) {
+                        properties.add(property.getName());
+
+                        return true;
+                    }
+
+                    @Override
+                    public boolean visitToOne(final ToOneProperty property) {
+                        properties.add(property.getName());
+
+                        return true;
+                    }
+
+                    @Override
+                    public boolean visitToMany(final ToManyProperty property) {
+                        properties.add(property.getName());
+
+                        return true;
+                    }
+                });
+
+                // Now add the declared property names to the error message.
+                boolean first = true;
+                for (String declaredProperty : properties) {
+                    if (first) {
+                        errorMessage.append(String.format("'%s'", 
declaredProperty));
+
+                        first = false;
+                    } else {
+                        errorMessage.append(String.format(", '%s'", 
declaredProperty));
+                    }
+                }
+
+                errorMessage.append(".");
+
+                throw new CayenneRuntimeException(errorMessage.toString());
+            }
+
+            // this should trigger fault resolving
+            propertyDescriptor.readProperty(object);
+        }
+    }
+
     /**
      * Converts a list of DataRows to a List of DataObject registered with this
      * DataContext.
@@ -604,6 +967,44 @@ public class DataContext extends BaseContext {
         });
     }
 
+    /**
+     * If ObjEntity qualifier is set, asks it to inject initial value to an
+     * object. Also performs all Persistent initialization operations
+     */
+    protected void injectInitialValue(Object obj) {
+        // must follow this exact order of property initialization per CAY-653,
+        // i.e. have
+        // the id and the context in place BEFORE setPersistence is called
+
+        Persistent object = (Persistent) obj;
+
+        object.setObjectContext(this);
+        object.setPersistenceState(PersistenceState.NEW);
+
+        GraphManager graphManager = getGraphManager();
+        synchronized (graphManager) {
+            graphManager.registerNode(object.getObjectId(), object);
+            graphManager.nodeCreated(object.getObjectId());
+        }
+
+        ObjEntity entity;
+        try {
+            entity = getEntityResolver().getObjEntity(object.getClass());
+        } catch (CayenneRuntimeException ex) {
+            // ObjEntity cannot be fetched, ignored
+            entity = null;
+        }
+
+        if (entity != null) {
+            if (entity.getDeclaredQualifier() instanceof ValueInjector) {
+                ((ValueInjector) 
entity.getDeclaredQualifier()).injectValue(object);
+            }
+        }
+
+        // invoke callbacks
+        
getEntityResolver().getCallbackRegistry().performCallbacks(LifecycleEvent.POST_ADD,
 object);
+    }
+
     /**
      * Unregisters a Collection of DataObjects from the DataContext and the
      * underlying ObjectStore. This operation also unsets DataContext for
@@ -615,6 +1016,34 @@ public class DataContext extends BaseContext {
         getObjectStore().objectsUnregistered(dataObjects);
     }
 
+    @Override
+    public void invalidateObjects(Collection<?> objects) {
+
+        // don't allow null collections as a matter of coding discipline
+        if (objects == null) {
+            throw new NullPointerException("Null collection of objects to 
invalidate");
+        }
+
+        if (!objects.isEmpty()) {
+            performGenericQuery(new RefreshQuery(objects));
+        }
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public <T> void invalidateObjects(T... objects) {
+        if (objects != null && objects.length > 0) {
+            performGenericQuery(new RefreshQuery(Arrays.asList(objects)));
+        }
+    }
+
+    @Override
+    public void propertyChanged(Persistent object, String property, Object 
oldValue, Object newValue) {
+        graphAction.handlePropertyChange(object, property, oldValue, newValue);
+    }
+
     /**
      * If the parent channel is a DataContext, reverts local changes to make
      * this context look like the parent, if the parent channel is a 
DataDomain,
@@ -685,7 +1114,6 @@ public class DataContext extends BaseContext {
         flushToParent(true);
     }
 
-    @Override
     protected GraphDiff onContextFlush(ObjectContext originatingContext, 
GraphDiff changes, boolean cascade) {
 
         boolean childContext = this != originatingContext && changes != null;
@@ -792,6 +1220,54 @@ public class DataContext extends BaseContext {
 
     }
 
+    /**
+     * @since 4.0
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> List<T> select(Select<T> query) {
+        return performQuery(query);
+    }
+
+    /**
+     * @since 4.0
+     */
+    @Override
+    public <T> T selectOne(Select<T> query) {
+        List<T> objects = select(query);
+
+        if (objects.size() == 0) {
+            return null;
+        } else if (objects.size() > 1) {
+            throw new CayenneRuntimeException("Expected zero or one object, 
instead query matched: %d", objects.size());
+        }
+
+        return objects.get(0);
+    }
+
+    /**
+     * @since 4.0
+     */
+    @Override
+    public <T> T selectFirst(Select<T> query) {
+        List<T> objects = select(query);
+
+        return (objects == null || objects.isEmpty()) ? null : objects.get(0);
+    }
+
+    /**
+     * @since 4.0
+     */
+    @Override
+    public <T> void iterate(Select<T> query, ResultIteratorCallback<T> 
callback) {
+
+        try (ResultIterator<T> it = iterator(query);) {
+            for (T t : it) {
+                callback.next(t);
+            }
+        }
+    }
+
     /**
      * Performs a single database select query returning result as a {@link 
ResultIterator}.
      * <p>
@@ -894,6 +1370,25 @@ public class DataContext extends BaseContext {
         return new DataContextQueryAction(this, context, query).execute();
     }
 
+    @Override
+    public GraphDiff onSync(ObjectContext originatingContext, GraphDiff 
changes, int syncType) {
+        switch (syncType) {
+            case DataChannel.ROLLBACK_CASCADE_SYNC:
+                return onContextRollback();
+            case DataChannel.FLUSH_NOCASCADE_SYNC:
+                return onContextFlush(originatingContext, changes, false);
+            case DataChannel.FLUSH_CASCADE_SYNC:
+                return onContextFlush(originatingContext, changes, true);
+            default:
+                throw new CayenneRuntimeException("Unrecognized SyncMessage 
type: %d", syncType);
+        }
+    }
+
+    GraphDiff onContextRollback() {
+        rollbackChanges();
+        return new CompoundDiff();
+    }
+
     /**
      * Performs a single database query that does not select rows. Returns an
      * array of update counts.
@@ -941,7 +1436,7 @@ public class DataContext extends BaseContext {
      * @since 1.1
      */
     public List<?> performQuery(String queryName, boolean expireCachedLists) {
-        return performQuery(queryName, Collections.EMPTY_MAP, 
expireCachedLists);
+        return performQuery(queryName, Collections.emptyMap(), 
expireCachedLists);
     }
 
     /**
@@ -974,14 +1469,14 @@ public class DataContext extends BaseContext {
      * @since 1.1
      */
     public boolean isUsingSharedSnapshotCache() {
-        return usingSharedSnaphsotCache;
+        return usingSharedSnapshotCache;
     }
 
     /**
      * @since 3.1
      */
     public void setUsingSharedSnapshotCache(boolean flag) {
-        this.usingSharedSnaphsotCache = flag;
+        this.usingSharedSnapshotCache = flag;
     }
 
     // ---------------------------------------------
@@ -1103,11 +1598,16 @@ public class DataContext extends BaseContext {
 
     }
 
-    // this completely meaningless override is needed to expose the method as
-    // package-private ... is there a better way?
-    @Override
+    /**
+     * @since 1.2
+     */
     protected void fireDataChannelChanged(Object postedBy, GraphDiff changes) {
-        super.fireDataChannelChanged(postedBy, changes);
+        EventManager manager = getEventManager();
+
+        if (manager != null) {
+            GraphEvent e = new GraphEvent(this, postedBy, changes);
+            manager.postEvent(e, DataChannel.GRAPH_CHANGED_SUBJECT);
+        }
     }
 
     TransactionFactory getTransactionFactory() {
@@ -1125,4 +1625,93 @@ public class DataContext extends BaseContext {
         this.transactionFactory = transactionFactory;
     }
 
+    @Override
+    public <T> ResultBatchIterator<T> batchIterator(Select<T> query, int size) 
{
+        return new ResultBatchIterator<T>(iterator(query), size);
+    }
+
+    /**
+     * Returns whether this ObjectContext performs object validation before
+     * commit is executed.
+     *
+     * @since 1.1
+     */
+    public boolean isValidatingObjectsOnCommit() {
+        return validatingObjectsOnCommit;
+    }
+
+    /**
+     * Sets the property defining whether this ObjectContext should perform
+     * object validation before commit is executed.
+     *
+     * @since 1.1
+     */
+    public void setValidatingObjectsOnCommit(boolean flag) {
+        this.validatingObjectsOnCommit = flag;
+    }
+
+    /**
+     * Returns a map of user-defined properties associated with this
+     * DataContext.
+     *
+     * @since 3.0
+     */
+    protected Map<String, Object> getUserProperties() {
+
+        // as not all users will take advantage of properties, creating the
+        // map on demand to keep the context lean...
+        if (userProperties == null) {
+            synchronized (this) {
+                if (userProperties == null) {
+                    userProperties = new ConcurrentHashMap<>();
+                }
+            }
+        }
+
+        return userProperties;
+    }
+
+    /**
+     * Returns a user-defined property previously set via 'setUserProperty'.
+     * Note that it is a caller responsibility to synchronize access to
+     * properties.
+     *
+     * @since 3.0
+     */
+    @Override
+    public Object getUserProperty(String key) {
+        return getUserProperties().get(key);
+    }
+
+    /**
+     * Sets a user-defined property. Note that it is a caller responsibility to
+     * synchronize access to properties.
+     *
+     * @since 3.0
+     */
+    @Override
+    public void setUserProperty(String key, Object value) {
+        getUserProperties().put(key, value);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @since 5.0
+     */
+    @Override
+    public void removeUserProperty(String key) {
+        getUserProperties().remove(key);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @since 5.0
+     */
+    @Override
+    public void clearUserProperties() {
+        getUserProperties().clear();
+    }
+
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/ObjectContextDeleteAction.java 
b/cayenne/src/main/java/org/apache/cayenne/access/ObjectContextDeleteAction.java
similarity index 97%
rename from 
cayenne/src/main/java/org/apache/cayenne/ObjectContextDeleteAction.java
rename to 
cayenne/src/main/java/org/apache/cayenne/access/ObjectContextDeleteAction.java
index e017c9660..430586c58 100644
--- a/cayenne/src/main/java/org/apache/cayenne/ObjectContextDeleteAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/ObjectContextDeleteAction.java
@@ -17,13 +17,18 @@
  *  under the License.
  ****************************************************************/
 
-package org.apache.cayenne;
+package org.apache.cayenne.access;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DeleteDenyException;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
 import org.apache.cayenne.graph.ArcId;
 import org.apache.cayenne.map.DeleteRule;
 import org.apache.cayenne.map.LifecycleEvent;
diff --git a/cayenne/src/test/java/org/apache/cayenne/MockBaseContext.java 
b/cayenne/src/test/java/org/apache/cayenne/MockBaseContext.java
deleted file mode 100644
index 30ef33769..000000000
--- a/cayenne/src/test/java/org/apache/cayenne/MockBaseContext.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-package org.apache.cayenne;
-
-import org.apache.cayenne.graph.GraphDiff;
-import org.apache.cayenne.graph.GraphManager;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.Select;
-
-import java.util.Collection;
-import java.util.List;
-
-public class MockBaseContext extends BaseContext {
-
-    @Override
-    public void commitChanges() {
-    }
-
-    @Override
-    public void commitChangesToParent() {
-    }
-
-    @Override
-    public Collection<?> deletedObjects() {
-        return null;
-    }
-
-    @Override
-    public EntityResolver getEntityResolver() {
-        return null;
-    }
-
-    @Override
-    public GraphManager getGraphManager() {
-        return null;
-    }
-
-    @Override
-    public Collection<?> modifiedObjects() {
-        return null;
-    }
-
-    @Override
-    public <T> T newObject(Class<T> persistentClass) {
-        return null;
-    }
-
-    @Override
-    public Collection<?> newObjects() {
-        return null;
-    }
-
-    @Override
-    protected GraphDiff onContextFlush(ObjectContext originatingContext, 
GraphDiff changes, boolean cascade) {
-        return null;
-    }
-
-    @Override
-    public QueryResponse performGenericQuery(Query query) {
-        return null;
-    }
-
-    @Override
-    public List performQuery(Query query) {
-        return null;
-    }
-
-    @Override
-    public void registerNewObject(Object object) {
-    }
-
-    @Override
-    public void rollbackChanges() {
-    }
-
-    @Override
-    public void rollbackChangesLocally() {
-    }
-
-    @Override
-    public Collection<?> uncommittedObjects() {
-        return null;
-    }
-
-    public ObjectContext createChildContext() {
-        return null;
-    }
-
-    public boolean hasChanges() {
-        return false;
-    }
-
-    public QueryResponse onQuery(ObjectContext originatingContext, Query 
query) {
-        return null;
-    }
-
-    @Override
-    public <T> ResultIterator<T> iterator(Select<T> query) {
-        return null;
-    }
-
-    @Override
-    public <T> ResultBatchIterator<T> batchIterator(Select<T> query, int size) 
{
-        return null;
-    }
-
-}
diff --git a/cayenne/src/test/java/org/apache/cayenne/BaseContextTest.java 
b/cayenne/src/test/java/org/apache/cayenne/access/DataContextTest.java
similarity index 83%
rename from cayenne/src/test/java/org/apache/cayenne/BaseContextTest.java
rename to cayenne/src/test/java/org/apache/cayenne/access/DataContextTest.java
index d53e356e7..1df21d649 100644
--- a/cayenne/src/test/java/org/apache/cayenne/BaseContextTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextTest.java
@@ -1,93 +1,98 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-package org.apache.cayenne;
-
-import org.apache.cayenne.cache.QueryCache;
-import org.apache.cayenne.runtime.CayenneRuntime;
-import org.apache.cayenne.di.DIBootstrap;
-import org.apache.cayenne.di.Injector;
-import org.apache.cayenne.di.Module;
-import org.junit.Test;
-
-import java.util.Map;
-
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.mock;
-
-public class BaseContextTest {
-
-    @Test
-    public void testUserPropertiesLazyInit() {
-        BaseContext context = new MockBaseContext();
-        assertNull(context.userProperties);
-
-        Map<String, Object> properties = context.getUserProperties();
-        assertNotNull(properties);
-        assertSame(properties, context.getUserProperties());
-    }
-
-    @Test
-    public void testAttachToRuntimeIfNeeded() {
-
-        final DataChannel channel = mock(DataChannel.class);
-        final QueryCache cache = mock(QueryCache.class);
-
-        Module testModule = binder -> {
-            binder.bind(DataChannel.class).toInstance(channel);
-            binder.bind(QueryCache.class).toInstance(cache);
-        };
-
-        Injector injector = DIBootstrap.createInjector(testModule);
-
-        BaseContext context = new MockBaseContext();
-        assertNull(context.channel);
-        assertNull(context.queryCache);
-
-        Injector oldInjector = CayenneRuntime.getThreadInjector();
-        try {
-
-            CayenneRuntime.bindThreadInjector(injector);
-
-            assertTrue(context.attachToRuntimeIfNeeded());
-            assertSame(channel, context.channel);
-
-            assertFalse(context.attachToRuntimeIfNeeded());
-            assertFalse(context.attachToRuntimeIfNeeded());
-        }
-        finally {
-            CayenneRuntime.bindThreadInjector(oldInjector);
-        }
-    }
-
-    @Test
-    public void testAttachToRuntimeIfNeeded_NoStack() {
-
-        BaseContext context = new MockBaseContext();
-        assertNull(context.channel);
-        assertNull(context.queryCache);
-
-        try {
-            context.attachToRuntimeIfNeeded();
-            fail("No thread stack, must have thrown");
-        }
-        catch (CayenneRuntimeException e) {
-            // expected
-        }
-    }
-}
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.cache.QueryCache;
+import org.apache.cayenne.di.DIBootstrap;
+import org.apache.cayenne.di.Injector;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.apache.cayenne.tx.TransactionFactory;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+
+public class DataContextTest {
+    @Test
+    public void testUserPropertiesLazyInit() {
+        DataContext context = new DataContext();
+        assertNull(context.userProperties);
+
+        Map<String, Object> properties = context.getUserProperties();
+        assertNotNull(properties);
+        assertSame(properties, context.getUserProperties());
+    }
+
+    @Test
+    public void testAttachToRuntimeIfNeeded() {
+
+        final DataChannel channel = mock(DataChannel.class);
+        final QueryCache cache = mock(QueryCache.class);
+        final TransactionFactory factory = mock(TransactionFactory.class);
+
+        Module testModule = binder -> {
+            binder.bind(DataChannel.class).toInstance(channel);
+            binder.bind(QueryCache.class).toInstance(cache);
+            binder.bind(TransactionFactory.class).toInstance(factory);
+        };
+
+        Injector injector = DIBootstrap.createInjector(testModule);
+
+        DataContext context = new DataContext();
+        assertNull(context.channel);
+        assertNull(context.queryCache);
+
+        Injector oldInjector = CayenneRuntime.getThreadInjector();
+        try {
+
+            CayenneRuntime.bindThreadInjector(injector);
+
+            assertTrue(context.attachToRuntimeIfNeeded());
+            assertSame(channel, context.getChannel());
+
+            assertFalse(context.attachToRuntimeIfNeeded());
+            assertFalse(context.attachToRuntimeIfNeeded());
+        }
+        finally {
+            CayenneRuntime.bindThreadInjector(oldInjector);
+        }
+    }
+
+    @Test
+    public void testAttachToRuntimeIfNeeded_NoStack() {
+
+        DataContext context = new DataContext();
+        assertNull(context.channel);
+        assertNull(context.queryCache);
+
+        try {
+            context.attachToRuntimeIfNeeded();
+            fail("No thread stack, must have thrown");
+        }
+        catch (CayenneRuntimeException e) {
+            // expected
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextLocalCacheIT.java
 
b/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextLocalCacheIT.java
index 67801b692..c15d8e82d 100644
--- 
a/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextLocalCacheIT.java
+++ 
b/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextLocalCacheIT.java
@@ -20,7 +20,6 @@ package org.apache.cayenne.access;
 
 import java.util.List;
 
-import org.apache.cayenne.BaseContext;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.query.ObjectSelect;
@@ -52,14 +51,14 @@ public class NestedDataContextLocalCacheIT extends 
RuntimeCase {
 
         ObjectContext child1 = runtime.newContext(context);
 
-        assertNull(((BaseContext) child1).getQueryCache().get(
+        assertNull(((DataContext) child1).getQueryCache().get(
                 query.getMetaData(child1.getEntityResolver())));
 
         assertNull(context.getQueryCache().get(
                 query.getMetaData(context.getEntityResolver())));
 
         List<?> results = child1.performQuery(query);
-        assertSame(results, ((BaseContext) child1).getQueryCache().get(
+        assertSame(results, ((DataContext) child1).getQueryCache().get(
                 query.getMetaData(child1.getEntityResolver())));
 
         assertNull(context.getQueryCache().get(

Reply via email to