Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions BackEnd/Data Access/Tables/DefaultTable.vb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ Public Class DefaultTable
Dim fileName As String = _table.GetStringProperty(TableProperty.FileName)
Dim filePath As String = GetFilePath(fileName)

' Validate that the XML file exists before attempting to load
If Not System.IO.File.Exists(filePath) Then
Throw New System.IO.FileNotFoundException($"XML file not found: {filePath}", fileName)
End If

Dim sourceName As String = _table.GetStringProperty(TableProperty.SourceTableName)
If sourceName Is Nothing Then
'JMich Adding support for different rank levels of equipment
Expand Down
65 changes: 56 additions & 9 deletions BackEnd/Data Access/XmlDB.vb
Original file line number Diff line number Diff line change
Expand Up @@ -154,25 +154,61 @@ Public Class XmlDB

Protected Sub EndInit()
ds.AcceptChanges()

' Proactive validation before enforcing constraints
Dim validator As New XmlValidator(ds)
If Not validator.ValidateAll() Then
' Show detailed validation errors
Dim errorMessage As String = validator.GetFormattedErrors()
ErrorHandler.ShowError(errorMessage, "XML Validation Failed", MessageBoxIcon.Error)
ErrorHandler.TriggerFatalError()
Return
End If

Try
ds.EnforceConstraints = True
Catch ex As ConstraintException
ErrorHandler.ShowError("One or more of your files contain invalid data. Please fix the data and restart the editor.", "Error Loading Files", ex)
' This is now a fallback - should rarely be hit due to proactive validation
Dim errMsg As New Text.StringBuilder()
errMsg.AppendLine("Constraint validation failed after proactive checks.")
errMsg.AppendLine("This may indicate a data race condition or complex constraint issue.")
errMsg.AppendLine()
errMsg.AppendLine("Error Details:")
errMsg.AppendLine(ex.Message)
errMsg.AppendLine()

For Each t As DataTable In ds.Tables
If t.HasErrors Then
Dim errStr As New Text.StringBuilder("Details:" & vbCrLf & vbCrLf)
Dim fileName As String = t.GetStringProperty(TableProperty.FileName)
If Not String.IsNullOrEmpty(fileName) Then
errStr.Append("File: " & fileName & vbCrLf)
errMsg.AppendLine("File: " & fileName)
Else
errStr.Append("Table: " & t.TableName & vbCrLf)
errMsg.AppendLine("Table: " & t.TableName)
End If
For i As Integer = 0 To t.GetErrors.GetUpperBound(0)
errStr.Append(vbCrLf & t.GetErrors(i).RowError)

' Enhanced error reporting with row numbers and column info
Dim errorRows As DataRow() = t.GetErrors()
For i As Integer = 0 To Math.Min(errorRows.GetUpperBound(0), 9) ' Limit to 10 errors
Dim row As DataRow = errorRows(i)
Dim rowIndex As Integer = t.Rows.IndexOf(row)
errMsg.AppendLine($" Row {rowIndex + 1}: {row.RowError}")

' Show which columns have errors
For Each col As DataColumn In t.Columns
If Not String.IsNullOrEmpty(row.GetColumnError(col)) Then
errMsg.AppendLine($" Column '{col.ColumnName}': {row.GetColumnError(col)}")
End If
Next
Next
ErrorHandler.ShowError(errStr.ToString, "Error Loading Files", MessageBoxIcon.Exclamation)

If errorRows.Length > 10 Then
errMsg.AppendLine($" ... and {errorRows.Length - 10} more error(s)")
End If
errMsg.AppendLine()
End If
Next

ErrorHandler.ShowError(errMsg.ToString(), "Error Loading Files", MessageBoxIcon.Error, ex)
ErrorHandler.TriggerFatalError()
End Try
ds.EndInit()
Expand Down Expand Up @@ -211,8 +247,19 @@ Public Class XmlDB
End If

If loadData Then
RaiseEvent LoadingTable(Me, fileName)
table.GetTableHandler.LoadData()
Try
RaiseEvent LoadingTable(Me, fileName)
table.GetTableHandler.LoadData()
Catch ex As System.IO.FileNotFoundException
ErrorHandler.ShowError($"Failed to load '{fileName}': File not found.{vbCrLf}{vbCrLf}Path: {ex.FileName}", "File Not Found", MessageBoxIcon.Error, ex)
Throw
Catch ex As Xml.XmlException
ErrorHandler.ShowError($"Failed to load '{fileName}': Invalid XML format.{vbCrLf}{vbCrLf}Line {ex.LineNumber}, Position {ex.LinePosition}: {ex.Message}", "XML Format Error", MessageBoxIcon.Error, ex)
Throw
Catch ex As Exception
ErrorHandler.ShowError($"Failed to load '{fileName}': {ex.Message}{vbCrLf}{vbCrLf}Table: {table.TableName}", "Error Loading Table", MessageBoxIcon.Error, ex)
Throw
End Try
End If

End If
Expand Down
276 changes: 276 additions & 0 deletions BackEnd/Data Access/XmlValidator.vb
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
Imports System.Data
Imports System.Text

Public Class XmlValidator
Private _dataSet As DataSet
Private _validationErrors As New List(Of ValidationError)

Public Sub New(dataSet As DataSet)
_dataSet = dataSet
End Sub

Public ReadOnly Property HasErrors As Boolean
Get
Return _validationErrors.Count > 0
End Get
End Property

Public ReadOnly Property Errors As List(Of ValidationError)
Get
Return _validationErrors
End Get
End ReadOnly Property

''' <summary>
''' Validates all data in the dataset before constraints are enforced
''' </summary>
Public Function ValidateAll() As Boolean
_validationErrors.Clear()

' Check for null/empty required fields
ValidateRequiredFields()

' Check for duplicate primary keys
ValidatePrimaryKeys()

' Check for foreign key violations
ValidateForeignKeys()

' Check for unique constraint violations
ValidateUniqueConstraints()

Return Not HasErrors
End Function

''' <summary>
''' Validates a single table before loading
''' </summary>
Public Function ValidateTable(table As DataTable) As Boolean
Dim startErrorCount = _validationErrors.Count

ValidateTableRequiredFields(table)
ValidateTablePrimaryKeys(table)
ValidateTableForeignKeys(table)
ValidateTableUniqueConstraints(table)

Return _validationErrors.Count = startErrorCount
End Function

Private Sub ValidateRequiredFields()
For Each table As DataTable In _dataSet.Tables
ValidateTableRequiredFields(table)
Next
End Sub

Private Sub ValidateTableRequiredFields(table As DataTable)
For Each col As DataColumn In table.Columns
If Not col.AllowDBNull AndAlso Not col.AutoIncrement Then
For i As Integer = 0 To table.Rows.Count - 1
Dim row As DataRow = table.Rows(i)
If row.RowState <> DataRowState.Deleted AndAlso IsDBNull(row(col)) Then
AddError(table, i, col.ColumnName,
"Required field is empty",
ValidationErrorType.RequiredField)
End If
Next
End If
Next
End Sub

Private Sub ValidatePrimaryKeys()
For Each table As DataTable In _dataSet.Tables
ValidateTablePrimaryKeys(table)
Next
End Sub

Private Sub ValidateTablePrimaryKeys(table As DataTable)
If table.PrimaryKey.Length = 0 Then Return

Dim pkValues As New Dictionary(Of String, Integer)
Dim pkColumns As String = String.Join(", ", table.PrimaryKey.Select(Function(c) c.ColumnName))

For i As Integer = 0 To table.Rows.Count - 1
Dim row As DataRow = table.Rows(i)
If row.RowState = DataRowState.Deleted Then Continue For

Dim keyValue As String = GetPrimaryKeyValue(row)
If pkValues.ContainsKey(keyValue) Then
AddError(table, i, pkColumns,
$"Duplicate primary key value: {keyValue}. First occurrence at row {pkValues(keyValue) + 1}",
ValidationErrorType.DuplicatePrimaryKey)
Else
pkValues.Add(keyValue, i)
End If
Next
End Sub

Private Sub ValidateForeignKeys()
For Each table As DataTable In _dataSet.Tables
ValidateTableForeignKeys(table)
Next
End Sub

Private Sub ValidateTableForeignKeys(table As DataTable)
For Each fk As ForeignKeyConstraint In table.Constraints.OfType(Of ForeignKeyConstraint)()
Dim parentTable As DataTable = fk.RelatedTable
Dim childColumns As String = String.Join(", ", fk.Columns.Select(Function(c) c.ColumnName))
Dim parentColumns As String = String.Join(", ", fk.RelatedColumns.Select(Function(c) c.ColumnName))

For i As Integer = 0 To table.Rows.Count - 1
Dim row As DataRow = table.Rows(i)
If row.RowState = DataRowState.Deleted Then Continue For

' Build foreign key value
Dim fkValues(fk.Columns.Length - 1) As Object
For j As Integer = 0 To fk.Columns.Length - 1
fkValues(j) = row(fk.Columns(j))
Next

' Check if the value exists in parent table
Dim parentRow As DataRow = parentTable.Rows.Find(fkValues)
If parentRow Is Nothing AndAlso Not IsNullForeignKey(fkValues) Then
Dim fkValueStr As String = String.Join(", ", fkValues.Select(Function(v) If(v Is Nothing, "NULL", v.ToString())))
AddError(table, i, childColumns,
$"Foreign key violation: Value '{fkValueStr}' does not exist in table '{parentTable.TableName}' column(s) '{parentColumns}'",
ValidationErrorType.ForeignKeyViolation)
End If
Next
Next
End Sub

Private Sub ValidateUniqueConstraints()
For Each table As DataTable In _dataSet.Tables
ValidateTableUniqueConstraints(table)
Next
End Sub

Private Sub ValidateTableUniqueConstraints(table As DataTable)
For Each uc As UniqueConstraint In table.Constraints.OfType(Of UniqueConstraint)()
If uc.IsPrimaryKey Then Continue For ' Already checked

Dim uniqueValues As New Dictionary(Of String, Integer)
Dim columnNames As String = String.Join(", ", uc.Columns.Select(Function(c) c.ColumnName))

For i As Integer = 0 To table.Rows.Count - 1
Dim row As DataRow = table.Rows(i)
If row.RowState = DataRowState.Deleted Then Continue For

Dim keyValue As String = GetUniqueConstraintValue(row, uc.Columns)
If uniqueValues.ContainsKey(keyValue) Then
AddError(table, i, columnNames,
$"Unique constraint violation: Duplicate value '{keyValue}'. First occurrence at row {uniqueValues(keyValue) + 1}",
ValidationErrorType.UniqueConstraintViolation)
Else
uniqueValues.Add(keyValue, i)
End If
Next
Next
End Sub

Private Function GetPrimaryKeyValue(row As DataRow) As String
Dim values As New List(Of String)
For Each col As DataColumn In row.Table.PrimaryKey
values.Add(If(row(col) Is Nothing, "NULL", row(col).ToString()))
Next
Return String.Join("|", values)
End Function

Private Function GetUniqueConstraintValue(row As DataRow, columns As DataColumn()) As String
Dim values As New List(Of String)
For Each col As DataColumn In columns
values.Add(If(row(col) Is Nothing, "NULL", row(col).ToString()))
Next
Return String.Join("|", values)
End Function

Private Function IsNullForeignKey(values As Object()) As Boolean
For Each v In values
If v Is Nothing OrElse IsDBNull(v) Then Return True
Next
Return False
End Function

Private Sub AddError(table As DataTable, rowIndex As Integer, columnName As String, message As String, errorType As ValidationErrorType)
Dim fileName As String = table.GetStringProperty(TableProperty.FileName)
If String.IsNullOrEmpty(fileName) Then fileName = table.TableName

_validationErrors.Add(New ValidationError() With {
.FileName = fileName,
.TableName = table.TableName,
.RowIndex = rowIndex,
.ColumnName = columnName,
.Message = message,
.ErrorType = errorType
})
End Sub

Public Function GetFormattedErrors() As String
If Not HasErrors Then Return String.Empty

Dim sb As New StringBuilder()
sb.AppendLine("XML Validation Errors Found:")
sb.AppendLine()

' Group errors by file
Dim errorsByFile = _validationErrors.GroupBy(Function(e) e.FileName)

For Each fileGroup In errorsByFile
sb.AppendLine($"File: {fileGroup.Key}")
sb.AppendLine(New String("-"c, 60))

' Group by error type
Dim errorsByType = fileGroup.GroupBy(Function(e) e.ErrorType)

For Each typeGroup In errorsByType
sb.AppendLine($" {GetErrorTypeDescription(typeGroup.Key)} ({typeGroup.Count()} error(s)):")

Dim maxErrors As Integer = 10 ' Limit display to first 10 errors per type
Dim displayCount As Integer = Math.Min(typeGroup.Count(), maxErrors)

For i As Integer = 0 To displayCount - 1
Dim err = typeGroup.ElementAt(i)
sb.AppendLine($" Row {err.RowIndex + 1}, Column '{err.ColumnName}': {err.Message}")
Next

If typeGroup.Count() > maxErrors Then
sb.AppendLine($" ... and {typeGroup.Count() - maxErrors} more error(s)")
End If
sb.AppendLine()
Next
Next

Return sb.ToString()
End Function

Private Function GetErrorTypeDescription(errorType As ValidationErrorType) As String
Select Case errorType
Case ValidationErrorType.RequiredField
Return "Required Field Errors"
Case ValidationErrorType.DuplicatePrimaryKey
Return "Duplicate Primary Key Errors"
Case ValidationErrorType.ForeignKeyViolation
Return "Foreign Key Violations"
Case ValidationErrorType.UniqueConstraintViolation
Return "Unique Constraint Violations"
Case Else
Return "Other Errors"
End Select
End Function
End Class

Public Class ValidationError
Public Property FileName As String
Public Property TableName As String
Public Property RowIndex As Integer
Public Property ColumnName As String
Public Property Message As String
Public Property ErrorType As ValidationErrorType
End Class

Public Enum ValidationErrorType
RequiredField
DuplicatePrimaryKey
ForeignKeyViolation
UniqueConstraintViolation
Other
End Enum