//******************************************************************************************************
// ConnectionStringParser.cs - Gbtc
//
// Copyright © 2013, 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:
// ----------------------------------------------------------------------------------------------------
// 10/14/2013 - Stephen C. Wills
// Generated original version of source code.
// 03/10/2017 - J. Ritchie Carroll
// Added checks for validation attributes.
//
//******************************************************************************************************
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using ExpressionEvaluator;
using GSF.ComponentModel;
using GSF.Reflection;
// ReSharper disable StaticMemberInGenericType
namespace GSF.Configuration
{
///
/// Parses connection strings based on a settings object whose properties
/// are annotated with the .
///
public class ConnectionStringParser
{
#region [ Members ]
// Nested Types
///
/// Stores reflected information from a
/// object used to parse connection strings.
///
protected class ConnectionStringProperty
{
///
/// The object.
///
public PropertyInfo PropertyInfo;
///
/// The type converter used to convert the value
/// of this property to and from a string.
///
public TypeConverter Converter;
///
/// The name of the property as it appears in the connection string.
///
public string[] Names;
///
/// The default value of the property if its value
/// is not explicitly specified in the connection string.
///
public object DefaultValue;
///
/// Indicates whether or not the property is required
/// to be explicitly defined in the connection string.
///
public bool Required;
///
/// Gets all validation attributes that may be applied to the property.
///
public ValidationAttribute[] ValidationAttributes;
///
/// Creates a new instance of the class.
///
/// The object.
///
/// Type registry to use when parsing
/// instances. Set to null for default registry.
///
public ConnectionStringProperty(PropertyInfo propertyInfo, TypeRegistry typeRegistry = null)
{
SettingNameAttribute settingNameAttribute;
DefaultValueAttribute defaultValueAttribute;
DefaultValueExpressionAttribute defaultValueExpressionAttribute;
TypeConverterAttribute typeConverterAttribute;
Type converterType;
PropertyInfo = propertyInfo;
Names = propertyInfo.TryGetAttribute(out settingNameAttribute) ? settingNameAttribute.Names : new[] { propertyInfo.Name };
bool hasDefaultValue = propertyInfo.TryGetAttribute(out defaultValueAttribute);
bool hasDefaultValueExpression = propertyInfo.TryGetAttribute(out defaultValueExpressionAttribute);
Required = !hasDefaultValue && !hasDefaultValueExpression;
if (Required)
{
DefaultValue = null;
}
else
{
if ((object)defaultValueAttribute == null)
{
ValueExpressionParser parser = new ValueExpressionParser(defaultValueExpressionAttribute, propertyInfo);
if ((object)typeRegistry != null)
parser.TypeRegistry = typeRegistry;
DefaultValue = parser.Eval();
}
else
{
DefaultValue = defaultValueAttribute.Value;
}
}
if (propertyInfo.TryGetAttribute(out typeConverterAttribute))
{
converterType = Type.GetType(typeConverterAttribute.ConverterTypeName);
if ((object)converterType != null)
Converter = (TypeConverter)Activator.CreateInstance(converterType);
}
propertyInfo.TryGetAttributes(out ValidationAttributes);
}
}
// Constants
///
/// Default value for the property.
///
public const char DefaultParameterDelimiter = ';';
///
/// Default value for the property.
///
public const char DefaultKeyValueDelimiter = '=';
///
/// Default value for the property.
///
public const char DefaultStartValueDelimiter = '{';
///
/// Default value for the property.
///
public const char DefaultEndValueDelimiter = '}';
///
/// Default value for the property.
///
public const bool DefaultExplicitlySpecifyDefaults = false;
///
/// Default value for the property.
///
public const bool DefaultSerializeUnspecifiedProperties = true;
// Fields
private char m_parameterDelimiter;
private char m_keyValueDelimiter;
private char m_startValueDelimiter;
private char m_endValueDelimiter;
private bool m_explicitlySpecifyDefaults;
private bool m_serializeUnspecifiedProperties;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
public ConnectionStringParser()
{
m_parameterDelimiter = DefaultParameterDelimiter;
m_keyValueDelimiter = DefaultKeyValueDelimiter;
m_startValueDelimiter = DefaultStartValueDelimiter;
m_endValueDelimiter = DefaultEndValueDelimiter;
m_serializeUnspecifiedProperties = DefaultSerializeUnspecifiedProperties;
}
#endregion
#region [ Properties ]
///
/// Gets or sets the parameter delimiter used to
/// separate key-value pairs in the connection string.
///
public char ParameterDelimiter
{
get
{
return m_parameterDelimiter;
}
set
{
m_parameterDelimiter = value;
}
}
///
/// Gets or sets the key-value delimiter used to
/// separate keys from values in the connection string.
///
public char KeyValueDelimiter
{
get
{
return m_keyValueDelimiter;
}
set
{
m_keyValueDelimiter = value;
}
}
///
/// Gets or sets the start value delimiter used to denote the
/// start of a value in the cases where the value contains one
/// of the delimiters defined for the connection string.
///
public char StartValueDelimiter
{
get
{
return m_startValueDelimiter;
}
set
{
m_startValueDelimiter = value;
}
}
///
/// Gets or sets the end value delimiter used to denote the
/// end of a value in the cases where the value contains one
/// of the delimiters defined for the connection string.
///
public char EndValueDelimiter
{
get
{
return m_endValueDelimiter;
}
set
{
m_endValueDelimiter = value;
}
}
///
/// Gets or sets the flag that determines whether to explicitly
/// specify parameter values that match their defaults when
/// serializing settings to a connection string.
///
public bool ExplicitlySpecifyDefaults
{
get
{
return m_explicitlySpecifyDefaults;
}
set
{
m_explicitlySpecifyDefaults = value;
}
}
///
/// Gets or sets the flag that determines whether to include properties which are not
/// annotated with the in the connection string.
///
public bool SerializeUnspecifiedProperties
{
get
{
return m_serializeUnspecifiedProperties;
}
set
{
m_serializeUnspecifiedProperties = value;
}
}
#endregion
#region [ Methods ]
///
/// Serializes the given into a connection string.
///
/// The object whose properties are to be serialized.
/// A connection string containing the serialized properties.
public virtual string ComposeConnectionString(object settingsObject)
{
ConnectionStringProperty[] connectionStringProperties;
Dictionary settings;
// Null objects don't have properties
if (settingsObject == null)
return string.Empty;
// Get the set of properties which are part of the connection string
connectionStringProperties = GetConnectionStringProperties(settingsObject.GetType());
// Create a dictionary of key-value pairs which
// can easily be converted to a connection string
settings = connectionStringProperties
.Select(property => Tuple.Create(property, property.PropertyInfo.GetValue(settingsObject)))
.Where(tuple => tuple.Item2 != null && (m_explicitlySpecifyDefaults || !tuple.Item2.Equals(tuple.Item1.DefaultValue)))
.ToDictionary(tuple => tuple.Item1.Names.First(), tuple => ConvertToString(tuple.Item2, tuple.Item1), StringComparer.CurrentCultureIgnoreCase);
// Convert the dictionary to a connection string and return the result
return settings.JoinKeyValuePairs(m_parameterDelimiter, m_keyValueDelimiter, m_startValueDelimiter, m_endValueDelimiter);
}
///
/// Deserializes the connection string parameters into the given .
///
/// The connection string to be parsed.
/// The object whose properties are to be populated with values from the connection string.
/// is null.
/// A required connection string parameter cannot be found in the connection string.
public virtual void ParseConnectionString(string connectionString, object settingsObject)
{
ConnectionStringProperty[] connectionStringProperties;
Dictionary settings;
string key;
string value;
// Null objects don't have properties
if (settingsObject == null)
throw new ArgumentNullException(nameof(settingsObject), "Unable to parse connection string because settings object is invalid.");
// Get the set of properties which are part of the connection string
connectionStringProperties = GetConnectionStringProperties(settingsObject.GetType());
// If there are no properties, then our work is done
if (connectionStringProperties.Length <= 0)
return;
// Parse the connection string into a dictionary of key-value pairs for easy lookups
settings = connectionString.ParseKeyValuePairs(m_parameterDelimiter, m_keyValueDelimiter, m_startValueDelimiter, m_endValueDelimiter);
foreach (ConnectionStringProperty property in connectionStringProperties)
{
value = string.Empty;
key = property.Names.FirstOrDefault(name => settings.TryGetValue(name, out value));
if ((object)key != null)
property.PropertyInfo.SetValue(settingsObject, ConvertToPropertyType(value, property));
else if (!property.Required)
property.PropertyInfo.SetValue(settingsObject, property.DefaultValue);
else
throw new ArgumentException("Unable to parse required connection string parameter because it does not exist in the connection string.", property.Names.First());
if ((object)property.ValidationAttributes == null)
continue;
object propertyValue = property.PropertyInfo.GetValue(settingsObject);
string propertyName = key ?? property.Names.First();
foreach (ValidationAttribute attr in property.ValidationAttributes)
attr.Validate(propertyValue, propertyName);
}
}
///
/// Gets the set of properties which are part of the connection string.
///
/// The type of the settings object used to look up properties via reflection.
/// The set of properties which are part of the connection string.
protected virtual ConnectionStringProperty[] GetConnectionStringProperties(Type settingsObjectType)
{
return m_serializeUnspecifiedProperties
? s_allPropertiesLookup.GetOrAdd(settingsObjectType, s_allPropertiesFactory)
: s_explicitPropertiesLookup.GetOrAdd(settingsObjectType, s_explicitPropertiesFactory);
}
///
/// Converts the given string value to the type of the given property.
///
/// The string value to be converted.
/// The property used to determine what type to convert to.
/// The given string converted to the type of the given property.
protected virtual object ConvertToPropertyType(string value, ConnectionStringProperty property)
{
return ((object)property.Converter != null)
? property.Converter.ConvertFromString(value)
: value.ConvertToType(property.PropertyInfo.PropertyType);
}
///
/// Converts the given object to a string.
///
/// The object to be converted.
/// The property which defines the type of the object.
/// The object converted to a string.
protected virtual string ConvertToString(object obj, ConnectionStringProperty property)
{
return ((object)property.Converter != null)
? property.Converter.ConvertToString(obj)
: Common.TypeConvertToString(obj);
}
#endregion
#region [ Static ]
// Static Fields
private static readonly ConcurrentDictionary s_allPropertiesLookup = new ConcurrentDictionary();
private static readonly ConcurrentDictionary s_explicitPropertiesLookup = new ConcurrentDictionary();
private static readonly Func s_allPropertiesFactory = t =>
{
SerializeSettingAttribute attribute;
return t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(property => property.CanRead && property.CanWrite)
.Where(property => !property.TryGetAttribute(out attribute) || attribute.Serialize)
.Select(property => new ConnectionStringProperty(property))
.ToArray();
};
private static readonly Func s_explicitPropertiesFactory = t =>
{
SerializeSettingAttribute attribute;
return t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(property => property.CanRead && property.CanWrite)
.Where(property => property.TryGetAttribute(out attribute) && attribute.Serialize)
.Select(property => new ConnectionStringProperty(property))
.ToArray();
};
// Static Methods
///
/// Converts XML to a connection string.
///
/// The root element of the XML to be converted to a connection string.
/// A connection string converted from XML.
public static string ToConnectionString(XElement element)
{
char[] reserved = { ';', '=', '{', '}' };
string name = element.Name.ToString();
string value = element.HasElements
? string.Join("; ", element.Elements().Select(ToConnectionString))
: (string)element;
return value.Any(reserved.Contains)
? $"{name}={{{value}}}"
: $"{name}={value}";
}
///
/// Converts a connection string to XML.
///
/// The connection string to be converted to XML.
/// The connection string does not define exactly one root element.
/// The XML root element converted from the connection string.
public static XElement ToXML(string connectionString)
{
XElement root;
Dictionary settings;
settings = connectionString.ParseKeyValuePairs();
if (settings.Count != 1)
throw new InvalidOperationException($"Connection string does not define exactly one root element: {connectionString}");
root = new XElement(settings.Keys.First());
SetXMLContent(root, settings.Values.First());
return root;
}
private static void SetXMLContent(XElement parent, string connectionStringValue)
{
Dictionary settings;
XElement element;
settings = connectionStringValue.ParseKeyValuePairs();
if (!settings.Any())
{
parent.Value = connectionStringValue;
}
else
{
foreach (KeyValuePair setting in settings)
{
element = new XElement(setting.Key);
SetXMLContent(element, setting.Value);
parent.Add(element);
}
}
}
#endregion
}
///
/// Parses connection strings based on a settings object whose properties
/// are annotated with .
///
///
/// The type of the attribute to search for when determining whether
/// to serialize a property to the connection string.
///
public class ConnectionStringParser : ConnectionStringParser where TParameterAttribute : Attribute
{
#region [ Properties ]
///
/// Redefined to throw an exception. This property has no meaning when
/// property serialization is determined by the existence of the typed parameter.
///
[EditorBrowsable(EditorBrowsableState.Never)]
public new bool SerializeUnspecifiedProperties
{
get
{
return false;
}
set
{
throw new InvalidOperationException("Not implemented");
}
}
#endregion
#region [ Methods ]
///
/// Gets the set of properties which are part of the connection string.
///
/// The type of the settings object used to look up properties via reflection.
/// The set of properties which are part of the connection string.
protected override ConnectionStringProperty[] GetConnectionStringProperties(Type settingsObjectType)
{
return s_connectionStringPropertiesLookup.GetOrAdd(settingsObjectType, s_valueFactory);
}
#endregion
#region [ Static ]
// Static Fields
private static readonly ConcurrentDictionary s_connectionStringPropertiesLookup = new ConcurrentDictionary();
private static TypeRegistry s_typeRegistry;
private static readonly Func s_valueFactory = t =>
{
TParameterAttribute attribute;
return t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(property => property.CanRead && property.CanWrite)
.Where(property => property.TryGetAttribute(out attribute))
.Select(property => new ConnectionStringProperty(property, s_typeRegistry))
.ToArray();
};
// Static Properties
///
/// Gets or sets instance used for evaluating
/// encountered instances of the on modeled
/// connection string values that are marked with .
///
///
/// Accessing this property will create a unique type registry for the current attribute type
/// which will initially contain the values found in
/// the and can be augmented with
/// custom types. Set to null to restore use of the default type registry.
///
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public static TypeRegistry TypeRegistry
{
get
{
return s_typeRegistry ?? (s_typeRegistry = ValueExpressionParser.DefaultTypeRegistry.Clone());
}
set
{
s_typeRegistry = value;
}
}
#endregion
}
///
/// Parses connection strings based on a settings object whose properties are annotated with
/// and .
///
///
/// The type of the attribute to search for when determining whether
/// to serialize a property to the connection string.
///
///
/// The type of the attribute to search for when determining which
/// parameters are to be parsed recursively as connection strings.
///
public class ConnectionStringParser : ConnectionStringParser
where TParameterAttribute : Attribute
where TNestedSettingsAttribute : Attribute
{
///
/// Serializes the given into a connection string.
///
/// The object whose properties are to be serialized.
/// A connection string containing the serialized properties.
public override string ComposeConnectionString(object settingsObject)
{
StringBuilder builder = new StringBuilder();
object nestedSettingsObject;
if (settingsObject == null)
return string.Empty;
builder.Append(base.ComposeConnectionString(settingsObject));
foreach (PropertyInfo property in GetNestedSettingsProperties(settingsObject))
{
nestedSettingsObject = property.GetValue(settingsObject);
if (nestedSettingsObject != null)
builder.Append($"; {GetNames(property).First()}={{ {ComposeConnectionString(nestedSettingsObject)} }}");
}
return builder.ToString().Trim(';', ' ');
}
///
/// Deserializes the connection string parameters into the given .
///
/// The connection string to be parsed.
/// The object whose properties are to be populated with values from the connection string.
/// is null.
/// A required connection string parameter cannot be found in the connection string.
public override void ParseConnectionString(string connectionString, object settingsObject)
{
Dictionary settings;
object nestedSettingsObject;
string nestedSettings;
base.ParseConnectionString(connectionString, settingsObject);
settings = connectionString.ParseKeyValuePairs();
foreach (PropertyInfo property in GetNestedSettingsProperties(settingsObject))
{
nestedSettingsObject = property.GetValue(settingsObject);
nestedSettings = string.Empty;
if (nestedSettingsObject != null)
{
nestedSettings = GetNames(property)
.Where(name => settings.TryGetValue(name, out nestedSettings))
.Select(name => nestedSettings)
.DefaultIfEmpty(string.Empty)
.First();
ParseConnectionString(nestedSettings, nestedSettingsObject);
}
}
}
// Gets a collection of properties from the settings object which represent the nested connection strings
private static PropertyInfo[] GetNestedSettingsProperties(object settingsObject)
{
TNestedSettingsAttribute nestedSettingsAttribute;
return settingsObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(property => property.TryGetAttribute(out nestedSettingsAttribute))
.ToArray();
}
// Gets a collection of names for the given property which can
// be used during parsing or composing of connection strings
private static string[] GetNames(PropertyInfo property)
{
SettingNameAttribute settingNameAttribute;
if (property.TryGetAttribute(out settingNameAttribute))
return settingNameAttribute.Names;
return new[] { property.Name };
}
}
}