From 4e19e37e359bd92863964f82766b9eadf9259005 Mon Sep 17 00:00:00 2001 From: Paulo Chang Date: Tue, 18 Aug 2026 19:35:40 +0200 Subject: [PATCH 1/2] Don't flag diff.index control nodes as invalid index definitions /oak:index/diff.index and /oak:index/diff.index.optimizer are control nodes for Oak's simplified index management (diff indexes, OAK-12010), not index definitions themselves. The validator previously required every oak:QueryIndexDefinition node to have type=lucene, compatVersion=2 and a --custom- node name, which these correctly-formed control nodes never satisfy, producing 3 false positives. Exempt exactly these two paths from those checks, while still requiring type=disabled on them, since Oak silently disables the whole merge mechanism if that value is anything else. Fixes #49 --- README.md | 2 + .../aem/cloud/AemCloudValidator.java | 18 ++++- .../aem/cloud/AemCloudValidatorTest.java | 70 +++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5d4960..dbc56f3 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ There is a mandatory naming policy for Oak index definition node names which enf Further details in . +The control nodes `/oak:index/diff.index` and `/oak:index/diff.index.optimizer` used by Oak's simplified index management (diff indexes) are exempt from the naming, `type=lucene` and `compatVersion` rules above, but must have `type` set to `disabled`. Further details in . + # Usage with Maven You can use this validator with the [FileVault Package Maven Plugin][3] in version 1.4.0 or higher like this diff --git a/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidator.java b/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidator.java index c4e451f..01f4d00 100644 --- a/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidator.java +++ b/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidator.java @@ -54,14 +54,20 @@ public class AemCloudValidator implements NodePathValidator, MetaInfPathValidato static final String VIOLATION_MESSAGE_MUTABLE_NODES_AND_IMMUTABLE_NODES_IN_SAME_PACKAGE = "Mutable and immutable nodes must not be mixed in the same package. You must separate those into two packages and give them both a dedicated package type!"; static final String VIOLATION_MESSAGE_NON_LUCENE_TYPE_INDEX_DEFINITION = "Only oak:QueryIndexDefinitions of type='lucene' are supported in AEMaaCS but found type='%s'. Compare with https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/operations/indexing#current-limitations"; static final String VIOLATION_MESSAGE_INVALID_COMPAT_VERSION_IN_INDEX_DEFINITION = "The compatVersion property of an oak:QueryIndexDefinition must be set to the Long value '2' but found '%s'. Compare with https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/operations/indexing#current-limitations"; - + static final String VIOLATION_MESSAGE_INVALID_DIFF_INDEX_TYPE = "The diff-index control node used for Oak simplified index management must have type='disabled' but found type='%s'. Further details at https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/operations/indexing#simplified-index-management-using-the-diff-index"; + // this path is relative to META-INF private static final Path INSTALL_HOOK_PATH = Paths.get(Constants.VAULT_DIR, Constants.HOOKS_DIR); /** * The allowed patterns are defined in https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/operations/indexing#preparing-the-new-index-definition */ private static final Pattern INDEX_DEFINITION_NAME_PATTERN = Pattern.compile(".*-\\d++-custom-\\d++"); - + /** + * Paths of the diff-index control nodes used by Oak simplified index management (OAK-12010). + * They are not index definitions themselves and don't follow the naming/type/compatVersion rules of regular index definitions. + */ + private static final Collection DIFF_INDEX_CONTROL_NODE_PATHS = Arrays.asList("/oak:index/diff.index", "/oak:index/diff.index.optimizer"); + private static final Collection IMMUTABLE_PATH_PREFIXES = Arrays.asList("/apps", "/libs", "/oak:index"); private static final Collection WRITABLE_PATHS_BY_DISTRIBUTION_IMPORTER = Arrays.asList( "/content", // access provided by system user content-writer-service and sling-distribution-importer @@ -213,6 +219,14 @@ static boolean isPackagePathInstalledConditionally(String runMode, Path packageR @Override public @Nullable Collection validate(@NotNull DocViewNode2 node, @NotNull NodeContext nodeContext, boolean isRoot) { if ("oak:QueryIndexDefinition".equals(node.getPrimaryType().orElse(""))) { + if (DIFF_INDEX_CONTROL_NODE_PATHS.contains(nodeContext.getNodePath())) { + String diffIndexType = node.getPropertyValue(PN_TYPE).orElse(""); + if (!"disabled".equals(diffIndexType)) { + return Collections.singleton(new ValidationMessage(defaultSeverity, + String.format(VIOLATION_MESSAGE_INVALID_DIFF_INDEX_TYPE, diffIndexType))); + } + return null; + } Collection messages = new ArrayList<>(); String indexType = node.getPropertyValue(PN_TYPE).orElse(""); if (!"lucene".equals(indexType)) { diff --git a/src/test/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorTest.java b/src/test/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorTest.java index 3eb96fa..099fcdb 100644 --- a/src/test/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorTest.java +++ b/src/test/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorTest.java @@ -132,6 +132,76 @@ void testInvalidLuceneIndexDefinitions() { )); } + @Test + void testValidDiffIndexDefinitions() { + AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + List properties = Arrays.asList( + new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "disabled")); + DocViewNode2 node = new DocViewNode2(NameConstants.JCR_ROOT, properties); + // diff.index control node, no version-suffix name, no compatVersion + NodeContext context = new NodeContextImpl("/oak:index/diff.index", Paths.get("_oak_index/test"),Paths.get("./jcr_root")); + Collection messages = new ArrayList<>(); + Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); + MatcherAssert.assertThat(messages, Matchers.empty()); + // diff.index.optimizer control node (Index Optimizer) + context = new NodeContextImpl("/oak:index/diff.index.optimizer", Paths.get("_oak_index/test"),Paths.get("./jcr_root")); + messages = new ArrayList<>(); + Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); + MatcherAssert.assertThat(messages, Matchers.empty()); + } + + @Test + void testInvalidDiffIndexType() { + AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + List properties = Arrays.asList( + new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "lucene")); + DocViewNode2 node = new DocViewNode2(NameConstants.JCR_ROOT, properties); + NodeContext context = new NodeContextImpl("/oak:index/diff.index", Paths.get("_oak_index/test"),Paths.get("./jcr_root")); + Collection messages = new ArrayList<>(); + Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); + MatcherAssert.assertThat(messages, Matchers.contains( + new ValidationMessage(ValidationMessageSeverity.ERROR, String.format(AemCloudValidator.VIOLATION_MESSAGE_INVALID_DIFF_INDEX_TYPE, "lucene")) + )); + // same for the Index Optimizer control node + context = new NodeContextImpl("/oak:index/diff.index.optimizer", Paths.get("_oak_index/test"),Paths.get("./jcr_root")); + messages = new ArrayList<>(); + Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); + MatcherAssert.assertThat(messages, Matchers.contains( + new ValidationMessage(ValidationMessageSeverity.ERROR, String.format(AemCloudValidator.VIOLATION_MESSAGE_INVALID_DIFF_INDEX_TYPE, "lucene")) + )); + } + + @Test + void testDiffIndexLookalikeNamesNotExempted() { + AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + List properties = Arrays.asList( + new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "lucene"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "compatVersion"), "2", PropertyType.LONG)); + DocViewNode2 node = new DocViewNode2(NameConstants.JCR_ROOT, properties); + // a real, correctly named custom index whose name happens to start with "diff." must not be exempted + NodeContext context = new NodeContextImpl("/oak:index/diff.indexer-1-custom-1", Paths.get("_oak_index/test"),Paths.get("./jcr_root")); + Collection messages = new ArrayList<>(); + Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); + MatcherAssert.assertThat(messages, Matchers.empty()); + // a lucene index misnamed to look like an Index Optimizer diff must still go through the normal naming/type/compatVersion checks + context = new NodeContextImpl("/oak:index/diff.index.custom-index", Paths.get("_oak_index/test"),Paths.get("./jcr_root")); + messages = new ArrayList<>(); + Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); + MatcherAssert.assertThat(messages, Matchers.contains( + new ValidationMessage(ValidationMessageSeverity.ERROR, String.format(AemCloudValidator.VIOLATION_MESSAGE_INVALID_INDEX_DEFINITION_NODE_NAME, "diff.index.custom-index")) + )); + // the control node name below a different parent path must not be exempted either + context = new NodeContextImpl("/apps/foo/diff.index", Paths.get("_oak_index/test"),Paths.get("./jcr_root")); + messages = new ArrayList<>(); + Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); + MatcherAssert.assertThat(messages, Matchers.contains( + new ValidationMessage(ValidationMessageSeverity.ERROR, String.format(AemCloudValidator.VIOLATION_MESSAGE_INVALID_INDEX_DEFINITION_NODE_NAME, "diff.index")) + )); + } + @Test void testInvalidPropertyIndexDefinition() { AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); From 1cf1b82d2cc9f48b0d7b6ece4810d2b898805e47 Mon Sep 17 00:00:00 2001 From: Paulo Chang Date: Wed, 19 Aug 2026 11:29:00 +0200 Subject: [PATCH 2/2] Fix readme to follow standard formatting for the new diff.index rules --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dbc56f3..4acfbe2 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ There is a mandatory naming policy for Oak index definition node names which enf Further details in . +## Enforce diff.index control nodes of type `disabled` + The control nodes `/oak:index/diff.index` and `/oak:index/diff.index.optimizer` used by Oak's simplified index management (diff indexes) are exempt from the naming, `type=lucene` and `compatVersion` rules above, but must have `type` set to `disabled`. Further details in . # Usage with Maven