content: /*****************************************************************
   *   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 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.exp.path.CayennePath;
  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;
  
  /**
   * 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().value();
  
                  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)) {
                      if(attr.isLazy()) {
                          property.writePropertyDirectly(object, null, new AttributeFault(property));
                      } else {
                          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().value();
  
                  // 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 (property.equals(curValue, oldValue)
                              && !property.equals(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())
                                      || !property.equals(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.get(join.getSourceName()) == null) {
                  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 !property.equals(currentId, targetId);
      }
  
      // not for instantiation
      DataRowUtils() {
      }
  }
  