// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo.Agent; using Microsoft.SqlTools.ServiceLayer.Management; namespace Microsoft.SqlTools.ServiceLayer.Agent { internal class JobStepsData { #region fields /// /// collection of job steps. /// private ArrayList jobSteps; /// /// List of job steps to be deleted /// private ArrayList deletedJobSteps; /// /// Parent JobData /// private JobData parent; /// /// Server context /// private CDataContainer context; /// /// Start Step /// private JobStepData startStep; /// /// list of available databases /// private string[] databases = null; #endregion #region public properties /// /// JobData structure this object is part of /// public JobData Parent { get { return this.parent; } } /// /// Server Version /// public Version Version { get { return this.parent.Version; } } /// /// Mode in which the dialog has been launched /// JobData.ActionMode Mode { get { if (this.parent != null) { return this.parent.Mode; } else { return JobData.ActionMode.Unknown; } } } /// /// List of steps in this job /// public ArrayList Steps { get { return this.jobSteps; } } /// /// The default start step /// public JobStepData StartStep { get { // we can't point to a step that is marked for deletion if (this.startStep != null && this.startStep.ToBeDeleted == true) { this.startStep = null; } // if the start step is null, and we have job steps, just point // the start step to the first one. if (this.startStep == null && this.jobSteps.Count > 0) { this.startStep = (JobStepData)this.jobSteps[0]; } return this.startStep; } set { this.startStep = value; } } /// /// List of all available databases on the server /// public string[] Databases { get { CheckAndLoadDatabases(); return this.databases; } } /// /// Indicates whether or not the order of the steps has changed /// public bool HasStepOrderChanged { get { bool orderChanged = false; foreach (JobStepData jsd in this.jobSteps) { if (jsd.StepIdChanged == true) { orderChanged = true; break; } } return orderChanged; } } /// /// Indicates whether or not the Job is read only /// public bool IsReadOnly { get { return parent.IsReadOnly; } } #endregion #region Events public event EventHandler StepOrderChanged; #endregion #region construction /// /// Create a new JobStepsData object with a new job step /// /// server context /// script for the job step /// owning data object public JobStepsData(CDataContainer context, string script, JobData parent) { if (context == null) { throw new ArgumentNullException("context"); } if (script == null) { throw new ArgumentNullException("strint"); } if (parent == null) { throw new ArgumentNullException("parent"); } CommonInit(context, parent, script); } /// /// Create a new jobsteps data object /// /// server context /// owning data object public JobStepsData(CDataContainer context, JobData parent) { if (context == null) { throw new ArgumentNullException("context"); } if (parent == null) { throw new ArgumentNullException("parent"); } CommonInit(context, parent, null); } /// /// Common initialization routines for constructrs /// /// /// /// private void CommonInit(CDataContainer context, JobData parent, string script) { this.context = context; this.parent = parent; this.deletedJobSteps = new ArrayList(); // if we're creating a new job if (this.parent.Mode != JobData.ActionMode.Edit) { SetDefaults(); if (script != null && script.Length != 0) { LoadFromScript(script); } } else { // load the JobStep objects LoadData(); } } #endregion #region public methods /// /// Add a new existing step to the end of the job step collection /// /// public void AddStep(JobStepData step) { this.jobSteps.Add(step); RecalculateStepIds(); } /// /// Insert a jobstep into an existing location /// /// /// public void InsertStep(int index, JobStepData step) { this.jobSteps.Insert(index, step); RecalculateStepIds(); } /// /// Delete a jobstep /// /// public void DeleteStep(JobStepData step) { if (step == null) { throw new ArgumentNullException("step"); } if (this.jobSteps.Contains(step)) { this.jobSteps.Remove(step); // make a note to delete the step this.deletedJobSteps.Add(step); step.ToBeDeleted = true; } RecalculateStepIds(); } /// /// Get a JobStepData object for a step id /// /// /// public JobStepData GetObjectForStep(int stepId) { JobStepData jobStep = null; if (this.jobSteps != null) { foreach (JobStepData jsd in this.jobSteps) { if (jsd.ID == stepId) { jobStep = jsd; break; } } } return jobStep; } /// /// Check for any job steps that are unreachable. /// Because there are only two paths and we don't care about circular references /// we can use a simplified search, rather than a full graph dfs or bfs. /// /// List of unreachable steps, or an empty list if there are none public List FindUnreachableJobSteps() { // array used to keep track of whether or not a step is reachable bool[] stepReachable = new bool[this.jobSteps.Count]; // mark the start step as reachable if (this.startStep != null && this.startStep.ID > 0 && this.startStep.ID <= this.jobSteps.Count) { stepReachable[this.startStep.ID - 1] = true; } // steps indexes start at 1 foreach (JobStepData step in this.jobSteps) { // check success actions if (step.SuccessAction == StepCompletionAction.GoToNextStep) { // if we aren't on the last step mark the next step as valid if (step.ID < this.jobSteps.Count) { stepReachable[step.ID] = true; } } else if (step.SuccessAction == StepCompletionAction.GoToStep) { if (step.SuccessStep != null && step.SuccessStep.ID <= this.jobSteps.Count) { stepReachable[step.SuccessStep.ID - 1] = true; } } // check failure actions if (step.FailureAction == StepCompletionAction.GoToNextStep) { // if we aren't on the last step mark the next step as valid if (step.ID < this.jobSteps.Count) { stepReachable[step.ID] = true; } } else if (step.FailureAction == StepCompletionAction.GoToStep) { if (step.FailStep != null && step.FailStep.ID <= this.jobSteps.Count) { stepReachable[step.FailStep.ID - 1] = true; } } } // walk through the array indicating if a step is reachable, and // add any that are not to the list of unreachable steps List unreachableSteps = new List(); for (int i = 0; i < stepReachable.Length; i++) { if (stepReachable[i] == false) { unreachableSteps.Add(this.jobSteps[i] as JobStepData); } } return unreachableSteps; } /// /// Checks to see if the Last steps success completion action will change. /// It will if we are editing a job, and the last steps Success Completion /// action is GoToNextStep /// /// true if changes will be automatically made to the last step public bool CheckIfLastStepCompletionActionWillChange() { bool lastStepCompletionActionWillChange = false; if(this.jobSteps.Count > 0) { // get the last step JobStepData lastStep = this.jobSteps[this.jobSteps.Count-1] as JobStepData; if (lastStep != null && parent.Mode == JobData.ActionMode.Edit && lastStep.SuccessAction == StepCompletionAction.GoToNextStep) { lastStepCompletionActionWillChange = true; } } return lastStepCompletionActionWillChange; } #endregion #region private/internal helpers /// /// Recalculate the step ids of the contained job steps /// internal void RecalculateStepIds() { for (int i = 0; i < this.jobSteps.Count; i++) { JobStepData jsd = jobSteps[i] as JobStepData; if (jsd != null) { jsd.ID = i + 1; } } OnStepOrderChanged(EventArgs.Empty); } /// /// Delayed loading of database information /// private void CheckAndLoadDatabases() { if (this.databases != null) { return; } // load databases collection this.databases = new string[this.context.Server.Databases.Count]; for (int i = 0; i < this.context.Server.Databases.Count; i++) { this.databases[i] = this.context.Server.Databases[i].Name; } } /// /// fire the StepOrderChanged event /// private void OnStepOrderChanged(EventArgs args) { if (this.StepOrderChanged != null) { this.StepOrderChanged(this, args); } } /// /// SMO job object we are manipulating /// internal Job Job { get { Job job = null; if (this.parent != null) { job = parent.Job; } return job; } } #endregion #region data loading /// /// Load a job step from a script /// /// private void LoadFromScript(string script) { this.jobSteps = new ArrayList(); JobStepData jsd = new JobStepData(this); jsd.Command = script; jsd.SubSystem = AgentSubSystem.TransactSql; jsd.ID = 1; jsd.Name = "1"; this.jobSteps.Add(jsd); } /// /// Load job steps from the server /// private void LoadData() { STParameters parameters = new STParameters(this.context.Document); string urn = string.Empty; string jobIdString = string.Empty; parameters.GetParam("urn", ref urn); parameters.GetParam("jobid", ref jobIdString); // save current state of default fields StringCollection originalFields = this.context.Server.GetDefaultInitFields(typeof(JobStep)); // Get all JobStep properties since the JobStepData class is going to use themn this.context.Server.SetDefaultInitFields(typeof(JobStep), true); try { Job job = null; // If JobID is passed in look up by jobID if (!string.IsNullOrEmpty(jobIdString)) { job = this.context.Server.JobServer.Jobs.ItemById(Guid.Parse(jobIdString)); } else { // or use urn path to query job job = this.context.Server.GetSmoObject(urn) as Job; } // load the data JobStepCollection steps = job.JobSteps; // allocate the array list this.jobSteps = new ArrayList(steps.Count); for (int i = 0; i < steps.Count; i++) { // add them in step id order int ii = 0; for (; ii < this.jobSteps.Count; ii++) { if (steps[i].ID < ((JobStepData)this.jobSteps[ii]).ID) { break; } } this.jobSteps.Insert(ii, new JobStepData(steps[i], this)); } // figure out the start step this.startStep = GetObjectForStep(job.StartStepID); // fixup all of the jobsteps failure/completion actions foreach (JobStepData jobStep in this.jobSteps) { jobStep.LoadCompletionActions(); } } finally { // revert to initial default fields for this type this.context.Server.SetDefaultInitFields(typeof(JobStep), originalFields); } } /// /// Set default values for a new empty job /// private void SetDefaults() { this.jobSteps = new ArrayList(); } #endregion #region saving /// /// Save changes to all job steps /// /// owner job /// True if any changes were saved public bool ApplyChanges(Job job) { bool changesMade = false; if (this.IsReadOnly) { return false; } bool scripting = this.context.Server.ConnectionContext.SqlExecutionModes == SqlExecutionModes.CaptureSql; // delete all of the deleted steps for (int i = deletedJobSteps.Count - 1; i >= 0; i--) { JobStepData step = this.deletedJobSteps[i] as JobStepData; if (step != null) { if (step.Created) { step.Delete(); changesMade = true; } } // don't clear the list if we are just scripting the action. if (!scripting) { deletedJobSteps.RemoveAt(i); } } bool forceRebuildingOfSteps = HasStepOrderChanged; // check to see if the step id's have changed. if so we will have to // drop and recreate all of the steps if (forceRebuildingOfSteps) { for (int i = this.jobSteps.Count - 1; i >= 0; --i) { JobStepData step = this.jobSteps[i] as JobStepData; // only delete steps that exist on the server if (step.Created) { step.Delete(); changesMade = true; } } } // update the remaining steps foreach (JobStepData step in this.jobSteps) { if (step.ApplyChanges(job, forceRebuildingOfSteps)) { changesMade = true; } } // update the start step if (StartStep == null && job.StartStepID != 0) { job.StartStepID = 0; changesMade = true; } else if (parent.Mode == JobData.ActionMode.Create || job.StartStepID != this.startStep.ID) { job.StartStepID = this.startStep.ID; job.Alter(); changesMade = true; } return changesMade; } #endregion } }