Replace chained instanceof filter dispatch with the existing FilterVisitor - #145
Conversation
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
maximthomas
left a comment
There was a problem hiding this comment.
praise: Both instanceof chains now go through the framework's own FilterVisitor, and connector extensibility and the old fallbacks are kept.
LeafExpressionVisitoris a non-static inner class (AbstractFilterTranslator.java:425), so connector overrides of the protectedcreateXxxExpressionmethods still bind.- Every in-repo custom
Filter(PassThroughFilter,RangeFilter,TrueFilter,FalseFilter) dispatches tovisitExtendedFilter, which returns the filter unchanged in the normalizer andnullin the translator, the same as the oldelsebranch. - The implicit null-safety of
instanceofwas noticed and restored at both entry points (ObjectNormalizerFacade.java:150,AbstractFilterTranslator.java:412).
issue (non-blocking): No test pins the null guard restored in AbstractFilterTranslator.createLeafExpression.
OpenICF-java-framework/connector-framework/src/main/java/org/identityconnectors/framework/common/objects/filter/AbstractFilterTranslator.java:412-414
The description presents testNullFilterReturnsNull as the regression test for a null fix made in both files, but that test only calls ObjectNormalizerFacade.normalizeFilter(null). A null leaf reaches this guard from translate(new AndFilter(eq, null)) (through simplifyAndDistribute, :279) and from translate(new NotFilter(null)) (through normalizeNot → negate(null) → :392). Without the guard, both throw an NPE where the base returned "everything". Measured: with the guard deleted, FilterTranslatorTests stays green (7/7 pass).
@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();
}Pin: once the guard is deleted, both assertions fail with an NPE.
issue (non-blocking): No test pins routing AND/OR/NOT sub-filters through the null-checked normalizeFilter.
OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/local/operations/ObjectNormalizerFacade.java:168, :208, :212
testNullFilterReturnsNull passes null only at the top level (:150), and testAnd/testOr/testNot use only non-null children. new NotFilter(null) and new AndFilter(x, null) are legal, and so is a one-element AndFilter(Collection), whose getRight() is null. Measured: with the three visit methods changed to call child.accept(this, p) directly, ObjectNormalizerFacadeTests stays green (16/16 pass, testNullFilterReturnsNull included), even though normalizeFilter(new NotFilter(null)) then throws an NPE.
@Test
public void testNullSubFilterIsPassedThrough() {
ObjectNormalizerFacade normalizer = createTestNormalizer();
assertNull(((NotFilter) normalizer.normalizeFilter(new NotFilter(null))).getFilter());
assertNull(((AndFilter) normalizer.normalizeFilter(
new AndFilter(FilterBuilder.contains(createTestAttribute()), null))).getRight());
assertNull(((OrFilter) normalizer.normalizeFilter(
new OrFilter(FilterBuilder.contains(createTestAttribute()), null))).getRight());
}Pin: this needs imports for AndFilter, NotFilter, OrFilter and the static org.testng.Assert.assertNull. Each assertion fails with an NPE under that mutant.
nitpick (non-blocking): The NormalizingFilterVisitor Javadoc says extended filters carry no attribute, but ExtendedMatchFilter does.
OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/local/operations/ObjectNormalizerFacade.java:157-160, :185-187
ExtendedMatchFilter extends AttributeFilter and dispatches to visitExtendedFilter, which returns it as is, so its attribute value is never normalized. That behaviour is unchanged from the base; the comment just gives the wrong reason for it.
/**
* 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.
*/note (non-blocking): A behaviour change the description does not mention.
ObjectNormalizerFacade.java:153,AbstractFilterTranslator.java:415: a third-partyFilteroutside the built-in set used to fall through theinstanceofchain (returned unchanged, ornull). Now its ownaccept(FilterVisitor, P)picks the branch. No in-repo implementation is affected, and the default search path already evaluated such filters throughFilteredResultsHandlerVisitor, so this was judged not a defect.
…it (#146) ## Problem Scattered `build-maven` matrix cells keep failing in `javadoc:jar (attach-javadocs)` with ``` Error fetching URL: https://docs.groovy-lang.org/latest/html/api/ (java.io.FileNotFoundException: .../package-list) ``` The javadoc `<link>` to `docs.groovy-lang.org/latest` makes every build fetch the Groovy link list over the network. javadoc tries `element-list` first and falls back to `package-list`. The `latest` docs now return 404 for `package-list`, so a single transient miss on `element-list` fails the build. The same link has also been seen to hang javadoc until the 6-hour job limit. Recent examples are #130, #142 and #145, whose failed cells went green on a plain re-run. ## Change - **`OpenICF-java-framework/pom.xml`**: remove the Groovy link. None of the framework modules expose Groovy types in their public API, so the link added nothing there. - **`OpenICF-java-framework/bundles-parent/pom.xml`**: replace both `<links>` blocks (`pluginManagement` and `<reporting>`) with an `offlineLink`. It points at a committed `package-list` under `bundles-parent/src/javadoc/groovy-2.4.21/`. The URL now targets the Groovy version the build actually uses (2.4.21) instead of `latest`, which currently holds the Groovy 5 docs. The connectors that expose Groovy types (groovy, ssh, kerberos) keep their cross-links. - If the local file cannot be found, for example in the `src/it` invoker projects, the plugin only logs an error and skips the link. It does not fail the build. - The file must be refreshed when the Groovy version changes. The comment next to the `groovyJavadocPackageList` property says so. ## Verification - `package` with `attach-javadocs` (`failOnWarnings=true`) on JDK 26 for `connector-framework-contract`, `connector-framework-internal`, `groovy-connector`, `ssh-connector` and `kerberos-connector`, plus `groovy-connector` on JDK 11: `BUILD SUCCESS`. - The javadoc options file (`-Ddebug=true`) has no network `-link` left. Groovy is passed as `-linkoffline https://docs.groovy-lang.org/2.4.21/html/api <local dir>`. - The generated HTML links to `docs.groovy-lang.org/2.4.21` in 30 files for groovy-connector (e.g. `groovy/lang/Closure.html`, `CompilerConfiguration.html`), 6 for ssh and 4 for kerberos. No links to `latest` remain.
5da658c to
dcc2e09
Compare
|
Addressed in the new commit, rebased onto current
Also in this commit: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: both of the new null-safety tests do their job, and the downstream suites listed in the test plan run and pass in CI.
- Mutation claims checked on this head. Deleting the guard at
AbstractFilterTranslator.java:412-414makesFilterTranslatorTests.testNullLeafTranslatesToEverythingfail with an NPE (:499). Callingchild.accept(this, p)at every composite site makesObjectNormalizerFacadeTests.testNullSubFilterIsPassedThroughfail (:187). NormalizingFilterVisitoris a stateless singleton that receives the facade asP(ObjectNormalizerFacade.java:163-166), so dispatch creates no visitor objects.OpenICF-ldap-connector,OpenICF-dbcommonandOpenICF-xml-connectorare modules of the root reactor, andbuild-mavenpasses atdcc2e099on all 9 OS/JDK cells.
issue (non-blocking): testNullSubFilterIsPassedThrough never builds a null left child, so no test covers the null-safe routing of getLeft() in visitAndFilter/visitOrFilter.
OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/local/operations/ObjectNormalizerFacade.java:170, :224; OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/local/operations/ObjectNormalizerFacadeTests.java:184-195
In the test, every null child is either on the right or inside a NOT. I replaced p.normalizeFilter(filter.getLeft()) with filter.getLeft().accept(this, p) at :170 and :224 only, and ObjectNormalizerFacadeTests still passes (17 run, 0 failed). Both new AndFilter(null, eq) and new OrFilter(null, eq) are legal, because CollectionUtil.newList keeps a null left. With that mutant they throw an NPE, where this head returns the null left unchanged. The mutation check in the description is therefore true only when all sites are mutated together.
assertNull(((AndFilter) normalizer.normalizeFilter(new AndFilter(null, contains)))
.getLeft());
assertNull(((OrFilter) normalizer.normalizeFilter(new OrFilter(null, contains)))
.getLeft());Pin: add these two asserts to testNullSubFilterIsPassedThrough. Both fail with an NPE under the left-only mutant and pass at this head.
issue (non-blocking): normalizeFilter now uses three stack frames for each AND/OR level; before this PR it used one.
OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/local/operations/ObjectNormalizerFacade.java:153, :168-172, :222-226
The recursion is now normalizeFilter -> Filter.accept -> visitAndFilter/visitOrFilter -> normalizeFilter. I measured it under -Xint on a thread with a 1M stack, running the normalizer and then the translator, in the same order as SearchImpl. A right-nested OR or AND of N EqualsFilters passes at the base (e6bad438) for every N up to 2500. At this head, normalizeFilter throws StackOverflowError above N = 2042. Two repeats gave the same numbers. With a warm JIT I could not reproduce the difference. Only filters more than 2000 levels deep running in interpreted mode are affected. I am not asking for a change: this is the price of visitor dispatch.
issue (non-blocking): This bug predates the PR: OrFilter.getRight() returns an AndFilter when there are three or more sub-filters, so the rewritten visitOrFilter turns FilterBuilder.or(a, b, c) into OR(a, AND(b, c)).
OpenICF-java-framework/connector-framework/src/main/java/org/identityconnectors/framework/common/objects/filter/OrFilter.java:77-87, ObjectNormalizerFacade.java:224-225, AbstractFilterTranslator.java:138
FilterBuilder.or(Collection) and or(Filter...) build a flat OrFilter (FilterBuilder.java:456-479). For a list longer than two, getRight() copies everything after the first element into new AndFilter(right). visitOrFilter then builds new OrFilter(norm(getLeft()), norm(getRight())). As a result, an object that matches only b, or only c, no longer matches the filter that SearchImpl passes to the connector. AbstractFilterTranslator.translate() -> normalizeNot() (:111, :138) walks the same getLeft()/getRight() pair. I traced this by reading the code and did not run it. The base behaves the same way, so this belongs in a separate issue. The fix is one line in OrFilter, and it corrects every walker that uses getLeft()/getRight():
right.removeFirst();
return new OrFilter(right);Pin: build a, b and c as EqualsFilters on different values. normalizeFilter(FilterBuilder.or(a, b, c)).accept(obj) should be true for an obj that matches only c; at this head it is false.
…sitor Closes java/chained-type-tests for ObjectNormalizerFacade.normalizeFilter (12 tests) and AbstractFilterTranslator.createLeafExpression (9 tests): both already had a real Filter type hierarchy and a FilterVisitor interface available, so the instanceof chains become proper visitor dispatch instead of a mechanical alternative. Behavior-preserving, including a null-safety edge case the first pass of this refactor briefly broke (caught by the existing test suite, fixed, and locked in with a new regression test). The other two java/chained-type-tests alerts (EqualsHashCodeBuilder, SQLUtil) are dismissed on GitHub as won't-fix: they dispatch on final JDK types (primitive array component types / Integer, Double, Blob, Timestamp, ...) that a visitor pattern cannot be added to.
…ethods - FilterTranslatorTests.testNullLeafTranslatesToEverything: a null leaf under AND/NOT translates to "everything" instead of throwing an NPE. - ObjectNormalizerFacadeTests.testNullSubFilterIsPassedThrough: null children of AND/OR/NOT (incl. a one-element AndFilter) pass through. - Correct the NormalizingFilterVisitor Javadoc: ExtendedMatchFilter has an attribute, it is just not normalized. - @OverRide on all FilterVisitor implementations (CodeQL java/missing-override-annotation). - Fix the 3A Systems copyright lines in the touched test files.
testNullSubFilterIsPassedThrough now also builds AndFilter(null, x) and OrFilter(null, x), so routing getLeft() through the null-checked normalizeFilter is pinned as well: each of the five composite sites, mutated on its own, fails the test with an NPE.
dcc2e09 to
3514d47
Compare
|
Addressed in the new commit, rebased onto current
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: the new asserts close the last gap in the null-safety tests. Each child site in the visitor now has its own test.
ObjectNormalizerFacadeTests.java:192-195addsnew AndFilter(null, contains)andnew OrFilter(null, contains). I mutated one site at a time, replacingp.normalizeFilter(...)withchild.accept(this, p)inObjectNormalizerFacade.java. Each of the five mutants makestestNullSubFilterIsPassedThroughfail with an NPE on its own line: NOT:187, AND-right:188, OR-right:190, AND-left:192, OR-left:194. The unmutated head passes, 17 run / 0 failed.- The change touches only tests. Rebased onto the new base, the two existing commits are unchanged, and
build-mavenpasses at3514d47eon all 9 OS/JDK cells (JDK 11-26).
Summary
Closes the last remaining CodeQL
note-severity category,java/chained-type-tests(4 alerts).ObjectNormalizerFacade.normalizeFilter(a 12-way instanceof chain) andAbstractFilterTranslator.createLeafExpression(a 9-way instanceof chain) both dispatch on the framework's ownFiltersubtypes. The framework already ships aFilterVisitorinterface for exactly this purpose (FilteredResultsHandlerVisitoris prior art). Both methods are rewritten as visitor dispatch instead of an instanceof chain.AbstractFilterTranslator's visitor is a private, non-static inner class, so itsvisitXxxFiltermethods can call the enclosing instance'sprotected createXxxExpression(...)methods. This keeps the point of the class: connector subclasses override those methods.instanceofonnullis alwaysfalseand falls through to theelsebranch.filter.accept(...)throwsNullPointerExceptionon anullfilter instead. The fix restores the null check at both entry points, and routes the AND/OR/NOT recursion inObjectNormalizerFacadeback through the null-checked public method rather than calling.acceptdirectly on sub-filters that may be null. A null leaf reaches the translator's guard throughtranslate(new AndFilter(eq, null))andtranslate(new NotFilter(null)).Filterimplementation outside the built-in set used to fall through the instanceof chain. The normalizer returned it unchanged and the translator returnednull("everything"). Now the filter's ownaccept(FilterVisitor, P)picks the branch. Every in-repo custom filter (PassThroughFilter,RangeFilter,TrueFilter,FalseFilter, plusPresenceFilterandExtendedMatchFilter) dispatches tovisitExtendedFilter, which keeps the old fallback. The default search path already evaluated such filters throughFilteredResultsHandlerVisitor.java/chained-type-testsalerts are dismissed on GitHub as won't-fix.EqualsHashCodeBuilder.appenddispatches on primitive array component types, andSQLUtil's JDBC parameter binding dispatches onInteger/Double/Blob/Timestamp/... Both dispatch on final JDK types we don't own, so a fix based on a visitor or on polymorphism is not possible.Test plan
ObjectNormalizerFacadeTests:testPresenceFilterPassedThroughUnchanged,testNullFilterReturnsNull(top-level null),testNullSubFilterIsPassedThrough(a null left or right child of AND and of OR, a null NOT operand, and a one-elementAndFilter).FilterTranslatorTests:testNullLeafTranslatesToEverything(null leaf under AND and NOT).testNullLeafTranslatesToEverythingfail with an NPE. In the normalizer, callingchild.accept(...)directly at any one of the five composite sites (left or right child of AND, left or right child of OR, the NOT operand), with the other four left intact, makestestNullSubFilterIsPassedThroughfail with an NPE.mvn install -DskipITsonconnector-framework,connector-framework-internal,OpenICF-ldap-connector,OpenICF-dbcommonandOpenICF-xml-connector, including their existing filter-translator test suites: all green.