Imports System.IO Imports System.Reflection Imports System.Xml.Serialization Imports DigitalData.Modules.Logging Imports DigitalData.Modules.Encryption Imports DigitalData.Modules.Config.ConfigAttributes Public Class ConfigManager(Of T) Private Const USER_CONFIG_NAME As String = "UserConfig.xml" Private Const COMPUTER_CONFIG_NAME As String = "ComputerConfig.xml" Private Const APP_CONFIG_NAME As String = "AppConfig.xml" Private ReadOnly _LogConfig As LogConfig Private ReadOnly _Logger As Logger Private ReadOnly _File As Filesystem.File Private ReadOnly _UserDirectory As String Private ReadOnly _UserConfigPath As String Private ReadOnly _ComputerDirectory As String Private ReadOnly _ComputerConfigPath As String Private ReadOnly _AppConfigDirectory As String Private ReadOnly _AppConfigPath As String Private ReadOnly _TestMode As Boolean = False Private ReadOnly _Blueprint As T Private ReadOnly _BlueprintType As Type Private ReadOnly _Serializer As XmlSerializer Private ReadOnly _ExcludedAttributes = New List(Of Type) From { GetType(ConnectionStringAttribute), GetType(ConnectionStringAppServerAttribute), GetType(ConnectionStringTestAttribute), GetType(EDMIAppServerAttribute), GetType(GlobalSettingAttribute) } Private ReadOnly _ConnectionStringAttributes = New List(Of Type) From { GetType(ConnectionStringAttribute), GetType(ConnectionStringAppServerAttribute), GetType(ConnectionStringTestAttribute) } ''' ''' Signals that all properties will be written to (and read from) the UserConfig.xml ''' ''' If Value is `True`: ''' - AppConfig.xml does NOT exist ''' - ComputerConfig.xml does NOT exist ''' - ConnectionStrings will be saved to or read from UserConfig.xml ''' ''' If Value is `False`: ''' - No ConnectionStrings will be saved to or read from UserConfig.xml ''' ''' Can be overwritten by optional parameter `ForceUserConfig` ''' Private _WriteAllValuesToUserConfig As Boolean = False ''' ''' Returns the currently loaded config object ''' ''' Public ReadOnly Property Config As T ''' ''' Path to the current user config. ''' ''' Public ReadOnly Property UserConfigPath As String Get Return _UserConfigPath End Get End Property ''' ''' Path to the current computer config. ''' ''' Public ReadOnly Property ComputerConfigPath As String Get Return _ComputerConfigPath End Get End Property ''' ''' Path to the current Application config. ''' ''' Public ReadOnly Property AppConfigPath As String Get Return _AppConfigPath End Get End Property ''' ''' Creates a new instance of the ConfigManager ''' ''' ''' LogConfig instance ''' The path to check for a user config file, eg. AppData (Usually Application.UserAppDataPath or Application.LocalUserAppDataPath) ''' The path to check for a computer config file, eg. ProgramData (Usually Application.CommonAppDataPath) ''' The path to check for a third config file. This is useful when running the Application in an environment where AppData/ProgramData directories are not available ''' Override values from ComputerConfig with UserConfig Public Sub New(LogConfig As LogConfig, UserConfigPath As String, ComputerConfigPath As String, Optional ApplicationStartupPath As String = "", Optional ForceUserConfig As Boolean = False) _LogConfig = LogConfig _Logger = LogConfig.GetLogger() _File = New Filesystem.File(_LogConfig) _Blueprint = Activator.CreateInstance(Of T) _BlueprintType = _Blueprint.GetType _Serializer = New XmlSerializer(_BlueprintType) _UserDirectory = _File.CreateDirectory(UserConfigPath) _UserConfigPath = Path.Combine(_UserDirectory, USER_CONFIG_NAME) If ComputerConfigPath <> String.Empty Then If IO.File.Exists(ComputerConfigPath) Then _ComputerDirectory = _File.CreateDirectory(ComputerConfigPath, False) Else _ComputerDirectory = ComputerConfigPath End If _ComputerConfigPath = Path.Combine(_ComputerDirectory, COMPUTER_CONFIG_NAME) End If If ApplicationStartupPath <> String.Empty Then _Logger.Info($"AppConfig is being used: [{ApplicationStartupPath}]") _AppConfigPath = Path.Combine(ApplicationStartupPath, APP_CONFIG_NAME) End If _WriteAllValuesToUserConfig = ForceUserConfig _Config = LoadConfig() End Sub ''' ''' Creates a new ConfigManager with a single (user)config path ''' ''' LogConfig instance ''' The path to check for a user config file, eg. AppData (Usually Application.UserAppDataPath or Application.LocalUserAppDataPath) Public Sub New(LogConfig As LogConfig, ConfigPath As String) MyClass.New(LogConfig, ConfigPath, String.Empty, String.Empty, ForceUserConfig:=True) End Sub ''' ''' Save the current config object to `UserConfigPath` ''' ''' Force saving all attributes including the attributes marked as excluded ''' True if save was successful, False otherwise Public Function Save(Optional ForceAll As Boolean = False) As Boolean Try WriteToFile(Config, _UserConfigPath, ForceAll) Return True Catch ex As Exception _Logger.Error(ex) Return False End Try End Function ''' ''' Reloads the config object from file. ''' ''' True if reload was successful, False otherwise Public Function Reload() As Boolean Try _Config = LoadConfig() Return True Catch ex As Exception _Logger.Error(ex) Return False End Try End Function ''' ''' Copies all properties from Source to Target, except those who have an attribute ''' listed in ExcludedAttributeTypes ''' ''' Source config object ''' Target config object ''' List of Attribute type to exclude Private Sub CopyValues(Source As T, Target As T, Optional ExcludedAttributeTypes As List(Of Type) = Nothing) Dim oType As Type = GetType(T) Dim oExcludedAttributeTypes = IIf(IsNothing(ExcludedAttributeTypes), New List(Of Type), ExcludedAttributeTypes) Dim oProperties = oType.GetProperties(). Where(Function(p) p.CanRead And p.CanWrite). Where(Function(p) For Each oAttributeType As Type In oExcludedAttributeTypes If Attribute.IsDefined(p, oAttributeType) Then Return False End If Next Return True End Function) For Each oProperty As PropertyInfo In oProperties ' TODO: Process individual Subfields of class-objects ' to allow for the PasswordAttribute to be set on class properies aka nested properties Dim oValue = oProperty.GetValue(Source, Nothing) If Not IsNothing(oValue) Then oProperty.SetValue(Target, oValue, Nothing) End If Next End Sub ''' ''' Filters a config object by copying all values except `ExcludedAttributeTypes` ''' ''' Config object ''' List of Attribute type to exclude ''' Private Function FilterValues(ByVal Data As T, ExcludedAttributeTypes As List(Of Type)) As T Dim oResult As T = Activator.CreateInstance(Of T) CopyValues(Data, oResult, ExcludedAttributeTypes) Return oResult End Function Private Function LoadConfig() As T ' first create an empty/default config object Dim oConfig As T = Activator.CreateInstance(_BlueprintType) ' try to load the special app config oConfig = LoadAppConfig(oConfig) ' try to load the computer config oConfig = LoadComputerConfig(oConfig) ' now try to load userconfig oConfig = LoadUserConfig(oConfig) Return oConfig End Function Private Function LoadAppConfig(ByVal Config As T) As T If Not String.IsNullOrEmpty(_AppConfigPath) AndAlso File.Exists(_AppConfigPath) Then Try Dim oAppConfig = ReadFromFile(_AppConfigPath) CopyValues(oAppConfig, Config) _Logger.Info("AppConfig exists and will be used. [{0}]", _AppConfigPath) Catch ex As Exception _Logger.Error(ex) _Logger.Warn("ApplicationConfig could not be loaded!") End Try _WriteAllValuesToUserConfig = False Else _Logger.Debug("ApplicationConfig does not exist.") _WriteAllValuesToUserConfig = True End If Return Config End Function Private Function LoadComputerConfig(ByVal Config As T) As T If _WriteAllValuesToUserConfig = False Then _Logger.Info("AppConfig exists. ComputerConfig will NOT be used") ElseIf File.Exists(_ComputerConfigPath) Then Try Dim oComputerConfig = ReadFromFile(_ComputerConfigPath) CopyValues(oComputerConfig, Config) _Logger.Info("ComputerConfig exists and will be used. [{0}]", _ComputerConfigPath) Catch ex As Exception _Logger.Error(ex) _Logger.Warn("Computer config could not be loaded!") End Try _WriteAllValuesToUserConfig = False Else _Logger.Debug("Computer config does not exist.") _WriteAllValuesToUserConfig = True End If Return Config End Function Private Function LoadUserConfig(ByVal Config As T) As T If File.Exists(_UserConfigPath) Then Try Dim oUserConfig = ReadFromFile(_UserConfigPath) _Logger.Debug("UserConfig exists and will be used. [{0}]", _UserConfigPath) ' if user config exists If Not IsNothing(oUserConfig) Then ' Copy values from user config to final config If _WriteAllValuesToUserConfig Then CopyValues(oUserConfig, Config, New List(Of Type)) Else CopyValues(oUserConfig, Config, _ExcludedAttributes) End If End If Catch ex As Exception _Logger.Error(ex) _Logger.Warn("User config could not be loaded!") End Try Else _Logger.Debug("User config does not exist. Default config will be created") WriteToFile(Config, _UserConfigPath, False) End If Return Config End Function Private Function TestHasAttribute(Config As T, AttributeType As Type) As Boolean For Each oProperty As PropertyInfo In Config.GetType.GetProperties() If Attribute.IsDefined(oProperty, GetType(ConnectionStringAttribute)) Then Return True End If Next Return False End Function ''' ''' Serialize a config object to byte array ''' ''' ''' Private Function Serialize(Data As T) As Byte() Try _Logger.Debug("Serializing config object") Using oStream = New MemoryStream() _Serializer.Serialize(oStream, Data) Return oStream.ToArray() End Using Catch ex As Exception _Logger.Error(ex) Throw ex End Try End Function ''' ''' Write an object to disk as xml ''' ''' The object to write ''' The file name to write to Private Sub WriteToFile(Data As T, Path As String, ForceAll As Boolean) Try _Logger.Debug("Saving config to: {0}", Path) ' If config was loaded from computer config, ' DO NOT save connection string, etc. to user config If _WriteAllValuesToUserConfig = False And ForceAll = False Then Data = FilterValues(Data, _ExcludedAttributes) End If Dim oBytes = Serialize(Data) Using oFileStream = New FileStream(Path, FileMode.Create, FileAccess.Write) oFileStream.Write(oBytes, 0, oBytes.Length) oFileStream.Flush() End Using Catch ex As Exception _Logger.Warn("Could not save config to {0}", Path) _Logger.Error(ex) Throw ex End Try End Sub ''' ''' Reads an xml from disk and deserializes to object ''' ''' Private Function ReadFromFile(Path As String) As T Try _Logger.Debug("Loading config from: {0}", Path) Dim oConfig As T Using oReader As New StreamReader(Path) oConfig = _Serializer.Deserialize(oReader) End Using ' If oConfig is Nothing, a config file was created but nothing was written to it. ' In this case we need to create oConfig from defaults so we have at least some config object If oConfig Is Nothing Then _Logger.Debug("Config file is valid but empty. Loading default values") oConfig = Activator.CreateInstance(_BlueprintType) End If Return oConfig Catch ex As Exception _Logger.Warn("Could not load config from {0}", Path) _Logger.Error(ex) Throw ex End Try End Function End Class