Skip to content

Commit e20b62e

Browse files
CopilotbedaHovorka
authored andcommitted
Add lenient XML validation for editor mode (Issue #261)
Implement two-stage parsing strategy that distinguishes between unparseable XML (malformed syntax) and parseable XML with validation errors: - Add XMLContextFactory.createContextLenient() with lenient parsing mode - Stage 1: Try full validation (schema + structural) - Stage 2: On validation failure, retry without schema validation - Return LenientParseResult with parseability status and validation errors - Enhance ValidationDialog with conditional "Open Anyway" button - Dynamic title: "Warning" for parseable errors, "Error" for unparseable - allowOpenAnyway parameter controls button visibility - Always focus Cancel button for conservative UX - Update MenuBar file opening logic with three-tier handling: 1. Valid XML → open directly 2. Parseable with errors → show ValidationDialog with "Open Anyway" 3. Unparseable → block with error message - Add comprehensive test coverage: - XMLContextFactoryLenientTest: 5 edge case tests (empty files, large files, special characters, exception handling, sequential parsing) - ValidationDialogTest: 5 interaction tests (button visibility, title adaptation, dual-button configuration) Enables users to open and fix XML files with validation errors while still blocking malformed/unparseable XML. Improves editor usability for error correction workflows. Test count: 1855 → 1860 (+5 net) Coverage: 38.5% on new code (SonarQube target: ≥80%)
1 parent 10da28f commit e20b62e

5 files changed

Lines changed: 589 additions & 36 deletions

File tree

src/main/kotlin/cz/vutbr/fit/interlockSim/gui/MenuBar.kt

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,20 @@ class MenuBar : JMenuBar() {
3030
/**
3131
* Opens a railway network file from disk into the EDITOR.
3232
*
33-
* Issue #258: Validation behavior for editor mode:
34-
* - EDITOR MODE: Show WARNING only, allow opening files with validation errors
33+
* Issue #258 & Enhancement: Lenient validation behavior for editor mode:
34+
* - EDITOR MODE: Show WARNING for parseable XML with validation errors, allow "Open Anyway"
3535
* (Users need to be able to open broken files to fix them)
36+
* - UNPARSEABLE XML (malformed syntax): BLOCK with error message
3637
* - SIMULATION MODE: BLOCK invalid XML from transforming to simulation context
3738
* (Invalid configurations must not be allowed to run simulations)
38-
* - If file cannot be loaded (malformed/unparseable XML), show simple error
3939
* - Validation will occur on SAVE to prevent creating invalid files
4040
*
41-
* Current implementation: If XML is malformed (unparseable), shows error and
42-
* doesn't open. For future enhancement, could show ValidationDialog with warnings
43-
* for validation errors but still allow editing.
41+
* Implementation (2026-02-06):
42+
* 1. Try lenient parsing via XMLContextFactory.createContextLenient()
43+
* 2. If unparseable (malformed XML): Show error dialog and block
44+
* 3. If parseable but has validation errors: Show ValidationDialog with "Open Anyway"
45+
* 4. If user clicks "Open Anyway": Load the context into editor for fixing
46+
* 5. If no errors: Load context directly
4447
*/
4548
private inner class OpenAction : AbstractAction("Open...") {
4649
override fun actionPerformed(e: ActionEvent) {
@@ -53,24 +56,58 @@ class MenuBar : JMenuBar() {
5356
val selectedFile: File = fileChooser.selectedFile
5457

5558
try {
56-
// Try to load the context from the selected file
57-
val editingContextFactory = getKoin().get<EditingContextFactory>()
58-
val context = editingContextFactory.createContext(selectedFile)
59+
// Use lenient parsing to separate unparseable XML from validation errors
60+
val editingContextFactory = getKoin().get<cz.vutbr.fit.interlockSim.xml.XMLContextFactory>()
61+
val parseResult = editingContextFactory.createContextLenient(selectedFile)
5962

60-
// Success - update the frame with the loaded context
61-
val frame = getKoin().get<Frame>()
62-
frame.setContext(context)
63+
when {
64+
// Case 1: Successfully parsed with no errors
65+
parseResult.isParseable && parseResult.validationResult.isValid -> {
66+
val context = parseResult.context!!
67+
val frame = getKoin().get<Frame>()
68+
frame.setContext(context)
69+
frame.modificationTracker.setCurrentFile(selectedFile)
70+
frame.modificationTracker.markClean()
71+
}
6372

64-
// Update modification tracker with loaded file
65-
frame.modificationTracker.setCurrentFile(selectedFile)
66-
frame.modificationTracker.markClean()
73+
// Case 2: Parseable but has validation errors - show ValidationDialog with "Open Anyway"
74+
parseResult.isParseable && !parseResult.validationResult.isValid -> {
75+
val context = parseResult.context!!
76+
val dialogResult =
77+
ValidationDialog.show(
78+
this@MenuBar,
79+
parseResult.validationResult,
80+
selectedFile,
81+
allowOpenAnyway = true
82+
)
83+
84+
if (dialogResult == ValidationDialog.DialogResult.OPEN_ANYWAY) {
85+
// User chose to open anyway - load context
86+
val frame = getKoin().get<Frame>()
87+
frame.setContext(context)
88+
frame.modificationTracker.setCurrentFile(selectedFile)
89+
frame.modificationTracker.markClean()
90+
}
91+
// If CANCEL, do nothing (file remains closed)
92+
}
93+
94+
// Case 3: Unparseable XML (malformed syntax) - show error and block
95+
else -> {
96+
JOptionPane.showMessageDialog(
97+
this@MenuBar,
98+
"Cannot open file: The XML is malformed and cannot be parsed.\n\n" +
99+
"Please check the file for syntax errors (missing tags, invalid characters, etc.).",
100+
"Unparseable XML",
101+
JOptionPane.ERROR_MESSAGE
102+
)
103+
}
104+
}
67105
} catch (exception: Exception) {
68-
// Issue #258: Failed to load file - show simple error, don't block with validation dialog
69-
// This allows the editor to remain open so user can create a new file or try another file
106+
// Unexpected error during parsing
70107
JOptionPane.showMessageDialog(
71108
this@MenuBar,
72109
"Failed to open file: ${exception.message}\n\n" +
73-
"The file may be malformed or contain invalid data.",
110+
"An unexpected error occurred while loading the file.",
74111
"Cannot Open File",
75112
JOptionPane.ERROR_MESSAGE
76113
)

src/main/kotlin/cz/vutbr/fit/interlockSim/gui/ValidationDialog.kt

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ import javax.swing.WindowConstants
3434
* - BEFORE: ValidationDialog shown when opening files → blocked opening invalid files
3535
* - AFTER: ValidationDialog shown only on SAVE → warns before saving invalid files
3636
*
37-
* Current Usage Pattern:
38-
* - EDITOR MODE: Opening files with errors shows simple error dialog (not this)
39-
* - Unparseable XML (malformed): Shows error, doesn't open
40-
* - Future: Parseable XML with warnings: Allow opening (show this dialog with warnings)
37+
* Current Usage Pattern (2026-02-06 Update):
38+
* - EDITOR MODE: Opening files with errors shows ValidationDialog with "Open Anyway" option
39+
* - Unparseable XML (malformed): Shows error, blocks opening
40+
* - Parseable XML with validation warnings: Shows dialog, allows "Open Anyway"
4141
* - SIMULATION MODE: Transformation to simulation blocks invalid XML
4242
* - SAVE OPERATION: Show this dialog as WARNING before saving (TODO: implement)
4343
*
@@ -64,16 +64,23 @@ import javax.swing.WindowConstants
6464
* @param parent Parent component for modal dialog
6565
* @param validationResult Result of validation containing errors/warnings
6666
* @param filePath Optional file path being validated
67+
* @param allowOpenAnyway If true, shows "Open Anyway" button alongside Cancel
6768
*/
6869
class ValidationDialog(
6970
parent: Component?,
7071
private val validationResult: ValidationResult,
71-
private val filePath: File? = null
72+
private val filePath: File? = null,
73+
private val allowOpenAnyway: Boolean = false
7274
) : JDialog() {
7375
private var userChoice: DialogResult = DialogResult.CANCEL
7476

7577
init {
76-
title = "⚠ Validation Error"
78+
title =
79+
if (allowOpenAnyway) {
80+
"⚠ Validation Warning"
81+
} else {
82+
"⚠ Validation Error"
83+
}
7784
isModal = true
7885
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
7986

@@ -179,9 +186,24 @@ class ValidationDialog(
179186
}
180187
}
181188

189+
// Open Anyway button (only if allowed)
190+
if (allowOpenAnyway) {
191+
val openAnywayButton =
192+
JButton("Open Anyway").apply {
193+
addActionListener {
194+
userChoice = DialogResult.OPEN_ANYWAY
195+
dispose()
196+
}
197+
// Make this button stand out as the action button
198+
isFocusPainted = true
199+
}
200+
panel.add(openAnywayButton)
201+
}
202+
182203
panel.add(cancelButton)
183204

184-
// Focus on Cancel button by default
205+
// Focus on appropriate button by default
206+
// Always focus Cancel to make user make a conscious choice
185207
cancelButton.requestFocusInWindow()
186208

187209
return panel
@@ -202,7 +224,13 @@ class ValidationDialog(
202224
/**
203225
* User clicked Cancel or closed the dialog.
204226
*/
205-
CANCEL
227+
CANCEL,
228+
229+
/**
230+
* User clicked "Open Anyway" to proceed despite validation errors.
231+
* Only available when allowOpenAnyway is true.
232+
*/
233+
OPEN_ANYWAY
206234
}
207235

208236
companion object {
@@ -212,14 +240,16 @@ class ValidationDialog(
212240
* @param parent Parent component for modal dialog
213241
* @param validationResult Result of validation
214242
* @param filePath Optional file path being validated
243+
* @param allowOpenAnyway If true, shows "Open Anyway" button alongside Cancel
215244
* @return User's choice from the dialog
216245
*/
217246
fun show(
218247
parent: Component?,
219248
validationResult: ValidationResult,
220-
filePath: File? = null
249+
filePath: File? = null,
250+
allowOpenAnyway: Boolean = false
221251
): DialogResult {
222-
val dialog = ValidationDialog(parent, validationResult, filePath)
252+
val dialog = ValidationDialog(parent, validationResult, filePath, allowOpenAnyway)
223253
return dialog.showDialog()
224254
}
225255
}

0 commit comments

Comments
 (0)