Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9332968
[#157] Add a stamp that tells whether the XML file has changed
maximthomas Oct 8, 2026
2e6c4c7
[#157] Load and save the XML file only when it has changed
maximthomas Oct 8, 2026
6eaed53
[#157] Keep reads of the XML document from writing to it
maximthomas Oct 8, 2026
0111d43
[#157] Do not hold the XMLConnector class lock while loading a file
maximthomas Oct 8, 2026
84df879
[#157] Normalize the text of a loaded XML document
maximthomas Oct 8, 2026
d0efc9c
[#157] Resolve a relative DTD of a store under a non-ASCII path
maximthomas Oct 8, 2026
bd73b5e
[#157] Load an existing store in the bundle IT
maximthomas Oct 8, 2026
529d3c6
[#157] Keep a store file that appears before a new document is saved
maximthomas Oct 9, 2026
9661a0c
[#157] Create a new store and load an outside edit in the bundle IT
maximthomas Oct 9, 2026
e30c0ae
[#157] Pin that a whitespace-only value of a loaded file is absent
maximthomas Oct 9, 2026
4625e87
[#157] Skip the non-ASCII directory test where the path cannot be enc…
maximthomas Oct 9, 2026
6533361
[#157] Say what the XMLConnector class monitor no longer covers
maximthomas Oct 9, 2026
37b4a3e
[#157] Pin the recent-past half of the racy window
maximthomas Oct 9, 2026
1b76414
[#157] Name the non-ASCII test directory after the test's store file
maximthomas Oct 9, 2026
40104db
[#157] Do not load the partial write of an unchanged new document
maximthomas Oct 9, 2026
bf79edd
[#157] Drop the new document when a file appears over it
maximthomas Oct 9, 2026
3c4cfc1
[#157] Report a collision only when a file is there to overwrite
maximthomas Oct 9, 2026
5934cdd
[#157] Drop the new document at the call sites, not inside the check
maximthomas Oct 9, 2026
03753ae
[#157] Take a new document's stamp as missing, not from a second look
maximthomas Oct 9, 2026
d10cd2a
[#157] Keep a new document whose failed save left a file behind an un…
maximthomas Oct 9, 2026
19962a6
[#157] Checksum the XML file as it is parsed, without a copy in memory
maximthomas Oct 9, 2026
0736d45
[#157] Tell an unreadable link from the file system, not from isKnown()
maximthomas Oct 9, 2026
a3d352d
[#157] Pin the stamp a new document starts from
maximthomas Oct 9, 2026
0db055f
[#157] Say which callers StatFailingFile diverts
maximthomas Oct 11, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ public class ConcurrentXMLHandler implements XMLHandler {
private volatile int invokers = 0;

public ConcurrentXMLHandler(XMLConfiguration config, Schema connSchema, XSSchemaSet xsdSchemas) {
proxy = new XMLHandlerImpl(config, connSchema, xsdSchemas);
this(new XMLHandlerImpl(config, connSchema, xsdSchemas));
}

ConcurrentXMLHandler(XMLHandler proxy) {
this.proxy = proxy;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted 2010 [name of copyright owner]"
* Portions Copyrighted 2026 3A Systems, LLC
*
* $Id$
*/
Expand All @@ -40,7 +41,6 @@
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.Uid;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

class ConnectorObjectCreator {

Expand All @@ -49,7 +49,6 @@ class ConnectorObjectCreator {
private Map<String, String> attributeClassMap;
private Map<String, AttributeInfo> attributeInfoMap;
private ObjectClass objectClass;
private NodeList nodeList;
private ConnectorObjectBuilder conObjBuilder;

protected ConnectorObjectCreator(HashMap<String, String> attrClasses, HashMap<String, AttributeInfo> attrInfos, ObjectClass objClass) {
Expand All @@ -58,18 +57,17 @@ protected ConnectorObjectCreator(HashMap<String, String> attrClasses, HashMap<St
this.objectClass = objClass;
}

protected ConnectorObject createConnectorObject(NodeList nodes) {
nodeList = nodes;
protected ConnectorObject createConnectorObject(Node entry) {
conObjBuilder = new ConnectorObjectBuilder();
conObjBuilder.setObjectClass(objectClass);

addAllAttributesToBuilder();
addAllAttributesToBuilder(entry);

return conObjBuilder.build();
}

// Add all the attributes to the connectorbuilder-object
private void addAllAttributesToBuilder() {
private void addAllAttributesToBuilder(Node entry) {

boolean hasUid = false;

Expand All @@ -78,8 +76,7 @@ private void addAllAttributesToBuilder() {

String nameTmp = "";

for (int i = 0; i < nodeList.getLength(); i++) {
Node attributeNode = nodeList.item(i);
for (Node attributeNode = entry.getFirstChild(); attributeNode != null; attributeNode = attributeNode.getNextSibling()) {
if (attributeNode.getNodeType() == Node.ELEMENT_NODE) {

Node textNode = attributeNode.getFirstChild();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2026 3A Systems, LLC.
*/

package org.forgerock.openicf.connectors.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Objects;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;

/**
* What the store file looked like at one moment: its modification time, size and file key, or that
* it did not exist. Stamps that differ in any of these mean the file has changed; an older
* modification time counts too, because {@code cp -p} sets one.
* <p>
* A file rewritten in place with the same size within one tick of the modification time clock
* keeps all of these. That can only go unnoticed while the stamp is racy: when it was taken, the
* modification time was less than {@link #RACY_WINDOW_MILLIS} before the clock, or after it.
* Callers compare the content ({@link #checksum}) behind a racy stamp.
*/
final class FileStamp {

/** The coarsest common tick: 2 s on FAT; 1 s on HFS+ and ext3. */
static final long RACY_WINDOW_MILLIS = 2000L;

/** No file: no modification time, which every existing file has. */
static final FileStamp MISSING = new FileStamp(null, -1L, null, false);

/** The file could not be read: matches nothing, not even itself. */
private static final FileStamp UNKNOWN = new FileStamp(null, -1L, null, true);

private final FileTime lastModified;
private final long size;
private final Object fileKey;
private final boolean racy;

private FileStamp(FileTime lastModified, long size, Object fileKey, boolean racy) {
this.lastModified = lastModified;
this.size = size;
this.fileKey = fileKey;
this.racy = racy;
}

static FileStamp read(File file) {
long now = System.currentTimeMillis();
try {
BasicFileAttributes attributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
FileTime modified = attributes.lastModifiedTime();
return new FileStamp(modified, attributes.size(), attributes.fileKey(),
now - modified.toMillis() < RACY_WINDOW_MILLIS);
} catch (NoSuchFileException e) {
return MISSING;
} catch (IOException e) {
return UNKNOWN;
}
}

/** The CRC-32 of the file's content. */
static long checksum(File file) throws IOException {
CRC32 crc = new CRC32();
try (InputStream in = new CheckedInputStream(Files.newInputStream(file.toPath()), crc)) {
byte[] buffer = new byte[64 * 1024];
while (in.read(buffer) != -1) {
// the stream updates the checksum
}
}
return crc.getValue();
}

boolean isRacy() {
return racy;
}

/** Whether the file could be read, or was known to be missing, when the stamp was taken. */
boolean isKnown() {
return this != UNKNOWN;
}

boolean sameState(FileStamp other) {
if (this == UNKNOWN || other == UNKNOWN) {
return false;
}
return Objects.equals(lastModified, other.lastModified)
&& size == other.size
&& Objects.equals(fileKey, other.fileKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
public class XMLConnector implements Connector, AuthenticateOp, CreateOp, DeleteOp, SearchOp<Query>, SchemaOp, TestOp, UpdateOp {

private static final Log log = Log.getLog(XMLConnector.class);
private static /*volatile*/ final Map<String, ConcurrentXMLHandler> XMLHandlerCache = new HashMap<String, ConcurrentXMLHandler>(1);
/** Guarded by {@code XMLConnector.class}. */
static final Map<String, ConcurrentXMLHandler> XMLHandlerCache = new HashMap<String, ConcurrentXMLHandler>(1);
private XMLConfiguration config;
private XMLHandler xmlInstanceHandler = null;

Expand All @@ -77,23 +78,23 @@ public Configuration getConfiguration() {
@Override
public void init(Configuration configuration) {
this.config = Assertions.nullChecked((XMLConfiguration) configuration, "config");
synchronized (XMLConnector.class) {
try {
String canonicalPath = config.getXmlFilePath().getCanonicalPath();
ConcurrentXMLHandler handler = XMLHandlerCache.get(canonicalPath);

ConcurrentXMLHandler handler;
try {
String canonicalPath = config.getXmlFilePath().getCanonicalPath();
synchronized (XMLConnector.class) {
handler = XMLHandlerCache.get(canonicalPath);
if (null == handler) {
SchemaParser schemaParser = new SchemaParser(XMLConnector.class, config.getXsdFilePath());
handler = new ConcurrentXMLHandler(config, schema(), schemaParser.getXsdSchema());
XMLHandlerCache.put(canonicalPath, handler);
}
xmlInstanceHandler = handler.init();
}
catch (IOException ex) {
log.error(ex, "Failed to get the CanonicalPath of {0}", config.getXmlFilePath());
throw new ConnectorIOException(ex);
}
} catch (IOException ex) {
log.error(ex, "Failed to get the CanonicalPath of {0}", config.getXmlFilePath());
throw new ConnectorIOException(ex);
}
// Outside the class monitor: loading one file, or waiting while another call saves it, must not hold up connectors on other files.
xmlInstanceHandler = handler.init();
log.info("XMLConnector initialized");
}

Expand Down
Loading
Loading