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