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

HicServices / RDMP / 6237307473

19 Sep 2023 04:02PM UTC coverage: 57.015% (-0.4%) from 57.44%
6237307473

push

github

web-flow
Feature/rc4 (#1570)

* Syntax tidying
* Dependency updates
* Event handling singletons (ThrowImmediately and co)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: James A Sutherland <>
Co-authored-by: James Friel <jfriel001@dundee.ac.uk>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

10734 of 20259 branches covered (0.0%)

Branch coverage included in aggregate %.

5922 of 5922 new or added lines in 565 files covered. (100.0%)

30687 of 52390 relevant lines covered (58.57%)

7361.8 hits per line

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

58.33
/Rdmp.Core/DataFlowPipeline/DataFlowPipelineEngineFactory.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.Linq;
10
using System.Reflection;
11
using Rdmp.Core.Curation.Data;
12
using Rdmp.Core.Curation.Data.DataLoad;
13
using Rdmp.Core.Curation.Data.Pipelines;
14
using Rdmp.Core.DataFlowPipeline.Requirements;
15
using Rdmp.Core.DataFlowPipeline.Requirements.Exceptions;
16
using Rdmp.Core.Repositories.Construction;
17
using Rdmp.Core.ReusableLibraryCode.Checks;
18
using Rdmp.Core.ReusableLibraryCode.Progress;
19

20
namespace Rdmp.Core.DataFlowPipeline;
21

22
/// <summary>
23
/// Creates DataFlowPipelineEngines from IPipelines.  An IPipeline is the persistent user configured reusable list of components (and arguments for those components) which
24
/// will achieve a given task for the user (e.g. import a csv file).  The DataFlowPipelineContext defines both the Generic flow object of the engine (T) and which IPipelines
25
/// will be judged compatible (based on PreInitialize requirements etc).  Some contexts require a specific source/destination component that is available only at runtime
26
/// and cannot be changed/configured by the user (FixedDestination/FixedSource).  If the context requires a FixedSource or FixedDestination then you must pass the ExplicitSource
27
/// object / ExplicitDestination object into the constructor.
28
/// 
29
/// <para>In general rather than trying to use this class directly you should package up your requirements/initialization objects into a PipelineUseCase and call GetEngine. </para>
30
/// </summary>
31
public class DataFlowPipelineEngineFactory : IDataFlowPipelineEngineFactory
32
{
33
    private readonly IDataFlowPipelineContext _context;
34
    private IPipelineUseCase _useCase;
35
    private Type _flowType;
36

37
    private Type _engineType;
38

39
    /// <summary>
40
    /// Creates a new factory which can translate <see cref="IPipeline"/> blueprints into runnable <see cref="IDataFlowPipelineEngine"/> instances.
41
    /// </summary>
42
    /// <param name="useCase">The use case which describes which <see cref="IPipeline"/> are compatible, which objects are available for hydration/preinitialization etc</param>
43
    public DataFlowPipelineEngineFactory(IPipelineUseCase useCase)
98✔
44
    {
45
        _context = useCase.GetContext();
98✔
46
        _useCase = useCase;
98✔
47
        _flowType = _context.GetFlowType();
98✔
48
        _engineType = typeof(DataFlowPipelineEngine<>).MakeGenericType(_flowType);
98✔
49
    }
98✔
50

51
    /// <inheritdoc/>
52
    public DataFlowPipelineEngineFactory(IPipelineUseCase useCase, IPipeline pipeline) : this(useCase)
98✔
53
    {
54
    }
98✔
55

56
    /// <inheritdoc/>
57
    public IDataFlowPipelineEngine Create(IPipeline pipeline, IDataLoadEventListener listener)
58
    {
59
        if (!_context.IsAllowable(pipeline, out var reason))
98!
60
            throw new Exception($"Cannot create pipeline because: {reason}");
×
61

62
        var destination = GetBest(_useCase.ExplicitDestination, CreateDestinationIfExists(pipeline), "destination");
98✔
63
        var source = GetBest(_useCase.ExplicitSource, CreateSourceIfExists(pipeline), "source");
98✔
64

65
        //engine (this is the source, target is the destination)
66
        var dataFlowEngine =
98✔
67
            (IDataFlowPipelineEngine)ObjectConstructor.ConstructIfPossible(_engineType, _context, source, destination,
98✔
68
                listener, pipeline);
98✔
69

70
        //now go fetch everything that the user has configured for this particular pipeline except the source and destination
71
        //get the factory to realize the freaky Export types defined in any assembly anywhere and set their DemandsInitialization properties based on the Arguments
72
        foreach (var component in pipeline.PipelineComponents.Where(pc =>
216✔
73
                         pc.ID != pipeline.DestinationPipelineComponent_ID &&
188✔
74
                         pc.ID != pipeline.SourcePipelineComponent_ID)
188✔
75
                     .Select(CreateComponent))
98✔
76
            dataFlowEngine.ComponentObjects.Add(component);
10✔
77

78
        return dataFlowEngine;
98✔
79
    }
80

81
    /// <summary>
82
    /// Returns the thing that is not null or throws an exception because both are blank.  also throws if both are populated
83
    /// </summary>
84
    /// <typeparam name="T2"></typeparam>
85
    /// <param name="explicitThing"></param>
86
    /// <param name="pipelineConfigurationThing"></param>
87
    /// <param name="descriptionOfWhatThingIs"></param>
88
    /// <returns></returns>
89
    private static T2 GetBest<T2>(T2 explicitThing, T2 pipelineConfigurationThing, string descriptionOfWhatThingIs)
90
    {
91
        // if explicitThing and pipelineConfigurationThing are both null
92
        //Means: explicitThing == null && pipelineConfigurationThing == null
93
        if (EqualityComparer<T2>.Default.Equals(explicitThing, default) &&
196!
94
            EqualityComparer<T2>.Default.Equals(pipelineConfigurationThing, default))
196✔
95
            throw new Exception(
×
96
                $"No explicit {descriptionOfWhatThingIs} was specified and there is no fixed {descriptionOfWhatThingIs} defined in the Pipeline configuration in the Catalogue");
×
97

98
        //if one of them only is null - XOR
99
        if (EqualityComparer<T2>.Default.Equals(explicitThing, default) ^
196!
100
            EqualityComparer<T2>.Default.Equals(pipelineConfigurationThing, default))
196✔
101
            return EqualityComparer<T2>.Default.Equals(explicitThing, default)
196✔
102
                ? pipelineConfigurationThing
196✔
103
                : explicitThing; //return the not null one
196✔
104

105
        //both of them are populated
106
        throw new Exception(
×
107
            $"Cannot have both the explicit {descriptionOfWhatThingIs} '{explicitThing}' (the code creating the pipeline said it had a specific {descriptionOfWhatThingIs} it wants to use) as well as the {descriptionOfWhatThingIs} configured in the Pipeline in the Catalogue '{pipelineConfigurationThing}' (this should have been picked up by the DataFlowPipelineContext checks above)");
×
108
    }
109

110

111
    /// <summary>
112
    /// Attempts to construct an instance of the class described by <see cref="IPipelineComponent.Class"/> and fulfil its <see cref="DemandsInitializationAttribute"/>.
113
    /// Returns null and populates <paramref name="ex"/> if this is not possible/errors.
114
    /// </summary>
115
    /// <param name="component"></param>
116
    /// <param name="ex"></param>
117
    /// <returns></returns>
118
    public static object TryCreateComponent(IPipelineComponent component, out Exception ex)
119
    {
120
        ex = null;
×
121
        try
122
        {
123
            return CreateComponent(component);
×
124
        }
125
        catch (Exception e)
×
126
        {
127
            ex = e;
×
128
            return null;
×
129
        }
130
    }
×
131

132
    private static object CreateComponent(IPipelineComponent toBuild)
133
    {
134
        var type = toBuild.GetClassAsSystemType() ?? throw new Exception($"Could not find Type '{toBuild.Class}'");
198!
135
        var toReturn = ObjectConstructor.Construct(type);
198✔
136

137
        //all the IArguments we need to initialize the class
138
        var allArguments = toBuild.GetAllArguments().ToArray();
198✔
139

140
        //get all possible properties that we could set on the underlying class
141
        foreach (var propertyInfo in toReturn.GetType().GetProperties())
3,876✔
142
        {
143
            SetPropertyIfDemanded(toBuild, toReturn, propertyInfo, allArguments);
1,740✔
144

145
            //see if any demand nested initialization
146
            var nestedInit =
1,740✔
147
                Attribute.GetCustomAttributes(propertyInfo)
1,740✔
148
                    .FirstOrDefault(a => a is DemandsNestedInitializationAttribute);
2,702✔
149

150
            //this one does
151
            if (nestedInit != null)
1,740✔
152
            {
153
                // initialise the container before nesting-setting all properties
154
                var container = Activator.CreateInstance(propertyInfo.PropertyType);
6✔
155

156
                foreach (var nestedProp in propertyInfo.PropertyType.GetProperties())
28✔
157
                    SetPropertyIfDemanded(toBuild, container, nestedProp, allArguments, propertyInfo);
8✔
158

159
                //use reflection to set the container
160
                propertyInfo.SetValue(toReturn, container, null);
6✔
161
            }
162
        }
163

164
        return toReturn;
198✔
165
    }
166

167
    /// <summary>
168
    /// Sets the value of a property on instance toReturn.
169
    /// </summary>
170
    /// <param name="toBuild">IPipelineComponent which is the persistence record - the template of what to build</param>
171
    /// <param name="toReturn">An instance of the Class referenced by IPipelineComponent.Class (or in the case of [DemandsNestedInitializationAttribute] a reference to the nested property)</param>
172
    /// <param name="propertyInfo">The specific property you are trying to populate on toBuild</param>
173
    /// <param name="arguments">IArguments of toBuild (the values to populate toReturn with)</param>
174
    /// <param name="nestedProperty">If you are populating a sub property of the class then pass the instance of the sub property as toBuild and pass the nesting property as nestedProperty</param>
175
    private static void SetPropertyIfDemanded(IPipelineComponent toBuild, object toReturn, PropertyInfo propertyInfo,
176
        IArgument[] arguments, PropertyInfo nestedProperty = null)
177
    {
178
        //see if any demand initialization
179
        var initialization =
1,748✔
180
            (DemandsInitializationAttribute)
1,748✔
181
            Attribute.GetCustomAttributes(propertyInfo)
1,748✔
182
                .FirstOrDefault(a => a is DemandsInitializationAttribute);
2,718✔
183

184
        //this one does
185
        if (initialization != null)
1,748✔
186
            try
187
            {
188
                //look for 'DeleteUsers' if not nested
189
                //look for 'Settings.DeleteUsers' if nested in a property called Settings on class
190
                var expectedArgumentName = nestedProperty != null
964✔
191
                    ? $"{nestedProperty.Name}.{propertyInfo.Name}"
964✔
192
                    : propertyInfo.Name;
964✔
193

194
                //get the appropriate value from arguments
195
                var argument = arguments.SingleOrDefault(n => n.Name.Equals(expectedArgumentName));
8,932✔
196

197
                //if there is no matching argument and no default value
198
                if (argument == null)
964!
199
                    if (initialization.DefaultValue == null && initialization.Mandatory)
×
200
                    {
201
                        var msg =
×
202
                            $"Class {toReturn.GetType().Name} has a property {propertyInfo.Name} marked with DemandsInitialization but no corresponding argument was found in the arguments (PipelineComponentArgument) of the PipelineComponent called {toBuild.Name}";
×
203

204
                        throw new PropertyDemandNotMetException(msg, toBuild, propertyInfo);
×
205
                    }
206
                    else
207
                    {
208
                        //use reflection to set the value
209
                        propertyInfo.SetValue(toReturn, initialization.DefaultValue, null);
×
210
                    }
211
                else
212
                    //use reflection to set the value
213
                    propertyInfo.SetValue(toReturn, argument.GetValueAsSystemType(), null);
964✔
214
            }
964✔
215
            catch (NotSupportedException e)
×
216
            {
217
                throw new Exception(
×
218
                    $"Class {toReturn.GetType().Name} has a property {propertyInfo.Name} but is of unexpected/unsupported type {propertyInfo.GetType()}",
×
219
                    e);
×
220
            }
221
    }
1,748✔
222

223
    /// <summary>
224
    /// Retrieves and creates an instance of the class described in the blueprint <see cref="IPipeline.Source"/> if there is one.  Pipelines do not have
225
    /// to have a source if the use case requires a fixed source instance generated at runtime.
226
    /// </summary>
227
    /// <param name="pipeline"></param>
228
    /// <returns></returns>
229
    public static object CreateSourceIfExists(IPipeline pipeline)
230
    {
231
        var source = pipeline.Source;
98✔
232

233
        //there is no configured destination
234
        return source == null ? null : CreateComponent(source);
98✔
235
    }
236

237
    /// <summary>
238
    /// Retrieves and creates an instance of the class described in the blueprint <see cref="IPipeline.Destination"/> if there is one.  Pipelines do not have
239
    /// to have a destination if the use case requires a fixed destination instance generated at runtime
240
    /// </summary>
241
    public static object CreateDestinationIfExists(IPipeline pipeline)
242
    {
243
        var destination = pipeline.Destination;
100✔
244

245
        //there is no configured destination
246
        if (destination == null)
100✔
247
            return null;
8✔
248

249
        //throw new NotSupportedException("The IsDestination PipelineComponent of pipeline '" + pipeline.Name + "' is an IDataFlowComponent but it is not an IDataFlowDestination which is a requirement of all destinations");
250

251
        return CreateComponent(destination);
92✔
252
    }
253

254
    /// <summary>
255
    /// Attempts to create an instance of <see cref="IDataFlowPipelineEngine"/> described by the blueprint <paramref name="pipeline"/>.  Components are then checked if they
256
    /// support <see cref="ICheckable"/> using the <paramref name="checkNotifier"/> to record the results.
257
    /// </summary>
258
    /// <param name="pipeline">The blueprint to attempt to generate</param>
259
    /// <param name="checkNotifier">The event notifier to record how it went</param>
260
    /// <param name="initizationObjects">The objects available for fulfilling IPipelineRequirements</param>
261
    public void Check(IPipeline pipeline, ICheckNotifier checkNotifier, object[] initizationObjects)
262
    {
263
        //Try to construct the pipeline into an in memory Engine based on the in Catalogue blueprint (Pipeline)
264
        IDataFlowPipelineEngine dataFlowPipelineEngine = null;
×
265
        try
266
        {
267
            dataFlowPipelineEngine = Create(pipeline, new FromCheckNotifierToDataLoadEventListener(checkNotifier));
×
268
            checkNotifier.OnCheckPerformed(new CheckEventArgs("Pipeline successfully constructed in memory",
×
269
                CheckResult.Success));
×
270
        }
×
271
        catch (Exception exception)
×
272
        {
273
            checkNotifier.OnCheckPerformed(
×
274
                new CheckEventArgs("Failed to construct pipeline, see Exception for details", CheckResult.Fail,
×
275
                    exception));
×
276
        }
×
277

278
        if (initizationObjects == null)
×
279
        {
280
            checkNotifier.OnCheckPerformed(new CheckEventArgs(
×
281
                "initializationObjects parameter has not been set (this is a programmer error most likely ask your developer to fix it - this parameter should be empty not null)",
×
282
                CheckResult.Fail));
×
283
            return;
×
284
        }
285

286
        //Initialize each component with the initialization objects so that they can check themselves (note that this should be preview data, hopefully the components don't run off and start nuking stuff just because they got their GO objects)
287
        if (dataFlowPipelineEngine != null)
×
288
        {
289
            try
290
            {
291
                dataFlowPipelineEngine.Initialize(initizationObjects);
×
292
                checkNotifier.OnCheckPerformed(
×
293
                    new CheckEventArgs(
×
294
                        $"Pipeline successfully initialized with {initizationObjects.Length} initialization objects",
×
295
                        CheckResult.Success));
×
296
            }
×
297
            catch (Exception exception)
×
298
            {
299
                checkNotifier.OnCheckPerformed(
×
300
                    new CheckEventArgs(
×
301
                        $"Pipeline initialization failed, there were {initizationObjects.Length} objects for use in initialization ({string.Join(",", initizationObjects.Select(o => o.ToString()))})",
×
302
                        CheckResult.Fail,
×
303
                        exception));
×
304
            }
×
305

306
            checkNotifier.OnCheckPerformed(new CheckEventArgs("About to check engine/components", CheckResult.Success));
×
307
            dataFlowPipelineEngine.Check(checkNotifier);
×
308
        }
309
    }
×
310
}
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