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

HicServices / RDMP / 11978888584

22 Nov 2024 07:27PM UTC coverage: 57.383% (-0.002%) from 57.385%
11978888584

push

github

jas88
Fix up redundant type inheritance

11206 of 21050 branches covered (53.24%)

Branch coverage included in aggregate %.

65 of 249 new or added lines in 42 files covered. (26.1%)

17 existing lines in 14 files now uncovered.

31718 of 53752 relevant lines covered (59.01%)

8290.69 hits per line

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

44.39
/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.ImportExport;
14
using Rdmp.Core.Curation.Data.Serialization;
15
using Rdmp.Core.MapsDirectlyToDatabaseTable;
16
using Rdmp.Core.MapsDirectlyToDatabaseTable.Attributes;
17
using Rdmp.Core.Repositories;
18
using Rdmp.Core.ReusableLibraryCode.Annotations;
19
using Rdmp.Core.ReusableLibraryCode.Checks;
20

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

23
/// <summary>
24
/// 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
25
/// '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'
26
/// 
27
/// <para>The class achieves this wide ranging functionality through the interaction of ProcessTaskType and Path.  e.g. when ProcessTaskType is Attacher then
28
/// Path functions as the Type name of a class that implements IAttacher e.g. 'LoadModules.Generic.Attachers.AnySeparatorFileAttacher'.  </para>
29
/// 
30
/// <para>Each ProcessTask can have one or more strongly typed arguments (see entity ProcessTaskArgument), these are discovered at design time by using
31
/// reflection to query the Path e.g. 'AnySeparatorFileAttacher' for all properties marked with [DemandsInitialization] attribute.  This allows for 3rd party developers
32
/// to write plugin classes to easily handle proprietary/bespoke source file types or complex data load requirements.</para>
33
/// </summary>
34
public class ProcessTask : DatabaseEntity, IProcessTask, INamed, ICheckable
35
{
36
    #region Database Properties
37

38
    private int _loadMetadataID;
39
    private int? _relatesSolelyToCatalogueID;
40
    private int _order;
41
    private string _path;
42
    private string _name;
43
    private LoadStage _loadStage;
44
    private ProcessTaskType _processTaskType;
45
    private bool _isDisabled;
46
#nullable enable
47
    private string? _SerialisableConfiguration;
48
#nullable disable
49
    /// <summary>
50
    /// The load the process task exists as part of
51
    /// </summary>
52
    [Relationship(typeof(LoadMetadata), RelationshipType.SharedObject)]
53
    public int LoadMetadata_ID
54
    {
55
        get => _loadMetadataID;
25,218✔
56
        set => SetField(ref _loadMetadataID, value);
934✔
57
    }
58

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

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

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

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

91
    /// <inheritdoc/>
92
    public LoadStage LoadStage
93
    {
94
        get => _loadStage;
3,830✔
95
        set => SetField(ref _loadStage, value);
960✔
96
    }
97

98
    /// <inheritdoc/>
99
    public ProcessTaskType ProcessTaskType
100
    {
101
        get => _processTaskType;
902✔
102
        set => SetField(ref _processTaskType, value);
1,058✔
103
    }
104

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

112

113
    /// <inheritdoc/>
114
#nullable enable
115
    public string? SerialisableConfiguration
116
    {
117
        get => _SerialisableConfiguration;
648✔
118
        set => SetField(ref _SerialisableConfiguration, value);
880✔
119
    }
120
#nullable disable
121

122
    #endregion
123

124
    #region Relationships
125

126
    /// <inheritdoc cref="LoadMetadata_ID"/>
127
    [NoMappingToDatabase]
128
    public LoadMetadata LoadMetadata => Repository.GetObjectByID<LoadMetadata>(LoadMetadata_ID);
168✔
129

130
    /// <inheritdoc/>
131
    [NoMappingToDatabase]
132
    public IEnumerable<ProcessTaskArgument> ProcessTaskArguments =>
133
        Repository.GetAllObjectsWithParent<ProcessTaskArgument>(this);
416✔
134

135
    /// <summary>
136
    /// All <see cref="ILoadProgress"/> (if any) that can be advanced by executing this load.  This allows batch execution of large loads
137
    /// </summary>
138
    [NoMappingToDatabase]
139
    public ILoadProgress[] LoadProgresses => LoadMetadata.LoadProgresses;
2✔
140

141
    #endregion
142

143
    public ProcessTask()
×
144
    {
145
    }
×
146

147
    /// <summary>
148
    /// 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)
149
    /// </summary>
150
    /// <param name="repository"></param>
151
    /// <param name="parent"></param>
152
    /// <param name="stage"></param>
153
    public ProcessTask(ICatalogueRepository repository, ILoadMetadata parent, LoadStage stage)
194✔
154
    {
155
        var order =
194✔
156
            repository.GetAllObjectsWithParent<ProcessTask>(parent).Select(static t => t.Order).DefaultIfEmpty().Max() + 1;
206✔
157

158
        repository.InsertAndHydrate(this, new Dictionary<string, object>
194✔
159
        {
194✔
160
            { "LoadMetadata_ID", parent.ID },
194✔
161
            { "ProcessTaskType", ProcessTaskType.Executable.ToString() },
194✔
162
            { "LoadStage", stage },
194✔
163
            { "Name", $"New Process{Guid.NewGuid()}" },
194✔
164
            { "Order", order },
194✔
165
        });
194✔
166
    }
194✔
167

168
    /// <summary>
169
    /// 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)
170
    /// </summary>
171
    /// <param name="repository"></param>
172
    /// <param name="parent"></param>
173
    /// <param name="stage"></param>
174
    /// <param name="serialisableConfiguration"></param>
175
    public ProcessTask(ICatalogueRepository repository, ILoadMetadata parent, LoadStage stage, string serialisableConfiguration = null)
×
176
    {
177
        var order =
×
NEW
178
            repository.GetAllObjectsWithParent<ProcessTask>(parent).Select(static t => t.Order).DefaultIfEmpty().Max() + 1;
×
179

180
        repository.InsertAndHydrate(this, new Dictionary<string, object>
×
181
        {
×
182
            { "LoadMetadata_ID", parent.ID },
×
183
            { "ProcessTaskType", ProcessTaskType.Executable.ToString() },
×
184
            { "LoadStage", stage },
×
185
            { "Name", $"New Process{Guid.NewGuid()}" },
×
186
            { "Order", order },
×
187
            {"SerialisableConfiguration", serialisableConfiguration}
×
188
        });
×
189
    }
×
190

191
    internal ProcessTask(ICatalogueRepository repository, DbDataReader r)
192
        : base(repository, r)
730✔
193
    {
194
        LoadMetadata_ID = int.Parse(r["LoadMetaData_ID"].ToString());
730✔
195

196
        if (r["RelatesSolelyToCatalogue_ID"] != DBNull.Value)
730!
197
            _relatesSolelyToCatalogueID = int.Parse(r["RelatesSolelyToCatalogue_ID"].ToString());
×
198

199
        Path = r["Path"] as string;
730✔
200
        Name = r["Name"] as string;
730✔
201
        Order = int.Parse(r["Order"].ToString());
730✔
202

203
        if (Enum.TryParse(r["ProcessTaskType"] as string, out ProcessTaskType processTaskType))
730!
204
            ProcessTaskType = processTaskType;
730✔
205
        else
206
            throw new Exception($"Could not parse ProcessTaskType:{r["ProcessTaskType"]}");
×
207

208
        if (Enum.TryParse(r["LoadStage"] as string, out LoadStage loadStage))
730!
209
            LoadStage = loadStage;
730✔
210
        else
211
            throw new Exception($"Could not parse LoadStage:{r["LoadStage"]}");
×
212

213
        IsDisabled = Convert.ToBoolean(r["IsDisabled"]);
730✔
214
        if(r["SerialisableConfiguration"] is not null)
730✔
215
            SerialisableConfiguration = r["SerialisableConfiguration"].ToString();
730✔
216
    }
730✔
217

218
    internal ProcessTask(ShareManager shareManager, ShareDefinition shareDefinition)
6✔
219
    {
220
        shareManager.UpsertAndHydrate(this, shareDefinition);
6✔
221
    }
6✔
222

223
    /// <inheritdoc/>
224
    public override string ToString() => Name;
70✔
225

226
    /// <inheritdoc/>
227
    public void Check(ICheckNotifier notifier)
228
    {
229
        switch (ProcessTaskType)
38!
230
        {
231
            case ProcessTaskType.Executable:
232
                CheckFileExistenceAndUniqueness(notifier);
×
233
                break;
×
234
            case ProcessTaskType.SQLFile:
235
                CheckFileExistenceAndUniqueness(notifier);
×
236
                CheckForProblemsInSQLFile(notifier);
×
237
                break;
×
238
            case ProcessTaskType.SQLBakFile:
239
                CheckFileExistenceAndUniqueness(notifier);
×
240
                break;
×
241
            case ProcessTaskType.Attacher:
242
                break;
243
            case ProcessTaskType.DataProvider:
244
                break;
245
            case ProcessTaskType.MutilateDataTable:
246
                break;
247
            default:
248
                throw new ArgumentOutOfRangeException();
×
249
        }
250
    }
38✔
251

252
    private void CheckForProblemsInSQLFile(ICheckNotifier notifier)
253
    {
254
        try
255
        {
256
            var sql = File.ReadAllText(Path);
×
257

258
            //let's check for any SQL that indicates user is trying to modify a STAGING table in a RAW script (for example)
259
            foreach (var tableInfo in LoadMetadata.GetDistinctTableInfoList(false))
×
260
                //for each stage get all the object names that are in that stage
261
                foreach (var stage in new[] { LoadStage.AdjustRaw, LoadStage.AdjustStaging, LoadStage.PostLoad })
×
262
                {
263
                    //process task belongs in that stage anyway so nothing is prohibited
264
                    if (stage == (LoadStage == LoadStage.Mounting ? LoadStage.AdjustRaw : LoadStage))
×
265
                        continue;
266

267
                    //figure out what is prohibited
268
                    var prohibitedSql = tableInfo.GetQuerySyntaxHelper()
×
269
                        .EnsureFullyQualified(tableInfo.GetDatabaseRuntimeName(stage), null,
×
270
                            tableInfo.GetRuntimeName(stage));
×
271

272
                    //if we reference it, complain
273
                    if (sql.Contains(prohibitedSql))
×
274
                        notifier.OnCheckPerformed(
×
275
                            new CheckEventArgs(
×
276
                                $"Sql in file '{Path}' contains a reference to '{prohibitedSql}' which is prohibited since the ProcessTask ('{Name}') runs in LoadStage {LoadStage}",
×
277
                                CheckResult.Warning));
×
278
                }
279
        }
×
280
        catch (Exception e)
×
281
        {
282
            notifier.OnCheckPerformed(new CheckEventArgs($"Failed to check the contents of the SQL file '{Path}'",
×
283
                CheckResult.Fail, e));
×
284
        }
×
285
    }
×
286

287
    private void CheckFileExistenceAndUniqueness(ICheckNotifier notifier)
288
    {
289
        if (string.IsNullOrWhiteSpace(Path))
×
290
        {
291
            notifier.OnCheckPerformed(new CheckEventArgs($"No Path specified for ProcessTask '{Name}'",
×
292
                CheckResult.Fail));
×
293
            return;
×
294
        }
295

296
        notifier.OnCheckPerformed(!File.Exists(Path)
×
297
            ? new CheckEventArgs($"Could not find File '{Path}' for ProcessTask '{Name}'", CheckResult.Fail)
×
298
            : new CheckEventArgs($"Found File '{Path}'", CheckResult.Success));
×
299

300

301
        var matchingPaths = Repository.GetAllObjects<ProcessTask>().Where(pt => pt.Path.Equals(Path));
×
302
        foreach (var duplicate in matchingPaths.Except(new[] { this }))
×
303
            notifier.OnCheckPerformed(
×
304
                new CheckEventArgs(
×
305
                    $"ProcessTask '{duplicate}' (ID={duplicate.ID}) also uses file '{System.IO.Path.GetFileName(Path)}'",
×
306
                    CheckResult.Warning));
×
307

308
        //conflicting tokens in Name string
309
        foreach (Match match in Regex.Matches(Name, @"'.*((\.exe')|(\.sql'))"))
×
310
            if (match.Success)
×
311
            {
312
                var referencedFile = System.IO.Path.GetFileName(match.Value.Trim('\''));
×
313
                var actualFile = System.IO.Path.GetFileName(Path);
×
314

315
                if (referencedFile != actualFile)
×
316
                    notifier.OnCheckPerformed(
×
317
                        new CheckEventArgs(
×
318
                            $"Name of ProcessTask '{Name}' (ID={ID}) references file '{match.Value}' but the Path of the ProcessTask is '{Path}'",
×
319
                            CheckResult.Fail));
×
320
            }
321
    }
×
322

323
    /// <summary>
324
    /// Returns all tables loaded by the parent <see cref="LoadMetadata"/>
325
    /// </summary>
326
    /// <returns></returns>
327
    public IEnumerable<TableInfo> GetTableInfos() => LoadMetadata.GetDistinctTableInfoList(true);
2✔
328

329
    /// <inheritdoc/>
330
    public IEnumerable<IArgument> GetAllArguments() => ProcessTaskArguments;
278✔
331

332
    /// <inheritdoc/>
333
    public IArgument CreateNewArgument() => new ProcessTaskArgument((ICatalogueRepository)Repository, this);
1,198✔
334

335
    /// <inheritdoc/>
336
    public string GetClassNameWhoArgumentsAreFor() => Path;
18✔
337

338
    /// <summary>
339
    /// Creates a new copy of the processTask and all its arguments in the database, this clone is then hooked up to the
340
    /// new LoadMetadata at the specified stage
341
    /// </summary>
342
    /// <param name="loadMetadata">The new LoadMetadata parent for the clone</param>
343
    /// <param name="loadStage">The new load stage to put the clone in </param>
344
    /// <returns>the new ProcessTask (the clone has a different ID to the parent)</returns>
345
    public ProcessTask CloneToNewLoadMetadataStage(LoadMetadata loadMetadata, LoadStage loadStage)
346
    {
347
        var cataRepository = (ICatalogueRepository)Repository;
4✔
348

349
        //clone only accepts sql connections so make sure we aren't in mysql land or something
350
        using (cataRepository.BeginNewTransaction())
4✔
351
        {
352
            try
353
            {
354
                //get list of arguments to also clone (will happen outside of transaction
355
                var toCloneArguments = ProcessTaskArguments.ToArray();
4✔
356

357
                //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
358
                var clone = new ProcessTask(CatalogueRepository, LoadMetadata, loadStage);
4✔
359
                CopyShallowValuesTo(clone);
4✔
360

361
                //foreach of our child arguments
362
                foreach (var argument in toCloneArguments)
12✔
363
                    //clone it but rewire it to the proper ProcessTask parent (the clone)
364
                    argument.ShallowClone(clone);
2✔
365

366
                //the values passed into parameter
367
                clone.LoadMetadata_ID = loadMetadata.ID;
4✔
368
                clone.LoadStage = loadStage;
4✔
369
                clone.SaveToDatabase();
4✔
370

371
                //it worked
372
                cataRepository.EndTransaction(true);
4✔
373

374
                //return the clone
375
                return clone;
4✔
376
            }
377
            catch (Exception)
×
378
            {
379
                cataRepository.EndTransaction(false);
×
380
                throw;
×
381
            }
382
        }
383
    }
4✔
384

385
    /// <inheritdoc/>
386
    public IArgument[] CreateArgumentsForClassIfNotExists(Type t) =>
387
        ArgumentFactory.CreateArgumentsForClassIfNotExistsGeneric(
70✔
388
                t,
70✔
389

70✔
390
                //tell it how to create new instances of us related to parent
70✔
391
                this,
70✔
392

70✔
393
                //what arguments already exist
70✔
394
                GetAllArguments().ToArray())
70✔
395

70✔
396
            //convert the result back from generic to specific (us)
70✔
397
            .ToArray();
70✔
398

399
    /// <inheritdoc/>
400
    public IArgument[] CreateArgumentsForClassIfNotExists<T>() => CreateArgumentsForClassIfNotExists(typeof(T));
66✔
401

402
    /// <summary>
403
    /// 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
404
    /// load data into STAGING/LIVE - only RAW).
405
    /// </summary>
406
    /// <param name="type"></param>
407
    /// <param name="stage"></param>
408
    /// <returns></returns>
409
    public static bool IsCompatibleStage(ProcessTaskType type, LoadStage stage)
410
    {
411
        return type switch
×
412
        {
×
413
            ProcessTaskType.Executable => true,
×
414
            ProcessTaskType.SQLFile => stage != LoadStage.GetFiles,
×
415
            ProcessTaskType.SQLBakFile => stage != LoadStage.GetFiles,
×
416
            ProcessTaskType.Attacher => stage == LoadStage.Mounting,
×
417
            ProcessTaskType.DataProvider => true,
×
418
            ProcessTaskType.MutilateDataTable => stage != LoadStage.GetFiles,
×
419
            _ => throw new ArgumentOutOfRangeException(nameof(type))
×
420
        };
×
421
    }
422

423
    /// <summary>
424
    /// True if <see cref="Path"/> is the name of a C# class (as opposed to the path to an executable or SQL file etc)
425
    /// </summary>
426
    /// <returns></returns>
427
    public bool IsPluginType() => ProcessTaskType == ProcessTaskType.Attacher ||
×
428
                                  ProcessTaskType == ProcessTaskType.MutilateDataTable ||
×
429
                                  ProcessTaskType == ProcessTaskType.DataProvider;
×
430

431
    /// <summary>
432
    /// 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
433
    /// can call <see cref="CreateArgumentsForClassIfNotExists"/>
434
    /// </summary>
435
    /// <param name="parameterName"></param>
436
    /// <param name="o"></param>
437
    public void SetArgumentValue(string parameterName, object o)
438
    {
439
        var matchingArgument = ProcessTaskArguments.SingleOrDefault(p => p.Name.Equals(parameterName)) ??
2,280!
440
                               throw new Exception(
114✔
441
                                   $"Could not find a ProcessTaskArgument called '{parameterName}', have you called CreateArgumentsForClassIfNotExists<T> yet?");
114✔
442
        matchingArgument.SetValue(o);
114✔
443
        matchingArgument.SaveToDatabase();
114✔
444
    }
114✔
445
}
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

© 2026 Coveralls, Inc