using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text; using System.Xml; using System.Xml.Schema; using System.IO; using System.Reflection; using System.Diagnostics; using System.Runtime.Serialization; using System.Security.Permissions; using System.Globalization; //using System.Workflow.Activities; using System.Workflow.ComponentModel; using System.Workflow.Runtime; using System.Workflow.Runtime.Hosting; using Hosting = System.Workflow.Runtime.Hosting; using System.Workflow.Runtime.Tracking; namespace System.Workflow.Runtime { /// /// RTTrackingProfile contains functionality specific to the runtime such as /// trackpoint and location matching and caching, cloning, handling dynamic updates... /// internal class RTTrackingProfile : ICloneable // ICloneable is deprecated { #region Private Data Members // // Client defined profile private TrackingProfile _profile = null; // // Type of the workflow that this profile is associated to private Type _workflowType = null; private Type _serviceType = null; // // List of qualified ids and the trackpoints that declared themselves as matches during static examination private Dictionary> _activities = new Dictionary>(); private List _activitiesIgnore = new List(); private Dictionary> _user = new Dictionary>(); private List _userIgnore = new List(); // // Indicates that the RTTrackingProfile instance is private and is safe to modify for a specific instance private bool _isPrivate = false; // // Indicates if a dynamic update is in-flight private bool _pendingWorkflowChange = false; // // The changes for a dynamic update private IList _pendingChanges = null; // // Activities (including those that are being added) can start executing while a dynamic update is pending // These cannot be added to the main cache until the update succeeds because the update might roll back. // However since we have to search for matching track points we might as well save that work. // This list will be copied into the main cache if the dynamic update completes successfully private Dictionary> _dynamicActivities = null; private List _dynamicActivitiesIgnore = null; private Dictionary> _dynamicUser = null; private List _dynamicUserIgnore = null; #endregion #region Constructors /// /// Default constructor /// protected RTTrackingProfile() { } /// /// Primary constructor /// /// /// /// internal RTTrackingProfile(TrackingProfile profile, Activity root, Type serviceType) { if (null == profile) throw new ArgumentNullException("profile"); if (null == root) throw new ArgumentNullException("root"); if (null == serviceType) throw new ArgumentNullException("serviceType"); _workflowType = root.GetType(); _serviceType = serviceType; // // "Clone" a private copy in case the tracking service holds a reference to // the profile it gave us and attempts to modify it at a later point TrackingProfileSerializer tps = new TrackingProfileSerializer(); StringWriter writer = new StringWriter(System.Globalization.CultureInfo.InvariantCulture); StringReader reader = null; TrackingProfile privateProfile = null; try { // // Let exceptions bubble back to the tracking service - // the profile must be valid per the schema. tps.Serialize(writer, profile); reader = new StringReader(writer.ToString()); privateProfile = tps.Deserialize(reader); } finally { if (null != reader) reader.Close(); if (null != writer) writer.Close(); } _profile = privateProfile; CheckAllActivities((Activity)root); } /// /// Constructor used for cloning. /// /// RTTrackingProfile to clone /// All members are shallow copied! Use MakePrivate to deep copy after cloning. private RTTrackingProfile(RTTrackingProfile runtimeProfile) { // // Shallow copy _profile = runtimeProfile._profile; _isPrivate = runtimeProfile._isPrivate; _pendingChanges = runtimeProfile._pendingChanges; _pendingWorkflowChange = runtimeProfile._pendingWorkflowChange; _workflowType = runtimeProfile._workflowType; // // Deep copy the cache. Items in the cache can // be shared but the cache themselves cannot as they may be modified // // Activity match and ignore cache _activities = new Dictionary>(runtimeProfile._activities.Count); foreach (KeyValuePair> kvp in runtimeProfile._activities) _activities.Add(kvp.Key, runtimeProfile._activities[kvp.Key]); _activitiesIgnore = new List(runtimeProfile._activitiesIgnore); // // Pending dynamic update activity match and ignore cache if (null != runtimeProfile._dynamicActivities) { _dynamicActivities = new Dictionary>(runtimeProfile._dynamicActivities.Count); foreach (KeyValuePair> kvp in runtimeProfile._dynamicActivities) _dynamicActivities.Add(kvp.Key, runtimeProfile._dynamicActivities[kvp.Key]); } if (null != runtimeProfile._dynamicActivitiesIgnore) _dynamicActivitiesIgnore = new List(runtimeProfile._dynamicActivitiesIgnore); // // User event match and ignore cache _user = new Dictionary>(runtimeProfile._user.Count); foreach (KeyValuePair> kvp in runtimeProfile._user) _user.Add(kvp.Key, runtimeProfile._user[kvp.Key]); _userIgnore = new List(runtimeProfile._userIgnore); // // Pending dynamic update activity match and ignore cache if (null != runtimeProfile._dynamicUser) { _dynamicUser = new Dictionary>(runtimeProfile._dynamicUser.Count); foreach (KeyValuePair> kvp in runtimeProfile._dynamicUser) _dynamicUser.Add(kvp.Key, kvp.Value); } if (null != runtimeProfile._dynamicUserIgnore) _dynamicUserIgnore = new List(runtimeProfile._dynamicUserIgnore); } #endregion #region Properties /// /// Indicates if the profile is specific to an individual instance. /// internal bool IsPrivate { get { return _isPrivate; } set { if (!(value) && (_isPrivate)) throw new InvalidOperationException(ExecutionStringManager.CannotResetIsPrivate); _isPrivate = value; } } /// /// Type of workflow to which this profile is associated /// internal Type WorkflowType { get { return _workflowType; } } /// /// Version of the profile /// internal Version Version { get { return _profile.Version; } } #endregion #region Internal Methods for Listeners internal bool TryTrackActivityEvent(Activity activity, ActivityExecutionStatus status, IServiceProvider provider, ActivityTrackingRecord record) { List points; // // Check the match caches. if (TryGetCacheItems(activity, out points)) { bool ret = false; foreach (ActivityTrackPointCacheItem item in points) { if (item.HasLocationConditions) { if (!item.Point.IsMatch(activity, status)) continue; } if (item.Events.Contains(status)) { ret = true; item.Point.Track(activity, provider, record.Body); record.Annotations.AddRange(item.Point.Annotations); } } return ret; } return false; } internal bool TryTrackUserEvent(Activity activity, string keyName, object argument, WorkflowExecutor exec, UserTrackingRecord record) { List points; if (TryGetCacheItems(activity, out points)) { bool ret = false; foreach (UserTrackPoint point in points) { if (point.IsMatch(activity, keyName, argument)) { ret = true; point.Track(activity, argument, exec, record.Body); record.Annotations.AddRange(point.Annotations); } } return ret; } return false; } internal bool TryTrackInstanceEvent(TrackingWorkflowEvent status, WorkflowTrackingRecord record) { bool track = false; foreach (WorkflowTrackPoint point in _profile.WorkflowTrackPoints) { if (point.IsMatch(status)) { record.Annotations.AddRange(point.Annotations); track = true; } } return track; } /// /// Called by TrackingListener to determine if a subscription is needed for an activity. /// Also used as an entry point for dynamically building cache entries for dynamically added activities. /// /// /// /// internal bool ActivitySubscriptionNeeded(Activity activity) { List points = null; if ((!_pendingWorkflowChange) || ((_pendingWorkflowChange) && (!IsPendingUpdateActivity(activity, true)))) { // // A dynamic update is not in progress or // the activity is not part of the dynamic update. // The main cache has all matching track points // // bool retry = true; while (retry) { if (_activitiesIgnore.Contains(activity.QualifiedName)) return false; if (_activities.TryGetValue(activity.QualifiedName, out points)) return true; else // // This activity isn't in either cache, look it up in the profile and add to cache CheckActivity(activity); } return false; } else { // // Dynamic update is in progress and this activity is being added as part of the update // Search the profile for matching track points and add them to the dynamic cache // (copied to the main cache at the successful completion of the update) // Don't go through CheckActivity because that adds to the main cache List user = null; if (CreateCacheItems(activity, out user)) CacheInsertUpdatePending(activity.QualifiedName, user); else _dynamicUserIgnore.Add(activity.QualifiedName); if (CreateCacheItems(activity, out points)) { CacheInsertUpdatePending(activity.QualifiedName, points); return true; } else { _dynamicActivitiesIgnore.Add(activity.QualifiedName); return false; } } } public void WorkflowChangeBegin(IList changeActions) { Debug.Assert(!_pendingWorkflowChange, "_pendingWorkflowChange should be false."); if (_pendingWorkflowChange) throw new InvalidOperationException(ExecutionStringManager.DynamicUpdateIsNotPending); if (!_isPrivate) throw new InvalidOperationException(ExecutionStringManager.ProfileIsNotPrivate); // // Initialize the temp dictionary for activities that are spun up during the update process // If the update succeeds we'll copy these to the main _subscriptions dictionary. _dynamicActivities = new Dictionary>(); _dynamicActivitiesIgnore = new List(); _dynamicUser = new Dictionary>(); _dynamicUserIgnore = new List(); _pendingChanges = changeActions; _pendingWorkflowChange = true; } public void WorkflowChangeCommit() { Debug.Assert(_pendingWorkflowChange, "Workflow change is not pending - no change to commit"); if (!_pendingWorkflowChange) return; if (!_isPrivate) throw new InvalidOperationException(ExecutionStringManager.ProfileIsNotPrivate); // // Remove items that have been deleted by this update // Must do all removes first as there may be a new action // with the same qid as a previous action that is being removed if (null != _pendingChanges) { foreach (WorkflowChangeAction action in _pendingChanges) { if (action is RemovedActivityAction) { // // Remove all references to this activity that might exist in our caches string qId = ((RemovedActivityAction)action).OriginalRemovedActivity.QualifiedName; _activities.Remove(qId); _activitiesIgnore.Remove(qId); _user.Remove(qId); _userIgnore.Remove(qId); } } } // // Copy any pending cache items to the regular activity track point cache if ((null != _dynamicActivities) && (_dynamicActivities.Count > 0)) foreach (KeyValuePair> kvp in _dynamicActivities) _activities.Add(kvp.Key, kvp.Value); if ((null != _dynamicActivitiesIgnore) && (_dynamicActivitiesIgnore.Count > 0)) _activitiesIgnore.AddRange(_dynamicActivitiesIgnore); if ((null != _dynamicUser) && (_dynamicUser.Count > 0)) foreach (KeyValuePair> kvp in _dynamicUser) _user.Add(kvp.Key, kvp.Value); if ((null != _dynamicUserIgnore) && (_dynamicUserIgnore.Count > 0)) _userIgnore.AddRange(_dynamicUserIgnore); // // All done, clean up _dynamicActivities = null; _dynamicActivitiesIgnore = null; _dynamicUser = null; _dynamicUserIgnore = null; _pendingChanges = null; _pendingWorkflowChange = false; } public void WorkflowChangeRollback() { // // Just clean up, there isn't any work to rollback because // any subscriptions that may have been added for a pending add activity // won't ever be hit as the activities haven't been added to the tree. _dynamicActivities = null; _dynamicActivitiesIgnore = null; _dynamicUser = null; _dynamicUserIgnore = null; _pendingChanges = null; _pendingWorkflowChange = false; } #endregion #region Private Cache Methods /// /// Create the static qualifiedid to trackpoint map /// /// private void CheckAllActivities(Activity activity) { CheckActivity((Activity)activity); // // Walk down the activity tree // Use EnabledActivities to get invisible activities // EnabledActivities will not return commented activities if (activity is CompositeActivity) foreach (Activity a in GetAllEnabledActivities((CompositeActivity)activity)) CheckAllActivities(a); } /// /// Recursively walk the activity tree and find all track points that match each activity /// /// private void CheckActivity(Activity activity) { // // Build caches of activity status change events string qId = activity.QualifiedName; List activities = null; if (CreateCacheItems(activity, out activities)) CacheInsert(qId, activities); else _activitiesIgnore.Add(qId); // // Build caches of user events List user = null; if (CreateCacheItems(activity, out user)) CacheInsert(qId, user); else _userIgnore.Add(qId); } /// /// Find all trackpoints that match an activity. /// /// Activity for which to determine subscription needs /// List to be populated with matching track points /// true if a subscription is needed; false if not private bool CreateCacheItems(Activity activity, out List includes) { includes = new List(); // // Check if we have any trackpoints that match this activity foreach (ActivityTrackPoint point in _profile.ActivityTrackPoints) { List events; bool hasCondition = false; if (point.IsMatch(activity, out events, out hasCondition)) includes.Add(new ActivityTrackPointCacheItem(point, events, hasCondition)); } return (includes.Count > 0); } /// /// Find all trackpoints that match user events for an activity. /// /// Activity for which to determine subscription needs /// List to be populated with matching track points /// true if a subscription is needed; false if not private bool CreateCacheItems(Activity activity, out List includes) { includes = new List(); // // Check if we have any trackpoints that match this activity foreach (UserTrackPoint point in _profile.UserTrackPoints) { if (point.IsMatch(activity)) includes.Add(point); } return (includes.Count > 0); } private void CacheInsert(string qualifiedID, List points) { // // Check to make sure the item isn't in the dictionary // If not add all track points Debug.Assert(!_activities.ContainsKey(qualifiedID), "QualifiedName is already in the activities cache"); if (_activities.ContainsKey(qualifiedID)) throw new InvalidOperationException(ExecutionStringManager.RTProfileActCacheDupKey); foreach (ActivityTrackPointCacheItem point in points) CacheInsert(qualifiedID, point); } private void CacheInsert(string qualifiedID, List points) { // // Check to make sure the item isn't in the dictionary // If not add all track points Debug.Assert(!_user.ContainsKey(qualifiedID), "QualifiedName is already in the user cache"); if (_user.ContainsKey(qualifiedID)) throw new InvalidOperationException(ExecutionStringManager.RTProfileActCacheDupKey); foreach (UserTrackPoint point in points) CacheInsert(qualifiedID, point); } private void CacheInsert(string qualifiedID, ActivityTrackPointCacheItem point) { List points = null; if (!_activities.TryGetValue(qualifiedID, out points)) { points = new List(); _activities.Add(qualifiedID, points); } points.Add(point); } private void CacheInsert(string qualifiedID, UserTrackPoint point) { List points = null; if (!_user.TryGetValue(qualifiedID, out points)) { points = new List(); _user.Add(qualifiedID, points); } points.Add(point); } private void CacheInsertUpdatePending(string qualifiedID, List points) { // // The activity has been added during a pending dynamic change // add it to a temporary lookup which will be copied to real cache // when the dynamic update commits. if ((!_isPrivate) || (!_pendingWorkflowChange)) throw new InvalidOperationException(ExecutionStringManager.ProfileIsNotPrivate); if (null == _dynamicActivities) throw new InvalidOperationException(ExecutionStringManager.RTProfileDynamicActCacheIsNull); List tmp = null; if (!_dynamicActivities.TryGetValue(qualifiedID, out tmp)) { tmp = new List(); _dynamicActivities.Add(qualifiedID, tmp); } foreach (ActivityTrackPointCacheItem point in points) tmp.Add(point); } private bool TryGetCacheItems(Activity activity, out List points) { points = null; if ((!_pendingWorkflowChange) || ((_pendingWorkflowChange) && (!IsPendingUpdateActivity(activity, true)))) { // // A dynamic update is not in progress or this activity // is not being added by the current dynamic update. // The main cache holds all matching track points return _activities.TryGetValue(activity.QualifiedName, out points); } else { // // Dynamic update is in progress return _dynamicActivities.TryGetValue(activity.QualifiedName, out points); } } private void CacheInsertUpdatePending(string qualifiedID, List points) { // // The activity has been added during a pending dynamic change // add it to a temporary lookup which will be copied to real cache // when the dynamic update commits. if ((!_isPrivate) || (!_pendingWorkflowChange)) throw new InvalidOperationException(ExecutionStringManager.ProfileIsNotPrivate); if (null == _dynamicUser) throw new InvalidOperationException(ExecutionStringManager.RTProfileDynamicActCacheIsNull); List tmp = null; if (!_dynamicUser.TryGetValue(qualifiedID, out tmp)) { tmp = new List(); _dynamicUser.Add(qualifiedID, tmp); } foreach (UserTrackPoint point in points) tmp.Add(point); } private bool TryGetCacheItems(Activity activity, out List points) { points = null; if ((!_pendingWorkflowChange) || ((_pendingWorkflowChange) && (!IsPendingUpdateActivity(activity, true)))) { // // A dynamic update is not in progress or this activity // is not being added by the current dynamic update. // The main cache holds all matching track points return _user.TryGetValue(activity.QualifiedName, out points); } else { // // Dynamic update is in progress return _dynamicUser.TryGetValue(activity.QualifiedName, out points); } } #endregion #region Private Methods // This function returns all the executable activities including secondary flow activities. public IList GetAllEnabledActivities(CompositeActivity compositeActivity) { if (compositeActivity == null) throw new ArgumentNullException("compositeActivity"); List allActivities = new List(compositeActivity.EnabledActivities); foreach (Activity secondaryFlowActivity in ((ISupportAlternateFlow)compositeActivity).AlternateFlowActivities) { if (!allActivities.Contains(secondaryFlowActivity)) allActivities.Add(secondaryFlowActivity); } return allActivities; } private bool IsPendingUpdateActivity(Activity activity, bool addedOnly) { // // If we don't have an update going on this method isn't valid if ((!_isPrivate) || (!_pendingWorkflowChange)) throw new InvalidOperationException(ExecutionStringManager.ProfileIsNotPrivate); // // if we don't have any changes we're done if ((null == _pendingChanges || _pendingChanges.Count <= 0)) return false; foreach (WorkflowChangeAction action in _pendingChanges) { string qualifiedId = null; if (action is ActivityChangeAction) { if (action is AddedActivityAction) { qualifiedId = ((AddedActivityAction)action).AddedActivity.QualifiedName; } else if (action is RemovedActivityAction) { if (!addedOnly) qualifiedId = ((RemovedActivityAction)action).OriginalRemovedActivity.QualifiedName; } else { Debug.Assert(false, ExecutionStringManager.UnknownActivityActionType); } if ((null != qualifiedId) && (0 == String.Compare(activity.QualifiedName, qualifiedId, StringComparison.Ordinal))) { return true; } } } return false; } #endregion #region ICloneable Members object ICloneable.Clone() { return this.Clone(); } internal RTTrackingProfile Clone() { return new RTTrackingProfile(this); } #endregion #region Contained Types private struct ActivityTrackPointCacheItem { internal ActivityTrackPointCacheItem(ActivityTrackPoint point, List events, bool hasConditions) { if (null == point) throw new ArgumentNullException("point"); if (null == events) throw new ArgumentNullException("events"); Point = point; Events = events; HasLocationConditions = hasConditions; } internal ActivityTrackPoint Point; internal List Events; internal bool HasLocationConditions; } #endregion } }