blob: 64b119458147340120dc0a85e7612c76237e8699 [file] [log] [blame]
/*****************************************************************
* 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() {
}
}