| /***************************************************************** |
| * 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.Fault; |
| import org.apache.cayenne.ObjectId; |
| import org.apache.cayenne.PersistenceState; |
| import org.apache.cayenne.Persistent; |
| import org.apache.cayenne.exp.parser.ASTDbPath; |
| import org.apache.cayenne.graph.ArcId; |
| import org.apache.cayenne.graph.GraphChangeHandler; |
| import org.apache.cayenne.graph.GraphDiff; |
| import org.apache.cayenne.graph.NodeDiff; |
| import org.apache.cayenne.map.DbEntity; |
| import org.apache.cayenne.map.DbRelationship; |
| import org.apache.cayenne.map.EntityResolver; |
| import org.apache.cayenne.map.ObjEntity; |
| import org.apache.cayenne.map.ObjRelationship; |
| import org.apache.cayenne.reflect.ArcProperty; |
| 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.cayenne.util.Util; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| /** |
| * A dynamic GraphDiff that represents a delta between object simple properties |
| * at diff creation time and its current state. |
| */ |
| public class ObjectDiff extends NodeDiff { |
| |
| private final String entityName; |
| |
| private transient ClassDescriptor classDescriptor; |
| |
| private Collection<NodeDiff> otherDiffs; |
| |
| private Map<String, Object> snapshot; |
| private Map<String, Object> arcSnapshot; |
| private Map<String, Object> currentArcSnapshot; |
| private Map<ArcOperation, ArcOperation> flatIds; |
| private Map<ArcOperation, ArcOperation> phantomFks; |
| |
| private Persistent object; |
| |
| ObjectDiff(final Persistent object) { |
| |
| super(object.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; |
| |
| EntityResolver entityResolver = object.getObjectContext().getEntityResolver(); |
| |
| this.entityName = object.getObjectId().getEntityName(); |
| this.classDescriptor = entityResolver.getClassDescriptor(entityName); |
| |
| int state = object.getPersistenceState(); |
| |
| // take snapshot of simple properties and arcs used for optimistic |
| // locking.. |
| |
| if (state == PersistenceState.COMMITTED || state == PersistenceState.DELETED |
| || state == PersistenceState.MODIFIED) { |
| |
| final ObjEntity entity = entityResolver.getObjEntity(entityName); |
| final boolean lock = entity.getLockType() == ObjEntity.LOCK_TYPE_OPTIMISTIC; |
| |
| this.snapshot = new HashMap<>(); |
| this.arcSnapshot = new HashMap<>(); |
| |
| classDescriptor.visitProperties(new PropertyVisitor() { |
| |
| @Override |
| public boolean visitAttribute(AttributeProperty property) { |
| snapshot.put(property.getName(), property.readProperty(object)); |
| return true; |
| } |
| |
| @Override |
| public boolean visitToMany(ToManyProperty property) { |
| return true; |
| } |
| |
| @Override |
| public boolean visitToOne(ToOneProperty property) { |
| boolean isUsedForLocking = entity.getRelationship(property.getName()).isUsedForLocking(); |
| |
| // eagerly resolve optimistically locked relationships |
| Object target = (lock && isUsedForLocking) |
| ? property.readProperty(object) |
| : property.readPropertyDirectly(object); |
| |
| if (target instanceof Persistent) { |
| target = ((Persistent) target).getObjectId(); |
| } |
| // else - null || Fault |
| |
| arcSnapshot.put(property.getName(), target); |
| return true; |
| } |
| }); |
| } |
| } |
| |
| Object getObject() { |
| return object; |
| } |
| |
| ClassDescriptor getClassDescriptor() { |
| // class descriptor is initiated in constructor, but is nullified on |
| // serialization |
| if (classDescriptor == null) { |
| EntityResolver entityResolver = object.getObjectContext().getEntityResolver(); |
| this.classDescriptor = entityResolver.getClassDescriptor(entityName); |
| } |
| |
| return classDescriptor; |
| } |
| |
| public Object getSnapshotValue(String propertyName) { |
| return snapshot != null ? snapshot.get(propertyName) : null; |
| } |
| |
| public ObjectId getArcSnapshotValue(String propertyName) { |
| Object value = arcSnapshot != null ? arcSnapshot.get(propertyName) : null; |
| |
| if (value instanceof Fault) { |
| Persistent target = (Persistent) ((Fault) value).resolveFault(object, propertyName); |
| |
| value = target != null ? target.getObjectId() : null; |
| arcSnapshot.put(propertyName, value); |
| } |
| |
| return (ObjectId) value; |
| } |
| |
| /** |
| * @since 4.2 |
| */ |
| 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); |
| |
| value = target != null ? target.getObjectId() : null; |
| currentArcSnapshot.put(propertyName, value); |
| } |
| return (ObjectId) value; |
| } |
| |
| boolean containsArcSnapshot(String propertyName) { |
| return arcSnapshot != null && arcSnapshot.containsKey(propertyName); |
| } |
| |
| /** |
| * Appends individual diffs to collection. |
| */ |
| void appendDiffs(Collection<NodeDiff> collection) { |
| |
| if (otherDiffs != null) { |
| collection.addAll(otherDiffs); |
| } |
| |
| collection.add(new NodeDiff(nodeId, diffId) { |
| |
| @Override |
| public void apply(GraphChangeHandler tracker) { |
| applySimplePropertyChanges(tracker); |
| } |
| |
| @Override |
| public void undo(GraphChangeHandler tracker) { |
| throw new UnsupportedOperationException(); |
| } |
| }); |
| } |
| |
| void addDiff(NodeDiff diff, ObjectStore parent) { |
| |
| boolean addDiff = true; |
| |
| if (diff instanceof ArcOperation) { |
| |
| ArcOperation arcDiff = (ArcOperation) diff; |
| Object targetId = arcDiff.getTargetNodeId(); |
| ArcId arcId = arcDiff.getArcId(); |
| |
| ArcProperty property = (ArcProperty) getClassDescriptor().getProperty(arcId.getForwardArc()); |
| |
| // note that some collection properties implement |
| // 'SingleObjectArcProperty', |
| // so we cant't do 'instanceof SingleObjectArcProperty' |
| // TODO: andrus, 3.22.2006 - should we consider this a bug? |
| |
| if (property == null && arcId.getForwardArc().startsWith(ASTDbPath.DB_PREFIX)) { |
| addPhantomFkDiff(arcDiff); |
| addDiff = false; |
| } else if (property instanceof ToManyProperty) { |
| |
| // record flattened op changes |
| |
| ObjRelationship relationship = property.getRelationship(); |
| if (relationship.isFlattened()) { |
| |
| if (flatIds == null) { |
| flatIds = new HashMap<>(); |
| } |
| |
| ArcOperation oldOp = flatIds.put(arcDiff, arcDiff); |
| |
| // "delete" cancels "create" and vice versa... |
| if (oldOp != null && oldOp.isDelete() != arcDiff.isDelete()) { |
| addDiff = false; |
| flatIds.remove(arcDiff); |
| |
| if (otherDiffs != null) { |
| otherDiffs.remove(oldOp); |
| } |
| } |
| } else if (property.getComplimentaryReverseArc() == null) { |
| |
| // register complimentary arc diff |
| ArcId arc = arcId.getReverseId(); |
| //new ArcId(ASTDbPath.DB_PREFIX + property.getComplimentaryReverseDbRelationshipPath(), property.getName()); |
| ArcOperation complimentaryOp = new ArcOperation(targetId, arcDiff.getNodeId(), arc, arcDiff.isDelete()); |
| parent.registerDiff(targetId, complimentaryOp); |
| } |
| |
| } else if (property instanceof ToOneProperty) { |
| |
| if (currentArcSnapshot == null) { |
| currentArcSnapshot = new HashMap<>(); |
| } |
| |
| currentArcSnapshot.put(arcId.getForwardArc(), targetId); |
| } else { |
| String message = (property == null) ? "No property for arcId " + arcId |
| : "Unrecognized property for arcId " + arcId + ": " + property; |
| throw new CayenneRuntimeException(message); |
| } |
| } |
| |
| if (addDiff) { |
| if (otherDiffs == null) { |
| otherDiffs = new ArrayList<>(3); |
| } |
| |
| otherDiffs.add(diff); |
| } |
| } |
| |
| private void addPhantomFkDiff(ArcOperation arcDiff) { |
| String arcId = arcDiff.getArcId().toString(); |
| |
| DbEntity dbEntity = classDescriptor.getEntity().getDbEntity(); |
| DbRelationship dbRelationship = dbEntity.getRelationship(arcId.substring(ASTDbPath.DB_PREFIX.length())); |
| |
| if (dbRelationship.isToMany()) { |
| return; |
| } |
| |
| if (currentArcSnapshot == null) { |
| currentArcSnapshot = new HashMap<>(); |
| } |
| |
| currentArcSnapshot.put(arcId, arcDiff.getTargetNodeId()); |
| |
| if (phantomFks == null) { |
| phantomFks = new HashMap<>(); |
| } |
| |
| ArcOperation oldOp = phantomFks.put(arcDiff, arcDiff); |
| |
| // "delete" cancels "create" and vice versa... |
| if (oldOp != null && oldOp.isDelete() != arcDiff.isDelete()) { |
| phantomFks.remove(arcDiff); |
| |
| if (otherDiffs != null) { |
| otherDiffs.remove(oldOp); |
| } |
| } |
| |
| } |
| |
| /** |
| * Checks whether at least a single property is modified. |
| */ |
| @Override |
| public boolean isNoop() { |
| |
| // if we have no baseline to compare with, assume that there are changes |
| if (snapshot == null) { |
| return false; |
| } |
| |
| if (flatIds != null && !flatIds.isEmpty()) { |
| return false; |
| } |
| |
| if (phantomFks != null && !phantomFks.isEmpty()) { |
| return false; |
| } |
| |
| int state = object.getPersistenceState(); |
| if (state == PersistenceState.NEW || state == PersistenceState.DELETED) { |
| return false; |
| } |
| |
| // check phantom mods |
| |
| final boolean[] modFound = new boolean[1]; |
| getClassDescriptor().visitProperties(new PropertyVisitor() { |
| |
| @Override |
| public boolean visitAttribute(AttributeProperty property) { |
| |
| Object oldValue = snapshot.get(property.getName()); |
| Object newValue = property.readProperty(object); |
| |
| if (!property.equals(oldValue, newValue)) { |
| modFound[0] = true; |
| } |
| |
| return !modFound[0]; |
| } |
| |
| @Override |
| public boolean visitToMany(ToManyProperty property) { |
| // flattened changes |
| return true; |
| } |
| |
| @Override |
| public boolean visitToOne(ToOneProperty property) { |
| if (arcSnapshot == null) { |
| return true; |
| } |
| |
| Object newValue = property.readPropertyDirectly(object); |
| if (newValue instanceof Fault) { |
| return true; |
| } |
| |
| Object oldValue = arcSnapshot.get(property.getName()); |
| if (!property.equals(oldValue, newValue != null ? ((Persistent) newValue).getObjectId() : null)) { |
| modFound[0] = true; |
| } |
| |
| return !modFound[0]; |
| } |
| }); |
| |
| return !modFound[0]; |
| } |
| |
| @Override |
| public void undo(GraphChangeHandler handler) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public void apply(final GraphChangeHandler handler) { |
| |
| if (otherDiffs != null) { |
| for (final GraphDiff diff : otherDiffs) { |
| diff.apply(handler); |
| } |
| } |
| |
| // phantomFks are not in 'otherDiffs', while flattened diffs are |
| if (phantomFks != null) { |
| for (GraphDiff diff : phantomFks.keySet()) { |
| diff.apply(handler); |
| } |
| } |
| |
| applySimplePropertyChanges(handler); |
| } |
| |
| private void applySimplePropertyChanges(final GraphChangeHandler handler) { |
| |
| getClassDescriptor().visitProperties(new PropertyVisitor() { |
| |
| @Override |
| public boolean visitAttribute(AttributeProperty property) { |
| |
| Object newValue = property.readProperty(object); |
| |
| // no baseline to compare |
| if (snapshot == null) { |
| |
| if (newValue != null) { |
| handler.nodePropertyChanged(nodeId, property.getName(), null, newValue); |
| } |
| } |
| // have baseline to compare |
| else { |
| Object oldValue = snapshot.get(property.getName()); |
| |
| if (!property.equals(oldValue, newValue)) { |
| handler.nodePropertyChanged(nodeId, property.getName(), oldValue, newValue); |
| } |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public boolean visitToMany(ToManyProperty property) { |
| return true; |
| } |
| |
| @Override |
| public boolean visitToOne(ToOneProperty property) { |
| return true; |
| } |
| }); |
| } |
| |
| /** |
| * This is used to update faults. |
| */ |
| void updateArcSnapshot(String propertyName, Persistent object) { |
| if (arcSnapshot == null) { |
| arcSnapshot = new HashMap<>(); |
| } |
| |
| arcSnapshot.put(propertyName, object != null ? object.getObjectId() : null); |
| } |
| |
| static final class ArcOperation extends NodeDiff { |
| |
| private Object targetNodeId; |
| private ArcId arcId; |
| private boolean delete; |
| |
| ArcOperation(Object nodeId, Object targetNodeId, ArcId arcId, boolean delete) { |
| |
| super(nodeId); |
| this.targetNodeId = targetNodeId; |
| this.arcId = arcId; |
| this.delete = delete; |
| } |
| |
| boolean isDelete() { |
| return delete; |
| } |
| |
| @Override |
| public int hashCode() { |
| // assuming String and ObjectId provide a good hashCode |
| return 31 * arcId.hashCode() + targetNodeId.hashCode(); |
| } |
| |
| @Override |
| public boolean equals(Object object) { |
| // compare ignoring op type... |
| if (object == this) { |
| return true; |
| } |
| |
| if (object == null) { |
| return false; |
| } |
| |
| if (!(object instanceof ArcOperation)) { |
| return false; |
| } |
| |
| ArcOperation other = (ArcOperation) object; |
| return arcId.equals(other.arcId) && Util.nullSafeEquals(targetNodeId, other.targetNodeId); |
| } |
| |
| @Override |
| public void apply(GraphChangeHandler tracker) { |
| |
| if (delete) { |
| tracker.arcDeleted(nodeId, targetNodeId, arcId); |
| } else { |
| tracker.arcCreated(nodeId, targetNodeId, arcId); |
| } |
| } |
| |
| @Override |
| public void undo(GraphChangeHandler tracker) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public ArcId getArcId() { |
| return arcId; |
| } |
| |
| public Object getTargetNodeId() { |
| return targetNodeId; |
| } |
| } |
| } |