SLING-9841: Adding support for skipping the loading of a path when a runmode matches
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoader.java
index 84dbeb6..d8160e9 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoader.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoader.java
@@ -33,6 +33,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.StringTokenizer;
 
 import javax.jcr.Item;
@@ -62,10 +63,13 @@
     // bundles whose registration failed and should be retried
     private List<Bundle> delayedBundles;
 
-    public BundleContentLoader(BundleHelper bundleHelper, ContentReaderWhiteboard contentReaderWhiteboard) {
+    private Set<String> runmodes;
+
+    public BundleContentLoader(BundleHelper bundleHelper, ContentReaderWhiteboard contentReaderWhiteboard, Set<String> runmodes) {
         super(contentReaderWhiteboard);
         this.bundleHelper = bundleHelper;
         this.delayedBundles = new LinkedList<>();
+        this.runmodes = runmodes;
     }
 
     public void dispose() {
@@ -215,7 +219,7 @@
         try {
             while (pathIter.hasNext()) {
                 final PathEntry pathEntry = pathIter.next();
-                if (!contentAlreadyLoaded || pathEntry.isOverwrite()) {
+                if (validRunmode(pathEntry) && (!contentAlreadyLoaded || pathEntry.isOverwrite())) {
                     String workspace = pathEntry.getWorkspace();
                     final Session targetSession;
                     if (workspace != null) {
@@ -290,13 +294,25 @@
     }
 
     /**
+     * Checks if the path entry has a runmode restriction set and the runmode isn't set in the Sling instance.
+     * 
+     * @param pathEntry the path entry to check
+     * @return true if the required runmode setting is not set or the instance runmodes doesn't contain the runmode
+     */
+    private boolean validRunmode(PathEntry pathEntry) {
+        return pathEntry.getSkipRunmode() == null || "".equals(pathEntry.getSkipRunmode())
+            || !runmodes.contains(pathEntry.getSkipRunmode());
+    }
+
+    /**
      * Handle content installation for a single path.
      *
      * @param bundle        The bundle containing the content.
      * @param path          The path
      * @param configuration
      * @param parent        The parent node.
-     * @param createdNodes  An optional list to store all new nodes. This list is used for an uninstall
+     * @param createdNodes  An optional list to store all new nodes. This list is
+     *                      used for an uninstall
      * @throws RepositoryException
      */
     private void installFromPath(final Bundle bundle, final String path, final PathEntry configuration, final Node parent, final List<String> createdNodes, final DefaultContentCreator contentCreator) throws RepositoryException {
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java
index d68d605..a9874aa 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java
@@ -94,6 +94,9 @@
     @Reference
     private ContentReaderWhiteboard contentReaderWhiteboard;
 
+    @Reference
+    private SlingSettingsService slingSettings;
+
     /**
      * The initial content loader which is called to load initial content up
      * into the repository when the providing bundle is installed.
@@ -222,7 +225,7 @@
     @Activate
     protected synchronized void activate(BundleContext bundleContext) {
         this.slingId = this.settingsService.getSlingId();
-        this.bundleContentLoader = new BundleContentLoader(this, contentReaderWhiteboard);
+        this.bundleContentLoader = new BundleContentLoader(this, contentReaderWhiteboard, slingSettings.getRunModes());
 
         bundleContext.addBundleListener(this);
 
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java
index 38898ae..4bf7d78 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java
@@ -93,6 +93,14 @@
      */
     public static final String IGNORE_CONTENT_READERS_DIRECTIVE = "ignoreImportProviders";
 
+    /**
+     * If the content should be skipped with a particular runmode. By
+     * default this will be an empty string which means the content will be
+     * installed regardless of the runmode
+     * @since 2.4.0
+     */
+    public static final String SKIP_RUNMODE = "skipRunmode";
+
     private final boolean propertyMerge;
     
     private final boolean nodeMerge;
@@ -106,6 +114,9 @@
     /** Should existing content properties be overwritten? */
     private final boolean overwriteProperties;
 
+    /** The content at the path will be skipped for this runmode */
+    private final String skipRunmode;
+
     /** Should existing content be uninstalled? */
     private final boolean uninstall;
 
@@ -233,6 +244,9 @@
         }
 
         // workspace directive
+        this.skipRunmode = entry.getDirectiveValue(SKIP_RUNMODE);
+
+        // workspace directive
         final String workspaceValue = entry.getDirectiveValue(WORKSPACE_DIRECTIVE);
         if (pathValue != null) {
             this.workspace = workspaceValue;
@@ -293,6 +307,10 @@
         return this.ignoreContentReaders.contains(extension);
     }
 
+    public String getSkipRunmode(){
+        return this.skipRunmode;
+    }
+
     public String getTarget() {
         return target;
     }
@@ -301,12 +319,10 @@
         return workspace;
     }
 
-
     public boolean isPropertyMerge() {
         return this.propertyMerge;
     }
 
-
     public boolean isMerge() {
         return this.nodeMerge;
     }
diff --git a/src/test/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoaderTest.java b/src/test/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoaderTest.java
index a985701..5004254 100644
--- a/src/test/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoaderTest.java
+++ b/src/test/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoaderTest.java
@@ -21,8 +21,11 @@
 import static java.util.Collections.singletonMap;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
 import static org.junit.Assert.assertThat;
 
+import java.util.Collections;
+
 import javax.jcr.Session;
 
 import org.apache.sling.api.resource.Resource;
@@ -60,7 +63,7 @@
 
         ContentReaderWhiteboard whiteboard = context.getService(ContentReaderWhiteboard.class);
 
-        contentLoader = new BundleContentLoader(bundleHelper, whiteboard);
+        contentLoader = new BundleContentLoader(bundleHelper, whiteboard, Collections.singleton("seed"));
     }
 
 
@@ -77,6 +80,31 @@
         assertThat("sling:resourceType was not properly set", imported.getResourceType(), equalTo("sling:Folder"));
     }
 
+
+    @Test
+    public void skippedRunmode() throws Exception {
+        Bundle mockBundle = newBundleWithInitialContent("SLING-INF/libs/app/skipped;path:=/libs/app/skipped;skipRunmode:=seed");
+        contentLoader.registerBundle(context.resourceResolver().adaptTo(Session.class), mockBundle, false);
+        Resource imported = context.resourceResolver().getResource("/libs/app/skipped");
+        assertThat("Import should have been skipped", imported, nullValue());
+    }
+
+    @Test
+    public void passedRunmode() throws Exception {
+        Bundle mockBundle = newBundleWithInitialContent("SLING-INF/libs/app/passed;path:=/libs/app/passed;skipRunmode:=runtime");
+        contentLoader.registerBundle(context.resourceResolver().adaptTo(Session.class), mockBundle, false);
+        Resource imported = context.resourceResolver().getResource("/libs/app/passed");
+        assertThat("Resource was not imported", imported, notNullValue());
+    }
+
+    @Test
+    public void emptyRunmode() throws Exception {
+        Bundle mockBundle = newBundleWithInitialContent("SLING-INF/libs/app/empty;path:=/libs/app/empty");
+        contentLoader.registerBundle(context.resourceResolver().adaptTo(Session.class), mockBundle, false);
+        Resource imported = context.resourceResolver().getResource("/libs/app/empty");
+        assertThat("Resource was not imported", imported, notNullValue());
+    }
+
     @Test
     public void loadContentWithRootPath() throws Exception {