From d7f43fc16e983538147a1ba9957a552b094f9484 Mon Sep 17 00:00:00 2001 From: Paulo Chang Date: Tue, 13 Oct 2026 16:29:53 +0200 Subject: [PATCH 1/3] Enforce diff index wrapper matching the target environment Adobe requires different wrappers for /oak:index/diff.index on Cloud Manager and on the local AEM SDK/RDE in https://experienceleague.adobe.com/en/docs/experience-cloud-kcs/kbarticles/ka-41939 Add option "allowLocalDiffIndexWrapper" to allow the local one This closes #51 --- README.md | 25 ++++++++- .../aem/cloud/AemCloudValidator.java | 22 +++++++- .../aem/cloud/AemCloudValidatorFactory.java | 7 ++- .../aem/cloud/AemCloudValidatorTest.java | 55 +++++++++++++++---- 4 files changed, 94 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4acfbe2..6ad5015 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Option | Mandatory | Description | Default Value | Since Version `allowReadOnlyMutablePaths` (or `allowVarNodeOutsideContainer` deprecated) | no | `true` means read-only paths (i.e. paths to which the service session used for mutable package installation on publish does not have write permission) should be allowed. Otherwise those will only be allowed in author-only packages included in a container package. | `false` | 1.2.0 `allowLibsNode` | no | `true` means that `libs` nodes are allowed in content packages. *Only set this to `true` when building packages which are part of the AEM product.* | `false` | 1.2.0 `allowHooksInMutableContent` | no | `true` means that JCR Install Hooks are allowed in content packages. *Only set this to `true` when building packages for local AEM SDK development or when explicitly allowed via OSGi configuration (details below in check description for install hooks).* | `false` | 1.3.0 +`allowLocalDiffIndexWrapper` | no | `true` means that the wrapper of the diff index required by the local AEM SDK and RDE is allowed. *Only set this to `true` when building packages for the local AEM SDK or RDE (details below in check description for the diff index).* | `false` | 1.5.0 # Included Checks @@ -77,9 +78,29 @@ 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` +## Enforce diff index wrapper matching the target environment -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 . +The node `/oak:index/diff.index` containing the `diff.json` used by Oak's simplified index management is exempt from the naming, `type=lucene` and `compatVersion` rules above. Its wrapper `.content.xml` must differ depending on the target environment though: Cloud Manager builds fail for an `oak:QueryIndexDefinition` while the local AEM SDK and RDE ignore every other node type (i.e. the custom indexes are not created there). + +By default the wrapper required by Cloud Manager is enforced, which must have `jcr:primaryType` set to `nt:unstructured`, `type` set to `lucene` and both `includedPaths` and `queryPaths` set to a non-existing path: + +``` + + + +``` + +With option `allowLocalDiffIndexWrapper` set to `true` the wrapper required by the local AEM SDK and RDE is allowed as well, which must have `jcr:primaryType` set to `oak:QueryIndexDefinition` and `type` set to `disabled`: + +``` + + + +``` + +Usually the wrapper is switched via Maven profiles, so the option should only be set in the profile used for building packages for the local AEM SDK and RDE. The control node `/oak:index/diff.index.optimizer` (Index Optimizer) must always be an `oak:QueryIndexDefinition` with `type` set to `disabled`. + +Further details in and . # Usage with Maven 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 01f4d00..0776872 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 @@ -55,6 +55,7 @@ public class AemCloudValidator implements NodePathValidator, MetaInfPathValidato 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"; + static final String VIOLATION_MESSAGE_INVALID_CLOUD_MANAGER_DIFF_INDEX_WRAPPER = "The diff-index node deployed via Cloud Manager must have jcr:primaryType='nt:unstructured', type='lucene' and both includedPaths and queryPaths set to a non-existing path (e.g. '/same') but found jcr:primaryType='%s', type='%s', includedPaths='%s' and queryPaths='%s'. Further details at https://experienceleague.adobe.com/en/docs/experience-cloud-kcs/kbarticles/ka-41939"; // this path is relative to META-INF private static final Path INSTALL_HOOK_PATH = Paths.get(Constants.VAULT_DIR, Constants.HOOKS_DIR); @@ -66,7 +67,8 @@ public class AemCloudValidator implements NodePathValidator, MetaInfPathValidato * 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 String DIFF_INDEX_PATH = "/oak:index/diff.index"; + private static final Collection DIFF_INDEX_CONTROL_NODE_PATHS = Arrays.asList(DIFF_INDEX_PATH, "/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( @@ -84,22 +86,26 @@ public class AemCloudValidator implements NodePathValidator, MetaInfPathValidato private final boolean allowReadOnlyMutablePaths; private final boolean allowLibsNode; private final boolean allowHooksInMutableContent; + private final boolean allowLocalDiffIndexWrapper; private boolean hasInstallHooks; private boolean hasImmutableNodes; private static final int MAX_NUM_VIOLATIONS_PER_TYPE = 5; private static final Name PN_TYPE = NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"); private static final Name PN_COMPAT_VERSION = NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "compatVersion"); + private static final Name PN_INCLUDED_PATHS = NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "includedPaths"); + private static final Name PN_QUERY_PATHS = NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "queryPaths"); private int numVarNodeViolations = 0; private int numLibNodeViolations = 0; private int numMutableNodeViolations = 0; - public AemCloudValidator(boolean allowReadOnlyMutablePaths, boolean allowLibsNode, boolean allowHooksInMutableContent, @Nullable PackageType packageType, + public AemCloudValidator(boolean allowReadOnlyMutablePaths, boolean allowLibsNode, boolean allowHooksInMutableContent, boolean allowLocalDiffIndexWrapper, @Nullable PackageType packageType, @Nullable ValidationContext containerValidationContext, @NotNull ValidationMessageSeverity defaultSeverity) { super(); this.allowReadOnlyMutablePaths = allowReadOnlyMutablePaths; this.allowLibsNode = allowLibsNode; this.allowHooksInMutableContent = allowHooksInMutableContent; + this.allowLocalDiffIndexWrapper = allowLocalDiffIndexWrapper; this.packageType = packageType; this.containerValidationContext = containerValidationContext; this.defaultSeverity = defaultSeverity; @@ -218,6 +224,18 @@ static boolean isPackagePathInstalledConditionally(String runMode, Path packageR @Override public @Nullable Collection validate(@NotNull DocViewNode2 node, @NotNull NodeContext nodeContext, boolean isRoot) { + String primaryType = node.getPrimaryType().orElse(""); + // the local wrapper makes Cloud Manager builds fail, compare with https://experienceleague.adobe.com/en/docs/experience-cloud-kcs/kbarticles/ka-41939 + if (DIFF_INDEX_PATH.equals(nodeContext.getNodePath()) && !(allowLocalDiffIndexWrapper && "oak:QueryIndexDefinition".equals(primaryType))) { + String indexType = node.getPropertyValue(PN_TYPE).orElse(""); + Optional includedPathsProperty = node.getProperty(PN_INCLUDED_PATHS); + Optional queryPathsProperty = node.getProperty(PN_QUERY_PATHS); + if (!"nt:unstructured".equals(primaryType) || !"lucene".equals(indexType) || !includedPathsProperty.isPresent() || !queryPathsProperty.isPresent()) { + return Collections.singleton(new ValidationMessage(defaultSeverity, + String.format(VIOLATION_MESSAGE_INVALID_CLOUD_MANAGER_DIFF_INDEX_WRAPPER, primaryType, indexType, + includedPathsProperty.map(p -> p.formatValue()).orElse("not set"), queryPathsProperty.map(p -> p.formatValue()).orElse("not set")))); + } + } if ("oak:QueryIndexDefinition".equals(node.getPrimaryType().orElse(""))) { if (DIFF_INDEX_CONTROL_NODE_PATHS.contains(nodeContext.getNodePath())) { String diffIndexType = node.getPropertyValue(PN_TYPE).orElse(""); diff --git a/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorFactory.java b/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorFactory.java index 8a16aeb..90f891b 100644 --- a/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorFactory.java +++ b/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorFactory.java @@ -30,6 +30,7 @@ public class AemCloudValidatorFactory implements ValidatorFactory { private static final String OPTION_ALLOW_READONLY_MUTABLE_PATHS = "allowReadOnlyMutablePaths"; private static final String OPTION_ALLOW_LIBS_NODE = "allowLibsNode"; private static final String OPTION_ALLOW_HOOKS_IN_MUTABLE_CONTENT = "allowHooksInMutableContent"; + private static final String OPTION_ALLOW_LOCAL_DIFF_INDEX_WRAPPER = "allowLocalDiffIndexWrapper"; private static final Logger LOGGER = LoggerFactory.getLogger(AemCloudValidatorFactory.class); @@ -45,13 +46,17 @@ public Validator createValidator(@NotNull ValidationContext context, @NotNull Va } boolean allowLibsNode = false; boolean allowHooksInMutableContent = false; + boolean allowLocalDiffIndexWrapper = false; if (settings.getOptions().containsKey(OPTION_ALLOW_LIBS_NODE)) { allowLibsNode = Boolean.parseBoolean(settings.getOptions().get(OPTION_ALLOW_LIBS_NODE)); } if (settings.getOptions().containsKey(OPTION_ALLOW_HOOKS_IN_MUTABLE_CONTENT)) { allowHooksInMutableContent = Boolean.parseBoolean(settings.getOptions().get(OPTION_ALLOW_HOOKS_IN_MUTABLE_CONTENT)); } - return new AemCloudValidator(allowReadOnlyMutablePaths, allowLibsNode, allowHooksInMutableContent, context.getProperties().getPackageType(), context.getContainerValidationContext(), settings.getDefaultSeverity()); + if (settings.getOptions().containsKey(OPTION_ALLOW_LOCAL_DIFF_INDEX_WRAPPER)) { + allowLocalDiffIndexWrapper = Boolean.parseBoolean(settings.getOptions().get(OPTION_ALLOW_LOCAL_DIFF_INDEX_WRAPPER)); + } + return new AemCloudValidator(allowReadOnlyMutablePaths, allowLibsNode, allowHooksInMutableContent, allowLocalDiffIndexWrapper, context.getProperties().getPackageType(), context.getContainerValidationContext(), settings.getDefaultSeverity()); } @Override 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 099fcdb..53a248c 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 @@ -70,27 +70,27 @@ void testIsPathWritableByDistributionJournalImporter() { @Test void testMutablePaths(){ - AemCloudValidator validator = new AemCloudValidator(false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(false, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = validator.validate("/var/subnode"); Assertions.assertFalse(messages.isEmpty()); } @Test void testAllowReadOnlyMutablePaths(){ - AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = validator.validate("/var/subnode"); Assertions.assertTrue(messages.isEmpty()); } @Test void testAllowHooksInMutableContent() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = new ArrayList<>(); Optional.ofNullable(validator.validateMetaInfPath(Paths.get("vault/hooks/install-hook.jar"))).ifPresent(messages::addAll); Optional.ofNullable(validator.done()).ifPresent(messages::addAll); Assertions.assertTrue(messages.stream().anyMatch(message -> message.getMessage().equals(AemCloudValidator.VIOLATION_MESSAGE_INSTALL_HOOK_IN_MUTABLE_PACKAGE))); - validator = new AemCloudValidator(true, false, true, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + validator = new AemCloudValidator(true, false, true, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); messages = new ArrayList<>(); Optional.ofNullable(validator.validateMetaInfPath(Paths.get("vault/hooks/install-hook.jar"))).ifPresent(messages::addAll); Optional.ofNullable(validator.done()).ifPresent(messages::addAll); @@ -99,7 +99,7 @@ void testAllowHooksInMutableContent() { @Test void testValidIndexDefinitions() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = new ArrayList<>(); List properties = Arrays.asList( new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), @@ -117,7 +117,7 @@ void testValidIndexDefinitions() { @Test void testInvalidLuceneIndexDefinitions() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = new ArrayList<>(); List properties = Arrays.asList( new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), @@ -134,7 +134,7 @@ void testInvalidLuceneIndexDefinitions() { @Test void testValidDiffIndexDefinitions() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, true, 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")); @@ -153,7 +153,7 @@ void testValidDiffIndexDefinitions() { @Test void testInvalidDiffIndexType() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, true, 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")); @@ -173,9 +173,44 @@ void testInvalidDiffIndexType() { )); } + @Test + void testCloudManagerDiffIndexWrapper() { + AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + List properties = Arrays.asList( + new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "nt:unstructured"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "lucene"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "includedPaths"), "/same"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "queryPaths"), "/same")); + DocViewNode2 node = new DocViewNode2(NameConstants.JCR_ROOT, properties); + // valid Cloud Manager wrapper + 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()); + // still allowed when allowing the local wrapper + validator = new AemCloudValidator(true, false, false, true, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); + MatcherAssert.assertThat(messages, Matchers.empty()); + // local wrapper and invalid wrapper are not supported by Cloud Manager + validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + messages = new ArrayList<>(); + properties = Arrays.asList( + new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "disabled")); + Optional.ofNullable(validator.validate(new DocViewNode2(NameConstants.JCR_ROOT, properties), context, true)).ifPresent(messages::addAll); + properties = Arrays.asList( + new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "nt:unstructured"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "lucene")); + Optional.ofNullable(validator.validate(new DocViewNode2(NameConstants.JCR_ROOT, properties), context, true)).ifPresent(messages::addAll); + MatcherAssert.assertThat(messages, Matchers.contains( + new ValidationMessage(ValidationMessageSeverity.ERROR, String.format(AemCloudValidator.VIOLATION_MESSAGE_INVALID_CLOUD_MANAGER_DIFF_INDEX_WRAPPER, "oak:QueryIndexDefinition", "disabled", "not set", "not set")), + new ValidationMessage(ValidationMessageSeverity.ERROR, String.format(AemCloudValidator.VIOLATION_MESSAGE_INVALID_CLOUD_MANAGER_DIFF_INDEX_WRAPPER, "nt:unstructured", "lucene", "not set", "not set")) + )); + } + @Test void testDiffIndexLookalikeNamesNotExempted() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, 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"), @@ -204,7 +239,7 @@ void testDiffIndexLookalikeNamesNotExempted() { @Test void testInvalidPropertyIndexDefinition() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = new ArrayList<>(); List properties = Arrays.asList( new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), From 7da537f05baf97e485edc03a12ea255bb46a60e9 Mon Sep 17 00:00:00 2001 From: Paulo Chang Date: Tue, 13 Oct 2026 16:41:15 +0200 Subject: [PATCH 2/3] Revert "Enforce diff index wrapper matching the target environment" This reverts commit d7f43fc16e983538147a1ba9957a552b094f9484. --- README.md | 25 +-------- .../aem/cloud/AemCloudValidator.java | 22 +------- .../aem/cloud/AemCloudValidatorFactory.java | 7 +-- .../aem/cloud/AemCloudValidatorTest.java | 55 ++++--------------- 4 files changed, 15 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 6ad5015..4acfbe2 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ Option | Mandatory | Description | Default Value | Since Version `allowReadOnlyMutablePaths` (or `allowVarNodeOutsideContainer` deprecated) | no | `true` means read-only paths (i.e. paths to which the service session used for mutable package installation on publish does not have write permission) should be allowed. Otherwise those will only be allowed in author-only packages included in a container package. | `false` | 1.2.0 `allowLibsNode` | no | `true` means that `libs` nodes are allowed in content packages. *Only set this to `true` when building packages which are part of the AEM product.* | `false` | 1.2.0 `allowHooksInMutableContent` | no | `true` means that JCR Install Hooks are allowed in content packages. *Only set this to `true` when building packages for local AEM SDK development or when explicitly allowed via OSGi configuration (details below in check description for install hooks).* | `false` | 1.3.0 -`allowLocalDiffIndexWrapper` | no | `true` means that the wrapper of the diff index required by the local AEM SDK and RDE is allowed. *Only set this to `true` when building packages for the local AEM SDK or RDE (details below in check description for the diff index).* | `false` | 1.5.0 # Included Checks @@ -78,29 +77,9 @@ There is a mandatory naming policy for Oak index definition node names which enf Further details in . -## Enforce diff index wrapper matching the target environment +## Enforce diff.index control nodes of type `disabled` -The node `/oak:index/diff.index` containing the `diff.json` used by Oak's simplified index management is exempt from the naming, `type=lucene` and `compatVersion` rules above. Its wrapper `.content.xml` must differ depending on the target environment though: Cloud Manager builds fail for an `oak:QueryIndexDefinition` while the local AEM SDK and RDE ignore every other node type (i.e. the custom indexes are not created there). - -By default the wrapper required by Cloud Manager is enforced, which must have `jcr:primaryType` set to `nt:unstructured`, `type` set to `lucene` and both `includedPaths` and `queryPaths` set to a non-existing path: - -``` - - - -``` - -With option `allowLocalDiffIndexWrapper` set to `true` the wrapper required by the local AEM SDK and RDE is allowed as well, which must have `jcr:primaryType` set to `oak:QueryIndexDefinition` and `type` set to `disabled`: - -``` - - - -``` - -Usually the wrapper is switched via Maven profiles, so the option should only be set in the profile used for building packages for the local AEM SDK and RDE. The control node `/oak:index/diff.index.optimizer` (Index Optimizer) must always be an `oak:QueryIndexDefinition` with `type` set to `disabled`. - -Further details in and . +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 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 0776872..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 @@ -55,7 +55,6 @@ public class AemCloudValidator implements NodePathValidator, MetaInfPathValidato 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"; - static final String VIOLATION_MESSAGE_INVALID_CLOUD_MANAGER_DIFF_INDEX_WRAPPER = "The diff-index node deployed via Cloud Manager must have jcr:primaryType='nt:unstructured', type='lucene' and both includedPaths and queryPaths set to a non-existing path (e.g. '/same') but found jcr:primaryType='%s', type='%s', includedPaths='%s' and queryPaths='%s'. Further details at https://experienceleague.adobe.com/en/docs/experience-cloud-kcs/kbarticles/ka-41939"; // this path is relative to META-INF private static final Path INSTALL_HOOK_PATH = Paths.get(Constants.VAULT_DIR, Constants.HOOKS_DIR); @@ -67,8 +66,7 @@ public class AemCloudValidator implements NodePathValidator, MetaInfPathValidato * 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 String DIFF_INDEX_PATH = "/oak:index/diff.index"; - private static final Collection DIFF_INDEX_CONTROL_NODE_PATHS = Arrays.asList(DIFF_INDEX_PATH, "/oak:index/diff.index.optimizer"); + 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( @@ -86,26 +84,22 @@ public class AemCloudValidator implements NodePathValidator, MetaInfPathValidato private final boolean allowReadOnlyMutablePaths; private final boolean allowLibsNode; private final boolean allowHooksInMutableContent; - private final boolean allowLocalDiffIndexWrapper; private boolean hasInstallHooks; private boolean hasImmutableNodes; private static final int MAX_NUM_VIOLATIONS_PER_TYPE = 5; private static final Name PN_TYPE = NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"); private static final Name PN_COMPAT_VERSION = NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "compatVersion"); - private static final Name PN_INCLUDED_PATHS = NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "includedPaths"); - private static final Name PN_QUERY_PATHS = NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "queryPaths"); private int numVarNodeViolations = 0; private int numLibNodeViolations = 0; private int numMutableNodeViolations = 0; - public AemCloudValidator(boolean allowReadOnlyMutablePaths, boolean allowLibsNode, boolean allowHooksInMutableContent, boolean allowLocalDiffIndexWrapper, @Nullable PackageType packageType, + public AemCloudValidator(boolean allowReadOnlyMutablePaths, boolean allowLibsNode, boolean allowHooksInMutableContent, @Nullable PackageType packageType, @Nullable ValidationContext containerValidationContext, @NotNull ValidationMessageSeverity defaultSeverity) { super(); this.allowReadOnlyMutablePaths = allowReadOnlyMutablePaths; this.allowLibsNode = allowLibsNode; this.allowHooksInMutableContent = allowHooksInMutableContent; - this.allowLocalDiffIndexWrapper = allowLocalDiffIndexWrapper; this.packageType = packageType; this.containerValidationContext = containerValidationContext; this.defaultSeverity = defaultSeverity; @@ -224,18 +218,6 @@ static boolean isPackagePathInstalledConditionally(String runMode, Path packageR @Override public @Nullable Collection validate(@NotNull DocViewNode2 node, @NotNull NodeContext nodeContext, boolean isRoot) { - String primaryType = node.getPrimaryType().orElse(""); - // the local wrapper makes Cloud Manager builds fail, compare with https://experienceleague.adobe.com/en/docs/experience-cloud-kcs/kbarticles/ka-41939 - if (DIFF_INDEX_PATH.equals(nodeContext.getNodePath()) && !(allowLocalDiffIndexWrapper && "oak:QueryIndexDefinition".equals(primaryType))) { - String indexType = node.getPropertyValue(PN_TYPE).orElse(""); - Optional includedPathsProperty = node.getProperty(PN_INCLUDED_PATHS); - Optional queryPathsProperty = node.getProperty(PN_QUERY_PATHS); - if (!"nt:unstructured".equals(primaryType) || !"lucene".equals(indexType) || !includedPathsProperty.isPresent() || !queryPathsProperty.isPresent()) { - return Collections.singleton(new ValidationMessage(defaultSeverity, - String.format(VIOLATION_MESSAGE_INVALID_CLOUD_MANAGER_DIFF_INDEX_WRAPPER, primaryType, indexType, - includedPathsProperty.map(p -> p.formatValue()).orElse("not set"), queryPathsProperty.map(p -> p.formatValue()).orElse("not set")))); - } - } if ("oak:QueryIndexDefinition".equals(node.getPrimaryType().orElse(""))) { if (DIFF_INDEX_CONTROL_NODE_PATHS.contains(nodeContext.getNodePath())) { String diffIndexType = node.getPropertyValue(PN_TYPE).orElse(""); diff --git a/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorFactory.java b/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorFactory.java index 90f891b..8a16aeb 100644 --- a/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorFactory.java +++ b/src/main/java/biz/netcentric/filevault/validator/aem/cloud/AemCloudValidatorFactory.java @@ -30,7 +30,6 @@ public class AemCloudValidatorFactory implements ValidatorFactory { private static final String OPTION_ALLOW_READONLY_MUTABLE_PATHS = "allowReadOnlyMutablePaths"; private static final String OPTION_ALLOW_LIBS_NODE = "allowLibsNode"; private static final String OPTION_ALLOW_HOOKS_IN_MUTABLE_CONTENT = "allowHooksInMutableContent"; - private static final String OPTION_ALLOW_LOCAL_DIFF_INDEX_WRAPPER = "allowLocalDiffIndexWrapper"; private static final Logger LOGGER = LoggerFactory.getLogger(AemCloudValidatorFactory.class); @@ -46,17 +45,13 @@ public Validator createValidator(@NotNull ValidationContext context, @NotNull Va } boolean allowLibsNode = false; boolean allowHooksInMutableContent = false; - boolean allowLocalDiffIndexWrapper = false; if (settings.getOptions().containsKey(OPTION_ALLOW_LIBS_NODE)) { allowLibsNode = Boolean.parseBoolean(settings.getOptions().get(OPTION_ALLOW_LIBS_NODE)); } if (settings.getOptions().containsKey(OPTION_ALLOW_HOOKS_IN_MUTABLE_CONTENT)) { allowHooksInMutableContent = Boolean.parseBoolean(settings.getOptions().get(OPTION_ALLOW_HOOKS_IN_MUTABLE_CONTENT)); } - if (settings.getOptions().containsKey(OPTION_ALLOW_LOCAL_DIFF_INDEX_WRAPPER)) { - allowLocalDiffIndexWrapper = Boolean.parseBoolean(settings.getOptions().get(OPTION_ALLOW_LOCAL_DIFF_INDEX_WRAPPER)); - } - return new AemCloudValidator(allowReadOnlyMutablePaths, allowLibsNode, allowHooksInMutableContent, allowLocalDiffIndexWrapper, context.getProperties().getPackageType(), context.getContainerValidationContext(), settings.getDefaultSeverity()); + return new AemCloudValidator(allowReadOnlyMutablePaths, allowLibsNode, allowHooksInMutableContent, context.getProperties().getPackageType(), context.getContainerValidationContext(), settings.getDefaultSeverity()); } @Override 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 53a248c..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 @@ -70,27 +70,27 @@ void testIsPathWritableByDistributionJournalImporter() { @Test void testMutablePaths(){ - AemCloudValidator validator = new AemCloudValidator(false, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = validator.validate("/var/subnode"); Assertions.assertFalse(messages.isEmpty()); } @Test void testAllowReadOnlyMutablePaths(){ - AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = validator.validate("/var/subnode"); Assertions.assertTrue(messages.isEmpty()); } @Test void testAllowHooksInMutableContent() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = new ArrayList<>(); Optional.ofNullable(validator.validateMetaInfPath(Paths.get("vault/hooks/install-hook.jar"))).ifPresent(messages::addAll); Optional.ofNullable(validator.done()).ifPresent(messages::addAll); Assertions.assertTrue(messages.stream().anyMatch(message -> message.getMessage().equals(AemCloudValidator.VIOLATION_MESSAGE_INSTALL_HOOK_IN_MUTABLE_PACKAGE))); - validator = new AemCloudValidator(true, false, true, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + validator = new AemCloudValidator(true, false, true, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); messages = new ArrayList<>(); Optional.ofNullable(validator.validateMetaInfPath(Paths.get("vault/hooks/install-hook.jar"))).ifPresent(messages::addAll); Optional.ofNullable(validator.done()).ifPresent(messages::addAll); @@ -99,7 +99,7 @@ void testAllowHooksInMutableContent() { @Test void testValidIndexDefinitions() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = new ArrayList<>(); List properties = Arrays.asList( new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), @@ -117,7 +117,7 @@ void testValidIndexDefinitions() { @Test void testInvalidLuceneIndexDefinitions() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = new ArrayList<>(); List properties = Arrays.asList( new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), @@ -134,7 +134,7 @@ void testInvalidLuceneIndexDefinitions() { @Test void testValidDiffIndexDefinitions() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, true, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + 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")); @@ -153,7 +153,7 @@ void testValidDiffIndexDefinitions() { @Test void testInvalidDiffIndexType() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, true, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + 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")); @@ -173,44 +173,9 @@ void testInvalidDiffIndexType() { )); } - @Test - void testCloudManagerDiffIndexWrapper() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); - List properties = Arrays.asList( - new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "nt:unstructured"), - new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "lucene"), - new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "includedPaths"), "/same"), - new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "queryPaths"), "/same")); - DocViewNode2 node = new DocViewNode2(NameConstants.JCR_ROOT, properties); - // valid Cloud Manager wrapper - 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()); - // still allowed when allowing the local wrapper - validator = new AemCloudValidator(true, false, false, true, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); - Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); - MatcherAssert.assertThat(messages, Matchers.empty()); - // local wrapper and invalid wrapper are not supported by Cloud Manager - validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); - messages = new ArrayList<>(); - properties = Arrays.asList( - new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), - new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "disabled")); - Optional.ofNullable(validator.validate(new DocViewNode2(NameConstants.JCR_ROOT, properties), context, true)).ifPresent(messages::addAll); - properties = Arrays.asList( - new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "nt:unstructured"), - new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "lucene")); - Optional.ofNullable(validator.validate(new DocViewNode2(NameConstants.JCR_ROOT, properties), context, true)).ifPresent(messages::addAll); - MatcherAssert.assertThat(messages, Matchers.contains( - new ValidationMessage(ValidationMessageSeverity.ERROR, String.format(AemCloudValidator.VIOLATION_MESSAGE_INVALID_CLOUD_MANAGER_DIFF_INDEX_WRAPPER, "oak:QueryIndexDefinition", "disabled", "not set", "not set")), - new ValidationMessage(ValidationMessageSeverity.ERROR, String.format(AemCloudValidator.VIOLATION_MESSAGE_INVALID_CLOUD_MANAGER_DIFF_INDEX_WRAPPER, "nt:unstructured", "lucene", "not set", "not set")) - )); - } - @Test void testDiffIndexLookalikeNamesNotExempted() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + 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"), @@ -239,7 +204,7 @@ void testDiffIndexLookalikeNamesNotExempted() { @Test void testInvalidPropertyIndexDefinition() { - AemCloudValidator validator = new AemCloudValidator(true, false, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); + AemCloudValidator validator = new AemCloudValidator(true, false, false, PackageType.CONTENT, null, ValidationMessageSeverity.ERROR); Collection messages = new ArrayList<>(); List properties = Arrays.asList( new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "oak:QueryIndexDefinition"), From 6163dec00eb0812a9948f338eb9287ef78aaaf64 Mon Sep 17 00:00:00 2001 From: Paulo Chang Date: Tue, 13 Oct 2026 16:46:03 +0200 Subject: [PATCH 3/3] Document and test diff index wrappers for Cloud Manager and local SDK/RDE Adobe requires different wrappers for /oak:index/diff.index on Cloud Manager and on the local AEM SDK/RDE in https://experienceleague.adobe.com/en/docs/experience-cloud-kcs/kbarticles/ka-41939 Both are already accepted, add a test for the Cloud Manager one and clarify that type="disabled" is only enforced for oak:QueryIndexDefinition --- README.md | 4 +++- .../validator/aem/cloud/AemCloudValidatorTest.java | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4acfbe2..a949c2f 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,9 @@ 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` when being of type `oak:QueryIndexDefinition`. Further details in . + +Cloud Manager and the local AEM SDK/RDE require different wrappers for `/oak:index/diff.index`, which are usually switched via Maven profiles: the wrapper for Cloud Manager is of type `nt:unstructured` (with `type="lucene"` and both `includedPaths` and `queryPaths` set to a non-existing path) and not checked by this validator, while the wrapper for the local AEM SDK and RDE is an `oak:QueryIndexDefinition` with `type="disabled"`. Further details in . # Usage with Maven 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 099fcdb..d83781e 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 @@ -149,6 +149,18 @@ void testValidDiffIndexDefinitions() { messages = new ArrayList<>(); Optional.ofNullable(validator.validate(node, context, true)).ifPresent(messages::addAll); MatcherAssert.assertThat(messages, Matchers.empty()); + // diff.index wrapper required by Cloud Manager (https://experienceleague.adobe.com/en/docs/experience-cloud-kcs/kbarticles/ka-41939) + properties = Arrays.asList( + new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "nt:unstructured"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "type"), "lucene"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "includedPaths"), "/same"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "queryPaths"), "/same"), + new DocViewProperty2(NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "async"), "async")); + node = new DocViewNode2(NameConstants.JCR_ROOT, properties); + context = new NodeContextImpl("/oak:index/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.empty()); } @Test