Modules/Modules.Config/ConfigManager.vb
2022-08-17 16:19:08 +02:00

389 lines
15 KiB
VB.net

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)
}
''' <summary>
''' 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`
''' </summary>
Private _WriteAllValuesToUserConfig As Boolean = False
''' <summary>
''' Returns the currently loaded config object
''' </summary>
''' <returns></returns>
Public ReadOnly Property Config As T
''' <summary>
''' Path to the current user config.
''' </summary>
''' <returns></returns>
Public ReadOnly Property UserConfigPath As String
Get
Return _UserConfigPath
End Get
End Property
''' <summary>
''' Path to the current computer config.
''' </summary>
''' <returns></returns>
Public ReadOnly Property ComputerConfigPath As String
Get
Return _ComputerConfigPath
End Get
End Property
''' <summary>
''' Path to the current Application config.
''' </summary>
''' <returns></returns>
Public ReadOnly Property AppConfigPath As String
Get
Return _AppConfigPath
End Get
End Property
''' <summary>
''' Creates a new instance of the ConfigManager
''' </summary>
''' <seealso cref="ConfigSample"/>
''' <param name="LogConfig">LogConfig instance</param>
''' <param name="UserConfigPath">The path to check for a user config file, eg. AppData (Usually Application.UserAppDataPath or Application.LocalUserAppDataPath)</param>
''' <param name="ComputerConfigPath">The path to check for a computer config file, eg. ProgramData (Usually Application.CommonAppDataPath)</param>
''' <param name="ApplicationStartupPath">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</param>
''' <param name="ForceUserConfig">Override values from ComputerConfig with UserConfig</param>
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
''' <summary>
''' Creates a new ConfigManager with a single (user)config path
''' </summary>
''' <param name="LogConfig">LogConfig instance</param>
''' <param name="ConfigPath">The path to check for a user config file, eg. AppData (Usually Application.UserAppDataPath or Application.LocalUserAppDataPath)</param>
Public Sub New(LogConfig As LogConfig, ConfigPath As String)
MyClass.New(LogConfig, ConfigPath, String.Empty, String.Empty, ForceUserConfig:=True)
End Sub
''' <summary>
''' Save the current config object to `UserConfigPath`
''' </summary>
''' <param name="ForceAll">Force saving all attributes including the attributes marked as excluded</param>
''' <returns>True if save was successful, False otherwise</returns>
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
''' <summary>
''' Reloads the config object from file.
''' </summary>
''' <returns>True if reload was successful, False otherwise</returns>
Public Function Reload() As Boolean
Try
_Config = LoadConfig()
Return True
Catch ex As Exception
_Logger.Error(ex)
Return False
End Try
End Function
''' <summary>
''' Copies all properties from Source to Target, except those who have an attribute
''' listed in ExcludedAttributeTypes
''' </summary>
''' <param name="Source">Source config object</param>
''' <param name="Target">Target config object</param>
''' <param name="ExcludedAttributeTypes">List of Attribute type to exclude</param>
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
''' <summary>
''' Filters a config object by copying all values except `ExcludedAttributeTypes`
''' </summary>
''' <param name="Data">Config object</param>
''' <param name="ExcludedAttributeTypes">List of Attribute type to exclude</param>
''' <returns></returns>
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
''' <summary>
''' Serialize a config object to byte array
''' </summary>
''' <param name="Data"></param>
''' <returns></returns>
Private Function Serialize(Data As T) As Byte()
Try
_Logger.Debug("Serializing config object")
Using oStream = New MemoryStream()
_Serializer.Serialize(oStream, Data)
_Logger.Debug("Object serialized.")
Return oStream.ToArray()
End Using
Catch ex As Exception
_Logger.Error(ex)
Throw ex
End Try
End Function
''' <summary>
''' Write an object to disk as xml
''' </summary>
''' <param name="Data">The object to write</param>
''' <param name="Path">The file name to write to</param>
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
''' <summary>
''' Reads an xml from disk and deserializes to object
''' </summary>
''' <returns></returns>
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