Skip to content

Commit 1bbdf23

Browse files
Copilotvharseko
andauthored
Fix NPE in XMLHandlerImpl.dispose caused by concurrent DOM serialization (#91)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vharseko <6818498+vharseko@users.noreply.github.com>
1 parent 53d1fd7 commit 1bbdf23

2 files changed

Lines changed: 77 additions & 43 deletions

File tree

OpenICF-xml-connector/src/main/java/org/forgerock/openicf/connectors/xml/ConcurrentXMLHandler.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,29 +111,36 @@ public Uid authenticate(String username, GuardedString password) {
111111
}
112112

113113
public XMLHandler init() {
114-
lock.readLock().lock();
114+
// Use the write lock so the invokers counter and proxy.init()
115+
// are not racing against a concurrent dispose() (or another init())
116+
// running under a shared read lock.
117+
lock.writeLock().lock();
115118
try {
116119
if (0 == invokers) {
117120
proxy.init();
118121
}
119122
invokers++;
120123
}
121124
finally {
122-
lock.readLock().unlock();
125+
lock.writeLock().unlock();
123126
}
124127
return this;
125128
}
126129

127130
public void dispose() {
128-
lock.readLock().lock();
131+
// Use the write lock so that proxy.dispose() (which serializes the
132+
// shared DOM through Saxon) cannot run concurrently with itself or
133+
// with any read-lock operation (search/authenticate) that walks the
134+
// same DOM. Mutating the invokers counter also requires exclusion.
135+
lock.writeLock().lock();
129136
try {
130137
invokers--;
131138
if (0 == invokers) {
132139
proxy.dispose();
133140
}
134141
}
135142
finally {
136-
lock.readLock().unlock();
143+
lock.writeLock().unlock();
137144
}
138145
}
139146

OpenICF-xml-connector/src/main/java/org/forgerock/openicf/connectors/xml/XMLHandlerImpl.java

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -388,47 +388,74 @@ public void dispose() {
388388
}
389389

390390
try {
391-
try {
392-
XPathFactory xpathFactory = new net.sf.saxon.xpath.XPathFactoryImpl();
393-
// XPath to find empty text nodes.
394-
XPathExpression xpathExp = xpathFactory.newXPath().compile("//text()[normalize-space(.) = '']");
395-
NodeList emptyTextNodes = null;
396-
emptyTextNodes = (NodeList) xpathExp.evaluate(document, XPathConstants.NODESET);
397-
398-
// Remove each empty text node from document.
399-
for (int i = 0; i < emptyTextNodes.getLength(); i++) {
400-
Node emptyTextNode = emptyTextNodes.item(i);
401-
emptyTextNode.getParentNode().removeChild(emptyTextNode);
391+
// Synchronize on the document so that XPath cleanup and the
392+
// Saxon transform happen atomically with respect to any other
393+
// thread that might still hold a reference to the same DOM.
394+
// Saxon's DOMSender walks children via NodeList.item(i) after
395+
// calling getLength(); a concurrent removeChild on the DOM
396+
// can make item(i) return null and trigger a NullPointerException
397+
// in DOMSender.walkNode.
398+
synchronized (document) {
399+
try {
400+
XPathFactory xpathFactory = new net.sf.saxon.xpath.XPathFactoryImpl();
401+
// XPath to find empty text nodes.
402+
XPathExpression xpathExp = xpathFactory.newXPath().compile("//text()[normalize-space(.) = '']");
403+
NodeList emptyTextNodes = (NodeList) xpathExp.evaluate(document, XPathConstants.NODESET);
404+
405+
// Snapshot the list before mutating the DOM, then remove
406+
// each empty text node (guarding against nodes whose
407+
// parent has already been detached).
408+
int len = emptyTextNodes.getLength();
409+
List<Node> toRemove = new ArrayList<Node>(len);
410+
for (int i = 0; i < len; i++) {
411+
Node n = emptyTextNodes.item(i);
412+
if (n != null) {
413+
toRemove.add(n);
414+
}
415+
}
416+
for (Node emptyTextNode : toRemove) {
417+
Node parent = emptyTextNode.getParentNode();
418+
if (parent != null) {
419+
parent.removeChild(emptyTextNode);
420+
}
421+
}
422+
} catch (XPathExpressionException e) {
423+
//We don't care. It's just formatting.
402424
}
403-
} catch (XPathExpressionException e) {
404-
//We don't care. It's just formatting.
405-
}
406425

407-
TransformerFactory transformerFactory = new net.sf.saxon.TransformerFactoryImpl();
408-
Transformer transformer = transformerFactory.newTransformer();
409-
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
410-
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
411-
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
412-
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
413-
414-
DOMSource source = new DOMSource(document);
415-
/* Running this code in java 5 we had to change
416-
StreamResult result = new StreamResult(config.getXmlFilePath());
417-
into
418-
StreamResult result = new StreamResult(config.getXmlFilePath().getPath());
419-
Otherwise you get the following error:
420-
javax.xml.transform.TransformerException: java.io.FileNotFoundException:
421-
*/
422-
/*
423-
* If the safePath is not escaped then it throws
424-
* net.sf.saxon.trans.XPathException: java.net.URISyntaxException:
425-
* Illegal character in safePath at index 9: /temp/XML Connector/test.xml
426-
* String safePath = config.getXmlFilePath().getPath().replaceAll(" ", "%20");
427-
*/
428-
FileOutputStream fos = new FileOutputStream(config.getXmlFilePath());
429-
StreamResult result = new StreamResult(fos);
430-
431-
transformer.transform(source, result);
426+
TransformerFactory transformerFactory = new net.sf.saxon.TransformerFactoryImpl();
427+
Transformer transformer = transformerFactory.newTransformer();
428+
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
429+
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
430+
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
431+
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
432+
433+
DOMSource source = new DOMSource(document);
434+
/* Running this code in java 5 we had to change
435+
StreamResult result = new StreamResult(config.getXmlFilePath());
436+
into
437+
StreamResult result = new StreamResult(config.getXmlFilePath().getPath());
438+
Otherwise you get the following error:
439+
javax.xml.transform.TransformerException: java.io.FileNotFoundException:
440+
*/
441+
/*
442+
* If the safePath is not escaped then it throws
443+
* net.sf.saxon.trans.XPathException: java.net.URISyntaxException:
444+
* Illegal character in safePath at index 9: /temp/XML Connector/test.xml
445+
* String safePath = config.getXmlFilePath().getPath().replaceAll(" ", "%20");
446+
*/
447+
FileOutputStream fos = new FileOutputStream(config.getXmlFilePath());
448+
try {
449+
StreamResult result = new StreamResult(fos);
450+
transformer.transform(source, result);
451+
} finally {
452+
try {
453+
fos.close();
454+
} catch (IOException ioe) {
455+
log.warn("Failed to close XML output stream: {0}", ioe);
456+
}
457+
}
458+
}
432459

433460
log.info("Saving changes to xml file");
434461
} catch (TransformerException ex) {

0 commit comments

Comments
 (0)