//****************************************************************************************************** // MetadataFile.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: // ---------------------------------------------------------------------------------------------------- // 03/08/2007 - Pinal C. Patel // Generated original version of source code. // 04/21/2009 - Pinal C. Patel // Converted to C#. // 09/03/2009 - Pinal C. Patel // Added Read() overload that takes string as its parameter for performing flexible reads. // 09/15/2009 - Stephen C. Wills // Added new header and license agreement. // 09/16/2009 - Pinal C. Patel // Changed the default value for SettingsCategory property to the type name. // 10/11/2010 - Mihir Brahmbhatt // Updated header and license agreement. // 12/14/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Configuration; using System.Drawing; using System.IO; using System.Linq; using System.Text; using GSF.Configuration; using GSF.IO; // ReSharper disable PossibleMultipleEnumeration namespace GSF.Historian.Files { /// /// Defines legacy operational modes for the . /// public enum MetadataFileLegacyMode { /// /// Only use new "..._dbase.dat2" format. /// Disabled, /// /// Use new "..._dbase.dat2" format and also maintain a legacy "..._dbase.dat" file. /// Compatible, /// /// Only use legacy "..._dbase.dat" file. /// Enabled } /// /// Represents a file containing s. /// /// [ToolboxBitmap(typeof(MetadataFile))] public class MetadataFile : IsamDataFileBase { #region [ Members ] // Nested Types private sealed class LegacyMetadataFile : IsamDataFileBase { #region [ Constructors ] /// /// Creates a new . /// /// Legacy file name. public LegacyMetadataFile(string fileName) { FileName = fileName; FileAccessMode = FileAccess.Write; } #endregion #region [ Methods ] /// /// Gets the binary size of a . /// /// A 32-bit signed integer. protected override int GetRecordSize() => MetadataRecord.FixedLength; /// /// Creates a new with the specified . /// /// 1-based index of the . /// A object. protected override MetadataRecord CreateNewRecord(int recordIndex) => new MetadataRecord(recordIndex, MetadataFileLegacyMode.Enabled); #endregion } // Fields private MetadataFileLegacyMode m_legacyMode; private string m_baseFileName; private readonly Dictionary m_records; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// public MetadataFile() { SettingsCategory = this.GetType().Name; m_legacyMode = MetadataFileLegacyMode.Disabled; // Default to only using new file format m_records = new Dictionary(); } #endregion #region [ Properties ] /// /// Determines legacy operation mode for the . /// public MetadataFileLegacyMode LegacyMode { get => m_legacyMode; set { if (m_legacyMode == value) return; m_legacyMode = value; if (m_legacyMode != MetadataFileLegacyMode.Enabled) return; lock (m_records) m_records.Clear(); } } /// /// Gets or sets the name of the file. /// /// /// Changing the when the file is open will cause the file to be re-opened. /// public override string FileName { get => base.FileName; set { base.FileName = value; m_baseFileName = value is null ? null : value.EndsWith("2") ? value.Substring(0, value.Length - 1) : value; } } /// /// Gets a boolean value that indicates whether the file data on disk is corrupt. /// public override bool IsCorrupt => m_legacyMode == MetadataFileLegacyMode.Enabled && base.IsCorrupt; /// /// Gets the number of file records on the disk. /// public override int RecordsOnDisk => m_legacyMode == MetadataFileLegacyMode.Enabled ? base.RecordsOnDisk : RecordsInMemory; // Difficult to calculate with variable length strings, so we just return count in memory /// /// Gets the number of file records loaded in memory. /// public override int RecordsInMemory { get { if (m_legacyMode == MetadataFileLegacyMode.Enabled) return base.RecordsInMemory; lock (m_records) return m_records.Keys.DefaultIfEmpty(0).Max(); } } /// /// Gets the descriptive status of the . /// public override string Status { get { StringBuilder status = new StringBuilder(); status.Append(base.Status); status.AppendLine($" Legacy operation mode: {m_legacyMode}"); return status.ToString(); } } #endregion #region [ Methods ] /// /// Saves settings of the file to the config file if the property is set to true. /// /// has a value of null or empty string. public override void SaveSettings() { if (!PersistSettings) return; base.SaveSettings(); // Save settings under the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory]; settings["LegacyMode", true].Update(m_legacyMode); config.Save(); } /// /// Loads saved settings of the file from the config file if the property is set to true. /// /// has a value of null or empty string. public override void LoadSettings() { if (!PersistSettings) return; base.LoadSettings(); // Load settings from the specified category. CategorizedSettingsElementCollection settings = ConfigurationFile.Current.Settings[SettingsCategory]; settings.Add("LegacyMode", m_legacyMode, "Metadata file legacy format mode. Value is one of \"Disabled\", \"Compatible\" or \"Enabled\" where \"Disabled\" means only use new format, \"Compatible\" means use new format and also write a legacy format file for compatibility and \"Enabled\" means only use the legacy format."); LegacyMode = settings["LegacyMode"].ValueAs(m_legacyMode); // Define new file name for non-legacy implementations if (m_legacyMode != MetadataFileLegacyMode.Enabled && !FileName.EndsWith("2")) FileName += "2"; // By design of the new metadata file format, data is always loaded into memory - regardless of configuration setting if (m_legacyMode != MetadataFileLegacyMode.Enabled) LoadOnOpen = true; } /// /// Reads s that matches the . /// /// Comma or semi-colon delimited list of IDs or text for which the matching s are to be retrieved. /// An object of . public IEnumerable Read(string searchPattern) { foreach (string searchPart in searchPattern.Split(',', ';')) { // Iterate through all parts. if (int.TryParse(searchPattern, out int id)) { // Exact id is specified. yield return Read(id); } else { // Text is specified, so search for matches. foreach (MetadataRecord record in Read()) { if (record.Name.IndexOf(searchPart, StringComparison.CurrentCultureIgnoreCase) >= 0 || record.Synonym1.IndexOf(searchPart, StringComparison.CurrentCultureIgnoreCase) >= 0 || record.Synonym2.IndexOf(searchPart, StringComparison.CurrentCultureIgnoreCase) >= 0 || record.Synonym3.IndexOf(searchPart, StringComparison.CurrentCultureIgnoreCase) >= 0 || record.Description.IndexOf(searchPart, StringComparison.CurrentCultureIgnoreCase) >= 0 || record.Remarks.IndexOf(searchPart, StringComparison.CurrentCultureIgnoreCase) >= 0 || record.HardwareInfo.IndexOf(searchPart, StringComparison.CurrentCultureIgnoreCase) >= 0) { yield return record; } } } } } /// /// Opens the file. /// public override void Open() { // Handle special cases for readers if (!PersistSettings || FileAccessMode == FileAccess.Read) { // Need to fall back on using old file format if a new one doesn't exist for historical reads if (!File.Exists(FileName) && !string.IsNullOrEmpty(m_baseFileName) && File.Exists(m_baseFileName)) base.FileName = m_baseFileName; // Use new file format if a newer one exists if (m_legacyMode != MetadataFileLegacyMode.Enabled && !FileName.EndsWith("2") && File.Exists(FileName + "2")) FileName += "2"; // Fall back on legacy mode if needed if (!FileName.EndsWith("2") && File.Exists(FileName)) m_legacyMode = MetadataFileLegacyMode.Enabled; } base.Open(); } /// /// Loads records from disk into memory. /// public override void Load() { if (m_legacyMode == MetadataFileLegacyMode.Enabled) { base.Load(); return; } if (!IsOpen) throw new InvalidOperationException($"MetadataFile \"{FileName}\" is not open"); // Waits for any pending request to save records before completing. SaveWaitHandle.Wait(); // Waits for any prior request to load records before completing. LoadWaitHandle.Wait(); LoadWaitHandle.Reset(); try { OnDataLoading(); lock (FileDataLock) { if (FileData.Length > 0) { FileData.Seek(0, SeekOrigin.Begin); BinaryReader reader = new BinaryReader(FileData); int count = reader.ReadInt32(); for (int i = 0; i < count; i++) { MetadataRecord record = new MetadataRecord(reader); lock (m_records) m_records[record.HistorianID] = record; } } } OnDataLoaded(); } finally { LoadWaitHandle.Set(); } } /// /// Reads file records from disk if records were not loaded in memory otherwise returns the records in memory. /// /// Records of the file. public override IEnumerable Read() { if (m_legacyMode == MetadataFileLegacyMode.Enabled) { foreach (MetadataRecord record in base.Read()) yield return record; } if (!IsOpen) throw new InvalidOperationException($"MetadataFile \"{FileName}\" is not open"); MetadataRecord[] records; lock (m_records) records = m_records.Values.ToArray(); foreach (MetadataRecord record in records) yield return record; } /// /// Reads specified file record from disk if records were not loaded in memory otherwise returns the record in memory. /// /// 1-based index of the record to be read. /// Record with the specified ID if it exists; otherwise null. public override MetadataRecord Read(int recordIndex) { if (m_legacyMode == MetadataFileLegacyMode.Enabled) return base.Read(recordIndex); if (!IsOpen) throw new InvalidOperationException($"MetadataFile \"{FileName}\" is not open"); lock (m_records) { if (m_records.TryGetValue(recordIndex, out MetadataRecord record)) return record; // For compatibility with legacy mode, return a blank record if index is less than max if (recordIndex < m_records.Keys.DefaultIfEmpty(0).Max()) return new MetadataRecord(recordIndex, m_legacyMode); } return null; } /// /// Saves records loaded in memory to disk. /// /// /// is equivalent to when records are not loaded in memory. /// public override void Save() { if (m_legacyMode == MetadataFileLegacyMode.Enabled) base.Save(); if (m_legacyMode == MetadataFileLegacyMode.Compatible) SaveLegacyMetadataFile(); if (m_legacyMode == MetadataFileLegacyMode.Disabled || m_legacyMode == MetadataFileLegacyMode.Compatible) { if (!IsOpen) throw new InvalidOperationException($"MetadataFile \"{FileName}\" is not open"); // Waits for any pending request to save records before completing. SaveWaitHandle.Wait(); // Waits for any prior request to load records before completing. LoadWaitHandle.Wait(); SaveWaitHandle.Reset(); try { OnDataSaving(); MetadataRecord[] records; lock (m_records) records = m_records.Values.ToArray(); lock (FileDataLock) { FileData.SetLength(0); BinaryWriter writer = new BinaryWriter(FileData); writer.Write(records.Length); foreach (MetadataRecord record in records) record.WriteImage(writer); FileData.Flush(); File.SetLastWriteTime(FileName, DateTime.Now); } OnDataSaved(); } finally { SaveWaitHandle.Set(); } } } /// /// Writes specified records to disk if records were not loaded in memory otherwise updates the records in memory. /// /// Records to be written. /// /// This operation will causes existing records to be deleted and replaced with the ones specified. /// public override void Write(IEnumerable records) { if (m_legacyMode == MetadataFileLegacyMode.Enabled) base.Write(records); if (m_legacyMode == MetadataFileLegacyMode.Compatible) SaveLegacyMetadataFile(records); if (m_legacyMode == MetadataFileLegacyMode.Disabled || m_legacyMode == MetadataFileLegacyMode.Compatible) { if (!IsOpen) throw new InvalidOperationException($"MetadataFile \"{FileName}\" is not open"); lock (m_records) { m_records.Clear(); foreach (MetadataRecord record in records) m_records[record.HistorianID] = record; } } } /// /// Writes specified record to disk if records were not loaded in memory otherwise updates the record in memory. /// /// 1-based index of the record to be written. /// Record to be written. public override void Write(int recordIndex, MetadataRecord record) { if (record is null) throw new ArgumentNullException(nameof(record)); if (m_legacyMode == MetadataFileLegacyMode.Enabled || m_legacyMode == MetadataFileLegacyMode.Compatible) base.Write(recordIndex, record); if (m_legacyMode == MetadataFileLegacyMode.Disabled || m_legacyMode == MetadataFileLegacyMode.Compatible) { lock (m_records) m_records[recordIndex] = record; } } private void SaveLegacyMetadataFile() { using (LegacyMetadataFile metadataFile = new LegacyMetadataFile(m_baseFileName)) { lock (m_records) { foreach (KeyValuePair record in m_records) metadataFile.Write(record.Key, record.Value); } metadataFile.Save(); } } private void SaveLegacyMetadataFile(IEnumerable records) { using (LegacyMetadataFile metadataFile = new LegacyMetadataFile(m_baseFileName)) { metadataFile.Write(records); metadataFile.Save(); } } /// /// Gets the binary size of a . /// /// A 32-bit signed integer. protected override int GetRecordSize() => MetadataRecord.FixedLength; /// /// Creates a new with the specified . /// /// 1-based index of the . /// A object. protected override MetadataRecord CreateNewRecord(int recordIndex) => new MetadataRecord(recordIndex, m_legacyMode); #endregion } }