Repository navigation
[#157] Save the XML document in linear time - #176
maximthomas wants to merge 15 commits into
Conversation
…n integration test
…the XML save walks stay linear
…clarations written by hand
vharseko
left a comment
There was a problem hiding this comment.
I found no correctness problem in the paths the connector actually uses: XMLHandlerImpl parses with entity references expanded, and dispose() runs under ConcurrentXMLHandler's write lock. The points below are two edge cases outside that path, three efficiency items, and two cleanups. I checked them against the PR description and #157 and don't argue with anything decided there: fixing only the save, sending Saxon events instead of using XSLTC, the namespace-order change, and leaving the atomic save to #160.
Edge cases (the connector's own parsing doesn't reach them)
1. normalizeText edits read-only entity-reference children — XmlDocumentWriter.java:95
Under the DOM rules, the children of an EntityReference are read-only. If a DOM is parsed with setExpandEntityReferences(false), as in <!DOCTYPE r [<!ENTITY e '<a/> <b/>'>]><r>&e;</r>, the walk reaches the blank text inside the entity and calls removeChild on it. That throws DOMException (NO_MODIFICATION_ALLOWED_ERR), which dispose()'s catch (TransformerException | SAXException | IOException) doesn't catch. The result: no "Failed saving changes" log line, no ConnectorException.wrap, and the file is never written. write() handles ENTITY_REFERENCE_NODE on purpose (entityReferencesAndDoctypeAreWrittenAsBefore), so normalizeText could skip entity-reference subtrees the same way. Unconfirmed: I haven't reproduced this on Xerces 2.6.2. The JDK parser I tried left the reference with no children.
2. bind() puts DOM level 1 nodes into a namespace the writer invented — XmlDocumentWriter.java:206
Take a parent built with createElementNS("urn:x", "a"), with no prefix and no xmlns attribute, and a child built with createElement("b"). The writer declares xmlns="urn:x" on <a>. For <b>, bind("", null) returns urn:x, so no xmlns="" is written and <b> reads back inside urn:x. The old serializer looked only at real xmlns attributes and left <b> in no namespace. The connector always uses prefixed names, so its own files aren't affected. This only matters to other callers of the writer.
Efficiency
3. A new Saxon factory, and so a new Configuration, on every save — XmlDocumentWriter.java:112
The connector isn't poolable, so nearly every operation ends in dispose() and pays this fixed cost. That matters most for small stores, where #157 measured 45–72 ms per operation. Because dispose() already runs under the write lock, one static factory (or one shared Configuration) would do. The old code had the same cost, but since this line is being rewritten anyway, it's a cheap fix.
4. normalizeText allocates for every text run — XmlDocumentWriter.java:81
Every text run, including the common case of a single TEXT_NODE with no text sibling, gets an ArrayList, a StringBuilder and a copy of its value. At 40,000 entries that is roughly 1M short-lived objects per save. A fast path would avoid most of it: when the node is a TEXT_NODE and getNextSibling() isn't text, check isXmlWhitespace(node.getNodeValue()) directly, and build the list and buffer only for real multi-node or CDATA runs.
5. The CRC-32 and the changed flag are computed but not used — XmlDocumentWriter.java:120
XMLHandlerImpl.dispose() ignores both return values, but every save still runs CRC-32 over the whole file (about 11.7 MB at 40,000 entries). If they are for #177, either add them there or say so here, as the description already does for setModified.
Cleanup
6. synchronized (document) no longer excludes anything — XMLHandlerImpl.java:388
The new comment notes that callers already hold the write lock, and nothing else in the module locks on the document. When init() failed and document is null, this block throws NullPointerException instead of the usual ConnectorException from getDocument(). Dropping the monitor, or going through getDocument(), would fix both.
7. following(node, root) throws NPE when root isn't an ancestor of node — XmlHandlerUtil.java:90
The loop climbs past the Document to null and then calls getNextSibling() on it. The precondition is only in the Javadoc, and this is a public method in a shared util class that #177 adds callers to. With n != null && n != root as the loop condition, misuse returns null instead of crashing.
…one when normalizing
… no namespace, as before
… list or a buffer
…n root is not an ancestor
|
I checked each point on this branch with the module's own Xerces 2.6.2 and Saxon 9.4.0.7. Fixes are in 1f97256..bd2fe56. For point 3 I left the code as it is; the numbers are below. The description has been updated (A6 rows; the CRC note under "Change"). 1. Entity-reference children. Reproduced on Xerces 2.6.2, with deferred and non-deferred DOMs: your 2. 3. A Saxon factory per save. The cost is real but small. On a warm JVM, 4. Allocation in 5. The CRC-32 and 6. 7. #177 is rebased onto bd2fe56. In its commit that loads and saves only on change, the new test from point 6 becomes |
vharseko
left a comment
There was a problem hiding this comment.
I checked the fix round 1f97256..bd2fe56 against the first round and your reply, on Xerces 2.6.2 and Saxon 9.4.0.7. Points 1, 4, 5, 6 and 7 are fixed as described. I accept both corrections in your reply (the write lock is per file; #177 adds no callers of following()), and keeping the factory local for point 3. The lone-Text shortcut builds the same tree as the old general branch for every input I tried. Two things are left, and the first one was introduced in this round.
1. bind() declares a prefix twice on one element, and the file is not well-formed — XmlDocumentWriter.java:205-212
sendElement first declares the element's own xmlns and xmlns:p attributes, then calls bind for the element's name. Since cbbee60, an unprefixed DOM level 1 element (its namespace is null) is bound to "". When that element has its own xmlns attribute with another value, bind declares "" a second time on the same element, and Saxon writes both declarations:
DOM (the parent is createElementNS("urn:x", "a")) |
old DOMSource output |
7a1a5eb | bd2fe56 |
|---|---|---|---|
createElement("b") + setAttribute("xmlns", "urn:y") |
<b xmlns="urn:y"/> |
<b xmlns="urn:y"/> |
<b xmlns="urn:y" xmlns=""/> |
createElement("b") + setAttributeNS(XMLNS, "xmlns", "urn:y") |
<b xmlns="urn:y"/> |
<b xmlns="urn:y"/> |
<b xmlns="urn:y" xmlns=""/> |
createElement("b") + setAttributeNS(XMLNS, "xmlns", "urn:x") |
<b/> |
<b xmlns="urn:x"/> |
<b xmlns="urn:x" xmlns=""/> |
createElementNS("urn:x", "b") + setAttributeNS(XMLNS, "xmlns", "urn:y") |
<b xmlns="urn:y"/> |
<b xmlns="urn:y" xmlns="urn:x"/> |
as 7a1a5eb |
A namespace-aware parser rejects every bd2fe56 cell, and the 7a1a5eb cell of the last row: Attribute "xmlns" bound to namespace "http://www.w3.org/2000/xmlns/" was already specified for element "b". The first three rows are new in cbbee60. The last row has been there since 6f83fa3, and I missed it in the first round. The connector does not build such a DOM: it parses namespace-aware, and its own elements and attributes don't contradict their xmlns attributes. So this is in the same category as point 2 of the first round, but the outcome is worse: instead of an element in the wrong namespace, the result is a file that cannot be loaded back.
A fix that gives the old output: let a declaration from the element's own attributes win, as DOMSource did.
String inScope = namespaces.getURI(prefix);
if (declared.contains(prefix)) {
return inScope == null ? "" : inScope;
}With this fix, rows 1, 2 and 4 are byte-for-byte equal to the old output. Row 3 becomes <b xmlns="urn:x"/>, the kept redeclaration that the description already lists. The module's 139 tests stay green. A case with the element's own xmlns attribute in unprefixedDomLevel1ElementIsInNoNamespace, or a separate test, would pin it.
2. textInsideAnEntityReferenceIsLeftAlone doesn't check that the walk continues after the reference — XmlDocumentWriterTests.java:148
The test kills the mutant in A6 row 1 (Node child = node.getFirstChild();). It does not kill one that stops the walk at the reference: with if (node.getNodeType() == Node.ENTITY_REFERENCE_NODE) { return; } before line 75, all 139 tests stay green, so a document with an unexpanded entity reference would keep all the whitespace after its first reference. Text after the reference would pin it. For example, with <r>&e; <c> </c></r>, assert that the blank Text node after the reference and the one inside c are removed.
|
Both points are fixed in b3fba18 and faf5d15, checked with the module's Xerces 2.6.2 and Saxon 9.4.0.7. The description is updated: the file-format note under "Change", "Tests", and a new A7 table. 1. A prefix declared twice on one element. Reproduced. Your four DOMs give exactly the outputs in your table, and the parser rejects every bd2fe56 cell. I took your rule: a prefix that the element already declares keeps that namespace ( The same defect has two more triggers, both there since 6f83fa3, and the fix covers both:
One difference from the old output remains. For a prefix other than the default one, the old serializer let the node's namespace win over the element's own For the mutation record: the check in 2. The walk after an entity reference. Confirmed: with
|
vharseko
left a comment
There was a problem hiding this comment.
I checked the third round, b3fba18 and faf5d15, against my second review and your reply, with the module's Xerces 2.6.2 and Saxon 9.4.0.7. Both points are fixed as described. The four DOMs from my table and your two extra triggers now give well-formed output, rows 1, 2 and 4 match the old output byte for byte, and textAfterAnEntityReferenceIsNormalized kills the mutant that returns at the first entity reference. I accept letting the xmlns:p attribute win for every prefix instead of copying Saxon's split rule. I also accept that the bind check can't be pinned on its own: with the declare check in place, I get the same bytes without it. Every CI job that has finished is green; the two Windows jobs are still pending.
One claim in the reply doesn't hold, and there's one nit.
1. An attribute moved into the declared namespace can clash with another attribute — XmlDocumentWriter.java:209
The reply says that once the xmlns:p attribute wins for every prefix, "the files are well-formed". That holds for the element, but not for attributes. An attribute moved into the declared namespace can end up with the same expanded name as another attribute of the same element.
DOM: b = createElementNS("urn:r", "b"), then setAttributeNS(XMLNS, "xmlns:p", "urn:x"), setAttributeNS("urn:y", "p:c", "1") and setAttributeNS("urn:x", "q:c", "2").
b in the output |
namespace-aware parse | |
|---|---|---|
old DOMSource |
<b xmlns:p="urn:y" xmlns:q="urn:x" p:c="1" q:c="2"/> |
ok: {urn:y}c, {urn:x}c |
| bd2fe56 | <b xmlns:p="urn:x" xmlns:p="urn:y" xmlns:q="urn:x" p:c="1" q:c="2"/> |
rejected: xmlns:p twice |
| faf5d15 | <b xmlns:p="urn:x" xmlns:q="urn:x" p:c="1" q:c="2"/> |
rejected: Attribute "c" bound to namespace "urn:x" was already specified for element "b" |
This round didn't introduce it: bd2fe56 already wrote an unreadable file here. Still, of the "any other prefix" differences you list, this is the only case I found where the old output was readable and the new one isn't. It's the same category as before: the connector doesn't build such a DOM, and a parsed store can't contain one. The old serializer has the same defect when the clash comes from the element's own prefix. With createElementNS("urn:x", "p:b") + p:c in urn:y + q:c in urn:x, both outputs are <p:b xmlns:p="urn:x" xmlns:q="urn:x" p:c="1" q:c="2"/>. The same holds for two p:c attributes in two namespaces, and in both cases the writer matches the old output byte for byte.
I'm not asking you to change the rule; closing this would need a fresh prefix for the moved attribute, which isn't worth it for such DOMs. A sentence in the description's file-format note is enough. It should say that when the namespace chosen for a prefix gives an attribute the expanded name of another attribute of the same element, the file is not well-formed, as with the old serializer when the clash comes from the element's prefix.
2. Nit: a broken line wrap in bind's Javadoc — XmlDocumentWriter.java:202-203
Line 202 breaks after "A DOM level 1 node has", well before the margin, and the sentence continues on line 203.
With the note in point 1 (or without it, if this thread is record enough), I have nothing else on this PR.
Part of #157
Merge order
This is the first of two PRs for part 1 of #157; the second, #177 ("[#157] Load and save the XML file only when it has changed"), is built on top of this branch. Please merge them one after the other: this PR first, then #177. After this PR is merged, #177 will be rebased onto the new master.
Problem
XMLConnectoris not poolable, so an operation that does not overlap with another one on the same file ends inXMLHandlerImpl.dispose(), which writes the whole document back. That save is quadratic in the number of entries. Measured with 40,000 entries,dispose()alone takes about 7.8 s (see the table below), and in the issue a search for one entry by__NAME__throughConnectorFacadetook 13.5 s at 40,000 entries.The save walks the Xerces DOM twice through Saxon: the XPath
//text()[normalize-space(.) = '']that removes whitespace-only text, andDOMSenderwhenTransformerFactoryImplserializes aDOMSource. Both go throughNodeList.item(i). On Xerces 2.6.2 the document'sNodeListCachefree list can become a self-cycle (freeNodeListCachepushes a cache that is already on the list), after which everyitem(i)on the container restarts from its first child, and a walk over N children costs O(N^2).This PR fixes the save only. Parsing, the lookups (XQuery) and the decision when to read and write the file are not changed here, so a full operation is still slower than linear. The save still truncates the file before it writes it, as before; making it atomic is #160.
Change
XmlDocumentWriter(package-private).normalizeText(Node)removes whitespace-only text and merges every other run of adjacent Text and CDATA siblings into one Text node. Whitespace means space, tab, CR and LF, which is whatnormalize-spacestrips; CDATA counts as text, as it did for the XPath. A lone Text node, the usual case, is checked in place. Text inside an entity reference is left alone, because the DOM makes it read-only; only a DOM parsed with entity references not expanded has such text, and the connector expands them.write(Document, File)walks the DOM with sibling pointers and sends SAX events to the same Saxon serializer (TransformerHandler, same output properties as before). It returns the CRC-32 of the bytes written, which this PR does not use: [#157] Load and save the XML file only when it has changed #177 compares it with the file's content when the file's timestamp is too recent to tell whether the file has changed.NodeList;normalizeTextsteps with the newXmlHandlerUtil.following(node, root)(the next sibling, or the next sibling of the nearest ancestor belowroot; arootthat is not an ancestor acts asnull).XMLHandlerImpl.dispose()callsnormalizeTextandwriteinstead of the XPath and theDOMSourcetransform. It takes the document throughgetDocument()and no longer locks the document's monitor: every caller holdsConcurrentXMLHandler's write lock, and nothing else locks on the document. A handler without a document now fails withgetDocument()'sConnectorExceptioninstead of aNullPointerException; the connector does not reach that case, because a failedinit()is never followed by the handler'sdispose(). A failed save is still an ERROR log line, thenConnectorException. Two cases change: anIOExceptionfrom closing the file was logged as WARN and the save counted as successful, and is now a failed save (A1 row 21), and theXPathExpressionExceptionthat the old code ignored is gone with the XPath. The old closinglog.info("Entry {0}", method)now saysExit.createDocument()setsxmlns:icfon the root beforexmlns:xsi, so a new file declaresicf,ri,xsiin the same order as before.createElementis written in no namespace, as before, unless it has anxmlnsattribute of its own (the connector creates its elements withcreateElementNS). The tests comparewrite's output byte by byte with the oldDOMSourceoutput of the same document. A hand-edited store can come out with its namespace declarations rearranged:writedeclares an element'sxmlnsattributes in the order of its attribute map (Xerces sorts them by name) and keeps a redeclaration of a binding already in scope, where the old serializer put the element's own namespace first and dropped such redeclarations. The namespaces of every element and attribute stay the same. A DOM built in code can bind one prefix to two namespaces on one element: anxmlnsattribute that contradicts the namespace of the element or of one of its attributes, or twoxmlnsattributes, one set withsetAttributeand one withsetAttributeNS.writedeclares such a prefix once, and the first binding wins: the element'sxmlnsattributes, then its name, then its attributes. The old serializer did the same for the default namespace; for any other prefix it let the node's namespace win over the element'sxmlns:pattribute.//text()[normalize-space(.) = '']removed that node and left the rest;normalizeTextremoves the whole run. Such runs appear when elements between two indentation nodes are removed, so a saved file can differ from the old output in whitespace only:<icf:OpenICFContainer .../>, not one that holds a line break;<ri:firstname> <![CDATA[ ]]> </ri:firstname>is emptied by the first save, not the second.Why the writer feeds Saxon and does not use XSLTC
The connector bundle embeds
xml-apis-1.3.04.jar, and the connector server loads bundles with a child-firstBundleClassLoader. Inside the bundlejavax.xml.transform.TransformerFactoryandorg.w3c.dom.*therefore come from xml-apis 1.3.04. That copy has noTransformerFactory.newDefaultInstance(), and the JDK's XSLTC transformer cannot take the bundle's DOM classes. Surefire cannot see this, because there the JDK classes win. The writer therefore createsnet.sf.saxon.TransformerFactoryImpldirectly.A new integration test,
XMLConnectorBundleIT, loads the packaged bundle jar with the same child-first loader and runs a save, a reload and a search through it. It is wired into the module withmaven-failsafe-plugin(**/*BundleIT.java, system propertybundleJar). To check that the IT is not vacuous,writewas switched to(SAXTransformerFactory) javax.xml.transform.TransformerFactory.newDefaultInstance(): the IT then fails withNoSuchMethodError.Measurements
dispose()time, oneXMLHandlerImplper run:init()parses the generated file, thendispose()is timed (normalize + serialize + write). Three runs per size in one JVM, so the first run of each size includes warm-up. Entries are generated by the issue's harness (XmlBreakdown,Gen):ri:__ACCOUNT__entries with__UID__,__NAME__and a few more fields. Master is 3348526, "after" is this branch at dca4047. The later commits add tests and Javadoc, and the review fixes in A6 and A7, of which only the lone-Text shortcut changes the cost of a save noticeably: in a separate probe at 40,000 entries, anormalizeTextpass over a freshly parsed store took 23-47 ms and 0.1 MB instead of 45-231 ms and 49 MB. JDK 26 (default JDK of the dev machine), Saxon-HE 9.4.0.7, Xerces 2.6.2. All numbers come from one developer machine (macOS) and were run once, so read them as orders of magnitude.dispose()before, median (min-max of 3 runs)dispose()after, median (min-max of 3 runs)At 40,000 entries
dispose()is about 22 times faster. The 10,000 and 20,000 rows are noisy (the first run of each size is slower); the 40,000 row is the clearest. Parse time is not changed by this PR (45-380 ms in both runs).Tests
OpenICF-xml-connector,mvn -o -pl OpenICF-xml-connector verify: surefire 142 run, 0 failed; failsafe 1 run, 0 failed (XMLConnectorBundleIT).XmlDocumentWriterTests(new) coversfollowing,normalizeText, andwrite, which is compared with the oldDOMSourceoutput (the serializer half; the old XPath is not part of that comparison) and re-read for namespaces (DOM level 1 nodes included), CDATA, comments, processing instructions, entity references and a prefix bound to two namespaces on one element; text inside an unexpanded entity reference is left alone, and the text after it is normalized.XMLConnectorTestsgains cases fordispose(): it allocates no Xerces node list caches (XercesNodeLists, test helper), whitespace-only values are saved as empty elements, deleting the last entry leaves an empty container, a failed save is logged and thrown, a successful save logsExit serialize, a save without a document throwsConnectorException, and a new file declares its namespaces in the old order.XmlConnectorTestUtilgains store-file helpers (writeAccounts,namesInFile,account) forXMLConnectorTestsandXMLConnectorBundleIT. It also gainssetModified(File, long), which has no caller here: its callers come with [#157] Load and save the XML file only when it has changed #177.valuesSurviveASaveAndAReload(a characterization of what Saxon wrote; green on the olddispose()too);commentsSplitTextRuns(kills "isTextcounts comments", a mutant no row's tree contains);normalizeAndWriteNeverUseNodeLists(the purpose of the writer at unit level: kills aNodeList.item(i)walk insendElement; green from its first run, because every row walks by sibling pointers);normalizeAndWriteMakeALinearNumberOfDomCalls(counts the DOM calls ofnormalizeText+writethroughCountingDom, test helper: 63,035 for 1,000 entries, 126,035 for 2,000; kills a walk that counts the earlier siblings of each child, insendElementor innormalizeText, which takes about 3.8 times the calls for twice the entries and whichnormalizeAndWriteNeverUseNodeListsmisses; green from its first run for the same reason).Test strength
Each row is an arm of the production code, the case that reaches it, what only that arm produces, and a mutant that only that case kills. "red at" is the commit where the case was red before its arm was written (A1: on the base
33485269; A2: row 1 on6f83fa3e, the writer without thedispose()change, and rows 2-4 on6f83fa3eplus row 1'sdispose(), which only callswriteand rethrows, because the olddispose()already does what they assert; A3: on4fbcc475, which had no failsafe execution; A4 and A5: on the mutants named in their rows, because their arms were already written in A1 and A2; A6: on the commit before each arm; A7: onbd2fe56f, or on the mutant named in the row where the arm was already there). Rows marked unpinnable are arms that no test can tell from their absence; the reason is given as found.A1:
XmlDocumentWriter,following(6f83fa3)file:line— function — condition)XmlHandlerUtil.java:92-94—following—sibling != null: return the next siblingfollowingIsTheNextSiblingfollowing(a, document)isb, nota's childxfollowingnot defined33485269XmlHandlerUtil.java:91—following— no sibling:n = n.getParentNode(), try againfollowingClimbsToTheNextSiblingOfAnAncestorfollowing(x, document)isbalthoughxhas no siblingreturn node.getNextSibling();(no climb)33485269XmlHandlerUtil.java:91—following— loop boundn != rootfollowingStopsAtRootfollowing(x, a)isnullalthoughahas the siblingbn != null(the walk leavesroot)33485269XmlDocumentWriter.java:81-85—normalizeText— keep side: a lone non-blank Text node stays where it isnormalizedDocumentIsLeftAlonea's Text node in<r><a>x</a><b/></r>is the same object after the callnormalizeTextnot defined (afterwards: every lone Text node replaced by a new one; the other 138 tests stay green on it)33485269XmlDocumentWriter.java:74-100—normalizeText— a blank lone Text node is removed (:83-84); the walk: descend,following(node, root)after a leaf,after,following(parent, root)after a run that ends its parentwhitespaceOnlyTextIsRemovedaempty, every child ofran element (also after the emptyc)33485269XmlDocumentWriter.java:248-256—normalizeText—isXmlWhitespace: only space, tab, CR, LF make a run blankpaddedAndNonXmlSpaceValuesAreKeptisXmlWhitespace=value.toString().isBlank()(Character.isWhitespace)33485269, re-run after reviewXmlDocumentWriter.java:243-246—normalizeText—isText:CDATA_SECTION_NODEis textwhitespaceOnlyCdataIsRemovedisText=TEXT_NODEonly33485269XmlDocumentWriter.java:81,86-98—normalizeText— a Text node with a text sibling after it starts a run: one Text node with the joined value (if (!isXmlWhitespace(value))insert)adjacentTextAndCdataBecomeOneTextNodeaholds one Text node" x<y "(after == null || !isText(after))(A5 row 1 also fails on it — verified)33485269XmlDocumentWriter.java:81—normalizeText—node.getNodeType() == Node.TEXT_NODE: a lone non-blank CDATA section is not a lone Text node and becomes a Text nodesingleCdataBecomesATextNodeb's only child is aTEXT_NODE"z"node.getNodeType() == Node.TEXT_NODE(verified)33485269XmlDocumentWriter.java:111-130—write— the road every document takes: SaxonTransformerHandlerwithINDENT=yes,startDocument, the document's children,endDocument;send:ELEMENT_NODE,TEXT_NODE;sendElement:xmlns:pattributes declared, element and attributes named throughbindoutputIsTheSameAsBeforeDOMSourceoutputwritenot defined (also killed here:INDENTdropped,xmlns:pnot declared — verified)33485269XmlDocumentWriter.java:119-129—write—CheckedOutputStream: returns the CRC-32 of the bytes writtenwriteReturnsTheChecksumOfTheFilereturn 0L;33485269XmlDocumentWriter.java:162,206-217—sendElement/bind/declare— namespace scope:pushContext,declarePrefix,getURI,!uri.equals(inScope)→declare,popContextsiblingsDeclareTheirOwnNamespacesp:aandp:bread back inurn:1bindreturns the node's namespace without declaring it (also killed here:popContextdropped — verified)33485269XmlDocumentWriter.java:138—send—case Node.CDATA_SECTION_NODEcdataIsWrittenAsTextrreads back with the textx<ydefault)33485269XmlDocumentWriter.java:149-153—send—case Node.ENTITY_REFERENCE_NODE: the children are sententityReferencesAndDoctypeAreWrittenAsBefore<r>v<x/>…</r>, equal to the old output<r/>)33485269XmlDocumentWriter.java:209-210—bind—uri == nullwith a prefix: the namespace in scope (prefixed DOM level 1 nodes such ascreateDocument()'sxsi:schemaLocation)prefixedAttributeWithoutANamespaceTakesTheOneInScopexsi:schemaLocationreads back in the XSI namespacereturn "";for that branch33485269XmlDocumentWriter.java:168-169—sendElement—XMLNS.equals(name): the default namespace declaration,xmlns=""includeddefaultNamespaceUndeclarationIsKeptareads back in no namespacexmlns:p33485269XmlDocumentWriter.java:182-183—sendElement—prefix.isEmpty() ? "": an unprefixed attribute is in no namespace and declares nothingnamespacedAttributeWithoutAPrefixDeclaresNoDefaultNamespacerreads back in no namespacebind(declaresxmlns="urn:x"onr)33485269XmlDocumentWriter.java:142-144—send—case Node.COMMENT_NODEcreatedDocumentReadsBackTheSame" c "reads back33485269XmlDocumentWriter.java:146-148—send—case Node.PROCESSING_INSTRUCTION_NODEhandEditedDocumentReadsBackTheSame<?pi data?>reads back33485269XmlDocumentWriter.java:116-118—write—METHODxml,ENCODINGUTF-8,{http://xml.apache.org/xslt}indent-amountxmland UTF-8 and ignores the Xalanindent-amountkey, so deleting any of the three lines leaves every output byte the same (verified); kept as "the output settings used before"XmlDocumentWriter.java:120—write— try-with-resources closes the stream; anIOExceptionfromclose()is a failed saveendDocumentreturns, so an unclosedFileOutputStreamleaves the same file (verified); the leak shows only as an open descriptor, and a test cannot makeclose()failXmlDocumentWriter.java:192-194—sendElement—endPrefixMappingfor each declared prefixTransformerHandlerignoresendPrefixMapping; dropping the loop leaves the output the same (verified); kept for the SAX contractXmlDocumentWriter.java:180—sendElement— the second loop skipsxmlnsandxmlns:*attributesTransformerHandlerdropsxmlnsattributes passed instartElement'sAttributes; passing them changes nothing (verified); kept for the SAX contractXmlDocumentWriter.java:154-156—send—default: break;(document types and the like are not written)defaultdoes the same). Row 14's exact comparison with the old output already shows "no DOCTYPE"A2:
dispose()uses the writer (4fbcc47, row 5's test in d37978d)file:line— function — condition)XMLHandlerImpl.java:390—dispose— the save goes throughXmlDocumentWriter.writeinstead of Saxon's XPath//text()andDOMSenderdisposeNeverUsesNodeListsinit()+dispose()of a loaded file,XercesNodeLists.used(getDocument())isfalsedispose()body (both Saxon walks go throughNodeList.item(i))6f83fa3eXMLHandlerImpl.java:389—dispose—XmlDocumentWriter.normalizeText(document)beforewritewhitespaceOnlyValueIsEmptiedBySavefirstname" \t "is an empty element in the file, as before #157writewithoutnormalizeText6f83fa3e+ row 1's write-onlydispose()XMLHandlerImpl.java:392-395—dispose—catch (TransformerException | SAXException | IOException ex): ERROR line, thenConnectorExceptionfailedSaveIsLoggedAndThrownSystem.errhasFailed saving changes to xml file: java.io.FileNotFoundException, anddispose()throwsConnectorExceptionlog.error(row 1's catch only rethrows; the compiler requires it from row 1 on)6f83fa3e+ row 1's write-onlydispose()XMLHandlerImpl.java:475—createDocument—xmlns:icfset on the root beforexmlns:xsinewFileDeclaresNamespacesAsBeforeicf,ri,xsiin that orderxmlns:icfattribute (the writer then declaresicflast:ri, xsi, icf)6f83fa3e+ row 1's write-onlydispose()XMLHandlerImpl.java:397—dispose— the closing log line of a successful save saysExit {0}successfulSaveLogsItsExitSystem.outhas a line ending inExit serializelog.info("Entry {0}", method)dca40471A3: IT on the packaged bundle (dca4047)
file:line— function — condition)OpenICF-xml-connector/pom.xml:124-144—maven-failsafe-pluginexecution (integration-test,verify; include**/*BundleIT.java; system propertybundleJar)XMLConnectorBundleIT#bundleSavesLoadsAndFindsEntriesTests run: 1forXMLConnectorBundleIT4fbcc475XmlDocumentWriter.java:112—write— Saxon's factory created directly (new net.sf.saxon.TransformerFactoryImpl(), A1's code), which only the bundle's child-first loader can tell apartXMLConnectorBundleIT#bundleSavesLoadsAndFindsEntrieswritetakes the JDK's transformer(SAXTransformerFactory) javax.xml.transform.TransformerFactory.newDefaultInstance()inwrite: under surefire only the format tests notice, the bundle's xml-apis 1.3.04 has no such method4fbcc475A4: a whitespace-only run is removed whole (d5236ca)
file:line— function — condition)XmlDocumentWriter.java:93,96-98—normalizeText— a blank run: every node of the run is removed, not only the firstwhitespaceOnlyRunOfSeveralNodesIsRemovedWholeain<r><a> <![CDATA[ ]]> </a></r>has no children6f83fa3eXMLHandlerImpl.java:389—dispose—normalizeTextremoves the two whitespace-only Text nodes that a deleted entry leaves side by sidedeletingTheLastEntryLeavesAnEmptyContainernormalizeText: the file keeps a line break (disposeNeverUsesNodeListsalso fails, through its node-list probe); row 1's mutant fails it too4fbcc475A5: a merged run of text keeps its place (7b13f6f)
file:line— function — condition)XmlDocumentWriter.java:94—normalizeText— the merged Text node is inserted before the first node of its runmergedRunKeepsItsPlacexin<r><x>a<![CDATA[b]]><!--c--></x></r>holds the Text nodeab, then the commentparent.appendChild(…)in place ofinsertBefore(…, node): the value moves after the comment (the other 134 tests stay green on it)6f83fa3eA6: review fixes (1f97256..bd2fe56)
file:line— function — condition)XmlDocumentWriter.java:75—normalizeText— an entity reference is not descended into: its children are read-onlytextInsideAnEntityReferenceIsLeftAlonenormalizeTextreturns and the reference keeps its blank Text node and its CDATA sectionNode child = node.getFirstChild();(removeChildthrowsDOMExceptionNO_MODIFICATION_ALLOWED_ERR)7a1a5eb5XmlDocumentWriter.java:209,212—bind— an unprefixed DOM level 1 element (createElement) is in no namespace, undeclaring a default namespace in scope withxmlns=""unprefixedDomLevel1ElementIsInNoNamespaceDOMSourceoutput (<b xmlns=""/>), with and without anxmlnsattribute on the parentif (uri == null)(the element takes the default namespace in scope)1f97256eXmlDocumentWriter.java:81-85—normalizeText— a lone Text node is checked in place, without a list or a bufferXMLHandlerImpl.java:387—dispose— the document comes fromgetDocument(), with no monitor on itsaveWithoutADocumentThrowsConnectorExceptiondispose()on a handler whoseinit()never ran throwsConnectorExceptionData file does not exists: …synchronized (document) { … }around the save, as before (aNullPointerException)f238a5a3XmlHandlerUtil.java:91—following— loop boundn != null: arootthat is not an ancestor acts asnullfollowingWithARootOutsideTheAncestorsEndsAtTheTopfollowing(b, a)isnullin<r><a><x/></a><b/></r>n != rootonly (aNullPointerExceptionabove the document)e851b4abA7: second review fixes (b3fba18, faf5d15)
file:line— function — condition)XmlDocumentWriter.java:226-228—declare— a prefix the element already declares is not declared againtwoXmlnsAttributesDeclareTheDefaultNamespaceOncerwith twoxmlnsattributes (setAttributeurn:y, thensetAttributeNSurn:z, which Xerces puts first) reads back from the file, inurn:zxmlnsdeclared twice, and the parser rejects the file; the other 141 tests stay green on it)bd2fe56fXmlDocumentWriter.java:209—bind—declared.contains(prefix): a prefix the element already declares, by an attribute or for its name, keeps that namespaceaPrefixIsDeclaredOncePerElementAsBeforeDOMSourceoutput for four children that bind one prefix to two namespaces (<b xmlns="urn:y"/>three times,<p:b xmlns:p="urn:x" p:c="v"/>)bd2fe56f(row 1's case fails too). Thebindcheck alone is unpinnable: Saxon 9.4's serializer writes the declarations it got fromstartPrefixMappingand does not compare them with the namespace passed tostartElementoraddAttribute, so with row 1's check in place, dropping it leaves all 142 tests green (verified); kept so that every name is sent with the namespace the file gives itbd2fe56fXmlDocumentWriter.java:75-76—normalizeText— after an entity reference the walk goes on withfollowing(node, root)textAfterAnEntityReferenceIsNormalized<r>&e; <c> </c></r>, the blank Text node after the reference and the one insidecare removedreturnat the first entity reference (the other 141 tests stay green on it; atbd2fe56fall 139 did)1f97256eA1 row 6's test was corrected in review (a literal U+3000 became
\u3000); its red was re-run with the corrected test on the tree without that arm.Notes on the rows: A1 has 24 rows, 5 unpinnable (20-24); A2 has 5 rows; A3 has 2 rows; A4 has 2 rows; A5 has 1 row; A6 has 5 rows, 1 unpinnable (3); A7 has 3 rows, and row 2's
bindcheck is unpinnable on its own. A2 row 5 was found at the last read: its arm is in 4fbcc47, its test was added afterwards, and its red ran ondca40471with that one line reverted. A3 row 2's arm was written in A1, so its "red" is the manual mutant described above (applied, run, reverted).