//******************************************************************************************************
// MeasurementKey.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:
// ----------------------------------------------------------------------------------------------------
// 09/02/2010 - J. Ritchie Carroll
// Generated original version of source code.
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Concurrent;
using System.Data;
using System.Threading;
using GSF.Data;
namespace GSF.TimeSeries
{
///
/// Represents a primary key for a measurement.
///
[Serializable]
public class MeasurementKey
{
#region [ Members ]
// Fields
private readonly Guid m_signalID;
private ulong m_id;
private string m_source;
private readonly int m_hashCode;
private readonly int m_runtimeID;
private MeasurementMetadata m_metadata;
#endregion
#region [ Constructors ]
private MeasurementKey(Guid signalID, ulong id, string source)
{
m_signalID = signalID;
m_id = id;
m_source = source;
m_hashCode = base.GetHashCode();
m_runtimeID = Interlocked.Increment(ref s_nextRuntimeID) - 1; // Returns the incremented value. Hints the -1
m_metadata = new MeasurementMetadata(this, null, 0, 1, null);
}
#endregion
#region [ Properties ]
///
/// Gets or sets ID of signal associated with this .
///
public Guid SignalID => m_signalID;
///
/// Gets or sets the numeric ID of this .
///
public ulong ID => m_id;
///
/// Gets or sets the source of this .
///
///
/// This value is typically used to track the archive name in which the measurement, that this represents, is stored.
///
public string Source => m_source;
///
/// A unique ID that is assigned at runtime to identify this instance of .
/// This value will change between life cycles, so it cannot be used to compare
/// instances that are running out of process or in a separate .
///
///
/// Since each is only tied to a single object,
/// this provides another unique identifier that is zero indexed.
/// This allows certain optimizations such as array lookups.
///
public int RuntimeID => m_runtimeID;
///
/// Gets the as they appear in the primary .
///
///
/// This is to be considered the reference value. Adapters are free to change this inside specific instances
/// This value should only be updated upon change in the primary data source using .
///
public MeasurementMetadata Metadata => m_metadata;
#endregion
#region [ Methods ]
///
/// Updates the values of the .
///
/// Gets or sets the text based tag name.
/// Defines an offset to add to the value.
/// Defines a multiplicative offset to apply to the value.
/// Defines the to use when downsampling this type of value.
public void SetMeasurementMetadata(string tagName, double adder, double multiplier, MeasurementValueFilterFunction valueFilter)
{
if (this == Undefined)
throw new NotSupportedException("Cannot set data source information for an undefined measurement.");
if (m_metadata.TagName != tagName || m_metadata.Adder != adder || m_metadata.Multiplier != multiplier || m_metadata.MeasurementValueFilter != valueFilter)
m_metadata = new MeasurementMetadata(this, tagName, adder, multiplier, valueFilter);
}
///
/// Returns a that represents the current .
///
/// A that represents the current .
public override string ToString()
{
return $"{m_source}:{m_id}";
}
///
/// Serves as a hash function for the current .
///
/// A hash code for the current .
public override int GetHashCode()
{
return m_hashCode;
}
#endregion
#region [ Static ]
// Static Fields
// All edits to as well as the ConcurrentDictionaries in s_keyCache should occur within a lock on s_syncEdits
private static readonly ConcurrentDictionary s_idCache = new ConcurrentDictionary();
private static readonly ConcurrentDictionary> s_keyCache = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
private static readonly object s_syncEdits = new object();
private static int s_nextRuntimeID;
///
/// Represents an undefined measurement key.
///
public static readonly MeasurementKey Undefined;
// Static Constructor
static MeasurementKey()
{
// Order of operations is critical here since MeasurementKey and MeasurementMetadata have a circular reference
Undefined = CreateUndefinedMeasurementKey();
MeasurementMetadata.CreateUndefinedMeasurementMetadata();
Undefined.m_metadata = MeasurementMetadata.Undefined;
}
// Static Methods
///
/// Constructs a new given the specified parameters.
///
/// ID of associated signal, if defined.
/// A string representation of the .
/// cannot be empty
/// The value is not in the correct format for a value.
public static MeasurementKey CreateOrUpdate(Guid signalID, string value)
{
string source;
ulong id;
if (!TrySplit(value, out source, out id))
throw new FormatException("The value is not in the correct format for a MeasurementKey value");
return CreateOrUpdate(signalID, source, id);
}
///
/// Constructs a new given the specified parameters.
///
/// ID of associated signal, if defined.
/// Source of the measurement that this represents (e.g., name of archive).
/// Numeric ID of the measurement that this represents.
/// cannot be empty.
/// cannot be null.
public static MeasurementKey CreateOrUpdate(Guid signalID, string source, ulong id)
{
Func addValueFactory;
Func updateValueFactory;
if (signalID == Guid.Empty)
throw new ArgumentException("Unable to update undefined measurement key", nameof(signalID));
if (string.IsNullOrWhiteSpace(source))
throw new ArgumentNullException(nameof(source), "MeasurementKey source cannot be null or empty");
addValueFactory = guid =>
{
// Create a new measurement key and add it to the KeyCache
ConcurrentDictionary idLookup = s_keyCache.GetOrAdd(source, s => new ConcurrentDictionary());
return idLookup[id] = new MeasurementKey(guid, id, source);
};
updateValueFactory = (guid, key) =>
{
ConcurrentDictionary idLookup;
// If existing measurement key is exactly the same as the
// one we are trying to create, simply return that key
if (key.ID == id && key.Source == source)
return key;
// Update source and ID and re-insert it into the KeyCache
key.m_source = source;
key.m_id = id;
idLookup = s_keyCache.GetOrAdd(source, s => new ConcurrentDictionary());
idLookup[id] = key;
return key;
};
// https://msdn.microsoft.com/en-us/library/ee378675(v=vs.110).aspx
//
// If you call AddOrUpdate simultaneously on different threads,
// addValueFactory may be called multiple times, but its key/value
// pair might not be added to the dictionary for every call.
//
// This lock prevents race conditions that might occur in the addValueFactory that
// could cause different MeasurementKey objects to be written to the KeyCache and IDCache
lock (s_syncEdits)
{
return s_idCache.AddOrUpdate(signalID, addValueFactory, updateValueFactory);
}
}
///
/// Constructs a new given the specified parameters.
///
/// ID of associated signal, if defined.
/// A string representation of the .
/// The measurement key that was created or updated or .
/// True if the measurement key was successfully created or updated, false otherwise.
/// cannot be empty.
/// Measurement key Source cannot be null.
public static bool TryCreateOrUpdate(Guid signalID, string value, out MeasurementKey key)
{
try
{
key = CreateOrUpdate(signalID, value);
return true;
}
catch
{
key = Undefined;
return false;
}
}
///
/// Constructs a new given the specified parameters.
///
/// ID of associated signal, if defined.
/// Source of the measurement that this represents (e.g., name of archive).
/// Numeric ID of the measurement that this represents.
/// The measurement key that was created or updated or .
/// True if the measurement key was successfully created or updated, false otherwise.
/// cannot be empty.
/// cannot be null.
public static bool TryCreateOrUpdate(Guid signalID, string source, ulong id, out MeasurementKey key)
{
try
{
key = CreateOrUpdate(signalID, source, id);
return true;
}
catch
{
key = Undefined;
return false;
}
}
///
/// Looks up the measurement key associated with the given signal ID.
///
/// The signal ID of the measurement key.
/// The measurement key associated with the given signal ID.
///
/// If no measurement key is found with the given signal ID,
/// this method returns .
///
public static MeasurementKey LookUpBySignalID(Guid signalID)
{
MeasurementKey key;
// ReSharper disable once InconsistentlySynchronizedField
if (signalID == Guid.Empty)
key = Undefined;
else if (!s_idCache.TryGetValue(signalID, out key))
key = Undefined;
return key;
}
///
/// Looks up the measurement key associated with the given source and ID.
///
/// The source of the signal.
/// The source-specific unique integer identifier.
/// The measurement key associated with the given source and ID.
///
/// If no measurement key is found with the given source and ID,
/// this method returns .
///
public static MeasurementKey LookUpBySource(string source, ulong id)
{
ConcurrentDictionary idLookup;
MeasurementKey key;
if (!s_keyCache.TryGetValue(source, out idLookup))
return Undefined;
if (!idLookup.TryGetValue(id, out key))
return Undefined;
return key;
}
///
/// Performs a lookup by signal ID and, failing that, attempts to create
/// the key using the given signal ID and the parsed source, and ID.
///
/// The signal ID of the key to be looked up.
/// A string representation of the .
///
/// If the lookup succeeds, an existing measurement key with a matching signalID.
/// If creation succeeds, a new measurement key with matching signal ID, source, and ID.
/// Otherwise, .
///
public static MeasurementKey LookUpOrCreate(Guid signalID, string value)
{
string source;
ulong id;
if (!TrySplit(value, out source, out id))
return LookUpOrCreate(signalID, Undefined.Source, Undefined.ID);
return LookUpOrCreate(signalID, source, id);
}
///
/// Performs a lookup by signal ID and, failing that, attempts to
/// create the key using the given signal ID, source, and ID.
///
/// The signal ID of the key to be looked up.
/// The source to use for the key if the lookup fails.
/// The ID to use for the key if the lookup fails.
///
/// If the lookup succeeds, an existing measurement key with a matching signalID.
/// If creation succeeds, a new measurement key with matching signal ID, source, and ID.
/// Otherwise, .
///
public static MeasurementKey LookUpOrCreate(Guid signalID, string source, ulong id)
{
MeasurementKey key = LookUpBySignalID(signalID);
if (key == Undefined && !TryCreateOrUpdate(signalID, source, id, out key))
return Undefined;
return key;
}
///
/// Performs a lookup by source and, failing that, attempts to create
/// the key using a newly generated signal ID and the parsed source and ID.
///
/// A string representation of the .
///
/// If the lookup succeeds, an existing measurement key with a matching signalID.
/// If creation succeeds, a new measurement key with matching signal ID, source, and ID.
/// Otherwise, .
///
public static MeasurementKey LookUpOrCreate(string value)
{
string source;
ulong id;
if (!TrySplit(value, out source, out id))
return Undefined;
return LookUpOrCreate(source, id);
}
///
/// Performs a lookup by source and, failing that, attempts to create
/// the key using a newly generated signal ID and the given source and ID.
///
/// The source to use for the key if the lookup fails.
/// The ID to use for the key if the lookup fails.
///
/// If the lookup succeeds, an existing measurement key with a matching signalID.
/// If creation succeeds, a new measurement key with matching signal ID, source, and ID.
/// Otherwise, .
///
public static MeasurementKey LookUpOrCreate(string source, ulong id)
{
MeasurementKey key = LookUpBySource(source, id);
if (key == Undefined && !TryCreateOrUpdate(Guid.NewGuid(), source, id, out key))
return Undefined;
return key;
}
///
/// Converts the string representation of a into its value equivalent.
///
/// A string representing the to convert.
/// A value equivalent the representation contained in .
/// The value is not in the correct format for a value.
public static MeasurementKey Parse(string value)
{
MeasurementKey key;
if (TryParse(value, out key))
return key;
throw new FormatException("The value is not in the correct format for a MeasurementKey value");
}
///
/// Attempts to convert the string representation of a into its value equivalent.
///
/// A string representing the to convert.
/// Output in which to stored parsed value.
/// A true if representation contained in could be parsed; otherwise false.
public static bool TryParse(string value, out MeasurementKey key)
{
string source;
ulong id;
// Split the input into source and ID
if (TrySplit(value, out source, out id))
{
// First attempt to look up an existing key
key = LookUpBySource(source, id);
if (key == Undefined)
{
try
{
// Lookup failed - attempt to create it with a newly generated signal ID
key = CreateOrUpdate(Guid.NewGuid(), source, id);
}
catch
{
// source is null or empty
key = Undefined;
}
}
}
else
{
// Incorrect format for a measurement key
key = Undefined;
}
return key != Undefined;
}
///
/// Establish default cache.
///
/// The database connection.
/// The database adapter type.
/// Measurement table name used to load measurement key cache.
///
/// Source tables are expected to have at least the following fields:
///
/// ID NVARCHAR Measurement key formatted as: ArchiveSource:PointID
/// SignalID GUID Unique identification for measurement
///
///
public static void EstablishDefaultCache(IDbConnection connection, Type adapterType, string measurementTable = "ActiveMeasurement")
{
string source;
ulong id;
// Establish default measurement key cache
foreach (DataRow measurement in connection.RetrieveData(adapterType, $"SELECT ID, SignalID FROM {measurementTable}").Rows)
{
if (TrySplit(measurement["ID"].ToString(), out source, out id))
CreateOrUpdate(measurement["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), source, id);
}
}
///
/// Creates the undefined measurement key. Used to initialize .
///
private static MeasurementKey CreateUndefinedMeasurementKey()
{
MeasurementKey key = new MeasurementKey(Guid.Empty, ulong.MaxValue, "__");
// Lock on s_syncEdits is not required since method is only called by the static constructor
s_keyCache.GetOrAdd(key.Source, kcf => new ConcurrentDictionary())[ulong.MaxValue] = key;
return key;
}
///
/// Attempts to split the given string representation
/// of a measurement key into a source and ID pair.
///
private static bool TrySplit(string value, out string source, out ulong id)
{
string[] elem;
if (!string.IsNullOrEmpty(value))
{
elem = value.Split(':');
if (elem.Length == 2 && ulong.TryParse(elem[1].Trim(), out id))
{
source = elem[0].Trim();
return true;
}
}
source = Undefined.Source;
id = Undefined.ID;
return false;
}
#endregion
}
}