/***************************************************************** * 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.util.Map; import org.apache.cayenne.DataRow; import org.apache.cayenne.ObjectId; import org.apache.cayenne.PersistenceState; import org.apache.cayenne.Persistent; import org.apache.cayenne.map.DbJoin; import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.map.ObjAttribute; 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; /** * DataRowUtils contains a number of static methods to work with DataRows. This is a * helper class for DataContext and ObjectStore. * * @since 1.1 */ class DataRowUtils { /** * Merges changes reflected in snapshot map to the object. Changes made to attributes * and to-one relationships will be merged. In case an object is already modified, * modified properties will not be overwritten. */ static void mergeObjectWithSnapshot( DataContext context, ClassDescriptor descriptor, Persistent object, DataRow snapshot) { int state = object.getPersistenceState(); if (state == PersistenceState.HOLLOW || descriptor.getEntity().isReadOnly()) { refreshObjectWithSnapshot(descriptor, object, snapshot, true); } else if (state != PersistenceState.COMMITTED) { forceMergeWithSnapshot(context, descriptor, object, snapshot); } else { // do not invalidate to-many relationships, since they might have // just been prefetched... refreshObjectWithSnapshot(descriptor, object, snapshot, false); } } /** * Replaces all object attribute values with snapshot values. Sets object state to * COMMITTED, unless the snapshot is partial in which case the state is set to HOLLOW */ static void refreshObjectWithSnapshot( ClassDescriptor descriptor, final Persistent object, final DataRow snapshot, final boolean invalidateToManyRelationships) { final boolean[] isPartialSnapshot = new boolean[1]; descriptor.visitProperties(new PropertyVisitor() { public boolean visitAttribute(AttributeProperty property) { ObjAttribute attr = property.getAttribute(); String dbAttrPath = attr.getDbAttributePath(); Object value = snapshot.get(dbAttrPath); property.writePropertyDirectly(object, null, value); // note that a check "snaphsot.get(..) == null" would be incorrect in this // case, as NULL value is entirely valid; still save a map lookup by // checking for the null value first if (value == null && !snapshot.containsKey(dbAttrPath)) { isPartialSnapshot[0] = true; } return true; } public boolean visitToMany(ToManyProperty property) { // "to many" relationships have no information to collect from // snapshot if (invalidateToManyRelationships) { property.invalidate(object); } return true; } public boolean visitToOne(ToOneProperty property) { property.invalidate(object); return true; } }); object.setPersistenceState(isPartialSnapshot[0] ? PersistenceState.HOLLOW : PersistenceState.COMMITTED); } static void forceMergeWithSnapshot( final DataContext context, ClassDescriptor descriptor, final Persistent object, final DataRow snapshot) { final ObjectDiff diff = context .getObjectStore() .getChangesByObjectId() .get(object.getObjectId()); descriptor.visitProperties(new PropertyVisitor() { public boolean visitAttribute(AttributeProperty property) { String dbAttrPath = property.getAttribute().getDbAttributePath(); // supports merging of partial snapshots... // check for null is cheaper than double lookup // for a key... so check for partial snapshot // only if the value is null Object newValue = snapshot.get(dbAttrPath); if (newValue != null || snapshot.containsKey(dbAttrPath)) { Object curValue = property.readPropertyDirectly(object); Object oldValue = diff != null ? diff.getSnapshotValue(property .getName()) : null; // if value not modified, update it from snapshot, // otherwise leave it alone if (Util.nullSafeEquals(curValue, oldValue) && !Util.nullSafeEquals(newValue, curValue)) { property.writePropertyDirectly(object, oldValue, newValue); } } return true; } public boolean visitToMany(ToManyProperty property) { // noop - nothing to merge return true; } public boolean visitToOne(ToOneProperty property) { ObjRelationship relationship = property.getRelationship(); if (relationship.isToPK()) { // TODO: will this work for flattened, how do we save snapshots for // them? // if value not modified, update it from snapshot, // otherwise leave it alone if (!isToOneTargetModified(property, object, diff)) { DbRelationship dbRelationship = relationship .getDbRelationships() .get(0); // must check before creating ObjectId because of partial // snapshots if (hasFK(dbRelationship, snapshot)) { ObjectId id = snapshot.createTargetObjectId( relationship.getTargetEntityName(), dbRelationship); if (diff == null || !diff.containsArcSnapshot(relationship.getName()) || !Util.nullSafeEquals(id, diff .getArcSnapshotValue(relationship.getName()))) { if (id == null) { property.writeProperty(object, null, null); } else { // we can't use 'localObject' if relationship is // optional or inheritance is involved // .. must turn to fault instead if (!relationship .isSourceDefiningTargetPrecenseAndType(context .getEntityResolver())) { property.invalidate(object); } else { property.writeProperty( object, null, context.findOrCreateObject(id)); } } } } } } return true; } }); } static boolean hasFK(DbRelationship relationship, Map<String, Object> snapshot) { for (final DbJoin join : relationship.getJoins()) { if (!snapshot.containsKey(join.getSourceName())) { return false; } } return true; } /** * Checks if an object has its to-one relationship target modified in memory. */ static boolean isToOneTargetModified( ArcProperty property, Persistent object, ObjectDiff diff) { if (object.getPersistenceState() != PersistenceState.MODIFIED || diff == null) { return false; } if (property.isFault(object)) { return false; } Persistent toOneTarget = (Persistent) property.readPropertyDirectly(object); ObjectId currentId = (toOneTarget != null) ? toOneTarget.getObjectId() : null; // if ObjectId is temporary, target is definitely modified... // this would cover NEW objects (what are the other cases of temp id??) if (currentId != null && currentId.isTemporary()) { return true; } if (!diff.containsArcSnapshot(property.getName())) { return false; } ObjectId targetId = diff.getArcSnapshotValue(property.getName()); return !Util.nullSafeEquals(currentId, targetId); } // not for instantiation DataRowUtils() { } }