• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

HicServices / RDMP / 7194961165

13 Dec 2023 12:07PM UTC coverage: 56.776% (-0.2%) from 57.013%
7194961165

push

github

web-flow
Merge Latest Release into main (#1702)

* Bump YamlDotNet from 13.3.1 to 13.4.0

Bumps [YamlDotNet](https://github.com/aaubry/YamlDotNet) from 13.3.1 to 13.4.0.
- [Release notes](https://github.com/aaubry/YamlDotNet/releases)
- [Commits](https://github.com/aaubry/YamlDotNet/compare/v13.3.1...v13.4.0)

---
updated-dependencies:
- dependency-name: YamlDotNet
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump shogo82148/actions-setup-perl from 1.23.1 to 1.24.1

Bumps [shogo82148/actions-setup-perl](https://github.com/shogo82148/actions-setup-perl) from 1.23.1 to 1.24.1.
- [Release notes](https://github.com/shogo82148/actions-setup-perl/releases)
- [Commits](https://github.com/shogo82148/actions-setup-perl/compare/v1.23.1...v1.24.1)

---
updated-dependencies:
- dependency-name: shogo82148/actions-setup-perl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix checkbox issue

* improve confirmation text (#1639)

* improve confirmation text
* Loop tidyup, use var where possible

---------

Co-authored-by: jas88 <j.a.sutherland@dundee.ac.uk>

* correct typo in create logging sql (#1640)

* Feature/ci codescan (#1641)

* Move SecurityCodescan.VS2019 to run on Github CI alone, integrate results with CodeQL
* Remove SecurityCodescan from Packages.md, no longer used via Nuget

---------

Co-authored-by: James A Sutherland <j@sutherland.pw>

* hide source control when not available

* Remove old Plugin object bits, tidy up (#1636)

* Remove old Plugin object bits, tidy up

* Purge remaining bits of AllExpiredPluginsNode

* Fix plugin display name in tree

* Update CreateNewDataExtractionProjectUI.cs

Casting fix

* Feature/rdmp42 delete plugins (#1642)

* add ui plugin delete functionality

* Warning and inherita... (continued)

10722 of 20351 branches covered (0.0%)

Branch coverage included in aggregate %.

215 of 789 new or added lines in 63 files covered. (27.25%)

39 existing lines in 16 files now uncovered.

30650 of 52518 relevant lines covered (58.36%)

7294.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.59
/Rdmp.Core/Curation/Data/DataLoad/ProcessTask.cs
1
// Copyright (c) The University of Dundee 2018-2019
2
// This file is part of the Research Data Management Platform (RDMP).
3
// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4
// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5
// You should have received a copy of the GNU General Public License along with RDMP. If not, see <https://www.gnu.org/licenses/>.
6

7
using System;
8
using System.Collections.Generic;
9
using System.Data.Common;
10
using System.IO;
11
using System.Linq;
12
using System.Text.RegularExpressions;
13
using Rdmp.Core.Curation.Data.Cohort;
14
using Rdmp.Core.Curation.Data.ImportExport;
15
using Rdmp.Core.Curation.Data.Serialization;
16
using Rdmp.Core.MapsDirectlyToDatabaseTable;
17
using Rdmp.Core.MapsDirectlyToDatabaseTable.Attributes;
18
using Rdmp.Core.Repositories;
19
using Rdmp.Core.ReusableLibraryCode.Annotations;
20
using Rdmp.Core.ReusableLibraryCode.Checks;
21

22
namespace Rdmp.Core.Curation.Data.DataLoad;
23

24
/// <summary>
25
/// Describes a specific operation carried out at a specific step of a LoadMetadata.  This could be 'unzip all files called *.zip in for loading' or
26
/// 'after loading the data to live, call sp_clean_table1' or 'Connect to webservice X and download 1,000,000 records which will be serialized into XML'
27
/// 
28
/// <para>The class achieves this wide ranging functionality through the interaction of ProcessTaskType and Path.  e.g. when ProcessTaskType is Attacher then
29
/// Path functions as the Type name of a class that implements IAttacher e.g. 'LoadModules.Generic.Attachers.AnySeparatorFileAttacher'.  </para>
30
/// 
31
/// <para>Each ProcessTask can have one or more strongly typed arguments (see entity ProcessTaskArgument), these are discovered at design time by using
32
/// reflection to query the Path e.g. 'AnySeparatorFileAttacher' for all properties marked with [DemandsInitialization] attribute.  This allows for 3rd party developers
33
/// to write plugin classes to easily handle proprietary/bespoke source file types or complex data load requirements.</para>
34
/// </summary>
35
public class ProcessTask : DatabaseEntity, IProcessTask, IOrderable, INamed, ICheckable
36
{
37
    #region Database Properties
38

39
    private int _loadMetadataID;
40
    private int? _relatesSolelyToCatalogueID;
41
    private int _order;
42
    private string _path;
43
    private string _name;
44
    private LoadStage _loadStage;
45
    private ProcessTaskType _processTaskType;
46
    private bool _isDisabled;
47

48
    /// <summary>
49
    /// The load the process task exists as part of
50
    /// </summary>
51
    [Relationship(typeof(LoadMetadata), RelationshipType.SharedObject)]
52
    public int LoadMetadata_ID
53
    {
54
        get => _loadMetadataID;
2,232✔
55
        set => SetField(ref _loadMetadataID, value);
770✔
56
    }
57

58
    /// <inheritdoc/>
59
    [Obsolete(
60
        "Since you can't change which Catalogues are loaded by a LoadMetadata at runtime, this property is now obsolete")]
61
    public int? RelatesSolelyToCatalogue_ID
62
    {
63
        get => _relatesSolelyToCatalogueID;
536✔
64
        set => SetField(ref _relatesSolelyToCatalogueID, value);
146✔
65
    }
66

67
    /// <inheritdoc/>
68
    public int Order
69
    {
70
        get => _order;
1,098✔
71
        set => SetField(ref _order, value);
780✔
72
    }
73

74
    /// <inheritdoc/>
75
    [AdjustableLocation]
76
    public string Path
77
    {
78
        get => _path;
1,006✔
79
        set => SetField(ref _path, value);
848✔
80
    }
81

82
    /// <inheritdoc cref="IProcessTask.Name"/>
83
    [NotNull]
84
    public string Name
85
    {
86
        get => _name;
778✔
87
        set => SetField(ref _name, value);
844✔
88
    }
89

90
    /// <inheritdoc/>
91
    public LoadStage LoadStage
92
    {
93
        get => _loadStage;
1,660✔
94
        set => SetField(ref _loadStage, value);
796✔
95
    }
96

97
    /// <inheritdoc/>
98
    public ProcessTaskType ProcessTaskType
99
    {
100
        get => _processTaskType;
882✔
101
        set => SetField(ref _processTaskType, value);
890✔
102
    }
103

104
    /// <inheritdoc/>
105
    public bool IsDisabled
106
    {
107
        get => _isDisabled;
1,156✔
108
        set => SetField(ref _isDisabled, value);
726✔
109
    }
110

111
    #endregion
112

113
    #region Relationships
114

115
    /// <inheritdoc cref="LoadMetadata_ID"/>
116
    [NoMappingToDatabase]
117
    public LoadMetadata LoadMetadata => Repository.GetObjectByID<LoadMetadata>(LoadMetadata_ID);
168✔
118

119
    /// <inheritdoc/>
120
    [NoMappingToDatabase]
121
    public IEnumerable<ProcessTaskArgument> ProcessTaskArguments =>
122
        Repository.GetAllObjectsWithParent<ProcessTaskArgument>(this);
414✔
123

124
    /// <summary>
125
    /// All <see cref="ILoadProgress"/> (if any) that can be advanced by executing this load.  This allows batch execution of large loads
126
    /// </summary>
127
    [NoMappingToDatabase]
128
    public ILoadProgress[] LoadProgresses => LoadMetadata.LoadProgresses;
2✔
129

130
    #endregion
131

132
    public ProcessTask()
×
133
    {
134
    }
×
135

136
    /// <summary>
137
    /// Creates a new operation in the data load (e.g. copy files from A to B, load all CSV files to RAW table B etc)
138
    /// </summary>
139
    /// <param name="repository"></param>
140
    /// <param name="parent"></param>
141
    /// <param name="stage"></param>
142
    public ProcessTask(ICatalogueRepository repository, ILoadMetadata parent, LoadStage stage)
190✔
143
    {
144
        var order =
190✔
145
            repository.GetAllObjectsWithParent<ProcessTask>(parent).Select(t => t.Order).DefaultIfEmpty().Max() + 1;
202✔
146

147
        repository.InsertAndHydrate(this, new Dictionary<string, object>
190✔
148
        {
190✔
149
            { "LoadMetadata_ID", parent.ID },
190✔
150
            { "ProcessTaskType", ProcessTaskType.Executable.ToString() },
190✔
151
            { "LoadStage", stage },
190✔
152
            { "Name", $"New Process{Guid.NewGuid()}" },
190✔
153
            { "Order", order }
190✔
154
        });
190✔
155
    }
190✔
156

157
    internal ProcessTask(ICatalogueRepository repository, DbDataReader r)
158
        : base(repository, r)
570✔
159
    {
160
        LoadMetadata_ID = int.Parse(r["LoadMetaData_ID"].ToString());
570✔
161

162
        if (r["RelatesSolelyToCatalogue_ID"] != DBNull.Value)
570!
163
            _relatesSolelyToCatalogueID = int.Parse(r["RelatesSolelyToCatalogue_ID"].ToString());
×
164

165
        Path = r["Path"] as string;
570✔
166
        Name = r["Name"] as string;
570✔
167
        Order = int.Parse(r["Order"].ToString());
570✔
168

169
        if (Enum.TryParse(r["ProcessTaskType"] as string, out ProcessTaskType processTaskType))
570!
170
            ProcessTaskType = processTaskType;
570✔
171
        else
172
            throw new Exception($"Could not parse ProcessTaskType:{r["ProcessTaskType"]}");
×
173

174
        if (Enum.TryParse(r["LoadStage"] as string, out LoadStage loadStage))
570!
175
            LoadStage = loadStage;
570✔
176
        else
177
            throw new Exception($"Could not parse LoadStage:{r["LoadStage"]}");
×
178

179
        IsDisabled = Convert.ToBoolean(r["IsDisabled"]);
570✔
180
    }
570✔
181

182
    internal ProcessTask(ShareManager shareManager, ShareDefinition shareDefinition)
6✔
183
    {
184
        shareManager.UpsertAndHydrate(this, shareDefinition);
6✔
185
    }
6✔
186

187
    /// <inheritdoc/>
188
    public override string ToString() => Name;
70✔
189

190
    /// <inheritdoc/>
191
    public void Check(ICheckNotifier notifier)
192
    {
193
        switch (ProcessTaskType)
38!
194
        {
195
            case ProcessTaskType.Executable:
196
                CheckFileExistenceAndUniqueness(notifier);
×
197
                break;
×
198
            case ProcessTaskType.SQLFile:
199
                CheckFileExistenceAndUniqueness(notifier);
×
200
                CheckForProblemsInSQLFile(notifier);
×
NEW
201
                break;
×
202
            case ProcessTaskType.SQLBakFile:
NEW
203
                CheckFileExistenceAndUniqueness(notifier);
×
UNCOV
204
                break;
×
205
            case ProcessTaskType.Attacher:
206
                break;
207
            case ProcessTaskType.DataProvider:
208
                break;
209
            case ProcessTaskType.MutilateDataTable:
210
                break;
211
            default:
212
                throw new ArgumentOutOfRangeException();
×
213
        }
214
    }
38✔
215

216
    private void CheckForProblemsInSQLFile(ICheckNotifier notifier)
217
    {
218
        try
219
        {
220
            var sql = File.ReadAllText(Path);
×
221

222
            //let's check for any SQL that indicates user is trying to modify a STAGING table in a RAW script (for example)
223
            foreach (var tableInfo in LoadMetadata.GetDistinctTableInfoList(false))
×
224
                //for each stage get all the object names that are in that stage
225
                foreach (var stage in new[] { LoadStage.AdjustRaw, LoadStage.AdjustStaging, LoadStage.PostLoad })
×
226
                {
227
                    //process task belongs in that stage anyway so nothing is prohibited
228
                    if (stage == (LoadStage == LoadStage.Mounting ? LoadStage.AdjustRaw : LoadStage))
×
229
                        continue;
230

231
                    //figure out what is prohibited
232
                    var prohibitedSql = tableInfo.GetQuerySyntaxHelper()
×
233
                        .EnsureFullyQualified(tableInfo.GetDatabaseRuntimeName(stage), null,
×
234
                            tableInfo.GetRuntimeName(stage));
×
235

236
                    //if we reference it, complain
237
                    if (sql.Contains(prohibitedSql))
×
238
                        notifier.OnCheckPerformed(
×
239
                            new CheckEventArgs(
×
240
                                $"Sql in file '{Path}' contains a reference to '{prohibitedSql}' which is prohibited since the ProcessTask ('{Name}') runs in LoadStage {LoadStage}",
×
241
                                CheckResult.Warning));
×
242
                }
243
        }
×
244
        catch (Exception e)
×
245
        {
246
            notifier.OnCheckPerformed(new CheckEventArgs($"Failed to check the contents of the SQL file '{Path}'",
×
247
                CheckResult.Fail, e));
×
248
        }
×
249
    }
×
250

251
    private void CheckFileExistenceAndUniqueness(ICheckNotifier notifier)
252
    {
253
        if (string.IsNullOrWhiteSpace(Path))
×
254
        {
255
            notifier.OnCheckPerformed(new CheckEventArgs($"No Path specified for ProcessTask '{Name}'",
×
256
                CheckResult.Fail));
×
257
            return;
×
258
        }
259

260
        notifier.OnCheckPerformed(!File.Exists(Path)
×
261
            ? new CheckEventArgs($"Could not find File '{Path}' for ProcessTask '{Name}'", CheckResult.Fail)
×
262
            : new CheckEventArgs($"Found File '{Path}'", CheckResult.Success));
×
263

264

265
        var matchingPaths = Repository.GetAllObjects<ProcessTask>().Where(pt => pt.Path.Equals(Path));
×
266
        foreach (var duplicate in matchingPaths.Except(new[] { this }))
×
267
            notifier.OnCheckPerformed(
×
268
                new CheckEventArgs(
×
269
                    $"ProcessTask '{duplicate}' (ID={duplicate.ID}) also uses file '{System.IO.Path.GetFileName(Path)}'",
×
270
                    CheckResult.Warning));
×
271

272
        //conflicting tokens in Name string
273
        foreach (Match match in Regex.Matches(Name, @"'.*((\.exe')|(\.sql'))"))
×
274
            if (match.Success)
×
275
            {
276
                var referencedFile = System.IO.Path.GetFileName(match.Value.Trim('\''));
×
277
                var actualFile = System.IO.Path.GetFileName(Path);
×
278

279
                if (referencedFile != actualFile)
×
280
                    notifier.OnCheckPerformed(
×
281
                        new CheckEventArgs(
×
282
                            $"Name of ProcessTask '{Name}' (ID={ID}) references file '{match.Value}' but the Path of the ProcessTask is '{Path}'",
×
283
                            CheckResult.Fail));
×
284
            }
285
    }
×
286

287
    /// <summary>
288
    /// Returns all tables loaded by the parent <see cref="LoadMetadata"/>
289
    /// </summary>
290
    /// <returns></returns>
291
    public IEnumerable<TableInfo> GetTableInfos() => LoadMetadata.GetDistinctTableInfoList(true);
2✔
292

293
    /// <inheritdoc/>
294
    public IEnumerable<IArgument> GetAllArguments() => ProcessTaskArguments;
276✔
295

296
    /// <inheritdoc/>
297
    public IArgument CreateNewArgument() => new ProcessTaskArgument((ICatalogueRepository)Repository, this);
1,198✔
298

299
    /// <inheritdoc/>
300
    public string GetClassNameWhoArgumentsAreFor() => Path;
18✔
301

302
    /// <summary>
303
    /// Creates a new copy of the processTask and all its arguments in the database, this clone is then hooked up to the
304
    /// new LoadMetadata at the specified stage
305
    /// </summary>
306
    /// <param name="loadMetadata">The new LoadMetadata parent for the clone</param>
307
    /// <param name="loadStage">The new load stage to put the clone in </param>
308
    /// <returns>the new ProcessTask (the clone has a different ID to the parent)</returns>
309
    public ProcessTask CloneToNewLoadMetadataStage(LoadMetadata loadMetadata, LoadStage loadStage)
310
    {
311
        var cataRepository = (ICatalogueRepository)Repository;
4✔
312

313
        //clone only accepts sql connections so make sure we aren't in mysql land or something
314
        using (cataRepository.BeginNewTransaction())
4✔
315
        {
316
            try
317
            {
318
                //get list of arguments to also clone (will happen outside of transaction
319
                var toCloneArguments = ProcessTaskArguments.ToArray();
4✔
320

321
                //create a new transaction for all the cloning - note that once all objects are cloned the transaction is committed then all the objects are adjusted outside the transaction
322
                var clone = new ProcessTask(CatalogueRepository, LoadMetadata, loadStage);
4✔
323
                CopyShallowValuesTo(clone);
4✔
324

325
                //foreach of our child arguments
326
                foreach (var argument in toCloneArguments)
12✔
327
                    //clone it but rewire it to the proper ProcessTask parent (the clone)
328
                    argument.ShallowClone(clone);
2✔
329

330
                //the values passed into parameter
331
                clone.LoadMetadata_ID = loadMetadata.ID;
4✔
332
                clone.LoadStage = loadStage;
4✔
333
                clone.SaveToDatabase();
4✔
334

335
                //it worked
336
                cataRepository.EndTransaction(true);
4✔
337

338
                //return the clone
339
                return clone;
4✔
340
            }
341
            catch (Exception)
×
342
            {
343
                cataRepository.EndTransaction(false);
×
344
                throw;
×
345
            }
346
        }
347
    }
4✔
348

349
    /// <inheritdoc/>
350
    public IArgument[] CreateArgumentsForClassIfNotExists(Type t) =>
351
        ArgumentFactory.CreateArgumentsForClassIfNotExistsGeneric(
70✔
352
                t,
70✔
353

70✔
354
                //tell it how to create new instances of us related to parent
70✔
355
                this,
70✔
356

70✔
357
                //what arguments already exist
70✔
358
                GetAllArguments().ToArray())
70✔
359

70✔
360
            //convert the result back from generic to specific (us)
70✔
361
            .ToArray();
70✔
362

363
    /// <inheritdoc/>
364
    public IArgument[] CreateArgumentsForClassIfNotExists<T>() => CreateArgumentsForClassIfNotExists(typeof(T));
66✔
365

366
    /// <summary>
367
    /// Returns true if the <see cref="ProcessTaskType"/> is allowed to happen during the given <see cref="LoadStage"/>  (e.g. you can't use an IAttacher to
368
    /// load data into STAGING/LIVE - only RAW).
369
    /// </summary>
370
    /// <param name="type"></param>
371
    /// <param name="stage"></param>
372
    /// <returns></returns>
373
    public static bool IsCompatibleStage(ProcessTaskType type, LoadStage stage)
374
    {
375
        return type switch
×
376
        {
×
377
            ProcessTaskType.Executable => true,
×
378
            ProcessTaskType.SQLFile => stage != LoadStage.GetFiles,
×
NEW
379
            ProcessTaskType.SQLBakFile => stage != LoadStage.GetFiles,
×
380
            ProcessTaskType.Attacher => stage == LoadStage.Mounting,
×
381
            ProcessTaskType.DataProvider => true,
×
382
            ProcessTaskType.MutilateDataTable => stage != LoadStage.GetFiles,
×
383
            _ => throw new ArgumentOutOfRangeException(nameof(type))
×
384
        };
×
385
    }
386

387
    /// <summary>
388
    /// True if <see cref="Path"/> is the name of a C# class (as opposed to the path to an executable or SQL file etc)
389
    /// </summary>
390
    /// <returns></returns>
391
    public bool IsPluginType() => ProcessTaskType == ProcessTaskType.Attacher ||
×
392
                                  ProcessTaskType == ProcessTaskType.MutilateDataTable ||
×
393
                                  ProcessTaskType == ProcessTaskType.DataProvider;
×
394

395
    /// <summary>
396
    /// Sets the value of the corresponding <see cref="IArgument"/> (which must already exist) to the given value.  If your argument doesn't exist yet you
397
    /// can call <see cref="CreateArgumentsForClassIfNotExists"/>
398
    /// </summary>
399
    /// <param name="parameterName"></param>
400
    /// <param name="o"></param>
401
    public void SetArgumentValue(string parameterName, object o)
402
    {
403
        var matchingArgument = ProcessTaskArguments.SingleOrDefault(p => p.Name.Equals(parameterName)) ??
2,280!
404
                               throw new Exception(
114✔
405
                                   $"Could not find a ProcessTaskArgument called '{parameterName}', have you called CreateArgumentsForClassIfNotExists<T> yet?");
114✔
406
        matchingArgument.SetValue(o);
114✔
407
        matchingArgument.SaveToDatabase();
114✔
408
    }
114✔
409
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc