TAP5-2657: blacklist for manifest autoloaded modules

diff --git a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/IOCUtilities.java b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/IOCUtilities.java
index d45b1b9..c14e5a5 100644
--- a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/IOCUtilities.java
+++ b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/IOCUtilities.java
@@ -16,7 +16,10 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
+import java.util.Collections;
 import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.jar.Manifest;
 
 import org.apache.tapestry5.commons.util.ExceptionUtils;
@@ -57,6 +60,8 @@
      * Scans the classpath for JAR Manifests that contain the Tapestry-Module-Classes attribute and adds each
      * corresponding class to the RegistryBuilder. In addition, looks for a system property named "tapestry.modules" and
      * adds all of those modules as well. The tapestry.modules approach is intended for development.
+     * To prevent auto-loading of Manifest-defined modules the system property named "tapestry.manifest-modules-blacklist"
+     * can be used.
      *
      * @param builder
      *         the builder to which modules will be added
@@ -65,6 +70,19 @@
      */
     public static void addDefaultModules(RegistryBuilder builder)
     {
+        Set<String> blacklistedManifestModules = new HashSet<>();
+        String modulesBlacklist = System.getProperty("tapestry.manifest-modules-blacklist");
+        if (modulesBlacklist != null)
+        {
+            String[] blacklistedClassnames = modulesBlacklist.split(",");
+
+            
+            for (String classname : blacklistedClassnames)
+            {
+                blacklistedManifestModules.add(classname.trim());
+            }
+        }
+
         try
         {
             Enumeration<URL> urls = builder.getClassLoader().getResources("META-INF/MANIFEST.MF");
@@ -73,7 +91,7 @@
             {
                 URL url = urls.nextElement();
 
-                addModulesInManifest(builder, url);
+                addModulesInManifest(builder, url, blacklistedManifestModules);
             }
 
             addModulesInList(builder, System.getProperty("tapestry.modules"));
@@ -84,7 +102,7 @@
         }
     }
 
-    private static void addModulesInManifest(RegistryBuilder builder, URL url)
+    private static void addModulesInManifest(RegistryBuilder builder, URL url, Set<String> blacklist)
     {
         InputStream in = null;
 
@@ -102,7 +120,7 @@
 
             String list = mf.getMainAttributes().getValue(MODULE_BUILDER_MANIFEST_ENTRY_NAME);
 
-            addModulesInList(builder, list);
+            addModulesInList(builder, list, blacklist);
         } catch (RuntimeException ex)
         {
             fail = ex;
@@ -123,13 +141,24 @@
 
     static void addModulesInList(RegistryBuilder builder, String list)
     {
+        addModulesInList(builder, list, Collections.emptySet());
+    }
+
+    static void addModulesInList(RegistryBuilder builder, String list, Set<String> blacklist)
+    {
         if (list == null) return;
 
         String[] classnames = list.split(",");
 
         for (String classname : classnames)
         {
-            builder.add(classname.trim());
+            String trimmedClassname = classname.trim();
+            if (blacklist != null && blacklist.contains(trimmedClassname))
+            {
+                continue;
+            }
+
+            builder.add(trimmedClassname);
         }
     }
 
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ManifestProcessingSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ManifestProcessingSpec.groovy
index 8c13398..ac76bc5 100644
--- a/tapestry-ioc/src/test/groovy/ioc/specs/ManifestProcessingSpec.groovy
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ManifestProcessingSpec.groovy
@@ -1,7 +1,10 @@
 package ioc.specs
 
+import com.example.ManifestModule
+
 import org.apache.tapestry5.ioc.IOCUtilities
 import org.apache.tapestry5.ioc.RegistryBuilder
+
 import spock.lang.Specification
 
 class ManifestProcessingSpec extends Specification {
@@ -31,7 +34,119 @@
 
     e.message.contains "Exception loading module(s) from manifest"
     e.message.contains "Failure loading Tapestry IoC module class does.not.exist.Module"
-
-
   }
+
+  def "valid class in manifest"() {
+
+    given:
+
+    File jar = new File("src/test/realjar")
+
+    expect:
+
+    // This is more to verify the module execution environment
+    jar.exists()
+    jar.isDirectory()
+
+    when:
+
+    URL url = jar.toURL()
+    URLClassLoader loader = new URLClassLoader([url] as URL[], Thread.currentThread().contextClassLoader)
+
+    RegistryBuilder builder = new RegistryBuilder(loader)
+
+    IOCUtilities.addDefaultModules(builder)
+
+    def reg = builder.build()
+    reg.performRegistryStartup()
+
+    then:
+
+    noExceptionThrown()
+
+    ManifestModule.startupCalled == true
+
+    cleanup:
+
+    ManifestModule.startupCalled = false
+  }
+
+  def "blacklisted manifest module not loaded"() {
+
+    given:
+
+    File jar = new File("src/test/realjar")
+
+    def props = System.getProperties()
+    props.setProperty("tapestry.manifest-modules-blacklist", "does.not.ExistModule,com.example.ManifestModule")
+
+    expect:
+
+    // This is more to verify the module execution environment
+    jar.exists()
+    jar.isDirectory()
+
+    when:
+
+    URL url = jar.toURL()
+    URLClassLoader loader = new URLClassLoader([url] as URL[], Thread.currentThread().contextClassLoader)
+
+    RegistryBuilder builder = new RegistryBuilder(loader)
+
+    IOCUtilities.addDefaultModules(builder)
+
+    def reg = builder.build()
+    reg.performRegistryStartup()
+
+    then:
+
+    noExceptionThrown()
+
+    ManifestModule.startupCalled == false
+
+    cleanup:
+
+    props.remove("tapestry.manifest-modules-blacklist")
+    ManifestModule.startupCalled = false
+  }
+
+  def "blacklisted manifest module empty"() {
+
+    given:
+
+    File jar = new File("src/test/realjar")
+
+    def props = System.getProperties()
+    props.setProperty("tapestry.manifest-modules-blacklist", "")
+
+    expect:
+
+    // This is more to verify the module execution environment
+    jar.exists()
+    jar.isDirectory()
+
+    when:
+
+    URL url = jar.toURL()
+    URLClassLoader loader = new URLClassLoader([url] as URL[], Thread.currentThread().contextClassLoader)
+
+    RegistryBuilder builder = new RegistryBuilder(loader)
+
+    IOCUtilities.addDefaultModules(builder)
+
+    def reg = builder.build()
+    reg.performRegistryStartup()
+
+    then:
+
+    noExceptionThrown()
+
+    ManifestModule.startupCalled == true
+
+    cleanup:
+
+    props.remove("tapestry.manifest-modules-blacklist")
+    ManifestModule.startupCalled = false
+  }
+
 }
diff --git a/tapestry-ioc/src/test/java/com/example/ManifestModule.java b/tapestry-ioc/src/test/java/com/example/ManifestModule.java
new file mode 100644
index 0000000..b09eec3
--- /dev/null
+++ b/tapestry-ioc/src/test/java/com/example/ManifestModule.java
@@ -0,0 +1,29 @@
+// Copyright 2021 The Apache Software Foundation
+//
+// Licensed 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 com.example;
+
+import org.apache.tapestry5.ioc.annotations.Startup;
+
+public class ManifestModule
+{
+    public static boolean startupCalled = false;
+
+    @Startup
+    public void doStartup()
+    {
+        ManifestModule.startupCalled = true;
+    }
+
+}
diff --git a/tapestry-ioc/src/test/realjar/META-INF/MANIFEST.MF b/tapestry-ioc/src/test/realjar/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..bfcd412
--- /dev/null
+++ b/tapestry-ioc/src/test/realjar/META-INF/MANIFEST.MF
@@ -0,0 +1,7 @@
+Manifest-Version: 1.0
+Archiver-Version: Plexus Archiver
+Created-By: Apache Maven
+Built-By: Ben
+Build-Jdk: 1.8.0_271
+Tapestry-Module-Classes: com.example.ManifestModule
+