698 lines · 29.1 KB · C#
RawDownload
//******************************************************************************************************//  DatabaseConfigurationLoader.cs - Gbtc////  Copyright © 2014, 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://  ----------------------------------------------------------------------------------------------------//  04/04/2014 - Stephen C. Wills//       Generated original version of source code.////****************************************************************************************************** using System;using System.Collections.Generic;using System.Data;using System.Linq;using System.Reflection;using GSF.Data;using GSF.IO;using GSF.Security;using GSF.Units; namespace GSF.TimeSeries.Configuration{    /// <summary>    /// Defines a method signature for a bootstrap data source operation.    /// </summary>    /// <param name="database">Connection to database.</param>    /// <param name="nodeIDQueryString">Formatted node ID Guid query string.</param>    /// <param name="trackingVersion">Latest version of the configuration to which data operations were previously applied.</param>    /// <param name="arguments">Optional data operation arguments.</param>    /// <param name="statusMessage">Reference to host status message function.</param>    /// <param name="processException">Reference to host process exception function.</param>    public delegate void DataOperationFunction(AdoDataConnection database, string nodeIDQueryString, ulong trackingVersion, string arguments, Action<string> statusMessage, Action<Exception> processException);     /// <summary>    /// Represents a configuration loader that gets its configuration from a database connection.    /// </summary>    public class DatabaseConfigurationLoader : IConfigurationLoader, IDisposable    {        #region [ Members ]         // Events         /// <summary>        /// Occurs when the configuration loader has a message to provide about its current status.        /// </summary>        public event EventHandler<EventArgs<string>> StatusMessage;         /// <summary>        /// Occurs when the configuration loader encounters a non-catastrophic exception.        /// </summary>        public event EventHandler<EventArgs<Exception>> ProcessException;         // Fields        private string m_connectionString;        private string m_dataProviderString;        private string m_nodeIDQueryString;         private AdoDataConnection m_database;         #endregion         #region [ Properties ]         /// <summary>        /// Gets or sets the connection string which        /// defines how to connect to the database.        /// </summary>        public string ConnectionString        {            get            {                return m_connectionString;            }            set            {                m_connectionString = value;            }        }         /// <summary>        /// Gets or sets the data provider string, which determines the        /// .NET types to use when opening connections to the database.        /// </summary>        public string DataProviderString        {            get            {                return m_dataProviderString;            }            set            {                m_dataProviderString = value;            }        }         /// <summary>        /// Gets or sets the string to use in queries when filtering results by node ID.        /// </summary>        public string NodeIDQueryString        {            get            {                return m_nodeIDQueryString;            }            set            {                m_nodeIDQueryString = value;            }        }         /// <summary>        /// Gets the flag that indicates whether augmentation is supported by this configuration loader.        /// </summary>        public bool CanAugment        {            get            {                try                {                    int trackedTables = 0;                    Execute(database => trackedTables = Convert.ToInt32(database.Connection.ExecuteScalar("SELECT COUNT(*) FROM TrackedTable")));                    return trackedTables > 0;                }                catch                {                    return false;                }            }        }         #endregion         #region [ Methods ]         /// <summary>        /// Opens the database connection.        /// </summary>        public void Open()        {            using (m_database)            {                m_database = new AdoDataConnection(m_connectionString, m_dataProviderString);                OnStatusMessage("Database connection opened.");            }        }         /// <summary>        /// Loads the entire configuration data set from scratch.        /// </summary>        /// <returns>The configuration data set.</returns>        public DataSet Load()        {            DataSet configuration = null;             Execute(database =>            {                ulong latestVersion;                DataTable entities;                 configuration = new DataSet("Iaon");                latestVersion = GetLatestVersion(0LU);                ExecuteDataOperations();                 // Load configuration entities defined in database                entities = GetEntities();                 // Add configuration entities table to system configuration for reference                configuration.Tables.Add(entities.Copy());                 // Add each configuration entity to the system configuration                foreach (DataRow entityRow in entities.Rows)                    configuration.Tables.Add(LoadTable(entityRow));                 AddVersion(configuration, latestVersion);                ExtractSecurityContext();            });             return configuration;        }         /// <summary>        /// Augments the given configuration data set with the changes        /// tracked since the version of the given configuration data set.        /// </summary>        /// <param name="configuration">The configuration data set to be augmented.</param>        public void Augment(DataSet configuration)        {            // Get the version of the configuration so we know            // which changes in the change table need to be updated            ulong currentVersion = GetVersion(configuration);             Execute(database =>            {                ulong latestVersion;                 DataTable entities;                string sourceName;                string runtimeName;                 DataTable trackedChanges;                string[] trackedTables;                int changeCount;                 DataTable entityTable;                string primaryKeyColumn;                HashSet<string> primaryKeys;                 DataRow[] entityChanges;                Dictionary<string, int> unchangedRecordIndexes;                Dictionary<string, DataRow> changedRecords;                List<int> rowsToRemove;                 Ticks operationStartTime;                Time operationElapsedTime;                bool success;                 // First check if we can augment                if (!CanAugment)                    throw new InvalidOperationException("Unable to augment configuration because it is not supported.");                 // First thing we do is get the latest version available from the TrackedChanges                // table. This will be the version we are upgrading to. We need to do this first                // to prevent potential race conditions that might occur between this and other                // processes that may be modifying the database that could result in failure to                // update changes to an augmented table                latestVersion = GetLatestVersion(currentVersion);                 // Execute data operations before loading anything else from the database                ExecuteDataOperations(currentVersion);                 // Load configuration entities defined in database                entities = GetEntities();                 // If any tables exist in the configuration data set that do not exist                // in the ConfigurationEntity table, they were either added at runtime                // or removed from ConfigurationEntity. In either case, they can be                // safely removed from the configuration data set                foreach (DataTable table in configuration.Tables.Cast<DataTable>().ToList())                {                    if (entities.Select().All(row => row["RuntimeName"].ToNonNullString() != table.TableName))                        configuration.Tables.Remove(table);                }                 // Add configuration entities table to system configuration for reference                configuration.Tables.Add(entities.Copy());                 // Get the changes since the current version of the configuration data set                trackedChanges = GetTrackedChanges(currentVersion);                 if (TrackedChangesAreValid(currentVersion))                {                    // If there is a gap between the version of this configuration and the minimum version                    // in the changes table, there might be some changes that were unaccounted for. Therefore,                    // we pretend there are no tracked tables so that all tables will be reloaded from scratch                    if (trackedChanges.Select().Select(row => Convert.ToUInt64(row["ID"])).DefaultIfEmpty(currentVersion + 1LU).Min() != currentVersion + 1LU)                    {                        currentVersion = ulong.MinValue;                        trackedTables = new string[0];                    }                    else                    {                        trackedTables = database.Connection.RetrieveData(database.AdapterType, "SELECT Name FROM TrackedTable").Select()                            .Select(row => row["Name"].ToNonNullString())                            .ToArray();                    }                }                else                {                    // If there is any tracked change version in the changes table that is smaller than the current                    // version, there may have been an unexpected manual database change or migration. Therefore,                    // we pretend there are no tracked tables so that all tables will be reloaded from scratch                    currentVersion = ulong.MinValue;                    trackedTables = new string[0];                }                 foreach (DataRow entityRow in entities.Rows)                {                    success = false;                     // Get the source name and runtime name of the table we are attempting to augment                    sourceName = entityRow["SourceName"].ToNonNullString();                    runtimeName = entityRow["RuntimeName"].ToNonNullString();                     // A table can only be augmented if the table is tracked and if it exists in the current configuration                    if (configuration.Tables.Contains(runtimeName) && trackedTables.Contains(sourceName))                    {                        try                        {                            // Get the list of changes specific to this table                            entityChanges = trackedChanges.Select(string.Format("TableName = '{0}'", sourceName));                             // If there are no changes to this table, there is nothing to do                            if (entityChanges.Length > 0)                            {                                // Track the amount of time it takes to do this operation                                operationStartTime = DateTime.UtcNow.Ticks;                                 // Get the actual table to be augmented out of the current configuration                                entityTable = configuration.Tables[runtimeName];                                 // Get the name of the column containing the primary key for that table                                primaryKeyColumn = entityChanges.Select(row => row["PrimaryKeyColumn"].ToNonNullString()).First();                                 // Get the distinct list of valid primary keys                                primaryKeys = new HashSet<string>(entityChanges.Select(row => row["PrimaryKeyValue"]).Where(primaryKey => primaryKey != null && primaryKey != DBNull.Value).Select(primaryKey => primaryKey.ToString()), StringComparer.CurrentCultureIgnoreCase);                                 // Get the relevant records from the entity table as well as the updated values from the database                                unchangedRecordIndexes = entityTable.Rows.Cast<DataRow>().Select((row, index) => Tuple.Create(row[primaryKeyColumn].ToString(), index)).Where(tuple => primaryKeys.Contains(tuple.Item1)).ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2, StringComparer.CurrentCultureIgnoreCase);                                changedRecords = GetChangedRecords(sourceName, primaryKeyColumn, currentVersion).Rows.Cast<DataRow>().ToDictionary(row => row[primaryKeyColumn].ToString(), row => row, StringComparer.CurrentCultureIgnoreCase);                                rowsToRemove = new List<int>();                                 // Go through the list of distinct primary key values and query                                // the database for the latest changes to each of the modified records                                changeCount = 0;                                 foreach (string primaryKey in primaryKeys)                                {                                    int unchangedIndex;                                    DataRow changedRow;                                     if (changedRecords.TryGetValue(primaryKey, out changedRow))                                    {                                        // Record was inserted or modified - add or                                        // update it in the augmented configuration                                        if (!unchangedRecordIndexes.TryGetValue(primaryKey, out unchangedIndex))                                        {                                            unchangedIndex = entityTable.Rows.Count;                                            entityTable.Rows.Add(entityTable.NewRow());                                        }                                         Update(entityTable.Rows[unchangedIndex], changedRow);                                    }                                    else                                    {                                        // Record was removed so we hang onto the index so we can                                        // remove it from the augmented configuration at the end                                        if (unchangedRecordIndexes.TryGetValue(primaryKey, out unchangedIndex))                                            rowsToRemove.Add(unchangedIndex);                                    }                                     changeCount++;                                }                                 // Remove rows in descending index order so that indexes                                // don't change for rows that need to be removed                                foreach (int index in rowsToRemove.OrderByDescending(index => index))                                {                                    int lastIndex = entityTable.Rows.Count - 1;                                     if (index != lastIndex)                                        Update(entityTable.Rows[index], entityTable.Rows[lastIndex]);                                     entityTable.Rows.RemoveAt(lastIndex);                                }                                 // Get the amount of time it took to augment the table                                operationElapsedTime = (DateTime.UtcNow.Ticks - operationStartTime).ToSeconds();                                 // Display information to the user                                OnStatusMessage(string.Format("Loaded {0} change{1} to \"{2}\" in {3}...", changeCount, changeCount == 1 ? "" : "s", runtimeName, operationElapsedTime.ToString(2)));                            }                             success = true;                        }                        catch (Exception ex)                        {                            OnStatusMessage(string.Format("WARNING: Unable to augment {0} table due to exception: {1}", runtimeName, ex.Message));                            OnProcessException(ex);                        }                    }                     if (!success)                    {                        // If we were unable to augment the table, we need to                        // drop the current configuration and load the whole table                        if (configuration.Tables.Contains(runtimeName))                            configuration.Tables.Remove(runtimeName);                         configuration.Tables.Add(LoadTable(entityRow));                    }                }                 AddVersion(configuration, latestVersion);                 try                {                    database.Connection.ExecuteNonQuery(string.Format("DELETE FROM TrackedChange WHERE ID <= {0}", latestVersion));                }                catch (Exception ex)                {                    OnStatusMessage(string.Format("WARNING: Unable to curtail the TrackedChange table due to exception: {0}", ex.Message));                    OnProcessException(ex);                }            });        }         /// <summary>        /// Closes the database connection.        /// </summary>        public void Close()        {            if ((object)m_database != null)            {                m_database.Dispose();                m_database = null;                 OnStatusMessage("Database connection closed.");            }        }         /// <summary>        /// Releases all the resources used by the <see cref="DatabaseConfigurationLoader"/> object.        /// </summary>        public void Dispose()        {            Close();        }         private void ExecuteDataOperations(ulong trackingVersion = ulong.MinValue)        {            Execute(database =>            {                string assemblyName = "", typeName = "", methodName = "", arguments;                Assembly assembly;                Type type;                MethodInfo method;                 foreach (DataRow row in database.Connection.RetrieveData(database.AdapterType, string.Format("SELECT * FROM DataOperation WHERE (NodeID IS NULL OR NodeID={0}) AND Enabled <> 0 ORDER BY LoadOrder", m_nodeIDQueryString)).Rows)                {                    try                    {                        OnStatusMessage(string.Format("Executing startup data operation \"{0}\".", row["Description"].ToNonNullString("Unlabeled")));                         // Load data operation parameters                        assemblyName = row["AssemblyName"].ToNonNullString();                        typeName = row["TypeName"].ToNonNullString();                        methodName = row["MethodName"].ToNonNullString();                        arguments = row["Arguments"].ToNonNullString();                         if (string.IsNullOrWhiteSpace(assemblyName))                            throw new InvalidOperationException("Data operation assembly name was not defined.");                         if (string.IsNullOrWhiteSpace(typeName))                            throw new InvalidOperationException("Data operation type name was not defined.");                         if (string.IsNullOrWhiteSpace(methodName))                            throw new InvalidOperationException("Data operation method name was not defined.");                         // Load data operation from containing assembly and type                        assembly = Assembly.LoadFrom(FilePath.GetAbsolutePath(assemblyName));                        type = assembly.GetType(typeName);                        method = type.GetMethod(methodName, BindingFlags.IgnoreCase | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.InvokeMethod);                         // Execute data operation via loaded assembly method                        ((DataOperationFunction)Delegate.CreateDelegate(typeof(DataOperationFunction), method))(database, m_nodeIDQueryString, trackingVersion, arguments, OnStatusMessage, OnProcessException);                    }                    catch (Exception ex)                    {                        OnStatusMessage(string.Format("WARNING: Failed to execute startup data operation \"{0} [{1}::{2}()]\" due to exception: {3}", assemblyName, typeName, methodName, ex.Message));                        OnProcessException(ex);                    }                }            });        }         private DataTable LoadTable(DataRow entityRow)        {            DataTable destination = null;             Execute(database =>            {                DataTable source;                DataColumnCollection columns;                Dictionary<int, int> columnIndex;                 Ticks operationStartTime;                Time operationElapsedTime;                 // Load configuration entity data filtered by node ID                operationStartTime = DateTime.UtcNow.Ticks;                source = database.Connection.RetrieveData(database.AdapterType, string.Format("SELECT * FROM {0} WHERE NodeID={1}", entityRow["SourceName"], m_nodeIDQueryString));                operationElapsedTime = (DateTime.UtcNow.Ticks - operationStartTime).ToSeconds();                 // Update table name as defined in configuration entity                source.TableName = entityRow["RuntimeName"].ToString();                 OnStatusMessage(string.Format("Loaded {0} row{1} from \"{2}\" in {3}...", source.Rows.Count, source.Rows.Count == 1 ? "" : "s", source.TableName, operationElapsedTime.ToString(2)));

Lines 1–500 of 698.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t