//****************************************************************************************************** // AdoOutputAdapter.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. // 05/02/2011 - J. Ritchie Carroll // Cast ID field back to a signed integer to work with most database types per suggestion by Hugo. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Reflection; using System.Text; using GSF; using GSF.Diagnostics; using GSF.TimeSeries; using GSF.TimeSeries.Adapters; namespace AdoAdapters { /// /// Represents an output adapter that archives measurements to a database. /// [Description("ADO: Archives measurements to an ADO data source")] public class AdoOutputAdapter : OutputAdapterBase { #region [ Members ] // Fields private readonly Dictionary m_fieldNames; private readonly List m_fieldList; private string m_dbTableName; private string m_dbConnectionString; private string m_dataProviderString; private string m_timestampFormat; private int m_bulkInsertLimit; private bool m_isJetEngine; private bool m_isOracle; private IDbConnection m_connection; private long m_measurementCount; private bool m_disposed; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// public AdoOutputAdapter() { m_fieldNames = new Dictionary(StringComparer.CurrentCultureIgnoreCase); m_fieldList = new List(); } #endregion #region [ Properties ] /// /// Gets or sets the table name in the data source used to archive data. /// [ConnectionStringParameter, Description("Define the table name in the data source used to archive data."), 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; m_isJetEngine = m_dbConnectionString.Contains("Microsoft.Jet.OLEDB"); } } /// /// 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 output measurement timestamps to the data source. /// [ConnectionStringParameter, Description("Define the date and time format used to output the timestamp to 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 maximum number of measurements to be collated into one insert statement. /// [ConnectionStringParameter, Description("Define the maximum number of measurements to be collated into one insert statement."), DefaultValue("1024")] public int BulkInsertLimit { get { return m_bulkInsertLimit; } set { m_bulkInsertLimit = value; } } /// /// Returns a flag that determines if measurements sent to this /// are destined for archival. /// public override bool OutputIsForArchive => true; /// /// Gets a flag that determines if this /// uses an asynchronous connection. /// protected override bool UseAsyncConnect => false; #endregion #region [ Methods ] /// /// Initializes this . /// public override void Initialize() { base.Initialize(); Dictionary settings = Settings; Type measurementType = typeof(IMeasurement); IEnumerable measurementProperties = GetAllProperties(measurementType).Select(property => property.Name); StringComparison ignoreCase = StringComparison.CurrentCultureIgnoreCase; string setting; // 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; m_fieldList.Add(fieldName); } 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"); } // 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; m_isJetEngine = m_dbConnectionString.Contains("Microsoft.Jet.OLEDB"); // 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 output as ticks. if (m_timestampFormat.Equals("null", ignoreCase)) m_timestampFormat = null; } // Get bulk insert limit if (settings.TryGetValue("bulkInsertLimit", out setting)) m_bulkInsertLimit = int.Parse(setting); else m_bulkInsertLimit = 1024; // Create a new database connection object. Dictionary dataProviderSettings = m_dataProviderString.ParseKeyValuePairs(); Assembly assm = Assembly.Load(dataProviderSettings["AssemblyName"]); Type connectionType = assm.GetType(dataProviderSettings["ConnectionType"]); m_connection = (IDbConnection)Activator.CreateInstance(connectionType); m_connection.ConnectionString = m_dbConnectionString; m_isOracle = m_connection.GetType().Name == "OracleConnection"; } /// /// Attempts to connect to this . /// protected override void AttemptConnection() { m_connection.Open(); } /// /// Attempts to disconnect from this . /// protected override void AttemptDisconnection() { m_connection.Close(); m_measurementCount = 0; } /// /// 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 $"Archived {m_measurementCount} measurements to database.".CenterText(maxLength); } /// /// Archives locally. /// /// Measurements to be archived. protected override void ProcessMeasurements(IMeasurement[] measurements) { if ((object)measurements != null) { for (int i = 0; i < measurements.Length; i += m_bulkInsertLimit) { BulkInsert(measurements.Skip(i).Take(m_bulkInsertLimit)); } } } /// /// 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 override void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { if (m_connection != null) m_connection.Dispose(); m_connection = null; } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } 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(); } private void BulkInsert(IEnumerable measurements) { Type measurementType = typeof(IMeasurement); IDbCommand command = null; StringBuilder commandBuilder = new StringBuilder(); string insertFormat = "INSERT INTO {0} ({1}) "; string selectFormat = "SELECT {0} "; string unionFormat = "UNION ALL "; StringBuilder valuesBuilder; string fields = m_fieldList.Aggregate((field1, field2) => field1 + "," + field2); string values; IDbDataParameter parameter = null; char paramChar = m_isOracle ? ':' : '@'; int paramCount = 0; try { command = m_connection.CreateCommand(); commandBuilder.Append(string.Format(insertFormat, m_dbTableName, fields)); foreach (IMeasurement measurement in measurements) { valuesBuilder = new StringBuilder(); // Build the values list. foreach (string fieldName in m_fieldList) { string propertyName = m_fieldNames[fieldName]; PropertyInfo firstOrDefault = GetAllProperties(measurementType).FirstOrDefault(prop => prop.Name == propertyName); if ((object)firstOrDefault != null) { object value = firstOrDefault.GetValue(measurement, null); if (valuesBuilder.Length > 0) valuesBuilder.Append(','); if ((object)value == null) { valuesBuilder.Append("NULL"); continue; } valuesBuilder.Append(paramChar); valuesBuilder.Append('p'); valuesBuilder.Append(paramCount); parameter = command.CreateParameter(); parameter.ParameterName = paramChar + "p" + paramCount; parameter.Direction = ParameterDirection.Input; switch (propertyName.ToLower()) { case "id": parameter.Value = m_isJetEngine ? "{" + value + "}" : value; break; case "key": parameter.Value = value.ToString(); break; case "timestamp": case "publishedtimestamp": case "receivedtimestamp": Ticks timestamp = (Ticks)value; // If the value is a timestamp, use the timestamp format // specified by the user when inserting the timestamp. if (m_timestampFormat == null) parameter.Value = (long)timestamp; else parameter.Value = timestamp.ToString(m_timestampFormat); break; case "stateflags": // IMeasurement.StateFlags field is an uint, cast this back to a // signed integer to work with most database field types parameter.Value = Convert.ToInt32(value); break; default: parameter.Value = value; break; } } command.Parameters.Add(parameter); paramCount++; } values = valuesBuilder.ToString(); commandBuilder.Append(string.Format(selectFormat, values)); commandBuilder.Append(unionFormat); } // Remove "UNION ALL " from the end of the command text. commandBuilder.Remove(commandBuilder.Length - unionFormat.Length, unionFormat.Length); // Set the command text and execute the command. command.CommandText = commandBuilder.ToString(); command.ExecuteNonQuery(); m_measurementCount += measurements.Count(); } finally { if ((object)command != null) command.Dispose(); } } #endregion } }