//******************************************************************************************************
// 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
}
}