mattcasters commented on PR #8247:
URL: https://github.com/apache/hop/pull/8247#issuecomment-5531070231

   Hi @leehaut,
   
   Thank you for investigating this issue! You did great detective work 
identifying the two root causes behind why pipelines and workflows were getting 
marked dirty after simply clicking OK:
   1. The copy factories inadvertently flipping `wrapperChanged` via setter 
calls like `setLocation`.
   2. The asymmetry between `null` and `""` across dialog round-trips.
   
   The fixes to `DefaultTransformMetaCopyFactory`, `DefaultActionCopyFactory`, 
and preserving the live `alreadyChanged` flag in the delegates are spot-on. 
Those should definitely be merged.
   
   However, I have some strong reservations regarding the additions to 
`XmlHandler` (`sameContentIgnoringEmptyValues`) and its usage in 
`XmlSnapshotUndo`:
   
   ---
   
   ### 1. Performance Impact on the UI Thread
   In `XmlSnapshotUndo.sameXmlContent`, this comparison is run on 
`commitDialogUndo` for the **entire pipeline or workflow XML**.
   
   Calling `XmlHandler.sameContentIgnoringEmptyValues` means:
   - Decompressing the entire pipeline snapshot twice.
   - Parsing both complete pipeline XML documents into full W3C DOM Document 
trees (`wrapLoadXmlString`) via `DocumentBuilder.parse()`.
   - Recursively walking every single node, collecting child lists, and 
trimming text on the SWT UI thread.
   - `applyDirtyFlag` then repeats this check comparing against 
`lastSavedSnapshot`.
   
   For large pipelines (dozens or hundreds of transforms, hops, notes, and 
connections), running full DOM tree builds and recursive comparisons on the UI 
thread whenever a dialog closes introduces significant latency and heavy GC 
churn. The snapshot undo system was specifically designed to use fast, 
single-pass string comparison: `decompress(left).equals(decompress(right))`.
   
   ---
   
   ### 2. Edge Cases and Potential Data Loss in the XML Comparator
   Implementing a custom XML equivalence crawler introduces subtle bugs:
   - **Mixed content / text dropped:** In `sameElementIgnoringEmptyValues`, if 
an element has child elements (`!leftChildren.isEmpty()`), `directText(left)` 
is never checked. Any direct text on that parent element is ignored in 
comparison.
   - **False equality on list / table rows:** In `extraEmptyListItemIsIgnored`, 
`<fields><field><name>a</name></field></fields>` is considered equal to 
`<fields><field><name>a</name></field><field/></fields>`. If a user adds a new 
blank row in a table/grid, or clears all values in a row, the comparator 
considers it unchanged. The file will not be marked dirty, no undo point will 
be created, and the edit won't be saved.
   - **Layering:** `XmlHandler` is a low-level XML utility class. It shouldn't 
contain domain-specific heuristics where omitted elements ≡ empty elements. In 
standard XML, an omitted element and an empty element can have very different 
meanings.
   
   ---
   
   ### 3. Suggested Alternative Approach
   The core reason `before.getXml().equals(after.getXml())` failed is:
   - An uninitialized field was `null` → `XmlMetadataUtil` omitted the tag.
   - The dialog opened, the text widget showed `""`, and on OK the dialog set 
the field to `""`.
   - `XmlMetadataUtil` serialized `""` as `<tag/>`.
   
   In Hop, empty strings and `null` for text properties are semantically the 
same (empty/unset). Hop XML files generally don't need or want empty tags like 
`<tag/>` cluttering the document.
   
   Rather than parsing DOM trees after the fact to paper over this difference:
   1. **In `XmlMetadataUtil`:** If we don't serialize empty strings (treating 
`""` like `null` in `serializeFieldValueToXml`):
      ```java
      if (value != null && !(value instanceof String s && s.isEmpty()))
      ```
      *(or alternatively in `GuiCompositeWidgets` / dialog binding by not 
overwriting an existing `null` value with `""` if the control is empty)*
   2. Then `before.getXml()` and `after.getXml()` will produce **byte-for-byte 
identical XML**.
   3. `XmlSnapshotUndo` can revert back to the simple, fast 
`decompress(left).equals(decompress(right))`.
   
   What do you think about separating the copy-factory fixes from the XML 
comparison, and handling the empty-string serialization at the source instead?
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to