// // Copyright (c) Microsoft Corporation. All rights reserved. // 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 newArguments; IList 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 SerializedNewArguments { get { return this.newArguments; } set { this.newArguments = value; } } [DataMember(EmitDefaultValue = false, Name = "oldArguments")] internal IList 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 NewArguments { get { if (this.newArguments == null) { this.newArguments = new List(); } return this.newArguments; } set { this.newArguments = value; } } internal IList OldArguments { get { if (this.oldArguments == null) { this.oldArguments = new List(); } 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 Entries { get { if (this.entries == null) { this.entries = new EntryCollection(); } return this.entries; } } public static IDictionary CalculateMapItems(Activity workflowDefinitionToBeUpdated) { return CalculateMapItems(workflowDefinitionToBeUpdated, null); } public static IDictionary CalculateMapItems(Activity workflowDefinitionToBeUpdated, LocationReferenceEnvironment environment) { return InternalCalculateMapItems(workflowDefinitionToBeUpdated, environment, false); } public static IDictionary CalculateImplementationMapItems(Activity activityDefinitionToBeUpdated) { return CalculateImplementationMapItems(activityDefinitionToBeUpdated, null); } public static IDictionary CalculateImplementationMapItems(Activity activityDefinitionToBeUpdated, LocationReferenceEnvironment environment) { return InternalCalculateMapItems(activityDefinitionToBeUpdated, environment, true); } public static DynamicUpdateMap Merge(params DynamicUpdateMap[] maps) { return Merge((IEnumerable)maps); } public static DynamicUpdateMap Merge(IEnumerable 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 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 currentIdSpace; public int MapIndex { get; set; } public void PushIdSpace(int id) { if (this.currentIdSpace == null) { this.currentIdSpace = new Stack(); } 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 { public EntryCollection() { } protected override int GetKeyForItem(DynamicUpdateMapEntry item) { return item.OldActivityId; } } } }