//******************************************************************************************************
// AdoInputAdapter.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:
// ----------------------------------------------------------------------------------------------------
// 11/18/2010 - Stephen C. Wills
// Generated original version of source code.
// 12/13/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using GSF;
using GSF.Diagnostics;
using GSF.IO;
using GSF.Parsing;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
using GSF.TimeSeries.Transport;
using SerializationFormat = GSF.SerializationFormat;
namespace AdoAdapters
{
///
/// Represents an input adapter that reads measurements from a database table.
///
[Description("ADO: Reads measurements from an ADO data source")]
public class AdoInputAdapter : InputAdapterBase
{
#region [ Members ]
// Fields
private readonly Dictionary m_fieldNames;
private string m_dbTableName;
private string m_dbConnectionString;
private string m_dataProviderString;
private string m_timestampFormat;
private int m_framesPerSecond;
private bool m_simulateTimestamps;
private IList m_dbMeasurements;
private string m_cacheFileName;
private int m_nextIndex;
#endregion
#region [ Constructors ]
///
/// Initializes a new instance of the class.
///
public AdoInputAdapter()
{
m_fieldNames = new Dictionary(StringComparer.CurrentCultureIgnoreCase);
m_dbMeasurements = new List();
}
#endregion
#region [ Properties ]
///
/// Gets or sets the table name in the data source from which measurements are read.
///
[ConnectionStringParameter,
Description("Define the table name in the data source from which measurements are read."),
DefaultValue("PICOMP")]
public string TableName
{
get
{
return m_dbTableName;
}
set
{
m_dbTableName = value;
}
}
///
/// Gets or sets the connection string used to connect to the data source.
///
[ConnectionStringParameter,
Description("Define the connection string used to connect to the data source."),
DefaultValue("")]
public string DbConnectionString
{
get
{
return m_dbConnectionString;
}
set
{
m_dbConnectionString = value;
}
}
///
/// Gets or sets the data provider string used to connect to the data source.
///
[ConnectionStringParameter,
Description("Define the data provider string used to connect to the data source."),
DefaultValue("AssemblyName={System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089}; ConnectionType=System.Data.Odbc.OdbcConnection; AdapterType=System.Data.Odbc.OdbcDataAdapter")]
public string DataProviderString
{
get
{
return m_dataProviderString;
}
set
{
m_dataProviderString = value;
}
}
///
/// Gets or sets the format used to read measurements from the data source.
///
[ConnectionStringParameter,
Description("Define the date and time format used to read timestamps from the data source."),
DefaultValue("dd-MMM-yyyy HH:mm:ss.fff")]
public string TimestampFormat
{
get
{
return m_timestampFormat;
}
set
{
m_timestampFormat = value;
}
}
///
/// Gets or sets the framerate simulated by this adapter.
///
[ConnectionStringParameter,
Description("Define the rate at which frames will be sent to the concentrator."),
DefaultValue(30)]
public int FramesPerSecond
{
get
{
return m_framesPerSecond;
}
set
{
m_framesPerSecond = value;
}
}
///
/// Gets or sets a value that determines whether the timestamps should
/// be simulated for the purposes of real-time concentration.
///
[ConnectionStringParameter,
Description("Indicate whether timestamps should be simulated for real-time concentration."),
DefaultValue(true)]
public bool SimulateTimestamps
{
get
{
return m_simulateTimestamps;
}
set
{
m_simulateTimestamps = value;
}
}
///
/// Gets or sets a cache file name so that when defined, future data loads will be from cache instead of the database as an optimization.
///
[ConnectionStringParameter,
Description("Defines a cache file name so that when defined, future data loads will be from cache instead of the database as an optimization."),
DefaultValue(null),
CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.FileDialogEditor", "type=save; defaultExt=.ado; filter=ADO Cache Files|*.ado|All Files|*.*")]
public string CacheFileName
{
get
{
return m_cacheFileName;
}
set
{
m_cacheFileName = value;
}
}
///
/// Gets a flag that determines if this
/// uses an asynchronous connection.
///
protected override bool UseAsyncConnect
{
get
{
return false;
}
}
///
/// Gets the flag indicating if this adapter supports temporal processing.
///
public override bool SupportsTemporalProcessing
{
get
{
return true;
}
}
#endregion
#region [ Methods ]
///
/// Initializes this .
///
public override void Initialize()
{
base.Initialize();
Dictionary settings = Settings;
string setting;
// Get the database field names.
GetFieldNames(settings);
// Get table name or default to PICOMP.
if (!settings.TryGetValue("tableName", out m_dbTableName))
m_dbTableName = "PICOMP";
// Get database connection string or default to empty.
if (!settings.TryGetValue("dbConnectionString", out m_dbConnectionString))
m_dbConnectionString = string.Empty;
// Get data provider string or default to a generic ODBC connection.
if (!settings.TryGetValue("dataProviderString", out m_dataProviderString))
m_dataProviderString = "AssemblyName={System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089}; ConnectionType=System.Data.Odbc.OdbcConnection; AdapterType=System.Data.Odbc.OdbcDataAdapter";
// Get timestamp format or default to "dd-MMM-yyyy HH:mm:ss.fff".
if (!settings.TryGetValue("timestampFormat", out m_timestampFormat))
m_timestampFormat = "dd-MMM-yyyy HH:mm:ss.fff";
else
{
// Null timestamp format means the timestamp is stored as ticks.
if (m_timestampFormat.Equals("null", StringComparison.CurrentCultureIgnoreCase))
m_timestampFormat = null;
}
// Get framerate or default to 30.
if (settings.TryGetValue("framesPerSecond", out setting))
m_framesPerSecond = int.Parse(setting);
else
m_framesPerSecond = 30;
// Override frames per second based on temporal processing interval if it's not set to default
if (ProcessingInterval > -1)
{
if (ProcessingInterval == 0)
{
m_framesPerSecond = (int)Ticks.PerSecond;
}
else
{
// Minimum processing rate for this class is one frame per second
if (ProcessingInterval >= 1000)
m_framesPerSecond = 1;
else
m_framesPerSecond = (int)Ticks.PerMillisecond / ProcessingInterval;
}
}
// Determine whether to perform timestamp simulation; default is true.
if (settings.TryGetValue("simulateTimestamps", out setting))
m_simulateTimestamps = bool.Parse(setting);
else
m_simulateTimestamps = true;
// Load cache file name, if defined
settings.TryGetValue("cacheFileName", out m_cacheFileName);
if (m_cacheFileName != null)
m_cacheFileName = FilePath.GetAbsolutePath(m_cacheFileName);
try
{
// Get measurements from the database.
ThreadPool.QueueUserWorkItem(GetDbMeasurements);
}
catch (Exception ex)
{
// Process exception for logging
OnProcessException(MessageLevel.Error, new InvalidOperationException("Failed to queue database query due to exception: " + ex.Message, ex));
}
}
///
/// Attempts to connect to this .
///
protected override void AttemptConnection()
{
//m_timer.Start();
}
///
/// Attempts to disconnect from this .
///
protected override void AttemptDisconnection()
{
//m_timer.Stop();
}
///
/// Gets a short one-line status of this .
///
/// Maximum length of the status message.
/// Text of the status message.
public override string GetShortStatus(int maxLength)
{
return $"{ProcessedMeasurements} measurements read from database.".CenterText(maxLength);
}
// Gets the database field names specified by the user in the connection string.
private void GetFieldNames(Dictionary settings)
{
IEnumerable measurementProperties = GetAllProperties(typeof(IMeasurement)).Select(property => property.Name);
StringComparison ignoreCase = StringComparison.CurrentCultureIgnoreCase;
// Parse database field names from the connection string.
foreach (string key in settings.Keys)
{
if (key.EndsWith("FieldName", ignoreCase))
{
int fieldNameIndex = key.LastIndexOf("FieldName", ignoreCase);
string subKey = key.Substring(0, fieldNameIndex);
string propertyName = measurementProperties.FirstOrDefault(name => name.Equals(subKey, ignoreCase));
string fieldName = settings[key];
if (propertyName != null)
m_fieldNames[fieldName] = propertyName;
else
OnProcessException(MessageLevel.Warning, new ArgumentException($"Measurement property not found: {subKey}"));
}
}
// If the user hasn't entered any field names, enter the default field names.
if (m_fieldNames.Count == 0)
{
m_fieldNames.Add("TAG", "TagName");
m_fieldNames.Add("TIME", "Timestamp");
m_fieldNames.Add("VALUE", "Value");
}
}
// Retrieves the measurements from the database.
private void GetDbMeasurements(object state)
{
IDbConnection connection = null;
// Get measurements from the database.
try
{
SignalIndexCache signalIndexCache = new SignalIndexCache();
CompactMeasurement measurement;
long startTime = DateTime.UtcNow.Ticks;
if (m_cacheFileName != null && File.Exists(m_cacheFileName))
{
OnStatusMessage(MessageLevel.Info, "Loading cached input data...");
try
{
using (FileStream data = File.OpenRead(m_cacheFileName))
{
byte[] buffer = new byte[4];
int signalIndexCacheImageSize;
int compactMeasurementSize;
int totalMeasurements;
// Read the signal index cache image size from the file
if (data.Read(buffer, 0, 4) != 4)
throw new EndOfStreamException();
signalIndexCacheImageSize = LittleEndian.ToInt32(buffer, 0);
// Resize buffer to accommodate exact signal index cache
buffer = new byte[signalIndexCacheImageSize];
// Read the signal index cache image from the file
if (data.Read(buffer, 0, signalIndexCacheImageSize) != signalIndexCacheImageSize)
throw new EndOfStreamException();
// Deserialize the signal index cache
signalIndexCache = Serialization.Deserialize(buffer, SerializationFormat.Binary);
// Read the size of each compact measurement from the file
if (data.Read(buffer, 0, 4) != 4)
throw new EndOfStreamException();
compactMeasurementSize = LittleEndian.ToInt32(buffer, 0);
// Read the total number of compact measurements from the file
if (data.Read(buffer, 0, 4) != 4)
throw new EndOfStreamException();
totalMeasurements = LittleEndian.ToInt32(buffer, 0);
// Resize buffer to accommodate compact measurement if needed (not likely)
if (buffer.Length < compactMeasurementSize)
buffer = new byte[compactMeasurementSize];
// Read each compact measurement image from the file
for (int i = 0; i < totalMeasurements; i++)
{
if (data.Read(buffer, 0, compactMeasurementSize) != compactMeasurementSize)
throw new EndOfStreamException();
// Parse compact measurement
measurement = new CompactMeasurement(signalIndexCache);
measurement.ParseBinaryImage(buffer, 0, compactMeasurementSize);
m_dbMeasurements.Add(measurement);
if (m_dbMeasurements.Count % 50000 == 0)
OnStatusMessage(MessageLevel.Info, $"Loaded {m_dbMeasurements.Count:N0} records so far...");
}
OnStatusMessage(MessageLevel.Info, $"Completed data load in {((Ticks)(DateTime.UtcNow.Ticks - startTime)).ToElapsedTimeString(2)}");
}
}
catch (Exception ex)
{
// ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull
if (ex is EndOfStreamException)
throw (EndOfStreamException)ex;
throw new EndOfStreamException(ex.Message, ex);
}
}
else
{
OnStatusMessage(MessageLevel.Info, "Loading database input data...");
const string MeasurementTable = "ActiveMeasurements";
Dictionary dataProviderSettings = m_dataProviderString.ParseKeyValuePairs();
Assembly assm = Assembly.Load(dataProviderSettings["AssemblyName"]);
Type connectionType = assm.GetType(dataProviderSettings["ConnectionType"]);
Dictionary lookupCache = new Dictionary();
IDbCommand command;
IDataReader dbReader;
MeasurementKey key;
Guid id;
ushort index = 0;
connection = (IDbConnection)Activator.CreateInstance(connectionType);
connection.ConnectionString = m_dbConnectionString;
connection.Open();
command = connection.CreateCommand();
command.CommandText = $"SELECT * FROM {m_dbTableName}";
using (dbReader = command.ExecuteReader())
{
while (dbReader.Read())
{
measurement = new CompactMeasurement(signalIndexCache);
foreach (string fieldName in m_fieldNames.Keys)
{
object value = dbReader[fieldName];
string propertyName = m_fieldNames[fieldName];
switch (propertyName)
{
case "Timestamp":
// If the value is a timestamp, use the timestamp format
// specified by the user when reading the timestamp.
if (m_timestampFormat == null)
measurement.Timestamp = long.Parse(value.ToNonNullString());
else
measurement.Timestamp = DateTime.ParseExact(value.ToNonNullString(), m_timestampFormat, CultureInfo.CurrentCulture);
break;
case "ID":
if (Guid.TryParse(value.ToString(), out id))
{
if (!lookupCache.TryGetValue(id, out key))
{
if (DataSource.Tables.Contains(MeasurementTable))
{
DataRow[] filteredRows = DataSource.Tables[MeasurementTable].Select($"SignalID = '{id}'");
if (filteredRows.Length > 0)
key = MeasurementKey.LookUpOrCreate(id, filteredRows[0]["ID"].ToString());
}
if (key != MeasurementKey.Undefined)
{
// Cache measurement key associated with ID
lookupCache[id] = key;
// Assign a runtime index optimization for distinct measurements
signalIndexCache.Reference.TryAdd(index++, key);
}
}
measurement.Metadata = key.Metadata;
}
break;
case "Key":
if (MeasurementKey.TryParse(value.ToString(), out key))
{
if (!lookupCache.ContainsKey(key.SignalID))
{
// Cache measurement key associated with ID
lookupCache[key.SignalID] = key;
// Assign a runtime index optimization for distinct measurements
signalIndexCache.Reference.TryAdd(index++, key);
}
measurement.Metadata = key.Metadata;
}
break;
case "Value":
measurement.Value = Convert.ToDouble(value);
break;
default:
PropertyInfo property = GetAllProperties(typeof(IMeasurement)).FirstOrDefault(propertyInfo => propertyInfo.Name == propertyName);
if (property != null)
{
Type propertyType = property.PropertyType;
Type valueType = value.GetType();
// ReSharper disable once UseMethodIsInstanceOfType
if (property.PropertyType.IsAssignableFrom(value.GetType()))
{
property.SetValue(measurement, value, null);
}
else if (property.PropertyType == typeof(string))
{
property.SetValue(measurement, value.ToNonNullString(), null);
}
else if (valueType == typeof(string))
{
MethodInfo parseMethod = propertyType.GetMethod("Parse", new[] { typeof(string) });
if (parseMethod != null && parseMethod.IsStatic)
property.SetValue(measurement, parseMethod.Invoke(null, new[] { value }), null);
}
else
{
string exceptionMessage = $"The type of field {fieldName} could not be converted to the type of property {propertyName}.";
OnProcessException(MessageLevel.Warning, new InvalidCastException(exceptionMessage));
}
}
else
{
string exceptionMessage = $"The type of field {fieldName} could not be converted to the type of property {propertyName} - no property match was found.";
OnProcessException(MessageLevel.Warning, new InvalidCastException(exceptionMessage));
}
break;
}
m_dbMeasurements.Add(measurement);
if (m_dbMeasurements.Count % 50000 == 0)
OnStatusMessage(MessageLevel.Info, $"Loaded {m_dbMeasurements.Count:N0} records so far...");
}
}
}
OnStatusMessage(MessageLevel.Info, "Sorting data by time...");
m_dbMeasurements = m_dbMeasurements.OrderBy(m => (long)m.Timestamp).ToList();
OnStatusMessage(MessageLevel.Info, $"Completed data load in {((Ticks)(DateTime.UtcNow.Ticks - startTime)).ToElapsedTimeString(2)}");
if (m_cacheFileName != null)
{
OnStatusMessage(MessageLevel.Info, "Caching data for next initialization...");
using (FileStream data = File.OpenWrite(m_cacheFileName))
{
byte[] signalIndexCacheImage = Serialization.Serialize(signalIndexCache, SerializationFormat.Binary);
int compactMeasurementSize = (new CompactMeasurement(signalIndexCache)).BinaryLength;
// Write the signal index cache image size to the file
data.Write(LittleEndian.GetBytes(signalIndexCacheImage.Length), 0, 4);
// Write the signal index cache image to the file
data.Write(signalIndexCacheImage, 0, signalIndexCacheImage.Length);
// Write the size of each compact measurement to the file
data.Write(LittleEndian.GetBytes(compactMeasurementSize), 0, 4);
// Write the total number of compact measurements to the file
data.Write(LittleEndian.GetBytes(m_dbMeasurements.Count), 0, 4);
// Write each compact measurement image to the file
for (int i = 0; i < m_dbMeasurements.Count; i++)
{
((ISupportBinaryImage)m_dbMeasurements[i]).CopyBinaryImageToStream(data);
}
}
}
}
OnStatusMessage(MessageLevel.Info, "Entering data read cycle...");
ThreadPool.QueueUserWorkItem(PublishData);
}
catch (EndOfStreamException ex)
{
OnProcessException(MessageLevel.Warning, new EndOfStreamException($"Failed load cached data from {m_cacheFileName} due to file corruption{(string.IsNullOrWhiteSpace(ex.Message) ? "," : ": " + ex.Message + " - ")} cache will be recreated from database"));
// If the cached file is corrupt, delete it and load from the database
if (File.Exists(m_cacheFileName))
File.Delete(m_cacheFileName);
m_dbMeasurements.Clear();
GetDbMeasurements(null);
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Warning, new InvalidOperationException("Failed during data load: " + ex.Message, ex));
}
finally
{
if (connection != null)
connection.Close();
}
}
private PropertyInfo[] GetAllProperties(Type type)
{
List typeList = new List();
typeList.Add(type);
if (type.IsInterface)
{
typeList.AddRange(type.GetInterfaces());
}
List propertyList = new List();
foreach (Type interfaceType in typeList)
{
foreach (PropertyInfo property in interfaceType.GetProperties())
{
propertyList.Add(property);
}
}
return propertyList.ToArray();
}
// Publishes the frame measurements.
private void PublishData(object state)
{
long now, currentDataTime, publicationTime = 0;
long ticksPerFrame = Ticks.PerSecond / m_framesPerSecond;
long toleranceWindow = ticksPerFrame / 2;
while (Enabled)
{
List measurements = new List();
now = DateTime.UtcNow.Ticks;
// See if it is time to publish
if (now - publicationTime >= ticksPerFrame)
{
// Initialize publication time
if (publicationTime == 0)
publicationTime = ((Ticks)now).BaselinedTimestamp(BaselineTimeInterval.Second);
else
publicationTime += ticksPerFrame;
currentDataTime = m_dbMeasurements[m_nextIndex].Timestamp;
// Prepare next frame of data for all measurements with the current time
while (m_nextIndex < m_dbMeasurements.Count && Math.Abs((long)m_dbMeasurements[m_nextIndex].Timestamp - currentDataTime) < toleranceWindow)
{
// Clone the measurement so we can safely update the timestamp
IMeasurement measurement = Measurement.Clone(m_dbMeasurements[m_nextIndex]);
if (m_simulateTimestamps)
measurement.Timestamp = publicationTime;
measurements.Add(measurement);
m_nextIndex++;
}
OnNewMeasurements(measurements);
// Prepare index for next check, time moving forward
if (m_nextIndex == m_dbMeasurements.Count)
m_nextIndex = 0;
}
Thread.Sleep(1);
}
}
#endregion
}
}