This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-analyser.git
The following commit(s) were added to refs/heads/master by this push:
new 04d2559 SLING-13205 Add a validator to detect conflicting RepoInit
statements (#57)
04d2559 is described below
commit 04d25597c14d1a4c3965d865dba751ee8c7cd5e5
Author: michalwedzik <[email protected]>
AuthorDate: Wed May 27 13:24:16 2026 +0200
SLING-13205 Add a validator to detect conflicting RepoInit statements
(#57)
Under some circumstances sling produces wrong paths in the repoinit, I
added logging to have the possibility to observe this situation in the logs
---------
Co-authored-by: sii12877 <[email protected]>
---
readme.md | 4 +
.../analyser/task/impl/CheckRepoInitConflicts.java | 62 +++++++++++
.../RepoInitConflictsValidator.java | 124 +++++++++++++++++++++
.../impl/repoinitconflicts/ValidationReport.java | 69 ++++++++++++
.../task/impl/CheckRepoInitConflictsTest.java | 124 +++++++++++++++++++++
.../RepoInitConflictsValidatorTest.java | 121 ++++++++++++++++++++
6 files changed, 504 insertions(+)
diff --git a/readme.md b/readme.md
index fe2ce18..3861ba1 100644
--- a/readme.md
+++ b/readme.md
@@ -106,6 +106,10 @@ This analyser requires additional configuration:
Checks the syntax of all repoinit sections.
+## `repoinit-conflicts`
+
+Validates repoinit sections for potential conflicts and reports findings as
warnings.
+
## `requirements-capabilities`
Checks bundle requirements/capabilities for consistency and completeness.
diff --git
a/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflicts.java
b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflicts.java
new file mode 100644
index 0000000..edf714b
--- /dev/null
+++
b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflicts.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl;
+
+import java.util.List;
+
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import
org.apache.sling.feature.analyser.task.impl.repoinitconflicts.RepoInitConflictsValidator;
+import
org.apache.sling.feature.analyser.task.impl.repoinitconflicts.ValidationReport;
+
+public class CheckRepoInitConflicts implements AnalyserTask {
+
+ @Override
+ public String getId() {
+ return "repoinit-conflicts";
+ }
+
+ @Override
+ public String getName() {
+ return "Repoinit Conflicts check";
+ }
+
+ /**
+ * Executes the Repoinit conflict validation.
+ * <p>
+ * This method retrieves the feature from the context, validates it using
+ * {@link RepoInitConflictsValidator}, and reports a warning if any
conflicts are found.
+ *
+ * @param context analyser task context containing the feature to validate
+ */
+ @Override
+ public void execute(final AnalyserTaskContext context) {
+ Feature feature = context.getFeature();
+
+ ValidationReport report = RepoInitConflictsValidator.validate(feature);
+
+ if (!report.hasConflicts()) {
+ return;
+ }
+
+ List<String> messages = report.generate();
+ messages.forEach(context::reportWarning);
+ }
+}
diff --git
a/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidator.java
b/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidator.java
new file mode 100644
index 0000000..ae4ffbc
--- /dev/null
+++
b/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidator.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl.repoinitconflicts;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.repoinit.parser.impl.ParseException;
+import org.apache.sling.repoinit.parser.impl.RepoInitParserImpl;
+import org.apache.sling.repoinit.parser.operations.CreatePath;
+import org.apache.sling.repoinit.parser.operations.Operation;
+import org.apache.sling.repoinit.parser.operations.PathSegmentDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressWarnings("java:S1874")
+public class RepoInitConflictsValidator {
+ private RepoInitConflictsValidator() {}
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(RepoInitConflictsValidator.class);
+
+ public static ValidationReport validate(final Feature feature) {
+ ValidationReport report = new ValidationReport();
+
+ final Extension repoinitExtension =
feature.getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT);
+
+ if (repoinitExtension == null
+ || repoinitExtension.getText() == null
+ || repoinitExtension.getType() != ExtensionType.TEXT) {
+ return report;
+ }
+
+ List<CreatePath[]> conflicts = findConflicts(repoinitExtension);
+
+ report.addConflicts(feature, conflicts);
+
+ return report;
+ }
+
+ private static List<CreatePath[]> findConflicts(Extension
repoinitExtension) {
+ try {
+ List<Operation> operations = new RepoInitParserImpl(new
StringReader(repoinitExtension.getText())).parse();
+
+ List<CreatePath> createPaths = operations.stream()
+ .filter(CreatePath.class::isInstance)
+ .map(op -> (CreatePath) op)
+ .collect(Collectors.toList());
+
+ return findConflictingPairs(createPaths);
+ } catch (ParseException e) {
+ LOGGER.error(
+ "Failed to parse repoinit statements, skipping conflict
validation. Error: {}", e.getMessage(), e);
+ return Collections.emptyList();
+ }
+ }
+
+ private static List<CreatePath[]> findConflictingPairs(List<CreatePath>
createPaths) {
+ int size = createPaths.size();
+ List<CreatePath[]> conflicts = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ for (int j = i + 1; j < size; j++) {
+ List<CreatePath[]> conflict =
findConflictPair(createPaths.get(i), createPaths.get(j));
+ if (!conflict.isEmpty()) {
+ conflicts.addAll(conflict);
+ }
+ }
+ }
+ return conflicts;
+ }
+
+ private static List<CreatePath[]> findConflictPair(CreatePath a,
CreatePath b) {
+ List<PathSegmentDefinition> aDefs = a.getDefinitions();
+ List<PathSegmentDefinition> bDefs = b.getDefinitions();
+
+ // different depth → no conflict
+ if (aDefs.size() != bDefs.size()) {
+ return Collections.emptyList();
+ }
+ List<CreatePath[]> conflicts = new ArrayList<>();
+ for (int i = 0; i < aDefs.size(); i++) {
+ PathSegmentDefinition aSeg = aDefs.get(i);
+ PathSegmentDefinition bSeg = bDefs.get(i);
+
+ // segments diverge → stop comparing this pair
+ if (!Objects.equals(aSeg.getSegment(), bSeg.getSegment())) {
+ return Collections.emptyList();
+ }
+
+ // same segment but different type → conflict
+ if (!Objects.equals(aSeg.getPrimaryType(), bSeg.getPrimaryType()))
{
+ CreatePath[] conflict = new CreatePath[2];
+ conflict[0] = a;
+ conflict[1] = b;
+
+ conflicts.add(conflict);
+ break;
+ }
+ }
+ return conflicts;
+ }
+}
diff --git
a/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/ValidationReport.java
b/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/ValidationReport.java
new file mode 100644
index 0000000..34945f6
--- /dev/null
+++
b/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/ValidationReport.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl.repoinitconflicts;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.feature.Feature;
+import org.apache.sling.repoinit.parser.operations.CreatePath;
+
+@SuppressWarnings("java:S1874")
+public class ValidationReport {
+
+ private final Map<Feature, List<CreatePath[]>> conflicts = new
LinkedHashMap<>();
+
+ public void addConflicts(Feature feature, List<CreatePath[]>
featureConflicts) {
+ if (!featureConflicts.isEmpty()) {
+ conflicts.put(feature, featureConflicts);
+ }
+ }
+
+ public boolean hasConflicts() {
+ return !conflicts.isEmpty();
+ }
+
+ public List<String> generate() {
+ List<String> messages = new ArrayList<>();
+ messages.add("Repoinit validation results:");
+
+ if (!hasConflicts()) {
+ messages.add("No issues found");
+ } else {
+ for (Map.Entry<Feature, List<CreatePath[]>> entry :
conflicts.entrySet()) {
+ Feature feature = entry.getKey();
+ List<CreatePath[]> featureConflicts = entry.getValue();
+
+ messages.add("Incorrect repoinit for feature " + feature);
+ messages.add("Found " + featureConflicts.size() + " sets of
conflicting repoinit statements");
+
+ for (CreatePath[] conflict : featureConflicts) {
+ messages.add("Conflicting statement :\n "
+ + conflict[0].asRepoInitString()
+ + "\n"
+ + conflict[1].asRepoInitString());
+ }
+ }
+ }
+
+ return messages;
+ }
+}
diff --git
a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflictsTest.java
b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflictsTest.java
new file mode 100644
index 0000000..35c17c7
--- /dev/null
+++
b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflictsTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CheckRepoInitConflictsTest {
+
+ @Test
+ void testConstants() {
+ final AnalyserTask task = new CheckRepoInitConflicts();
+
+ assertEquals("repoinit-conflicts", task.getId());
+ assertEquals("Repoinit Conflicts check", task.getName());
+ }
+
+ @Test
+ void shouldNotReportWarningWhenNoRepoinit() {
+ final AnalyserTaskContext ctx =
Mockito.mock(AnalyserTaskContext.class);
+
+ Feature feature = Mockito.mock(Feature.class);
+ org.apache.sling.feature.Extensions extensions =
Mockito.mock(org.apache.sling.feature.Extensions.class);
+
+ Mockito.when(ctx.getFeature()).thenReturn(feature);
+ Mockito.when(feature.getExtensions()).thenReturn(extensions);
+ Mockito.when(extensions.getByName("repoinit")).thenReturn(null);
+
+ CheckRepoInitConflicts task = new CheckRepoInitConflicts();
+ task.execute(ctx);
+
+ Mockito.verify(ctx).getFeature();
+ Mockito.verifyNoMoreInteractions(ctx);
+ }
+
+ @Test
+ void shouldNotReportWarningWhenNoConflicts() {
+ final AnalyserTaskContext ctx =
Mockito.mock(AnalyserTaskContext.class);
+
+ Feature feature = featureWithExtension(
+ textExtension("create path (sling:Folder) /apps/a/b\n" +
"create path (sling:Folder) /apps/a/c"));
+
+ Mockito.when(ctx.getFeature()).thenReturn(feature);
+
+ CheckRepoInitConflicts task = new CheckRepoInitConflicts();
+ task.execute(ctx);
+
+ Mockito.verify(ctx).getFeature();
+ Mockito.verifyNoMoreInteractions(ctx);
+ }
+
+ @Test
+ void shouldReportWarningWhenConflictExists() {
+ final AnalyserTaskContext ctx =
Mockito.mock(AnalyserTaskContext.class);
+
+ Feature feature =
+ featureWithExtension(textExtension("create path (sling:Folder)
/apps/a/b(cq:ClientLibraryFolder)\n"
+ + "create path (sling:Folder) /apps/a/b"));
+
+ Mockito.when(ctx.getFeature()).thenReturn(feature);
+
+ CheckRepoInitConflicts task = new CheckRepoInitConflicts();
+ task.execute(ctx);
+
+ Mockito.verify(ctx).getFeature();
+ Mockito.verify(ctx).reportWarning(Mockito.contains("conflicting
repoinit"));
+ Mockito.verify(ctx).reportWarning(Mockito.contains("Conflicting
statement"));
+ }
+
+ @Test
+ void shouldIgnoreInvalidRepoinitSyntax() {
+ final AnalyserTaskContext ctx =
Mockito.mock(AnalyserTaskContext.class);
+
+ Feature feature = featureWithExtension(textExtension("invalid $$$"));
+
+ Mockito.when(ctx.getFeature()).thenReturn(feature);
+
+ CheckRepoInitConflicts task = new CheckRepoInitConflicts();
+ task.execute(ctx);
+
+ Mockito.verify(ctx).getFeature();
+ Mockito.verifyNoMoreInteractions(ctx);
+ }
+
+ private Extension textExtension(String text) {
+ Extension extension = Mockito.mock(Extension.class);
+ Mockito.when(extension.getType()).thenReturn(ExtensionType.TEXT);
+ Mockito.when(extension.getText()).thenReturn(text);
+ return extension;
+ }
+
+ private Feature featureWithExtension(Extension extension) {
+ Feature feature = Mockito.mock(Feature.class);
+ org.apache.sling.feature.Extensions extensions =
Mockito.mock(org.apache.sling.feature.Extensions.class);
+
+ Mockito.when(feature.getExtensions()).thenReturn(extensions);
+ Mockito.when(extensions.getByName("repoinit")).thenReturn(extension);
+
+ return feature;
+ }
+}
diff --git
a/src/test/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidatorTest.java
b/src/test/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidatorTest.java
new file mode 100644
index 0000000..b2e538b
--- /dev/null
+++
b/src/test/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidatorTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl.repoinitconflicts;
+
+import java.util.List;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RepoInitConflictsValidatorTest {
+
+ @Test
+ void shouldReturnNoIssuesWhenFeatureHasNoRepoinit() {
+ Feature feature = mock(Feature.class);
+
when(feature.getExtensions()).thenReturn(mock(org.apache.sling.feature.Extensions.class));
+ when(feature.getExtensions().getByName("repoinit")).thenReturn(null);
+
+ ValidationReport report = RepoInitConflictsValidator.validate(feature);
+
+ assertFalse(report.hasConflicts());
+ assertTrue(report.generate().contains("No issues found"));
+ }
+
+ @Test
+ void shouldReturnNoIssuesForCorrectRepoinit() {
+ Extension extension =
+ textExtension("create path (sling:Folder) /apps/a/b\n" +
"create path (sling:Folder) /apps/a/c\n"
+ + "create path (sling:Folder)
/apps/a/c/d(sling:OrderedFolder)");
+
+ Feature feature = featureWithExtension(extension);
+
+ ValidationReport report = RepoInitConflictsValidator.validate(feature);
+
+ assertFalse(report.hasConflicts());
+ assertTrue(report.generate().contains("No issues found"));
+ }
+
+ @Test
+ void shouldReportConflictForSamePathDifferentType() {
+ Extension extension = textExtension(
+ "create path (sling:Folder) /apps/a/b(sling:OrderedFolder)\n"
+ "create path (sling:Folder) /apps/a/b");
+
+ Feature feature = featureWithExtension(extension);
+
+ ValidationReport report = RepoInitConflictsValidator.validate(feature);
+ assertTrue(report.hasConflicts());
+
+ List<String> result = report.generate();
+ assertTrue(result.get(1).contains("Incorrect repoinit for feature"));
+ assertTrue(result.get(2).contains("Found 1 sets of conflicting
repoinit statements"));
+ assertTrue(result.get(3).contains("/apps/a/b"));
+ }
+
+ @Test
+ void shouldReportMultipleConflicts() {
+ Extension extension = textExtension("create path (sling:Folder)
/apps/a/b(sling:OrderedFolder)\n"
+ + "create path (sling:Folder) /apps/a/b\n"
+ + "create path (sling:Folder) /apps/x/y(sling:OrderedFolder)\n"
+ + "create path (sling:Folder) /apps/x/y");
+
+ Feature feature = featureWithExtension(extension);
+
+ ValidationReport report = RepoInitConflictsValidator.validate(feature);
+ assertTrue(report.hasConflicts());
+
+ List<String> result = report.generate();
+ assertTrue(result.get(2).contains("Found 2 sets of conflicting
repoinit statements"));
+ }
+
+ @Test
+ void shouldIgnoreInvalidRepoinitSyntax() {
+ Extension extension = textExtension("invalid $$$");
+
+ Feature feature = featureWithExtension(extension);
+
+ ValidationReport report = RepoInitConflictsValidator.validate(feature);
+
+ assertFalse(report.hasConflicts());
+ assertTrue(report.generate().contains("No issues found"));
+ }
+
+ private Extension textExtension(String text) {
+ Extension extension = mock(Extension.class);
+ when(extension.getType()).thenReturn(ExtensionType.TEXT);
+ when(extension.getText()).thenReturn(text);
+ return extension;
+ }
+
+ private Feature featureWithExtension(Extension extension) {
+ Feature feature = mock(Feature.class);
+ org.apache.sling.feature.Extensions extensions =
mock(org.apache.sling.feature.Extensions.class);
+
+ when(feature.getExtensions()).thenReturn(extensions);
+ when(extensions.getByName("repoinit")).thenReturn(extension);
+
+ return feature;
+ }
+}