File: System\Data\Services\Client\AtomMaterializerLog.cs
Project: ndp\fx\src\DataWeb\Client\System.Data.Services.Client.csproj (System.Data.Services.Client)
//---------------------------------------------------------------------
// <copyright file="AtomMaterializerLog.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
// Provides a class that can keep a record of changes done to the
// data service state.
// </summary>
//---------------------------------------------------------------------
 
namespace System.Data.Services.Client
{
    #region Namespaces.
 
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Reflection;
    using System.Xml;
    using System.Xml.Linq;
    using System.Text;
 
    #endregion Namespaces.
 
    /// <summary>
    /// Use this class to keep a log of changes done by the materializer.
    /// </summary>
    internal class AtomMaterializerLog
    {
        #region Private fields.
 
        /// <summary>Associated data context for the log.</summary>
        private readonly DataServiceContext context;
 
        /// <summary>Dictionary of identity URI to instances created during previous AppendOnly moves.</summary>
        private readonly Dictionary<String, AtomEntry> appendOnlyEntries;
 
        /// <summary>Dictionary of identity URI to entries with media that aren't otherwise tracked.</summary>
        private readonly Dictionary<String, AtomEntry> foundEntriesWithMedia;
 
        /// <summary>Dictionary of identity URI to tracked entities.</summary>
        private readonly Dictionary<String, AtomEntry> identityStack;
 
        /// <summary>List of link descriptors (data for links and state).</summary>
        private readonly List<LinkDescriptor> links;
 
        /// <summary>Merge option used to apply changes.</summary>
        private readonly MergeOption mergeOption;
 
        /// <summary>Target instance to refresh.</summary>
        private object insertRefreshObject;
 
        #endregion Private fields.
 
        #region Constructors.
 
        /// <summary>
        /// Initializes a new <see cref="AtomMaterializerLog"/> instance.
        /// </summary>
        /// <param name="context">Associated data context for the log.</param>
        /// <param name="mergeOption">Merge option used to apply changes.</param>
        /// <remarks>
        /// Note that the merge option can't be changed.
        /// </remarks>
        internal AtomMaterializerLog(DataServiceContext context, MergeOption mergeOption)
        {
            Debug.Assert(context != null, "context != null");
            this.appendOnlyEntries = new Dictionary<string, AtomEntry>(EqualityComparer<String>.Default);
            this.context = context;
            this.mergeOption = mergeOption;
            this.foundEntriesWithMedia = new Dictionary<String, AtomEntry>(EqualityComparer<String>.Default);
            this.identityStack = new Dictionary<String, AtomEntry>(EqualityComparer<String>.Default);
            this.links = new List<LinkDescriptor>();
        }
 
        #endregion Constructors.
 
        #region Internal properties.
 
        /// <summary>Whether changes are being tracked.</summary>
        internal bool Tracking
        {
            get 
            { 
                return this.mergeOption != MergeOption.NoTracking; 
            }
        }
 
        #endregion Internal properties.
 
        #region Internal methods.
 
        /// <summary>Applies all accumulated changes to the associated data context.</summary>
        /// <remarks>The log should be cleared after this method successfully executed.</remarks>
        internal void ApplyToContext()
        {
            Debug.Assert(
                this.mergeOption != MergeOption.OverwriteChanges || this.foundEntriesWithMedia.Count == 0,
                "mergeOption != MergeOption.OverwriteChanges || foundEntriesWithMedia.Count == 0 - we only use the 'entries-with-media' lookaside when we're not in overwrite mode, otherwise we track everything through identity stack");
 
            if (!this.Tracking)
            {
                return;
            }
 
            foreach (KeyValuePair<String, AtomEntry> entity in this.identityStack)
            {
                AtomEntry entry = entity.Value;
                if (entry.CreatedByMaterializer ||
                    entry.ResolvedObject == this.insertRefreshObject ||
                    entry.ShouldUpdateFromPayload)
                {
                    // Create a new descriptor and try to attach, if one already exists, get the existing reference instead.
                    EntityDescriptor descriptor = new EntityDescriptor(entity.Key, entry.QueryLink, entry.EditLink, entry.ResolvedObject, null, null, null, entry.ETagText, EntityStates.Unchanged);
                    descriptor = this.context.InternalAttachEntityDescriptor(descriptor, false);
 
                    // we should always reset descriptor's state to Unchanged (old v1 behaviour)
                    descriptor.State = EntityStates.Unchanged;
 
                    this.ApplyMediaEntryInformation(entry, descriptor);
                    descriptor.ServerTypeName = entry.TypeName;
                }
                else
                {
                    // Refresh the entity state indirectly by calling TryGetEntity.
                    EntityStates state;
                    this.context.TryGetEntity(entity.Key, entry.ETagText, this.mergeOption, out state);
                }
            }
 
            // Regardless of the merge mode, media link information should
            // always be applied to the context.
            foreach (AtomEntry entry in this.foundEntriesWithMedia.Values)
            {
                Debug.Assert(entry.ResolvedObject != null, "entry.ResolvedObject != null -- otherwise it wasn't found");
                EntityDescriptor descriptor = this.context.GetEntityDescriptor(entry.ResolvedObject);
                this.ApplyMediaEntryInformation(entry, descriptor);
            }
 
            foreach (LinkDescriptor link in this.links)
            {
                if (EntityStates.Added == link.State)
                {
                    // Added implies collection
                    if ((EntityStates.Deleted == this.context.GetEntityDescriptor(link.Target).State) ||
                        (EntityStates.Deleted == this.context.GetEntityDescriptor(link.Source).State))
                    {
                        this.context.DeleteLink(link.Source, link.SourceProperty, link.Target);
                    }
                    else
                    {
                        this.context.AttachLink(link.Source, link.SourceProperty, link.Target, this.mergeOption);
                    }
                }
                else if (EntityStates.Modified == link.State)
                {
                    // Modified implies reference
                    object target = link.Target;
                    if (MergeOption.PreserveChanges == this.mergeOption)
                    {
                        LinkDescriptor end = this.context.GetLinks(link.Source, link.SourceProperty).FirstOrDefault();
                        if (null != end && null == end.Target)
                        {
                            // leave the SetLink(link.Source, link.SourceProperty, null)
                            continue;
                        }
 
                        if ((null != target) && (EntityStates.Deleted == this.context.GetEntityDescriptor(target).State) ||
                            (EntityStates.Deleted == this.context.GetEntityDescriptor(link.Source).State))
                        {
                            target = null;
                        }
                    }
 
                    this.context.AttachLink(link.Source, link.SourceProperty, target, this.mergeOption);
                }
                else
                {
                    // detach link
                    Debug.Assert(EntityStates.Detached == link.State, "not detached link");
                    this.context.DetachLink(link.Source, link.SourceProperty, link.Target);
                }
            }
        }
 
        /// <summary>Clears all state in the log.</summary>
        internal void Clear()
        {
            this.foundEntriesWithMedia.Clear();
            this.identityStack.Clear();
            this.links.Clear();
            this.insertRefreshObject = null;
        }
 
        /// <summary>
        /// Invoke this method to notify the log that an existing
        /// instance was found while resolving an object.
        /// </summary>
        /// <param name="entry">Entry for instance.</param>
        internal void FoundExistingInstance(AtomEntry entry)
        {
            Debug.Assert(entry != null, "entry != null");
            Debug.Assert(ShouldTrackWithContext(entry), "Existing entries should be entity");
 
            if (this.mergeOption == MergeOption.OverwriteChanges)
            {
                this.identityStack[entry.Identity] = entry;
            }
            else if (this.Tracking && entry.MediaLinkEntry == true)
            {
                this.foundEntriesWithMedia[entry.Identity] = entry;
            }
        }
 
        /// <summary>
        /// Invoke this method to notify the log that the
        /// target instance of a "directed" update was found.
        /// </summary>
        /// <param name="entry">Entry found.</param>
        /// <remarks>
        /// The target instance is typically the object that we
        /// expect will get refreshed by the response from a POST
        /// method.
        /// 
        /// For example if a create a Customer and POST it to
        /// a service, the response of the POST will return the
        /// re-serialized instance, with (important!) server generated
        /// values and URIs.
        /// </remarks>
        internal void FoundTargetInstance(AtomEntry entry)
        {
            Debug.Assert(entry != null, "entry != null");
            Debug.Assert(entry.ResolvedObject != null, "entry.ResolvedObject != null -- otherwise this is not a target");
 
            if (ShouldTrackWithContext(entry))
            {
                this.context.AttachIdentity(entry.Identity, entry.QueryLink, entry.EditLink, entry.ResolvedObject, entry.ETagText);
                this.identityStack.Add(entry.Identity, entry);
                this.insertRefreshObject = entry.ResolvedObject;
            }
        }
 
        /// <summary>Attempts to resolve an entry from those tracked in the log.</summary>
        /// <param name="entry">Entry to resolve.</param>
        /// <param name="existingEntry">
        /// After invocation, an existing entry with the same identity as 
        /// <paramref name="entry"/>; possibly null.
        /// </param>
        /// <returns>true if an existing entry was found; false otherwise.</returns>
        internal bool TryResolve(AtomEntry entry, out AtomEntry existingEntry)
        {
            Debug.Assert(entry != null, "entry != null");
            Debug.Assert(entry.Identity != null, "entry.Identity != null");
 
            if (this.identityStack.TryGetValue(entry.Identity, out existingEntry))
            {
                return true;
            }
 
            if (this.appendOnlyEntries.TryGetValue(entry.Identity, out existingEntry))
            {
                // The AppendOnly entries are valid only as long as they were not modified
                // between calls to .MoveNext().
                EntityStates state;
                this.context.TryGetEntity(entry.Identity, entry.ETagText, this.mergeOption, out state);
                if (state == EntityStates.Unchanged)
                {
                    return true;
                }
                else
                {
                    this.appendOnlyEntries.Remove(entry.Identity);
                }
            }
 
            existingEntry = null;
            return false;
        }
 
        /// <summary>
        /// Invoke this method to notify the log that a new link was
        /// added to a collection.
        /// </summary>
        /// <param name="source">
        /// Instance with the collection to which <paramref name="target"/> 
        /// was added.
        /// </param>
        /// <param name="propertyName">Property name for collection.</param>
        /// <param name="target">Object which was added.</param>
        internal void AddedLink(AtomEntry source, string propertyName, object target)
        {
            Debug.Assert(source != null, "source != null");
            Debug.Assert(propertyName != null, "propertyName != null");
 
            if (!this.Tracking)
            {
                return;
            }
 
            if (ShouldTrackWithContext(source) && ShouldTrackWithContext(target))
            {
                LinkDescriptor item = new LinkDescriptor(source.ResolvedObject, propertyName, target, EntityStates.Added);
                this.links.Add(item);
            }
        }
 
        /// <summary>
        /// Invoke this method to notify the log that a new instance
        /// was created.
        /// </summary>
        /// <param name="entry">Entry for the created instance.</param>
        internal void CreatedInstance(AtomEntry entry)
        {
            Debug.Assert(entry != null, "entry != null");
            Debug.Assert(entry.ResolvedObject != null, "entry.ResolvedObject != null -- otherwise, what did we create?");
            Debug.Assert(entry.CreatedByMaterializer, "entry.CreatedByMaterializer -- otherwise we shouldn't be calling this");
 
            if (ShouldTrackWithContext(entry))
            {
                this.identityStack.Add(entry.Identity, entry);
                if (this.mergeOption == MergeOption.AppendOnly)
                {
                    this.appendOnlyEntries.Add(entry.Identity, entry);
                }
            }
        }
 
        /// <summary>
        /// Invoke this method to notify the log that a link was removed 
        /// from a collection.
        /// </summary>
        /// <param name="source">
        /// Instance with the collection from which <paramref name="target"/> 
        /// was removed.
        /// </param>
        /// <param name="propertyName">Property name for collection.</param>
        /// <param name="target">Object which was removed.</param>
        internal void RemovedLink(AtomEntry source, string propertyName, object target)
        {
            Debug.Assert(source != null, "source != null");
            Debug.Assert(propertyName != null, "propertyName != null");
 
            if (ShouldTrackWithContext(source) && ShouldTrackWithContext(target))
            {
                Debug.Assert(this.Tracking, "this.Tracking -- otherwise there's an 'if' missing (it happens to be that the assert holds for all current callers");
                LinkDescriptor item = new LinkDescriptor(source.ResolvedObject, propertyName, target, EntityStates.Detached);
                this.links.Add(item);
            }
        }
 
        /// <summary>
        /// Invoke this method to notify the log that a link was set on
        /// a property.
        /// </summary>
        /// <param name="source">Entry for source object.</param>
        /// <param name="propertyName">Name of property set.</param>
        /// <param name="target">Target object.</param>
        internal void SetLink(AtomEntry source, string propertyName, object target)
        {
            Debug.Assert(source != null, "source != null");
            Debug.Assert(propertyName != null, "propertyName != null");
 
            if (!this.Tracking)
            {
                return;
            }
 
            if (ShouldTrackWithContext(source) && ShouldTrackWithContext(target))
            {
                Debug.Assert(this.Tracking, "this.Tracking -- otherwise there's an 'if' missing (it happens to be that the assert holds for all current callers");
                LinkDescriptor item = new LinkDescriptor(source.ResolvedObject, propertyName, target, EntityStates.Modified);
                this.links.Add(item);
            }
        }
 
        #endregion Internal methods.
 
        #region Private methods.
 
        /// <summary>
        /// Returns true if we should track this entry with context
        /// </summary>
        /// <param name="entry">The atom entry</param>
        /// <returns>true if entry should be tracked</returns>
        private static bool ShouldTrackWithContext(AtomEntry entry)
        {
            Debug.Assert(entry.ActualType != null, "Entry with no type added to log");
            return entry.ActualType.IsEntityType;
        }
 
        /// <summary>
        /// Returns true if we should track this entity with context
        /// </summary>
        /// <param name="entity">The resolved instance</param>
        /// <returns>true if entry should be tracked</returns>
        private static bool ShouldTrackWithContext(object entity)
        {
            if (entity == null)
            {
                // you can set link to null, we need to track these values
                return true;
            }
 
            ClientType type = ClientType.Create(entity.GetType());
            return type.IsEntityType;
        }
 
        /// <summary>
        /// Applies the media entry information (if any) to the specified 
        /// <paramref name="descriptor"/>.
        /// </summary>
        /// <param name="entry">Entry with (potential) media entry information to apply.</param>
        /// <param name="descriptor">Descriptor to update.</param>
        private void ApplyMediaEntryInformation(AtomEntry entry, EntityDescriptor descriptor)
        {
            Debug.Assert(entry != null, "entry != null");
            Debug.Assert(descriptor != null, "descriptor != null");
 
            if (entry.MediaEditUri != null || entry.MediaContentUri != null)
            {
                // 
 
                if (entry.MediaEditUri != null)
                {
                    descriptor.EditStreamUri = new Uri(this.context.BaseUriWithSlash, entry.MediaEditUri);
                }
 
                if (entry.MediaContentUri != null)
                {
                    descriptor.ReadStreamUri = new Uri(this.context.BaseUriWithSlash, entry.MediaContentUri);
                }
 
                descriptor.StreamETag = entry.StreamETagText;
            }
        }
 
        #endregion Private methods.
    }
}