Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
010a8c9
[#157] Add an index of XML entries by __UID__ and __NAME__
maximthomas Oct 11, 2026
cd1064a
[#157] Find entries for create, update, delete and authenticate throu…
maximthomas Oct 11, 2026
95cf244
[#157] Answer identifier and unfiltered searches from the index
maximthomas Oct 11, 2026
a848dc3
[#157] Pin identifier lookups on a large XML file
maximthomas Oct 11, 2026
29be9e3
[#157] Rebuild the index on save only when the text normalization cha…
maximthomas Oct 11, 2026
ffc433d
[#157] Pin the XQuery fallback for non-identifier searches
maximthomas Oct 11, 2026
6ec4dda
[#157] Correct the scale test comment and lower its limit
maximthomas Oct 11, 2026
200323f
[#157] Fix the wording of the EntryIndex class Javadoc
maximthomas Oct 11, 2026
cf43e67
[#157] Ignore added entries when the document has no container root
maximthomas Oct 11, 2026
39504d6
[#157] Keep the no-document error when searching through the index
maximthomas Oct 11, 2026
e690138
[#157] Index entries written inside a created or updated entry
maximthomas Oct 12, 2026
1cb2d47
[#157] Pin index lookups after a reload, a failed rename, '&' and per…
maximthomas Oct 12, 2026
6beb334
[#157] Release the scale test's handler from the static cache
maximthomas Oct 12, 2026
fba445b
[#157] Share the __UID__ to __NAME__ rule between the translator and …
maximthomas Oct 12, 2026
3e9db1e
[#157] Walk a subtree for its entries in one place in EntryIndex
maximthomas Oct 12, 2026
ccd2125
[#157] Hold each entry's keys in an array instead of a LinkedHashSet
maximthomas Oct 12, 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 @@ -30,6 +30,7 @@
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.forgerock.openicf.connectors.xml.query.abstracts.Query;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.ConnectorObject;
Expand Down Expand Up @@ -107,6 +108,19 @@ public Collection<ConnectorObject> search(String query, ObjectClass objectClass)
return result;
}

@Override
public Collection<ConnectorObject> search(Query query, ObjectClass objectClass) {
Collection<ConnectorObject> result = null;
lock.readLock().lock();
try {
result = proxy.search(query, objectClass);
}
finally {
lock.readLock().unlock();
}
return result;
}

@Override
public Uid authenticate(String username, GuardedString password) {
Uid result = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
/*
* 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 static org.forgerock.openicf.connectors.xml.util.XmlHandlerUtil.following;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;

import org.identityconnectors.framework.common.objects.Name;
import org.identityconnectors.framework.common.objects.Uid;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
* Finds entries by {@code __UID__} and {@code __NAME__} without walking the document.
* <p>
* Gives the same entries, in the same order, as {@code /icf:OpenICFContainer//ri:CLASS} filtered by
* {@code $x/icf:FIELD = 'VALUE'}: entries at any depth below the container, an entry under
* every value of its identifier child elements, values compared as they are, results in document
* order, so the first of several duplicates comes first.
* <p>
* Every change to the entries must be reported through {@link #added}, {@link #removed} and
* {@link #rekeyed}. Nothing is computed on read, so concurrent readers are safe while nobody
* changes the index; callers change it under the handler's write lock only.
* <p>
* An entry inside another entry's identifier element is part of that identifier's value, but
* changing or removing it re-keys only the inner entry: the outer one keeps its old key until the
* next {@link #rebuild}. The connector never writes that shape; only a hand-edited file has it.
*/
final class EntryIndex {

private final String icfNamespace;
private final String riNamespace;
private final Set<String> objectClasses;

private final Map<Element, Indexed> entries = new IdentityHashMap<Element, Indexed>();
private final Map<String, TreeMap<Long, Element>> byClass = new HashMap<String, TreeMap<Long, Element>>();
private final Map<Key, List<Indexed>> byIdentifier = new HashMap<Key, List<Indexed>>();
private long nextOrder;
/** Whether the last rebuilt document has the container as its root; otherwise nothing is indexed. */
private boolean containerRoot;

EntryIndex(String icfNamespace, String riNamespace, Set<String> objectClasses) {
this.icfNamespace = icfNamespace;
this.riNamespace = riNamespace;
this.objectClasses = new HashSet<String>(objectClasses);
}

/** Replaces the content of the index with the entries of {@code document}. */
void rebuild(Document document) {
entries.clear();
byClass.clear();
byIdentifier.clear();
nextOrder = 0;
containerRoot = false;
Element root = document.getDocumentElement();
if (root == null || !icfNamespace.equals(root.getNamespaceURI())
|| !XMLHandlerImpl.ICF_CONTAINER_TAG.equals(root.getLocalName())) {
return;
}
containerRoot = true;
addAll(root);
}

Element first(String objectClass, String field, String value) {
List<Indexed> list = byIdentifier.get(new Key(objectClass, field, value));
return list == null ? null : list.get(0).element;
}

List<Element> find(String objectClass, String field, String value) {
List<Indexed> list = byIdentifier.get(new Key(objectClass, field, value));
if (list == null) {
return Collections.emptyList();
}
List<Element> result = new ArrayList<Element>(list.size());
for (Indexed indexed : list) {
result.add(indexed.element);
}
return result;
}

List<Element> all(String objectClass) {
TreeMap<Long, Element> ordered = byClass.get(objectClass);
return ordered == null ? Collections.<Element>emptyList() : new ArrayList<Element>(ordered.values());
}

/** Records an entry just appended as the last child of the container, and every entry inside it. */
void added(Element entry) {
if (containerRoot) {
addAll(entry);
}
}

/** Forgets {@code subtree} if it is an entry, and every entry inside it. */
void removed(Node subtree) {
for (Element entry : entriesIn(subtree)) {
Indexed indexed = entries.remove(entry);
if (indexed != null) {
byClass.get(indexed.objectClass).remove(indexed.order);
unlink(indexed);
}
}
}

/**
* Reads the identifiers of an entry whose children have changed; its place in document order stays.
* A new entry inside it, such as a value element named like an object class, has no place in
* document order yet, so the whole index is rebuilt then.
*/
void rekeyed(Element entry) {
Indexed indexed = entries.get(entry);
if (indexed == null) {
return;
}
if (holdsUnindexedEntry(entry)) {
rebuild(entry.getOwnerDocument());
return;
}
unlink(indexed);
indexed.keys = keysOf(entry, indexed.objectClass);
link(indexed);
}

private boolean isEntry(Node node) {
return riNamespace.equals(node.getNamespaceURI()) && objectClasses.contains(node.getLocalName());
}

/** Indexes {@code subtree} if it is an entry, and every entry inside it, in document order. */
private void addAll(Node subtree) {
for (Element entry : entriesIn(subtree)) {
add(entry);
}
}

private boolean holdsUnindexedEntry(Node subtree) {
for (Element entry : entriesIn(subtree)) {
if (!entries.containsKey(entry)) {
return true;
}
}
return false;
}

/** {@code subtree} if it is an entry, and every entry inside it, in document order. */
private List<Element> entriesIn(Node subtree) {
List<Element> found = new ArrayList<Element>();
Node node = subtree;
while (node != null) {
Node child = null;
if (node.getNodeType() == Node.ELEMENT_NODE) {
if (isEntry(node)) {
found.add((Element) node);
}
child = node.getFirstChild();
}
node = child != null ? child : following(node, subtree);
}
return found;
}

private void add(Element entry) {
String objectClass = entry.getLocalName();
Indexed indexed = new Indexed(entry, objectClass, nextOrder++, keysOf(entry, objectClass));
entries.put(entry, indexed);
TreeMap<Long, Element> ordered = byClass.get(objectClass);
if (ordered == null) {
ordered = new TreeMap<Long, Element>();
byClass.put(objectClass, ordered);
}
ordered.put(indexed.order, entry);
link(indexed);
}

/**
* Identifier child elements only: {@code $x/icf:FIELD} selects children, not descendants. Each key
* once; an array, because an entry usually has one or two.
*/
private Key[] keysOf(Element entry, String objectClass) {
List<Key> keys = new ArrayList<Key>(2);
for (Node child = entry.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getNodeType() == Node.ELEMENT_NODE && icfNamespace.equals(child.getNamespaceURI())) {
String field = child.getLocalName();
if (Uid.NAME.equals(field) || Name.NAME.equals(field)) {
Key key = new Key(objectClass, field, child.getTextContent());
if (!keys.contains(key)) {
keys.add(key);
}
}
}
}
return keys.toArray(new Key[keys.size()]);
}

private void link(Indexed indexed) {
for (Key key : indexed.keys) {
List<Indexed> list = byIdentifier.get(key);
if (list == null) {
list = new ArrayList<Indexed>(1);
byIdentifier.put(key, list);
}
int i = list.size();
while (i > 0 && list.get(i - 1).order > indexed.order) {
i--;
}
list.add(i, indexed);
}
}

private void unlink(Indexed indexed) {
for (Key key : indexed.keys) {
List<Indexed> list = byIdentifier.get(key);
list.remove(indexed);
if (list.isEmpty()) {
byIdentifier.remove(key);
}
}
}

private static final class Indexed {
final Element element;
final String objectClass;
final long order;
Key[] keys;

Indexed(Element element, String objectClass, long order, Key[] keys) {
this.element = element;
this.objectClass = objectClass;
this.order = order;
this.keys = keys;
}
}

private static final class Key {
final String objectClass;
final String field;
final String value;

Key(String objectClass, String field, String value) {
this.objectClass = objectClass;
this.field = field;
this.value = value;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof Key)) {
return false;
}
Key other = (Key) o;
return objectClass.equals(other.objectClass) && field.equals(other.field) && value.equals(other.value);
}

@Override
public int hashCode() {
return Objects.hash(objectClass, field, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
package org.forgerock.openicf.connectors.xml;

import org.forgerock.openicf.connectors.xml.query.abstracts.Query;
import org.forgerock.openicf.connectors.xml.query.QueryBuilder;
import org.forgerock.openicf.connectors.xml.xsdparser.SchemaParser;
import java.io.IOException;
import java.util.Collection;
Expand Down Expand Up @@ -215,9 +214,7 @@ public FilterTranslator<Query> createFilterTranslator(ObjectClass objClass, Oper
@Override
public void executeQuery(ObjectClass objClass, Query query, ResultsHandler handler, OperationOptions options) {
synchronized (getLock()) {
QueryBuilder queryBuilder = new QueryBuilder(query, objClass);
Collection<ConnectorObject> hits = xmlInstanceHandler.search(
queryBuilder.toString(), objClass);
Collection<ConnectorObject> hits = xmlInstanceHandler.search(query, objClass);
int count = 0;
for (ConnectorObject hit : hits) {
count++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,15 @@ public XMLFilterTranslator(boolean preferUidOverName) {
}

private String getAttributeName(Attribute attribute) {
String attrName = attribute.getName();
if (!preferUidOverName && Uid.NAME.equals(attrName)) {
attrName = Name.NAME;
}
return attrName;
return elementName(attribute.getName(), preferUidOverName);
}

/**
* The element an attribute is stored in: {@code __UID__} is stored as {@code __NAME__} unless
* {@code preferUidOverName}. XMLHandlerImpl looks entries up by the same rule.
*/
static String elementName(String attributeName, boolean preferUidOverName) {
return !preferUidOverName && Uid.NAME.equals(attributeName) ? Name.NAME : attributeName;
}

@Override
Expand Down Expand Up @@ -203,7 +207,7 @@ public Query createLessThanOrEqualExpression(LessThanOrEqualFilter filter, boole

private Query createComparisonQuery(String name, String operator, String value) {
Query query = new QueryImpl();
query.set(new ComparisonQuery("$x/" + name, operator, escape(value)));
query.set(new ComparisonQuery("$x/" + name, operator, value, escape(value)));

return query;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
* 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$
*/
package org.forgerock.openicf.connectors.xml;

import java.util.Collection;
import java.util.Set;
import org.forgerock.openicf.connectors.xml.query.abstracts.Query;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.ConnectorObject;
Expand All @@ -43,6 +45,8 @@ public interface XMLHandler {

Collection<ConnectorObject> search(String query, ObjectClass objectClass);

Collection<ConnectorObject> search(Query query, ObjectClass objectClass);

Uid authenticate(String username, GuardedString password);

XMLHandler init();
Expand Down
Loading
Loading