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


The following commit(s) were added to refs/heads/master by this push:
     new bf3446627 CAY-2876 Memory leak in the ObjectStore
bf3446627 is described below

commit bf344662776351b64deb1fdf0bbe0676db86c92e
Author: Nikita Timofeev <stari...@gmail.com>
AuthorDate: Wed Feb 26 16:54:52 2025 +0400

    CAY-2876 Memory leak in the ObjectStore
---
 RELEASE-NOTES.txt                                  |   1 +
 .../commitlog/CommitLogFilter_All_FlattenedIT.java |  10 +-
 .../access/DefaultObjectMapRetainStrategy.java     |   4 +-
 .../apache/cayenne/access/NoSyncObjectStore.java   |   4 +-
 .../java/org/apache/cayenne/access/ObjectDiff.java |  43 +++--
 .../cayenne/access/ObjectMapRetainStrategy.java    |   4 +-
 .../org/apache/cayenne/access/ObjectStore.java     | 195 +++++++++++----------
 .../apache/cayenne/access/ObjectStoreEntry.java    |  70 ++++++++
 .../cayenne/access/ObjectStoreGraphDiff.java       |   7 +-
 .../access/DataContextPrefetchMultistepIT.java     |   2 +-
 .../cayenne/access/DataContextSerializationIT.java |   4 +-
 .../org/apache/cayenne/access/ObjectStoreTest.java |   2 +-
 12 files changed, 219 insertions(+), 127 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index f964462b3..52ab66e33 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -21,6 +21,7 @@ Bug Fixes:
 CAY-2701 MySQL DST-related LocalDateTime issues
 CAY-2871 QualifierTranslator breaks on a relationship with a compound FK
 CAY-2872 CayenneModeler "Documentation" link is broken
+CAY-2876 Memory leak in the ObjectStore
 CAY-2879 Negative number for non parameterized ObjectSelect query not 
processed correctly
 
 ----------------------------------
diff --git 
a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
 
b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
index 7a755890a..63c0f1a6e 100644
--- 
a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
+++ 
b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
@@ -27,6 +27,8 @@ import org.apache.cayenne.commitlog.model.ObjectChange;
 import org.apache.cayenne.commitlog.model.ObjectChangeType;
 import org.apache.cayenne.commitlog.model.ToManyRelationshipChange;
 import org.apache.cayenne.commitlog.unit.FlattenedRuntimeCase;
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.runtime.CoreModule;
 import org.apache.cayenne.query.SelectById;
 import org.apache.cayenne.runtime.CayenneRuntimeBuilder;
 import org.junit.Before;
@@ -48,6 +50,8 @@ public class CommitLogFilter_All_FlattenedIT extends 
FlattenedRuntimeCase {
        protected CayenneRuntimeBuilder configureCayenne() {
                this.mockListener = mock(CommitLogListener.class);
                return super.configureCayenne()
+                               .addModule(b -> CoreModule.extend(b)
+                                               
.setProperty(Constants.OBJECT_RETAIN_STRATEGY_PROPERTY, "soft"))
                                .addModule(b -> 
CommitLogModule.extend(b).addListener(mockListener));
        }
 
@@ -63,9 +67,9 @@ public class CommitLogFilter_All_FlattenedIT extends 
FlattenedRuntimeCase {
                e4.insert(12);
                e34.insert(1, 11);
 
-               E3 e3 = SelectById.query(E3.class, 1).selectOne(context);
-               E4 e4_1 = SelectById.query(E4.class, 11).selectOne(context);
-               E4 e4_2 = SelectById.query(E4.class, 12).selectOne(context);
+               E3 e3 = SelectById.queryId(E3.class, 1).selectOne(context);
+               E4 e4_1 = SelectById.queryId(E4.class, 11).selectOne(context);
+               E4 e4_2 = SelectById.queryId(E4.class, 12).selectOne(context);
 
                doAnswer((Answer<Object>) invocation -> {
 
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/DefaultObjectMapRetainStrategy.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/DefaultObjectMapRetainStrategy.java
index 05f248ed8..8fcbf42db 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/DefaultObjectMapRetainStrategy.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/DefaultObjectMapRetainStrategy.java
@@ -22,7 +22,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.Persistent;
+import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.di.Inject;
@@ -46,7 +46,7 @@ public class DefaultObjectMapRetainStrategy implements 
ObjectMapRetainStrategy {
         this.runtimeProperties = runtimeProperties;
     }
 
-    public Map<Object, Persistent> createObjectMap() {
+    public Map<ObjectId, ObjectStoreEntry> createObjectMap() {
         String strategy = 
runtimeProperties.get(Constants.OBJECT_RETAIN_STRATEGY_PROPERTY);
 
         if (strategy == null || WEAK_RETAIN_STRATEGY.equals(strategy)) {
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/NoSyncObjectStore.java 
b/cayenne/src/main/java/org/apache/cayenne/access/NoSyncObjectStore.java
index 9358c08d2..914543a23 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/NoSyncObjectStore.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/NoSyncObjectStore.java
@@ -20,7 +20,7 @@ package org.apache.cayenne.access;
 
 import java.util.Map;
 
-import org.apache.cayenne.Persistent;
+import org.apache.cayenne.ObjectId;
 
 /**
  * An {@link ObjectStore} which doesn't receive notifications 
@@ -30,7 +30,7 @@ import org.apache.cayenne.Persistent;
  */
 public class NoSyncObjectStore extends ObjectStore {
     
-    public NoSyncObjectStore(DataRowStore dataRowCache, Map<Object, 
Persistent> objectMap) {
+    public NoSyncObjectStore(DataRowStore dataRowCache, Map<ObjectId, 
ObjectStoreEntry> objectMap) {
         super(dataRowCache, objectMap);
     }
     
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ObjectDiff.java 
b/cayenne/src/main/java/org/apache/cayenne/access/ObjectDiff.java
index 9b2b68de4..6ae2df6d3 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectDiff.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectDiff.java
@@ -65,23 +65,22 @@ public class ObjectDiff extends NodeDiff {
     private Map<ArcOperation, ArcOperation> flatIds;
     private Map<ArcOperation, ArcOperation> phantomFks;
 
-    private Persistent object;
+    private final ObjectStoreEntry entry;
 
-    ObjectDiff(final Persistent object) {
+    ObjectDiff(final ObjectStoreEntry entry) {
 
-        super(object.getObjectId());
+        super(entry.persistent().getObjectId());
 
-        // retain the object, as ObjectStore may have weak references to
-        // registered
-        // objects and we can't allow it to deallocate dirty objects.
-        this.object = object;
+        // retain the object, as ObjectStore may have weak references to 
registered objects,
+        // and we can't allow it to deallocate dirty objects.
+        this.entry = entry;
 
-        EntityResolver entityResolver = 
object.getObjectContext().getEntityResolver();
+        EntityResolver entityResolver = 
object().getObjectContext().getEntityResolver();
 
-        this.entityName = object.getObjectId().getEntityName();
+        this.entityName = object().getObjectId().getEntityName();
         this.classDescriptor = entityResolver.getClassDescriptor(entityName);
 
-        int state = object.getPersistenceState();
+        int state = object().getPersistenceState();
 
         // take snapshot of simple properties and arcs used for optimistic
         // locking..
@@ -99,7 +98,7 @@ public class ObjectDiff extends NodeDiff {
 
                 @Override
                 public boolean visitAttribute(AttributeProperty property) {
-                    snapshot.put(property.getName(), 
property.readProperty(object));
+                    snapshot.put(property.getName(), 
property.readProperty(object()));
                     return true;
                 }
 
@@ -114,8 +113,8 @@ public class ObjectDiff extends NodeDiff {
                     
                     // eagerly resolve optimistically locked relationships
                     Object target = (lock && isUsedForLocking)
-                            ? property.readProperty(object)
-                            : property.readPropertyDirectly(object);
+                            ? property.readProperty(object())
+                            : property.readPropertyDirectly(object());
 
                     if (target instanceof Persistent) {
                         target = ((Persistent) target).getObjectId();
@@ -129,15 +128,15 @@ public class ObjectDiff extends NodeDiff {
         }
     }
 
-    Object getObject() {
-        return object;
+    Persistent object() {
+        return entry.persistent();
     }
 
     ClassDescriptor getClassDescriptor() {
         // class descriptor is initiated in constructor, but is nullified on
         // serialization
         if (classDescriptor == null) {
-            EntityResolver entityResolver = 
object.getObjectContext().getEntityResolver();
+            EntityResolver entityResolver = 
object().getObjectContext().getEntityResolver();
             this.classDescriptor = 
entityResolver.getClassDescriptor(entityName);
         }
 
@@ -152,7 +151,7 @@ public class ObjectDiff extends NodeDiff {
         Object value = arcSnapshot != null ? arcSnapshot.get(propertyName) : 
null;
 
         if (value instanceof Fault) {
-            Persistent target = (Persistent) ((Fault) 
value).resolveFault(object, propertyName);
+            Persistent target = (Persistent) ((Fault) 
value).resolveFault(object(), propertyName);
 
             value = target != null ? target.getObjectId() : null;
             arcSnapshot.put(propertyName, value);
@@ -167,7 +166,7 @@ public class ObjectDiff extends NodeDiff {
     public ObjectId getCurrentArcSnapshotValue(String propertyName) {
         Object value = currentArcSnapshot != null ? 
currentArcSnapshot.get(propertyName) : null;
         if (value instanceof Fault) {
-            Persistent target = (Persistent) ((Fault) 
value).resolveFault(object, propertyName);
+            Persistent target = (Persistent) ((Fault) 
value).resolveFault(object(), propertyName);
 
             value = target != null ? target.getObjectId() : null;
             currentArcSnapshot.put(propertyName, value);
@@ -328,7 +327,7 @@ public class ObjectDiff extends NodeDiff {
             return false;
         }
 
-        int state = object.getPersistenceState();
+        int state = object().getPersistenceState();
         if (state == PersistenceState.NEW || state == 
PersistenceState.DELETED) {
             return false;
         }
@@ -342,7 +341,7 @@ public class ObjectDiff extends NodeDiff {
             public boolean visitAttribute(AttributeProperty property) {
 
                 Object oldValue = snapshot.get(property.getName());
-                Object newValue = property.readProperty(object);
+                Object newValue = property.readProperty(object());
 
                 if (!property.equals(oldValue, newValue)) {
                     modFound[0] = true;
@@ -363,7 +362,7 @@ public class ObjectDiff extends NodeDiff {
                     return true;
                 }
 
-                Object newValue = property.readPropertyDirectly(object);
+                Object newValue = property.readPropertyDirectly(object());
                 if (newValue instanceof Fault) {
                     return true;
                 }
@@ -411,7 +410,7 @@ public class ObjectDiff extends NodeDiff {
             @Override
             public boolean visitAttribute(AttributeProperty property) {
 
-                Object newValue = property.readProperty(object);
+                Object newValue = property.readProperty(object());
 
                 // no baseline to compare
                 if (snapshot == null) {
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/ObjectMapRetainStrategy.java 
b/cayenne/src/main/java/org/apache/cayenne/access/ObjectMapRetainStrategy.java
index 8d1e4fbb9..3749298d0 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/ObjectMapRetainStrategy.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/ObjectMapRetainStrategy.java
@@ -20,7 +20,7 @@ package org.apache.cayenne.access;
 
 import java.util.Map;
 
-import org.apache.cayenne.Persistent;
+import org.apache.cayenne.ObjectId;
 
 /**
  * A strategy for retaining objects in {@link ObjectStore}. The strategy can 
be weak, soft
@@ -30,5 +30,5 @@ import org.apache.cayenne.Persistent;
  */
 public interface ObjectMapRetainStrategy {
 
-    Map<Object, Persistent> createObjectMap();
+    Map<ObjectId, ObjectStoreEntry> createObjectMap();
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java 
b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
index 0dcdc7aa1..ff7f2facd 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
@@ -53,7 +53,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * ObjectStore stores objects using their ObjectId as a key. It works as a 
dedicated
@@ -65,14 +65,16 @@ import java.util.concurrent.ConcurrentHashMap;
  */
 public class ObjectStore implements Serializable, SnapshotEventListener, 
GraphManager {
 
-    protected Map<Object, Persistent> objectMap;
+    protected Map<ObjectId, ObjectStoreEntry> objectMap;
     protected Map<Object, ObjectDiff> changes;
 
     /**
      * Map that tracks flattened paths for given object Id that is present in 
db.
      * Presence of path in this map is used to separate insert from update 
case of flattened records.
      * @since 4.1
+     * @deprecated since 5.0 unused
      */
+    @Deprecated
     protected Map<Object, Map<CayennePath, ObjectId>> trackedFlattenedPaths;
 
     // a sequential id used to tag GraphDiffs so that they can later be sorted 
in the
@@ -106,7 +108,7 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
      * 
      * @since 3.0
      */
-    public ObjectStore(DataRowStore dataRowCache, Map<Object, Persistent> 
objectMap) {
+    public ObjectStore(DataRowStore dataRowCache, Map<ObjectId, 
ObjectStoreEntry> objectMap) {
         setDataRowCache(dataRowCache);
         if (objectMap != null) {
             this.objectMap = objectMap;
@@ -137,7 +139,7 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
     Collection<GraphDiff> getLifecycleEventInducedChanges() {
         return lifecycleEventInducedChanges != null
                 ? lifecycleEventInducedChanges
-                : Collections.<GraphDiff>emptyList();
+                : Collections.emptyList();
     }
 
     void registerLifecycleEventInducedChange(GraphDiff diff) {
@@ -165,11 +167,13 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
 
         if (objectDiff == null) {
 
-            Persistent object = objectMap.get(nodeId);
-            if (object == null) {
+            ObjectStoreEntry entry = objectMap.get((ObjectId)nodeId);
+            if (entry == null || !entry.hasObject()) {
                 throw new CayenneRuntimeException("No object is registered in 
context with Id %s", nodeId);
             }
 
+            Persistent object = entry.persistent();
+
             if (object.getPersistenceState() == PersistenceState.COMMITTED) {
                 object.setPersistenceState(PersistenceState.MODIFIED);
 
@@ -196,7 +200,7 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
                 }
             }
 
-            objectDiff = new ObjectDiff(object);
+            objectDiff = new ObjectDiff(entry);
             objectDiff.setDiffId(++currentDiffId);
             changes.put(nodeId, objectDiff);
         }
@@ -214,7 +218,13 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
      * @since 1.2
      */
     public int registeredObjectsCount() {
-        return objectMap.size();
+        AtomicInteger counter = new AtomicInteger();
+        objectMap.forEach((id, obj) -> {
+            if(obj.hasObject()){
+                counter.incrementAndGet();
+            }
+        });
+        return counter.get();
     }
 
     /**
@@ -283,7 +293,7 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
      */
     // this method is exactly the same as "objectsInvalidated", only 
additionally it
     // throws out registered objects
-    public synchronized void objectsUnregistered(Collection objects) {
+    public synchronized void objectsUnregistered(Collection<?> objects) {
         if (objects.isEmpty()) {
             return;
         }
@@ -298,9 +308,6 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
             // remove object but not snapshot
             objectMap.remove(id);
             changes.remove(id);
-            if(id != null && trackedFlattenedPaths != null) {
-                trackedFlattenedPaths.remove(id);
-            }
             ids.add(id);
 
             object.setObjectContext(null);
@@ -314,10 +321,10 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
             // send an event for removed snapshots
             getDataRowCache().processSnapshotChanges(
                     this,
-                    Collections.<ObjectId, DataRow>emptyMap(),
-                    Collections.<ObjectId>emptyList(),
+                    Collections.emptyMap(),
+                    Collections.emptyList(),
                     ids,
-                    Collections.<ObjectId>emptyList());
+                    Collections.emptyList());
         }
     }
 
@@ -386,11 +393,11 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
 
         for (Object id : changes.keySet()) {
 
-            Persistent object = objectMap.get(id);
+            ObjectStoreEntry object = objectMap.get((ObjectId)id);
 
             // assume that no new or deleted objects are present (as otherwise 
commit
             // wouldn't have been phantom).
-            object.setPersistenceState(PersistenceState.COMMITTED);
+            
object.persistent().setPersistenceState(PersistenceState.COMMITTED);
         }
 
         // clear caches
@@ -405,8 +412,10 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
     public void postprocessAfterCommit(GraphDiff parentChanges) {
 
         // scan through changed objects, set persistence state to committed
-        for (Object id : changes.keySet()) {
-            Persistent object = objectMap.get(id);
+        for (Object key : changes.keySet()) {
+            ObjectId id = (ObjectId) key;
+            // persistent object for the diff should always exist
+            Persistent object = objectMap.get(id).persistent();
 
             switch (object.getPersistenceState()) {
                 case PersistenceState.DELETED:
@@ -503,7 +512,7 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
      * Returns an iterator over the registered objects.
      */
     public synchronized Iterator<Persistent> getObjectIterator() {
-        return objectMap.values().iterator();
+        return new EntryIterator(objectMap.values().iterator());
     }
 
     /**
@@ -524,9 +533,9 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
     public synchronized List<Persistent> objectsInState(int state) {
         List<Persistent> filteredObjects = new ArrayList<>();
 
-        for (Persistent object : objectMap.values()) {
-            if (object.getPersistenceState() == state) {
-                filteredObjects.add(object);
+        for (ObjectStoreEntry entry : objectMap.values()) {
+            if (entry.hasObject() && entry.persistent().getPersistenceState() 
== state) {
+                filteredObjects.add(entry.persistent());
             }
         }
 
@@ -586,11 +595,21 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
     }
 
     void processIdChange(Object nodeId, Object newId) {
-        Persistent object = objectMap.remove(nodeId);
-
-        if (object != null) {
-            object.setObjectId((ObjectId) newId);
-            objectMap.put(newId, object);
+        ObjectStoreEntry entry = objectMap.remove((ObjectId)nodeId);
+
+        if (entry != null) {
+            ObjectId id = (ObjectId) newId;
+            entry.persistent().setObjectId(id);
+            objectMap.merge(id, entry, (oldValue, newValue) -> {
+                if(oldValue.trackedFlattenedPaths != null) {
+                    if(newValue.trackedFlattenedPaths != null) {
+                        
newValue.trackedFlattenedPaths.putAll(oldValue.trackedFlattenedPaths);
+                    } else {
+                        newValue.trackedFlattenedPaths = 
oldValue.trackedFlattenedPaths;
+                    }
+                }
+                return newValue;
+            });
 
             ObjectDiff change = changes.remove(nodeId);
             if (change != null) {
@@ -598,12 +617,6 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
             }
         }
 
-        if(trackedFlattenedPaths != null) {
-            Map<CayennePath, ObjectId> paths = 
trackedFlattenedPaths.remove(nodeId);
-            if(paths != null) {
-                trackedFlattenedPaths.put(newId, paths);
-            }
-        }
     }
 
     /**
@@ -614,9 +627,10 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
     void processDeletedID(ObjectId nodeId) {
 
         // access object map directly - the method should be called in a 
synchronized context...
-        Persistent object = objectMap.get(nodeId);
+        ObjectStoreEntry entry = objectMap.get(nodeId);
 
-        if (object != null) {
+        if (entry != null && entry.hasObject()) {
+            Persistent object = entry.persistent();
             DataContextDelegate delegate;
 
             switch (object.getPersistenceState()) {
@@ -712,11 +726,14 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
     void processIndirectlyModifiedIDs(Collection<ObjectId> 
indirectlyModifiedIDs) {
         for (ObjectId oid : indirectlyModifiedIDs) {
             // access object map directly - the method should be called in a 
synchronized context...
-            final Persistent object = objectMap.get(oid);
+            ObjectStoreEntry entry = objectMap.get(oid);
 
-            if (object == null || object.getPersistenceState() != 
PersistenceState.COMMITTED) {
+            if (entry == null
+                    || !entry.hasObject()
+                    || entry.persistent().getPersistenceState() != 
PersistenceState.COMMITTED) {
                 continue;
             }
+            Persistent object = entry.persistent();
 
             // for now break all "independent" object relationships...
             // in the future we may want to be more precise and go after 
modified
@@ -766,11 +783,12 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
     void processUpdatedSnapshot(ObjectId nodeId, DataRow diff) {
 
         // access object map directly - the method should be called in a 
synchronized context...
-        Persistent object = objectMap.get(nodeId);
+        ObjectStoreEntry entry = objectMap.get(nodeId);
 
         // no object, or HOLLOW object require no processing
-        if (object != null) {
+        if (entry != null && entry.hasObject()) {
 
+            Persistent object = entry.persistent();
             int state = object.getPersistenceState();
             if (state != PersistenceState.HOLLOW) {
 
@@ -844,13 +862,12 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
      * @since 1.2
      */
     @Override
-    public synchronized Object getNode(Object nodeId) {
-        return objectMap.get(nodeId);
-    }
-
-    // non-synchronized version of getNode for private use
-    final Object getNodeNoSync(Object nodeId) {
-        return objectMap.get(nodeId);
+    public synchronized Persistent getNode(Object nodeId) {
+        ObjectStoreEntry entry = objectMap.get((ObjectId) nodeId);
+        if(entry == null) {
+            return null;
+        }
+        return entry.persistent();
     }
 
     /**
@@ -861,7 +878,13 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
      */
     @Override
     public synchronized Collection<Object> registeredNodes() {
-        return new ArrayList<Object>(objectMap.values());
+        List<Object> values = new ArrayList<>(objectMap.size());
+        objectMap.forEach((id, entry) -> {
+            if(entry.hasObject()) {
+                values.add(entry.persistent());
+            }
+        });
+        return values;
     }
 
     /**
@@ -869,15 +892,15 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
      */
     @Override
     public synchronized void registerNode(Object nodeId, Object nodeObject) {
-        objectMap.put(nodeId, (Persistent) nodeObject);
+        objectMap.put((ObjectId)nodeId, new ObjectStoreEntry((Persistent) 
nodeObject));
     }
 
     /**
      * @since 1.2
      */
     @Override
-    public synchronized Object unregisterNode(Object nodeId) {
-        Object object = getNode(nodeId);
+    public synchronized Persistent unregisterNode(Object nodeId) {
+        Persistent object = getNode(nodeId);
         if (object != null) {
             objectsUnregistered(Collections.singleton(object));
         }
@@ -975,52 +998,28 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
         registerDiff(nodeId, diff);
     }
 
-    /**
-     * Check that flattened path for given object ID has data row in DB.
-     * @since 4.1
-     */
-    boolean hasFlattenedPath(ObjectId objectId, CayennePath path) {
-        if(trackedFlattenedPaths == null) {
-            return false;
-        }
-        return trackedFlattenedPaths
-                .getOrDefault(objectId, 
Collections.emptyMap()).containsKey(path);
-    }
-
     /**
      * @since 4.2
      */
     public ObjectId getFlattenedId(ObjectId objectId, CayennePath path) {
-        if(trackedFlattenedPaths == null) {
-            return null;
-        }
-
-        return trackedFlattenedPaths
-                .getOrDefault(objectId, Collections.emptyMap()).get(path);
+        ObjectStoreEntry entry = objectMap.get(objectId);
+        return entry == null ? null : entry.getFlattenedId(path);
     }
 
     /**
      * @since 4.2
      */
     public Collection<ObjectId> getFlattenedIds(ObjectId objectId) {
-        if(trackedFlattenedPaths == null) {
-            return Collections.emptyList();
-        }
-
-        return trackedFlattenedPaths
-                .getOrDefault(objectId, Collections.emptyMap()).values();
+        ObjectStoreEntry entry = objectMap.get(objectId);
+        return entry == null ? null : entry.getFlattenedIds();
     }
 
     /**
      * @since 5.0
      */
-    public Map<CayennePath,ObjectId> getFlattenedPathIdMap(ObjectId objectId) {
-        if(trackedFlattenedPaths == null) {
-            return Collections.emptyMap();
-        }
-
-        return trackedFlattenedPaths
-                .getOrDefault(objectId, Collections.emptyMap());
+    public Map<CayennePath, ObjectId> getFlattenedPathIdMap(ObjectId objectId) 
{
+        ObjectStoreEntry entry = objectMap.get(objectId);
+        return entry == null ? null : entry.getFlattenedPathIdMap();
     }
 
     /**
@@ -1028,12 +1027,8 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
      * @since 4.1
      */
     public void markFlattenedPath(ObjectId objectId, CayennePath path, 
ObjectId id) {
-        if(trackedFlattenedPaths == null) {
-            trackedFlattenedPaths = new ConcurrentHashMap<>();
-        }
-        trackedFlattenedPaths
-                .computeIfAbsent(objectId, o -> new ConcurrentHashMap<>())
-                .put(path, id);
+        objectMap.computeIfAbsent(objectId, objId -> new 
ObjectStoreEntry(null))
+                .markFlattenedPath(path, id);
     }
 
     // an ObjectIdQuery optimized for retrieval of multiple snapshots - it can 
be reset
@@ -1077,4 +1072,28 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
             throw new UnsupportedOperationException();
         }
     }
+
+    static class EntryIterator implements Iterator<Persistent> {
+
+        final Iterator<ObjectStoreEntry> iterator;
+
+        EntryIterator(Iterator<ObjectStoreEntry> iterator) {
+            this.iterator = iterator;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public Persistent next() {
+            return iterator.next().persistent();
+        }
+
+        @Override
+        public void remove() {
+            iterator.remove();
+        }
+    }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStoreEntry.java 
b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStoreEntry.java
new file mode 100644
index 000000000..8aa0a3261
--- /dev/null
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStoreEntry.java
@@ -0,0 +1,70 @@
+/*****************************************************************
+ *   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.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.exp.path.CayennePath;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @since 5.0
+ */
+public class ObjectStoreEntry implements Serializable {
+
+    final protected Persistent persistent;
+    protected Map<CayennePath, ObjectId> trackedFlattenedPaths;
+
+    public ObjectStoreEntry(Persistent persistent) {
+        this.persistent = persistent;
+    }
+
+    public Persistent persistent() {
+        return persistent;
+    }
+
+    public boolean hasObject() {
+        return persistent != null;
+    }
+
+    public void markFlattenedPath(CayennePath path, ObjectId objectId) {
+        if (trackedFlattenedPaths == null) {
+            trackedFlattenedPaths = new ConcurrentHashMap<>();
+        }
+        trackedFlattenedPaths.put(path, objectId);
+    }
+
+    public ObjectId getFlattenedId(CayennePath path) {
+        return trackedFlattenedPaths == null ? null : 
trackedFlattenedPaths.get(path);
+    }
+
+    public Collection<ObjectId> getFlattenedIds() {
+        return trackedFlattenedPaths == null ? Collections.emptyList() : 
trackedFlattenedPaths.values();
+    }
+
+    public Map<CayennePath, ObjectId> getFlattenedPathIdMap() {
+        return trackedFlattenedPaths == null ? Collections.emptyMap() : 
trackedFlattenedPaths;
+    }
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java 
b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java
index 3043fa403..0208cf5ea 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java
@@ -70,8 +70,7 @@ public class ObjectStoreGraphDiff implements GraphDiff {
         boolean noop = true;
 
         // build a new collection for validation as validation methods may
-        // result in
-        // ObjectStore modifications
+        // result in ObjectStore modifications
 
         Collection<Validating> objectsToValidate = null;
 
@@ -81,12 +80,12 @@ public class ObjectStoreGraphDiff implements GraphDiff {
 
                 noop = false;
 
-                if (diff.getObject() instanceof Validating) {
+                if (diff.object() instanceof Validating) {
                     if (objectsToValidate == null) {
                         objectsToValidate = new ArrayList<>();
                     }
 
-                    objectsToValidate.add((Validating) diff.getObject());
+                    objectsToValidate.add((Validating) diff.object());
                 }
 
             }
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
 
b/cayenne/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
index 7bc9eab7b..897d60f53 100644
--- 
a/cayenne/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
+++ 
b/cayenne/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
@@ -113,7 +113,7 @@ public class DataContextPrefetchMultistepIT extends 
RuntimeCase {
         // get garbage collected, and we won't be able to detect them
         // so ensure ObjectStore uses a regular map just for this test
 
-        context.getObjectStore().objectMap = new HashMap<Object, Persistent>();
+        context.getObjectStore().objectMap = new HashMap<>();
 
         // Check the target ArtistExhibit objects do not exist yet
 
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/access/DataContextSerializationIT.java
 
b/cayenne/src/test/java/org/apache/cayenne/access/DataContextSerializationIT.java
index 441a9f8e9..face100de 100644
--- 
a/cayenne/src/test/java/org/apache/cayenne/access/DataContextSerializationIT.java
+++ 
b/cayenne/src/test/java/org/apache/cayenne/access/DataContextSerializationIT.java
@@ -21,8 +21,8 @@ package org.apache.cayenne.access;
 
 import org.apache.cayenne.Cayenne;
 import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.PersistenceState;
-import org.apache.cayenne.Persistent;
 import org.apache.cayenne.runtime.CayenneRuntime;
 import org.apache.cayenne.configuration.DefaultRuntimeProperties;
 import org.apache.cayenne.di.Inject;
@@ -152,7 +152,7 @@ public class DataContextSerializationIT extends RuntimeCase 
{
                 new DefaultRuntimeProperties(domain.getProperties()),
                 domain.getEventManager());
 
-        Map<Object, Persistent> map = new HashMap<>();
+        Map<ObjectId, ObjectStoreEntry> map = new HashMap<>();
 
         DataContext localCacheContext = new DataContext(domain, new 
ObjectStore(
                 snapshotCache,
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/access/ObjectStoreTest.java 
b/cayenne/src/test/java/org/apache/cayenne/access/ObjectStoreTest.java
index e79770498..b25336196 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/ObjectStoreTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/ObjectStoreTest.java
@@ -42,7 +42,7 @@ public class ObjectStoreTest {
     @Before
     public void before() {
         DataRowStore sharedCache = mock(DataRowStore.class);
-        this.objectStore = new ObjectStore(sharedCache, new HashMap<Object, 
Persistent>());
+        this.objectStore = new ObjectStore(sharedCache, new HashMap<>());
     }
 
     @Test


Reply via email to