Java tutorial
/***************************************************************** * 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 * * http://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 java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.DataObject; import org.apache.cayenne.DataRow; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.ObjectId; import org.apache.cayenne.PersistenceState; import org.apache.cayenne.Persistent; import org.apache.cayenne.access.ObjectDiff.ArcOperation; import org.apache.cayenne.access.event.SnapshotEvent; import org.apache.cayenne.access.event.SnapshotEventListener; import org.apache.cayenne.graph.ChildDiffLoader; import org.apache.cayenne.graph.GraphChangeHandler; import org.apache.cayenne.graph.GraphDiff; import org.apache.cayenne.graph.GraphManager; import org.apache.cayenne.graph.NodeCreateOperation; import org.apache.cayenne.graph.NodeDeleteOperation; import org.apache.cayenne.graph.NodeDiff; import org.apache.cayenne.graph.NodePropertyChangeOperation; import org.apache.cayenne.query.ObjectIdQuery; import org.apache.cayenne.reflect.AttributeProperty; import org.apache.cayenne.reflect.ClassDescriptor; import org.apache.cayenne.reflect.PropertyVisitor; import org.apache.cayenne.reflect.ToManyProperty; import org.apache.cayenne.reflect.ToOneProperty; import org.apache.commons.collections.map.AbstractReferenceMap; import org.apache.commons.collections.map.ReferenceMap; /** * ObjectStore stores objects using their ObjectId as a key. It works as a dedicated * object cache for a DataContext. Users rarely need to access ObjectStore directly, as * DataContext serves as a facade, providing cover methods for most ObjectStore * operations. * * @since 1.0 */ public class ObjectStore implements Serializable, SnapshotEventListener, GraphManager { /** * Factory method to create default Map for storing registered objects. * * @since 3.0 * @return a map with hard referenced keys and weak referenced values. */ static Map<Object, Persistent> createObjectMap() { return new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK); } protected Map<Object, Persistent> objectMap; protected Map<Object, ObjectDiff> changes; // a sequential id used to tag GraphDiffs so that they can later be sorted in the // original creation order int currentDiffId; /** * Stores a reference to the DataRowStore. * <p> * <i>Serialization note: </i> It is up to the owner of this ObjectStore to initialize * DataRowStore after deserialization of this object. ObjectStore will not know how to * restore the DataRowStore by itself. * </p> */ protected transient DataRowStore dataRowCache; // used to avoid incorrect on-demand DataRowStore initialization after deserialization protected boolean dataRowCacheSet; private Collection<GraphDiff> lifecycleEventInducedChanges; /** * The DataContext that owns this ObjectStore. */ protected DataContext context; /** * @deprecated since 3.1 */ @Deprecated public ObjectStore() { this(null); } /** * @deprecated since 3.1 */ @Deprecated public ObjectStore(DataRowStore dataRowCache) { this(dataRowCache, createObjectMap()); } /** * Creates an ObjectStore with {@link DataRowStore} and a map to use for storing * registered objects. Passed map doesn't require any special synchronization * behavior, as ObjectStore is synchronized itself. * * @since 3.0 */ public ObjectStore(DataRowStore dataRowCache, Map<Object, Persistent> objectMap) { setDataRowCache(dataRowCache); if (objectMap != null) { this.objectMap = objectMap; } else { throw new CayenneRuntimeException("Object map is null."); } this.changes = new HashMap<Object, ObjectDiff>(); } /** * @since 3.0 */ void childContextSyncStarted() { lifecycleEventInducedChanges = new ArrayList<GraphDiff>(); } /** * @since 3.0 */ void childContextSyncStopped() { lifecycleEventInducedChanges = null; } /** * @since 3.0 */ Collection<GraphDiff> getLifecycleEventInducedChanges() { return lifecycleEventInducedChanges != null ? lifecycleEventInducedChanges : Collections.EMPTY_LIST; } void registerLifecycleEventInducedChange(GraphDiff diff) { if (ChildDiffLoader.isProcessingChildDiff()) { // reset so that subsequent event-induced changes could get registered... ChildDiffLoader.setExternalChange(Boolean.FALSE); } else { lifecycleEventInducedChanges.add(diff); } } /** * Registers object change. * * @since 1.2 */ synchronized ObjectDiff registerDiff(Object nodeId, NodeDiff diff) { if (diff != null) { diff.setDiffId(++currentDiffId); } ObjectDiff objectDiff = changes.get(nodeId); if (objectDiff == null) { Persistent object = objectMap.get(nodeId); if (object == null) { throw new CayenneRuntimeException("No object is registered in context with Id " + nodeId); } if (object.getPersistenceState() == PersistenceState.COMMITTED) { object.setPersistenceState(PersistenceState.MODIFIED); // TODO: andrus 3/23/2006 snapshot versions are obsolete, but there is no // replacement yet, so we still need to handle them... if (object instanceof DataObject) { DataObject dataObject = (DataObject) object; DataRow snapshot = getCachedSnapshot((ObjectId) nodeId); if (snapshot != null && snapshot.getVersion() != dataObject.getSnapshotVersion()) { DataContextDelegate delegate = context.nonNullDelegate(); if (delegate.shouldMergeChanges(dataObject, snapshot)) { ClassDescriptor descriptor = context.getEntityResolver() .getClassDescriptor(((ObjectId) nodeId).getEntityName()); DataRowUtils.forceMergeWithSnapshot(context, descriptor, dataObject, snapshot); dataObject.setSnapshotVersion(snapshot.getVersion()); delegate.finishedMergeChanges(dataObject); } } } } objectDiff = new ObjectDiff(object); objectDiff.setDiffId(++currentDiffId); changes.put(nodeId, objectDiff); } if (diff != null) { objectDiff.addDiff(diff); } return objectDiff; } /** * Returns a number of objects currently registered with this ObjectStore. * * @since 1.2 */ public int registeredObjectsCount() { return objectMap.size(); } /** * Returns a DataRowStore associated with this ObjectStore. */ public DataRowStore getDataRowCache() { // perform deferred initialization... // Andrus, 11/7/2005 - potential problem with on-demand deferred initialization is // that deserialized context won't receive any events... which maybe ok, since it // didn't while it was stored in serialized form. if (dataRowCache == null && context != null && dataRowCacheSet) { synchronized (this) { if (dataRowCache == null) { DataDomain domain = context.getParentDataDomain(); if (domain != null) { setDataRowCache(domain.getSharedSnapshotCache()); } } } } return dataRowCache; } /** * Sets parent DataRowStore. Registers to receive SnapshotEvents if the cache is * configured to allow ObjectStores to receive such events. */ // note that as of 1.2, ObjectStore does not access DataRowStore directly when // retrieving snapshots. Instead it sends a query via the DataContext's channel so // that every element in the channel chain could intercept snapshot requests public void setDataRowCache(DataRowStore dataRowCache) { if (dataRowCache == this.dataRowCache) { return; } if (this.dataRowCache != null && this.dataRowCache.getEventManager() != null) { this.dataRowCache.getEventManager().removeListener(this, this.dataRowCache.getSnapshotEventSubject()); } this.dataRowCache = dataRowCache; if (dataRowCache != null && dataRowCache.getEventManager() != null) { // setting itself as non-blocking listener, // since event sending thread will likely be locking sender's // ObjectStore and snapshot cache itself. dataRowCache.getEventManager().addNonBlockingListener(this, "snapshotsChanged", SnapshotEvent.class, dataRowCache.getSnapshotEventSubject(), dataRowCache); } dataRowCacheSet = dataRowCache != null; } /** * Evicts a collection of DataObjects from the ObjectStore, invalidates the underlying * cache snapshots. Changes objects state to TRANSIENT. This method can be used for * manual cleanup of Cayenne cache. */ // this method is exactly the same as "objectsInvalidated", only additionally it // throws out registered objects public synchronized void objectsUnregistered(Collection objects) { if (objects.isEmpty()) { return; } Collection<ObjectId> ids = new ArrayList<ObjectId>(objects.size()); Iterator it = objects.iterator(); while (it.hasNext()) { Persistent object = (Persistent) it.next(); ObjectId id = object.getObjectId(); // remove object but not snapshot objectMap.remove(id); changes.remove(id); ids.add(id); object.setObjectContext(null); object.setObjectId(null); object.setPersistenceState(PersistenceState.TRANSIENT); } // TODO, andrus 3/28/2006 - DRC is null in nested contexts... implement // propagation of unregister operation through the stack ... or do the opposite // and keep unregister local even for non-nested DC? if (getDataRowCache() != null) { // send an event for removed snapshots getDataRowCache().processSnapshotChanges(this, Collections.EMPTY_MAP, Collections.EMPTY_LIST, ids, Collections.EMPTY_LIST); } } /** * Reverts changes to all stored uncomitted objects. * * @since 1.1 */ public synchronized void objectsRolledBack() { Iterator it = getObjectIterator(); // collect candidates while (it.hasNext()) { Persistent object = (Persistent) it.next(); int objectState = object.getPersistenceState(); switch (objectState) { case PersistenceState.NEW: it.remove(); object.setObjectContext(null); object.setObjectId(null); object.setPersistenceState(PersistenceState.TRANSIENT); break; case PersistenceState.DELETED: // Do the same as for modified... deleted is only a persistence state, // so // rolling the object back will set the state to committed case PersistenceState.MODIFIED: // this will clean any modifications and defer refresh from snapshot // till the next object accessor is called object.setPersistenceState(PersistenceState.HOLLOW); break; default: // Transient, committed and hollow need no handling break; } } // reset changes ... using new HashMap to allow event listeners to analyze the // original changes map after the rollback this.changes = new HashMap<Object, ObjectDiff>(); } /** * Builds and returns GraphDiff reflecting all uncommitted object changes. * * @since 1.2 */ ObjectStoreGraphDiff getChanges() { return new ObjectStoreGraphDiff(this); } /** * Returns internal changes map. * * @since 1.2 */ Map<Object, ObjectDiff> getChangesByObjectId() { return changes; } /** * @since 1.2 */ void postprocessAfterPhantomCommit() { for (Object id : changes.keySet()) { Persistent object = objectMap.get(id); // assume that no new or deleted objects are present (as otherwise commit // wouldn't have been phantom). object.setPersistenceState(PersistenceState.COMMITTED); } // clear caches this.changes.clear(); } /** * Internal unsynchronized method to process objects state after commit. * * @since 1.2 */ void postprocessAfterCommit(GraphDiff parentChanges) { // scan through changed objects, set persistence state to committed for (Object id : changes.keySet()) { Persistent object = objectMap.get(id); switch (object.getPersistenceState()) { case PersistenceState.DELETED: objectMap.remove(id); object.setObjectContext(null); object.setPersistenceState(PersistenceState.TRANSIENT); break; case PersistenceState.NEW: case PersistenceState.MODIFIED: object.setPersistenceState(PersistenceState.COMMITTED); break; } } // re-register changed object ids if (!parentChanges.isNoop()) { parentChanges.apply(new GraphChangeHandler() { public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) { } public void arcDeleted(Object nodeId, Object targetNodeId, Object arcId) { } public void nodeCreated(Object nodeId) { } public void nodeIdChanged(Object nodeId, Object newId) { processIdChange(nodeId, newId); } public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) { } public void nodeRemoved(Object nodeId) { } }); } // create new instance of changes map so that event listeners who stored the // original diff don't get affected this.changes = new HashMap<Object, ObjectDiff>(); } /** * Returns a snapshot for ObjectId from the underlying snapshot cache. If cache * contains no snapshot, a null is returned. * * @since 1.1 */ public DataRow getCachedSnapshot(ObjectId oid) { if (context != null && context.getChannel() != null) { ObjectIdQuery query = new CachedSnapshotQuery(oid); List<?> results = context.getChannel().onQuery(context, query).firstList(); return results.isEmpty() ? null : (DataRow) results.get(0); } else { return null; } } /** * Returns a snapshot for ObjectId from the underlying snapshot cache. If cache * contains no snapshot, it will attempt fetching it using provided QueryEngine. If * fetch attempt fails or inconsistent data is returned, underlying cache will throw a * CayenneRuntimeException. * * @since 1.2 */ public synchronized DataRow getSnapshot(ObjectId oid) { if (context != null && context.getChannel() != null) { ObjectIdQuery query = new ObjectIdQuery(oid, true, ObjectIdQuery.CACHE); List<?> results = context.getChannel().onQuery(context, query).firstList(); return results.isEmpty() ? null : (DataRow) results.get(0); } else { return null; } } /** * Returns an iterator over the registered objects. */ public synchronized Iterator getObjectIterator() { return objectMap.values().iterator(); } /** * Returns <code>true</code> if there are any modified, deleted or new objects * registered with this ObjectStore, <code>false</code> otherwise. This method will * treat "phantom" modifications are real ones. I.e. if you "change" an object * property to an equivalent value, this method will still think such object is * modified. Phantom modifications are only detected and discarded during commit. */ public synchronized boolean hasChanges() { return !changes.isEmpty(); } /** * Return a subset of registered objects that are in a certain persistence state. * Collection is returned by copy. */ public synchronized List<Persistent> objectsInState(int state) { List<Persistent> filteredObjects = new ArrayList<Persistent>(); for (Persistent object : objectMap.values()) { if (object.getPersistenceState() == state) { filteredObjects.add(object); } } return filteredObjects; } /** * SnapshotEventListener implementation that processes snapshot change event, updating * DataObjects that have the changes. * <p> * <i>Implementation note: </i> This method should not attempt to alter the underlying * DataRowStore, since it is normally invoked *AFTER* the DataRowStore was modified as * a result of some external interaction. * </p> * * @since 1.1 */ public void snapshotsChanged(SnapshotEvent event) { // filter events that we should not process if (event.getPostedBy() != this && event.getSource() == this.getDataRowCache()) { processSnapshotEvent(event); } } /** * @since 1.2 */ synchronized void processSnapshotEvent(SnapshotEvent event) { Map modifiedDiffs = event.getModifiedDiffs(); if (modifiedDiffs != null && !modifiedDiffs.isEmpty()) { Iterator oids = modifiedDiffs.entrySet().iterator(); while (oids.hasNext()) { Map.Entry entry = (Map.Entry) oids.next(); processUpdatedSnapshot(entry.getKey(), (DataRow) entry.getValue()); } } Collection deletedIDs = event.getDeletedIds(); if (deletedIDs != null && !deletedIDs.isEmpty()) { Iterator it = deletedIDs.iterator(); while (it.hasNext()) { processDeletedID(it.next()); } } processInvalidatedIDs(event.getInvalidatedIds()); processIndirectlyModifiedIDs(event.getIndirectlyModifiedIds()); // TODO: andrus, 3/28/2006 - 'SnapshotEventDecorator' serves as a bridge (or // rather a noop wrapper) between old snapshot events and new GraphEvents. Once // SnapshotEvents are replaced with GraphEvents (in 2.0) we won't need it GraphDiff diff = new SnapshotEventDecorator(event); ObjectContext originatingContext = (event.getPostedBy() instanceof ObjectContext) ? (ObjectContext) event.getPostedBy() : null; context.fireDataChannelChanged(originatingContext, diff); } void processIdChange(Object nodeId, Object newId) { Persistent object = objectMap.remove(nodeId); if (object != null) { object.setObjectId((ObjectId) newId); objectMap.put(newId, object); ObjectDiff change = changes.remove(nodeId); if (change != null) { changes.put(newId, change); } } } /** * Requires external synchronization. * * @since 1.2 */ void processDeletedID(Object nodeId) { // access object map directly - the method should be called in a synchronized // context... Persistent object = objectMap.get(nodeId); if (object != null) { DataObject dataObject = (object instanceof DataObject) ? (DataObject) object : null; DataContextDelegate delegate; switch (object.getPersistenceState()) { case PersistenceState.COMMITTED: case PersistenceState.HOLLOW: case PersistenceState.DELETED: // consult delegate delegate = context.nonNullDelegate(); if (dataObject == null || delegate.shouldProcessDelete(dataObject)) { objectMap.remove(nodeId); changes.remove(nodeId); // setting DataContext to null will also set // state to transient object.setObjectContext(null); if (dataObject != null) { delegate.finishedProcessDelete(dataObject); } } break; case PersistenceState.MODIFIED: // consult delegate delegate = context.nonNullDelegate(); if (dataObject != null && delegate.shouldProcessDelete(dataObject)) { object.setPersistenceState(PersistenceState.NEW); changes.remove(nodeId); registerNode(nodeId, object); nodeCreated(nodeId); delegate.finishedProcessDelete(dataObject); } break; } } } /** * @since 1.1 */ void processInvalidatedIDs(Collection invalidatedIDs) { if (invalidatedIDs != null && !invalidatedIDs.isEmpty()) { Iterator it = invalidatedIDs.iterator(); while (it.hasNext()) { ObjectId oid = (ObjectId) it.next(); DataObject object = (DataObject) getNode(oid); if (object == null) { continue; } // TODO: refactor "switch" to avoid code duplication switch (object.getPersistenceState()) { case PersistenceState.COMMITTED: object.setPersistenceState(PersistenceState.HOLLOW); break; case PersistenceState.MODIFIED: DataContext context = (DataContext) object.getObjectContext(); DataRow diff = getSnapshot(oid); // consult delegate if it exists DataContextDelegate delegate = context.nonNullDelegate(); if (delegate.shouldMergeChanges(object, diff)) { ClassDescriptor descriptor = context.getEntityResolver() .getClassDescriptor(oid.getEntityName()); DataRowUtils.forceMergeWithSnapshot(context, descriptor, object, diff); delegate.finishedMergeChanges(object); } case PersistenceState.HOLLOW: // do nothing break; case PersistenceState.DELETED: // TODO: Do nothing? Or treat as merged? break; } } } } /** * Requires external synchronization. * * @since 1.1 */ void processIndirectlyModifiedIDs(Collection indirectlyModifiedIDs) { Iterator indirectlyModifiedIt = indirectlyModifiedIDs.iterator(); while (indirectlyModifiedIt.hasNext()) { ObjectId oid = (ObjectId) indirectlyModifiedIt.next(); // access object map directly - the method should be called in a synchronized // context... final DataObject object = (DataObject) objectMap.get(oid); if (object == null || object.getPersistenceState() != PersistenceState.COMMITTED) { continue; } // for now break all "independent" object relationships... // in the future we may want to be more precise and go after modified // relationships only, or even process updated lists without invalidating... DataContextDelegate delegate = context.nonNullDelegate(); if (delegate.shouldMergeChanges(object, null)) { ClassDescriptor descriptor = context.getEntityResolver().getClassDescriptor(oid.getEntityName()); descriptor.visitProperties(new PropertyVisitor() { public boolean visitToMany(ToManyProperty property) { property.invalidate(object); return true; } public boolean visitToOne(ToOneProperty property) { if (property.getRelationship().isSourceIndependentFromTargetChange()) { property.invalidate(object); } return true; } public boolean visitAttribute(AttributeProperty property) { return true; } }); delegate.finishedProcessDelete(object); } } } /** * Requires external synchronization. * * @since 1.1 */ void processUpdatedSnapshot(Object nodeId, DataRow diff) { // access object map directly - the method should be called in a synchronized // context... DataObject object = (DataObject) objectMap.get(nodeId); // no object, or HOLLOW object require no processing if (object != null) { int state = object.getPersistenceState(); if (state != PersistenceState.HOLLOW) { // perform same steps as resolveHollow() if (state == PersistenceState.COMMITTED) { // consult delegate if it exists DataContextDelegate delegate = context.nonNullDelegate(); if (delegate.shouldMergeChanges(object, diff)) { ClassDescriptor descriptor = context.getEntityResolver() .getClassDescriptor(((ObjectId) nodeId).getEntityName()); // TODO: andrus, 5/26/2006 - call to 'getSnapshot' is expensive, // however my attempts to merge the 'diff' instead of snapshot // via 'refreshObjectWithSnapshot' resulted in even worse // performance. // This sounds counterintuitive (Not sure if this is some HotSpot // related glitch)... still keeping the old algorithm here until // we // switch from snapshot events to GraphEvents and all this code // becomes obsolete. DataRow snapshot = getSnapshot(object.getObjectId()); DataRowUtils.refreshObjectWithSnapshot(descriptor, object, snapshot, true); delegate.finishedMergeChanges(object); } } // merge modified and deleted else if (state == PersistenceState.DELETED || state == PersistenceState.MODIFIED) { // consult delegate if it exists DataContextDelegate delegate = context.nonNullDelegate(); if (delegate.shouldMergeChanges(object, diff)) { ClassDescriptor descriptor = context.getEntityResolver() .getClassDescriptor(((ObjectId) nodeId).getEntityName()); DataRowUtils.forceMergeWithSnapshot(context, descriptor, object, diff); delegate.finishedMergeChanges(object); } } } } } /** * @since 1.2 */ public DataContext getContext() { return context; } /** * @since 1.2 */ public void setContext(DataContext context) { this.context = context; } // *********** GraphManager Methods ******** // ========================================= /** * Returns a registered DataObject or null of no object exists for the ObjectId. * * @since 1.2 */ 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); } /** * Returns all registered DataObjects. List is returned by copy and can be modified by * the caller. * * @since 1.2 */ public synchronized Collection<Object> registeredNodes() { return new ArrayList<Object>(objectMap.values()); } /** * @since 1.2 */ public synchronized void registerNode(Object nodeId, Object nodeObject) { objectMap.put(nodeId, (Persistent) nodeObject); } /** * @since 1.2 */ public synchronized Object unregisterNode(Object nodeId) { Object object = getNode(nodeId); if (object != null) { objectsUnregistered(Collections.singleton(object)); } return object; } /** * Does nothing. * * @since 1.2 */ public void nodeIdChanged(Object nodeId, Object newId) { throw new UnsupportedOperationException("nodeIdChanged"); } /** * @since 1.2 */ public void nodeCreated(Object nodeId) { NodeDiff diff = new NodeCreateOperation(nodeId); if (lifecycleEventInducedChanges != null) { registerLifecycleEventInducedChange(diff); } registerDiff(nodeId, diff); } /** * @since 1.2 */ public void nodeRemoved(Object nodeId) { NodeDiff diff = new NodeDeleteOperation(nodeId); if (lifecycleEventInducedChanges != null) { registerLifecycleEventInducedChange(diff); } registerDiff(nodeId, diff); } /** * Records dirty object snapshot. * * @since 1.2 */ public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) { if (lifecycleEventInducedChanges != null) { registerLifecycleEventInducedChange( new NodePropertyChangeOperation(nodeId, property, oldValue, newValue)); } registerDiff(nodeId, null); } /** * @since 1.2 */ public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) { NodeDiff diff = new ArcOperation(nodeId, targetNodeId, arcId.toString(), false); if (lifecycleEventInducedChanges != null) { registerLifecycleEventInducedChange(diff); } registerDiff(nodeId, diff); } /** * @since 1.2 */ public void arcDeleted(Object nodeId, Object targetNodeId, Object arcId) { NodeDiff diff = new ArcOperation(nodeId, targetNodeId, arcId.toString(), true); if (lifecycleEventInducedChanges != null) { registerLifecycleEventInducedChange(diff); } registerDiff(nodeId, diff); } // an ObjectIdQuery optimized for retrieval of multiple snapshots - it can be reset // with the new id final class CachedSnapshotQuery extends ObjectIdQuery { CachedSnapshotQuery(ObjectId oid) { super(oid, true, ObjectIdQuery.CACHE_NOREFRESH); } void resetId(ObjectId oid) { this.objectId = oid; this.replacementQuery = null; } } class SnapshotEventDecorator implements GraphDiff { SnapshotEvent event; SnapshotEventDecorator(SnapshotEvent event) { this.event = event; } SnapshotEvent getEvent() { return event; } public void apply(GraphChangeHandler handler) { throw new UnsupportedOperationException(); } public boolean isNoop() { throw new UnsupportedOperationException(); } public void undo(GraphChangeHandler handler) { throw new UnsupportedOperationException(); } } }