Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -20,6 +20,7 @@
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
* Portions Copyrighted 2010-2013 ForgeRock AS.
* Portions Copyrighted 2026 3A Systems, LLC
*/
package org.identityconnectors.framework.impl.api.local.operations;

Expand All @@ -34,12 +35,12 @@
import org.identityconnectors.framework.common.objects.SyncDelta;
import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
import org.identityconnectors.framework.common.objects.filter.AndFilter;
import org.identityconnectors.framework.common.objects.filter.AttributeFilter;
import org.identityconnectors.framework.common.objects.filter.ContainsAllValuesFilter;
import org.identityconnectors.framework.common.objects.filter.ContainsFilter;
import org.identityconnectors.framework.common.objects.filter.EndsWithFilter;
import org.identityconnectors.framework.common.objects.filter.EqualsFilter;
import org.identityconnectors.framework.common.objects.filter.Filter;
import org.identityconnectors.framework.common.objects.filter.FilterVisitor;
import org.identityconnectors.framework.common.objects.filter.GreaterThanFilter;
import org.identityconnectors.framework.common.objects.filter.GreaterThanOrEqualFilter;
import org.identityconnectors.framework.common.objects.filter.LessThanFilter;
Expand Down Expand Up @@ -146,47 +147,93 @@ public SyncDelta normalizeSyncDelta(SyncDelta delta) {
* @return The normalized filter.
*/
public Filter normalizeFilter(Filter filter) {
if (filter instanceof ContainsFilter) {
AttributeFilter afilter = (AttributeFilter) filter;
return new ContainsFilter(normalizeAttribute(afilter.getAttribute()));
} else if (filter instanceof EndsWithFilter) {
AttributeFilter afilter = (AttributeFilter) filter;
return new EndsWithFilter(normalizeAttribute(afilter.getAttribute()));
} else if (filter instanceof EqualsFilter) {
AttributeFilter afilter = (AttributeFilter) filter;
return new EqualsFilter(normalizeAttribute(afilter.getAttribute()));
} else if (filter instanceof GreaterThanFilter) {
AttributeFilter afilter = (AttributeFilter) filter;
return new GreaterThanFilter(normalizeAttribute(afilter.getAttribute()));
} else if (filter instanceof GreaterThanOrEqualFilter) {
AttributeFilter afilter = (AttributeFilter) filter;
return new GreaterThanOrEqualFilter(normalizeAttribute(afilter.getAttribute()));
} else if (filter instanceof LessThanFilter) {
AttributeFilter afilter = (AttributeFilter) filter;
return new LessThanFilter(normalizeAttribute(afilter.getAttribute()));
} else if (filter instanceof LessThanOrEqualFilter) {
AttributeFilter afilter = (AttributeFilter) filter;
return new LessThanOrEqualFilter(normalizeAttribute(afilter.getAttribute()));
} else if (filter instanceof StartsWithFilter) {
AttributeFilter afilter = (AttributeFilter) filter;
return new StartsWithFilter(normalizeAttribute(afilter.getAttribute()));
} else if (filter instanceof ContainsAllValuesFilter) {
AttributeFilter afilter = (AttributeFilter) filter;
return new ContainsAllValuesFilter(normalizeAttribute(afilter.getAttribute()));
} else if (filter instanceof NotFilter) {
NotFilter notFilter = (NotFilter) filter;
return new NotFilter(normalizeFilter(notFilter.getFilter()));
} else if (filter instanceof AndFilter) {
AndFilter andFilter = (AndFilter) filter;
return new AndFilter(normalizeFilter(andFilter.getLeft()), normalizeFilter(andFilter
.getRight()));
} else if (filter instanceof OrFilter) {
OrFilter orFilter = (OrFilter) filter;
return new OrFilter(normalizeFilter(orFilter.getLeft()), normalizeFilter(orFilter
if (filter == null) {
return null;
}
return filter.accept(NormalizingFilterVisitor.INSTANCE, this);
}

/**
* Applies {@link #normalizeAttribute(Attribute)} to every attribute
* referenced by a filter, recursing through composite (AND/OR/NOT)
* filters. Filters dispatched to visitExtendedFilter (presence,
* extended-match, pass-through and custom filters) are returned
* unchanged; an ExtendedMatchFilter's attribute is not normalized.
*/
private static final class NormalizingFilterVisitor implements
FilterVisitor<Filter, ObjectNormalizerFacade> {

static final NormalizingFilterVisitor INSTANCE = new NormalizingFilterVisitor();

@Override
public Filter visitAndFilter(ObjectNormalizerFacade p, AndFilter filter) {
return new AndFilter(p.normalizeFilter(filter.getLeft()), p.normalizeFilter(filter
.getRight()));
} else {
}

@Override
public Filter visitContainsFilter(ObjectNormalizerFacade p, ContainsFilter filter) {
return new ContainsFilter(p.normalizeAttribute(filter.getAttribute()));
}

@Override
public Filter visitContainsAllValuesFilter(ObjectNormalizerFacade p,
ContainsAllValuesFilter filter) {
return new ContainsAllValuesFilter(p.normalizeAttribute(filter.getAttribute()));
}

@Override
public Filter visitEqualsFilter(ObjectNormalizerFacade p, EqualsFilter filter) {
return new EqualsFilter(p.normalizeAttribute(filter.getAttribute()));
}

@Override
public Filter visitExtendedFilter(ObjectNormalizerFacade p, Filter filter) {
return filter;
}

@Override
public Filter visitGreaterThanFilter(ObjectNormalizerFacade p, GreaterThanFilter filter) {
return new GreaterThanFilter(p.normalizeAttribute(filter.getAttribute()));
}

@Override
public Filter visitGreaterThanOrEqualFilter(ObjectNormalizerFacade p,
GreaterThanOrEqualFilter filter) {
return new GreaterThanOrEqualFilter(p.normalizeAttribute(filter.getAttribute()));
}

@Override
public Filter visitLessThanFilter(ObjectNormalizerFacade p, LessThanFilter filter) {
return new LessThanFilter(p.normalizeAttribute(filter.getAttribute()));
}

@Override
public Filter visitLessThanOrEqualFilter(ObjectNormalizerFacade p,
LessThanOrEqualFilter filter) {
return new LessThanOrEqualFilter(p.normalizeAttribute(filter.getAttribute()));
}

@Override
public Filter visitNotFilter(ObjectNormalizerFacade p, NotFilter filter) {
return new NotFilter(p.normalizeFilter(filter.getFilter()));
}

@Override
public Filter visitOrFilter(ObjectNormalizerFacade p, OrFilter filter) {
return new OrFilter(p.normalizeFilter(filter.getLeft()), p.normalizeFilter(filter
.getRight()));
}

@Override
public Filter visitStartsWithFilter(ObjectNormalizerFacade p, StartsWithFilter filter) {
return new StartsWithFilter(p.normalizeAttribute(filter.getAttribute()));
}

@Override
public Filter visitEndsWithFilter(ObjectNormalizerFacade p, EndsWithFilter filter) {
return new EndsWithFilter(p.normalizeAttribute(filter.getAttribute()));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
* Portions Copyrighted 2026 3A Systems, LLC
*/
package org.identityconnectors.framework.impl.api.local.operations;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;

import java.util.Collections;

import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
Expand All @@ -35,8 +39,11 @@
import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
import org.identityconnectors.framework.common.objects.SyncDeltaType;
import org.identityconnectors.framework.common.objects.SyncToken;
import org.identityconnectors.framework.common.objects.filter.AndFilter;
import org.identityconnectors.framework.common.objects.filter.Filter;
import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
import org.identityconnectors.framework.common.objects.filter.NotFilter;
import org.identityconnectors.framework.common.objects.filter.OrFilter;
import org.identityconnectors.framework.common.serializer.SerializerUtil;
import org.identityconnectors.framework.spi.AttributeNormalizer;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -162,6 +169,35 @@ public void testNot() {
assertNormalizedFilter(expected, filter);
}

@Test
public void testPresenceFilterPassedThroughUnchanged() {
Filter filter = FilterBuilder.present("foo");
assertNormalizedFilter(filter, filter);
}

@Test
public void testNullFilterReturnsNull() {
assertEquals(createTestNormalizer().normalizeFilter(null), null);
}

@Test
public void testNullSubFilterIsPassedThrough() {
ObjectNormalizerFacade normalizer = createTestNormalizer();
Filter contains = FilterBuilder.contains(createTestAttribute());
assertNull(((NotFilter) normalizer.normalizeFilter(new NotFilter(null))).getFilter());
assertNull(((AndFilter) normalizer.normalizeFilter(new AndFilter(contains, null)))
.getRight());
assertNull(((OrFilter) normalizer.normalizeFilter(new OrFilter(contains, null)))
.getRight());
assertNull(((AndFilter) normalizer.normalizeFilter(new AndFilter(null, contains)))
.getLeft());
assertNull(((OrFilter) normalizer.normalizeFilter(new OrFilter(null, contains)))
.getLeft());
// a one-element AndFilter has a null right-hand side
assertNull(((AndFilter) normalizer.normalizeFilter(new AndFilter(Collections
.singletonList(contains)))).getRight());
}

@Test
public void testContainsAllValues() {
Filter expected = FilterBuilder.containsAllValues(createNormalizedTestAttribute());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,28 +409,89 @@ private T createLeafExpression(Filter filter) {
* @return The expression or null (for everything)
*/
private T createLeafExpression(Filter filter, boolean not) {
if (filter instanceof ContainsFilter) {
return createContainsExpression((ContainsFilter) filter, not);
} else if (filter instanceof EndsWithFilter) {
return createEndsWithExpression((EndsWithFilter) filter, not);
} else if (filter instanceof EqualsFilter) {
return createEqualsExpression((EqualsFilter) filter, not);
} else if (filter instanceof GreaterThanFilter) {
return createGreaterThanExpression((GreaterThanFilter) filter, not);
} else if (filter instanceof GreaterThanOrEqualFilter) {
return createGreaterThanOrEqualExpression((GreaterThanOrEqualFilter) filter, not);
} else if (filter instanceof LessThanFilter) {
return createLessThanExpression((LessThanFilter) filter, not);
} else if (filter instanceof LessThanOrEqualFilter) {
return createLessThanOrEqualExpression((LessThanOrEqualFilter) filter, not);
} else if (filter instanceof StartsWithFilter) {
return createStartsWithExpression((StartsWithFilter) filter, not);
} else if (filter instanceof ContainsAllValuesFilter) {
return createContainsAllValuesExpression((ContainsAllValuesFilter) filter, not);
} else {
if (filter == null) {
return null;
}
return filter.accept(new LeafExpressionVisitor(), not);
}

/**
* Dispatches a leaf filter to its corresponding createXxxExpression
* override. AND/OR/NOT filters never reach here (see
* {@link #createLeafExpression(Filter)}, which already unwraps a
* top-level NOT); those and any other unrecognized filter kind return
* {@code null}, matching the previous instanceof-chain's fallback.
*/
private final class LeafExpressionVisitor implements FilterVisitor<T, Boolean> {

@Override
public T visitAndFilter(Boolean not, AndFilter filter) {
// unrecognized expression - nothing we can do
return null;
}

@Override
public T visitContainsFilter(Boolean not, ContainsFilter filter) {
return createContainsExpression(filter, not);
}

@Override
public T visitContainsAllValuesFilter(Boolean not, ContainsAllValuesFilter filter) {
return createContainsAllValuesExpression(filter, not);
}

@Override
public T visitEqualsFilter(Boolean not, EqualsFilter filter) {
return createEqualsExpression(filter, not);
}

@Override
public T visitExtendedFilter(Boolean not, Filter filter) {
// unrecognized expression - nothing we can do
return null;
}

@Override
public T visitGreaterThanFilter(Boolean not, GreaterThanFilter filter) {
return createGreaterThanExpression(filter, not);
}

@Override
public T visitGreaterThanOrEqualFilter(Boolean not, GreaterThanOrEqualFilter filter) {
return createGreaterThanOrEqualExpression(filter, not);
}

@Override
public T visitLessThanFilter(Boolean not, LessThanFilter filter) {
return createLessThanExpression(filter, not);
}

@Override
public T visitLessThanOrEqualFilter(Boolean not, LessThanOrEqualFilter filter) {
return createLessThanOrEqualExpression(filter, not);
}

@Override
public T visitNotFilter(Boolean not, NotFilter filter) {
// unrecognized expression - nothing we can do
return null;
}

@Override
public T visitOrFilter(Boolean not, OrFilter filter) {
// unrecognized expression - nothing we can do
return null;
}

@Override
public T visitStartsWithFilter(Boolean not, StartsWithFilter filter) {
return createStartsWithExpression(filter, not);
}

@Override
public T visitEndsWithFilter(Boolean not, EndsWithFilter filter) {
return createEndsWithExpression(filter, not);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
* Portions Copyrighted 2026 3A Systems, LLC
*/
package org.identityconnectors.framework.common.objects.filter;

Expand Down Expand Up @@ -492,6 +493,14 @@ public void testSimplifyNoAndNoOrNoLeaf() {
"( CONTAINS d d )", atIndex(1));
}

@Test
public void testNullLeafTranslatesToEverything() {
Filter eq = FilterBuilder.equalTo(AttributeBuilder.build("a", "a"));
assertEquals(new AllFiltersTranslator().translate(new AndFilter(eq, null)),
new AllFiltersTranslator().translate(eq));
assertThat(new AllFiltersTranslator().translate(new NotFilter(null))).isEmpty();
}

private static String translateSingle(AbstractFilterTranslator<String> translator, Filter filter) {
List<String> translated = translator.translate(filter);
assertEquals(translated.size(), 1);
Expand Down
Loading