//******************************************************************************************************
// BinaryImageParserBase.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/12/2007 - J. Ritchie Carroll
// Generated original version of source code.
// 08/07/2009 - Josh L. Patterson
// Edited Comments.
// 09/14/2009 - Stephen C. Wills
// Added new header and license agreement.
// 11/23/2011 - J. Ritchie Carroll
// Modified to support buffer optimized ISupportBinaryImage.
// 12/14/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.ComponentModel;
using System.IO;
using System.Text;
using GSF.Units;
namespace GSF.Parsing
{
///
/// This class defines the fundamental functionality for parsing any stream of binary data.
///
///
/// This parser is designed as a write-only stream such that data can come from any source.
///
public abstract class BinaryImageParserBase : Stream, IBinaryImageParser
{
#region [ Members ]
///
/// Specifies the default value for the property.
///
public static readonly byte[] DefaultProtocolSyncBytes = new byte[] { 0xAA };
// Events
///
/// Occurs when data image fails deserialized due to an exception.
///
///
/// is the remaining portion of the binary image that failed to parse.
///
public event EventHandler> DataDiscarded;
///
/// Occurs when an is encountered while attempting to parse data.
///
///
/// is the encountered while parsing data.
///
public event EventHandler> ParsingException;
///
/// Occurs when buffer parsing has completed.
///
public event EventHandler BufferParsed;
// Fields
///
/// Tracks if data stream has been initialized.
///
///
/// Only relevant if is true.
///
protected bool StreamInitialized;
///
/// Remaining unparsed buffer from last parsing execution, if any.
///
protected byte[] UnparsedBuffer;
private byte[] m_protocolSyncBytes;
private long m_buffersProcessed;
private string m_name;
private Ticks m_startTime;
private Ticks m_stopTime;
private bool m_enabled;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
protected BinaryImageParserBase()
{
m_protocolSyncBytes = DefaultProtocolSyncBytes;
m_name = this.GetType().Name;
}
#endregion
#region [ Properties ]
///
/// Gets or sets a boolean value that indicates whether the data parser is currently enabled.
///
///
/// Setting to true will start the if it is not started,
/// setting to false will stop the if it is started.
///
public virtual bool Enabled
{
get
{
return m_enabled;
}
set
{
if (value && !m_enabled)
Start();
else if (!value && m_enabled)
Stop();
}
}
///
/// Gets flag that determines if this protocol parsing implementation uses synchronization bytes.
///
public abstract bool ProtocolUsesSyncBytes
{
get;
}
///
/// Gets or sets synchronization bytes for this parsing implementation, if used.
///
public virtual byte[] ProtocolSyncBytes
{
get
{
return m_protocolSyncBytes;
}
set
{
m_protocolSyncBytes = value;
}
}
///
/// Gets the total amount of time, in seconds, that the has been active.
///
public Time RunTime
{
get
{
Ticks processingTime = 0;
if (m_startTime > 0)
{
if (m_stopTime > 0)
processingTime = m_stopTime - m_startTime;
else
processingTime = DateTime.Now.Ticks - m_startTime;
}
if (processingTime < 0)
processingTime = 0;
return processingTime.ToSeconds();
}
}
///
/// Gets the total number of buffer images processed so far.
///
/// Total number of buffer images processed so far.
public long TotalProcessedBuffers
{
get
{
return m_buffersProcessed;
}
}
///
/// Gets a value indicating whether the current stream supports reading.
///
///
/// The is implemented as a WriteOnly stream, so this defaults to false.
///
public override bool CanRead
{
get
{
return false;
}
}
///
/// Gets a value indicating whether the current stream supports seeking.
///
///
/// The is implemented as a WriteOnly stream, so this defaults to false.
///
public override bool CanSeek
{
get
{
return false;
}
}
///
/// Gets a value indicating whether the current stream supports writing.
///
///
/// The is implemented as a WriteOnly stream, so this defaults to true.
///
public override bool CanWrite
{
get
{
return true;
}
}
///
/// Gets current status of .
///
public virtual string Status
{
get
{
StringBuilder status = new StringBuilder();
status.Append(" Current parser state: ");
status.Append(m_enabled ? "Active" : "Idle");
status.AppendLine();
if (ProtocolUsesSyncBytes)
{
status.Append("Data synchronization bytes: 0x");
status.Append(ByteEncoding.Hexadecimal.GetString(ProtocolSyncBytes));
status.AppendLine();
}
status.Append(" Total parser run-time: ");
status.Append(RunTime.ToString());
status.AppendLine();
status.Append(" Total buffers processed: ");
status.Append(m_buffersProcessed);
status.AppendLine();
return status.ToString();
}
}
///
/// Gets the name of .
///
public virtual string Name
{
get
{
return m_name;
}
set
{
m_name = value;
}
}
#endregion
#region [ Methods ]
///
/// Start the streaming data parser.
///
public virtual void Start()
{
// Reset statistics
m_buffersProcessed = 0;
m_stopTime = 0;
m_startTime = DateTime.Now.Ticks;
// Initialized state depends whether or not derived class uses a protocol synchronization byte
StreamInitialized = !ProtocolUsesSyncBytes;
m_enabled = true;
}
///
/// Stops the streaming data parser.
///
public virtual void Stop()
{
m_enabled = false;
m_stopTime = DateTime.Now.Ticks;
}
///
/// Parses the object implementing the interface.
///
/// Object to be parsed that implements the interface.
///
/// This function takes the binary image from and writes the buffer to the stream for parsing.
///
public virtual void Parse(ISupportBinaryImage image)
{
int length = image.BinaryLength;
byte[] buffer = new byte[length];
// Generate the binary image
image.GenerateBinaryImage(buffer, 0);
// Write the buffer to the parsing queue
Write(buffer, 0, length);
}
// Stream implementation overrides
///
/// Writes a sequence of bytes onto the stream for parsing.
///
/// An array of bytes. This method copies count bytes from buffer to the current stream.
/// The zero-based byte offset in buffer at which to begin copying bytes to the current stream.
/// The number of bytes to be written to the current stream.
public override void Write(byte[] buffer, int offset, int count)
{
if (m_enabled)
{
// If ProtocolUsesSyncByte is true, first call to write after start will be uninitialized,
// thus the attempt below to "align" data stream to specified ProtocolSyncBytes.
if (StreamInitialized)
{
// Directly parse frame using calling thread (typically communications thread)
ParseBuffer(buffer, offset, count);
}
else
{
// Initial stream may be anywhere in the middle of a frame, so we attempt to locate sync byte(s) to "line-up" data stream
int syncBytesPosition = buffer.IndexOfSequence(ProtocolSyncBytes, offset, count);
if (syncBytesPosition > -1)
{
StreamInitialized = true;
ParseBuffer(buffer, syncBytesPosition, count - (syncBytesPosition - offset));
}
}
// Track total processed buffer images
m_buffersProcessed++;
}
}
///
/// When overridden in a derived class, clears all buffers for this stream and causes any buffered data
/// to be written to the underlying device.
///
public override void Flush()
{
}
#region [ Unimplemented Stream Overrides ]
// The parser is designed as a write only stream - so the following methods do not apply
///
/// The parser is designed as a write only stream, so this method is not implemented.
///
/// Cannot read from WriteOnly stream.
/// Array of s.
/// An value for the offset.
/// An value for the count.
/// An as the number of bytes read. Well. It would, if implemented.
[EditorBrowsable(EditorBrowsableState.Never)]
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException("Cannot read from WriteOnly stream");
}
///
/// The parser is designed as a write only stream, so this method is not implemented.
///
/// WriteOnly stream has no position.
/// A value for the offset.
/// A .
/// Returns a value indicating the point that was sought.
[EditorBrowsable(EditorBrowsableState.Never)]
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException("WriteOnly stream has no position");
}
///
/// The parser is designed as a write only stream, so this method is not implemented.
///
/// WriteOnly stream has no length.
/// A value.
[EditorBrowsable(EditorBrowsableState.Never)]
public override void SetLength(long value)
{
throw new NotImplementedException("WriteOnly stream has no length");
}
///
/// The parser is designed as a write only stream, so this method is not implemented.
///
///
/// WriteOnly stream has no length. Returned value will always be -1.
///
[EditorBrowsable(EditorBrowsableState.Never)]
public override long Length
{
get
{
return -1;
}
}
///
/// The parser is designed as a write only stream, so this method is not implemented.
///
///
/// WriteOnly stream has no position. Returned value will always be -1 and any assigned value will be ignored.
///
[EditorBrowsable(EditorBrowsableState.Never)]
public override long Position
{
get
{
return -1;
}
set
{
}
}
#endregion
// Parse buffer image - user implements protocol specific "ParseFrame" function to extract data from image
private void ParseBuffer(byte[] buffer, int offset, int count)
{
int endOfBuffer = 0;
try
{
// Prepend any left over buffer data from last parse call
if ((object)UnparsedBuffer != null)
{
// Combine remaining buffer from last call and current buffer together as a single image
buffer = UnparsedBuffer.Combine(0, UnparsedBuffer.Length, buffer, offset, count);
offset = 0;
count = buffer.Length;
UnparsedBuffer = null;
}
endOfBuffer = offset + count - 1;
int parsedFrameLength;
// Move through buffer parsing all available frames
while (!(offset > endOfBuffer) && m_enabled)
{
try
{
// Call derived class frame parsing algorithm - this is protocol specific
parsedFrameLength = ParseFrame(buffer, offset, endOfBuffer - offset + 1);
}
catch (Exception ex)
{
// If protocol defines synchronization bytes there's a chance at recovering any unused portion of this buffer,
// otherwise we'll just rethrow the exception and scrap the buffer...
if (ProtocolUsesSyncBytes)
{
// Attempt to locate sync byte(s) after exception to line data stream back up
int syncBytesPosition = buffer.IndexOfSequence(ProtocolSyncBytes, offset + 1, endOfBuffer - offset);
if (syncBytesPosition > -1)
{
// Found the next sync byte(s), pass through malformed frame
parsedFrameLength = syncBytesPosition - offset;
// We'll still let consumer know there was an issue
OnDataDiscarded(buffer.BlockCopy(offset, parsedFrameLength));
OnParsingException(ex);
}
else
{
throw;
}
}
else
{
throw;
}
}
// Returned value represents total bytes of data in the buffer image that were
// parsed. There could still be data remaining in the buffer, but user parsing
// algorithm could have decided that there was not enough buffer available for
// parsing and returned a "zero" - meaning no data was parsed.
if (parsedFrameLength > 0)
{
// If frame was parsed, increment buffer offset by frame length
offset += parsedFrameLength;
}
else
{
// If not, save off remaining buffer to prepend onto next read
UnparsedBuffer = buffer.BlockCopy(offset, endOfBuffer - offset + 1);
break;
}
}
}
catch (Exception ex)
{
// Probable malformed data image, discard data on move on...
StreamInitialized = !ProtocolUsesSyncBytes;
UnparsedBuffer = null;
if (endOfBuffer != 0)
count = endOfBuffer - offset + 1;
OnDataDiscarded(buffer.BlockCopy(offset, count));
OnParsingException(ex);
}
// Notify consumer that buffer parsing is complete
OnBufferParsed();
}
///
/// Protocol specific frame parsing algorithm.
///
/// Buffer containing data to parse.
/// Offset index into buffer that represents where to start parsing.
/// Maximum length of valid data from offset.
/// The length of the data that was parsed.
///
///
/// Implementers can choose to focus on parsing a single frame in the buffer even if there are other frames available in the buffer.
/// Base class will continue to move through buffer on behalf of derived class until all the buffer has been processed. Just return
/// the total amount of data was parsed and the remaining unparsed will be prepended to next received buffer.
///
///
/// Derived implementations should return an integer value that represents the length of the data that was parsed, and zero if not
/// enough data was able to be parsed. Note that exceptions are expensive when parsing fast moving streaming data and a good coding
/// practice for implementations of this method will be to not throw an exception when there is not enough data to parse the data,
/// instead check the property to verify there is enough buffer data to represent the desired image. If
/// there is not enough data to represent the image return zero and base class will prepend buffer onto next incoming set of data.
///
///
/// Because of the expense incurred when an exception is thrown, any exceptions encountered in the derived implementations of this method
/// will cause the current data buffer to be discarded and a event to be raised. Doing this prevents
/// exceptions from being thrown repeatedly for the same data. If your code implementation recognizes a malformed image, you can raise
/// a custom event to indicate this instead of throwing as exception and keep moving through the buffer as an optimization.
///
///
protected abstract int ParseFrame(byte[] buffer, int offset, int length);
///
/// Raises the event.
///
/// Source buffer that contains output that failed to parse.
protected virtual void OnDataDiscarded(byte[] buffer)
{
if ((object)DataDiscarded != null)
DataDiscarded(this, new EventArgs(buffer));
}
///
/// Raises the event.
///
/// The that was encountered during parsing.
protected virtual void OnParsingException(Exception ex)
{
if ((object)ParsingException != null)
ParsingException(this, new EventArgs(ex));
}
///
/// Raises the event.
///
protected virtual void OnBufferParsed()
{
if ((object)BufferParsed != null)
BufferParsed(this, EventArgs.Empty);
}
#endregion
}
}