//****************************************************************************************************** // 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. // //****************************************************************************************************** using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Text; using System.Xml.Linq; using GSF.Reflection; 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; /// /// Creates a new instance of the class. /// /// The object. public ConnectionStringProperty(PropertyInfo propertyInfo) { SettingNameAttribute settingNameAttribute; DefaultValueAttribute defaultValueAttribute; TypeConverterAttribute typeConverterAttribute; Type converterType; PropertyInfo = propertyInfo; Names = propertyInfo.TryGetAttribute(out settingNameAttribute) ? settingNameAttribute.Names : new string[] { propertyInfo.Name }; Required = !propertyInfo.TryGetAttribute(out defaultValueAttribute); DefaultValue = !Required ? defaultValueAttribute.Value : null; if (propertyInfo.TryGetAttribute(out typeConverterAttribute)) { converterType = Type.GetType(typeConverterAttribute.ConverterTypeName); if ((object)converterType != null) Converter = (TypeConverter)Activator.CreateInstance(converterType); } } } // 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()); } } /// /// 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 ConcurrentDictionary s_allPropertiesLookup = new ConcurrentDictionary(); private static ConcurrentDictionary s_explicitPropertiesLookup = new ConcurrentDictionary(); private static 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 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) ? string.Format("{0}={{{1}}}", name, value) : string.Format("{0}={1}", 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(string.Format("Connection string does not define exactly one root element: {0}", 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 ConcurrentDictionary s_connectionStringPropertiesLookup = new ConcurrentDictionary(); private static 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)) .ToArray(); }; #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(string.Format("; {0}={{ {1} }}", 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 string[] { property.Name }; } } }