/// <summary>
        /// Initializes <see cref="SynchronizedClientSubscription"/>.
        /// </summary>
        public override void Initialize()
        {
            MeasurementKey[] inputMeasurementKeys;
            string           setting;

            if (Settings.TryGetValue("inputMeasurementKeys", out setting))
            {
                // IMPORTANT: The allowSelect argument of ParseInputMeasurementKeys must be null
                //            in order to prevent SQL injection via the subscription filter expression
                inputMeasurementKeys   = AdapterBase.ParseInputMeasurementKeys(DataSource, false, setting);
                m_requestedInputFilter = setting;

                // IMPORTANT: We need to remove the setting before calling base.Initialize()
                //            or else we will still be subject to SQL injection
                Settings.Remove("inputMeasurementKeys");
            }
            else
            {
                inputMeasurementKeys   = new MeasurementKey[0];
                m_requestedInputFilter = null;
            }

            base.Initialize();

            // Set the InputMeasurementKeys and UsePrecisionTimer properties after calling
            // base.Initialize() so that the base class does not overwrite our settings
            InputMeasurementKeys = inputMeasurementKeys;
            UsePrecisionTimer    = false;

            if (Settings.TryGetValue("bufferBlockRetransmissionTimeout", out setting))
            {
                m_bufferBlockRetransmissionTimeout = double.Parse(setting);
            }
            else
            {
                m_bufferBlockRetransmissionTimeout = 5.0D;
            }

            if (Settings.TryGetValue("requestNaNValueFilter", out setting))
            {
                m_isNaNFiltered = m_parent.AllowNaNValueFilter && setting.ParseBoolean();
            }
            else
            {
                m_isNaNFiltered = false;
            }

            m_bufferBlockRetransmissionTimer           = Common.TimerScheduler.CreateTimer((int)(m_bufferBlockRetransmissionTimeout * 1000.0D));
            m_bufferBlockRetransmissionTimer.AutoReset = false;
            m_bufferBlockRetransmissionTimer.Elapsed  += BufferBlockRetransmissionTimer_Elapsed;

            // Handle temporal session initialization
            if (this.TemporalConstraintIsDefined())
            {
                m_iaonSession = this.CreateTemporalSession();
            }
        }
Beispiel #2
0
        public IMeasurement[] GetMeasurements(string filterExpression)
        {
            MeasurementKey[] keys        = AdapterBase.ParseInputMeasurementKeys(m_dataSource, false, filterExpression);
            IMeasurement     measurement = null;

            return(keys
                   .Where(key => m_measurementLookup.TryGetValue(key, out measurement))
                   .Select(key => measurement)
                   .ToArray());
        }
Beispiel #3
0
        public MeasurementKey GetMeasurementKey(string filterExpression)
        {
            MeasurementKey[] keys = AdapterBase.ParseInputMeasurementKeys(m_dataSource, false, filterExpression);

            if (keys.Length > 1)
            {
                throw new InvalidOperationException($"Ambiguous filter returned {keys.Length} measurement keys: {filterExpression}.");
            }

            if (keys.Length == 0)
            {
                return(MeasurementKey.Undefined);
            }

            return(keys[0]);
        }
Beispiel #4
0
        public virtual void RefreshMetadata()
        {
            // Force a recalculation of input measurement keys so that system can appropriately update routing tables
            string setting;

            if (Settings.TryGetValue("inputMeasurementKeys", out setting))
            {
                InputMeasurementKeys = AdapterBase.ParseInputMeasurementKeys(DataSource, setting);
            }
            else
            {
                InputMeasurementKeys = null;
            }

            InputSourceIDs = InputSourceIDs;
        }
Beispiel #5
0
        public IMeasurement GetMeasurement(string filterExpression)
        {
            MeasurementKey[] keys = AdapterBase.ParseInputMeasurementKeys(m_dataSource, false, filterExpression);
            IMeasurement     measurement;

            if (keys.Length > 1)
            {
                throw new InvalidOperationException($"Ambiguous filter returned {keys.Length} measurements: {filterExpression}.");
            }

            if (keys.Length == 0 || !m_measurementLookup.TryGetValue(keys[0], out measurement))
            {
                return(null);
            }

            return(measurement);
        }
Beispiel #6
0
        /// <summary>
        /// Parses connection string. Derived classes should override for custom connection string parsing.
        /// </summary>
        /// <param name="instance">Target <see cref="IIndependentAdapterManager"/> instance.</param>
        public static void HandleParseConnectionString(this IIndependentAdapterManager instance)
        {
            // Parse all properties marked with ConnectionStringParameterAttribute from provided ConnectionString value
            ConnectionStringParser parser = new ConnectionStringParser();

            parser.ParseConnectionString(instance.ConnectionString, instance);

            // Parse input measurement keys like class was a typical adapter
            if (instance.Settings.TryGetValue(nameof(instance.InputMeasurementKeys), out string setting))
            {
                instance.InputMeasurementKeys = AdapterBase.ParseInputMeasurementKeys(instance.DataSource, true, setting, instance.SourceMeasurementTable);
            }

            // Parse output measurement keys like class was a typical adapter
            if (instance.Settings.TryGetValue(nameof(instance.OutputMeasurements), out setting))
            {
                instance.OutputMeasurements = AdapterBase.ParseOutputMeasurements(instance.DataSource, true, setting, instance.SourceMeasurementTable);
            }
        }
Beispiel #7
0
        private void AssignInputMeasurements(Dictionary <string, string> settings)
        {
            if (!settings.ContainsKey("inputMeasurementKeys") && settings.TryGetValue("variableList", out string variableList))
            {
                Dictionary <string, string> variables = variableList.ParseKeyValuePairs();

                if (variables.Count > 0)
                {
                    settings["inputMeasurementKeys"] = string.Join(";", variables.Values);
                }
            }

            MeasurementKey[] inputMeasurementKeys = settings.TryGetValue("inputMeasurementKeys", out string setting) ?
                                                    AdapterBase.ParseInputMeasurementKeys(m_dataSource, false, setting) :
                                                    Array.Empty <MeasurementKey>();

            m_inputSignalIDs = inputMeasurementKeys.Select(k => k.SignalID).ToArray();
            LoadMeasurements(m_inputSignalIDs, dataGridViewInputMeasurements, groupBoxInputMeasurements);
        }
Beispiel #8
0
        private List <MetadataRecord> GetMetadata()
        {
            Ticks operationTime;
            Ticks operationStartTime;

            // Load historian meta-data
            ShowMessage(">>> Loading source connection metadata...");

            operationStartTime = DateTime.UtcNow.Ticks;
            List <MetadataRecord> metadata = MetadataRecord.Query(m_settings.HostAddress, m_settings.MetadataPort, m_settings.MetadataTimeout);

            operationTime = DateTime.UtcNow.Ticks - operationStartTime;

            ShowMessage("*** Metadata Load Complete ***");
            ShowMessage($"Total metadata load time {operationTime.ToElapsedTimeString(3)}...");

            // Parse meta-data expression
            ShowMessage(">>> Processing filter expression for metadata...");
            operationStartTime = DateTime.UtcNow.Ticks;
            MeasurementKey[]      inputKeys   = AdapterBase.ParseInputMeasurementKeys(MetadataRecord.Metadata, false, m_settings.PointList, "MeasurementDetail");
            List <ulong>          pointIDList = inputKeys.Select(key => (ulong)key.ID).ToList();
            List <MetadataRecord> records     = new List <MetadataRecord>();

            foreach (ulong pointID in pointIDList)
            {
                MetadataRecord record = metadata.FirstOrDefault(md => md.PointID == pointID);

                if ((object)record != null)
                {
                    records.Add(record);
                }
            }

            operationTime = DateTime.UtcNow.Ticks - operationStartTime;

            ShowMessage($">>> Historian read will be for {pointIDList.Count:N0} points based on provided meta-data expression.");

            ShowMessage("*** Filter Expression Processing Complete ***");
            ShowMessage($"Total filter expression processing time {operationTime.ToElapsedTimeString(3)}...");

            return(records);
        }
Beispiel #9
0
        /// <summary>
        /// Determines if subscriber has rights to specified <paramref name="signalID"/>.
        /// </summary>
        /// <param name="signalID"><see cref="Guid"/> signal ID to lookup.</param>
        /// <returns><c>true</c> if subscriber has rights to specified <paramref name="signalID"/>; otherwise <c>false</c>.</returns>
        public bool SubscriberHasRights(Guid signalID)
        {
            // TODO: Abstract publisher ACL logic -- except subscriber enabled flag does not need to be checked
            const string FilterRegex = @"(ALLOW|DENY)\s+WHERE\s+([^;]*)";

            DataRow subscriber;

            DataRow[] subscriberMeasurementGroups;

            IEnumerable <bool> explicitAuthorizationFlags;
            IEnumerable <bool> explicitGroupAuthorizationFlags;
            IEnumerable <bool> implicitFilterAuthorizationFlags;

            bool explicitlyAuthorized         = false;
            bool explicitlyAuthorizedByGroup  = false;
            bool implicitlyAuthorizedByFilter = false;

            // This is different from the ACL logic used by the system --
            // because we are only calculating effective permissions to aid with configuration,
            // we do not care whether the Subscriber is enabled
            subscriber = m_subscriberPermissionsDataSet.Tables["Subscribers"].Select(string.Format("ID = '{0}'", CurrentItem.ID)).FirstOrDefault();

            // If subscriber has been disabled or removed
            // from the list of valid subscribers,
            // they no longer have rights to any signals
            if ((object)subscriber == null)
            {
                return(false);
            }

            // Look up explicitly defined individual measurements
            explicitAuthorizationFlags = m_subscriberPermissionsDataSet.Tables["SubscriberMeasurements"].Select(string.Format("SubscriberID = '{0}' AND SignalID = '{1}'", CurrentItem.ID, signalID))
                                         .Select(measurement => measurement["Allowed"].ToNonNullString("0").ParseBoolean());

            foreach (bool flag in explicitAuthorizationFlags)
            {
                if (flag)
                {
                    explicitlyAuthorized = true;
                }
                else
                {
                    return(false);
                }
            }

            if (explicitlyAuthorized)
            {
                return(true);
            }

            // Look up explicitly defined group based measurements
            subscriberMeasurementGroups = m_subscriberPermissionsDataSet.Tables["SubscriberMeasurementGroups"].Select(string.Format("SubscriberID = '{0}'", CurrentItem.ID));

            explicitGroupAuthorizationFlags = subscriberMeasurementGroups
                                              .Where(subscriberMeasurementGroup => m_subscriberPermissionsDataSet.Tables["MeasurementGroupMeasurements"].Select(string.Format("SignalID = '{0}' AND MeasurementGroupID = {1}", signalID, subscriberMeasurementGroup["MeasurementGroupID"])).Length > 0)
                                              .Select(subscriberMeasurementGroup => subscriberMeasurementGroup["Allowed"].ToNonNullString("0").ParseBoolean());

            foreach (bool flag in explicitGroupAuthorizationFlags)
            {
                if (flag)
                {
                    explicitlyAuthorizedByGroup = true;
                }
                else
                {
                    return(false);
                }
            }

            if (explicitlyAuthorizedByGroup)
            {
                return(true);
            }

            // Look up implicitly defined filter based measurements
            implicitFilterAuthorizationFlags = Regex.Matches(subscriber["AccessControlFilter"].ToNonNullString().ReplaceControlCharacters(), FilterRegex, RegexOptions.IgnoreCase)
                                               .Cast <Match>()
                                               .Where(match => m_subscriberPermissionsDataSet.Tables["ActiveMeasurements"].Select(string.Format("SignalID = '{0}' AND ({1})", signalID, match.Groups[2].Value)).Length > 0)
                                               .Select(match => (match.Groups[1].Value == "ALLOW"));

            foreach (bool flag in implicitFilterAuthorizationFlags)
            {
                if (flag)
                {
                    implicitlyAuthorizedByFilter = true;
                }
                else
                {
                    return(false);
                }
            }

            if (implicitlyAuthorizedByFilter)
            {
                return(true);
            }

            // Look up implicitly defined group based measurements
            return(subscriberMeasurementGroups
                   .Select(subscriberMeasurementGroup => Tuple.Create(subscriberMeasurementGroup, m_subscriberPermissionsDataSet.Tables["MeasurementGroups"].Select(string.Format("ID = {0}", subscriberMeasurementGroup["MeasurementGroupID"]))))
                   .Where(tuple => tuple.Item2.Any(measurementGroup => AdapterBase.ParseInputMeasurementKeys(m_subscriberPermissionsDataSet, false, measurementGroup["FilterExpression"].ToNonNullString()).Select(key => key.SignalID).Contains(signalID)))
                   .Select(tuple => tuple.Item1["Allowed"].ToNonNullString("0").ParseBoolean())
                   .DefaultIfEmpty(false)
                   .All(allowed => allowed));
        }
Beispiel #10
0
 public MeasurementKey[] GetMeasurementKeys(string filterExpression)
 {
     return(AdapterBase.ParseInputMeasurementKeys(m_dataSource, false, filterExpression));
 }
Beispiel #11
0
        private IEnumerable <DataSourceValueGroup> QueryTarget(Target sourceTarget, string queryExpression, DateTime startTime, DateTime stopTime, string interval, bool decimate, bool dropEmptySeries, CancellationToken cancellationToken)
        {
            if (queryExpression.ToLowerInvariant().Contains(DropEmptySeriesCommand))
            {
                dropEmptySeries = true;
                queryExpression = queryExpression.ReplaceCaseInsensitive(DropEmptySeriesCommand, "");
            }

            // A single target might look like the following:
            // PPA:15; STAT:20; SETSUM(COUNT(PPA:8; PPA:9; PPA:10)); FILTER ActiveMeasurements WHERE SignalType IN ('IPHA', 'VPHA'); RANGE(PPA:99; SUM(FILTER ActiveMeasurements WHERE SignalType = 'FREQ'; STAT:12))

            HashSet <string> targetSet        = new HashSet <string>(new[] { queryExpression }, StringComparer.OrdinalIgnoreCase); // Targets include user provided input, so casing should be ignored
            HashSet <string> reducedTargetSet = new HashSet <string>(StringComparer.OrdinalIgnoreCase);
            List <Match>     seriesFunctions  = new List <Match>();

            foreach (string target in targetSet)
            {
                // Find any series functions in target
                Match[] matchedFunctions = TargetCache <Match[]> .GetOrAdd(target, () =>
                                                                           s_seriesFunctions.Matches(target).Cast <Match>().ToArray());

                if (matchedFunctions.Length > 0)
                {
                    seriesFunctions.AddRange(matchedFunctions);

                    // Reduce target to non-function expressions - important so later split on ';' succeeds properly
                    string reducedTarget = target;

                    foreach (string expression in matchedFunctions.Select(match => match.Value))
                    {
                        reducedTarget = reducedTarget.Replace(expression, "");
                    }

                    if (!string.IsNullOrWhiteSpace(reducedTarget))
                    {
                        reducedTargetSet.Add(reducedTarget);
                    }
                }
                else
                {
                    reducedTargetSet.Add(target);
                }
            }

            if (seriesFunctions.Count > 0)
            {
                // Execute series functions
                foreach (Tuple <SeriesFunction, string, GroupOperation> parsedFunction in seriesFunctions.Select(ParseSeriesFunction))
                {
                    foreach (DataSourceValueGroup valueGroup in ExecuteSeriesFunction(sourceTarget, parsedFunction, startTime, stopTime, interval, decimate, dropEmptySeries, cancellationToken))
                    {
                        yield return(valueGroup);
                    }
                }

                // Use reduced target set that excludes any series functions
                targetSet = reducedTargetSet;
            }

            // Query any remaining targets
            if (targetSet.Count > 0)
            {
                // Split remaining targets on semi-colon, this way even multiple filter expressions can be used as inputs to functions
                string[] allTargets = targetSet.Select(target => target.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)).SelectMany(currentTargets => currentTargets).ToArray();

                // Expand target set to include point tags for all parsed inputs
                foreach (string target in allTargets)
                {
                    targetSet.UnionWith(TargetCache <string[]> .GetOrAdd(target, () => AdapterBase.ParseInputMeasurementKeys(Metadata, false, target).Select(key => key.TagFromKey(Metadata)).ToArray()));
                }

                Dictionary <ulong, string> targetMap = new Dictionary <ulong, string>();

                // Target set now contains both original expressions and newly parsed individual point tags - to create final point list we
                // are only interested in the point tags, provided either by direct user entry or derived by parsing filter expressions
                foreach (string target in targetSet)
                {
                    // Reduce all targets down to a dictionary of point ID's mapped to point tags
                    MeasurementKey key = TargetCache <MeasurementKey> .GetOrAdd(target, () => target.KeyFromTag(Metadata));

                    if (key == MeasurementKey.Undefined)
                    {
                        Tuple <MeasurementKey, string> result = TargetCache <Tuple <MeasurementKey, string> > .GetOrAdd($"signalID@{target}", () => target.KeyAndTagFromSignalID(Metadata));

                        key = result.Item1;
                        string pointTag = result.Item2;

                        if (key == MeasurementKey.Undefined)
                        {
                            result = TargetCache <Tuple <MeasurementKey, string> > .GetOrAdd($"key@{target}", () =>
                            {
                                MeasurementKey.TryParse(target, out MeasurementKey parsedKey);
                                return(new Tuple <MeasurementKey, string>(parsedKey, parsedKey.TagFromKey(Metadata)));
                            });

                            key      = result.Item1;
                            pointTag = result.Item2;

                            if (key != MeasurementKey.Undefined)
                            {
                                targetMap[key.ID] = pointTag;
                            }
                        }
                        else
                        {
                            targetMap[key.ID] = pointTag;
                        }
                    }
                    else
                    {
                        targetMap[key.ID] = target;
                    }
                }

                // Query underlying data source for each target - to prevent parallel read from data source we enumerate immediately
                List <DataSourceValue> dataValues = QueryDataSourceValues(startTime, stopTime, interval, decimate, targetMap)
                                                    .TakeWhile(_ => !cancellationToken.IsCancellationRequested).ToList();

                foreach (KeyValuePair <ulong, string> target in targetMap)
                {
                    yield return new DataSourceValueGroup
                           {
                               Target          = target.Value,
                               RootTarget      = target.Value,
                               SourceTarget    = sourceTarget,
                               Source          = dataValues.Where(dataValue => dataValue.Target.Equals(target.Value)),
                               DropEmptySeries = dropEmptySeries
                           }
                }
                ;
            }
        }
Beispiel #12
0
        private Func <Guid, bool> BuildLookup(DataSet dataSource, Guid subscriberID)
        {
            HashSet <Guid> authorizedSignals = new HashSet <Guid>();

            const string filterRegex = @"(ALLOW|DENY)\s+WHERE\s+([^;]*)";

            DataRow subscriber;

            DataRow[] subscriberMeasurementGroups;

            //==================================================================
            //Check if subscriber is disabled or removed

            // If subscriber has been disabled or removed
            // from the list of valid subscribers,
            // they no longer have rights to any signals
            subscriber = dataSource.Tables["Subscribers"].Select($"ID = '{subscriberID}' AND Enabled <> 0").FirstOrDefault();

            if ((object)subscriber == null)
            {
                return(id => false);
            }

            //=================================================================
            // Check group implicitly authorized signals

            subscriberMeasurementGroups = dataSource.Tables["SubscriberMeasurementGroups"].Select($"SubscriberID = '{subscriberID}'");

            subscriberMeasurementGroups
            .Join(dataSource.Tables["MeasurementGroups"].Select(),
                  row => row.ConvertField <int>("MeasurementGroupID"),
                  row => row.ConvertField <int>("ID"),
                  (subscriberMeasurementGroup, measurementGroup) =>
            {
                bool allowed  = subscriberMeasurementGroup.ConvertField <bool>("Allowed");
                string filter = measurementGroup.ConvertField <string>("FilterExpression");

                return(AdapterBase.ParseInputMeasurementKeys(dataSource, false, filter)
                       .Select(key => new { Allowed = allowed, key.SignalID }));
            })
            .SelectMany(list => list)
            .GroupBy(obj => obj.SignalID)
            .Where(grouping => grouping.All(obj => obj.Allowed))
            .ToList()
            .ForEach(grouping => authorizedSignals.Add(grouping.Key));

            //=================================================================
            //Check implicitly authorized signals

            List <Match> matches = Regex.Matches(subscriber["AccessControlFilter"].ToNonNullString().ReplaceControlCharacters(), filterRegex, RegexOptions.IgnoreCase)
                                   .Cast <Match>()
                                   .ToList();

            // Combine individual allow statements into a single measurement filter
            string allowFilter = string.Join(" OR ", matches
                                             .Where(match => match.Groups[1].Value == "ALLOW")
                                             .Select(match => $"({match.Groups[2].Value})"));

            // Combine individual deny statements into a single measurement filter
            string denyFilter = string.Join(" OR ", matches
                                            .Where(match => match.Groups[1].Value == "DENY")
                                            .Select(match => $"({match.Groups[2].Value})"));

            if (!string.IsNullOrEmpty(allowFilter))
            {
                foreach (DataRow row in dataSource.Tables["ActiveMeasurements"].Select(allowFilter))
                {
                    authorizedSignals.Add(row.ConvertField <Guid>("SignalID"));
                }
            }

            if (!string.IsNullOrEmpty(denyFilter))
            {
                foreach (DataRow row in dataSource.Tables["ActiveMeasurements"].Select(denyFilter))
                {
                    authorizedSignals.Remove(row.ConvertField <Guid>("SignalID"));
                }
            }

            //==================================================================
            //Check explicit group authorizations

            subscriberMeasurementGroups
            .Join(dataSource.Tables["MeasurementGroupMeasurements"].Select(),
                  row => row.ConvertField <int>("MeasurementGroupID"),
                  row => row.ConvertField <int>("MeasurementGroupID"),
                  (subscriberMeasurementGroup, measurementGroupMeasurement) => new
            {
                Allowed  = subscriberMeasurementGroup.ConvertField <bool>("Allowed"),
                SignalID = measurementGroupMeasurement.ConvertField <Guid>("SignalID")
            })
            .GroupBy(obj => obj.SignalID)
            .Select(grouping => new
            {
                Allowed  = grouping.All(obj => obj.Allowed),
                SignalID = grouping.Key
            })
            .ToList()
            .ForEach(obj =>
            {
                if (obj.Allowed)
                {
                    authorizedSignals.Add(obj.SignalID);
                }
                else
                {
                    authorizedSignals.Remove(obj.SignalID);
                }
            });

            //===================================================================
            // Check explicit authorizations

            DataRow[] explicitAuthorizations = dataSource.Tables["SubscriberMeasurements"].Select($"SubscriberID = '{subscriberID}'");

            // Add all explicitly authorized signals to authorizedSignals
            foreach (DataRow explicitAuthorization in explicitAuthorizations)
            {
                if (explicitAuthorization.ConvertField <bool>("Allowed"))
                {
                    authorizedSignals.Add(explicitAuthorization.ConvertField <Guid>("SignalID"));
                }
            }

            // Remove all explicitly unauthorized signals from authorizedSignals
            foreach (DataRow explicitAthorization in explicitAuthorizations)
            {
                if (!explicitAthorization.ConvertField <bool>("Allowed"))
                {
                    authorizedSignals.Remove(explicitAthorization.ConvertField <Guid>("SignalID"));
                }
            }

            return(id => authorizedSignals.Contains(id));
        }
Beispiel #13
0
        // Internal Functions

        private void ReadArchive(object state)
        {
            try
            {
                double    timeRange           = (m_settings.EndTime - m_settings.StartTime).TotalSeconds;
                long      receivedPoints      = 0;
                long      processedDataBlocks = 0;
                long      duplicatePoints     = 0;
                Ticks     operationTime;
                Ticks     operationStartTime;
                DataPoint point          = new DataPoint();
                DateTime  firstTimestamp = new DateTime(0L);
                DateTime  lastTimestamp  = new DateTime(0L);

                using (Algorithm algorithm = new Algorithm())
                {
                    algorithm.ShowMessage     = ShowUpdateMessage;
                    algorithm.MessageInterval = m_settings.MessageInterval;
                    algorithm.StartTime       = m_settings.StartTime;
                    algorithm.EndTime         = m_settings.EndTime;
                    algorithm.FrameRate       = m_settings.FrameRate;
                    algorithm.TimeRange       = timeRange;
                    algorithm.Log             = m_log;

                    // Load historian meta-data
                    ShowUpdateMessage(">>> Loading source connection metadata...");

                    operationStartTime = DateTime.UtcNow.Ticks;
                    algorithm.Metadata = MetadataRecord.Query(m_settings.HostAddress, m_settings.MetadataPort, m_settings.MetadataTimeout);
                    operationTime      = DateTime.UtcNow.Ticks - operationStartTime;

                    ShowUpdateMessage("*** Metadata Load Complete ***");
                    ShowUpdateMessage($"Total metadata load time {operationTime.ToElapsedTimeString(3)}...");

                    ShowUpdateMessage(">>> Processing filter expression for metadata...");

                    operationStartTime = DateTime.UtcNow.Ticks;
                    MeasurementKey[] inputKeys   = AdapterBase.ParseInputMeasurementKeys(MetadataRecord.Metadata, false, textBoxPointList.Text, "MeasurementDetail");
                    List <ulong>     pointIDList = inputKeys.Select(key => (ulong)key.ID).ToList();
                    operationTime = DateTime.UtcNow.Ticks - operationStartTime;

                    // Allow algorithm to augment (or even replace) point ID list as provided by user
                    algorithm.AugmentPointIDList(pointIDList);

                    ShowUpdateMessage($">>> Historian read will be for {pointIDList.Count:N0} points based on provided meta-data expression and algorithm augmentation.");

                    // Reduce metadata to filtered point list
                    ShowUpdateMessage($">>> Reducing metadata to the {pointIDList.Count:N0} defined points...");

                    List <MetadataRecord> records = new List <MetadataRecord>();

                    foreach (ulong pointID in pointIDList)
                    {
                        MetadataRecord record = algorithm.Metadata.FirstOrDefault(metadata => metadata.PointID == pointID);

                        if ((object)record != null)
                        {
                            records.Add(record);
                        }
                    }

                    algorithm.Metadata = records;

                    ShowUpdateMessage("*** Filter Expression Processing Complete ***");
                    ShowUpdateMessage($"Total filter expression processing time {operationTime.ToElapsedTimeString(3)}...");

                    ShowUpdateMessage(">>> Initializing algorithm...");
                    algorithm.Initialize();

                    ShowUpdateMessage(">>> Starting archive read...");

                    // Start historian data read
                    operationStartTime = DateTime.UtcNow.Ticks;

                    using (SnapDBClient historianClient = new SnapDBClient(m_settings.HostAddress, m_settings.DataPort, m_settings.InstanceName, m_settings.StartTime, m_settings.EndTime, m_settings.FrameRate, pointIDList))
                    {
                        // Scan to first record
                        if (!historianClient.ReadNext(point))
                        {
                            throw new InvalidOperationException("No data for specified time range in openHistorian connection!");
                        }

                        ulong currentTimestamp;
                        receivedPoints++;

                        while (!m_formClosing)
                        {
                            int  timeComparison;
                            bool readSuccess = true;

                            // Create a new data block for current timestamp and load first/prior point
                            Dictionary <ulong, DataPoint> dataBlock = new Dictionary <ulong, DataPoint>
                            {
                                [point.PointID] = point.Clone()
                            };

                            currentTimestamp = point.Timestamp;

                            // Load remaining data for current timestamp
                            do
                            {
                                // Scan to next record
                                if (!historianClient.ReadNext(point))
                                {
                                    readSuccess = false;
                                    break;
                                }

                                receivedPoints++;
                                timeComparison = DataPoint.CompareTimestamps(point.Timestamp, currentTimestamp, m_settings.FrameRate);

                                if (timeComparison == 0)
                                {
                                    // Timestamps are compared based on configured frame rate - if archived data rate is
                                    // higher than configured frame rate, then data block will contain only latest values
                                    if (dataBlock.ContainsKey(point.PointID))
                                    {
                                        duplicatePoints++;
                                    }

                                    dataBlock[point.PointID] = point.Clone();
                                }
                            }while (timeComparison == 0);

                            // Finished with data read
                            if (!readSuccess)
                            {
                                ShowUpdateMessage(">>> End of data in range encountered...");
                                break;
                            }

                            if (++processedDataBlocks % m_settings.MessageInterval == 0)
                            {
                                ShowUpdateMessage($"{Environment.NewLine}{receivedPoints:N0} points{(duplicatePoints > 0 ? $", which included {duplicatePoints:N0} duplicates," : "")} read so far averaging {receivedPoints / (DateTime.UtcNow.Ticks - operationStartTime).ToSeconds():N0} points per second.");
                                UpdateProgressBar((int)((1.0D - new Ticks(m_settings.EndTime.Ticks - (long)point.Timestamp).ToSeconds() / timeRange) * 100.0D));
                            }

                            try
                            {
                                lastTimestamp = new DateTime((long)currentTimestamp);

                                if (firstTimestamp.Ticks == 0L)
                                {
                                    firstTimestamp = lastTimestamp;
                                }

                                // Analyze data block
                                algorithm.Execute(lastTimestamp, dataBlock.Values.ToArray());
                            }
                            catch (Exception ex)
                            {
                                ShowUpdateMessage($"ERROR: Algorithm exception: {ex.Message}");
                                m_log.Publish(MessageLevel.Error, "AlgorithmError", "Failed while processing data from the historian", exception: ex);
                            }
                        }

                        operationTime = DateTime.UtcNow.Ticks - operationStartTime;

                        if (m_formClosing)
                        {
                            ShowUpdateMessage("*** Historian Read Canceled ***");
                            UpdateProgressBar(0);
                        }
                        else
                        {
                            ShowUpdateMessage("*** Historian Read Complete ***");
                            UpdateProgressBar(100);
                        }

                        algorithm.Complete();

                        // Show some operational statistics
                        long   expectedPoints   = (long)(timeRange * m_settings.FrameRate * algorithm.Metadata.Count);
                        double dataCompleteness = Math.Round(receivedPoints / (double)expectedPoints * 100000.0D) / 1000.0D;

                        string overallSummary =
                            $"Total processing time {operationTime.ToElapsedTimeString(3)} at {receivedPoints / operationTime.ToSeconds():N0} points per second.{Environment.NewLine}" +
                            $"{Environment.NewLine}" +
                            $"           Meta-data points: {algorithm.Metadata.Count}{Environment.NewLine}" +
                            $"          Time-span covered: {timeRange:N0} seconds: {Ticks.FromSeconds(timeRange).ToElapsedTimeString(2)}{Environment.NewLine}" +
                            $"       Processed timestamps: {processedDataBlocks:N0}{Environment.NewLine}" +
                            $"            Expected points: {expectedPoints:N0} @ {m_settings.FrameRate:N0} samples per second{Environment.NewLine}" +
                            $"            Received points: {receivedPoints:N0}{Environment.NewLine}" +
                            $"           Duplicate points: {duplicatePoints:N0}{Environment.NewLine}" +
                            $"          Data completeness: {dataCompleteness:N3}%{Environment.NewLine}" +
                            $"  First timestamp with data: {firstTimestamp:yyyy-MM-dd HH:mm:ss.fff}{Environment.NewLine}" +
                            $"   Last timestamp with data: {lastTimestamp:yyyy-MM-dd HH:mm:ss.fff}{Environment.NewLine}";

                        ShowUpdateMessage(overallSummary);
                    }
                }
            }
            catch (Exception ex)
            {
                ShowUpdateMessage($"!!! Failure during historian read: {ex.Message}");
                m_log.Publish(MessageLevel.Error, "HistorianDataRead", "Failed while reading data from the historian", exception: ex);
            }
            finally
            {
                SetGoButtonEnabledState(true);
            }
        }