SLING-11203 Introduce requireImportProvider directive (#15)

A declared requireImportProvider directive so the author can ensure that
the files are processed as intended
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/ImportOptions.java b/src/main/java/org/apache/sling/jcr/contentloader/ImportOptions.java
index f66d680..4fc2e3e 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/ImportOptions.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/ImportOptions.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sling.jcr.contentloader;
 
+import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ConsumerType;
 
 /**
@@ -86,4 +87,13 @@
 	 */
 	public abstract boolean isIgnoredImportProvider(String extension);
 
+    /**
+     * Check if the given entry name should require a matching registered 
+     * import provider.
+     *
+     * @param name the entry name to check
+     * @return true to require an import provider, false otherwise
+     */
+    public abstract boolean isImportProviderRequired(@NotNull String name);
+
 }
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/PathEntry.java b/src/main/java/org/apache/sling/jcr/contentloader/PathEntry.java
index cae3970..527b63f 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/PathEntry.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/PathEntry.java
@@ -109,6 +109,14 @@
     public static final String IGNORE_CONTENT_READERS_DIRECTIVE = "ignoreImportProviders";
 
     /**
+     * The require content readers directive specifying which of the available 
+     * {@link org.apache.sling.jcr.contentloader.ContentReader}s should exist before
+     * content loading. This is a string value that defaults to the emptystring.
+     * @since 2.5.2
+     */
+    public static final String REQUIRE_CONTENT_READERS_DIRECTIVE = "requireImportProviders";
+
+    /**
      * The flag "maven:mount" is not actually used by the JCR Content Loader. It can be used
      * to signal to the "fsmount" goal of the sling-maven-plugin to ignore a certain Sling-Initial-Content entry
      * of a Maven project when "sling:mount" is executed on the command line.
@@ -128,6 +136,7 @@
         CHECKIN_DIRECTIVE,
         AUTOCHECKOUT_DIRECTIVE,
         IGNORE_CONTENT_READERS_DIRECTIVE,
+        REQUIRE_CONTENT_READERS_DIRECTIVE,
         MAVEN_MOUNT_DIRECTIVE
     ));
 
@@ -156,6 +165,9 @@
     /** Which content readers should be ignored? @since 2.0.4 */
     private final List<String> ignoreContentReaders;
 
+    /** Which content readers should be required? @since 2.5.2 */
+    private final List<String> requireContentReaders;
+
     /**
      * Target path where initial content will be loaded. If it´s null then
      * target node is the root node
@@ -327,6 +339,16 @@
             }
         }
 
+        // expand directive
+        this.requireContentReaders = new ArrayList<>();
+        final String requireContentReadersValue = entry.getDirectiveValue(REQUIRE_CONTENT_READERS_DIRECTIVE);
+        if ( requireContentReadersValue != null && requireContentReadersValue.length() > 0 ) {
+            final StringTokenizer st = new StringTokenizer(requireContentReadersValue, ",");
+            while ( st.hasMoreTokens() ) {
+                this.requireContentReaders.add(st.nextToken());
+            }
+        }
+
         // workspace directive
         final String workspaceValue = entry.getDirectiveValue(WORKSPACE_DIRECTIVE);
         if (pathValue != null) {
@@ -392,6 +414,42 @@
         return new HashSet<>(ignoreContentReaders);
     }
 
+    @Override
+    public boolean isImportProviderRequired(@NotNull String name) {
+        boolean required = false;
+
+        if (!this.requireContentReaders.isEmpty()) {
+            // a directive was supplied, so use a filter to check if the 
+            //  name ends with the suffix and is not listed in the ignored 
+            //  import provider set
+            required = this.requireContentReaders.stream()
+                            .anyMatch(suffix ->
+                                        // verify the file suffix matches
+                                        hasNameSuffix(name, suffix) &&
+                                        // and not one of the ignored providers
+                                        !isIgnoredImportProvider(suffix));
+        }
+        return required;
+    }
+
+    /**
+     * Check if the name ends with the supplied suffix
+     * 
+     * @param name the name to check
+     * @param suffix the suffix to check
+     * @return true if the name ends with the suffix
+     */
+    private boolean hasNameSuffix(String name, String suffix) {
+               // ensure neither arg is null
+        return name != null && suffix != null &&
+               // is longer than suffix
+               name.length() > suffix.length() && 
+               // ends with suffix
+               name.endsWith(suffix) &&  
+               // dot before the suffix
+               '.' == name.charAt(name.length() - suffix.length() - 1);
+    }
+
     public String getTarget() {
         return target;
     }
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 71bd020..acfce12 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
@@ -105,9 +105,28 @@
     }
 
     /**
+     * Retry loading bundles that have previously been delayed 
+     * @param metadataSession the JCR Session for reading/writing metadata
+     */
+    public void retryDelayedBundles(final Session metadataSession) {
+        // handle delayed bundles, might help now
+        int currentSize = -1;
+        for (int i = delayedBundles.size(); i > 0 && currentSize != delayedBundles.size()
+                && !delayedBundles.isEmpty(); i--) {
+            for (Iterator<Bundle> di = delayedBundles.iterator(); di.hasNext();) {
+                Bundle delayed = di.next();
+                if (registerBundleInternal(metadataSession, delayed, true, false)) {
+                    di.remove();
+                }
+            }
+            currentSize = delayedBundles.size();
+        }
+    }
+
+    /**
      * Register a bundle and install its content.
      *
-     * @param metadataSession the JCR Session for reading/writing metadat
+     * @param metadataSession the JCR Session for reading/writing metadata
      * @param bundle the bundle to install
      */
     public void registerBundle(final Session metadataSession, final Bundle bundle, final boolean isUpdate) {
@@ -120,18 +139,7 @@
         log.debug("Registering bundle {} for content loading.", bundle.getSymbolicName());
 
         if (registerBundleInternal(metadataSession, bundle, false, isUpdate)) {
-            // handle delayed bundles, might help now
-            int currentSize = -1;
-            for (int i = delayedBundles.size(); i > 0 && currentSize != delayedBundles.size()
-                    && !delayedBundles.isEmpty(); i--) {
-                for (Iterator<Bundle> di = delayedBundles.iterator(); di.hasNext();) {
-                    Bundle delayed = di.next();
-                    if (registerBundleInternal(metadataSession, delayed, true, false)) {
-                        di.remove();
-                    }
-                }
-                currentSize = delayedBundles.size();
-            }
+            retryDelayedBundles(metadataSession);
         } else if (!isUpdate) {
             // add to delayed bundles - if this is not an update!
             delayedBundles.add(bundle);
@@ -188,6 +196,12 @@
                 bundleHelper.unlockBundleContentInfo(metadataSession, bundle, success, createdNodes);
             }
 
+        } catch (ContentReaderUnavailableException crue) {
+            // if we are retrying we already logged this message once, so we
+            // won't log it again
+            if (!isRetry) {
+                log.warn("Cannot load initial content for bundle {} : {}", bundle.getSymbolicName(), crue.getMessage());
+            }
         } catch (RepositoryException re) {
             // if we are retrying we already logged this message once, so we
             // won't log it again
@@ -244,7 +258,7 @@
      * @return If the content should be removed on uninstall, a list of top nodes
      */
     private List<String> installContent(final Session defaultSession, final Bundle bundle,
-            final Iterator<PathEntry> pathIter, final boolean contentAlreadyLoaded) throws RepositoryException {
+            final Iterator<PathEntry> pathIter, final boolean contentAlreadyLoaded) throws RepositoryException, ContentReaderUnavailableException {
 
         final List<String> createdNodes = new ArrayList<>();
         final Map<String, Session> createdSessions = new HashMap<>();
@@ -352,7 +366,7 @@
      */
     private void installFromPath(final Bundle bundle, final String path, final PathEntry configuration,
             final Node parent, final List<String> createdNodes, final DefaultContentCreator contentCreator)
-            throws RepositoryException {
+            throws RepositoryException, ContentReaderUnavailableException {
 
         // init content creator
         contentCreator.init(configuration, getContentReaders(), createdNodes, null);
@@ -434,7 +448,7 @@
      */
     private void handleFile(final String entry, final Bundle bundle, final Map<String, Node> processedEntries,
             final PathEntry configuration, final Node parent, final List<String> createdNodes,
-            final DefaultContentCreator contentCreator) throws RepositoryException {
+            final DefaultContentCreator contentCreator) throws RepositoryException, ContentReaderUnavailableException {
 
         final URL file = bundle.getEntry(entry);
         final String name = getName(entry);
@@ -466,7 +480,14 @@
                     log.warn("No node created for file {} {}", file, name);
                 }
             } else {
-                log.debug("Can't find content reader for entry {} at {}", entry, name);
+                // if we require a ContentReader for this entry but didn't find one
+                //   then throw an exception to stop processing this bundle and put 
+                //   it into the delayedBundles list to retry later
+                if (configuration.isImportProviderRequired(name)) {
+                    throw new ContentReaderUnavailableException(String.format("Unable to locate a required content reader for entry %s", entry));
+                } else {
+                    log.debug("Can't find content reader for entry {} at {}", entry, name);
+                }
             }
 
             // otherwise just place as file
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoaderListener.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoaderListener.java
index f00f231..09dfff0 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoaderListener.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/BundleContentLoaderListener.java
@@ -64,7 +64,7 @@
         Constants.SERVICE_DESCRIPTION
                 + "=Apache Sling Content Loader Implementation" }, immediate = true, configurationPolicy = ConfigurationPolicy.OPTIONAL)
 @Designate(ocd = BundleContentLoaderConfiguration.class, factory = false)
-public class BundleContentLoaderListener implements SynchronousBundleListener, BundleHelper {
+public class BundleContentLoaderListener implements SynchronousBundleListener, BundleHelper, ContentReaderWhiteboardListener {
 
     public static final String PROPERTY_CONTENT_LOADED = "content-loaded";
     public static final String PROPERTY_CONTENT_LOADED_AT = "content-load-time";
@@ -133,6 +133,26 @@
     @Reference(target = "(component.name=org.apache.sling.jcr.contentloader.internal.readers.ZipReader)")
     private ContentReader mandatoryContentReader4;
 
+    // ---------- ContentReaderWhiteboardListener -----------------------------------------------
+
+    /**
+     * When a new ContentReader component arrives, try to re-process any
+     * delayed bundles in case the new ContentReader makes it possible to
+     * process them now
+     */
+    @Override
+    public synchronized void handleContentReaderAdded(ContentReader operation) {
+        Session session = null;
+        try {
+            session = this.getSession();
+            bundleContentLoader.retryDelayedBundles(session);
+        } catch (Exception t) {
+            log.error("handleContentReaderAdded: Problem loading initial content of delayed bundles", t);
+        } finally {
+            this.ungetSession(session);
+        }
+    }
+
     // ---------- BundleListener -----------------------------------------------
 
     /**
@@ -240,6 +260,8 @@
         this.bundleContentLoader = new BundleContentLoader(this, contentReaderWhiteboard, configuration);
 
         bundleContext.addBundleListener(this);
+        // start listening for new ContentReader components
+        contentReaderWhiteboard.setListener(this);
 
         Session session = null;
         try {
@@ -289,6 +311,8 @@
     @Deactivate
     protected synchronized void deactivate(BundleContext bundleContext) {
         bundleContext.removeBundleListener(this);
+        // stop listening for new ContentReader components
+        contentReaderWhiteboard.removeListener();
 
         if (this.bundleContentLoader != null) {
             this.bundleContentLoader.dispose();
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderUnavailableException.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderUnavailableException.java
new file mode 100644
index 0000000..0396d8e
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderUnavailableException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.jcr.contentloader.internal;
+
+/**
+ * This exception is thrown when a required ContentReader is not yet
+ * available.
+ */
+class ContentReaderUnavailableException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public ContentReaderUnavailableException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderWhiteboard.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderWhiteboard.java
index 3c6062e..0cbbac5 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderWhiteboard.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderWhiteboard.java
@@ -30,10 +30,19 @@
     })
 public class ContentReaderWhiteboard {
 
+    private ContentReaderWhiteboardListener listener;
+
     private Map<String, ContentReader> readersByExtension = new LinkedHashMap<>();
 
     private Map<String, ContentReader> readersByType = new LinkedHashMap<>();
 
+    public void setListener(ContentReaderWhiteboardListener listener) {
+        this.listener = listener;
+    }
+    public void removeListener() {
+        setListener(null);
+    }
+
     public Map<String, ContentReader> getReadersByExtension() {
         return readersByExtension;
     }
@@ -61,6 +70,12 @@
                 }
             }
         }
+
+        // notify the listener that we have a new content reader
+        ContentReaderWhiteboardListener l = this.listener;
+        if (l != null) {
+            l.handleContentReaderAdded(operation);
+        }
     }
 
     protected void unbindContentReader(final Map<String, Object> properties) {
@@ -81,4 +96,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderWhiteboardListener.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderWhiteboardListener.java
new file mode 100644
index 0000000..f572501
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReaderWhiteboardListener.java
@@ -0,0 +1,26 @@
+/*
+ * 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.jcr.contentloader.internal;
+
+import org.apache.sling.jcr.contentloader.ContentReader;
+
+/**
+ * Callback interface to allow the BundleContentLoaderListener to do work
+ * when a new ContentReader component arrives
+ */
+public interface ContentReaderWhiteboardListener {
+
+    void handleContentReaderAdded(final ContentReader operation);
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/contentloader/ImportOptionsFactory.java b/src/test/java/org/apache/sling/jcr/contentloader/ImportOptionsFactory.java
index c1ab108..9708dcb 100644
--- a/src/test/java/org/apache/sling/jcr/contentloader/ImportOptionsFactory.java
+++ b/src/test/java/org/apache/sling/jcr/contentloader/ImportOptionsFactory.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sling.jcr.contentloader;
 
+import org.jetbrains.annotations.NotNull;
+
 public final class ImportOptionsFactory {
     
     public static final int NO_OPTIONS = 0;
@@ -33,8 +35,9 @@
     public static final int IGNORE_IMPORT_PROVIDER = 0x1 << 5;
     
     public static final int CHECK_IN = 0x1 << 6;
-    
-    
+
+    public static final int REQUIRE_IMPORT_PROVIDER = 0x1 << 7;
+
     public static ImportOptions createImportOptions(int options){
         return new ImportOptions() {
             @Override
@@ -63,6 +66,11 @@
             }
 
             @Override
+            public boolean isImportProviderRequired(@NotNull String name) {
+                return (options & REQUIRE_IMPORT_PROVIDER) > NO_OPTIONS;
+            }
+
+            @Override
             public boolean isPropertyMerge() {
                 return (options & SYNCH_PROPERTIES) > NO_OPTIONS;
             }
diff --git a/src/test/java/org/apache/sling/jcr/contentloader/internal/SLING11203Test.java b/src/test/java/org/apache/sling/jcr/contentloader/internal/SLING11203Test.java
new file mode 100644
index 0000000..d06d281
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/contentloader/internal/SLING11203Test.java
@@ -0,0 +1,170 @@
+/*
+ * 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.jcr.contentloader.internal;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+
+import javax.jcr.Session;
+
+import org.apache.sling.jcr.contentloader.ContentReader;
+import org.apache.sling.jcr.contentloader.internal.readers.JsonReader;
+import org.apache.sling.jcr.contentloader.internal.readers.OrderedJsonReader;
+import org.apache.sling.jcr.contentloader.internal.readers.XmlReader;
+import org.apache.sling.jcr.contentloader.internal.readers.ZipReader;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.osgi.framework.Bundle;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Testing content loader waiting for required content reader
+ */
+public class SLING11203Test {
+
+    protected org.slf4j.Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Rule
+    public final SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+    private BundleContentLoaderListener bundleHelper;
+
+    @Rule
+    public TestRule watcher = new TestWatcher() {
+
+        /* (non-Javadoc)
+         * @see org.junit.rules.TestWatcher#starting(org.junit.runner.Description)
+         */
+        @Override
+        protected void starting(Description description) {
+            logger.info("Starting test: {}", description.getMethodName());
+        }
+
+        /* (non-Javadoc)
+         * @see org.junit.rules.TestWatcher#finished(org.junit.runner.Description)
+         */
+        @Override
+        protected void finished(Description description) {
+           logger.info("Finished test: {}", description.getMethodName());
+        }
+
+    };
+
+    @Before
+    public void prepareContentLoader() throws Exception {
+        // NOTE: initially only the default set of content readers are registered
+        context.registerInjectActivateService(JsonReader.class);
+        context.registerInjectActivateService(OrderedJsonReader.class);
+        context.registerInjectActivateService(XmlReader.class);
+        context.registerInjectActivateService(ZipReader.class);
+
+        // whiteboard which holds readers
+        context.registerInjectActivateService(new ContentReaderWhiteboard());
+
+        // register the content loader service
+        bundleHelper = context.registerInjectActivateService(new BundleContentLoaderListener());
+
+    }
+
+    @Test
+    public void loadContentWithoutDirectiveExpectedContentReaderRegistered() throws Exception {
+        loadContentWithDirective();
+
+        // check node was not added during parsing the file
+        assertThat("Included resource should not have been imported", context.resourceResolver().getResource("/libs/app"), nullValue());
+        // check file was not loaded as non-parsed file
+        assertThat("Included resource should not have been imported", context.resourceResolver().getResource("/libs/app.sling11203"), nullValue());
+    }
+
+    @Test
+    public void loadContentWithDirectiveExpectedContentReaderRegisteredBeforeBundleLoaded() throws Exception {
+        // register the content reader that we require before registering the bundle
+        registerCustomContentReader();
+
+        loadContentWithDirective();
+
+        // check node was added during parsing the file
+        assertThat("Included resource should have been imported", context.resourceResolver().getResource("/libs/app"), notNullValue());
+        // check file was not loaded as non-parsed file
+        assertThat("Included resource should not have been imported", context.resourceResolver().getResource("/libs/app.sling11203"), nullValue());
+    }
+
+    @Test
+    public void loadContentWithDirectiveExpectedContentReaderRegisteredAfterBundleLoaded() throws Exception {
+        loadContentWithDirective();
+
+        // check node was not added during parsing the file
+        assertThat("Included resource should not have been imported", context.resourceResolver().getResource("/libs/app"), nullValue());
+        // check file was not loaded as non-parsed file
+        assertThat("Included resource should not have been imported", context.resourceResolver().getResource("/libs/app.sling11203"), nullValue());
+
+        // register the content reader that we require
+        registerCustomContentReader();
+
+        // check node was added during parsing the file
+        assertThat("Included resource should have been imported", context.resourceResolver().getResource("/libs/app"), notNullValue());
+        // check file was not loaded as non-parsed file
+        assertThat("Included resource should not have been imported", context.resourceResolver().getResource("/libs/app.sling11203"), nullValue());
+    }
+
+    protected void registerCustomContentReader() {
+        // register the content reader that we require after registering the bundle
+        //   to trigger the retry
+        context.registerService(ContentReader.class, new SLING11203XmlReader(), 
+                Collections.singletonMap(ContentReader.PROPERTY_EXTENSIONS, "sling11203"));
+    }
+
+    protected void loadContentWithDirective() throws Exception {
+        // dig the BundleContentLoader out of the component field so we get the
+        //  same instance so the state for the retry logic is there
+        Field privateBundleContentLoaderField = BundleContentLoaderListener.class.getDeclaredField("bundleContentLoader");
+        privateBundleContentLoaderField.setAccessible(true);
+        BundleContentLoader contentLoader = (BundleContentLoader)privateBundleContentLoaderField.get(bundleHelper);
+
+        // requireImportProviders directive, so it should check if the specified 
+        //  required content reader is available
+        String initialContentHeader = "SLING-INF3/libs;path:=/libs;requireImportProviders:=sling11203";
+        Bundle mockBundle = BundleContentLoaderTest.newBundleWithInitialContent(context, initialContentHeader);
+
+        contentLoader.registerBundle(context.resourceResolver().adaptTo(Session.class), mockBundle, false);
+    }
+
+    /**
+     * A custom xml reader with a different file extension
+     */
+    public static class SLING11203XmlReader extends XmlReader {
+
+        private SLING11203XmlReader() {
+            super();
+            activate();
+        }
+
+    }
+
+}
diff --git a/src/test/resources/SLING-INF3/libs/app.sling11203 b/src/test/resources/SLING-INF3/libs/app.sling11203
new file mode 100644
index 0000000..b8d80c6
--- /dev/null
+++ b/src/test/resources/SLING-INF3/libs/app.sling11203
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<node>
+    <primaryNodeType>sling:Folder</primaryNodeType>
+</node>