//****************************************************************************************************** // ArchiveFileAllocationTable.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: // ---------------------------------------------------------------------------------------------------- // 02/18/2007 - Pinal C. Patel // Generated original version of code based on DatAWare system specifications by Brian B. Fox, GSF. // 01/23/2008 - Pinal C. Patel // Added thread safety to all FindDataBlock() methods. // Recoded RequestDataBlock() method to include the logic to use previously used partially filled // data blocks first. // 03/31/2008 - Pinal C. Patel // Removed intervaled persisting of FAT since FAT is persisted when new block is requested. // Recoded RequestDataBlock() method to speed up the block request process based on the block index // suggestion provided from the state information of the point. // 07/14/2008 - Pinal C. Patel // Added overload to GetDataBlock() method that takes a block index. // 04/21/2009 - Pinal C. Patel // Converted to C#. // 09/15/2009 - Stephen C. Wills // Added new header and license agreement. // 09/23/2009 - Pinal C. Patel // Edited code comments. // 12/03/2009 - Pinal C. Patel // Modified FindDataBlocks() to accurately find matching data blocks even when the specified // search timespan is a sub-second range. // 10/11/2010 - Mihir Brahmbhatt // Updated header and license agreement. // 11/30/2011 - J. Ritchie Carroll // Modified to support buffer optimized ISupportBinaryImage. // 12/14/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.IO; using System.Linq; using GSF.Interop; using GSF.Parsing; using GSF.Units; namespace GSF.Historian.Files { /// /// Represents the File Allocation Table of an . /// /// . /// /// public class ArchiveFileAllocationTable : ISupportBinaryImage { #region [ Members ] // Nested Types /// /// Defines the fixed table region of the . /// private class FixedTableRegion : ISupportBinaryImage { #region [ Members ] // Constants /// /// Specifies the number of bytes in the binary image of the . /// public const int FixedLength = 32; // Fields private readonly ArchiveFileAllocationTable m_parent; #endregion #region [ Constructors ] /// /// Creates a new instance of the . /// /// Reference to parent . public FixedTableRegion(ArchiveFileAllocationTable parent) { m_parent = parent; } #endregion #region [ Properties ] /// /// Gets the length of the . /// public int BinaryLength { get { return FixedLength; } } #endregion #region [ Methods ] /// /// Initializes by parsing the specified containing a binary image. /// /// Buffer containing binary image to parse. /// 0-based starting index in the to start parsing. /// Valid number of bytes within from . /// The number of bytes used for initialization in the (i.e., the number of bytes parsed). /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public int ParseBinaryImage(byte[] buffer, int startIndex, int length) { buffer.ValidateParameters(startIndex, length); decimal startTime = (decimal)LittleEndian.ToDouble(buffer, startIndex); decimal stopTime = (decimal)LittleEndian.ToDouble(buffer, startIndex + 8); // Validate file time tags if (startTime < TimeTag.MinValue.Value || startTime > TimeTag.MaxValue.Value) startTime = TimeTag.MinValue.Value; if (stopTime < TimeTag.MinValue.Value || stopTime > TimeTag.MaxValue.Value) stopTime = TimeTag.MinValue.Value; m_parent.FileStartTime = new TimeTag(startTime); m_parent.FileEndTime = new TimeTag(stopTime); m_parent.DataPointsReceived = LittleEndian.ToInt32(buffer, startIndex + 16); m_parent.DataPointsArchived = LittleEndian.ToInt32(buffer, startIndex + 20); m_parent.DataBlockSize = LittleEndian.ToInt32(buffer, startIndex + 24); m_parent.DataBlockCount = LittleEndian.ToInt32(buffer, startIndex + 28); return FixedLength; } /// /// Generates binary image of the and copies it into the given buffer, for bytes. /// /// Buffer used to hold generated binary image of the source object. /// 0-based starting index in the to start writing. /// The number of bytes written to the . /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public int GenerateBinaryImage(byte[] buffer, int startIndex) { int length = BinaryLength; buffer.ValidateParameters(startIndex, length); Buffer.BlockCopy(LittleEndian.GetBytes((double)m_parent.m_fileStartTime.Value), 0, buffer, startIndex, 8); Buffer.BlockCopy(LittleEndian.GetBytes((double)m_parent.m_fileEndTime.Value), 0, buffer, startIndex + 8, 8); Buffer.BlockCopy(LittleEndian.GetBytes(m_parent.m_dataPointsReceived), 0, buffer, startIndex + 16, 4); Buffer.BlockCopy(LittleEndian.GetBytes(m_parent.m_dataPointsArchived), 0, buffer, startIndex + 20, 4); Buffer.BlockCopy(LittleEndian.GetBytes(m_parent.m_dataBlockSize), 0, buffer, startIndex + 24, 4); Buffer.BlockCopy(LittleEndian.GetBytes(m_parent.m_dataBlockCount), 0, buffer, startIndex + 28, 4); return length; } #endregion } /// /// Defines the variable table region of the . /// private class VariableTableRegion : ISupportBinaryImage { #region [ Members ] // Fields private readonly ArchiveFileAllocationTable m_parent; #endregion #region [ Constructors ] /// /// Creates a new instance of the . /// /// Reference to parent . public VariableTableRegion(ArchiveFileAllocationTable parent) { m_parent = parent; } #endregion #region [ Properties ] /// /// Gets the length of the . /// public int BinaryLength { get { // We add the extra bytes for the array descriptor that are required for reading the file from a VB6 style application. return (ArrayDescriptorLength + (m_parent.m_dataBlockCount * ArchiveDataBlockPointer.FixedLength)); } } #endregion #region [ Methods ] /// /// Initializes by parsing the specified containing a binary image. /// /// Buffer containing binary image to parse. /// 0-based starting index in the to start parsing. /// Valid number of bytes within from . /// The number of bytes used for initialization in the (i.e., the number of bytes parsed). /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public int ParseBinaryImage(byte[] buffer, int startIndex, int length) { buffer.ValidateParameters(startIndex, length); ArchiveDataBlockPointer blockPointer; // Set parsing cursor beyond old-style visual basic array descriptor int index = startIndex + ArrayDescriptorLength; for (int i = 0; i < m_parent.m_dataBlockCount; i++) { blockPointer = new ArchiveDataBlockPointer(m_parent.m_parent, i); index += blockPointer.ParseBinaryImage(buffer, index, length - (index - startIndex)); m_parent.m_dataBlockPointers.Add(blockPointer); } return (index - startIndex); } /// /// Generates binary image of the and copies it into the given buffer, for bytes. /// /// Buffer used to hold generated binary image of the source object. /// 0-based starting index in the to start writing. /// The number of bytes written to the . /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public int GenerateBinaryImage(byte[] buffer, int startIndex) { int length = BinaryLength; buffer.ValidateParameters(startIndex, length); VBArrayDescriptor arrayDescriptor = VBArrayDescriptor.OneBasedOneDimensionalArray(m_parent.m_dataBlockCount); startIndex += arrayDescriptor.GenerateBinaryImage(buffer, startIndex); lock (m_parent.m_dataBlockPointers) { for (int i = 0; i < m_parent.m_dataBlockPointers.Count; i++) { startIndex += m_parent.m_dataBlockPointers[i].GenerateBinaryImage(buffer, startIndex); } } return length; } #endregion } // Constants private const int ArrayDescriptorLength = 10; // Fields private TimeTag m_fileStartTime; private TimeTag m_fileEndTime; private int m_dataPointsReceived; private int m_dataPointsArchived; private int m_dataBlockSize; private int m_dataBlockCount; private readonly List m_dataBlockPointers; private readonly ArchiveFile m_parent; private readonly FixedTableRegion m_fixedTableRegion; private readonly VariableTableRegion m_variableTableRegion; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// /// An object. internal ArchiveFileAllocationTable(ArchiveFile parent) { m_parent = parent; m_dataBlockPointers = new List(); m_fixedTableRegion = new FixedTableRegion(this); m_variableTableRegion = new VariableTableRegion(this); if (m_parent.FileData.Length == 0) { // File is brand new. m_fileStartTime = TimeTag.MinValue; m_fileEndTime = TimeTag.MinValue; m_dataBlockSize = m_parent.DataBlockSize; m_dataBlockCount = ArchiveFile.MaximumDataBlocks(m_parent.FileSize, m_parent.DataBlockSize); for (int i = 0; i < m_dataBlockCount; i++) { m_dataBlockPointers.Add(new ArchiveDataBlockPointer(m_parent, i)); } } else { // Existing file, read table regions: // Seek to beginning of fixed table region m_parent.FileData.Seek(-m_fixedTableRegion.BinaryLength, SeekOrigin.End); // Parse fixed table region m_fixedTableRegion.ParseBinaryImageFromStream(m_parent.FileData); // Seek to beginning of variable table region (above fixed from bottom of file) m_parent.FileData.Seek(-(m_fixedTableRegion.BinaryLength + m_variableTableRegion.BinaryLength), SeekOrigin.End); // Parse variable table region m_variableTableRegion.ParseBinaryImageFromStream(m_parent.FileData); } } #endregion #region [ Properties ] /// /// Gets or sets the of the oldest in the . /// /// The value being assigned is not between 01/01/1995 and 01/19/2063. public TimeTag FileStartTime { get { return m_fileStartTime; } set { if (value < TimeTag.MinValue || value > TimeTag.MaxValue) throw new ArgumentException("Value must between 01/01/1995 and 01/19/2063"); m_fileStartTime = value; } } /// /// Gets or sets the of the newest in the . /// /// The value being assigned is not between 01/01/1995 and 01/19/2063. public TimeTag FileEndTime { get { return m_fileEndTime; } set { if (value < TimeTag.MinValue || value > TimeTag.MaxValue) throw new ArgumentException("Value must between 01/01/1995 and 01/19/2063"); m_fileEndTime = value; } } /// /// Gets or sets the number s received by the for archival. /// /// The value being assigned is not positive or zero. public int DataPointsReceived { get { return m_dataPointsReceived; } set { if (value < 0) throw new ArgumentException("Value must be positive or zero"); m_dataPointsReceived = value; } } /// /// Gets or sets the number s archived by the . /// /// The value being assigned is not positive or zero. public int DataPointsArchived { get { return m_dataPointsArchived; } set { if (value < 0) throw new ArgumentException("Value must be positive or zero"); m_dataPointsArchived = value; } } /// /// Gets the size (in KB) of a single in the . /// public int DataBlockSize { get { return m_dataBlockSize; } private set { if (value < 1) throw new ArgumentException("Value must be positive"); m_dataBlockSize = value; } } /// /// Gets the total number of s in the . /// public int DataBlockCount { get { return m_dataBlockCount; } private set { if (value < 1) throw new ArgumentException("Value must be positive"); m_dataBlockCount = value; } } /// /// Gets the number of used s in the . /// public int DataBlocksUsed { get { return m_dataBlockCount - DataBlocksAvailable; } } /// /// Gets the number of unused s in the . /// public int DataBlocksAvailable { get { ArchiveDataBlock unusedDataBlock = FindDataBlock(-1); if ((object)unusedDataBlock != null) return m_dataBlockCount - unusedDataBlock.Index; return 0; } } /// /// Gets the s to the s in the . /// /// /// WARNING: is not thread safe. Synchronized access is required. /// public IList DataBlockPointers { get { return m_dataBlockPointers.AsReadOnly(); } } /// /// Gets the length of the . /// public int BinaryLength { get { return m_variableTableRegion.BinaryLength + m_fixedTableRegion.BinaryLength; } } private long DataLength { get { return (m_dataBlockCount * (m_dataBlockSize * 1024)); } } #endregion #region [ Methods ] /// /// Generates binary image of the and copies it into the given buffer, for bytes. /// /// Buffer used to hold generated binary image of the source object. /// 0-based starting index in the to start writing. /// The number of bytes written to the . /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public int GenerateBinaryImage(byte[] buffer, int startIndex) { int length = BinaryLength; buffer.ValidateParameters(startIndex, length); startIndex += m_variableTableRegion.GenerateBinaryImage(buffer, startIndex); m_fixedTableRegion.GenerateBinaryImage(buffer, startIndex); return length; } // This method is never used since constructor parses the point allocation table int ISupportBinaryImage.ParseBinaryImage(byte[] buffer, int startIndex, int length) { throw new NotSupportedException(); } /// /// Saves the data to the . /// /// Forces an immediate disk flush for save. public void Save(bool forceFlush = false) { // Leave space for data blocks. lock (m_parent.FileData) { if (m_parent.FileData.Length == 0) m_parent.FileData.SetLength((long)(m_parent.FileSize * SI2.Mega)); m_parent.FileData.Seek(DataLength, SeekOrigin.Begin); this.CopyBinaryImageToStream(m_parent.FileData); if (!m_parent.CacheWrites) m_parent.FileData.Flush(); } } /// /// Extends the by one . /// public void Extend() { Extend(1); } /// /// Extends the by the specified number of s. /// /// Number of s to add to the . public void Extend(int dataBlocksToAdd) { // Extend the FAT. lock (m_dataBlockPointers) { for (int i = 1; i <= dataBlocksToAdd; i++) { m_dataBlockPointers.Add(new ArchiveDataBlockPointer(m_parent, m_dataBlockPointers.Count)); } m_dataBlockCount = m_dataBlockPointers.Count; } Save(); // Initialize newly added data blocks. ArchiveDataBlock dataBlock; for (int i = m_dataBlockCount - dataBlocksToAdd; i < m_dataBlockCount; i++) { dataBlock = new ArchiveDataBlock(m_parent, i, -1); dataBlock.Reset(); } } /// /// Returns the first in the for the specified . /// /// Historian identifier whose is to be retrieved. /// true to pre-read data to locate write cursor. /// object if a match is found; otherwise null. public ArchiveDataBlock FindDataBlock(int historianID, bool preRead = true) { ArchiveDataBlockPointer pointer; lock (m_dataBlockPointers) { pointer = m_dataBlockPointers.FirstOrDefault(dataBlockPointer => dataBlockPointer.HistorianID == historianID); } if ((object)pointer == null) return null; return pointer.GetDataBlock(preRead); } /// /// Returns the last in the for the specified . /// /// Historian identifier. /// true to pre-read data to locate write cursor. /// object if a match is found; otherwise null. public ArchiveDataBlock FindLastDataBlock(int historianID, bool preRead = true) { ArchiveDataBlockPointer pointer; lock (m_dataBlockPointers) { pointer = m_dataBlockPointers.LastOrDefault(dataBlockPointer => dataBlockPointer.HistorianID == historianID); } if ((object)pointer == null) return null; return pointer.GetDataBlock(preRead); } /// /// Returns all s in the for the specified . /// /// Historian identifier. /// true to pre-read data to locate write cursor. /// A collection of s. public List FindDataBlocks(int historianID, bool preRead = true) { return FindDataBlocks(historianID, TimeTag.MinValue, preRead); } /// /// Returns all s in the for the specified with points later than the specified . /// /// Historian identifier. /// Start . /// true to pre-read data to locate write cursor. /// A collection of s. public List FindDataBlocks(int historianID, TimeTag startTime, bool preRead = true) { return FindDataBlocks(historianID, startTime, TimeTag.MaxValue, preRead); } /// /// Returns all s in the for the specified with s between the specified and . /// /// Historian identifier. /// Start . /// End . /// true to pre-read data to locate write cursor. /// A collection of s. public List FindDataBlocks(int historianID, TimeTag startTime, TimeTag endTime, bool preRead = true) { if ((object)startTime == null) startTime = TimeTag.MaxValue; if ((object)endTime == null) endTime = TimeTag.MaxValue; List blockPointers; lock (m_dataBlockPointers) { // Get all block pointers for given point ID over specified time range blockPointers = m_dataBlockPointers.FindAll(dataBlockPointer => dataBlockPointer.Matches(historianID, startTime, endTime)); // Look for pointer to data block on the borders of the specified range which may contain data if (!(startTime == TimeTag.MinValue || endTime == TimeTag.MaxValue)) { // There are 2 different search criteria for this: // 1) If matching data block pointers have been found, then find data block pointer before the first matching data block pointer. // or // 2) Find the last data block pointer in the time range of TimeTag.MinValue to m_searchEndTime. TimeTag searchEndTime = endTime; if (blockPointers.Count > 0) searchEndTime = new TimeTag(blockPointers.First().StartTime.Value - 1.0M); ArchiveDataBlockPointer borderMatch = m_dataBlockPointers.LastOrDefault(dataBlockPointer => dataBlockPointer.Matches(historianID, TimeTag.MinValue, searchEndTime)); if ((object)borderMatch != null) blockPointers.Insert(0, borderMatch); } } // Return list of data blocks for given block pointers return blockPointers.Select(pointer => pointer.GetDataBlock(preRead)).ToList(); } /// /// Returns an for writing s for the specified . /// /// Historian identifier for which the is being requested. /// of the to be written to the . /// of the last used for writing s for the . /// object if available; otherwise null if all s have been allocated. internal ArchiveDataBlock RequestDataBlock(int historianID, TimeTag dataTime, int blockIndex) { ArchiveDataBlock dataBlock = null; ArchiveDataBlockPointer dataBlockPointer = null; if (blockIndex >= 0 && blockIndex < m_dataBlockCount) { // Valid data block index is specified, so retrieve the corresponding data block. lock (m_dataBlockPointers) { dataBlockPointer = m_dataBlockPointers[blockIndex]; } dataBlock = dataBlockPointer.DataBlock; if (!dataBlockPointer.IsAllocated && dataBlock.SlotsUsed > 0) { // Clear existing data from the data block since it is unallocated. dataBlock.Reset(); } else if (dataBlockPointer.IsAllocated && (dataBlockPointer.HistorianID != historianID || (dataBlockPointer.HistorianID == historianID && dataBlock.SlotsAvailable == 0))) { // Search for a new data block since the suggested data block cannot be used. blockIndex = -1; } } if (blockIndex < 0) { // Negative data block index is specified indicating a search must be performed for a data block. dataBlock = FindLastDataBlock(historianID); if ((object)dataBlock != null && dataBlock.SlotsAvailable == 0) { // Previously used data block is full. dataBlock = null; } if ((object)dataBlock == null) { // Look for the first unallocated data block. dataBlock = FindDataBlock(-1); if ((object)dataBlock == null) { // Extend the file for historic writes only. if (m_parent.FileType == ArchiveFileType.Historic) { Extend(); dataBlock = m_dataBlockPointers[m_dataBlockPointers.Count - 1].DataBlock; } } else { // Reset the unallocated data block if there is data in it. if (dataBlock.SlotsUsed > 0) dataBlock.Reset(); } } // Get the pointer to the data block so that its information can be updated if necessary. if ((object)dataBlock == null) { dataBlockPointer = null; } else { lock (m_dataBlockPointers) { dataBlockPointer = m_dataBlockPointers[dataBlock.Index]; } } } if ((object)dataBlockPointer != null && !dataBlockPointer.IsAllocated) { // Mark the data block as allocated. dataBlockPointer.HistorianID = historianID; dataBlockPointer.StartTime = dataTime; // Set the file start time if not set. if (m_fileStartTime == TimeTag.MinValue) m_fileStartTime = dataTime; // Persist data block information to disk. lock (m_parent.FileData) { // We'll write information about the just allocated data block to the file. m_parent.FileData.Seek(DataLength + ArrayDescriptorLength + (dataBlockPointer.Index * ArchiveDataBlockPointer.FixedLength), SeekOrigin.Begin); dataBlockPointer.CopyBinaryImageToStream(m_parent.FileData); // We'll also write the fixed part of the FAT data that resides at the end. m_parent.FileData.Seek(-m_fixedTableRegion.BinaryLength, SeekOrigin.End); // Copy generated binary image to stream m_fixedTableRegion.CopyBinaryImageToStream(m_parent.FileData); if (!m_parent.CacheWrites) m_parent.FileData.Flush(); } // Re-fetch the data block with updated information after allocation. dataBlock = dataBlockPointer.DataBlock; } return dataBlock; } #endregion } }