//****************************************************************************************************** // AdapterBase.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 09/02/2010 - J. Ritchie Carroll // Generated original version of source code. // 12/20/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using GSF.Annotations; using GSF.Configuration; using GSF.Data; using GSF.Diagnostics; using GSF.Units; // ReSharper disable TryCastAlwaysSucceeds namespace GSF.TimeSeries.Adapters { /// /// Represents the base class for any adapter. /// [SuppressMessage("SonarQube.Methods", "S1206", Justification = "Class life-cycle is controlled through library.")] public abstract class AdapterBase : IAdapter { #region [ Members ] // Constants /// /// Default measurement reporting interval. /// public const int DefaultMeasurementReportingInterval = 100000; /// /// Default initialization timeout. /// public const int DefaultInitializationTimeout = 15000; // Events /// /// Provides status messages to consumer. /// /// /// is new status message. /// public event EventHandler> StatusMessage; /// /// Event is raised when there is an exception encountered while processing. /// /// /// is the exception that was thrown. /// public event EventHandler> ProcessException; /// /// Event is raised when are updated. /// public event EventHandler InputMeasurementKeysUpdated; /// /// Event is raised when are updated. /// public event EventHandler OutputMeasurementsUpdated; /// /// Event is raised when adapter is aware of a configuration change. /// public event EventHandler ConfigurationChanged; /// /// Event is raised when is disposed. /// public event EventHandler Disposed; // Fields private string m_name; private uint m_id; private string m_connectionString; private Dictionary m_settings; private DataSet m_dataSource; private int m_initializationTimeout; private bool m_autoStart; private MeasurementKey[] m_inputMeasurementKeys; private IMeasurement[] m_outputMeasurements; private long m_processedMeasurements; private int m_measurementReportingInterval; private bool m_enabled; private long m_startTime; private long m_stopTime; private DateTime m_startTimeConstraint; private DateTime m_stopTimeConstraint; private int m_processingInterval; private int m_hashCode; private bool m_initialized; private bool m_disposed; #endregion #region [ Constructors ] /// /// Constructs a new instance of the . /// protected AdapterBase() { Log = Logger.CreatePublisher(GetType(), MessageClass.Framework); m_name = this.GetType().Name; Log.InitialStackMessages = Log.InitialStackMessages.Union("AdapterName", m_name); m_settings = new Dictionary(); m_startTimeConstraint = DateTime.MinValue; m_stopTimeConstraint = DateTime.MaxValue; m_processingInterval = -1; GenHashCode(); // Set incoming measurements to none by default m_inputMeasurementKeys = new MeasurementKey[0]; m_initializationTimeout = DefaultInitializationTimeout; } /// /// Releases the unmanaged resources before the object is reclaimed by . /// ~AdapterBase() { Dispose(false); } #endregion #region [ Properties ] /// /// Log messages generated by an adapter. /// protected LogPublisher Log { get; } /// /// Gets or sets the name of this . /// /// /// Derived classes should provide a name for the adapter. /// public virtual string Name { get { return m_name; } set { m_name = value; Log.InitialStackMessages = Log.InitialStackMessages.Union("AdapterName", m_name); GenHashCode(); } } /// /// Gets or sets numeric ID associated with this . /// public virtual uint ID { get { return m_id; } set { m_id = value; GenHashCode(); } } /// /// Gets or sets key/value pair connection information specific to this . /// /// /// /// Example connection string using manually defined measurements:
/// inputMeasurementKeys={P1:1245;P1:1247;P2:1335};
/// outputMeasurements={P3:1345,60.0,1.0;P3:1346;P3:1347}
/// When defined manually, elements in key:
/// * inputMeasurementKeys are defined as "ArchiveSource:PointID"
/// * outputMeasurements are defined as "ArchiveSource:PointID,Adder,Multiplier", the adder and multiplier are optional /// defaulting to 0.0 and 1.0 respectively. ///
///
/// /// Example connection string using measurements defined in a table:
/// inputMeasurementKeys={FILTER ActiveMeasurements WHERE Company='GSF' AND SignalType='FREQ' ORDER BY ID};
/// outputMeasurements={FILTER ActiveMeasurements WHERE SignalType IN ('IPHA','VPHA') AND Phase='+'}
///
/// Basic filtering syntax is as follows:
///
/// {FILTER <TableName> WHERE <Expression> [ORDER BY <SortField>]}
///
/// Source tables are expected to have at least the following fields:
/// /// /// Name /// Type /// Description. /// /// /// ID /// NVARCHAR /// Measurement key formatted as: ArchiveSource:PointID. /// /// /// PointTag /// NVARCHAR /// Point tag of measurement. /// /// /// Adder /// FLOAT /// Adder to apply to value, if any (default to 0.0). /// /// /// Multiplier /// FLOAT /// Multiplier to apply to value, if any (default to 1.0). /// /// ///
///
public virtual string ConnectionString { get { return m_connectionString; } set { m_connectionString = value; // Pre-parse settings upon connection string assignment if (string.IsNullOrWhiteSpace(m_connectionString)) m_settings = new Dictionary(); else m_settings = m_connectionString.ParseKeyValuePairs(); } } /// /// Gets connection info for adapter, if any. /// /// /// For example, this could return IP or host name of source connection. /// public virtual string ConnectionInfo => null; /// /// Gets or sets based data source available to this . /// public virtual DataSet DataSource { get { return m_dataSource; } set { m_dataSource = value; } } /// /// Gets or sets maximum time system will wait during for initialization. /// /// /// Set to to wait indefinitely. /// public virtual int InitializationTimeout { get { return m_initializationTimeout; } set { m_initializationTimeout = value; } } /// /// Gets or sets flag indicating if adapter should automatically start or otherwise connect on demand. /// public virtual bool AutoStart { get { return m_autoStart; } set { m_autoStart = value; } } /// /// Gets or sets primary keys of input measurements the expects, if any. /// /// /// If your adapter needs to receive all measurements, you must explicitly set InputMeasurementKeys to null. /// [ConnectionStringParameter, DefaultValue(null), Description("Defines primary keys of input measurements the adapter expects; can be one of a filter expression, measurement key, point tag or Guid."), CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")] public virtual MeasurementKey[] InputMeasurementKeys { get { return m_inputMeasurementKeys; } set { if (m_inputMeasurementKeys == value) return; if ((object)m_inputMeasurementKeys != null && (object)value != null) { if (new HashSet(m_inputMeasurementKeys).SetEquals(value)) return; } m_inputMeasurementKeys = value; OnInputMeasurementKeysUpdated(); } } /// /// Gets or sets output measurements that the will produce, if any. /// [ConnectionStringParameter, DefaultValue(null), Description("Defines primary keys of output measurements the adapter expects; can be one of a filter expression, measurement key, point tag or Guid."), CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")] public virtual IMeasurement[] OutputMeasurements { get { return m_outputMeasurements; } set { m_outputMeasurements = value; OnOutputMeasurementsUpdated(); } } /// /// Gets the total number of measurements handled thus far by the . /// public virtual long ProcessedMeasurements => m_processedMeasurements; /// /// Gets or sets the measurement reporting interval. /// /// /// This is used to determined how many measurements should be processed before reporting status. /// public virtual int MeasurementReportingInterval { get { return m_measurementReportingInterval; } set { m_measurementReportingInterval = value; } } /// /// Gets or sets flag indicating if the adapter has been initialized successfully. /// public virtual bool Initialized { get { return m_initialized; } set { m_initialized = value; } } /// /// Gets or sets enabled state of this . /// /// /// Base class simply starts or stops based on flag value. Derived classes should /// extend this if needed to enable or disable operational state of the adapter based on flag value. /// public bool Enabled { get { return m_enabled; } set { if (m_enabled && !value) Stop(); else if (!m_enabled && value) Start(); } } /// /// Gets the UTC time this was started. /// public Ticks StartTime => m_startTime; /// /// Gets the UTC time this was stopped. /// public Ticks StopTime => m_stopTime; /// /// Gets the flag indicating if this adapter supports temporal processing. /// public abstract bool SupportsTemporalProcessing { get; } /// /// Gets the start time temporal processing constraint defined by call to . /// /// /// This value will be when start time constraint is not set - meaning the adapter /// is processing data in real-time. /// public virtual DateTime StartTimeConstraint => m_startTimeConstraint; /// /// Gets the stop time temporal processing constraint defined by call to . /// /// /// This value will be when stop time constraint is not set - meaning the adapter /// is processing data in real-time. /// public virtual DateTime StopTimeConstraint => m_stopTimeConstraint; /// /// Gets or sets the desired processing interval, in milliseconds, for the adapter. /// /// /// With the exception of the values of -1 and 0, this value specifies the desired processing interval for data, i.e., /// basically a delay, or timer interval, over which to process data. A value of -1 means to use the default processing /// interval while a value of 0 means to process data as fast as possible. /// public virtual int ProcessingInterval { get { return m_processingInterval; } set { m_processingInterval = value; if (m_processingInterval < -1) m_processingInterval = -1; } } /// /// Gets the total amount of time, in seconds, that the adapter has been active. /// public virtual Time RunTime { get { Ticks processingTime = 0; if (m_startTime > 0) { if (m_stopTime > 0) processingTime = m_stopTime - m_startTime; else processingTime = DateTime.UtcNow.Ticks - m_startTime; } if (processingTime < 0) processingTime = 0; return processingTime.ToSeconds(); } } /// /// Gets settings parsed when was assigned. /// public Dictionary Settings => m_settings; /// /// Gets the status of this . /// /// /// Derived classes should provide current status information about the adapter for display purposes. /// public virtual string Status { get { const int MaxMeasurementsToShow = 10; StringBuilder status = new StringBuilder(); DataSet dataSource = this.DataSource; status.AppendFormat(" Data source defined: {0}", dataSource != null); status.AppendLine(); if ((object)dataSource != null) { status.AppendFormat(" Referenced data source: {0}, {1:N0} tables", dataSource.DataSetName, dataSource.Tables.Count); status.AppendLine(); } status.AppendFormat(" Initialization timeout: {0}", InitializationTimeout < 0 ? "Infinite" : InitializationTimeout.ToString("N0") + " milliseconds"); status.AppendLine(); status.AppendFormat(" Adapter initialized: {0}", Initialized); status.AppendLine(); status.AppendFormat(" Operational state: {0}", Enabled ? "Running" : "Stopped"); status.AppendLine(); status.AppendFormat(" Connect on demand: {0}", !AutoStart); status.AppendLine(); status.AppendFormat(" Processed measurements: {0:N0}", ProcessedMeasurements); status.AppendLine(); status.AppendFormat(" Total adapter run time: {0}", RunTime.ToString(3)); status.AppendLine(); status.AppendFormat(" Temporal processing: {0}", SupportsTemporalProcessing ? "Supported" : "Unsupported"); status.AppendLine(); if (SupportsTemporalProcessing) { status.AppendFormat(" Start time constraint: {0}", StartTimeConstraint == DateTime.MinValue ? "Unspecified" : StartTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fff")); status.AppendLine(); status.AppendFormat(" Stop time constraint: {0}", StopTimeConstraint == DateTime.MaxValue ? "Unspecified" : StopTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fff")); status.AppendLine(); status.AppendFormat(" Processing interval: {0}", ProcessingInterval < 0 ? "Default" : (ProcessingInterval == 0 ? "As fast as possible" : ProcessingInterval.ToString("N0") + " milliseconds")); status.AppendLine(); } status.AppendFormat(" Item reporting interval: Every {0:N0} items", MeasurementReportingInterval); status.AppendLine(); status.AppendFormat(" Adapter ID: {0}", ID); status.AppendLine(); Dictionary keyValuePairs = Settings; char[] keyChars; string value; status.AppendFormat(" Connection string: {0:N0} key/value pairs", keyValuePairs.Count); // 1 2 3 4 5 6 7 // 123456789012345678901234567890123456789012345678901234567890123456789012345678 // Key = Value // 1 2 3 4 5 // 12345678901234567890123456789012345678901234567890 status.AppendLine(); status.AppendLine(); foreach (KeyValuePair item in keyValuePairs) { keyChars = item.Key.Trim().ToCharArray(); keyChars[0] = char.ToUpper(keyChars[0]); value = item.Value.Trim(); if (value.Length > 50) value = value.TruncateRight(47) + "..."; status.AppendFormat("{0} = {1}", new string(keyChars).TruncateRight(25).PadLeft(25), value.PadRight(50)); status.AppendLine(); } status.AppendLine(); if (OutputMeasurements != null && OutputMeasurements.Length > OutputMeasurements.Count(m => m.Key == MeasurementKey.Undefined)) { status.AppendFormat(" Output measurements: {0:N0} defined measurements", OutputMeasurements.Length); status.AppendLine(); status.AppendLine(); for (int i = 0; i < Math.Min(OutputMeasurements.Length, MaxMeasurementsToShow); i++) { status.Append(OutputMeasurements[i].ToString().TruncateRight(40).PadLeft(40)); status.Append(" "); status.AppendLine(OutputMeasurements[i].ID.ToString()); } if (OutputMeasurements.Length > MaxMeasurementsToShow) status.AppendLine("...".PadLeft(26)); status.AppendLine(); } if (InputMeasurementKeys != null && InputMeasurementKeys.Length > InputMeasurementKeys.Count(k => k == MeasurementKey.Undefined)) { status.AppendFormat(" Input measurements: {0:N0} defined measurements", InputMeasurementKeys.Length); status.AppendLine(); status.AppendLine(); for (int i = 0; i < Math.Min(InputMeasurementKeys.Length, MaxMeasurementsToShow); i++) { status.AppendLine(InputMeasurementKeys[i].ToString().TruncateRight(25).CenterText(50)); } if (InputMeasurementKeys.Length > MaxMeasurementsToShow) status.AppendLine("...".CenterText(50)); status.AppendLine(); } return status.ToString(); } } /// /// Gets a flag that indicates whether the object has been disposed. /// public bool IsDisposed => m_disposed; #endregion #region [ Methods ] /// /// Releases all the resources used by the object. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (!m_disposed) { m_disposed = true; // Prevent duplicate dispose. Disposed?.Invoke(this, EventArgs.Empty); } } /// /// Initializes . /// public virtual void Initialize() { Initialized = false; Dictionary settings = Settings; string setting; if (settings.TryGetValue("inputMeasurementKeys", out setting)) InputMeasurementKeys = ParseInputMeasurementKeys(DataSource, true, setting); else InputMeasurementKeys = new MeasurementKey[0]; if (settings.TryGetValue("outputMeasurements", out setting)) OutputMeasurements = ParseOutputMeasurements(DataSource, true, setting); if (settings.TryGetValue("measurementReportingInterval", out setting)) MeasurementReportingInterval = int.Parse(setting); else MeasurementReportingInterval = DefaultMeasurementReportingInterval; if (settings.TryGetValue("connectOnDemand", out setting)) AutoStart = !setting.ParseBoolean(); else AutoStart = true; string startTime, stopTime, parameters; bool startTimeDefined = settings.TryGetValue("startTimeConstraint", out startTime); bool stopTimeDefined = settings.TryGetValue("stopTimeConstraint", out stopTime); if (startTimeDefined || stopTimeDefined) { settings.TryGetValue("timeConstraintParameters", out parameters); SetTemporalConstraint(startTime, stopTime, parameters); } int processingInterval; if (settings.TryGetValue("processingInterval", out setting) && !string.IsNullOrWhiteSpace(setting) && int.TryParse(setting, out processingInterval)) ProcessingInterval = processingInterval; } /// /// Starts the or restarts it if it is already running. /// [AdapterCommand("Starts the adapter or restarts it if it is already running.", "Administrator", "Editor")] public virtual void Start() { if (m_disposed) throw new ObjectDisposedException(Name, "Cannot start adapter because it is disposed."); // Make sure we are stopped (e.g., disconnected) before attempting to start (e.g., connect) if (m_enabled) Stop(); m_enabled = true; m_stopTime = 0; m_startTime = DateTime.UtcNow.Ticks; } /// /// Stops the . /// [AdapterCommand("Stops the adapter.", "Administrator", "Editor")] public virtual void Stop() { if (m_disposed) throw new ObjectDisposedException(Name, "Cannot stop adapter because it is disposed."); m_enabled = false; m_stopTime = DateTime.UtcNow.Ticks; } /// /// Manually sets the initialized state of the . /// /// Desired initialized state. [AdapterCommand("Manually sets the initialized state of the adapter.", "Administrator", "Editor")] public virtual void SetInitializedState(bool initialized) => this.Initialized = initialized; /// /// Gets a short one-line status of this . /// /// Maximum number of available characters for display. /// A short one-line summary of the current status of this . public abstract string GetShortStatus(int maxLength); /// /// Defines a temporal processing constraint for the adapter. /// /// Defines a relative or exact start time for the temporal constraint. /// Defines a relative or exact stop time for the temporal constraint. /// Defines any temporal parameters related to the constraint. /// /// /// This method defines a temporal processing constraint for an adapter, i.e., the start and stop time over which an /// adapter will process data. Actual implementation of the constraint will be adapter specific. Implementations /// should be able to dynamically handle multiple calls to this function with new constraints. Passing in null /// for the and should cancel the temporal constraint and /// return the adapter to standard / real-time operation. /// /// /// The and parameters can be specified in one of the /// following formats: /// /// /// Time Format /// Format Description /// /// /// 12-30-2000 23:59:59.033 /// Absolute date and time. /// /// /// * /// Evaluates to . /// /// /// *-20s /// Evaluates to 20 seconds before . /// /// /// *-10m /// Evaluates to 10 minutes before . /// /// /// *-1h /// Evaluates to 1 hour before . /// /// /// *-1d /// Evaluates to 1 day before . /// /// /// /// [AdapterCommand("Defines a temporal processing constraint for the adapter.", "Administrator", "Editor", "Viewer")] public virtual void SetTemporalConstraint(string startTime, string stopTime, string constraintParameters) { m_startTimeConstraint = string.IsNullOrWhiteSpace(startTime) ? DateTime.MinValue : ParseTimeTag(startTime); m_stopTimeConstraint = string.IsNullOrWhiteSpace(stopTime) ? DateTime.MaxValue : ParseTimeTag(stopTime); } /// /// Serves as a hash function for the current . /// /// A hash code for the current . public override int GetHashCode() { // ReSharper disable once NonReadonlyMemberInGetHashCode return m_hashCode; } /// /// Raises the event. /// /// New status message. [Obsolete("Switch to using overload with MessageLevel parameter - this method may be removed from future builds.", false)] protected void OnStatusMessage(string status) { OnStatusMessage(MessageLevel.Info, status, "Unclassified Status"); } /// /// Raises the event with a formatted status message. /// /// Formatted status message. /// Arguments for . /// /// This overload combines string.Format and SendStatusMessage for convenience. /// [StringFormatMethod("formattedStatus"), Obsolete("Switch to using overload with MessageLevel parameter - this method may be removed from future builds.", false)] protected void OnStatusMessage(string formattedStatus, params object[] args) { OnStatusMessage(MessageLevel.Info, string.Format(formattedStatus, args), "Unclassified Status"); } /// /// Raises the event and sends this data to the . /// /// The to assign to this message /// New status message. /// A fixed string to classify this event; defaults to null. /// to use, if any; defaults to . /// /// should be a constant string value associated with what type of message is being /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred. /// protected virtual void OnStatusMessage(MessageLevel level, string status, string eventName = null, MessageFlags flags = MessageFlags.None) { try { Log.Publish(level, flags, eventName, status); using (Logger.SuppressLogMessages()) StatusMessage?.Invoke(this, new EventArgs(GetStatusWithMessageLevelPrefix(status, level))); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for StatusMessage event: {ex.Message}", ex), "ConsumerEventException"); } } /// /// Raises the event. /// /// Processing . [Obsolete("Switch to using overload with MessageLevel parameter - this method may be removed from future builds.", false)] protected void OnProcessException(Exception ex) { OnProcessException(MessageLevel.Info, ex, "Unclassified Exception"); } /// /// Raises the event. /// /// The to assign to this message /// Processing . /// A fixed string to classify this event; defaults to null. /// to use, if any; defaults to . /// /// should be a constant string value associated with what type of message is being /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred. /// protected virtual void OnProcessException(MessageLevel level, Exception exception, string eventName = null, MessageFlags flags = MessageFlags.None) { try { Log.Publish(level, flags, eventName, exception?.Message, null, exception); using (Logger.SuppressLogMessages()) ProcessException?.Invoke(this, new EventArgs(exception)); } catch (Exception ex) { // We protect our code from consumer thrown exceptions Log.Publish(MessageLevel.Info, "ConsumerEventException", $"Exception in consumer handler for ProcessException event: {ex.Message}", null, ex); } } /// /// Raises event. /// protected virtual void OnInputMeasurementKeysUpdated() { try { InputMeasurementKeysUpdated?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for InputMeasurementKeysUpdated event: {ex.Message}", ex), "ConsumerEventException"); } } /// /// Raises event. /// protected virtual void OnOutputMeasurementsUpdated() { try { OutputMeasurementsUpdated?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for OutputMeasurementsUpdated event: {ex.Message}", ex), "ConsumerEventException"); } } /// /// Raises event. /// protected virtual void OnConfigurationChanged() { try { ConfigurationChanged?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ConfigurationChanged event: {ex.Message}", ex), "ConsumerEventException"); } } /// /// Safely increments the total processed measurements. /// /// [MethodImpl(MethodImplOptions.Synchronized)] protected virtual void IncrementProcessedMeasurements(long totalAdded) { // Check to see if total number of added points will exceed process interval used to show periodic // messages of how many points have been archived so far... int interval = MeasurementReportingInterval; if (interval > 0) { bool showMessage = m_processedMeasurements + totalAdded >= (m_processedMeasurements / interval + 1) * interval; m_processedMeasurements += totalAdded; if (showMessage) OnStatusMessage(MessageLevel.Info, $"{m_processedMeasurements:N0} measurements have been processed so far...", "Processed Measurement Count"); } } private void GenHashCode() { // We cache hash code during construction or after element value change to speed usage m_hashCode = (Name + ID).GetHashCode(); } #endregion #region [ Static ] // Static Fields private static readonly Regex s_filterExpression = new Regex("(FILTER[ ]+(TOP[ ]+(?\\d+)[ ]+)?(?\\w+)[ ]+WHERE[ ]+(?.+)[ ]+ORDER[ ]+BY[ ]+(?\\w+))|(FILTER[ ]+(TOP[ ]+(?\\d+)[ ]+)?(?\\w+)[ ]+WHERE[ ]+(?.+))", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex s_timetagExpression = new Regex("\\*(?[+-]?\\d*\\.?\\d*)(?\\w+)", RegexOptions.Compiled); // Static Methods /// /// Gets status message with common time-series messaging prefix for given . /// /// Status message. /// Message level. /// Status message with common time-series messaging prefix for given . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetStatusWithMessageLevelPrefix(string status, MessageLevel level) { switch (level) { case MessageLevel.Debug: return $"DEBUG: {status}"; case MessageLevel.Warning: return $"WARNING: {status}"; case MessageLevel.Error: return $"ERROR: {status}"; case MessageLevel.Critical: return $"CRITICAL: {status}"; default: return status; } } /// /// Parses a standard FILTER styles expression into its constituent parts. /// /// Filter expression to parse. /// Name of table in filter expression. /// Where expression in filter expression. /// Sort field, if any, in filter expression. /// Total row restriction, if any, in filter expression. /// true if filter expression was successfully parsed; otherwise false. /// /// This method can be safely called from multiple threads. /// public static bool ParseFilterExpression(string filterExpression, out string tableName, out string whereExpression, out string sortField, out int takeCount) { tableName = null; whereExpression = null; sortField = null; takeCount = 0; if (string.IsNullOrWhiteSpace(filterExpression)) return false; Match filterMatch; lock (s_filterExpression) { filterMatch = s_filterExpression.Match(filterExpression.ReplaceControlCharacters()); } if (!filterMatch.Success) return false; tableName = filterMatch.Result("${TableName}").Trim(); whereExpression = filterMatch.Result("${Expression}").Trim(); sortField = filterMatch.Result("${SortField}").Trim(); string maxRows = filterMatch.Result("${MaxRows}").Trim(); if (string.IsNullOrEmpty(maxRows) || !int.TryParse(maxRows, out takeCount)) takeCount = int.MaxValue; return true; } /// /// Parses a string formatted as an absolute or relative time tag. /// /// String formatted as an absolute or relative time tag. /// /// representing the parsed string formatted as an absolute or relative time tag. /// /// /// /// Relative times are parsed based on an offset to current time (UTC) specified by "*". /// /// /// The parameter can be specified in one of the following formats: /// /// /// Time Format /// Format Description /// /// /// 12-30-2000 23:59:59.033 /// Absolute date and time. /// /// /// * /// Evaluates to . /// /// /// *-20s /// Evaluates to 20 seconds before . /// /// /// *-10m /// Evaluates to 10 minutes before . /// /// /// *-1h /// Evaluates to 1 hour before . /// /// /// *-1d /// Evaluates to 1 day before . /// /// /// *+2d /// Evaluates to 2 days from . /// /// /// /// /// parameter cannot be null or empty. /// does not contain a valid string representation of a date and time. public static DateTime ParseTimeTag(string timetag) { if (string.IsNullOrWhiteSpace(timetag)) throw new ArgumentNullException(nameof(timetag), "Timetag string cannot be null or empty."); DateTime dateTime; if (timetag.Contains("*")) { // Relative time is specified. // Examples: // 1) * (Now) // 2) *-20s (20 seconds ago) // 3) *-10m (10 minutes ago) // 4) *-1h (1 hour ago) // 5) *-1d (1 day ago) // 6) *+2d (2 days from now) dateTime = DateTime.UtcNow; timetag = timetag.RemoveWhiteSpace(); if (timetag.Length > 1) { Match timetagMatch; lock (s_timetagExpression) { timetagMatch = s_timetagExpression.Match(timetag); } if (timetagMatch.Success) { double offset = double.Parse(timetagMatch.Result("${Offset}").Trim()); string unit = timetagMatch.Result("${Unit}").Trim().ToLower(); switch (unit[0]) { case 's': dateTime = dateTime.AddSeconds(offset); break; case 'm': dateTime = dateTime.AddMinutes(offset); break; case 'h': dateTime = dateTime.AddHours(offset); break; case 'd': dateTime = dateTime.AddDays(offset); break; } } else { // Expression match failed, attempt to parse absolute time specification. dateTime = DateTime.Parse(timetag, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); } } } else { // Absolute time is specified. dateTime = DateTime.Parse(timetag, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); } return dateTime; } /// /// Loads an or instance's input measurement keys from a specific set of source ID's. /// /// to load input measurement keys for. /// Measurement table name used to load input source ID's. /// /// Any existing input measurement keys will be distinctly merged with those associated with specified source ID's. /// public static void LoadInputSourceIDs(IAdapter adapter, string measurementTable = "ActiveMeasurements") { string[] sourceIDs = null; if (adapter is IOutputAdapter) sourceIDs = (adapter as IOutputAdapter).InputSourceIDs; else if (adapter is IActionAdapter) sourceIDs = (adapter as IActionAdapter).InputSourceIDs; if (sourceIDs != null) { // Attempt to lookup input measurement keys for given source IDs from default measurement table, if defined try { if (adapter.DataSource.Tables.Contains(measurementTable)) { StringBuilder likeExpression = new StringBuilder(); // Build like expression for each source ID foreach (string sourceID in sourceIDs) { if (likeExpression.Length > 0) likeExpression.Append(" OR "); likeExpression.AppendFormat("ID LIKE '{0}:*'", sourceID); } DataRow[] filteredRows = adapter.DataSource.Tables[measurementTable].Select(likeExpression.ToString()); MeasurementKey[] sourceIDKeys = null; if (filteredRows.Length > 0) sourceIDKeys = filteredRows.Select(row => MeasurementKey.LookUpOrCreate(row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), row["ID"].ToNonNullString(MeasurementKey.Undefined.ToString()))).ToArray(); if (sourceIDKeys != null) { // Combine input measurement keys for source IDs with any existing input measurement keys and return unique set if ((object)adapter.InputMeasurementKeys == null) adapter.InputMeasurementKeys = sourceIDKeys; else adapter.InputMeasurementKeys = sourceIDKeys.Concat(adapter.InputMeasurementKeys).Distinct().ToArray(); } } } catch { // Errors here are not catastrophic, this simply limits the auto-assignment of input measurement keys based on specified source ID's } } } /// /// Loads an or instance's output measurements from a specific set of source ID's. /// /// to load output measurements for. /// Measurement table name used to load output source ID's. /// /// Any existing output measurements will be distinctly merged with those associated with specified source ID's. /// public static void LoadOutputSourceIDs(IAdapter adapter, string measurementTable = "ActiveMeasurements") { string[] sourceIDs = null; if (adapter is IInputAdapter) sourceIDs = (adapter as IInputAdapter).OutputSourceIDs; else if (adapter is IActionAdapter) sourceIDs = (adapter as IActionAdapter).OutputSourceIDs; if (sourceIDs != null) { // Attempt to lookup output measurements for given source IDs from default measurement table, if defined try { if (adapter.DataSource.Tables.Contains(measurementTable)) { StringBuilder likeExpression = new StringBuilder(); // Build like expression for each source ID foreach (string sourceID in sourceIDs) { if (likeExpression.Length > 0) likeExpression.Append(" OR "); likeExpression.AppendFormat("ID LIKE '{0}:*'", sourceID); } DataRow[] filteredRows = adapter.DataSource.Tables[measurementTable].Select(likeExpression.ToString()); MeasurementKey[] sourceIDKeys = null; if (filteredRows.Length > 0) sourceIDKeys = filteredRows.Select(row => MeasurementKey.LookUpOrCreate(row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), row["ID"].ToNonNullString(MeasurementKey.Undefined.ToString()))).ToArray(); if (sourceIDKeys != null) { List measurements = new List(); foreach (MeasurementKey key in sourceIDKeys) { // Create a new measurement for the provided field level information Measurement measurement = new Measurement(); measurement.Metadata = key.Metadata; measurements.Add(measurement); } // Combine output measurements for source IDs with any existing output measurements and return unique set if ((object)adapter.OutputMeasurements == null) adapter.OutputMeasurements = measurements.ToArray(); else adapter.OutputMeasurements = measurements.Concat(adapter.OutputMeasurements).Distinct().ToArray(); } } } catch { // Errors here are not catastrophic, this simply limits the auto-assignment of input measurement keys based on specified source ID's } } } // Input keys can use DataSource for filtering desired set of input or output measurements // based on any table and fields in the data set by using a filter expression instead of // a list of measurement ID's. The format is as follows: // FILTER WHERE [ORDER BY ] // Source tables are expected to have at least the following fields: // // ID NVARCHAR Measurement key formatted as: ArchiveSource:PointID // SignalID GUID Unique identification for measurement // PointTag NVARCHAR Point tag of measurement // Adder FLOAT Adder to apply to value, if any (default to 0.0) // Multiplier FLOAT Multiplier to apply to value, if any (default to 1.0) // // Could have used standard SQL syntax here but didn't want to give user the impression // that this a standard SQL expression when it isn't - so chose the word FILTER to make // consumer was aware that this was not SQL, but SQL "like". The WHERE clause expression // uses standard SQL syntax (it is simply the DataTable.Select filter expression). /// /// Parses input measurement keys from connection string setting. /// /// The used to define input measurement keys. /// Determines if database access via "SELECT" statement should be allowed for defining input measurement keys (see remarks about security). /// Value of setting used to define input measurement keys, typically "inputMeasurementKeys". /// Measurement table name used to load additional meta-data; this is not used when specifying a FILTER expression. /// User selected input measurement keys. /// /// Security warning: allowing SELECT statements, i.e., setting to true, should only be allowed in cases where SQL /// injection would not be an issue (e.g., in places where a user can already access the database and would have nothing to gain via an injection attack). /// public static MeasurementKey[] ParseInputMeasurementKeys(DataSet dataSource, bool allowSelect, string value, string measurementTable = "ActiveMeasurements") { List keys = new List(); MeasurementKey key; Guid id; string tableName, expression, sortField; int takeCount; bool dataSourceAvailable = (object)dataSource != null; if (string.IsNullOrWhiteSpace(value)) return keys.ToArray(); value = value.Trim(); if (dataSourceAvailable && ParseFilterExpression(value, out tableName, out expression, out sortField, out takeCount)) { foreach (DataRow row in dataSource.Tables[tableName].Select(expression, sortField).Take(takeCount)) { key = MeasurementKey.LookUpOrCreate(row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), row["ID"].ToString()); if (key != MeasurementKey.Undefined) keys.Add(key); } } else if (allowSelect && value.StartsWith("SELECT ", StringComparison.OrdinalIgnoreCase)) { try { // Load settings from the system settings category ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings["systemSettings"]; settings.Add("AllowSelectFilterExpresssions", false, "Determines if database backed SELECT statements should be allowed as filter expressions for defining input and output measurements for adapters."); // Global configuration setting can override ability to use SELECT statements if (settings["AllowSelectFilterExpresssions"].ValueAsBoolean()) { using (AdoDataConnection database = new AdoDataConnection("systemSettings")) { DataTable results = database.Connection.RetrieveData(database.AdapterType, value); foreach (DataRow row in results.Rows) { key = MeasurementKey.LookUpOrCreate(row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), row["ID"].ToString()); if (key != MeasurementKey.Undefined) keys.Add(key); } } } else { throw new InvalidOperationException("Database backed SELECT statements are disabled in the configuration."); } } catch (Exception ex) { throw new InvalidOperationException($"Could not parse input measurement definition from select statement \"{value}\": {ex.Message}", ex); } } else { // Add manually defined measurement keys foreach (string item in value.Split(';')) { if (string.IsNullOrWhiteSpace(item)) continue; if (Guid.TryParse(item, out id)) { // The item was parsed as a signal ID so do a straight lookup key = MeasurementKey.LookUpBySignalID(id); if (key == MeasurementKey.Undefined && dataSourceAvailable && dataSource.Tables.Contains(measurementTable)) { DataRow[] filteredRows = dataSource.Tables[measurementTable].Select($"SignalID = '{id}'"); if (filteredRows.Length > 0) MeasurementKey.TryCreateOrUpdate(id, filteredRows[0]["ID"].ToString(), out key); } } else if (!MeasurementKey.TryParse(item, out key)) { if (dataSourceAvailable && dataSource.Tables.Contains(measurementTable)) { DataRow[] filteredRows; // The item could not be parsed as a signal ID, but we do have a data source we can use to find the signal ID filteredRows = dataSource.Tables[measurementTable].Select($"ID = '{item.Trim()}'"); if (filteredRows.Length == 0) filteredRows = dataSource.Tables[measurementTable].Select($"PointTag = '{item.Trim()}'"); if (filteredRows.Length > 0) key = MeasurementKey.LookUpOrCreate(filteredRows[0]["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), filteredRows[0]["ID"].ToString()); } // If all else fails, attempt to parse the item as a measurement key if (key == MeasurementKey.Undefined) { if (id == Guid.Empty) throw new InvalidOperationException($"Could not parse input measurement definition \"{item}\" as a filter expression, measurement key, point tag or Guid"); throw new InvalidOperationException($"Measurement (targeted for input) with an ID of \"{item}\" is not defined or is not enabled"); } } if (key != MeasurementKey.Undefined) keys.Add(key); } } return keys.ToArray(); } /// /// Parses output measurement keys from connection string setting. /// /// The used to define output measurements. /// Determines if database access via "SELECT" statement should be allowed for defining output measurement keys (see remarks about security). /// Value of setting used to define output measurements, typically "outputMeasurements". /// Measurement table name used to load additional meta-data; this is not used when specifying a FILTER expression. /// User selected output measurements. /// /// Security warning: allowing SELECT statements, i.e., setting to true, should only be allowed in cases where SQL /// injection would not be an issue (e.g., in places where a user can already access the database and would have nothing to gain via an injection attack). /// public static MeasurementKey[] ParseOutputMeasurementKeys(DataSet dataSource, bool allowSelect, string value, string measurementTable = "ActiveMeasurements") { return ParseOutputMeasurements(dataSource, allowSelect, value, measurementTable).MeasurementKeys().ToArray(); } /// /// Parses output measurements from connection string setting. /// /// The used to define output measurements. /// Determines if database access via "SELECT" statement should be allowed for defining output measurements (see remarks about security). /// Value of setting used to define output measurements, typically "outputMeasurements". /// Measurement table name used to load additional meta-data; this is not used when specifying a FILTER expression. /// User selected output measurements. /// /// Security warning: allowing SELECT statements, i.e., setting to true, should only be allowed in cases where SQL /// injection would not be an issue (e.g., in places where a user can already access the database and would have nothing to gain via an injection attack). /// public static IMeasurement[] ParseOutputMeasurements(DataSet dataSource, bool allowSelect, string value, string measurementTable = "ActiveMeasurements") { List measurements = new List(); Measurement measurement; MeasurementKey key; Guid id; string tableName, expression, sortField; int takeCount; bool dataSourceAvailable = (object)dataSource != null; if (string.IsNullOrWhiteSpace(value)) return measurements.ToArray(); value = value.Trim(); if (dataSourceAvailable && ParseFilterExpression(value, out tableName, out expression, out sortField, out takeCount)) { foreach (DataRow row in dataSource.Tables[tableName].Select(expression, sortField).Take(takeCount)) { id = row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(); key = MeasurementKey.LookUpOrCreate(id, row["ID"].ToString()); measurement = new Measurement { Metadata = key.Metadata, }; measurements.Add(measurement); } } else if (allowSelect && value.StartsWith("SELECT ", StringComparison.OrdinalIgnoreCase)) { try { // Load settings from the system settings category ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings["systemSettings"]; settings.Add("AllowSelectFilterExpresssions", false, "Determines if database backed SELECT statements should be allowed as filter expressions for defining input and output measurements for adapters."); // Global configuration setting can override ability to use SELECT statements if (settings["AllowSelectFilterExpresssions"].ValueAsBoolean()) { using (AdoDataConnection database = new AdoDataConnection("systemSettings")) { DataTable results = database.Connection.RetrieveData(database.AdapterType, value); foreach (DataRow row in results.Rows) { id = row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(); key = MeasurementKey.LookUpOrCreate(id, row["ID"].ToString()); measurement = new Measurement { Metadata = key.Metadata, }; measurements.Add(measurement); } } } else { throw new InvalidOperationException("Database backed SELECT statements are disabled in the configuration."); } } catch (Exception ex) { throw new InvalidOperationException($"Could not parse output measurement definition from select statement \"{value}\": {ex.Message}", ex); } } else { string[] elem; double adder, multipler; foreach (string item in value.Split(';')) { if (string.IsNullOrWhiteSpace(item)) continue; elem = item.Trim().Split(','); // Trim all tokens ahead of time for (int i = 0; i < elem.Length; i++) elem[i] = elem[i].Trim(); if (Guid.TryParse(elem[0], out id)) { // The item was parsed as a signal ID so do a straight lookup key = MeasurementKey.LookUpBySignalID(id); if (key == MeasurementKey.Undefined && dataSourceAvailable && dataSource.Tables.Contains(measurementTable)) { DataRow[] filteredRows = dataSource.Tables[measurementTable].Select($"SignalID = '{id}'"); if (filteredRows.Length > 0) MeasurementKey.TryCreateOrUpdate(id, filteredRows[0]["ID"].ToString(), out key); } } else if (!MeasurementKey.TryParse(elem[0], out key)) { if (dataSourceAvailable && dataSource.Tables.Contains(measurementTable)) { DataRow[] filteredRows; // The item could not be parsed as a signal ID, but we do have a data source we can use to find the signal ID filteredRows = dataSource.Tables[measurementTable].Select($"ID = '{elem[0]}'"); if (filteredRows.Length == 0) { // Point tags can have commas so we do not support specification of adders and multipliers in this case - // therefore, we must do our lookup based on item (not elem) and clear elem if the lookup is successful filteredRows = dataSource.Tables[measurementTable].Select($"PointTag = '{item.Trim()}'"); if (filteredRows.Length > 0) elem = new string[0]; } if (filteredRows.Length > 0) key = MeasurementKey.LookUpOrCreate(filteredRows[0]["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), filteredRows[0]["ID"].ToString()); } // If all else fails, attempt to parse the item as a measurement key if (key == MeasurementKey.Undefined) { if (id == Guid.Empty) throw new InvalidOperationException($"Could not parse output measurement definition \"{item}\" as a filter expression, measurement key, point tag or Guid"); throw new InvalidOperationException($"Measurement (targeted for output) with an ID of \"{item}\" is not defined or is not enabled"); } } if (key == MeasurementKey.Undefined) continue; // Adder and multiplier may be optionally specified if (elem.Length < 2 || !double.TryParse(elem[1], out adder)) adder = 0.0D; if (elem.Length < 3 || !double.TryParse(elem[2], out multipler)) multipler = 1.0D; // Create a new measurement for the provided field level information measurement = new Measurement { Metadata = key.Metadata.ChangeAdderMultiplier(adder, multipler), }; // Attempt to lookup other associated measurement meta-data from default measurement table, if defined try { if (dataSourceAvailable && dataSource.Tables.Contains(measurementTable)) { DataRow[] filteredRows = dataSource.Tables[measurementTable].Select($"ID = '{key}'"); if (filteredRows.Length > 0) { DataRow row = filteredRows[0]; measurement.SetTagName(row["PointTag"].ToNonNullString()); // Manually specified adder and multiplier take precedence, but if none were specified, // then those defined in the meta-data are used instead if (elem.Length < 3) measurement.SetMultiplier(double.Parse(row["Multiplier"].ToString())); if (elem.Length < 2) measurement.SetAdder(double.Parse(row["Adder"].ToString())); } } } catch { // Errors here are not catastrophic, this simply limits the available meta-data measurement.SetTagName(string.Empty); } measurements.Add(measurement); } } return measurements.ToArray(); } #endregion } }