File: System\Activities\DynamicUpdate\DynamicUpdateMap.cs
Project: ndp\cdf\src\NetFx40\System.Activities\System.Activities.csproj (System.Activities)
// <copyright>
//   Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
 
namespace System.Activities.DynamicUpdate
{
    using System;
    using System.Activities.DynamicUpdate;
    using System.Activities.XamlIntegration;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Globalization;
    using System.Runtime;
    using System.Runtime.Serialization;
 
    [DataContract]
    [TypeConverter(typeof(DynamicUpdateMapConverter))]
    public class DynamicUpdateMap
    {
        static DynamicUpdateMap noChanges = new DynamicUpdateMap();
        static DynamicUpdateMap dummyMap = new DynamicUpdateMap();
 
        internal EntryCollection entries;        
        IList<ArgumentInfo> newArguments;
        IList<ArgumentInfo> oldArguments;        
        
        internal DynamicUpdateMap()
        {
        }
 
        public static DynamicUpdateMap NoChanges
        {
            get
            {
                return noChanges;
            }
        }
 
        [DataMember(EmitDefaultValue = false, Name = "entries")]
        internal EntryCollection SerializedEntries
        {
            get { return this.entries; }
            set { this.entries = value; }
        }
 
        [DataMember(EmitDefaultValue = false, Name = "newArguments")]
        internal IList<ArgumentInfo> SerializedNewArguments
        {
            get { return this.newArguments; }
            set { this.newArguments = value; }
        }
 
        [DataMember(EmitDefaultValue = false, Name = "oldArguments")]
        internal IList<ArgumentInfo> SerializedOldArguments
        {
            get { return this.oldArguments; }
            set { this.oldArguments = value; }
        }
 
        // this is a dummy map to be used for creating a NativeActivityUpdateContext
        // for calling UpdateInstance() on activities without map entries.
        // this should not be used anywhere except for creating NativeActivityUpdateContext.
        internal static DynamicUpdateMap DummyMap
        {
            get { return dummyMap; }
        }
 
        internal IList<ArgumentInfo> NewArguments
        {
            get
            {
                if (this.newArguments == null)
                {
                    this.newArguments = new List<ArgumentInfo>();
                }
                return this.newArguments;
            }
            set
            {
                this.newArguments = value;
            }
        }
 
        internal IList<ArgumentInfo> OldArguments
        {
            get
            {
                if (this.oldArguments == null)
                {
                    this.oldArguments = new List<ArgumentInfo>();
                }
                return this.oldArguments;
            }
            set
            {
                this.oldArguments = value;
            }
        }
 
        [DataMember(EmitDefaultValue = false)]
        internal bool ArgumentsAreUnknown
        {
            get;
            set;
        }
 
        [DataMember(EmitDefaultValue = false)]
        internal bool IsImplementationAsRoot
        {
            get;
            set;
        }
 
        [DataMember(EmitDefaultValue = false)]
        internal int NewDefinitionMemberCount
        {
            get;
            set;
        }
 
        internal int OldDefinitionMemberCount
        {
            get
            {
                return this.Entries.Count;
            }
        }
 
        [DataMember(EmitDefaultValue = false)]
        internal bool IsForImplementation { get; set; }
 
        // IdSpaces always have at least one member. So a count of 0 means that this is
        // DynamicUpdateMap.NoChanges, or a serialized equivalent.
        internal bool IsNoChanges
        {
            get
            {
                return this.NewDefinitionMemberCount == 0;
            }
        }
 
        // use the internal method AddEntry() instead
        private IList<DynamicUpdateMapEntry> Entries
        {
            get
            {
                if (this.entries == null)
                {
                    this.entries = new EntryCollection();
                }
 
                return this.entries;
            }
        }
 
        public static IDictionary<object, DynamicUpdateMapItem> CalculateMapItems(Activity workflowDefinitionToBeUpdated)
        {
            return CalculateMapItems(workflowDefinitionToBeUpdated, null);
        }
 
        public static IDictionary<object, DynamicUpdateMapItem> CalculateMapItems(Activity workflowDefinitionToBeUpdated, LocationReferenceEnvironment environment)
        {
            return InternalCalculateMapItems(workflowDefinitionToBeUpdated, environment, false);
        }
 
        public static IDictionary<object, DynamicUpdateMapItem> CalculateImplementationMapItems(Activity activityDefinitionToBeUpdated)
        {
            return CalculateImplementationMapItems(activityDefinitionToBeUpdated, null);
        }
 
        public static IDictionary<object, DynamicUpdateMapItem> CalculateImplementationMapItems(Activity activityDefinitionToBeUpdated, LocationReferenceEnvironment environment)
        {
            return InternalCalculateMapItems(activityDefinitionToBeUpdated, environment, true);
        }
 
        public static DynamicUpdateMap Merge(params DynamicUpdateMap[] maps)
        {
            return Merge((IEnumerable<DynamicUpdateMap>)maps);
        }
 
        public static DynamicUpdateMap Merge(IEnumerable<DynamicUpdateMap> maps)
        {
            if (maps == null)
            {
                throw FxTrace.Exception.ArgumentNull("maps");
            }
 
            // We could try to optimize this by merging the entire set at once, but it's simpler
            // to just do pairwise merging
            int index = 0;
            DynamicUpdateMap result = null;
            foreach (DynamicUpdateMap nextMap in maps)
            {
                result = Merge(result, nextMap, new MergeErrorContext { MapIndex = index });
                index++;
            }
 
            return result;
        }
 
        static IDictionary<object, DynamicUpdateMapItem> InternalCalculateMapItems(Activity workflowDefinitionToBeUpdated, LocationReferenceEnvironment environment, bool forImplementation)
        {
            if (workflowDefinitionToBeUpdated == null)
            {
                throw FxTrace.Exception.ArgumentNull("workflowDefinitionToBeUpdated");
            }
 
            DynamicUpdateMapBuilder.Preparer preparer = new DynamicUpdateMapBuilder.Preparer(workflowDefinitionToBeUpdated, environment, forImplementation);
            return preparer.Prepare();
        }        
 
        public DynamicUpdateMapQuery Query(Activity updatedWorkflowDefinition, Activity originalWorkflowDefinition)
        {            
            if (this.IsNoChanges)
            {
                throw FxTrace.Exception.AsError(new InvalidOperationException(SR.NoChangesMapQueryNotSupported));
            }
 
            if (this.IsForImplementation)
            {
                ValidateDefinitionMatchesImplementationMap(updatedWorkflowDefinition, this.NewDefinitionMemberCount, "updatedWorkflowDefinition");
                ValidateDefinitionMatchesImplementationMap(originalWorkflowDefinition, this.OldDefinitionMemberCount, "originalWorkflowDefinition");
            }
            else
            {
                ValidateDefinitionMatchesMap(updatedWorkflowDefinition, this.NewDefinitionMemberCount, "updatedWorkflowDefinition");
                ValidateDefinitionMatchesMap(originalWorkflowDefinition, this.OldDefinitionMemberCount, "originalWorkflowDefinition");
            }            
 
            return new DynamicUpdateMapQuery(this, updatedWorkflowDefinition, originalWorkflowDefinition);
        }
 
        internal static bool CanUseImplementationMapAsRoot(Activity workflowDefinition)
        {
            Fx.Assert(workflowDefinition.IsMetadataCached, "This should only be called for cached definition");
 
            // We can only use the implementation map as a root map if the worklflow has no public children
            return
                workflowDefinition.Children.Count == 0 &&
                workflowDefinition.ImportedChildren.Count == 0 &&
                workflowDefinition.Delegates.Count == 0 &&
                workflowDefinition.ImportedDelegates.Count == 0 &&
                workflowDefinition.RuntimeVariables.Count == 0;
        }
 
        internal static DynamicUpdateMap Merge(DynamicUpdateMap first, DynamicUpdateMap second, MergeErrorContext errorContext)
        {
            if (first == null || second == null)
            {
                return first ?? second;
            }
 
            if (first.IsNoChanges || second.IsNoChanges)
            {
                // DynamicUpdateMap.NoChanges has zero members, so we need to special-case it here.
                return first.IsNoChanges ? second : first;
            }
 
            ThrowIfMapsIncompatible(first, second, errorContext);
 
            DynamicUpdateMap result = new DynamicUpdateMap
            {
                IsForImplementation = first.IsForImplementation,
                NewDefinitionMemberCount = second.NewDefinitionMemberCount,
                ArgumentsAreUnknown = first.ArgumentsAreUnknown && second.ArgumentsAreUnknown,
                oldArguments = first.ArgumentsAreUnknown ? second.oldArguments : first.oldArguments,
                newArguments = second.ArgumentsAreUnknown ? first.newArguments : second.newArguments
            };
 
            foreach (DynamicUpdateMapEntry firstEntry in first.Entries)
            {
                DynamicUpdateMapEntry parent = null;
                if (firstEntry.Parent != null)
                {
                    result.TryGetUpdateEntry(firstEntry.Parent.OldActivityId, out parent);
                }
 
                if (firstEntry.IsRemoval)
                {
                    result.AddEntry(firstEntry.Clone(parent));
                }
                else
                {
                    DynamicUpdateMapEntry secondEntry = second.entries[firstEntry.NewActivityId];
                    result.AddEntry(DynamicUpdateMapEntry.Merge(firstEntry, secondEntry, parent, errorContext));
                }
            }
 
            return result;
        }        
 
        internal void AddEntry(DynamicUpdateMapEntry entry)
        {
            this.Entries.Add(entry);
        }
 
        // Wrap an implementation map in a dummy map. This allows use of an implementation map as the
        // root map in the case when the root is an x:Class with no public children.
        internal DynamicUpdateMap AsRootMap()
        {
            Fx.Assert(this.IsForImplementation, "This should only be called on implementation map");
 
            if (!ActivityComparer.ListEquals(this.NewArguments, this.OldArguments))
            {
                throw FxTrace.Exception.AsError(new InstanceUpdateException(SR.InvalidImplementationAsWorkflowRootForRuntimeStateBecauseArgumentsChanged));
            }
 
            DynamicUpdateMap result = new DynamicUpdateMap
            {
                IsImplementationAsRoot = true,
                NewDefinitionMemberCount = 1
            };
            result.AddEntry(new DynamicUpdateMapEntry(1, 1)
            {
                ImplementationUpdateMap = this,
            });
            return result;
        }
 
        internal void ThrowIfInvalid(Activity updatedDefinition)
        {
            Fx.Assert(updatedDefinition.IsMetadataCached, "Caller should have ensured cached definition");
            Fx.Assert(updatedDefinition.Parent == null && !this.IsForImplementation, "This should only be called on a workflow definition");
 
            this.ThrowIfInvalid(updatedDefinition.MemberOf);
        }
 
        // We verify that the count of all IdSpaces is as expected.
        // We could choose to be looser, and only check the IdSpaces that have children active;
        // but realistically, if all provided implementation maps don't match, something is probably wrong.
        // Conversely, we could check the correctness of every environment map, but it doesn't seem worth
        // doing that much work. If we find a mismatch on the environment of an executing activity, we'll
        // throw at that point.
        void ThrowIfInvalid(IdSpace updatedIdSpace)
        {
            if (this.IsNoChanges)
            {
                // 0 means this is NoChanges map, since every workflow has at least one member
                return;
            }
 
            if (this.NewDefinitionMemberCount != updatedIdSpace.MemberCount)
            {
                throw FxTrace.Exception.AsError(new InstanceUpdateException(SR.InvalidUpdateMap(
                    SR.WrongMemberCount(updatedIdSpace.Owner, updatedIdSpace.MemberCount, this.NewDefinitionMemberCount))));
            }
 
            foreach (DynamicUpdateMapEntry entry in this.Entries)
            {
                if (entry.ImplementationUpdateMap != null)
                {
                    Activity implementationOwner = updatedIdSpace[entry.NewActivityId];
                    if (implementationOwner == null)
                    {
                        string expectedId = entry.NewActivityId.ToString(CultureInfo.InvariantCulture);
                        if (updatedIdSpace.Owner != null)
                        {
                            expectedId = updatedIdSpace.Owner.Id + "." + expectedId;
                        }
                        throw FxTrace.Exception.AsError(new InstanceUpdateException(SR.InvalidUpdateMap(
                            SR.ActivityNotFound(expectedId))));
                    }
 
                    if (implementationOwner.ParentOf == null)
                    {
                        throw FxTrace.Exception.AsError(new InstanceUpdateException(SR.InvalidUpdateMap(
                            SR.ActivityHasNoImplementation(implementationOwner))));
                    }
 
                    entry.ImplementationUpdateMap.ThrowIfInvalid(implementationOwner.ParentOf);
                }
            }
        }
 
        internal bool TryGetUpdateEntryByNewId(int newId, out DynamicUpdateMapEntry entry)
        {
            Fx.Assert(!this.IsNoChanges, "This method is never supposed to be called on the NoChanges map.");
 
            entry = null;
 
            for (int i = 0; i < this.Entries.Count; i++)
            {
                DynamicUpdateMapEntry currentEntry = this.Entries[i];
                if (currentEntry.NewActivityId == newId)
                {
                    entry = currentEntry;
                    return true;
                }
            }
            return false;
        }
 
        internal bool TryGetUpdateEntry(int oldId, out DynamicUpdateMapEntry entry)
        {
            if (this.entries != null && this.entries.Count > 0)
            {
                if (this.entries.Contains(oldId))
                {
                    entry = this.entries[oldId];
                    return true;
                }
            }
 
            entry = null;
            return false;
        }
 
        // rootIdSpace is optional.  if it's null, result.NewActivity will be null
        internal UpdatedActivity GetUpdatedActivity(QualifiedId oldQualifiedId, IdSpace rootIdSpace)
        {
            UpdatedActivity result = new UpdatedActivity();
            int[] oldIdSegments = oldQualifiedId.AsIDArray();
            int[] newIdSegments = null;
            IdSpace currentIdSpace = rootIdSpace;
            DynamicUpdateMap currentMap = this;
 
            Fx.Assert(!this.IsForImplementation, "This method is never supposed to be called on an implementation map.");
 
            for (int i = 0; i < oldIdSegments.Length; i++)
            {
                if (currentMap == null || currentMap.Entries.Count == 0)
                {
                    break;
                }
 
                DynamicUpdateMapEntry entry;
                if (!currentMap.TryGetUpdateEntry(oldIdSegments[i], out entry))
                {
                    // UpdateMap should contain entries for all old activities in the IdSpace
                    int[] subIdSegments = new int[i + 1];
                    Array.Copy(oldIdSegments, subIdSegments, subIdSegments.Length);
                    throw FxTrace.Exception.AsError(new InstanceUpdateException(SR.InvalidUpdateMap(
                        SR.MapEntryNotFound(new QualifiedId(subIdSegments)))));
                }
 
                if (entry.IsIdChange)
                {
                    if (newIdSegments == null)
                    {
                        newIdSegments = new int[oldIdSegments.Length];
                        Array.Copy(oldIdSegments, newIdSegments, oldIdSegments.Length);
                    }
 
                    newIdSegments[i] = entry.NewActivityId;
                }
 
                Activity currentActivity = null;
                if (currentIdSpace != null && !entry.IsRemoval)
                {
                    currentActivity = currentIdSpace[entry.NewActivityId];
                    if (currentActivity == null)
                    {
                        // New Activity pointed to by UpdateMap should exist
                        string activityId = currentIdSpace.Owner.Id + "." + entry.NewActivityId.ToString(CultureInfo.InvariantCulture);
                        throw FxTrace.Exception.AsError(new InstanceUpdateException(SR.InvalidUpdateMap(
                            SR.ActivityNotFound(activityId))));
                    }
                    currentIdSpace = currentActivity.ParentOf;
                }
 
                if (i == oldIdSegments.Length - 1)
                {
                    result.Map = currentMap;
                    result.MapEntry = entry;
                    result.NewActivity = currentActivity;
                }
                else if (entry.IsRuntimeUpdateBlocked || entry.IsUpdateBlockedByUpdateAuthor)
                {
                    currentMap = null;
                }
                else
                {
                    currentMap = entry.ImplementationUpdateMap;
                }
            }
 
            result.IdChanged = newIdSegments != null;
            result.NewId = result.IdChanged ? new QualifiedId(newIdSegments) : oldQualifiedId;
 
            return result;
        }
 
        static void ThrowIfMapsIncompatible(DynamicUpdateMap first, DynamicUpdateMap second, MergeErrorContext errorContext)
        {
            Fx.Assert(!first.IsNoChanges && !second.IsNoChanges, "This method is never supposed to be called on the NoChanges map.");
 
            if (first.IsForImplementation != second.IsForImplementation)
            {
                errorContext.Throw(SR.InvalidMergeMapForImplementation(first.IsForImplementation, second.IsForImplementation));
            }
            if (first.NewDefinitionMemberCount != second.OldDefinitionMemberCount)
            {
                errorContext.Throw(SR.InvalidMergeMapMemberCount(first.NewDefinitionMemberCount, second.OldDefinitionMemberCount));
            }
            if (!first.ArgumentsAreUnknown && !second.ArgumentsAreUnknown && first.IsForImplementation && 
                !ActivityComparer.ListEquals(first.newArguments, second.oldArguments))
            {
                if (first.NewArguments.Count != second.OldArguments.Count)
                {
                    errorContext.Throw(SR.InvalidMergeMapArgumentCount(first.NewArguments.Count, second.OldArguments.Count));
                }
                else
                {
                    errorContext.Throw(SR.InvalidMergeMapArgumentsChanged);
                }
            }
        }
 
        static void ValidateDefinitionMatchesMap(Activity activity, int memberCount, string parameterName)
        {
            if (activity == null)
            {
                throw FxTrace.Exception.ArgumentNull(parameterName);
            }
            if (activity.MemberOf == null)
            {
                throw FxTrace.Exception.Argument(parameterName, SR.ActivityIsUncached);
            }
            if (activity.Parent != null)
            {
                throw FxTrace.Exception.Argument(parameterName, SR.ActivityIsNotRoot);
            }
            if (activity.MemberOf.MemberCount != memberCount)
            {
                throw FxTrace.Exception.Argument(parameterName, SR.InvalidUpdateMap(
                    SR.WrongMemberCount(activity.MemberOf.Owner, activity.MemberOf.MemberCount, memberCount)));
            }
        }
 
        static void ValidateDefinitionMatchesImplementationMap(Activity activity, int memberCount, string parameterName)
        {
            if (activity == null)
            {
                throw FxTrace.Exception.ArgumentNull(parameterName);
            }
            if (activity.MemberOf == null)
            {
                throw FxTrace.Exception.Argument(parameterName, SR.ActivityIsUncached);
            }
            if (activity.Parent != null)
            {
                throw FxTrace.Exception.Argument(parameterName, SR.ActivityIsNotRoot);
            }
            if (activity.ParentOf == null)
            {
                throw FxTrace.Exception.Argument(parameterName, SR.InvalidUpdateMap(
                    SR.ActivityHasNoImplementation(activity)));
            }
            if (activity.ParentOf.MemberCount != memberCount)
            {
                throw FxTrace.Exception.Argument(parameterName, SR.InvalidUpdateMap(
                    SR.WrongMemberCount(activity.ParentOf.Owner, activity.ParentOf.MemberCount, memberCount)));
            }
            if (!CanUseImplementationMapAsRoot(activity))
            {
                throw FxTrace.Exception.Argument(parameterName, SR.InvalidImplementationAsWorkflowRoot);
            }
        }
 
        internal struct UpdatedActivity
        {
            // This can be true even if Map & MapEntry are null, if a parent ID changed.
            // It can also be false even when Map & MapEntry are non-null, if the update didn't produce an ID shift.
            public bool IdChanged;
 
            public QualifiedId NewId;
 
            // Null if the activity's IDSpace wasn't updated.
            public DynamicUpdateMap Map;
            public DynamicUpdateMapEntry MapEntry;
 
            // Null when we're dealing with just a serialized instance with no definition.
            public Activity NewActivity;
        }
 
        internal class MergeErrorContext
        {
            private Stack<int> currentIdSpace;
            public int MapIndex { get; set; }
 
            public void PushIdSpace(int id)
            {
                if (this.currentIdSpace == null)
                {
                    this.currentIdSpace = new Stack<int>();
                }
                this.currentIdSpace.Push(id);
            }
 
            public void PopIdSpace()
            {
                this.currentIdSpace.Pop();
            }
 
            public void Throw(string detail)
            {
                QualifiedId id = null;
                if (this.currentIdSpace != null && this.currentIdSpace.Count > 0)
                {
                    int[] idSegments = new int[this.currentIdSpace.Count];
                    for (int i = idSegments.Length - 1; i >= 0; i--)
                    {
                        idSegments[i] = this.currentIdSpace.Pop();
                    }
                    id = new QualifiedId(idSegments);
                }
 
                string errorMessage;
                if (id == null)
                {
                    errorMessage = SR.InvalidRootMergeMap(this.MapIndex, detail);
                }
                else
                {
                    errorMessage = SR.InvalidMergeMap(this.MapIndex, id, detail);
                }
 
                throw FxTrace.Exception.Argument("maps", errorMessage);
            }
        }
 
        [CollectionDataContract]
        internal class EntryCollection : KeyedCollection<int, DynamicUpdateMapEntry>
        {
            public EntryCollection()
            {
            }
 
            protected override int GetKeyForItem(DynamicUpdateMapEntry item)
            {
                return item.OldActivityId;
            }
        }
    }
}