You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/10/23 08:27:46 UTC

(maven) branch master updated: feat: internalize plexus-classworlds (fixes #11028)

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new 6399a8e556 feat: internalize plexus-classworlds (fixes #11028)
6399a8e556 is described below

commit 6399a8e556e52afdfb8b85e6b83179fdf41ad0f0
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Oct 23 10:27:39 2026 +0200

    feat: internalize plexus-classworlds (fixes #11028)
    
    - Add impl/maven-classworlds: fork of plexus-classworlds with org.codehaus.plexus.classworlds implementation, backward-compat adapters under org.codehaus.classworlds, and new JPMS capabilities (module directive in m2.conf, META-INF/maven/module-access descriptor)
    - Replace external plexus-classworlds dependency with maven-classworlds across all modules
    - Remove classWorldsVersion property from root pom.xml
    - Preserve org.codehaus.classworlds compat package until eclipse-sisu/sisu-project#280 is merged and released (tracked in #13693)
    - Public API surface for classworlds deferred per review feedback
    - Prerequisite for #13491 (JLine on module-path) and #13494 (per-plugin ModuleLayer support)
    - Supersedes #11029
---
 apache-maven/src/assembly/component.xml            |   8 +-
 compat/maven-compat/pom.xml                        |   4 +-
 compat/maven-embedder/pom.xml                      |   4 +-
 compat/maven-plugin-api/pom.xml                    |   4 +-
 compat/maven-resolver-provider/pom.xml             |   5 +
 impl/maven-classworlds/pom.xml                     | 123 +++++
 .../codehaus/classworlds/BytesURLConnection.java   |  68 +++
 .../classworlds/BytesURLStreamHandler.java         |  65 +++
 .../java/org/codehaus/classworlds/ClassRealm.java  | 128 +++++
 .../codehaus/classworlds/ClassRealmAdapter.java    | 163 ++++++
 .../classworlds/ClassRealmReverseAdapter.java      | 141 ++++++
 .../java/org/codehaus/classworlds/ClassWorld.java  |  85 ++++
 .../codehaus/classworlds/ClassWorldAdapter.java    | 104 ++++
 .../codehaus/classworlds/ClassWorldException.java  | 122 +++++
 .../classworlds/ClassWorldReverseAdapter.java      | 113 +++++
 .../classworlds/ConfigurationException.java        |  96 ++++
 .../org/codehaus/classworlds/Configurator.java     | 151 ++++++
 .../codehaus/classworlds/ConfiguratorAdapter.java  |  92 ++++
 .../codehaus/classworlds/DefaultClassRealm.java    | 159 ++++++
 .../classworlds/DuplicateRealmException.java       | 114 +++++
 .../java/org/codehaus/classworlds/Launcher.java    |  76 +++
 .../codehaus/classworlds/NoSuchRealmException.java | 114 +++++
 .../org/codehaus/classworlds/package-info.java     |  37 ++
 .../codehaus/plexus/classworlds/ClassWorld.java    | 305 ++++++++++++
 .../plexus/classworlds/ClassWorldException.java    |  88 ++++
 .../plexus/classworlds/ClassWorldListener.java     |  28 +-
 .../org/codehaus/plexus/classworlds/UrlUtils.java  |  70 +++
 .../launcher/ConfigurationException.java           |  66 +++
 .../classworlds/launcher/ConfigurationHandler.java | 113 +++++
 .../classworlds/launcher/ConfigurationParser.java  | 492 ++++++++++++++++++
 .../plexus/classworlds/launcher/Configurator.java  | 357 +++++++++++++
 .../plexus/classworlds/launcher/Interpolator.java  | 168 +++++++
 .../plexus/classworlds/launcher/Launcher.java      | 365 ++++++++++++++
 .../plexus/classworlds/realm/ClassRealm.java       | 450 +++++++++++++++++
 .../classworlds/realm/DuplicateRealmException.java |  83 +++
 .../codehaus/plexus/classworlds/realm/Entry.java   | 199 ++++++++
 .../classworlds/realm/FilteredClassRealm.java      |  74 +++
 .../classworlds/realm/NoSuchRealmException.java    |  83 +++
 .../classworlds/strategy/AbstractStrategy.java     |  80 +++
 .../classworlds/strategy/OsgiBundleStrategy.java   |  88 ++++
 .../classworlds/strategy/ParentFirstStrategy.java  |  81 +++
 .../classworlds/strategy/SelfFirstStrategy.java    |  81 +++
 .../plexus/classworlds/strategy/Strategy.java      |  56 +++
 .../classworlds/strategy/StrategyFactory.java      |  67 +++
 .../classworlds/AbstractClassWorldsTestCase.java   |  61 +++
 .../org/codehaus/plexus/classworlds/ClassView.java | 130 +++++
 .../plexus/classworlds/ClassWorldTest.java         | 158 ++++++
 .../plexus/classworlds/TestJarGenerator.java       | 554 +++++++++++++++++++++
 .../org/codehaus/plexus/classworlds/TestUtil.java  |  79 +++
 .../codehaus/plexus/classworlds/UrlUtilsTest.java  |  49 ++
 .../launcher/ConfigurationParserTest.java          | 151 ++++++
 .../classworlds/launcher/ConfiguratorTest.java     | 360 +++++++++++++
 .../plexus/classworlds/launcher/LauncherTest.java  | 138 +++++
 .../classworlds/realm/ClassRealmImplTest.java      | 373 ++++++++++++++
 .../classworlds/realm/DefaultClassRealmTest.java   | 346 +++++++++++++
 .../plexus/classworlds/realm/EntryTest.java        | 186 +++++++
 .../classworlds/realm/FilteredClassRealmTest.java  | 107 ++++
 .../classworlds/strategy/StrategyFactoryTest.java  |  79 +++
 .../plexus/classworlds/strategy/StrategyTest.java  | 153 ++++++
 .../src/test/test-data/a.properties                |  18 +
 .../src/test/test-data/dupe-main.conf              |   3 +
 .../src/test/test-data/dupe-realm.conf             |   9 +
 .../src/test/test-data/early-import.conf           |   2 +
 .../src/test/test-data/inheritance.conf            |  18 +
 .../src/test/test-data/launch-noclass.conf         |   6 +
 .../src/test/test-data/launch-nomethod.conf        |   7 +
 .../src/test/test-data/nested.properties           |  18 +
 .../src/test/test-data/optionally-existent.conf    |  14 +
 .../src/test/test-data/optionally-nonexistent.conf |  13 +
 .../src/test/test-data/realm-syntax.conf           |   3 +
 .../src/test/test-data/resources/classworlds.conf  |  12 +
 .../src/test/test-data/set-using-existent.conf     |  19 +
 .../test/test-data/set-using-existent.properties   |  19 +
 .../src/test/test-data/set-using-missing.conf      |  19 +
 .../src/test/test-data/set-using-nonexistent.conf  |  19 +
 .../src/test/test-data/unhandled.conf              |   2 +
 .../test/test-data/valid-enh-launch-exitCode.conf  |   6 +
 .../src/test/test-data/valid-enh-launch.conf       |   6 +
 .../src/test/test-data/valid-from-from-from.conf   |  24 +
 .../src/test/test-data/valid-launch-exitCode.conf  |   5 +
 .../src/test/test-data/valid-launch.conf           |   5 +
 .../src/test/test-data/valid.conf                  |  24 +
 impl/maven-cli/pom.xml                             |   4 +-
 .../java/org/apache/maven/cling/MavenCling.java    |  10 +-
 impl/maven-core/pom.xml                            |  38 +-
 .../internal/LifecycleDependencyResolver.java      |  10 +-
 .../plugin/internal/DefaultMavenPluginManager.java |   6 +-
 .../project/DefaultProjectBuildingHelper.java      |   9 +-
 .../main/resources/META-INF/maven/extension.xml    |   7 +-
 .../classrealm/DefaultClassRealmManagerTest.java   |   4 +-
 impl/pom.xml                                       |   1 +
 .../extension/TestClassRealmManagerDelegate.java   |   2 +-
 .../TestClassRealmManagerDelegate.java             |   2 +-
 .../TestClassRealmManagerDelegate.java             |   2 +-
 pom.xml                                            |  17 +-
 src/graph/ReactorGraph.java                        |   2 +-
 96 files changed, 8635 insertions(+), 44 deletions(-)

diff --git a/apache-maven/src/assembly/component.xml b/apache-maven/src/assembly/component.xml
index 2298b21134..84f3bda039 100644
--- a/apache-maven/src/assembly/component.xml
+++ b/apache-maven/src/assembly/component.xml
@@ -23,14 +23,18 @@ under the License.
       <useProjectArtifact>false</useProjectArtifact>
       <outputDirectory>boot</outputDirectory>
       <includes>
-        <include>org.codehaus.plexus:plexus-classworlds</include>
+        <include>org.apache.maven:maven-classworlds</include>
       </includes>
+      <!-- Preserve the legacy plexus-classworlds-*.jar file name for backward compatibility:
+           wrapper scripts, CI tooling, and IDEs that glob boot/plexus-classworlds-*.jar continue
+           to work without change after the internalization. -->
+      <outputFileNameMapping>plexus-classworlds-${artifact.version}.jar</outputFileNameMapping>
     </dependencySet>
     <dependencySet>
       <useProjectArtifact>false</useProjectArtifact>
       <outputDirectory>lib</outputDirectory>
       <excludes>
-        <exclude>org.codehaus.plexus:plexus-classworlds</exclude>
+        <exclude>org.apache.maven:maven-classworlds</exclude>
       </excludes>
     </dependencySet>
   </dependencySets>
diff --git a/compat/maven-compat/pom.xml b/compat/maven-compat/pom.xml
index 2f5439fcdd..6fd5e533f1 100644
--- a/compat/maven-compat/pom.xml
+++ b/compat/maven-compat/pom.xml
@@ -121,8 +121,8 @@ under the License.
     </dependency>
 
     <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-classworlds</artifactId>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-classworlds</artifactId>
     </dependency>
     <dependency>
       <groupId>org.codehaus.plexus</groupId>
diff --git a/compat/maven-embedder/pom.xml b/compat/maven-embedder/pom.xml
index 2df8588ec1..f1343b7bcf 100644
--- a/compat/maven-embedder/pom.xml
+++ b/compat/maven-embedder/pom.xml
@@ -146,8 +146,8 @@ under the License.
     </dependency>
 
     <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-classworlds</artifactId>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-classworlds</artifactId>
     </dependency>
     <dependency>
       <groupId>org.codehaus.plexus</groupId>
diff --git a/compat/maven-plugin-api/pom.xml b/compat/maven-plugin-api/pom.xml
index 9f8f42bbf8..94a7f005d2 100644
--- a/compat/maven-plugin-api/pom.xml
+++ b/compat/maven-plugin-api/pom.xml
@@ -79,8 +79,8 @@ under the License.
       </exclusions>
     </dependency>
     <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-classworlds</artifactId>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-classworlds</artifactId>
     </dependency>
     <dependency>
       <groupId>org.apache.maven.resolver</groupId>
diff --git a/compat/maven-resolver-provider/pom.xml b/compat/maven-resolver-provider/pom.xml
index 79c7507bec..7130c64d41 100644
--- a/compat/maven-resolver-provider/pom.xml
+++ b/compat/maven-resolver-provider/pom.xml
@@ -151,6 +151,11 @@ under the License.
       <artifactId>plexus-testing</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-classworlds</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
diff --git a/impl/maven-classworlds/pom.xml b/impl/maven-classworlds/pom.xml
new file mode 100644
index 0000000000..768d4199aa
--- /dev/null
+++ b/impl/maven-classworlds/pom.xml
@@ -0,0 +1,123 @@
+<?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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.maven</groupId>
+    <artifactId>maven-impl-modules</artifactId>
+    <version>4.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-classworlds</artifactId>
+  <packaging>jar</packaging>
+
+  <name>Maven 4 Classworlds</name>
+  <description>A class loader framework</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+        <configuration>
+          <excludes>
+            <exclude>src/test/test-data/**</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <configuration>
+          <excludes>org/codehaus/classworlds/**</excludes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifest>
+              <mainClass>org.codehaus.plexus.classworlds.launcher.Launcher</mainClass>
+            </manifest>
+          </archive>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <redirectTestOutputToFile>true</redirectTestOutputToFile>
+          <argLine>-ea:org.codehaus.classworlds... -ea:org.codehaus.plexus.classworlds...</argLine>
+          <forkCount>1</forkCount>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <excludes>
+            <exclude>org/codehaus/plexus/classworlds/event/*</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>copy</goal>
+            </goals>
+            <phase>generate-test-resources</phase>
+            <configuration>
+              <artifactItems>
+                <artifactItem>
+                  <groupId>org.apache.ant</groupId>
+                  <artifactId>ant</artifactId>
+                  <version>1.10.14</version>
+                </artifactItem>
+                <artifactItem>
+                  <groupId>org.apache.logging.log4j</groupId>
+                  <artifactId>log4j-api</artifactId>
+                  <version>2.23.1</version>
+                </artifactItem>
+                <artifactItem>
+                  <groupId>jakarta.xml.bind</groupId>
+                  <artifactId>jakarta.xml.bind-api</artifactId>
+                  <version>4.0.2</version>
+                </artifactItem>
+              </artifactItems>
+              <outputDirectory>${project.build.directory}/test-lib</outputDirectory>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/BytesURLConnection.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/BytesURLConnection.java
new file mode 100644
index 0000000000..656e77cb09
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/BytesURLConnection.java
@@ -0,0 +1,68 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * URLConnection implementation for byte arrays.
+ *
+ * <p><b>Note:</b> This is a legacy class provided for backward compatibility with Maven 2.
+ * New code should avoid using this internal implementation detail.</p>
+ *
+ * @deprecated This is a legacy internal class.
+ */
+@Deprecated
+public class BytesURLConnection extends URLConnection {
+    protected final byte[] content;
+
+    protected int offset;
+
+    protected int length;
+
+    public BytesURLConnection(URL url, byte[] content) {
+        super(url);
+        this.content = content;
+    }
+
+    public void connect() {}
+
+    public InputStream getInputStream() {
+        return new ByteArrayInputStream(content);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/BytesURLStreamHandler.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/BytesURLStreamHandler.java
new file mode 100644
index 0000000000..182c572774
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/BytesURLStreamHandler.java
@@ -0,0 +1,65 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+/**
+ * URLStreamHandler implementation for byte arrays.
+ *
+ * <p><b>Note:</b> This is a legacy class provided for backward compatibility with Maven 2.
+ * New code should avoid using this internal implementation detail.</p>
+ *
+ * @author Hani Suleiman (hani@formicary.net) Date: Oct 20, 2003 12:45:18 AM
+ * @deprecated This is a legacy internal class.
+ */
+@Deprecated
+public class BytesURLStreamHandler extends URLStreamHandler {
+    final byte[] content;
+
+    int offset;
+
+    int length;
+
+    public BytesURLStreamHandler(byte[] content) {
+        this.content = content;
+    }
+
+    public URLConnection openConnection(URL url) {
+        return new BytesURLConnection(url, content);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealm.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealm.java
new file mode 100644
index 0000000000..0469a480d3
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealm.java
@@ -0,0 +1,128 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+
+Copyright 2002 (C) The Werken Company. All Rights Reserved.
+
+Redistribution and use of this software and associated documentation
+("Software"), with or without modification, are permitted provided
+that the following conditions are met:
+
+1. Redistributions of source code must retain copyright
+   statements and notices.  Redistributions must also contain a
+   copy of this document.
+
+2. Redistributions in binary form must reproduce the
+   above copyright notice, this list of conditions and the
+   following disclaimer in the documentation and/or other
+   materials provided with the distribution.
+
+3. The name "classworlds" must not be used to endorse or promote
+   products derived from this Software without prior written
+   permission of The Werken Company.  For written permission,
+   please contact bob@werken.com.
+
+4. Products derived from this Software may not be called "classworlds"
+   nor may "classworlds" appear in their names without prior written
+   permission of The Werken Company. "classworlds" is a registered
+   trademark of The Werken Company.
+
+5. Due credit should be given to The Werken Company.
+   (http://classworlds.werken.com/).
+
+THIS SOFTWARE IS PROVIDED BY THE WERKEN COMPANY AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+THE WERKEN COMPANY OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+
+/**
+ * <p>Autonomous sub-portion of a <code>ClassWorld</code>.</p>
+ *
+ * <p>This class most closed maps to the <code>ClassLoader</code>
+ * role from Java and in facts can provide a <code>ClassLoader</code>
+ * view of itself using {@link #getClassLoader}.</p>
+ *
+ * <p>This is a legacy interface from Classworlds 1.x, preserved for binary compatibility.
+ * The compiled bytecode of {@code org.eclipse.sisu:org.eclipse.sisu.plexus} references this
+ * interface directly. Removing it breaks all Maven 3+ builds.
+ * New code should use {@link org.codehaus.plexus.classworlds.realm.ClassRealm}.</p>
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ * @author <a href="mailto:jason@zenplex.com">Jason van Zyl</a>
+ * @deprecated Use {@link org.codehaus.plexus.classworlds.realm.ClassRealm} for new code.
+ *             This interface must remain on the classpath for Sisu compatibility.
+ */
+@SuppressWarnings("rawtypes")
+@Deprecated
+public interface ClassRealm {
+    String getId();
+
+    ClassWorld getWorld();
+
+    void importFrom(String realmId, String pkgName) throws NoSuchRealmException;
+
+    void addConstituent(URL constituent);
+
+    ClassRealm locateSourceRealm(String className);
+
+    void setParent(ClassRealm classRealm);
+
+    ClassRealm createChildRealm(String id) throws DuplicateRealmException;
+
+    ClassLoader getClassLoader();
+
+    ClassRealm getParent();
+
+    URL[] getConstituents();
+
+    // ----------------------------------------------------------------------
+    // Classloading
+    // ----------------------------------------------------------------------
+
+    Class loadClass(String name) throws ClassNotFoundException;
+
+    // ----------------------------------------------------------------------
+    // Resource handling
+    // ----------------------------------------------------------------------
+
+    URL getResource(String name);
+
+    Enumeration findResources(String name) throws IOException;
+
+    InputStream getResourceAsStream(String name);
+
+    void display();
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealmAdapter.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealmAdapter.java
new file mode 100644
index 0000000000..0a6c45f91e
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealmAdapter.java
@@ -0,0 +1,163 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+
+/**
+ * Adapter that wraps a modern {@link org.codehaus.plexus.classworlds.realm.ClassRealm} and exposes
+ * it as the legacy {@link ClassRealm} interface.
+ *
+ * <p>This class is referenced directly by the compiled bytecode of
+ * {@code org.eclipse.sisu:org.eclipse.sisu.plexus} and must remain on the classpath for
+ * Maven 3+ to function. Do not remove or rename it. New code should not use this adapter;
+ * use {@link org.codehaus.plexus.classworlds.realm.ClassRealm} directly.</p>
+ *
+ * @author Andrew Williams
+ * @deprecated Legacy adapter retained for Sisu binary compatibility.
+ */
+@SuppressWarnings({"UnnecessaryLocalVariable", "DeprecatedIsStillUsed", "rawtypes"})
+@Deprecated
+public class ClassRealmAdapter implements ClassRealm {
+
+    public static ClassRealmAdapter getInstance(org.codehaus.plexus.classworlds.realm.ClassRealm newRealm) {
+        ClassRealmAdapter adapter = new ClassRealmAdapter(newRealm);
+        return adapter;
+    }
+
+    private final org.codehaus.plexus.classworlds.realm.ClassRealm realm;
+
+    private ClassRealmAdapter(org.codehaus.plexus.classworlds.realm.ClassRealm newRealm) {
+        this.realm = newRealm;
+    }
+
+    public String getId() {
+        return realm.getId();
+    }
+
+    public ClassWorld getWorld() {
+        return ClassWorldAdapter.getInstance(realm.getWorld());
+    }
+
+    public void importFrom(String realmId, String pkgName) throws NoSuchRealmException {
+        try {
+            realm.importFrom(realmId, pkgName);
+        } catch (org.codehaus.plexus.classworlds.realm.NoSuchRealmException e) {
+            throw new NoSuchRealmException(getWorld(), e.getId());
+        }
+    }
+
+    public void addConstituent(URL constituent) {
+        realm.addURL(constituent);
+    }
+
+    public ClassRealm locateSourceRealm(String className) {
+        ClassLoader importLoader = realm.getImportClassLoader(className);
+        if (importLoader instanceof org.codehaus.plexus.classworlds.realm.ClassRealm) {
+            return ClassRealmAdapter.getInstance((org.codehaus.plexus.classworlds.realm.ClassRealm) importLoader);
+        } else {
+            return null;
+        }
+    }
+
+    public void setParent(ClassRealm classRealm) {
+        if (classRealm != null) {
+            realm.setParentRealm(ClassRealmReverseAdapter.getInstance(classRealm));
+        }
+    }
+
+    public ClassRealm createChildRealm(String id) throws DuplicateRealmException {
+        try {
+            return ClassRealmAdapter.getInstance(realm.createChildRealm(id));
+        } catch (org.codehaus.plexus.classworlds.realm.DuplicateRealmException e) {
+            throw new DuplicateRealmException(getWorld(), e.getId());
+        }
+    }
+
+    public ClassLoader getClassLoader() {
+        return realm;
+    }
+
+    public ClassRealm getParent() {
+        return ClassRealmAdapter.getInstance(realm.getParentRealm());
+    }
+
+    public ClassRealm getParentRealm() {
+        return ClassRealmAdapter.getInstance(realm.getParentRealm());
+    }
+
+    public URL[] getConstituents() {
+        return realm.getURLs();
+    }
+
+    public Class loadClass(String name) throws ClassNotFoundException {
+        return realm.loadClass(name);
+    }
+
+    public URL getResource(String name) {
+        return realm.getResource(trimLeadingSlash(name));
+    }
+
+    public Enumeration findResources(String name) throws IOException {
+        return realm.findResources(trimLeadingSlash(name));
+    }
+
+    public InputStream getResourceAsStream(String name) {
+        return realm.getResourceAsStream(trimLeadingSlash(name));
+    }
+
+    public void display() {
+        realm.display();
+    }
+
+    public boolean equals(Object o) {
+        if (!(o instanceof ClassRealm)) return false;
+        return getId().equals(((ClassRealm) o).getId());
+    }
+
+    /**
+     * Provides backward-compat with the old classworlds which accepted resource names with leading slashes.
+     */
+    private String trimLeadingSlash(String resource) {
+        if (resource != null && resource.startsWith("/")) {
+            return resource.substring(1);
+        } else {
+            return resource;
+        }
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealmReverseAdapter.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealmReverseAdapter.java
new file mode 100644
index 0000000000..f0dfc2ab8e
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealmReverseAdapter.java
@@ -0,0 +1,141 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+
+/**
+ * A reverse adapter for ClassRealms.
+ *
+ * <p><b>Note:</b> This is a legacy internal class provided for backward compatibility with Maven 2.
+ * New code should avoid using this adapter.</p>
+ *
+ * @author Andrew Williams
+ * @deprecated This is a legacy internal class.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+@Deprecated
+public class ClassRealmReverseAdapter extends org.codehaus.plexus.classworlds.realm.ClassRealm {
+
+    public static ClassRealmReverseAdapter getInstance(ClassRealm oldRealm) {
+        return new ClassRealmReverseAdapter(oldRealm);
+    }
+
+    private final ClassRealm realm;
+
+    private ClassRealmReverseAdapter(ClassRealm oldRealm) {
+        super(ClassWorldReverseAdapter.getInstance(oldRealm.getWorld()), oldRealm.getId(), oldRealm.getClassLoader());
+        this.realm = oldRealm;
+    }
+
+    public String getId() {
+        return realm.getId();
+    }
+
+    public org.codehaus.plexus.classworlds.ClassWorld getWorld() {
+        return ClassWorldReverseAdapter.getInstance(realm.getWorld());
+    }
+
+    public void importFrom(String realmId, String pkgName)
+            throws org.codehaus.plexus.classworlds.realm.NoSuchRealmException {
+        try {
+            realm.importFrom(realmId, pkgName);
+        } catch (NoSuchRealmException e) {
+            throw new org.codehaus.plexus.classworlds.realm.NoSuchRealmException(getWorld(), e.getId());
+        }
+    }
+
+    public void addURL(URL constituent) {
+        realm.addConstituent(constituent);
+    }
+
+    public org.codehaus.plexus.classworlds.realm.ClassRealm locateSourceRealm(String className) {
+        return getInstance(realm.locateSourceRealm(className));
+    }
+
+    public void setParentRealm(org.codehaus.plexus.classworlds.realm.ClassRealm classRealm) {
+        realm.setParent(ClassRealmAdapter.getInstance(classRealm));
+    }
+
+    public org.codehaus.plexus.classworlds.realm.ClassRealm createChildRealm(String id)
+            throws org.codehaus.plexus.classworlds.realm.DuplicateRealmException {
+        try {
+            return getInstance(realm.createChildRealm(id));
+        } catch (DuplicateRealmException e) {
+            throw new org.codehaus.plexus.classworlds.realm.DuplicateRealmException(getWorld(), e.getId());
+        }
+    }
+
+    public ClassLoader getClassLoader() {
+        return realm.getClassLoader();
+    }
+
+    public org.codehaus.plexus.classworlds.realm.ClassRealm getParentRealm() {
+        return getInstance(realm.getParent());
+    }
+
+    public URL[] getURLs() {
+        return realm.getConstituents();
+    }
+
+    public Class loadClass(String name) throws ClassNotFoundException {
+        return realm.loadClass(name);
+    }
+
+    public URL getResource(String name) {
+        return realm.getResource(name);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Enumeration findResources(String name) throws IOException {
+        return realm.findResources(name);
+    }
+
+    public InputStream getResourceAsStream(String name) {
+        return realm.getResourceAsStream(name);
+    }
+
+    public void display() {
+        realm.display();
+    }
+
+    public boolean equals(Object o) {
+        if (!(o instanceof ClassRealm)) return false;
+        return getId().equals(((ClassRealm) o).getId());
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorld.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorld.java
new file mode 100644
index 0000000000..cdc0195798
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorld.java
@@ -0,0 +1,85 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.util.Collection;
+
+/**
+ * A compatibility wrapper for {@link org.codehaus.plexus.classworlds.ClassWorld}
+ * provided for legacy code.
+ *
+ * <p><b>Note:</b> This is a legacy class provided for backward compatibility with Maven 2.
+ * New code should use {@link org.codehaus.plexus.classworlds.ClassWorld}.</p>
+ *
+ * @author Andrew Williams
+ * @deprecated Use {@link org.codehaus.plexus.classworlds.ClassWorld}
+ */
+@SuppressWarnings("rawtypes")
+@Deprecated
+public class ClassWorld {
+    private ClassWorldAdapter adapter;
+
+    public ClassWorld(String realmId, ClassLoader classLoader) {
+        adapter = ClassWorldAdapter.getInstance(new org.codehaus.plexus.classworlds.ClassWorld(realmId, classLoader));
+    }
+
+    public ClassWorld() {
+        adapter = ClassWorldAdapter.getInstance(new org.codehaus.plexus.classworlds.ClassWorld());
+    }
+
+    public ClassWorld(boolean ignore) {
+        /* fake */
+    }
+
+    public ClassRealm newRealm(String id) throws DuplicateRealmException {
+        return adapter.newRealm(id);
+    }
+
+    public ClassRealm newRealm(String id, ClassLoader classLoader) throws DuplicateRealmException {
+        return adapter.newRealm(id, classLoader);
+    }
+
+    public void disposeRealm(String id) throws NoSuchRealmException {
+        adapter.disposeRealm(id);
+    }
+
+    public ClassRealm getRealm(String id) throws NoSuchRealmException {
+        return adapter.getRealm(id);
+    }
+
+    public Collection getRealms() {
+        return adapter.getRealms();
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldAdapter.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldAdapter.java
new file mode 100644
index 0000000000..e3ff6471b6
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldAdapter.java
@@ -0,0 +1,104 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.util.Collection;
+import java.util.Vector;
+
+/**
+ * An adapter for ClassWorlds.
+ *
+ * <p><b>Note:</b> This is a legacy internal class provided for backward compatibility with Maven 2.
+ * New code should avoid using this adapter.</p>
+ *
+ * @author Andrew Williams
+ * @deprecated This is a legacy internal class.
+ */
+@SuppressWarnings("rawtypes")
+@Deprecated
+public class ClassWorldAdapter extends ClassWorld {
+
+    public static ClassWorldAdapter getInstance(org.codehaus.plexus.classworlds.ClassWorld newWorld) {
+        return new ClassWorldAdapter(newWorld);
+    }
+
+    private final org.codehaus.plexus.classworlds.ClassWorld world;
+
+    private ClassWorldAdapter(org.codehaus.plexus.classworlds.ClassWorld newWorld) {
+        super(false);
+        this.world = newWorld;
+    }
+
+    public ClassRealm newRealm(String id) throws DuplicateRealmException {
+        try {
+            return ClassRealmAdapter.getInstance(world.newRealm(id));
+        } catch (org.codehaus.plexus.classworlds.realm.DuplicateRealmException e) {
+            throw new DuplicateRealmException(this, e.getId());
+        }
+    }
+
+    public ClassRealm newRealm(String id, ClassLoader classLoader) throws DuplicateRealmException {
+        try {
+            return ClassRealmAdapter.getInstance(world.newRealm(id, classLoader));
+        } catch (org.codehaus.plexus.classworlds.realm.DuplicateRealmException e) {
+            throw new DuplicateRealmException(this, e.getId());
+        }
+    }
+
+    public void disposeRealm(String id) throws NoSuchRealmException {
+        try {
+            world.disposeRealm(id);
+        } catch (org.codehaus.plexus.classworlds.realm.NoSuchRealmException e) {
+            throw new NoSuchRealmException(this, e.getId());
+        }
+    }
+
+    public ClassRealm getRealm(String id) throws NoSuchRealmException {
+        try {
+            return ClassRealmAdapter.getInstance(world.getRealm(id));
+        } catch (org.codehaus.plexus.classworlds.realm.NoSuchRealmException e) {
+            throw new NoSuchRealmException(this, e.getId());
+        }
+    }
+
+    public Collection getRealms() {
+        Collection<org.codehaus.plexus.classworlds.realm.ClassRealm> realms = world.getRealms();
+        Vector<ClassRealmAdapter> ret = new Vector<>();
+        for (org.codehaus.plexus.classworlds.realm.ClassRealm classRealm : realms) {
+            ret.add(ClassRealmAdapter.getInstance(classRealm));
+        }
+        return ret;
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldException.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldException.java
new file mode 100644
index 0000000000..76f51e3e7a
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldException.java
@@ -0,0 +1,122 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+
+Copyright 2002 (C) The Werken Company. All Rights Reserved.
+
+Redistribution and use of this software and associated documentation
+("Software"), with or without modification, are permitted provided
+that the following conditions are met:
+
+1. Redistributions of source code must retain copyright
+   statements and notices.  Redistributions must also contain a
+   copy of this document.
+
+2. Redistributions in binary form must reproduce the
+   above copyright notice, this list of conditions and the
+   following disclaimer in the documentation and/or other
+   materials provided with the distribution.
+
+3. The name "classworlds" must not be used to endorse or promote
+   products derived from this Software without prior written
+   permission of The Werken Company.  For written permission,
+   please contact bob@werken.com.
+
+4. Products derived from this Software may not be called "classworlds"
+   nor may "classworlds" appear in their names without prior written
+   permission of The Werken Company. "classworlds" is a registered
+   trademark of The Werken Company.
+
+5. Due credit should be given to The Werken Company.
+   (http://classworlds.werken.com/).
+
+THIS SOFTWARE IS PROVIDED BY THE WERKEN COMPANY AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+THE WERKEN COMPANY OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+/**
+ * Base exception for <code>ClassWorld</code> errors.
+ *
+ * <p><b>Note:</b> This is a legacy exception provided for backward compatibility with Maven 2.
+ * New code should use {@link org.codehaus.plexus.classworlds.ClassWorldException}.</p>
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ * @deprecated Use {@link org.codehaus.plexus.classworlds.ClassWorldException}
+ */
+@Deprecated
+public class ClassWorldException extends Exception {
+    // ------------------------------------------------------------
+    //     Instance members
+    // ------------------------------------------------------------
+
+    /**
+     * The world.
+     */
+    private final ClassWorld world;
+
+    // ------------------------------------------------------------
+    //     Constructors
+    // ------------------------------------------------------------
+
+    /**
+     * Construct.
+     *
+     * @param world The world.
+     */
+    public ClassWorldException(final ClassWorld world) {
+        this.world = world;
+    }
+
+    /**
+     * Construct.
+     *
+     * @param world The world.
+     * @param msg   The detail message.
+     */
+    public ClassWorldException(final ClassWorld world, final String msg) {
+        super(msg);
+        this.world = world;
+    }
+
+    // ------------------------------------------------------------
+    //     Instance methods
+    // ------------------------------------------------------------
+
+    /**
+     * Retrieve the world.
+     *
+     * @return The world.
+     */
+    public ClassWorld getWorld() {
+        return this.world;
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldReverseAdapter.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldReverseAdapter.java
new file mode 100644
index 0000000000..7429f73591
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldReverseAdapter.java
@@ -0,0 +1,113 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Vector;
+
+/**
+ * A reverse adapter for ClassWorlds.
+ *
+ * <p><b>Note:</b> This is a legacy internal class provided for backward compatibility with Maven 2.
+ * New code should avoid using this adapter.</p>
+ *
+ * @author Andrew Williams
+ * @deprecated This is a legacy internal class.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+@Deprecated
+public class ClassWorldReverseAdapter extends org.codehaus.plexus.classworlds.ClassWorld {
+    private static final HashMap instances = new HashMap();
+
+    public static ClassWorldReverseAdapter getInstance(ClassWorld oldWorld) {
+        if (instances.containsKey(oldWorld)) return (ClassWorldReverseAdapter) instances.get(oldWorld);
+        ClassWorldReverseAdapter adapter = new ClassWorldReverseAdapter(oldWorld);
+        instances.put(oldWorld, adapter);
+        return adapter;
+    }
+
+    private final ClassWorld world;
+
+    private ClassWorldReverseAdapter(ClassWorld newWorld) {
+        super();
+        this.world = newWorld;
+    }
+
+    public org.codehaus.plexus.classworlds.realm.ClassRealm newRealm(String id)
+            throws org.codehaus.plexus.classworlds.realm.DuplicateRealmException {
+        try {
+            return ClassRealmReverseAdapter.getInstance(world.newRealm(id));
+        } catch (DuplicateRealmException e) {
+            throw new org.codehaus.plexus.classworlds.realm.DuplicateRealmException(this, e.getId());
+        }
+    }
+
+    public org.codehaus.plexus.classworlds.realm.ClassRealm newRealm(String id, ClassLoader classLoader)
+            throws org.codehaus.plexus.classworlds.realm.DuplicateRealmException {
+        try {
+            return ClassRealmReverseAdapter.getInstance(world.newRealm(id, classLoader));
+        } catch (DuplicateRealmException e) {
+            throw new org.codehaus.plexus.classworlds.realm.DuplicateRealmException(this, e.getId());
+        }
+    }
+
+    public void disposeRealm(String id) throws org.codehaus.plexus.classworlds.realm.NoSuchRealmException {
+        try {
+            world.disposeRealm(id);
+        } catch (NoSuchRealmException e) {
+            throw new org.codehaus.plexus.classworlds.realm.NoSuchRealmException(this, e.getId());
+        }
+    }
+
+    public org.codehaus.plexus.classworlds.realm.ClassRealm getRealm(String id)
+            throws org.codehaus.plexus.classworlds.realm.NoSuchRealmException {
+        try {
+            return ClassRealmReverseAdapter.getInstance(world.getRealm(id));
+        } catch (NoSuchRealmException e) {
+            throw new org.codehaus.plexus.classworlds.realm.NoSuchRealmException(this, e.getId());
+        }
+    }
+
+    public Collection getRealms() {
+        Collection realms = world.getRealms();
+        Vector ret = new Vector();
+        for (Object o : realms) {
+            ClassRealm realm = (ClassRealm) o;
+            ret.add(ClassRealmReverseAdapter.getInstance(realm));
+        }
+        return ret;
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ConfigurationException.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ConfigurationException.java
new file mode 100644
index 0000000000..97396ba668
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ConfigurationException.java
@@ -0,0 +1,96 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+
+Copyright 2002 (C) The Werken Company. All Rights Reserved.
+
+Redistribution and use of this software and associated documentation
+("Software"), with or without modification, are permitted provided
+that the following conditions are met:
+
+1. Redistributions of source code must retain copyright
+   statements and notices.  Redistributions must also contain a
+   copy of this document.
+
+2. Redistributions in binary form must reproduce the
+   above copyright notice, this list of conditions and the
+   following disclaimer in the documentation and/or other
+   materials provided with the distribution.
+
+3. The name "classworlds" must not be used to endorse or promote
+   products derived from this Software without prior written
+   permission of The Werken Company.  For written permission,
+   please contact bob@werken.com.
+
+4. Products derived from this Software may not be called "classworlds"
+   nor may "classworlds" appear in their names without prior written
+   permission of The Werken Company. "classworlds" is a registered
+   trademark of The Werken Company.
+
+5. Due credit should be given to The Werken Company.
+   (http://classworlds.werken.com/).
+
+THIS SOFTWARE IS PROVIDED BY THE WERKEN COMPANY AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+THE WERKEN COMPANY OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+/**
+ * Indicates an error during <code>Launcher</code> configuration.
+ *
+ * <p><b>Note:</b> This is a legacy exception provided for backward compatibility with Maven 2.
+ * New code should use {@link org.codehaus.plexus.classworlds.launcher.ConfigurationException}.</p>
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ * @deprecated Use {@link org.codehaus.plexus.classworlds.launcher.ConfigurationException}
+ */
+@Deprecated
+public class ConfigurationException extends Exception {
+    /**
+     * Construct.
+     *
+     * @param msg The message.
+     */
+    public ConfigurationException(String msg) {
+        super(msg);
+    }
+
+    /**
+     * Construct.
+     *
+     * @param msg    The message.
+     * @param lineNo The number of configuraton line where the problem occured.
+     * @param line   The configuration line where the problem occured.
+     */
+    public ConfigurationException(String msg, int lineNo, String line) {
+        super(msg + " (" + lineNo + "): " + line);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/Configurator.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/Configurator.java
new file mode 100644
index 0000000000..0853c80b5d
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/Configurator.java
@@ -0,0 +1,151 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+
+/**
+ * A compatibility wrapper for {@link org.codehaus.plexus.classworlds.launcher.Configurator}
+ * provided for legacy code.
+ *
+ * <p><b>Note:</b> This is a legacy class provided for backward compatibility with Maven 2.
+ * New code should use {@link org.codehaus.plexus.classworlds.launcher.Configurator}.</p>
+ *
+ * @author Andrew Williams
+ * @deprecated Use {@link org.codehaus.plexus.classworlds.launcher.Configurator}
+ */
+@Deprecated
+public class Configurator {
+    private final ConfiguratorAdapter config;
+
+    /** Construct.
+     *
+     *  @param launcher The launcher to configure.
+     */
+    public Configurator(Launcher launcher) {
+        config = ConfiguratorAdapter.getInstance(
+                new org.codehaus.plexus.classworlds.launcher.Configurator(launcher), launcher);
+    }
+
+    /** Construct.
+     *
+     *  @param world The classWorld to configure.
+     */
+    public Configurator(ClassWorld world) {
+        config = ConfiguratorAdapter.getInstance(
+                new org.codehaus.plexus.classworlds.launcher.Configurator(ClassWorldReverseAdapter.getInstance(world)),
+                world);
+    }
+
+    /** set world.
+     *  this setter is provided so you can use the same configurator to configure several "worlds"
+     *
+     *  @param world The classWorld to configure.
+     */
+    public void setClassWorld(ClassWorld world) {
+        config.setClassWorld(world);
+    }
+
+    /**
+     * Configure from a file.
+     *
+     * @param is The config input stream
+     * @throws IOException             If an error occurs reading the config file.
+     * @throws MalformedURLException   If the config file contains invalid URLs.
+     * @throws ConfigurationException  If the config file is corrupt.
+     * @throws DuplicateRealmException If the config file defines two realms with the same id.
+     * @throws NoSuchRealmException    If the config file defines a main entry point in
+     *                                 a non-existent realm.
+     */
+    public void configure(InputStream is)
+            throws IOException, MalformedURLException, ConfigurationException, DuplicateRealmException,
+                    NoSuchRealmException {
+        config.configureAdapter(is);
+    }
+
+    /**
+     * Associate parent realms with their children.
+     */
+    protected void associateRealms() {
+        config.associateRealms();
+    }
+
+    /**
+     * Load a glob into the specified classloader.
+     *
+     * @param line  The path configuration line.
+     * @param realm The realm to populate
+     * @throws MalformedURLException If the line does not represent
+     *                               a valid path element.
+     * @throws FileNotFoundException If the line does not represent
+     *                               a valid path element in the filesystem.
+     */
+    protected void loadGlob(String line, ClassRealm realm) throws MalformedURLException, FileNotFoundException {
+        loadGlob(line, realm, false);
+    }
+
+    /**
+     * Load a glob into the specified classloader.
+     *
+     * @param line  The path configuration line.
+     * @param realm The realm to populate
+     * @param optionally Whether the path is optional or required
+     * @throws MalformedURLException If the line does not represent
+     *                               a valid path element.
+     * @throws FileNotFoundException If the line does not represent
+     *                               a valid path element in the filesystem.
+     */
+    @SuppressWarnings("RedundantThrows")
+    protected void loadGlob(String line, ClassRealm realm, boolean optionally)
+            throws MalformedURLException, FileNotFoundException {
+        config.loadGlob(line, realm, optionally);
+    }
+
+    /**
+     * Filter a string for system properties.
+     *
+     * @param text The text to filter.
+     * @return The filtered text.
+     * @throws ConfigurationException If the property does not
+     *                                exist or if there is a syntax error.
+     */
+    @SuppressWarnings("RedundantThrows")
+    protected String filter(String text) throws ConfigurationException {
+        return config.filter(text);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ConfiguratorAdapter.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ConfiguratorAdapter.java
new file mode 100644
index 0000000000..8cd912c801
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ConfiguratorAdapter.java
@@ -0,0 +1,92 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+
+/**
+ * An adapter for Configurators.
+ *
+ * <p><b>Note:</b> This is a legacy internal class provided for backward compatibility with Maven 2.
+ * New code should avoid using this adapter.</p>
+ *
+ * @deprecated This is a legacy internal class.
+ */
+@Deprecated
+public class ConfiguratorAdapter extends Configurator {
+
+    public static ConfiguratorAdapter getInstance(
+            org.codehaus.plexus.classworlds.launcher.Configurator newConfig, Launcher launcher) {
+        return new ConfiguratorAdapter(newConfig, launcher);
+    }
+
+    public static ConfiguratorAdapter getInstance(
+            org.codehaus.plexus.classworlds.launcher.Configurator newConfig, ClassWorld world) {
+        return new ConfiguratorAdapter(newConfig, world);
+    }
+
+    private final org.codehaus.plexus.classworlds.launcher.Configurator config;
+
+    private ConfiguratorAdapter(org.codehaus.plexus.classworlds.launcher.Configurator config, Launcher launcher) {
+        super(launcher);
+        this.config = config;
+    }
+
+    private ConfiguratorAdapter(org.codehaus.plexus.classworlds.launcher.Configurator config, ClassWorld world) {
+        super(world);
+        this.config = config;
+    }
+
+    public void associateRealms() {
+        config.associateRealms();
+    }
+
+    @SuppressWarnings("DuplicateThrows")
+    public void configureAdapter(InputStream is)
+            throws IOException, MalformedURLException, ConfigurationException, DuplicateRealmException,
+                    NoSuchRealmException {
+        try {
+            config.configure(is);
+        } catch (org.codehaus.plexus.classworlds.launcher.ConfigurationException e) {
+            throw new ConfigurationException(e.getMessage());
+        } catch (org.codehaus.plexus.classworlds.realm.DuplicateRealmException e) {
+            throw new DuplicateRealmException(ClassWorldAdapter.getInstance(e.getWorld()), e.getId());
+        } catch (org.codehaus.plexus.classworlds.realm.NoSuchRealmException e) {
+            throw new NoSuchRealmException(ClassWorldAdapter.getInstance(e.getWorld()), e.getId());
+        }
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/DefaultClassRealm.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/DefaultClassRealm.java
new file mode 100644
index 0000000000..6afe78449b
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/DefaultClassRealm.java
@@ -0,0 +1,159 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+/**
+ * A compatibility wrapper for {@link org.codehaus.plexus.classworlds.realm.ClassRealm}
+ * provided for legacy code.
+ *
+ * <p><b>Note:</b> This is a legacy class provided for backward compatibility with Maven 2.
+ * New code should use {@link org.codehaus.plexus.classworlds.realm.ClassRealm}.</p>
+ *
+ * @author Andrew Williams
+ * @deprecated Use {@link org.codehaus.plexus.classworlds.realm.ClassRealm}
+ */
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+
+@SuppressWarnings("rawtypes")
+@Deprecated
+public class DefaultClassRealm implements ClassRealm {
+    private final ClassRealmAdapter adapter;
+
+    public DefaultClassRealm(ClassWorld world, String id) {
+        this(world, id, null);
+    }
+
+    public DefaultClassRealm(ClassWorld world, String id, ClassLoader foreignClassLoader) {
+        this.adapter = ClassRealmAdapter.getInstance(new org.codehaus.plexus.classworlds.realm.ClassRealm(
+                ClassWorldReverseAdapter.getInstance(world), id, foreignClassLoader));
+    }
+
+    public URL[] getConstituents() {
+        return adapter.getConstituents();
+    }
+
+    public ClassRealm getParent() {
+        return adapter.getParentRealm();
+    }
+
+    public void setParent(ClassRealm parent) {
+        adapter.setParent(parent);
+    }
+
+    public String getId() {
+        return adapter.getId();
+    }
+
+    public ClassWorld getWorld() {
+        return adapter.getWorld();
+    }
+
+    public void importFrom(String realmId, String packageName) throws NoSuchRealmException {
+        adapter.importFrom(realmId, packageName);
+    }
+
+    public void addConstituent(URL constituent) {
+        adapter.addConstituent(constituent);
+    }
+
+    /**
+     *  Adds a byte[] class definition as a constituent for locating classes.
+     *  Currently uses BytesURLStreamHandler to hold a reference of the byte[] in memory.
+     *  This ensures we have a unifed URL resource model for all constituents.
+     *  The code to cache to disk is commented out - maybe a property to choose which method?
+     *
+     *  @param constituent class name
+     *  @param b the class definition as a byte[]
+     *  @throws ClassNotFoundException when class couldn't be loaded
+     */
+    public void addConstituent(String constituent, byte[] b) throws ClassNotFoundException {
+        try {
+            File path, file;
+            if (constituent.lastIndexOf('.') != -1) {
+                path = new File("byteclass/"
+                        + constituent
+                                .substring(0, constituent.lastIndexOf('.') + 1)
+                                .replace('.', File.separatorChar));
+                file = new File(path, constituent.substring(constituent.lastIndexOf('.') + 1) + ".class");
+            } else {
+                path = new File("byteclass/");
+                file = new File(path, constituent + ".class");
+            }
+            addConstituent(new URL(null, file.toURI().toURL().toExternalForm(), new BytesURLStreamHandler(b)));
+        } catch (java.io.IOException e) {
+            throw new ClassNotFoundException("Couldn't load byte stream.", e);
+        }
+    }
+
+    public ClassRealm locateSourceRealm(String classname) {
+        return adapter.locateSourceRealm(classname);
+    }
+
+    public ClassLoader getClassLoader() {
+        return adapter.getClassLoader();
+    }
+
+    public ClassRealm createChildRealm(String id) throws DuplicateRealmException {
+        return adapter.createChildRealm(id);
+    }
+
+    // ----------------------------------------------------------------------
+    // ClassLoader API
+    // ----------------------------------------------------------------------
+
+    public Class loadClass(String name) throws ClassNotFoundException {
+        return adapter.loadClass(name);
+    }
+
+    public URL getResource(String name) {
+        return adapter.getResource(name);
+    }
+
+    public InputStream getResourceAsStream(String name) {
+        return adapter.getResourceAsStream(name);
+    }
+
+    public Enumeration findResources(String name) throws IOException {
+        return adapter.findResources(name);
+    }
+
+    public void display() {
+        adapter.display();
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/DuplicateRealmException.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/DuplicateRealmException.java
new file mode 100644
index 0000000000..c139554310
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/DuplicateRealmException.java
@@ -0,0 +1,114 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+
+Copyright 2002 (C) The Werken Company. All Rights Reserved.
+
+Redistribution and use of this software and associated documentation
+("Software"), with or without modification, are permitted provided
+that the following conditions are met:
+
+1. Redistributions of source code must retain copyright
+   statements and notices.  Redistributions must also contain a
+   copy of this document.
+
+2. Redistributions in binary form must reproduce the
+   above copyright notice, this list of conditions and the
+   following disclaimer in the documentation and/or other
+   materials provided with the distribution.
+
+3. The name "classworlds" must not be used to endorse or promote
+   products derived from this Software without prior written
+   permission of The Werken Company.  For written permission,
+   please contact bob@werken.com.
+
+4. Products derived from this Software may not be called "classworlds"
+   nor may "classworlds" appear in their names without prior written
+   permission of The Werken Company. "classworlds" is a registered
+   trademark of The Werken Company.
+
+5. Due credit should be given to The Werken Company.
+   (http://classworlds.werken.com/).
+
+THIS SOFTWARE IS PROVIDED BY THE WERKEN COMPANY AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+THE WERKEN COMPANY OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+/**
+ * Indicates an attempt to add a <code>ClassRealm</code> to a
+ * <code>ClassWorld</code> with a duplicate id.
+ *
+ * <p><b>Note:</b> This is a legacy exception provided for backward compatibility with Maven 2.
+ * New code should use {@link org.codehaus.plexus.classworlds.realm.DuplicateRealmException}.</p>
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ * @deprecated Use {@link org.codehaus.plexus.classworlds.realm.DuplicateRealmException}
+ */
+@Deprecated
+public class DuplicateRealmException extends ClassWorldException {
+    // ------------------------------------------------------------
+    //     Instance members
+    // ------------------------------------------------------------
+
+    /**
+     * The realm id.
+     */
+    private final String id;
+
+    // ------------------------------------------------------------
+    //     Constructors
+    // ------------------------------------------------------------
+
+    /**
+     * Construct.
+     *
+     * @param world The world.
+     * @param id    The realm id.
+     */
+    public DuplicateRealmException(ClassWorld world, String id) {
+        super(world, id);
+        this.id = id;
+    }
+
+    // ------------------------------------------------------------
+    //     Instance methods
+    // ------------------------------------------------------------
+
+    /**
+     * Retrieve the duplicate realm id.
+     *
+     * @return The id.
+     */
+    public String getId() {
+        return this.id;
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/Launcher.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/Launcher.java
new file mode 100644
index 0000000000..d181dbbc90
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/Launcher.java
@@ -0,0 +1,76 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+/**
+ * A compatibility wrapper for {@link org.codehaus.plexus.classworlds.launcher.Launcher}
+ * provided for legacy code.
+ *
+ * <p><b>Note:</b> This is a legacy class provided for backward compatibility with Maven 2.
+ * New code should use {@link org.codehaus.plexus.classworlds.launcher.Launcher}.</p>
+ *
+ * @author Andrew Williams
+ * @deprecated Use {@link org.codehaus.plexus.classworlds.launcher.Launcher}
+ */
+@Deprecated
+public class Launcher extends org.codehaus.plexus.classworlds.launcher.Launcher {
+    public Launcher() {}
+
+    // ------------------------------------------------------------
+    //     Class methods
+    // ------------------------------------------------------------
+
+    /**
+     * Launch the launcher from the command line.
+     * Will exit using System.exit with an exit code of 0 for success, 100 if there was an unknown exception,
+     * or some other code for an application error.
+     *
+     * @param args The application command-line arguments.
+     */
+    public static void main(String[] args) {
+        org.codehaus.plexus.classworlds.launcher.Launcher.main(args);
+    }
+
+    /**
+     * Launch the launcher.
+     *
+     * @param args The application command-line arguments.
+     * @return an integer exit code
+     * @throws Exception If an error occurs.
+     */
+    public static int mainWithExitCode(String[] args) throws Exception {
+        return org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode(args);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/NoSuchRealmException.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/NoSuchRealmException.java
new file mode 100644
index 0000000000..065cd9cec4
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/NoSuchRealmException.java
@@ -0,0 +1,114 @@
+/*
+ * 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.codehaus.classworlds;
+
+/*
+
+Copyright 2002 (C) The Werken Company. All Rights Reserved.
+
+Redistribution and use of this software and associated documentation
+("Software"), with or without modification, are permitted provided
+that the following conditions are met:
+
+1. Redistributions of source code must retain copyright
+   statements and notices.  Redistributions must also contain a
+   copy of this document.
+
+2. Redistributions in binary form must reproduce the
+   above copyright notice, this list of conditions and the
+   following disclaimer in the documentation and/or other
+   materials provided with the distribution.
+
+3. The name "classworlds" must not be used to endorse or promote
+   products derived from this Software without prior written
+   permission of The Werken Company.  For written permission,
+   please contact bob@werken.com.
+
+4. Products derived from this Software may not be called "classworlds"
+   nor may "classworlds" appear in their names without prior written
+   permission of The Werken Company. "classworlds" is a registered
+   trademark of The Werken Company.
+
+5. Due credit should be given to The Werken Company.
+   (http://classworlds.werken.com/).
+
+THIS SOFTWARE IS PROVIDED BY THE WERKEN COMPANY AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+THE WERKEN COMPANY OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+/**
+ * Indicates an attempt to retrieve a <code>ClassRealm</code> from a
+ * <code>ClassWorld</code> with an invalid id.
+ *
+ * <p><b>Note:</b> This is a legacy exception provided for backward compatibility with Maven 2.
+ * New code should use {@link org.codehaus.plexus.classworlds.realm.NoSuchRealmException}.</p>
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ * @deprecated Use {@link org.codehaus.plexus.classworlds.realm.NoSuchRealmException}
+ */
+@Deprecated
+public class NoSuchRealmException extends ClassWorldException {
+    // ------------------------------------------------------------
+    //     Instance members
+    // ------------------------------------------------------------
+
+    /**
+     * The realm id.
+     */
+    private final String id;
+
+    // ------------------------------------------------------------
+    //     Constructors
+    // ------------------------------------------------------------
+
+    /**
+     * Construct.
+     *
+     * @param world The world.
+     * @param id    The realm id.
+     */
+    public NoSuchRealmException(ClassWorld world, String id) {
+        super(world, id);
+        this.id = id;
+    }
+
+    // ------------------------------------------------------------
+    //     Instance methods
+    // ------------------------------------------------------------
+
+    /**
+     * Retrieve the invalid realm id.
+     *
+     * @return The id.
+     */
+    public String getId() {
+        return this.id;
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/package-info.java b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/package-info.java
new file mode 100644
index 0000000000..e94806c4c7
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/classworlds/package-info.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+/**
+ * Legacy Classworlds 1.x API, preserved for binary compatibility with Eclipse Sisu.
+ *
+ * <p>This package has been deprecated since version 2.5.2 (2014). New code should use
+ * {@code org.codehaus.plexus.classworlds} instead.</p>
+ *
+ * <p><strong>Do not remove this package.</strong> The compiled bytecode of
+ * {@code org.eclipse.sisu:org.eclipse.sisu.plexus} references {@link ClassRealm},
+ * {@link ClassRealmAdapter}, and {@link ClassRealmReverseAdapter} from this package.
+ * Removing them causes {@code ClassNotFoundException} at runtime for any application
+ * using Sisu, including all Maven 3+ builds.</p>
+ *
+ * <p>PR #141 removed this package and had to be reverted immediately (commit 223416e).
+ * See {@code COMPATIBILITY.md} for details on what Sisu references and what preconditions
+ * must be satisfied before any future removal.</p>
+ *
+ * @deprecated Use {@code org.codehaus.plexus.classworlds} for new code.
+ */
+package org.codehaus.classworlds;
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorld.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorld.java
new file mode 100644
index 0000000000..f341a9b4a8
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorld.java
@@ -0,0 +1,305 @@
+/*
+ * 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.codehaus.plexus.classworlds;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
+import org.codehaus.plexus.classworlds.realm.FilteredClassRealm;
+import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
+
+/**
+ * A collection of <code>ClassRealm</code>s, indexed by id.
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ */
+public class ClassWorld implements Closeable {
+
+    private static final System.Logger LOGGER = System.getLogger(ClassWorld.class.getName());
+
+    private Map<String, ClassRealm> realms;
+
+    private final List<ClassWorldListener> listeners = new ArrayList<>();
+
+    private ModuleLayer moduleLayer;
+
+    private ModuleLayer.Controller moduleLayerController;
+
+    public ClassWorld(String realmId, ClassLoader classLoader) {
+        this();
+        try {
+            newRealm(realmId, classLoader);
+        } catch (DuplicateRealmException e) {
+            // Will never happen as we are just creating the world.
+        }
+    }
+
+    public ClassWorld() {
+        this.realms = new LinkedHashMap<>();
+    }
+
+    public ClassRealm newRealm(String id) throws DuplicateRealmException {
+        return newRealm(id, getClass().getClassLoader());
+    }
+
+    public ClassRealm newRealm(String id, ClassLoader classLoader) throws DuplicateRealmException {
+        return newRealm(id, classLoader, null);
+    }
+
+    /**
+     * Adds a class realm with filtering.
+     * Only resources/classes whose name matches a given predicate are exposed.
+     * @param id The identifier for this realm, must not be <code>null</code>.
+     * @param classLoader The base class loader for this realm, may be <code>null</code> to use the bootstrap class
+     *            loader.
+     * @param filter a predicate to apply to each resource name to determine if it should be loaded through this class loader
+     * @return the created class realm
+     * @throws DuplicateRealmException in case a realm with the given id does already exist
+     * @since 2.7.0
+     * @see FilteredClassRealm
+     */
+    public synchronized ClassRealm newRealm(String id, ClassLoader classLoader, Predicate<String> filter)
+            throws DuplicateRealmException {
+        if (realms.containsKey(id)) {
+            throw new DuplicateRealmException(this, id);
+        }
+        ClassRealm realm;
+        if (filter == null) {
+            realm = new ClassRealm(this, id, classLoader);
+        } else {
+            realm = new FilteredClassRealm(filter, this, id, classLoader);
+        }
+        realms.put(id, realm);
+        for (ClassWorldListener listener : listeners) {
+            listener.realmCreated(realm);
+        }
+        return realm;
+    }
+
+    /**
+     * Closes all contained class realms.
+     * @since 2.7.0
+     */
+    @Override
+    public synchronized void close() throws IOException {
+        realms.values().forEach(this::disposeRealm);
+        realms.clear();
+    }
+
+    public synchronized void disposeRealm(String id) throws NoSuchRealmException {
+        ClassRealm realm = realms.remove(id);
+        if (realm != null) {
+            disposeRealm(realm);
+        } else {
+            throw new NoSuchRealmException(this, id);
+        }
+    }
+
+    private void disposeRealm(ClassRealm realm) {
+        try {
+            realm.close();
+        } catch (IOException ignore) {
+        }
+        for (ClassWorldListener listener : listeners) {
+            listener.realmDisposed(realm);
+        }
+    }
+
+    public synchronized ClassRealm getRealm(String id) throws NoSuchRealmException {
+        if (realms.containsKey(id)) {
+            return realms.get(id);
+        }
+        throw new NoSuchRealmException(this, id);
+    }
+
+    public synchronized Collection<ClassRealm> getRealms() {
+        return Collections.unmodifiableList(new ArrayList<>(realms.values()));
+    }
+
+    public synchronized void setModuleLayer(ModuleLayer moduleLayer, ModuleLayer.Controller controller) {
+        this.moduleLayer = moduleLayer;
+        this.moduleLayerController = controller;
+    }
+
+    public ModuleLayer getModuleLayer() {
+        return moduleLayer;
+    }
+
+    public ModuleLayer.Controller getModuleLayerController() {
+        return moduleLayerController;
+    }
+
+    /**
+     * Exports a package from a named module to the given target module.
+     * Looks up the source module in the runtime layer first, then the boot layer.
+     * Only runtime layer modules can be modified via the Controller; boot layer
+     * modules require {@code --add-exports} in the launcher script.
+     *
+     * @param moduleName the source module name
+     * @param packageName the package to export
+     * @param target the target module to export to
+     */
+    public synchronized void addExports(String moduleName, String packageName, Module target) {
+        applyModuleAccess(moduleName, packageName, target, false);
+    }
+
+    /**
+     * Opens a package from a named module to the given target module for deep reflection.
+     * Looks up the source module in the runtime layer first, then the boot layer.
+     * Only runtime layer modules can be modified via the Controller; boot layer
+     * modules require {@code --add-opens} in the launcher script.
+     *
+     * @param moduleName the source module name
+     * @param packageName the package to open
+     * @param target the target module to open to
+     */
+    public synchronized void addOpens(String moduleName, String packageName, Module target) {
+        applyModuleAccess(moduleName, packageName, target, true);
+    }
+
+    /**
+     * Adds a reads edge from the named source module to the given target module.
+     * Only runtime layer modules can be modified via the Controller.
+     *
+     * @param sourceModuleName the source module name
+     * @param target the target module to read
+     */
+    public synchronized void addReads(String sourceModuleName, Module target) {
+        Module source = findModule(sourceModuleName);
+        if (source == null) {
+            LOGGER.log(
+                    System.Logger.Level.WARNING,
+                    "module-access: module ''{0}'' not found, ignoring add-reads",
+                    sourceModuleName);
+            return;
+        }
+        if (moduleLayerController == null) {
+            LOGGER.log(
+                    System.Logger.Level.WARNING,
+                    "module-access: no ModuleLayer controller available, cannot add-reads {0}",
+                    sourceModuleName);
+            return;
+        }
+        if (isBootLayerModule(source)) {
+            LOGGER.log(
+                    System.Logger.Level.WARNING,
+                    "module-access: module ''{0}'' is in the boot layer and cannot be modified at runtime;"
+                            + " use --add-reads {0}=ALL-UNNAMED on the JVM command line instead",
+                    sourceModuleName);
+            return;
+        }
+        moduleLayerController.addReads(source, target);
+    }
+
+    private void applyModuleAccess(String moduleName, String packageName, Module target, boolean open) {
+        String directive = open ? "add-opens" : "add-exports";
+        Module source = findModule(moduleName);
+        if (source == null) {
+            LOGGER.log(
+                    System.Logger.Level.WARNING,
+                    "module-access: module ''{0}'' not found, ignoring {1} {0}/{2}",
+                    moduleName,
+                    directive,
+                    packageName);
+            return;
+        }
+        if (moduleLayerController == null) {
+            LOGGER.log(
+                    System.Logger.Level.WARNING,
+                    "module-access: no ModuleLayer controller available, cannot {0} {1}/{2}",
+                    directive,
+                    moduleName,
+                    packageName);
+            return;
+        }
+        if (isBootLayerModule(source)) {
+            LOGGER.log(
+                    System.Logger.Level.WARNING,
+                    "module-access: module ''{0}'' is in the boot layer and cannot be modified at runtime;"
+                            + " use --{1} {0}/{2}=ALL-UNNAMED on the JVM command line instead",
+                    moduleName,
+                    directive,
+                    packageName);
+            return;
+        }
+        if (open) {
+            moduleLayerController.addOpens(source, packageName, target);
+        } else {
+            moduleLayerController.addExports(source, packageName, target);
+        }
+    }
+
+    private Module findModule(String moduleName) {
+        if (moduleLayer != null) {
+            Module m = moduleLayer.findModule(moduleName).orElse(null);
+            if (m != null) {
+                return m;
+            }
+        }
+        return ModuleLayer.boot().findModule(moduleName).orElse(null);
+    }
+
+    private boolean isBootLayerModule(Module module) {
+        return module.getLayer() == ModuleLayer.boot();
+    }
+
+    // from exports branch
+    public synchronized ClassRealm getClassRealm(String id) {
+        if (realms.containsKey(id)) {
+            return realms.get(id);
+        }
+        return null;
+    }
+
+    public synchronized void addListener(ClassWorldListener listener) {
+        // TODO ideally, use object identity, not equals
+        if (!listeners.contains(listener)) {
+            listeners.add(listener);
+        }
+    }
+
+    public synchronized void removeListener(ClassWorldListener listener) {
+        listeners.remove(listener);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorldException.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorldException.java
new file mode 100644
index 0000000000..72f7f52b1e
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorldException.java
@@ -0,0 +1,88 @@
+/*
+ * 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.codehaus.plexus.classworlds;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+/**
+ * Base exception for <code>ClassWorld</code> errors.
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ */
+public class ClassWorldException extends Exception {
+    // ------------------------------------------------------------
+    //     Instance members
+    // ------------------------------------------------------------
+
+    /**
+     * The world.
+     */
+    private ClassWorld world;
+
+    // ------------------------------------------------------------
+    //     Constructors
+    // ------------------------------------------------------------
+
+    /**
+     * Construct.
+     *
+     * @param world The world.
+     */
+    public ClassWorldException(final ClassWorld world) {
+        this.world = world;
+    }
+
+    /**
+     * Construct.
+     *
+     * @param world The world.
+     * @param msg   The detail message.
+     */
+    public ClassWorldException(final ClassWorld world, final String msg) {
+        super(msg);
+        this.world = world;
+    }
+
+    // ------------------------------------------------------------
+    //     Instance methods
+    // ------------------------------------------------------------
+
+    /**
+     * Retrieve the world.
+     *
+     * @return The world.
+     */
+    public ClassWorld getWorld() {
+        return this.world;
+    }
+}
diff --git a/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions-no-descriptor/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorldListener.java
similarity index 52%
copy from its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions-no-descriptor/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java
copy to impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorldListener.java
index edb262ce2d..e181cf397d 100644
--- a/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions-no-descriptor/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorldListener.java
@@ -16,14 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.its.core_extensions;
+package org.codehaus.plexus.classworlds;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
 
-import org.apache.maven.classrealm.ClassRealmManagerDelegate;
-import org.apache.maven.classrealm.ClassRealmRequest;
 import org.codehaus.plexus.classworlds.realm.ClassRealm;
-import org.codehaus.plexus.component.annotations.Component;
 
-@Component(role = ClassRealmManagerDelegate.class, hint = "TestClassRealmManagerDelegate")
-public class TestClassRealmManagerDelegate implements ClassRealmManagerDelegate {
-    public void setupRealm(ClassRealm classRealm, ClassRealmRequest request) {}
+public interface ClassWorldListener {
+    void realmCreated(ClassRealm realm);
+
+    void realmDisposed(ClassRealm realm);
 }
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/UrlUtils.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/UrlUtils.java
new file mode 100644
index 0000000000..32ed616a5e
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/UrlUtils.java
@@ -0,0 +1,70 @@
+/*
+ * 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.codehaus.plexus.classworlds;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
+ */
+public class UrlUtils {
+    public static String normalizeUrlPath(String name) {
+        if (name.startsWith("/")) {
+            name = name.substring(1);
+        }
+        // Looking for org/codehaus/werkflow/personality/basic/../common/core-idioms.xml
+        //                                               |    i  |
+        //                                               +-------+ remove
+        //
+        int i = name.indexOf("/..");
+        // Can't be at the beginning because we have no root to refer to so
+        // we start at 1.
+        if (i > 0) {
+            int j = name.lastIndexOf("/", i - 1);
+            if (j >= 0) {
+                name = name.substring(0, j) + name.substring(i + 3);
+            }
+        }
+        return name;
+    }
+
+    public static Set<URL> getURLs(URLClassLoader loader) {
+        return new HashSet<>(Arrays.asList(loader.getURLs()));
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationException.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationException.java
new file mode 100644
index 0000000000..4eb4b57570
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationException.java
@@ -0,0 +1,66 @@
+/*
+ * 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.codehaus.plexus.classworlds.launcher;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+/**
+ * Indicates an error during <code>Launcher</code> configuration.
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ */
+public class ConfigurationException extends Exception {
+    /**
+     * Construct.
+     *
+     * @param msg The message.
+     */
+    public ConfigurationException(String msg) {
+        super(msg);
+    }
+
+    /**
+     * Construct.
+     *
+     * @param msg    The message.
+     * @param lineNo The number of configuration line where the problem occurred.
+     * @param line   The configuration line where the problem occurred.
+     */
+    public ConfigurationException(String msg, int lineNo, String line) {
+        super(msg + " (" + lineNo + "): " + line);
+    }
+
+    protected ConfigurationException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationHandler.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationHandler.java
new file mode 100644
index 0000000000..628055b525
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationHandler.java
@@ -0,0 +1,113 @@
+/*
+ * 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.codehaus.plexus.classworlds.launcher;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.File;
+import java.net.URL;
+
+import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
+import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
+
+/**
+ * Receive notification of the logical content of launcher configuration, independently from parsing.
+ *
+ * @author Igor Fedorenko
+ */
+public interface ConfigurationHandler {
+
+    /**
+     * Define the main class name
+     * @param mainClassName the main class name
+     * @param mainRealmName the main realm from which the main class is loaded
+     */
+    void setAppMain(String mainClassName, String mainRealmName);
+
+    /**
+     * Define a new realm
+     * @param realmName the new realm name
+     * @throws DuplicateRealmException when realm with name already exists
+     */
+    void addRealm(String realmName) throws DuplicateRealmException;
+
+    /**
+     * Add an import specification from a realm
+     * @param realmName the realm name
+     * @param importSpec the import specification
+     * @throws NoSuchRealmException if realm doesn't exist
+     */
+    void addImportFrom(String realmName, String importSpec) throws NoSuchRealmException;
+
+    /**
+     * Add a file to the realm
+     * @param file the file to load content from
+     */
+    void addLoadFile(File file);
+
+    /**
+     * Add an URL to the realm
+     * @param url the url to load content from
+     */
+    void addLoadURL(URL url);
+
+    /**
+     * Add a file to the module path. Jars added via this method will be loaded
+     * as JPMS named modules in a {@link ModuleLayer}, rather than on the classpath.
+     * @param file the jar file to add to the module path
+     */
+    default void addModuleFile(File file) {}
+
+    /**
+     * Export a package from a module to a target module.
+     * @param module the source module name
+     * @param pkg the package to export
+     * @param target the target module name, or {@code ALL-UNNAMED} for the unnamed module
+     */
+    default void addExports(String module, String pkg, String target) {}
+
+    /**
+     * Open a package from a module to a target module for deep reflection.
+     * @param module the source module name
+     * @param pkg the package to open
+     * @param target the target module name, or {@code ALL-UNNAMED} for the unnamed module
+     */
+    default void addOpens(String module, String pkg, String target) {}
+
+    /**
+     * Add a reads edge from a source module to a target module.
+     * @param source the source module name
+     * @param target the target module name
+     */
+    default void addReads(String source, String target) {}
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationParser.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationParser.java
new file mode 100644
index 0000000000..22b8604c8b
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationParser.java
@@ -0,0 +1,492 @@
+/*
+ * 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.codehaus.plexus.classworlds.launcher;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Properties;
+
+import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
+import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
+
+/**
+ * Event based launcher configuration parser, delegating effective configuration handling to ConfigurationHandler.
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ * @author Jason van Zyl
+ * @author Igor Fedorenko
+ * @see ConfigurationHandler
+ */
+public class ConfigurationParser {
+    public static final String MAIN_PREFIX = "main is";
+
+    public static final String SET_PREFIX = "set";
+
+    public static final String IMPORT_PREFIX = "import";
+
+    public static final String LOAD_PREFIX = "load";
+
+    public static final String MODULE_PREFIX = "module";
+
+    public static final String ADD_EXPORTS_PREFIX = "add-exports";
+
+    public static final String ADD_OPENS_PREFIX = "add-opens";
+
+    public static final String ADD_READS_PREFIX = "add-reads";
+
+    /**
+     * Optionally spec prefix.
+     */
+    public static final String OPTIONALLY_PREFIX = "optionally";
+
+    protected static final String FROM_SEPARATOR = " from ";
+
+    protected static final String USING_SEPARATOR = " using ";
+
+    protected static final String DEFAULT_SEPARATOR = " default ";
+
+    private final ConfigurationHandler handler;
+
+    private final Properties systemProperties;
+
+    public ConfigurationParser(ConfigurationHandler handler, Properties systemProperties) {
+        this.handler = handler;
+        this.systemProperties = systemProperties;
+    }
+
+    /**
+     * Parse launcher configuration file and send events to the handler.
+     *
+     * @param is the inputstream
+     * @throws IOException when IOException occurs
+     * @throws ConfigurationException when ConfigurationException occurs
+     * @throws DuplicateRealmException when realm already exists
+     * @throws NoSuchRealmException when realm doesn't exist
+     */
+    public void parse(InputStream is)
+            throws IOException, ConfigurationException, DuplicateRealmException, NoSuchRealmException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+            String line;
+            int lineNo = 0;
+            boolean mainSet = false;
+            String curRealm = null;
+            while (true) {
+                line = reader.readLine();
+                if (line == null) {
+                    break;
+                }
+                ++lineNo;
+                line = line.trim();
+                if (canIgnore(line)) {
+                    continue;
+                }
+                char lineFirstChar = line.charAt(0);
+                switch (lineFirstChar) {
+                    case 'm':
+                        if (line.startsWith(MODULE_PREFIX)) {
+                            handleModuleConfiguration(line, lineNo);
+                        } else {
+                            mainSet = handleMainConfiguration(line, lineNo, mainSet);
+                        }
+                        break;
+                    case 's':
+                        if (handleSetConfiguration(line, lineNo)) {
+                            continue;
+                        }
+                        break;
+                    case '[':
+                        curRealm = handleRealmConfiguration(line, lineNo);
+                        break;
+                    case 'i':
+                        handleImportConfiguration(line, lineNo, curRealm);
+                        break;
+                    case 'l':
+                        handleLoadConfiguration(line, lineNo);
+                        break;
+                    case 'a':
+                        handleAddDirective(line, lineNo);
+                        break;
+                    case 'o':
+                        handleOptionallyConfiguration(line, lineNo);
+                        break;
+                    default:
+                        throw new ConfigurationException("Unhandled configuration", lineNo, line);
+                }
+            }
+        }
+    }
+
+    /**
+     * Load a glob into the specified classloader.
+     *
+     * @param line       The path configuration line.
+     * @param optionally Whether the path is optional or required
+     * @throws MalformedURLException If the line does not represent
+     *                               a valid path element.
+     * @throws FileNotFoundException If the line does not represent
+     *                               a valid path element in the filesystem.
+     * @throws ConfigurationException will never occur (thrown for backwards compatibility)
+     */
+    protected void loadGlob(String line, boolean optionally)
+            throws MalformedURLException, FileNotFoundException, ConfigurationException {
+        File globFile = new File(line);
+        File dir = globFile.getParentFile();
+        if (!dir.exists()) {
+            if (optionally) {
+                return;
+            } else {
+                throw new FileNotFoundException(dir.toString());
+            }
+        }
+        String localName = globFile.getName();
+        int starLoc = localName.indexOf("*");
+        final String prefix = localName.substring(0, starLoc);
+        final String suffix = localName.substring(starLoc + 1);
+        File[] matches = dir.listFiles((dir1, name) -> {
+            if (!name.startsWith(prefix)) {
+                return false;
+            }
+            if (!name.endsWith(suffix)) {
+                return false;
+            }
+            return true;
+        });
+        for (File match : matches) {
+            handler.addLoadFile(match);
+        }
+    }
+
+    /**
+     * Filter a string for system properties.
+     *
+     * <p>Supports {@code ${name}}, {@code ${name:-default}} (use default when the property is
+     * absent or empty), and {@code ${name:+alternate}} (use alternate when the property is
+     * present and non-empty) syntax.</p>
+     *
+     * @param text The text to filter.
+     * @return The filtered text.
+     * @throws ConfigurationException If a required property does not exist or if there is a
+     *                                syntax error (unterminated placeholder).
+     */
+    protected String filter(String text) throws ConfigurationException {
+        // Quick check: if no placeholder present, return as-is
+        if (!text.contains("${")) {
+            return text;
+        }
+        // Validate: no unterminated placeholders
+        int start = text.indexOf("${");
+        while (start >= 0) {
+            int stop = text.indexOf("}", start);
+            if (stop < 0) {
+                throw new ConfigurationException("Unterminated property: " + text.substring(start));
+            }
+            start = text.indexOf("${", stop + 1);
+        }
+        try {
+            String result = Interpolator.interpolate(text, name -> {
+                String value = systemProperties.getProperty(name);
+                /* do our best if we are not running from surefire */
+                if (value == null && "basedir".equals(name)) {
+                    value = new File("").getAbsolutePath();
+                }
+                return value;
+            });
+            // If any placeholder survived unexpanded (no default, property missing), report it
+            int unresolved = result.indexOf("${");
+            if (unresolved >= 0) {
+                int stop = result.indexOf("}", unresolved);
+                String propName =
+                        stop > unresolved ? result.substring(unresolved + 2, stop) : result.substring(unresolved + 2);
+                throw new ConfigurationException("No such property: " + propName);
+            }
+            return result;
+        } catch (IllegalArgumentException e) {
+            throw new ConfigurationException(e.getMessage());
+        }
+    }
+
+    /**
+     * Determine if a line can be ignored because it is
+     * a comment or simply blank.
+     *
+     * @param line The line to test.
+     * @return <code>true</code> if the line is ignorable,
+     *         otherwise <code>false</code>.
+     */
+    private boolean canIgnore(String line) {
+        return (line.isEmpty() || line.startsWith("#"));
+    }
+
+    private boolean handleMainConfiguration(String line, int lineNo, boolean mainSet) throws ConfigurationException {
+        if (line.startsWith(MAIN_PREFIX)) {
+            if (mainSet) {
+                throw new ConfigurationException("Duplicate main configuration", lineNo, line);
+            }
+            int fromLoc = line.indexOf(FROM_SEPARATOR, MAIN_PREFIX.length());
+            if (fromLoc < 0) {
+                throw new ConfigurationException("Missing from clause", lineNo, line);
+            }
+            String mainClassName =
+                    filter(line.substring(MAIN_PREFIX.length(), fromLoc).trim());
+            String mainRealmName =
+                    filter(line.substring(fromLoc + FROM_SEPARATOR.length()).trim());
+            this.handler.setAppMain(mainClassName, mainRealmName);
+            return true;
+        }
+        throw new ConfigurationException("Unhandled configuration", lineNo, line);
+    }
+
+    private boolean handleSetConfiguration(String line, int lineNo) throws ConfigurationException {
+        if (line.startsWith(SET_PREFIX)) {
+            String conf = line.substring(SET_PREFIX.length()).trim();
+            int usingLoc = conf.indexOf(USING_SEPARATOR);
+            String property = null;
+            String propertiesFileName = null;
+            if (usingLoc >= 0) {
+                property = conf.substring(0, usingLoc).trim();
+                propertiesFileName = filter(
+                        conf.substring(usingLoc + USING_SEPARATOR.length()).trim());
+                conf = propertiesFileName;
+            }
+            String defaultValue = null;
+            int defaultLoc = conf.indexOf(DEFAULT_SEPARATOR);
+            if (defaultLoc >= 0) {
+                defaultValue = filter(
+                        conf.substring(defaultLoc + DEFAULT_SEPARATOR.length()).trim());
+                if (property == null) {
+                    property = conf.substring(0, defaultLoc).trim();
+                } else {
+                    propertiesFileName = conf.substring(0, defaultLoc).trim();
+                }
+            }
+            String value = systemProperties.getProperty(property);
+            if (value != null) {
+                return true;
+            }
+            if (propertiesFileName != null) {
+                File propertiesFile = new File(propertiesFileName);
+                if (propertiesFile.exists()) {
+                    Properties properties = new Properties();
+                    try (InputStream inputStream = Files.newInputStream(Paths.get(propertiesFileName))) {
+                        properties.load(inputStream);
+                        value = properties.getProperty(property);
+                    } catch (Exception e) {
+                        // do nothing
+                    }
+                }
+            }
+            if (value == null && defaultValue != null) {
+                value = defaultValue;
+            }
+            if (value != null) {
+                value = filter(value);
+                systemProperties.setProperty(property, value);
+            }
+            return false;
+        }
+        throw new ConfigurationException("Unhandled configuration", lineNo, line);
+    }
+
+    private String handleRealmConfiguration(String line, int lineNo)
+            throws ConfigurationException, DuplicateRealmException {
+        int rbrack = line.indexOf("]");
+        if (rbrack < 0) {
+            throw new ConfigurationException("Invalid realm specifier", lineNo, line);
+        }
+        String realmName = line.substring(1, rbrack);
+        handler.addRealm(realmName);
+        return realmName;
+    }
+
+    private void handleImportConfiguration(String line, int lineNo, String curRealm)
+            throws ConfigurationException, NoSuchRealmException {
+        if (line.startsWith(IMPORT_PREFIX)) {
+            if (curRealm == null) {
+                throw new ConfigurationException("Unhandled import", lineNo, line);
+            }
+            int fromLoc = line.indexOf(FROM_SEPARATOR, IMPORT_PREFIX.length());
+            if (fromLoc < 0) {
+                throw new ConfigurationException("Missing from clause", lineNo, line);
+            }
+            String importSpec = line.substring(IMPORT_PREFIX.length(), fromLoc).trim();
+            String realmName = line.substring(fromLoc + FROM_SEPARATOR.length()).trim();
+            handler.addImportFrom(realmName, importSpec);
+            return;
+        }
+        throw new ConfigurationException("Unhandled configuration", lineNo, line);
+    }
+
+    private void handleLoadConfiguration(String line, int lineNo)
+            throws ConfigurationException, FileNotFoundException, MalformedURLException {
+        if (line.startsWith(LOAD_PREFIX)) {
+            String constituent = line.substring(LOAD_PREFIX.length()).trim();
+            constituent = filter(constituent);
+            if (constituent.contains("*")) {
+                loadGlob(constituent, false /*not optionally*/);
+            } else {
+                File file = new File(constituent);
+                if (file.exists()) {
+                    handler.addLoadFile(file);
+                } else {
+                    try {
+                        handler.addLoadURL(new URL(constituent));
+                    } catch (MalformedURLException e) {
+                        throw new FileNotFoundException(constituent);
+                    }
+                }
+            }
+            return;
+        }
+        throw new ConfigurationException("Unhandled configuration", lineNo, line);
+    }
+
+    private void handleModuleConfiguration(String line, int lineNo)
+            throws ConfigurationException, FileNotFoundException, MalformedURLException {
+        if (line.startsWith(MODULE_PREFIX)) {
+            String constituent = line.substring(MODULE_PREFIX.length()).trim();
+            constituent = filter(constituent);
+            if (constituent.contains("*")) {
+                moduleGlob(constituent);
+            } else {
+                File file = new File(constituent);
+                if (file.exists()) {
+                    handler.addModuleFile(file);
+                } else {
+                    throw new FileNotFoundException(constituent);
+                }
+            }
+            return;
+        }
+        throw new ConfigurationException("Unhandled configuration", lineNo, line);
+    }
+
+    private void handleAddDirective(String line, int lineNo) throws ConfigurationException {
+        if (line.startsWith(ADD_EXPORTS_PREFIX)) {
+            String spec = line.substring(ADD_EXPORTS_PREFIX.length()).trim();
+            parseModulePackageDirective(spec, lineNo, line, handler::addExports);
+        } else if (line.startsWith(ADD_OPENS_PREFIX)) {
+            String spec = line.substring(ADD_OPENS_PREFIX.length()).trim();
+            parseModulePackageDirective(spec, lineNo, line, handler::addOpens);
+        } else if (line.startsWith(ADD_READS_PREFIX)) {
+            String spec = line.substring(ADD_READS_PREFIX.length()).trim();
+            int eqLoc = spec.indexOf('=');
+            if (eqLoc < 0) {
+                throw new ConfigurationException("Missing '=' in add-reads directive", lineNo, line);
+            }
+            String source = spec.substring(0, eqLoc).trim();
+            String target = spec.substring(eqLoc + 1).trim();
+            handler.addReads(source, target);
+        } else {
+            throw new ConfigurationException("Unhandled configuration", lineNo, line);
+        }
+    }
+
+    private void parseModulePackageDirective(String spec, int lineNo, String line, ModulePackageHandler mph)
+            throws ConfigurationException {
+        int slashLoc = spec.indexOf('/');
+        if (slashLoc < 0) {
+            throw new ConfigurationException("Missing '/' in module/package directive", lineNo, line);
+        }
+        String module = spec.substring(0, slashLoc).trim();
+        String rest = spec.substring(slashLoc + 1).trim();
+        String pkg;
+        String target;
+        int eqLoc = rest.indexOf('=');
+        if (eqLoc >= 0) {
+            pkg = rest.substring(0, eqLoc).trim();
+            target = rest.substring(eqLoc + 1).trim();
+        } else {
+            pkg = rest;
+            target = "ALL-UNNAMED";
+        }
+        mph.handle(module, pkg, target);
+    }
+
+    @FunctionalInterface
+    private interface ModulePackageHandler {
+        void handle(String module, String pkg, String target);
+    }
+
+    protected void moduleGlob(String line) throws MalformedURLException, FileNotFoundException {
+        File globFile = new File(line);
+        File dir = globFile.getParentFile();
+        if (!dir.exists()) {
+            return;
+        }
+        String localName = globFile.getName();
+        int starLoc = localName.indexOf("*");
+        final String prefix = localName.substring(0, starLoc);
+        final String suffix = localName.substring(starLoc + 1);
+        File[] matches = dir.listFiles((dir1, name) -> name.startsWith(prefix) && name.endsWith(suffix));
+        if (matches != null) {
+            for (File match : matches) {
+                handler.addModuleFile(match);
+            }
+        }
+    }
+
+    private void handleOptionallyConfiguration(String line, int lineNo)
+            throws ConfigurationException, FileNotFoundException, MalformedURLException {
+        if (line.startsWith(OPTIONALLY_PREFIX)) {
+            String constituent = line.substring(OPTIONALLY_PREFIX.length()).trim();
+            constituent = filter(constituent);
+            if (constituent.contains("*")) {
+                loadGlob(constituent, true /*optionally*/);
+            } else {
+                File file = new File(constituent);
+                if (file.exists()) {
+                    handler.addLoadFile(file);
+                } else {
+                    try {
+                        handler.addLoadURL(new URL(constituent));
+                    } catch (MalformedURLException e) {
+                        // swallow
+                    }
+                }
+            }
+            return;
+        }
+        throw new ConfigurationException("Unhandled configuration", lineNo, line);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/Configurator.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/Configurator.java
new file mode 100644
index 0000000000..5701169d11
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/Configurator.java
@@ -0,0 +1,357 @@
+/*
+ * 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.codehaus.plexus.classworlds.launcher;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.module.Configuration;
+import java.lang.module.FindException;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ResolutionException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
+import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
+
+/**
+ * <code>Launcher</code> configurator.
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ * @author Jason van Zyl
+ */
+public class Configurator implements ConfigurationHandler {
+    private static final System.Logger LOGGER = System.getLogger(Configurator.class.getName());
+
+    /**
+     * The launcher to configure.
+     */
+    private Launcher launcher;
+
+    private ClassWorld world;
+
+    /**
+     * Processed Realms.
+     */
+    private Map<String, ClassRealm> configuredRealms;
+
+    /**
+     * Current Realm.
+     */
+    private ClassRealm curRealm;
+
+    private ClassLoader foreignClassLoader = null;
+
+    private List<Path> modulePaths = new ArrayList<>();
+
+    /**
+     * Module JAR paths that could not be loaded into a JPMS layer and must be added to the
+     * first realm that gets created instead (used when the {@code module} directive in the
+     * configuration file precedes any {@code [section]} header, so no realm exists yet when
+     * the fallback fires).
+     */
+    private List<Path> pendingModuleClasspathFallback = new ArrayList<>();
+
+    private List<String[]> exportDirectives = new ArrayList<>();
+
+    private List<String[]> openDirectives = new ArrayList<>();
+
+    private List<String[]> readDirectives = new ArrayList<>();
+
+    /**
+     * Construct.
+     *
+     * @param launcher The launcher to configure.
+     */
+    public Configurator(Launcher launcher) {
+        this.launcher = launcher;
+        configuredRealms = new HashMap<>();
+        if (launcher != null) {
+            this.foreignClassLoader = launcher.getSystemClassLoader();
+        }
+    }
+
+    /**
+     * Construct.
+     *
+     * @param world The classWorld to configure.
+     */
+    public Configurator(ClassWorld world) {
+        setClassWorld(world);
+    }
+
+    /**
+     * set world.
+     * this setter is provided so you can use the same configurator to configure several "worlds"
+     *
+     * @param world The classWorld to configure.
+     */
+    public void setClassWorld(ClassWorld world) {
+        this.world = world;
+        configuredRealms = new HashMap<>();
+    }
+
+    /**
+     * Configure from a file.
+     *
+     * @param is The config input stream
+     * @throws IOException             If an error occurs reading the config file.
+     * @throws MalformedURLException   If the config file contains invalid URLs.
+     * @throws ConfigurationException  If the config file is corrupt.
+     * @throws DuplicateRealmException If the config file defines two realms with the same id.
+     * @throws NoSuchRealmException    If the config file defines a main entry point in
+     *                                 a non-existent realm.
+     */
+    public void configure(InputStream is)
+            throws IOException, ConfigurationException, DuplicateRealmException, NoSuchRealmException {
+        if (world == null) {
+            world = new ClassWorld();
+        }
+        curRealm = null;
+        foreignClassLoader = null;
+        modulePaths = new ArrayList<>();
+        exportDirectives = new ArrayList<>();
+        openDirectives = new ArrayList<>();
+        readDirectives = new ArrayList<>();
+        if (this.launcher != null) {
+            foreignClassLoader = this.launcher.getSystemClassLoader();
+        }
+        ConfigurationParser parser = new ConfigurationParser(this, System.getProperties());
+        parser.parse(is);
+        createModuleLayer();
+        // Associate child realms to their parents.
+        associateRealms();
+        if (this.launcher != null) {
+            this.launcher.setWorld(world);
+        }
+    }
+
+    // TODO return this to protected when the legacy wrappers can be removed.
+    /**
+     * Associate parent realms with their children.
+     */
+    public void associateRealms() {
+        List<String> sortRealmNames = new ArrayList<>(configuredRealms.keySet());
+        // sort by name
+        sortRealmNames.sort(String::compareTo);
+        // So now we have something like the following for defined
+        // realms:
+        //
+        // root
+        // root.maven
+        // root.maven.plugin
+        //
+        // Now if the name of a realm is a superset of an existing realm
+        // the we want to make child/parent associations.
+        for (String realmName : sortRealmNames) {
+            int j = realmName.lastIndexOf('.');
+            if (j > 0) {
+                String parentRealmName = realmName.substring(0, j);
+                ClassRealm parentRealm = configuredRealms.get(parentRealmName);
+                if (parentRealm != null) {
+                    ClassRealm realm = configuredRealms.get(realmName);
+                    realm.setParentRealm(parentRealm);
+                }
+            }
+        }
+    }
+
+    public void addImportFrom(String relamName, String importSpec) throws NoSuchRealmException {
+        curRealm.importFrom(relamName, importSpec);
+    }
+
+    public void addLoadFile(File file) {
+        try {
+            curRealm.addURL(file.toURI().toURL());
+        } catch (MalformedURLException e) {
+            // can't really happen... or can it?
+        }
+    }
+
+    public void addLoadURL(URL url) {
+        curRealm.addURL(url);
+    }
+
+    public void addRealm(String realmName) throws DuplicateRealmException {
+        curRealm = world.newRealm(realmName, foreignClassLoader);
+        // If the 'module' directive failed to create a JPMS layer before any section was
+        // declared, drain the deferred classpath fallback into this first realm.
+        if (!pendingModuleClasspathFallback.isEmpty()) {
+            for (Path path : pendingModuleClasspathFallback) {
+                try {
+                    curRealm.addURL(path.toUri().toURL());
+                } catch (MalformedURLException e) {
+                    // ignore
+                }
+            }
+            pendingModuleClasspathFallback.clear();
+        }
+        // Stash the configured realm for subsequent association processing.
+        configuredRealms.put(realmName, curRealm);
+    }
+
+    public void addModuleFile(File file) {
+        modulePaths.add(file.toPath());
+    }
+
+    public List<Path> getModulePaths() {
+        return modulePaths;
+    }
+
+    public void addExports(String module, String pkg, String target) {
+        exportDirectives.add(new String[] {module, pkg, target});
+    }
+
+    public void addOpens(String module, String pkg, String target) {
+        openDirectives.add(new String[] {module, pkg, target});
+    }
+
+    public void addReads(String source, String target) {
+        readDirectives.add(new String[] {source, target});
+    }
+
+    private void createModuleLayer() {
+        if (modulePaths.isEmpty()
+                && exportDirectives.isEmpty()
+                && openDirectives.isEmpty()
+                && readDirectives.isEmpty()) {
+            return;
+        }
+        if (!modulePaths.isEmpty()) {
+            try {
+                Set<String> bootModuleNames = ModuleLayer.boot().modules().stream()
+                        .map(Module::getName)
+                        .collect(Collectors.toSet());
+                ModuleFinder finder = ModuleFinder.of(modulePaths.toArray(new Path[0]));
+                Set<String> newModules = finder.findAll().stream()
+                        .map(ref -> ref.descriptor().name())
+                        .filter(name -> !bootModuleNames.contains(name))
+                        .collect(Collectors.toSet());
+                if (!newModules.isEmpty()) {
+                    Configuration cf =
+                            ModuleLayer.boot().configuration().resolve(finder, ModuleFinder.of(), newModules);
+                    ClassLoader parent =
+                            foreignClassLoader != null ? foreignClassLoader : ClassLoader.getSystemClassLoader();
+                    ModuleLayer.Controller controller =
+                            ModuleLayer.defineModulesWithOneLoader(cf, List.of(ModuleLayer.boot()), parent);
+                    ModuleLayer layer = controller.layer();
+                    ClassLoader oldForeignClassLoader = foreignClassLoader;
+                    foreignClassLoader = layer.findLoader(newModules.iterator().next());
+                    world.setModuleLayer(layer, controller);
+                    // Realms created during parsing (before this module layer existed) used the
+                    // old foreignClassLoader as their parent. Retroactively update them so they
+                    // delegate to the new module layer loader, making module-layer classes (e.g.
+                    // JLine) accessible from the classrealm without duplicating jars on the
+                    // classpath.
+                    for (ClassRealm realm : configuredRealms.values()) {
+                        if (realm.getParentClassLoader() == oldForeignClassLoader) {
+                            realm.setParentClassLoader(foreignClassLoader);
+                        }
+                    }
+                }
+            } catch (FindException | ResolutionException | LayerInstantiationException e) {
+                LOGGER.log(
+                        System.Logger.Level.DEBUG,
+                        "Failed to create JPMS module layer, falling back to classpath mode: " + e.getMessage());
+                addModulePathsToClasspath();
+            }
+        }
+        Module unnamedTarget = getClass().getModule();
+        for (String[] directive : exportDirectives) {
+            Module target = resolveTargetModule(directive[2], unnamedTarget);
+            if (target != null) {
+                world.addExports(directive[0], directive[1], target);
+            }
+        }
+        for (String[] directive : openDirectives) {
+            Module target = resolveTargetModule(directive[2], unnamedTarget);
+            if (target != null) {
+                world.addOpens(directive[0], directive[1], target);
+            }
+        }
+        for (String[] directive : readDirectives) {
+            Module target = resolveTargetModule(directive[1], unnamedTarget);
+            if (target != null) {
+                world.addReads(directive[0], target);
+            }
+        }
+    }
+
+    private void addModulePathsToClasspath() {
+        if (curRealm != null) {
+            for (Path path : modulePaths) {
+                try {
+                    curRealm.addURL(path.toUri().toURL());
+                } catch (MalformedURLException e) {
+                    // ignore
+                }
+            }
+        } else {
+            // The 'module' directive appeared before any [section]: no realm exists yet.
+            // Defer adding these JARs to the classpath of the first realm to be created.
+            pendingModuleClasspathFallback.addAll(modulePaths);
+        }
+    }
+
+    private Module resolveTargetModule(String targetName, Module unnamedTarget) {
+        if ("ALL-UNNAMED".equals(targetName)) {
+            return unnamedTarget;
+        }
+        ModuleLayer layer = world.getModuleLayer();
+        if (layer != null) {
+            Module m = layer.findModule(targetName).orElse(null);
+            if (m != null) {
+                return m;
+            }
+        }
+        return ModuleLayer.boot().findModule(targetName).orElse(null);
+    }
+
+    public void setAppMain(String mainClassName, String mainRealmName) {
+        if (this.launcher != null) {
+            this.launcher.setAppMain(mainClassName, mainRealmName);
+        }
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/Interpolator.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/Interpolator.java
new file mode 100644
index 0000000000..5849d67fcd
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/Interpolator.java
@@ -0,0 +1,168 @@
+/*
+ * 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.codehaus.plexus.classworlds.launcher;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.UnaryOperator;
+
+/**
+ * String interpolator supporting {@code ${var}}, {@code ${var:-default}} and {@code ${var:+alternate}}
+ * syntax. Copied from {@code org.apache.maven.impl.model.DefaultInterpolator} to keep maven-classworlds
+ * free of any maven-impl dependency.
+ */
+class Interpolator {
+
+    private static final char ESCAPE_CHAR = '\\';
+    private static final String DELIM_START = "${";
+    private static final String DELIM_STOP = "}";
+    private static final String MARKER = "$__";
+    private static final String OP_DEFAULT = ":-";
+    private static final String OP_ALTERNATE = ":+";
+
+    private Interpolator() {}
+
+    /**
+     * Interpolates {@code val}, resolving {@code ${name}} placeholders via {@code callback}.
+     * Supports {@code ${var:-default}} (use default when var is absent/empty) and
+     * {@code ${var:+alternate}} (use alternate when var is present and non-empty).
+     *
+     * @param val      the string to interpolate
+     * @param callback function that resolves a variable name to its value, or {@code null}
+     * @return the interpolated string
+     */
+    static String interpolate(String val, UnaryOperator<String> callback) {
+        return unescape(doSubstVars(val, null, null, callback));
+    }
+
+    private static String doSubstVars(
+            String val, String currentKey, Set<String> cycleMap, UnaryOperator<String> callback) {
+        if (val == null || val.isEmpty()) {
+            return val;
+        }
+        if (cycleMap == null) {
+            cycleMap = new HashSet<>();
+        }
+        if (currentKey != null) {
+            cycleMap.add(currentKey);
+        }
+        int startDelim;
+        int stopDelim = -1;
+        do {
+            stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
+            while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR) {
+                stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
+            }
+            startDelim = val.indexOf(DELIM_START);
+            while (stopDelim >= 0) {
+                int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
+                if ((idx < 0) || (idx > stopDelim)) {
+                    break;
+                } else if (idx < stopDelim) {
+                    startDelim = idx;
+                }
+            }
+        } while (startDelim >= 0 && stopDelim >= 0 && stopDelim < startDelim + DELIM_START.length());
+        if ((startDelim < 0) || (stopDelim < 0)) {
+            cycleMap.remove(currentKey);
+            return val;
+        }
+        String variable = val.substring(startDelim + DELIM_START.length(), stopDelim);
+        String substValue = processSubstitution(variable, cycleMap, callback);
+        val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length());
+        val = doSubstVars(val, currentKey, cycleMap, callback);
+        cycleMap.remove(currentKey);
+        return val;
+    }
+
+    private static String processSubstitution(String variable, Set<String> cycleMap, UnaryOperator<String> callback) {
+        int startIdx = 0;
+        String substValue = null;
+        while (startIdx < variable.length()) {
+            int idx1 = variable.indexOf(OP_DEFAULT, startIdx);
+            int idx2 = variable.indexOf(OP_ALTERNATE, startIdx);
+            int idx = idx1 >= 0 ? (idx2 >= 0 ? Math.min(idx1, idx2) : idx1) : idx2;
+            if (idx < 0) {
+                if (substValue == null) {
+                    substValue = resolveVariable(variable.substring(startIdx), cycleMap, callback);
+                }
+                break;
+            }
+            String varPart = variable.substring(startIdx, idx);
+            if (substValue == null) {
+                substValue = resolveVariable(varPart, cycleMap, callback);
+            }
+            int nextIdx1 = variable.indexOf(OP_DEFAULT, idx + 2);
+            int nextIdx2 = variable.indexOf(OP_ALTERNATE, idx + 2);
+            int nextIdx = nextIdx1 >= 0 ? (nextIdx2 >= 0 ? Math.min(nextIdx1, nextIdx2) : nextIdx1) : nextIdx2;
+            String op = variable.substring(idx, idx + 2);
+            String opValue = variable.substring(idx + 2, nextIdx >= 0 ? nextIdx : variable.length());
+            String processedOpValue = doSubstVars(opValue, variable, cycleMap, callback);
+            if (OP_ALTERNATE.equals(op)) {
+                if (substValue != null && !substValue.isEmpty()) {
+                    substValue = processedOpValue;
+                    break;
+                }
+            } else {
+                // OP_DEFAULT
+                if (substValue == null || substValue.isEmpty()) {
+                    substValue = processedOpValue;
+                    break;
+                }
+            }
+            startIdx = nextIdx >= 0 ? nextIdx : variable.length();
+        }
+        // When the variable is unresolvable (no value, no default), preserve the placeholder
+        // so that filter() can detect it and throw ConfigurationException.
+        if (substValue == null) {
+            substValue = MARKER + "{" + variable + "}";
+        }
+        return substValue;
+    }
+
+    private static String resolveVariable(String variable, Set<String> cycleMap, UnaryOperator<String> callback) {
+        if (!cycleMap.add(variable)) {
+            throw new IllegalArgumentException("Recursive variable reference: " + variable);
+        }
+        String value = callback != null ? callback.apply(variable) : null;
+        if (value != null) {
+            value = doSubstVars(value, variable, cycleMap, callback);
+        }
+        cycleMap.remove(variable);
+        return value;
+    }
+
+    private static String unescape(String val) {
+        if (val == null || val.isEmpty()) {
+            return val;
+        }
+        if (val.contains(MARKER)) {
+            val = val.replace(MARKER, "$");
+        }
+        int escape = val.indexOf(ESCAPE_CHAR);
+        while (escape >= 0 && escape < val.length() - 1) {
+            char c = val.charAt(escape + 1);
+            if (c == '{' || c == '}') {
+                val = val.substring(0, escape) + val.substring(escape + 1);
+            }
+            escape = val.indexOf(ESCAPE_CHAR, escape + 1);
+        }
+        return val;
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/Launcher.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/Launcher.java
new file mode 100644
index 0000000000..1dfefc77cd
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/Launcher.java
@@ -0,0 +1,365 @@
+/*
+ * 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.codehaus.plexus.classworlds.launcher;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
+import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
+
+/**
+ * <p>Command-line invokable application launcher.</p>
+ *
+ * <p>This launcher class assists in the creation of classloaders and <code>ClassRealm</code>s
+ * from a configuration file and the launching of the application's <code>main</code>
+ * method from the correct class loaded through the correct classloader.</p>
+ *
+ * <p> The path to the configuration file is specified using the <code>classworlds.conf</code>
+ * system property, typically specified using the <code>-D</code> switch to
+ * <code>java</code>.</p>
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ */
+public class Launcher {
+    protected static final String CLASSWORLDS_CONF = "classworlds.conf";
+
+    protected static final String UBERJAR_CONF_DIR = "WORLDS-INF/conf/";
+
+    protected ClassLoader systemClassLoader;
+
+    protected String mainClassName;
+
+    protected String mainRealmName;
+
+    protected ClassWorld world;
+
+    private int exitCode = 0;
+
+    public Launcher() {
+        this.systemClassLoader = Thread.currentThread().getContextClassLoader();
+    }
+
+    public void setSystemClassLoader(ClassLoader loader) {
+        this.systemClassLoader = loader;
+    }
+
+    public ClassLoader getSystemClassLoader() {
+        return this.systemClassLoader;
+    }
+
+    public int getExitCode() {
+        return exitCode;
+    }
+
+    public void setAppMain(String mainClassName, String mainRealmName) {
+        this.mainClassName = mainClassName;
+        this.mainRealmName = mainRealmName;
+    }
+
+    public String getMainRealmName() {
+        return this.mainRealmName;
+    }
+
+    public String getMainClassName() {
+        return this.mainClassName;
+    }
+
+    public void setWorld(ClassWorld world) {
+        this.world = world;
+    }
+
+    public ClassWorld getWorld() {
+        return this.world;
+    }
+
+    /**
+     * Configure from a file.
+     *
+     * @param is The config input stream.
+     * @throws IOException             If an error occurs reading the config file.
+     * @throws MalformedURLException   If the config file contains invalid URLs.
+     * @throws ConfigurationException  If the config file is corrupt.
+     * @throws DuplicateRealmException If the config file defines two realms
+     *                                 with the same id.
+     * @throws NoSuchRealmException    If the config file defines a main entry
+     *                                 point in a non-existent realm.
+     */
+    public void configure(InputStream is)
+            throws IOException, ConfigurationException, DuplicateRealmException, NoSuchRealmException {
+        Configurator configurator = new Configurator(this);
+        configurator.configure(is);
+    }
+
+    /**
+     * Retrieve the main entry class.
+     *
+     * @return The main entry class.
+     * @throws ClassNotFoundException If the class cannot be found.
+     * @throws NoSuchRealmException   If the specified main entry realm does not exist.
+     */
+    public Class<?> getMainClass() throws ClassNotFoundException, NoSuchRealmException {
+        return getMainRealm().loadClass(getMainClassName());
+    }
+
+    /**
+     * Retrieve the main entry realm.
+     *
+     * @return The main entry realm.
+     * @throws NoSuchRealmException If the specified main entry realm does not exist.
+     */
+    public ClassRealm getMainRealm() throws NoSuchRealmException {
+        return getWorld().getRealm(getMainRealmName());
+    }
+
+    /**
+     * Retrieve the enhanced main entry method.
+     *
+     * @return The enhanced main entry method.
+     * @throws ClassNotFoundException If the main entry class cannot be found.
+     * @throws NoSuchMethodException  If the main entry method cannot be found.
+     * @throws NoSuchRealmException   If the main entry realm cannot be found.
+     */
+    protected Method getEnhancedMainMethod()
+            throws ClassNotFoundException, NoSuchMethodException, NoSuchRealmException {
+        Class<?> cwClass = getMainRealm().loadClass(ClassWorld.class.getName());
+        Method m = getMainClass().getMethod("main", String[].class, cwClass);
+        int modifiers = m.getModifiers();
+        if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) {
+            if (m.getReturnType() == Integer.TYPE || m.getReturnType() == Void.TYPE) {
+                return m;
+            }
+        }
+        throw new NoSuchMethodException("public static void main(String[] args, ClassWorld world)");
+    }
+
+    /**
+     * Retrieve the main entry method.
+     *
+     * @return The main entry method.
+     * @throws ClassNotFoundException If the main entry class cannot be found.
+     * @throws NoSuchMethodException  If the main entry method cannot be found.
+     * @throws NoSuchRealmException   If the main entry realm cannot be found.
+     */
+    protected Method getMainMethod() throws ClassNotFoundException, NoSuchMethodException, NoSuchRealmException {
+        Method m = getMainClass().getMethod("main", String[].class);
+        int modifiers = m.getModifiers();
+        if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) {
+            if (m.getReturnType() == Integer.TYPE || m.getReturnType() == Void.TYPE) {
+                return m;
+            }
+        }
+        throw new NoSuchMethodException("public static void main(String[] args) in " + getMainClass());
+    }
+
+    /**
+     * Launch the application.
+     *
+     * @param args The application args.
+     * @throws ClassNotFoundException    If the main entry class cannot be found.
+     * @throws IllegalAccessException    If the method cannot be accessed.
+     * @throws InvocationTargetException If the target of the invokation is invalid.
+     * @throws NoSuchMethodException     If the main entry method cannot be found.
+     * @throws NoSuchRealmException      If the main entry realm cannot be found.
+     */
+    public void launch(String[] args)
+            throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, NoSuchMethodException,
+                    NoSuchRealmException {
+        try {
+            launchEnhanced(args);
+            return;
+        } catch (NoSuchMethodException e) {
+            // ignore
+        }
+        launchStandard(args);
+    }
+
+    /**
+     * <p>Attempt to launch the application through the enhanced main method.</p>
+     *
+     * <p>This will seek a method with the exact signature of:</p>
+     * <pre>
+     *  public static void main(String[] args, ClassWorld world)
+     *  </pre>
+     *
+     * @param args The application args.
+     * @throws ClassNotFoundException    If the main entry class cannot be found.
+     * @throws IllegalAccessException    If the method cannot be accessed.
+     * @throws InvocationTargetException If the target of the invokation is
+     *                                   invalid.
+     * @throws NoSuchMethodException     If the main entry method cannot be found.
+     * @throws NoSuchRealmException      If the main entry realm cannot be found.
+     */
+    protected void launchEnhanced(String[] args)
+            throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, NoSuchMethodException,
+                    NoSuchRealmException {
+        ClassRealm mainRealm = getMainRealm();
+        Class<?> mainClass = getMainClass();
+        Method mainMethod = getEnhancedMainMethod();
+        ClassLoader cl = mainRealm;
+        // ----------------------------------------------------------------------
+        // This is what the classloader for the main realm looks like when we
+        // boot from the command line:
+        // ----------------------------------------------------------------------
+        // [ AppLauncher$AppClassLoader ] : $CLASSPATH envar
+        //           ^
+        //           |
+        //           |
+        // [ AppLauncher$ExtClassLoader ] : ${java.home}/jre/lib/ext/*.jar
+        //           ^
+        //           |
+        //           |
+        // [ Strategy ]
+        // ----------------------------------------------------------------------
+        Thread.currentThread().setContextClassLoader(cl);
+        Object ret = mainMethod.invoke(mainClass, args, getWorld());
+        if (ret instanceof Integer) {
+            exitCode = (Integer) ret;
+        }
+        Thread.currentThread().setContextClassLoader(systemClassLoader);
+    }
+
+    /**
+     * <p>Attempt to launch the application through the standard main method.</p>
+     *
+     * <p>This will seek a method with the exact signature of:</p>
+     *
+     * <pre>
+     *  public static void main(String[] args)
+     *  </pre>
+     *
+     * @param args The application args.
+     * @throws ClassNotFoundException    If the main entry class cannot be found.
+     * @throws IllegalAccessException    If the method cannot be accessed.
+     * @throws InvocationTargetException If the target of the invokation is
+     *                                   invalid.
+     * @throws NoSuchMethodException     If the main entry method cannot be found.
+     * @throws NoSuchRealmException      If the main entry realm cannot be found.
+     */
+    protected void launchStandard(String[] args)
+            throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, NoSuchMethodException,
+                    NoSuchRealmException {
+        ClassRealm mainRealm = getMainRealm();
+        Class<?> mainClass = getMainClass();
+        Method mainMethod = getMainMethod();
+        Thread.currentThread().setContextClassLoader(mainRealm);
+        Object ret = mainMethod.invoke(mainClass, new Object[] {args});
+        if (ret instanceof Integer) {
+            exitCode = (Integer) ret;
+        }
+        Thread.currentThread().setContextClassLoader(systemClassLoader);
+    }
+
+    // ------------------------------------------------------------
+    //     Class methods
+    // ------------------------------------------------------------
+
+    /**
+     * Launch the launcher from the command line.
+     * Will exit using System.exit with an exit code of 0 for success, 100 if there was an unknown exception,
+     * or some other code for an application error.
+     *
+     * @param args The application command-line arguments.
+     */
+    public static void main(String[] args) {
+        try {
+            int exitCode = mainWithExitCode(args);
+            System.exit(exitCode);
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.exit(100);
+        }
+    }
+
+    /**
+     * Launch the launcher.
+     *
+     * @param args The application command-line arguments.
+     * @return an integer exit code
+     * @throws Exception If an error occurs.
+     */
+    public static int mainWithExitCode(String[] args) throws Exception {
+        String classworldsConf = System.getProperty(CLASSWORLDS_CONF);
+        InputStream is;
+        Launcher launcher = new Launcher();
+        ClassLoader cl = Thread.currentThread().getContextClassLoader();
+        launcher.setSystemClassLoader(cl);
+        if (classworldsConf != null) {
+            is = Files.newInputStream(Paths.get(classworldsConf));
+        } else {
+            if ("true".equals(System.getProperty("classworlds.bootstrapped"))) {
+                is = cl.getResourceAsStream(UBERJAR_CONF_DIR + CLASSWORLDS_CONF);
+            } else {
+                is = cl.getResourceAsStream(CLASSWORLDS_CONF);
+            }
+        }
+        if (is == null) {
+            throw new Exception("classworlds configuration not specified nor found in the classpath");
+        }
+        launcher.configure(is);
+        is.close();
+        try {
+            launcher.launch(args);
+        } catch (InvocationTargetException e) {
+            ClassRealm realm = launcher.getWorld().getRealm(launcher.getMainRealmName());
+            URL[] constituents = realm.getURLs();
+            System.out.println("---------------------------------------------------");
+            for (int i = 0; i < constituents.length; i++) {
+                System.out.println("constituent[" + i + "]: " + constituents[i]);
+            }
+            System.out.println("---------------------------------------------------");
+            // Decode ITE (if we can)
+            Throwable t = e.getTargetException();
+            if (t instanceof Exception) {
+                throw (Exception) t;
+            }
+            if (t instanceof Error) {
+                throw (Error) t;
+            }
+            // Else just toss the ITE
+            throw e;
+        }
+        return launcher.getExitCode();
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/ClassRealm.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/ClassRealm.java
new file mode 100644
index 0000000000..97833a35fc
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/ClassRealm.java
@@ -0,0 +1,450 @@
+/*
+ * 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.codehaus.plexus.classworlds.realm;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.codehaus.plexus.classworlds.strategy.Strategy;
+import org.codehaus.plexus.classworlds.strategy.StrategyFactory;
+
+/**
+ * The class loading gateway. Each class realm has access to a base class loader, imports form zero or more other class
+ * loaders, an optional parent class loader and of course its own class path. When queried for a class/resource, a class
+ * realm will always query its base class loader first before it delegates to a pluggable strategy. The strategy in turn
+ * controls the order in which imported class loaders, the parent class loader and the realm itself are searched. The
+ * base class loader is assumed to be capable of loading of the bootstrap classes.
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ * @author Jason van Zyl
+ */
+public class ClassRealm extends URLClassLoader {
+
+    private final ClassWorld world;
+
+    private final String id;
+
+    private SortedSet<Entry> foreignImports;
+
+    private SortedSet<Entry> parentImports;
+
+    private Strategy strategy;
+
+    private ClassLoader parentClassLoader;
+
+    /**
+     * Creates a new class realm.
+     *
+     * @param world           The class world this realm belongs to, must not be <code>null</code>.
+     * @param id              The identifier for this realm, must not be <code>null</code>.
+     * @param baseClassLoader The base class loader for this realm, may be <code>null</code> to use the bootstrap class
+     *                        loader.
+     */
+    public ClassRealm(ClassWorld world, String id, ClassLoader baseClassLoader) {
+        super(new URL[0], baseClassLoader);
+        this.world = world;
+        this.id = id;
+        foreignImports = new TreeSet<>();
+        strategy = StrategyFactory.getStrategy(this);
+    }
+
+    /**
+     * Creates a new class realm with a specific class loading strategy.
+     *
+     * @param world           The class world this realm belongs to, must not be <code>null</code>.
+     * @param id              The identifier for this realm, must not be <code>null</code>.
+     * @param baseClassLoader The base class loader for this realm, may be <code>null</code> to use the bootstrap class
+     *                        loader.
+     * @param strategy        The class loading strategy, must not be <code>null</code>.
+     */
+    public ClassRealm(ClassWorld world, String id, ClassLoader baseClassLoader, Strategy strategy) {
+        super(new URL[0], baseClassLoader);
+        this.world = world;
+        this.id = id;
+        foreignImports = new TreeSet<>();
+        this.strategy = Objects.requireNonNull(strategy, "strategy cannot be null");
+    }
+
+    public String getId() {
+        return this.id;
+    }
+
+    public ClassWorld getWorld() {
+        return this.world;
+    }
+
+    public void importFromParent(String packageName) {
+        if (parentImports == null) {
+            parentImports = new TreeSet<>();
+        }
+        parentImports.add(new Entry(null, packageName));
+    }
+
+    boolean isImportedFromParent(String name) {
+        if (parentImports != null && !parentImports.isEmpty()) {
+            for (Entry entry : parentImports) {
+                if (entry.matches(name)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        return true;
+    }
+
+    public void importFrom(String realmId, String packageName) throws NoSuchRealmException {
+        importFrom(getWorld().getRealm(realmId), packageName);
+    }
+
+    public void importFrom(ClassLoader classLoader, String packageName) {
+        foreignImports.add(new Entry(classLoader, packageName));
+    }
+
+    public ClassLoader getImportClassLoader(String name) {
+        for (Entry entry : foreignImports) {
+            if (entry.matches(name)) {
+                return entry.getClassLoader();
+            }
+        }
+        return null;
+    }
+
+    public Collection<ClassRealm> getImportRealms() {
+        Collection<ClassRealm> importRealms = new HashSet<>();
+        for (Entry entry : foreignImports) {
+            if (entry.getClassLoader() instanceof ClassRealm) {
+                importRealms.add((ClassRealm) entry.getClassLoader());
+            }
+        }
+        return importRealms;
+    }
+
+    public Strategy getStrategy() {
+        return strategy;
+    }
+
+    /**
+     * Replaces the class loading strategy for this realm.
+     *
+     * @param strategy the new strategy, must not be {@code null}
+     */
+    public void setStrategy(Strategy strategy) {
+        this.strategy = Objects.requireNonNull(strategy, "strategy cannot be null");
+    }
+
+    public void setParentClassLoader(ClassLoader parentClassLoader) {
+        this.parentClassLoader = parentClassLoader;
+    }
+
+    public ClassLoader getParentClassLoader() {
+        return parentClassLoader;
+    }
+
+    public void setParentRealm(ClassRealm realm) {
+        this.parentClassLoader = realm;
+    }
+
+    public ClassRealm getParentRealm() {
+        return (parentClassLoader instanceof ClassRealm) ? (ClassRealm) parentClassLoader : null;
+    }
+
+    // Implementation of the original method signature for backward compatibility
+    public ClassRealm createChildRealm(String id) throws DuplicateRealmException {
+        ClassRealm childRealm = getWorld().newRealm(id, (ClassLoader) null);
+        childRealm.setParentRealm(this);
+        return childRealm;
+    }
+
+    public void addURL(URL url) {
+        String urlStr = url.toExternalForm();
+        if (urlStr.startsWith("jar:") && urlStr.endsWith("!/")) {
+            urlStr = urlStr.substring(4, urlStr.length() - 2);
+            try {
+                url = new URL(urlStr);
+            } catch (MalformedURLException e) {
+                e.printStackTrace();
+            }
+        }
+        super.addURL(url);
+    }
+
+    public void addExports(String moduleName, String packageName) {
+        world.addExports(moduleName, packageName, getUnnamedModule());
+    }
+
+    public void addOpens(String moduleName, String packageName) {
+        world.addOpens(moduleName, packageName, getUnnamedModule());
+    }
+
+    public void addReads(String moduleName) {
+        world.addReads(moduleName, getUnnamedModule());
+    }
+
+    // ----------------------------------------------------------------------
+    // We delegate to the Strategy here so that we can change the behavior
+    // of any existing ClassRealm.
+    // ----------------------------------------------------------------------
+
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+        return loadClass(name, false);
+    }
+
+    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+        return unsynchronizedLoadClass(name, resolve);
+    }
+
+    private Class<?> unsynchronizedLoadClass(String name, boolean resolve) throws ClassNotFoundException {
+        try {
+            // first, try loading bootstrap classes
+            return super.loadClass(name, resolve);
+        } catch (ClassNotFoundException e) {
+            // next, try loading via imports, self and parent as controlled by strategy
+            return strategy.loadClass(name);
+        }
+    }
+
+    // overwrites
+    // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html#findClass(java.lang.String,java.lang.String)
+    // introduced in Java9
+    protected Class<?> findClass(String moduleName, String name) {
+        if (moduleName != null) {
+            return null;
+        }
+        try {
+            return findClassInternal(name);
+        } catch (ClassNotFoundException e) {
+            try {
+                return strategy.getRealm().findClass(name);
+            } catch (ClassNotFoundException nestedException) {
+                return null;
+            }
+        }
+    }
+
+    protected Class<?> findClass(String name) throws ClassNotFoundException {
+        /*
+         * NOTE: This gets only called from ClassLoader.loadClass(Class, boolean) while we try to check for bootstrap
+         * stuff. Don't scan our class path yet, loadClassFromSelf() will do this later when called by the strategy.
+         */
+        throw new ClassNotFoundException(name);
+    }
+
+    protected Class<?> findClassInternal(String name) throws ClassNotFoundException {
+        return super.findClass(name);
+    }
+
+    public URL getResource(String name) {
+        URL resource = super.getResource(name);
+        return resource != null ? resource : strategy.getResource(name);
+    }
+
+    public URL findResource(String name) {
+        return super.findResource(name);
+    }
+
+    public Enumeration<URL> getResources(String name) throws IOException {
+        Collection<URL> resources = new LinkedHashSet<>(Collections.list(super.getResources(name)));
+        resources.addAll(Collections.list(strategy.getResources(name)));
+        return Collections.enumeration(resources);
+    }
+
+    public Enumeration<URL> findResources(String name) throws IOException {
+        return super.findResources(name);
+    }
+
+    // ----------------------------------------------------------------------------
+    // Display methods
+    // ----------------------------------------------------------------------------
+
+    public void display() {
+        display(System.out);
+    }
+
+    public void display(PrintStream out) {
+        out.println("-----------------------------------------------------");
+        for (ClassRealm cr = this; cr != null; cr = (ClassRealm) cr.getParentRealm()) {
+            out.println("realm =    " + cr.getId());
+            out.println("strategy = " + cr.getStrategy().getClass().getName());
+            showUrls(cr, out);
+            out.println();
+        }
+        out.println("-----------------------------------------------------");
+    }
+
+    private static void showUrls(ClassRealm classRealm, PrintStream out) {
+        URL[] urls = classRealm.getURLs();
+        for (int i = 0; i < urls.length; i++) {
+            out.println("urls[" + i + "] = " + urls[i]);
+        }
+        out.println("Number of foreign imports: " + classRealm.foreignImports.size());
+        for (Entry entry : classRealm.foreignImports) {
+            out.println("import: " + entry);
+        }
+        if (classRealm.parentImports != null) {
+            out.println("Number of parent imports: " + classRealm.parentImports.size());
+            for (Entry entry : classRealm.parentImports) {
+                out.println("import: " + entry);
+            }
+        }
+    }
+
+    public String toString() {
+        return "ClassRealm[" + getId() + ", parent: " + getParentClassLoader() + "]";
+    }
+
+    // ---------------------------------------------------------------------------------------------
+    // Search methods that can be ordered by strategies to load a class
+    // ---------------------------------------------------------------------------------------------
+
+    public Class<?> loadClassFromImport(String name) {
+        ClassLoader importClassLoader = getImportClassLoader(name);
+        if (importClassLoader != null) {
+            try {
+                return importClassLoader.loadClass(name);
+            } catch (ClassNotFoundException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    public Class<?> loadClassFromSelf(String name) {
+        synchronized (getClassLoadingLock(name)) {
+            try {
+                Class<?> clazz = findLoadedClass(name);
+                if (clazz == null) {
+                    clazz = findClassInternal(name);
+                }
+                return clazz;
+            } catch (ClassNotFoundException e) {
+                return null;
+            }
+        }
+    }
+
+    public Class<?> loadClassFromParent(String name) {
+        ClassLoader parent = getParentClassLoader();
+        if (parent != null && isImportedFromParent(name)) {
+            try {
+                return parent.loadClass(name);
+            } catch (ClassNotFoundException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    // ---------------------------------------------------------------------------------------------
+    // Search methods that can be ordered by strategies to get a resource
+    // ---------------------------------------------------------------------------------------------
+
+    public URL loadResourceFromImport(String name) {
+        ClassLoader importClassLoader = getImportClassLoader(name);
+        if (importClassLoader != null) {
+            return importClassLoader.getResource(name);
+        }
+        return null;
+    }
+
+    public URL loadResourceFromSelf(String name) {
+        return findResource(name);
+    }
+
+    public URL loadResourceFromParent(String name) {
+        ClassLoader parent = getParentClassLoader();
+        if (parent != null && isImportedFromParent(name)) {
+            return parent.getResource(name);
+        } else {
+            return null;
+        }
+    }
+
+    // ---------------------------------------------------------------------------------------------
+    // Search methods that can be ordered by strategies to get resources
+    // ---------------------------------------------------------------------------------------------
+
+    public Enumeration<URL> loadResourcesFromImport(String name) {
+        ClassLoader importClassLoader = getImportClassLoader(name);
+        if (importClassLoader != null) {
+            try {
+                return importClassLoader.getResources(name);
+            } catch (IOException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    public Enumeration<URL> loadResourcesFromSelf(String name) {
+        try {
+            return findResources(name);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    public Enumeration<URL> loadResourcesFromParent(String name) {
+        ClassLoader parent = getParentClassLoader();
+        if (parent != null && isImportedFromParent(name)) {
+            try {
+                return parent.getResources(name);
+            } catch (IOException e) {
+                // eat it
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void close() throws IOException {
+        super.close();
+    }
+
+    static {
+        registerAsParallelCapable();
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/DuplicateRealmException.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/DuplicateRealmException.java
new file mode 100644
index 0000000000..7e2a34dcfc
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/DuplicateRealmException.java
@@ -0,0 +1,83 @@
+/*
+ * 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.codehaus.plexus.classworlds.realm;
+
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.codehaus.plexus.classworlds.ClassWorldException;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+/**
+ * Indicates an attempt to add a <code>ClassRealm</code> to a
+ * <code>ClassWorld</code> with a duplicate id.
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ */
+public class DuplicateRealmException extends ClassWorldException {
+    // ------------------------------------------------------------
+    //     Instance members
+    // ------------------------------------------------------------
+
+    /**
+     * The realm id.
+     */
+    private String id;
+
+    // ------------------------------------------------------------
+    //     Constructors
+    // ------------------------------------------------------------
+
+    /**
+     * Construct.
+     *
+     * @param world The world.
+     * @param id    The realm id.
+     */
+    public DuplicateRealmException(ClassWorld world, String id) {
+        super(world, "Duplicate realm: " + id);
+        this.id = id;
+    }
+
+    // ------------------------------------------------------------
+    //     Instance methods
+    // ------------------------------------------------------------
+
+    /**
+     * Retrieve the duplicate realm id.
+     *
+     * @return The id.
+     */
+    public String getId() {
+        return this.id;
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/Entry.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/Entry.java
new file mode 100644
index 0000000000..31798eca5c
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/Entry.java
@@ -0,0 +1,199 @@
+/*
+ * 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.codehaus.plexus.classworlds.realm;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+/**
+ * Import description entry.
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ */
+class Entry implements Comparable<Entry> {
+
+    final ClassLoader classLoader;
+
+    final String pkgName;
+
+    Entry(ClassLoader realm, String pkgName) {
+        this.classLoader = realm;
+        this.pkgName = pkgName;
+    }
+
+    // ------------------------------------------------------------
+    //     Instance methods
+    // ------------------------------------------------------------
+
+    /**
+     * Retrieve the class loader.
+     *
+     * @return The class loader.
+     */
+    ClassLoader getClassLoader() {
+        return this.classLoader;
+    }
+
+    /**
+     * Retrieve the package name.
+     *
+     * @return The package name.
+     */
+    String getPackageName() {
+        return this.pkgName;
+    }
+
+    /**
+     * Determine if the class/resource name matches the package
+     * described by this entry.
+     *
+     * @param name The class or resource name to test, must not be <code>null</code>.
+     * @return <code>true</code> if this entry matches the
+     *         classname, otherwise <code>false</code>.
+     */
+    boolean matches(String name) {
+        String pkg = getPackageName();
+        if (pkg.endsWith(".*")) {
+            String pkgName;
+            if (name.indexOf('/') < 0) {
+                // a binary class name, e.g. java.lang.Object
+                int index = name.lastIndexOf('.');
+                pkgName = (index < 0) ? "" : name.substring(0, index);
+            } else {
+                // a resource name, e.g. java/lang/Object.class
+                int index = name.lastIndexOf('/');
+                pkgName = (index < 0) ? "" : name.substring(0, index).replace('/', '.');
+            }
+            return pkgName.length() == pkg.length() - 2 && pkg.regionMatches(0, pkgName, 0, pkgName.length());
+        } else if (pkg.length() > 0) {
+            if (name.indexOf('/') < 0) {
+                // a binary class name, e.g. java.lang.Object
+                if (name.startsWith(pkg)) {
+                    if (name.length() == pkg.length()) {
+                        // exact match of class name
+                        return true;
+                    } else if (name.charAt(pkg.length()) == '.') {
+                        // prefix match of package name
+                        return true;
+                    } else if (name.charAt(pkg.length()) == '$') {
+                        // prefix match of enclosing type
+                        return true;
+                    }
+                }
+            } else {
+                // a resource name, e.g. java/lang/Object.class
+                if (name.equals(pkg)) {
+                    // exact match of resource name
+                    return true;
+                }
+                pkg = pkg.replace('.', '/');
+                if (name.startsWith(pkg) && name.length() > pkg.length()) {
+                    if (name.charAt(pkg.length()) == '/') {
+                        // prefix match of package directory
+                        return true;
+                    } else if (name.charAt(pkg.length()) == '$') {
+                        // prefix match of nested class file
+                        return true;
+                    } else if (name.length() == pkg.length() + 6 && name.endsWith(".class")) {
+                        // exact match of class file
+                        return true;
+                    }
+                }
+            }
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    //     java.lang.Comparable
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    /**
+     * Compare this entry to another for relative ordering.
+     * <p/>
+     * <p/>
+     * The natural ordering of Entry objects is reverse-alphabetical
+     * based upon package name.
+     * </p>
+     *
+     * @param that The object to compare.
+     * @return -1 if this object sorts before that object, 0
+     *         if they are equal, or 1 if this object sorts
+     *         after that object.
+     */
+    public int compareTo(Entry that) {
+        // We are reverse sorting this list, so that
+        // we get longer matches first:
+        //
+        //     com.werken.foo.bar
+        //     com.werken.foo
+        //     com.werken
+        return -(getPackageName().compareTo(that.getPackageName()));
+    }
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    //     java.lang.Object
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    /**
+     * Test this entry for equality to another.
+     * <p/>
+     * <p/>
+     * Consistent with {@link #compareTo}, this method tests
+     * for equality purely on the package name.
+     * </p>
+     *
+     * @param thatObj The object to compare
+     * @return <code>true</code> if the two objects are
+     *         semantically equivalent, otherwise <code>false</code>.
+     */
+    public boolean equals(Object thatObj) {
+        Entry that = (Entry) thatObj;
+        return getPackageName().equals(that.getPackageName());
+    }
+
+    /**
+     * <p/>
+     * Consistent with {@link #equals}, this method creates a hashCode
+     * based on the packagename.
+     * </p>
+     */
+    public int hashCode() {
+        return getPackageName().hashCode();
+    }
+
+    public String toString() {
+        return "Entry[import " + getPackageName() + " from realm " + getClassLoader() + "]";
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/FilteredClassRealm.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/FilteredClassRealm.java
new file mode 100644
index 0000000000..dfb2eefa91
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/FilteredClassRealm.java
@@ -0,0 +1,74 @@
+/*
+ * 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.codehaus.plexus.classworlds.realm;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.function.Predicate;
+
+import org.codehaus.plexus.classworlds.ClassWorld;
+
+/**
+ * Similar to {@link ClassRealm} but only exposing some resources of the underlying URL.
+ * Only supposed to be called from {@link ClassWorld}.
+ */
+public class FilteredClassRealm extends ClassRealm {
+    private final Predicate<String> filter;
+
+    /**
+     * Creates a new class realm.
+     *
+     * @param filter a predicate to apply to each resource name to determine if it should be loaded through this class loader
+     * @param world The class world this realm belongs to, must not be <code>null</code>.
+     * @param id The identifier for this realm, must not be <code>null</code>.
+     * @param baseClassLoader The base class loader for this realm, may be <code>null</code> to use the bootstrap class
+     *            loader.
+     */
+    public FilteredClassRealm(Predicate<String> filter, ClassWorld world, String id, ClassLoader baseClassLoader) {
+        super(world, id, baseClassLoader);
+        this.filter = filter;
+    }
+
+    @Override
+    protected Class<?> findClassInternal(String name) throws ClassNotFoundException {
+        String resourceName = name.replace('.', '/').concat(".class");
+        if (!filter.test(resourceName)) {
+            throw new ClassNotFoundException(name);
+        }
+        return super.findClassInternal(name);
+    }
+
+    @Override
+    public URL findResource(String name) {
+        if (!filter.test(name)) {
+            return null;
+        }
+        return super.findResource(name);
+    }
+
+    @Override
+    public Enumeration<URL> findResources(String name) throws IOException {
+        if (!filter.test(name)) {
+            return Collections.emptyEnumeration();
+        }
+        return super.findResources(name);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/NoSuchRealmException.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/NoSuchRealmException.java
new file mode 100644
index 0000000000..7837d86461
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/NoSuchRealmException.java
@@ -0,0 +1,83 @@
+/*
+ * 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.codehaus.plexus.classworlds.realm;
+
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.codehaus.plexus.classworlds.ClassWorldException;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+/**
+ * Indicates an attempt to retrieve a <code>ClassRealm</code> from a
+ * <code>ClassWorld</code> with an invalid id.
+ *
+ * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
+ */
+public class NoSuchRealmException extends ClassWorldException {
+    // ------------------------------------------------------------
+    //     Instance members
+    // ------------------------------------------------------------
+
+    /**
+     * The realm id.
+     */
+    private String id;
+
+    // ------------------------------------------------------------
+    //     Constructors
+    // ------------------------------------------------------------
+
+    /**
+     * Construct.
+     *
+     * @param world The world.
+     * @param id    The realm id.
+     */
+    public NoSuchRealmException(ClassWorld world, String id) {
+        super(world, "No such realm: " + id);
+        this.id = id;
+    }
+
+    // ------------------------------------------------------------
+    //     Instance methods
+    // ------------------------------------------------------------
+
+    /**
+     * Retrieve the invalid realm id.
+     *
+     * @return The id.
+     */
+    public String getId() {
+        return this.id;
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/AbstractStrategy.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/AbstractStrategy.java
new file mode 100644
index 0000000000..a8f0be1e17
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/AbstractStrategy.java
@@ -0,0 +1,80 @@
+/*
+ * 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.codehaus.plexus.classworlds.strategy;
+
+import java.net.URL;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.LinkedHashSet;
+
+import org.codehaus.plexus.classworlds.UrlUtils;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+/**
+ * @author Jason van Zyl
+ */
+public abstract class AbstractStrategy implements Strategy {
+
+    protected ClassRealm realm;
+
+    public AbstractStrategy(ClassRealm realm) {
+        this.realm = realm;
+    }
+
+    protected String getNormalizedResource(String name) {
+        return UrlUtils.normalizeUrlPath(name);
+    }
+
+    protected Enumeration<URL> combineResources(Enumeration<URL> en1, Enumeration<URL> en2, Enumeration<URL> en3) {
+        Collection<URL> urls = new LinkedHashSet<>();
+        addAll(urls, en1);
+        addAll(urls, en2);
+        addAll(urls, en3);
+        return Collections.enumeration(urls);
+    }
+
+    private void addAll(Collection<URL> target, Enumeration<URL> en) {
+        if (en != null) {
+            while (en.hasMoreElements()) {
+                target.add(en.nextElement());
+            }
+        }
+    }
+
+    public ClassRealm getRealm() {
+        return realm;
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/OsgiBundleStrategy.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/OsgiBundleStrategy.java
new file mode 100644
index 0000000000..41867c7c0f
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/OsgiBundleStrategy.java
@@ -0,0 +1,88 @@
+/*
+ * 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.codehaus.plexus.classworlds.strategy;
+
+/*
+ * Copyright 2001-2010 Codehaus 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.
+ */
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+
+public class OsgiBundleStrategy extends AbstractStrategy {
+
+    // java.* from parent
+    // imported packages [Import-Package header with explicit constraints on the exporter]
+    // requires bundle [Required-Bundle]
+    // self [Bundle-Classpath header]
+    // attached fragments
+    //
+    // We need to trya and be OSGi r4 compliant in the loading of all the bundles so that we can try to
+    // load eclipse without requiring equinox. Or any other OSGi container for that matter.
+    public OsgiBundleStrategy(ClassRealm realm) {
+        super(realm);
+    }
+
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+        Class<?> clazz = realm.loadClassFromImport(name);
+        if (clazz == null) {
+            clazz = realm.loadClassFromSelf(name);
+            if (clazz == null) {
+                clazz = realm.loadClassFromParent(name);
+                if (clazz == null) {
+                    throw new ClassNotFoundException(name);
+                }
+            }
+        }
+        return clazz;
+    }
+
+    public URL getResource(String name) {
+        URL resource = realm.loadResourceFromImport(name);
+        if (resource == null) {
+            resource = realm.loadResourceFromSelf(name);
+            if (resource == null) {
+                resource = realm.loadResourceFromParent(name);
+            }
+        }
+        return resource;
+    }
+
+    public Enumeration<URL> getResources(String name) throws IOException {
+        Enumeration<URL> imports = realm.loadResourcesFromImport(name);
+        Enumeration<URL> self = realm.loadResourcesFromSelf(name);
+        Enumeration<URL> parent = realm.loadResourcesFromParent(name);
+        return combineResources(imports, self, parent);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/ParentFirstStrategy.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/ParentFirstStrategy.java
new file mode 100644
index 0000000000..12243b25a3
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/ParentFirstStrategy.java
@@ -0,0 +1,81 @@
+/*
+ * 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.codehaus.plexus.classworlds.strategy;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+
+/**
+ * @author Jason van Zyl
+ */
+public class ParentFirstStrategy extends AbstractStrategy {
+
+    public ParentFirstStrategy(ClassRealm realm) {
+        super(realm);
+    }
+
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+        Class<?> clazz = realm.loadClassFromImport(name);
+        if (clazz == null) {
+            clazz = realm.loadClassFromParent(name);
+            if (clazz == null) {
+                clazz = realm.loadClassFromSelf(name);
+                if (clazz == null) {
+                    throw new ClassNotFoundException(name);
+                }
+            }
+        }
+        return clazz;
+    }
+
+    public URL getResource(String name) {
+        URL resource = realm.loadResourceFromImport(name);
+        if (resource == null) {
+            resource = realm.loadResourceFromParent(name);
+            if (resource == null) {
+                resource = realm.loadResourceFromSelf(name);
+            }
+        }
+        return resource;
+    }
+
+    public Enumeration<URL> getResources(String name) throws IOException {
+        Enumeration<URL> imports = realm.loadResourcesFromImport(name);
+        Enumeration<URL> parent = realm.loadResourcesFromParent(name);
+        Enumeration<URL> self = realm.loadResourcesFromSelf(name);
+        return combineResources(imports, parent, self);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/SelfFirstStrategy.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/SelfFirstStrategy.java
new file mode 100644
index 0000000000..34116474a6
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/SelfFirstStrategy.java
@@ -0,0 +1,81 @@
+/*
+ * 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.codehaus.plexus.classworlds.strategy;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+
+/**
+ * @author Jason van Zyl
+ */
+public class SelfFirstStrategy extends AbstractStrategy {
+
+    public SelfFirstStrategy(ClassRealm realm) {
+        super(realm);
+    }
+
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+        Class<?> clazz = realm.loadClassFromImport(name);
+        if (clazz == null) {
+            clazz = realm.loadClassFromSelf(name);
+            if (clazz == null) {
+                clazz = realm.loadClassFromParent(name);
+                if (clazz == null) {
+                    throw new ClassNotFoundException(name);
+                }
+            }
+        }
+        return clazz;
+    }
+
+    public URL getResource(String name) {
+        URL resource = realm.loadResourceFromImport(name);
+        if (resource == null) {
+            resource = realm.loadResourceFromSelf(name);
+            if (resource == null) {
+                resource = realm.loadResourceFromParent(name);
+            }
+        }
+        return resource;
+    }
+
+    public Enumeration<URL> getResources(String name) throws IOException {
+        Enumeration<URL> imports = realm.loadResourcesFromImport(name);
+        Enumeration<URL> self = realm.loadResourcesFromSelf(name);
+        Enumeration<URL> parent = realm.loadResourcesFromParent(name);
+        return combineResources(imports, self, parent);
+    }
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/Strategy.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/Strategy.java
new file mode 100644
index 0000000000..ad382a5392
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/Strategy.java
@@ -0,0 +1,56 @@
+/*
+ * 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.codehaus.plexus.classworlds.strategy;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+
+/**
+ * A strategy is a class for defining how classes and resources are located
+ * in classworlds.
+ */
+public interface Strategy {
+
+    Class<?> loadClass(String name) throws ClassNotFoundException;
+
+    URL getResource(String name);
+
+    Enumeration<URL> getResources(String name) throws IOException;
+
+    ClassRealm getRealm();
+}
diff --git a/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/StrategyFactory.java b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/StrategyFactory.java
new file mode 100644
index 0000000000..a9afe599e7
--- /dev/null
+++ b/impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/StrategyFactory.java
@@ -0,0 +1,67 @@
+/*
+ * 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.codehaus.plexus.classworlds.strategy;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+
+/**
+ * StrategyFactory loads a strategy, either default or from a given hint.
+ *
+ * <p>Supported hints:
+ * <ul>
+ *   <li>{@code "self-first"} (default) — realm's own classpath is searched before the parent</li>
+ *   <li>{@code "parent-first"} — parent classloader is searched before the realm's own classpath</li>
+ *   <li>{@code "osgi"} — OSGi bundle loading order (java.*, imports, self, fragments)</li>
+ * </ul>
+ */
+public class StrategyFactory {
+
+    public static Strategy getStrategy(ClassRealm realm) {
+        return getStrategy(realm, "self-first");
+    }
+
+    public static Strategy getStrategy(ClassRealm realm, String hint) {
+        if (hint == null || hint.isEmpty() || "self-first".equals(hint) || "default".equals(hint)) {
+            return new SelfFirstStrategy(realm);
+        } else if ("parent-first".equals(hint)) {
+            return new ParentFirstStrategy(realm);
+        } else if ("osgi".equals(hint)) {
+            return new OsgiBundleStrategy(realm);
+        } else {
+            throw new IllegalArgumentException(
+                    "Unknown strategy hint '" + hint + "': supported hints are 'self-first', 'parent-first', 'osgi'");
+        }
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/AbstractClassWorldsTestCase.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/AbstractClassWorldsTestCase.java
new file mode 100644
index 0000000000..846be0b1a1
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/AbstractClassWorldsTestCase.java
@@ -0,0 +1,61 @@
+/*
+ * 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.codehaus.plexus.classworlds;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.net.URL;
+
+import org.junit.jupiter.api.BeforeAll;
+
+/**
+ * @author Jason van Zyl
+ */
+public abstract class AbstractClassWorldsTestCase {
+
+    /**
+     * Generate test fixture JARs once per test run into {@code target/test-jars/}.
+     * Using JUnit 5 {@code @BeforeAll} with a non-static method requires
+     * {@code @TestInstance(Lifecycle.PER_CLASS)}, but a static method works with default lifecycle.
+     * We keep it static so subclasses need no annotation.
+     */
+    @BeforeAll
+    static void generateTestJars() throws Exception {
+        TestJarGenerator generator = new TestJarGenerator(TestUtil.getTestJarsDir());
+        generator.generate();
+    }
+
+    protected URL getJarUrl(String jarName) {
+        return TestUtil.getTestResourceUrl(jarName);
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/ClassView.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/ClassView.java
new file mode 100644
index 0000000000..cffd6a1a1c
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/ClassView.java
@@ -0,0 +1,130 @@
+/*
+ * 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.codehaus.plexus.classworlds;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+public class ClassView {
+    /**
+     * * Formats Class information for debug output purposes.
+     * *
+     * * @param clz  the Class to print information for
+     * *
+     * * @return a String describing the Class in detail
+     */
+    public static String toString(Class<?> clz) {
+        if (clz.isPrimitive()) {
+            return clz.toString();
+        } else if (clz.isArray()) {
+            return "Array of " + toString(clz.getComponentType());
+        } else if (clz.isInterface()) {
+            return toInterfaceString(clz, "");
+        } else {
+            return toClassString(clz, "");
+        }
+    }
+
+    /**
+     * * Formats Class information for debug output purposes.
+     * *
+     * * @param clz      the Class to print information for
+     * * @param sIndent  the indentation to precede each line of output
+     * *
+     * * @return a String describing the Class in detail
+     */
+    private static String toClassString(Class<?> clz, String sIndent) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(sIndent)
+                .append("Class ")
+                .append(clz.getName())
+                .append("  (")
+                .append(toString(clz.getClassLoader()))
+                .append(')');
+
+        sIndent += "  ";
+
+        Class<?>[] aclz = clz.getInterfaces();
+        for (Class<?> aClass : aclz) {
+            sb.append('\n').append(toInterfaceString(aClass, sIndent));
+        }
+
+        clz = clz.getSuperclass();
+        if (clz != null) {
+            sb.append('\n').append(toClassString(clz, sIndent));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * * Formats interface information for debug output purposes.
+     * *
+     * * @param clz      the interface Class to print information for
+     * * @param sIndent  the indentation to precede each line of output
+     * *
+     * * @return a String describing the interface Class in detail
+     */
+    private static String toInterfaceString(Class<?> clz, String sIndent) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(sIndent)
+                .append("Interface ")
+                .append(clz.getName())
+                .append("  (")
+                .append(toString(clz.getClassLoader()))
+                .append(')');
+
+        Class<?>[] aclz = clz.getInterfaces();
+        for (Class<?> aClass : aclz) {
+            clz = aClass;
+
+            sb.append('\n').append(toInterfaceString(clz, sIndent + "  "));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * * Format a description for the specified ClassLoader object.
+     * *
+     * * @param loader  the ClassLoader instance (or null)
+     * *
+     * * @return a String description of the ClassLoader
+     */
+    private static String toString(ClassLoader loader) {
+        if (loader == null) {
+            return "System ClassLoader";
+        }
+
+        return "ClassLoader class=" + loader.getClass().getName() + ", hashCode=" + loader.hashCode();
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/ClassWorldTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/ClassWorldTest.java
new file mode 100644
index 0000000000..75f6ebd399
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/ClassWorldTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.codehaus.plexus.classworlds;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Enumeration;
+
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
+import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class ClassWorldTest extends AbstractClassWorldsTestCase {
+    private ClassWorld world;
+
+    @BeforeEach
+    public void setUp() {
+        this.world = new ClassWorld();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        this.world = null;
+    }
+
+    @Test
+    void testEmpty() {
+        assertTrue(this.world.getRealms().isEmpty());
+    }
+
+    @Test
+    void testNewRealm() throws Exception {
+        ClassRealm realm = this.world.newRealm("foo");
+
+        assertNotNull(realm);
+    }
+
+    @Test
+    void testGetRealm() throws Exception {
+        ClassRealm realm = this.world.newRealm("foo");
+
+        assertSame(realm, this.world.getRealm("foo"));
+    }
+
+    @Test
+    void testNewRealmDuplicate() {
+        try {
+            this.world.newRealm("foo");
+            this.world.newRealm("foo");
+
+            fail("throw DuplicateRealmException");
+        } catch (DuplicateRealmException e) {
+            // expected and correct
+
+            assertSame(this.world, e.getWorld());
+
+            assertEquals("foo", e.getId());
+        }
+    }
+
+    @Test
+    void testGetRealmNoSuch() {
+        try {
+            this.world.getRealm("foo");
+            fail("throw NoSuchRealmException");
+        } catch (NoSuchRealmException e) {
+            // expected and correct
+
+            assertSame(this.world, e.getWorld());
+
+            assertEquals("foo", e.getId());
+        }
+    }
+
+    @Test
+    void testGetRealms() throws Exception {
+        assertTrue(this.world.getRealms().isEmpty());
+
+        ClassRealm foo = this.world.newRealm("foo");
+
+        assertEquals(1, this.world.getRealms().size());
+
+        assertTrue(this.world.getRealms().contains(foo));
+
+        ClassRealm bar = this.world.newRealm("bar");
+
+        assertEquals(2, this.world.getRealms().size());
+
+        assertTrue(this.world.getRealms().contains(bar));
+    }
+
+    @Test
+    void testPLX334() throws Exception {
+        ClassLoader loader = new URLClassLoader(new URL[] {getJarUrl("component1-1.0.jar")});
+        world.newRealm("netbeans", loader);
+        ClassRealm plexus = world.newRealm("plexus");
+        plexus.importFrom("netbeans", "META-INF/plexus");
+        plexus.importFrom("netbeans", "org.codehaus.plexus");
+        Enumeration<URL> e = plexus.getResources("META-INF/plexus/components.xml");
+        assertNotNull(e);
+        int resourceCount = 0;
+        for (Enumeration<URL> resources = e; resources.hasMoreElements(); ) {
+            URL obj = resources.nextElement();
+            assertTrue(obj.getPath().contains("/component1-1.0.jar!/META-INF/plexus/components.xml"));
+            resourceCount++;
+        }
+        //        assertEquals( 2, resourceCount );
+        // for some reason surefire-plugin 2.3 returned 2 items there:
+        //        for example:
+        // jar:file:/home/mkleint/.m2/repository/org/codehaus/plexus/plexus-archiver/1.0-alpha-7/plexus-archiver-1.0-alpha-7.jar!/META-INF/plexus/components.xml
+        // jar:file:/home/mkleint/src/plexus-trunk/plexus-classworlds/src/test-jars/component1-1.0.jar!/META-INF/plexus/components.xml
+        //    However only 1 is correct, which is actually returned by the 2.4 surefire-plugin
+
+        assertEquals(1, resourceCount);
+        Class<?> c = plexus.loadClass("org.codehaus.plexus.Component1");
+        assertNotNull(c);
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/TestJarGenerator.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/TestJarGenerator.java
new file mode 100644
index 0000000000..f316f446af
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/TestJarGenerator.java
@@ -0,0 +1,554 @@
+/*
+ * 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.codehaus.plexus.classworlds;
+
+import javax.tools.DiagnosticCollector;
+import javax.tools.FileObject;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+/**
+ * Generates test fixture JARs at test time using the JDK's {@code javac} and {@code jar} tools,
+ * eliminating the need to commit pre-compiled binary JARs to version control.
+ *
+ * <p>All JARs are written to {@code target/test-jars/} under the module base directory.
+ * The generator is idempotent: existing JARs are overwritten on each run. Note that
+ * JARs from a previous run whose names are no longer generated are not deleted.
+ *
+ * <p>Called once per test run from {@link AbstractClassWorldsTestCase#generateTestJars()}.
+ */
+public class TestJarGenerator {
+
+    private final Path outputDir;
+    private final JavaCompiler compiler;
+
+    /** Map of compiled class name → bytecode, accumulated across compile() calls. */
+    private final Map<String, byte[]> compiledClasses = new LinkedHashMap<>();
+
+    public TestJarGenerator(Path outputDir) throws IOException {
+        this.outputDir = outputDir;
+        Files.createDirectories(outputDir);
+        this.compiler = ToolProvider.getSystemJavaCompiler();
+        if (compiler == null) {
+            throw new IllegalStateException("No system Java compiler available – tests must run on a JDK, not a JRE.");
+        }
+    }
+
+    // -------------------------------------------------------------------------
+    // Public entry point
+    // -------------------------------------------------------------------------
+
+    /** Generate all test fixture JARs into {@link #outputDir}. */
+    public void generate() throws Exception {
+        generateSimpleJars();
+        generateComponentJars();
+        generateMiscJars();
+    }
+
+    /** Simple single-class and deadlock fixture JARs. */
+    private void generateSimpleJars() throws Exception {
+        // Simple stubs – no external dependencies
+        compileClasses(
+                null,
+                src("a/A.java", "package a; public class A { public static void main(String[] a) {} }"),
+                src("a/Aa.java", "package a; public class Aa { public static int main(String[] a) { return 15; } }"));
+        jar(
+                "a.jar",
+                res("a.properties", "a properties\n"),
+                res("common.properties", "common.properties"),
+                cls("a/A.class"),
+                cls("a/Aa.class"));
+        compiledClasses.clear();
+
+        compileClasses(
+                null,
+                src(
+                        "b/B.java",
+                        "package b;\npublic class B {\n"
+                                + "  public static void main(String[] a, org.codehaus.plexus.classworlds.ClassWorld w) {}\n"
+                                + "}"),
+                src(
+                        "b/Bb.java",
+                        "package b;\npublic class Bb {\n"
+                                + "  public static int main(String[] a, org.codehaus.plexus.classworlds.ClassWorld w) { return 45; }\n"
+                                + "}"));
+        jar(
+                "b.jar",
+                res("b.properties", "name = b.properties"),
+                res("common.properties", "name = common.properties"),
+                cls("b/B.class"),
+                cls("b/Bb.class"));
+        compiledClasses.clear();
+
+        // b_old uses the legacy org.codehaus.classworlds.ClassWorld adapter
+        compileClasses(
+                null,
+                src(
+                        "b/B.java",
+                        "package b;\npublic class B {\n"
+                                + "  public static void main(String[] a, org.codehaus.classworlds.ClassWorld w) {}\n"
+                                + "}"),
+                src(
+                        "b/Bb.java",
+                        "package b;\npublic class Bb {\n"
+                                + "  public static int main(String[] a, org.codehaus.classworlds.ClassWorld w) { return 0; }\n"
+                                + "}"));
+        jar("b_old.jar", cls("b/B.class"), cls("b/Bb.class"));
+        compiledClasses.clear();
+
+        compileClasses(null, src("c/C.java", "package c; public class C {}"));
+        jar("c.jar", cls("c/C.class"));
+        compiledClasses.clear();
+
+        compileClasses(null, src("d/D.java", "package d; public class D {}"));
+        jar("d.jar", cls("d/D.class"));
+        compiledClasses.clear();
+
+        // deadlock: cross-extends to trigger classloader deadlock test
+        compileClasses(
+                null,
+                src("deadlock/B.java", "package deadlock; public class B {}"),
+                src("deadlock/D.java", "package deadlock; public class D {}"),
+                src("deadlock/A.java", "package deadlock; public class A extends B {}"),
+                src("deadlock/C.java", "package deadlock; public class C extends D {}"));
+        jar(
+                "deadlock.jar",
+                cls("deadlock/A.class"),
+                cls("deadlock/B.class"),
+                cls("deadlock/C.class"),
+                cls("deadlock/D.class"));
+        compiledClasses.clear();
+    }
+
+    /** Component0–5 plexus fixture JARs. */
+    private void generateComponentJars() throws Exception {
+        // circular: cross-reference between A and B, A has inner class C
+        compileClasses(
+                null,
+                src(
+                        "A.java",
+                        "public class A {\n"
+                                + "  public static B b = new B();\n"
+                                + "  public static C c = new C();\n"
+                                + "  public B foo(B arg) { return new B(); }\n"
+                                + "  public C foo(C arg) { return new C(); }\n"
+                                + "  public static A create(boolean arg) { return arg ? new A() : new C(); }\n"
+                                + "  public static class C extends A {}\n"
+                                + "}"),
+                src(
+                        "B.java",
+                        "public class B {\n"
+                                + "  public A a;\n"
+                                + "  public A foo(A arg) { return new A(); }\n"
+                                + "}"));
+        jar(
+                "circular-0.1.jar",
+                res("pom.xml", circularPom()),
+                res("src/main/java/org/A.java", circularSourceA()),
+                res("src/main/java/org/B.java", circularSourceB()),
+                res("META-INF/maven/test/circular/pom.xml", circularPom()),
+                res(
+                        "META-INF/maven/test/circular/pom.properties",
+                        "#Generated by Maven\nversion=0.1\ngroupId=test\nartifactId=circular\n"),
+                cls("A.class"),
+                cls("A$C.class"),
+                cls("B.class"));
+        compiledClasses.clear();
+
+        // component0..4: interface + impl pairs in org.codehaus.plexus package
+        for (int i = 0; i <= 4; i++) {
+            String iface = "Component" + i;
+            String impl = "Default" + iface;
+            compileClasses(
+                    null,
+                    src(
+                            "org/codehaus/plexus/" + iface + ".java",
+                            "package org.codehaus.plexus; public interface " + iface + " {}"),
+                    src(
+                            "org/codehaus/plexus/" + impl + ".java",
+                            "package org.codehaus.plexus; public class " + impl + " implements " + iface + " {}"));
+            jar(
+                    "component" + i + "-1.0.jar",
+                    res(
+                            "META-INF/plexus/components.xml",
+                            plexusComponents("org.codehaus.plexus." + iface, "org.codehaus.plexus." + impl)),
+                    cls("org/codehaus/plexus/" + iface + ".class"),
+                    cls("org/codehaus/plexus/" + impl + ".class"));
+            compiledClasses.clear();
+        }
+
+        // component5: two versions with different method signatures (realm isolation test)
+        compileClasses(
+                null,
+                src("test/Component5.java", "package test; public interface Component5 { String printOld(); }"),
+                src(
+                        "test/DefaultComponent5.java",
+                        "package test; public class DefaultComponent5 implements Component5 {"
+                                + " public String printOld() { return \"old\"; } }"));
+        jar(
+                "component5-1.0.jar",
+                res("META-INF/plexus/components.xml", plexusComponents("test.Component5", "test.DefaultComponent5")),
+                cls("test/Component5.class"),
+                cls("test/DefaultComponent5.class"));
+        compiledClasses.clear();
+
+        compileClasses(
+                null,
+                src("test/Component5.java", "package test; public interface Component5 { String printNew(); }"),
+                src(
+                        "test/DefaultComponent5.java",
+                        "package test; public class DefaultComponent5 implements Component5 {"
+                                + " public String printNew() { return \"new\"; } }"));
+        jar(
+                "component5-2.0.jar",
+                res("META-INF/plexus/components.xml", plexusComponents("test.Component5", "test.DefaultComponent5")),
+                cls("test/Component5.class"),
+                cls("test/DefaultComponent5.class"));
+        compiledClasses.clear();
+    }
+
+    /** from-from, nested, and werkflow JARs. */
+    private void generateMiscJars() throws Exception {
+        // from-from: simple main class + META-INF/maven metadata
+        compileClasses(
+                null,
+                src(
+                        "com/from/from/from/Main.java",
+                        "package com.from.from.from; public class Main { public static void main(String[] a) {} }"));
+        jar(
+                "from-from-0.0.1-from-load-import.jar",
+                res("META-INF/maven/com.from/from-from/pom.xml", fromFromPom()),
+                res(
+                        "META-INF/maven/com.from/from-from/pom.properties",
+                        "artifactId=from-from\ngroupId=com.from\nversion=0.0.1-from-load-import"),
+                cls("com/from/from/from/Main.class"));
+        compiledClasses.clear();
+
+        // a.jar must exist before nested.jar (which embeds it)
+        buildNestedJar();
+
+        // werkflow: resource-only jar (no classes)
+        buildWerkflowJar();
+    }
+
+    // -------------------------------------------------------------------------
+    // Compilation helpers
+    // -------------------------------------------------------------------------
+
+    /**
+     * Compile one or more in-memory Java source units.
+     * Results are accumulated into {@link #compiledClasses}.
+     *
+     * @param extraClasspath additional JAR paths to add to the compile classpath (may be null)
+     */
+    private void compileClasses(List<Path> extraClasspath, SourceUnit... sources) throws Exception {
+        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+
+        try (StandardJavaFileManager fm = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) {
+
+            // Build classpath: current test classpath + any extras
+            List<File> classpathFiles = currentClasspath();
+            if (extraClasspath != null) {
+                for (Path p : extraClasspath) {
+                    classpathFiles.add(p.toFile());
+                }
+            }
+            fm.setLocation(StandardLocation.CLASS_PATH, classpathFiles);
+
+            // In-memory output file manager
+            InMemoryFileManager memFm = new InMemoryFileManager(fm, compiledClasses);
+
+            List<JavaFileObject> units = new ArrayList<>();
+            for (SourceUnit su : sources) {
+                units.add(su);
+            }
+
+            JavaCompiler.CompilationTask task = compiler.getTask(null, memFm, diagnostics, null, null, units);
+            boolean ok = task.call();
+            if (!ok) {
+                StringBuilder sb = new StringBuilder("Compilation failed:\n");
+                for (var d : diagnostics.getDiagnostics()) {
+                    sb.append(d).append('\n');
+                }
+                throw new RuntimeException(sb.toString());
+            }
+        }
+    }
+
+    /** Collect the current JVM classpath entries as File objects. */
+    private static List<File> currentClasspath() {
+        List<File> result = new ArrayList<>();
+        String cp = System.getProperty("java.class.path", "");
+        for (String entry : cp.split(File.pathSeparator)) {
+            if (!entry.isEmpty()) {
+                result.add(new File(entry));
+            }
+        }
+        return result;
+    }
+
+    // -------------------------------------------------------------------------
+    // JAR assembly helpers
+    // -------------------------------------------------------------------------
+
+    /**
+     * Assemble a JAR from a mix of {@link ClassEntry} (bytecode from compiledClasses)
+     * and {@link ResourceEntry} (inline text content).
+     */
+    private void jar(String name, JarContent... entries) throws IOException {
+        Path out = outputDir.resolve(name);
+        Manifest mf = new Manifest();
+        mf.getMainAttributes().putValue("Manifest-Version", "1.0");
+
+        try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(out), mf)) {
+            for (JarContent e : entries) {
+                jos.putNextEntry(new JarEntry(e.entryName()));
+                jos.write(e.bytes(compiledClasses));
+                jos.closeEntry();
+            }
+        }
+    }
+
+    /** Build nested.jar: a.properties + lib/a.jar (a.jar embedded). */
+    private void buildNestedJar() throws IOException {
+        Path aJar = outputDir.resolve("a.jar");
+        byte[] aJarBytes = Files.readAllBytes(aJar);
+
+        Path out = outputDir.resolve("nested.jar");
+        Manifest mf = new Manifest();
+        mf.getMainAttributes().putValue("Manifest-Version", "1.0");
+
+        try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(out), mf)) {
+            jos.putNextEntry(new JarEntry("nested.properties"));
+            jos.write("nested.properties\n".getBytes(StandardCharsets.UTF_8));
+            jos.closeEntry();
+
+            jos.putNextEntry(new JarEntry("lib/a.jar"));
+            jos.write(aJarBytes);
+            jos.closeEntry();
+        }
+    }
+
+    /** Build werkflow.jar: resource-only (XML files, no classes). */
+    private void buildWerkflowJar() throws IOException {
+        Path out = outputDir.resolve("resources/werkflow.jar");
+        Files.createDirectories(out.getParent());
+
+        Manifest mf = new Manifest();
+        mf.getMainAttributes().putValue("Manifest-Version", "1.0");
+
+        try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(out), mf)) {
+            addXmlEntry(jos, "org/codehaus/werkflow/personality/basic/basic-syntax.xml", "<syntax/>");
+            addXmlEntry(jos, "org/codehaus/werkflow/personality/bpel4ws/bpel4ws-syntax.xml", "<syntax/>");
+            addXmlEntry(jos, "org/codehaus/werkflow/personality/common/core-idioms.xml", "<idioms/>");
+        }
+    }
+
+    private static void addXmlEntry(JarOutputStream jos, String name, String content) throws IOException {
+        jos.putNextEntry(new JarEntry(name));
+        jos.write(content.getBytes(StandardCharsets.UTF_8));
+        jos.closeEntry();
+    }
+
+    // -------------------------------------------------------------------------
+    // Static resource strings
+    // -------------------------------------------------------------------------
+
+    private static String circularPom() {
+        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                + "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n"
+                + "         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+                + "         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0"
+                + " http://maven.apache.org/maven-v4_0_0.xsd\">\n"
+                + "  <modelVersion>4.0.0</modelVersion>\n"
+                + "  <groupId>test</groupId>\n"
+                + "  <artifactId>circular</artifactId>\n"
+                + "  <version>0.1</version>\n"
+                + "</project>\n";
+    }
+
+    private static String circularSourceA() {
+        return "public class A {\n"
+                + "    public static B b = new B();\n"
+                + "    public static C c = new C();\n"
+                + "    public B foo( B arg ) { return new B(); }\n"
+                + "    public C foo( C arg ) { return new C(); }\n"
+                + "    public static A create( boolean arg) { return arg ? new A() : new C(); }\n"
+                + "    public static class C extends A {}\n"
+                + "}\n";
+    }
+
+    private static String circularSourceB() {
+        return "public class B {\n" + "    public A a;\n" + "    public A foo( A arg ) { return new A(); }\n" + "}\n";
+    }
+
+    private static String plexusComponents(String role, String impl) {
+        return "<component-set>\n"
+                + "  <components>\n"
+                + "    <component>\n"
+                + "      <role>" + role + "</role>\n"
+                + "      <implementation>" + impl + "</implementation>\n"
+                + "    </component>\n"
+                + "  </components>\n"
+                + "</component-set>\n";
+    }
+
+    private static String fromFromPom() {
+        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                + "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n"
+                + "         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+                + "         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0"
+                + " http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n"
+                + "  <modelVersion>4.0.0</modelVersion>\n"
+                + "  <groupId>com.from</groupId>\n"
+                + "  <artifactId>from-from</artifactId>\n"
+                + "  <version>0.0.1-from-load-import</version>\n"
+                + "</project>\n";
+    }
+
+    // -------------------------------------------------------------------------
+    // DSL helpers for jar() entries
+    // -------------------------------------------------------------------------
+
+    interface JarContent {
+        String entryName();
+
+        byte[] bytes(Map<String, byte[]> compiled) throws IOException;
+    }
+
+    /** Bytecode entry — key is the slash-separated class name + ".class". */
+    record ClassEntry(String name) implements JarContent {
+        @Override
+        public String entryName() {
+            return name;
+        }
+
+        @Override
+        public byte[] bytes(Map<String, byte[]> compiled) {
+            // Strip ".class" to get the map key used by InMemoryFileManager
+            String key = name.endsWith(".class") ? name.substring(0, name.length() - 6) : name;
+            byte[] b = compiled.get(key);
+            if (b == null) {
+                throw new RuntimeException(
+                        "No compiled bytecode for: " + key + " (available: " + compiled.keySet() + ")");
+            }
+            return b;
+        }
+    }
+
+    /** Inline text resource entry. */
+    record ResourceEntry(String name, String content) implements JarContent {
+        @Override
+        public String entryName() {
+            return name;
+        }
+
+        @Override
+        public byte[] bytes(Map<String, byte[]> compiled) {
+            return content.getBytes(StandardCharsets.UTF_8);
+        }
+    }
+
+    private static ClassEntry cls(String name) {
+        return new ClassEntry(name);
+    }
+
+    private static ResourceEntry res(String name, String content) {
+        return new ResourceEntry(name, content);
+    }
+
+    // -------------------------------------------------------------------------
+    // In-memory source unit
+    // -------------------------------------------------------------------------
+
+    static class SourceUnit extends SimpleJavaFileObject {
+        private final String src;
+
+        SourceUnit(String className, String src) {
+            super(URI.create("mem:///" + className.replace('.', '/') + ".java"), Kind.SOURCE);
+            this.src = src;
+        }
+
+        @Override
+        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+            return src;
+        }
+    }
+
+    private static SourceUnit src(String path, String src) {
+        // path like "a/A.java" → class name "a.A"
+        String className = path.replace('/', '.').replace(".java", "");
+        return new SourceUnit(className, src);
+    }
+
+    // -------------------------------------------------------------------------
+    // In-memory output file manager
+    // -------------------------------------------------------------------------
+
+    /**
+     * Routes compiled {@code .class} output into a {@code Map<String, byte[]>}
+     * instead of writing to disk.
+     */
+    static class InMemoryFileManager extends javax.tools.ForwardingJavaFileManager<StandardJavaFileManager> {
+        private final Map<String, byte[]> classBytes;
+
+        InMemoryFileManager(StandardJavaFileManager delegate, Map<String, byte[]> classBytes) {
+            super(delegate);
+            this.classBytes = classBytes;
+        }
+
+        @Override
+        public JavaFileObject getJavaFileForOutput(
+                Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
+            return new SimpleJavaFileObject(URI.create("mem:///" + className.replace('.', '/') + ".class"), kind) {
+                @Override
+                public OutputStream openOutputStream() {
+                    return new ByteArrayOutputStream() {
+                        @Override
+                        public void close() throws IOException {
+                            super.close();
+                            // key uses '/' separator (matches JarEntry names without ".class")
+                            classBytes.put(className.replace('.', '/'), toByteArray());
+                        }
+                    };
+                }
+            };
+        }
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/TestUtil.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/TestUtil.java
new file mode 100644
index 0000000000..f5423ec1ce
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/TestUtil.java
@@ -0,0 +1,79 @@
+/*
+ * 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.codehaus.plexus.classworlds;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * @author <a href="bwalding@jakarta.org">Ben Walding</a>
+ */
+public class TestUtil {
+
+    /**
+     * Returns the URL for a test fixture JAR (or resource file) by name.
+     *
+     * <p>JARs are looked up in {@code target/test-jars/} where {@link TestJarGenerator} writes them.
+     * The {@code resources/} sub-directory (e.g. {@code werkflow.jar}) lives under
+     * {@code target/test-jars/resources/}.
+     */
+    public static URL getTestResourceUrl(String resourceName) {
+        File resourceFile = getTestJarsDir().resolve(resourceName).toFile();
+        try {
+            return resourceFile.toURI().toURL();
+        } catch (MalformedURLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** Directory where generated test JARs are written ({@code <basedir>/target/test-jars/}). */
+    public static Path getTestJarsDir() {
+        return Paths.get(getBasedir(), "target", "test-jars");
+    }
+
+    public static String getBasedir() {
+        String basedir = System.getProperty("basedir");
+
+        /* do our best if we are not running from surefire */
+        if (basedir == null || basedir.length() <= 0) {
+            basedir = (new File("")).getAbsolutePath();
+        }
+
+        return basedir;
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/UrlUtilsTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/UrlUtilsTest.java
new file mode 100644
index 0000000000..cdecf0a44b
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/UrlUtilsTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.codehaus.plexus.classworlds;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class UrlUtilsTest {
+
+    @Test
+    public void testNormalizeUrlPath() {
+        assertEquals("org/codehaus/Test.class", UrlUtils.normalizeUrlPath("org/codehaus/Test.class"));
+        assertEquals("org/Test.class", UrlUtils.normalizeUrlPath("org/codehaus/../Test.class"));
+        assertEquals("../../some.jar/org/Test.class", UrlUtils.normalizeUrlPath("../../some.jar/org/Test.class"));
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/ConfigurationParserTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/ConfigurationParserTest.java
new file mode 100644
index 0000000000..838fc55811
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/ConfigurationParserTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.codehaus.plexus.classworlds.launcher;
+
+import org.codehaus.plexus.classworlds.AbstractClassWorldsTestCase;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class ConfigurationParserTest extends AbstractClassWorldsTestCase {
+
+    ConfigurationParser configurator = new ConfigurationParser(null, System.getProperties());
+
+    @Test
+    void testFilterUnterminated() {
+        try {
+            this.configurator.filter("${cheese");
+            fail("throw ConfigurationException");
+        } catch (ConfigurationException e) {
+            // expected and correct
+            assertTrue(e.getMessage().startsWith("Unterminated"));
+        }
+    }
+
+    @Test
+    void testFilterSolitary() throws Exception {
+        System.setProperty("classworlds.test.prop", "test prop value");
+
+        String result = this.configurator.filter("${classworlds.test.prop}");
+
+        assertEquals("test prop value", result);
+    }
+
+    @Test
+    void testFilterAtStart() throws Exception {
+        System.setProperty("classworlds.test.prop", "test prop value");
+
+        String result = this.configurator.filter("${classworlds.test.prop}cheese");
+
+        assertEquals("test prop valuecheese", result);
+    }
+
+    @Test
+    void testFilterAtEnd() throws Exception {
+        System.setProperty("classworlds.test.prop", "test prop value");
+
+        String result = this.configurator.filter("cheese${classworlds.test.prop}");
+
+        assertEquals("cheesetest prop value", result);
+    }
+
+    @Test
+    void testFilterMultiple() throws Exception {
+        System.setProperty("classworlds.test.prop.one", "test prop value one");
+
+        System.setProperty("classworlds.test.prop.two", "test prop value two");
+
+        String result =
+                this.configurator.filter("I like ${classworlds.test.prop.one} and ${classworlds.test.prop.two} a lot");
+
+        assertEquals("I like test prop value one and test prop value two a lot", result);
+    }
+
+    @Test
+    void testFilterNonExistent() {
+        try {
+            this.configurator.filter("${gollygeewillikers}");
+            fail("throw ConfigurationException");
+        } catch (ConfigurationException e) {
+            // expected and correct
+            assertTrue(e.getMessage().startsWith("No such property"));
+        }
+    }
+
+    @Test
+    void testFilterInMiddle() throws Exception {
+        System.setProperty("classworlds.test.prop", "test prop value");
+
+        String result = this.configurator.filter("cheese${classworlds.test.prop}toast");
+
+        assertEquals("cheesetest prop valuetoast", result);
+    }
+
+    @Test
+    void testFilterDefaultValueWhenAbsent() throws Exception {
+        // Property does not exist — should use the default after :-
+        System.clearProperty("classworlds.test.missing");
+
+        String result = this.configurator.filter("${classworlds.test.missing:-fallback}");
+
+        assertEquals("fallback", result);
+    }
+
+    @Test
+    void testFilterDefaultValueNotUsedWhenPresent() throws Exception {
+        System.setProperty("classworlds.test.present", "actual");
+
+        String result = this.configurator.filter("${classworlds.test.present:-fallback}");
+
+        assertEquals("actual", result);
+    }
+
+    @Test
+    void testFilterAlternateValueWhenPresent() throws Exception {
+        System.setProperty("classworlds.test.present", "actual");
+
+        String result = this.configurator.filter("${classworlds.test.present:+alternate}");
+
+        assertEquals("alternate", result);
+    }
+
+    @Test
+    void testFilterAlternateValueNotUsedWhenAbsent() {
+        System.clearProperty("classworlds.test.missing");
+
+        // ${missing:+alternate}: property absent → alternate is NOT used → treated as unresolved → exception
+        try {
+            this.configurator.filter("${classworlds.test.missing:+alternate}");
+            fail("throw ConfigurationException");
+        } catch (ConfigurationException e) {
+            assertTrue(e.getMessage().startsWith("No such property"));
+        }
+    }
+
+    @Test
+    void testFilterDefaultValueWithSurroundingText() throws Exception {
+        System.clearProperty("classworlds.test.missing");
+
+        String result = this.configurator.filter("prefix/${classworlds.test.missing:-default}/suffix");
+
+        assertEquals("prefix/default/suffix", result);
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/ConfiguratorTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/ConfiguratorTest.java
new file mode 100644
index 0000000000..d8a3f73937
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/ConfiguratorTest.java
@@ -0,0 +1,360 @@
+/*
+ * 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.codehaus.plexus.classworlds.launcher;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.net.URL;
+import java.util.Collection;
+
+import org.codehaus.plexus.classworlds.AbstractClassWorldsTestCase;
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.codehaus.plexus.classworlds.TestUtil;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class ConfiguratorTest extends AbstractClassWorldsTestCase {
+    private Launcher launcher;
+    private Configurator configurator;
+
+    @BeforeEach
+    public void setUp() {
+        this.launcher = new Launcher();
+        this.configurator = new Configurator(this.launcher);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        this.launcher = null;
+        this.configurator = null;
+        System.getProperties().remove("set.using.existent");
+        System.getProperties().remove("set.using.default");
+        System.getProperties().remove("set.using.nonexistent");
+        System.getProperties().remove("set.using.nonexistent.default");
+        System.getProperties().remove("set.using.missing");
+        System.getProperties().remove("set.using.filtered.default");
+    }
+
+    @Test
+    void testConfigureNonexistent() throws Exception {
+        try {
+            this.configurator.configure(getConfigPath("notfound.conf"));
+            fail("throw FileNotFoundException");
+        } catch (FileNotFoundException e) {
+            // expected and correct
+        }
+    }
+
+    @Test
+    void testConfigureDuplicateMain() throws Exception {
+        try {
+            this.configurator.configure(getConfigPath("dupe-main.conf"));
+            fail("throw ConfigurationException");
+        } catch (ConfigurationException e) {
+            // expected and correct
+            assertTrue(e.getMessage().startsWith("Duplicate main"));
+        }
+    }
+
+    @Test
+    void testConfigureDuplicateRealm() throws Exception {
+        try {
+            this.configurator.configure(getConfigPath("dupe-realm.conf"));
+            fail("throw DuplicateRealmException");
+        } catch (DuplicateRealmException e) {
+            // expected and correct
+            assertEquals("dupe.realm", e.getId());
+        }
+    }
+
+    @Test
+    void testConfigureEarlyImport() throws Exception {
+        try {
+            this.configurator.configure(getConfigPath("early-import.conf"));
+            fail("throw ConfigurationException");
+        } catch (ConfigurationException e) {
+            // expected and correct
+            assertTrue(e.getMessage().startsWith("Unhandled import"));
+        }
+    }
+
+    @Test
+    void testConfigureRealmSyntax() throws Exception {
+        try {
+            this.configurator.configure(getConfigPath("realm-syntax.conf"));
+            fail("throw ConfigurationException");
+        } catch (ConfigurationException e) {
+            // expected and correct
+            assertTrue(e.getMessage().startsWith("Invalid realm"));
+        }
+    }
+
+    @Test
+    void testConfigureValid() throws Exception {
+        this.configurator.configure(getConfigPath("valid.conf"));
+
+        assertEquals("org.apache.maven.app.App", this.launcher.getMainClassName());
+
+        assertEquals("maven", this.launcher.getMainRealmName());
+
+        ClassWorld world = this.launcher.getWorld();
+
+        Collection<ClassRealm> realms = world.getRealms();
+
+        assertEquals(4, realms.size());
+
+        assertNotNull(world.getRealm("ant"));
+        assertNotNull(world.getRealm("maven"));
+        assertNotNull(world.getRealm("xml"));
+
+        ClassRealm antRealm = world.getRealm("ant");
+        ClassRealm mavenRealm = world.getRealm("maven");
+        ClassRealm xmlRealm = world.getRealm("xml");
+        ClassRealm globRealm = world.getRealm("glob");
+
+        assertSame(null, antRealm.getImportClassLoader("org.apache.tools.Ant"));
+
+        // Ant has dependency to xerces:xercesImpl (test)
+        assertSame(null, antRealm.getImportClassLoader("org.xml.sax.SAXException"));
+
+        assertSame(xmlRealm, antRealm.getImportClassLoader("jakarta.xml.bind.JAXBException"));
+
+        assertSame(null, mavenRealm.getImportClassLoader("org.apache.maven.app.App"));
+
+        assertSame(xmlRealm, mavenRealm.getImportClassLoader("jakarta.xml.bind.JAXBException"));
+
+        URL[] urls = globRealm.getURLs();
+
+        assertArrayContains(urls, TestUtil.getTestResourceUrl("nested.jar"));
+        assertArrayContains(urls, TestUtil.getTestResourceUrl("a.jar"));
+        assertArrayContains(urls, TestUtil.getTestResourceUrl("b.jar"));
+        assertArrayContains(urls, TestUtil.getTestResourceUrl("c.jar"));
+    }
+
+    @Test
+    void testConfigureOptionallyNonExistent() throws Exception {
+        this.configurator.configure(getConfigPath("optionally-nonexistent.conf"));
+
+        assertEquals("org.apache.maven.app.App", this.launcher.getMainClassName());
+
+        assertEquals("opt", this.launcher.getMainRealmName());
+
+        ClassWorld world = this.launcher.getWorld();
+
+        Collection<ClassRealm> realms = world.getRealms();
+
+        assertEquals(1, realms.size());
+
+        assertNotNull(world.getRealm("opt"));
+
+        ClassRealm optRealm = world.getRealm("opt");
+
+        URL[] urls = optRealm.getURLs();
+
+        assertEquals(0, urls.length, "no urls");
+    }
+
+    @Test
+    void testConfigureOptionallyExistent() throws Exception {
+        this.configurator.configure(getConfigPath("optionally-existent.conf"));
+
+        assertEquals("org.apache.maven.app.App", this.launcher.getMainClassName());
+
+        assertEquals("opt", this.launcher.getMainRealmName());
+
+        ClassWorld world = this.launcher.getWorld();
+
+        Collection<ClassRealm> realms = world.getRealms();
+
+        assertEquals(1, realms.size());
+
+        assertNotNull(world.getRealm("opt"));
+
+        ClassRealm optRealm = world.getRealm("opt");
+
+        URL[] urls = optRealm.getURLs();
+
+        assertEquals(1, urls.length, "one url");
+
+        assertSame(null, optRealm.getImportClassLoader("jakarta.xml.bind.JAXBException"));
+    }
+
+    @Test
+    void testConfigureUnhandled() throws Exception {
+        try {
+            this.configurator.configure(getConfigPath("unhandled.conf"));
+            fail("throw ConfigurationException");
+        } catch (ConfigurationException e) {
+            // expected and correct
+            assertTrue(e.getMessage().startsWith("Unhandled configuration"));
+        }
+    }
+
+    @Test
+    void testSetUsingExistent() throws Exception {
+        assertNull(System.getProperty("set.using.existent"));
+
+        this.configurator.configure(getConfigPath("set-using-existent.conf"));
+
+        assertEquals("testSetUsingExistent", System.getProperty("set.using.existent"));
+    }
+
+    @Test
+    void testSetUsingNonExistent() throws Exception {
+        assertNull(System.getProperty("set.using.nonexistent"));
+
+        this.configurator.configure(getConfigPath("set-using-nonexistent.conf"));
+
+        assertNull(System.getProperty("set.using.nonexistent"));
+    }
+
+    @Test
+    void testSetUsingNonExistentDefault() throws Exception {
+        assertNull(System.getProperty("set.using.nonexistent.default"));
+
+        this.configurator.configure(getConfigPath("set-using-nonexistent.conf"));
+
+        assertEquals("testSetUsingNonExistentDefault", System.getProperty("set.using.nonexistent.default"));
+    }
+
+    @Test
+    void testSetUsingNonExistentOverride() throws Exception {
+        assertNull(System.getProperty("set.using.default"));
+        System.setProperty("set.using.default", "testSetUsingNonExistentOverride");
+
+        this.configurator.configure(getConfigPath("set-using-nonexistent.conf"));
+
+        assertEquals("testSetUsingNonExistentOverride", System.getProperty("set.using.default"));
+    }
+
+    @Test
+    void testSetUsingExistentOverride() throws Exception {
+        assertNull(System.getProperty("set.using.existent"));
+        System.setProperty("set.using.existent", "testSetUsingExistentOverride");
+
+        this.configurator.configure(getConfigPath("set-using-existent.conf"));
+
+        assertEquals("testSetUsingExistentOverride", System.getProperty("set.using.existent"));
+    }
+
+    @Test
+    void testSetUsingExistentDefault() throws Exception {
+        assertNull(System.getProperty("set.using.default"));
+
+        this.configurator.configure(getConfigPath("set-using-existent.conf"));
+
+        assertEquals("testSetUsingExistentDefault", System.getProperty("set.using.default"));
+    }
+
+    @Test
+    void testSetUsingMissingDefault() throws Exception {
+        assertNull(System.getProperty("set.using.missing"));
+
+        this.configurator.configure(getConfigPath("set-using-missing.conf"));
+
+        assertEquals("testSetUsingMissingDefault", System.getProperty("set.using.missing"));
+    }
+
+    @Test
+    void testSetUsingMissingOverride() throws Exception {
+        assertNull(System.getProperty("set.using.missing"));
+        System.setProperty("set.using.missing", "testSetUsingMissingOverride");
+
+        this.configurator.configure(getConfigPath("set-using-missing.conf"));
+
+        assertEquals("testSetUsingMissingOverride", System.getProperty("set.using.missing"));
+    }
+
+    @Test
+    void testSetUsingFilteredDefault() throws Exception {
+        assertNull(System.getProperty("set.using.filtered.default"));
+
+        this.configurator.configure(getConfigPath("set-using-missing.conf"));
+
+        assertEquals(System.getProperty("user.home") + "/m2", System.getProperty("set.using.filtered.default"));
+    }
+
+    @Test
+    void testFromFromFrom() throws Exception {
+        this.configurator.configure(getConfigPath("valid-from-from-from.conf"));
+
+        assertEquals("com.from.from.from.Main", this.launcher.getMainClassName());
+
+        assertEquals("from", this.launcher.getMainRealmName());
+
+        ClassWorld world = this.launcher.getWorld();
+
+        ClassRealm antRealm = world.getRealm("ant");
+        Collection<ClassRealm> antImportRealms = antRealm.getImportRealms();
+        assertEquals(1, antImportRealms.size());
+        assertEquals("from", antImportRealms.stream().findFirst().get().getId());
+
+        ClassRealm mavenRealm = world.getRealm("maven");
+        Collection<ClassRealm> mavenImportRealms = mavenRealm.getImportRealms();
+        assertEquals(1, mavenImportRealms.size());
+        assertEquals("from", mavenImportRealms.stream().findFirst().get().getId());
+
+        ClassRealm globRealm = world.getRealm("glob");
+        Collection<ClassRealm> globImportRealms = globRealm.getImportRealms();
+        assertEquals(0, globImportRealms.size());
+
+        ClassRealm fromRealm = world.getRealm("from");
+        Collection<ClassRealm> fromImportRealms = fromRealm.getImportRealms();
+        assertEquals(0, fromImportRealms.size());
+    }
+
+    private FileInputStream getConfigPath(String name) throws Exception {
+        return new FileInputStream(new File(new File(TestUtil.getBasedir(), "src/test/test-data"), name));
+    }
+
+    private void assertArrayContains(URL[] array, URL url) {
+        for (URL value : array) {
+            if (url.equals(value)) {
+                return;
+            }
+        }
+        fail("URL (" + url + ") not found in array of URLs");
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/LauncherTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/LauncherTest.java
new file mode 100644
index 0000000000..39ca08cb73
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/LauncherTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.codehaus.plexus.classworlds.launcher;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+import java.io.File;
+import java.io.FileInputStream;
+
+import org.codehaus.plexus.classworlds.AbstractClassWorldsTestCase;
+import org.codehaus.plexus.classworlds.TestUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class LauncherTest extends AbstractClassWorldsTestCase {
+    private Launcher launcher;
+
+    @BeforeEach
+    public void setUp() {
+        this.launcher = new Launcher();
+
+        this.launcher.setSystemClassLoader(Thread.currentThread().getContextClassLoader());
+    }
+
+    @AfterEach
+    public void tearDown() {
+        this.launcher = null;
+    }
+
+    @Test
+    void testConfigureValid() throws Exception {
+        launcher.configure(getConfigPath("valid-launch.conf"));
+
+        Class<?> mainClass = launcher.getMainClass();
+
+        assertNotNull(mainClass);
+
+        assertEquals("a.A", mainClass.getName());
+
+        assertEquals("app", launcher.getMainRealm().getId());
+    }
+
+    @Test
+    void testLaunchValidStandard() throws Exception {
+        launcher.configure(getConfigPath("valid-launch.conf"));
+
+        launcher.launch(new String[] {});
+    }
+
+    @Test
+    void testLaunchValidStandardExitCode() throws Exception {
+        launcher.configure(getConfigPath("valid-launch-exitCode.conf"));
+
+        launcher.launch(new String[] {});
+
+        assertEquals(15, launcher.getExitCode(), "check exit code");
+    }
+
+    @Test
+    void testLaunchValidEnhanced() throws Exception {
+        launcher.configure(getConfigPath("valid-enh-launch.conf"));
+
+        launcher.launch(new String[] {});
+    }
+
+    @Test
+    void testLaunchValidEnhancedExitCode() throws Exception {
+        launcher.configure(getConfigPath("valid-enh-launch-exitCode.conf"));
+
+        launcher.launch(new String[] {});
+
+        assertEquals(45, launcher.getExitCode(), "check exit code");
+    }
+
+    @Test
+    void testLaunchNoSuchMethod() throws Exception {
+        launcher.configure(getConfigPath("launch-nomethod.conf"));
+
+        try {
+            launcher.launch(new String[] {});
+            fail("should have thrown NoSuchMethodException");
+        } catch (NoSuchMethodException e) {
+            // expected and correct
+        }
+    }
+
+    @Test
+    void testLaunchClassNotFound() throws Exception {
+        launcher.configure(getConfigPath("launch-noclass.conf"));
+
+        try {
+            launcher.launch(new String[] {});
+            fail("throw ClassNotFoundException");
+        } catch (ClassNotFoundException e) {
+            // expected and correct
+        }
+    }
+
+    private FileInputStream getConfigPath(String name) throws Exception {
+        String basedir = TestUtil.getBasedir();
+
+        return new FileInputStream(new File(new File(basedir, "src/test/test-data"), name));
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/ClassRealmImplTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/ClassRealmImplTest.java
new file mode 100644
index 0000000000..fb2dd15063
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/ClassRealmImplTest.java
@@ -0,0 +1,373 @@
+/*
+ * 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.codehaus.plexus.classworlds.realm;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.codehaus.plexus.classworlds.AbstractClassWorldsTestCase;
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class ClassRealmImplTest extends AbstractClassWorldsTestCase {
+    private ClassWorld world;
+
+    @BeforeEach
+    public void setUp() {
+        this.world = new ClassWorld();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        this.world = null;
+    }
+
+    @Test
+    void testNewRealm() throws Exception {
+        ClassRealm realm = this.world.newRealm("foo");
+        assertNotNull(realm);
+        assertSame(this.world, realm.getWorld());
+        assertEquals("foo", realm.getId());
+    }
+
+    @Test
+    void testLocateSourceRealmNoImports() {
+        ClassRealm realm = new ClassRealm(this.world, "foo", null);
+        assertSame(null, realm.getImportClassLoader("com.werken.Stuff"));
+    }
+
+    @Test
+    void testLocateSourceRealmSimpleImport() throws Exception {
+        ClassRealm mainRealm = this.world.newRealm("main");
+        ClassRealm werkflowRealm = this.world.newRealm("werkflow");
+        mainRealm.importFrom("werkflow", "com.werken.werkflow");
+
+        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com.werken.werkflow.WerkflowEngine"));
+        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com/werken/werkflow/some.properties"));
+        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com.werken.werkflow.process.ProcessManager"));
+        assertSame(null, mainRealm.getImportClassLoader("com.werken.blissed.Process"));
+        assertSame(null, mainRealm.getImportClassLoader("java.lang.Object"));
+        assertSame(null, mainRealm.getImportClassLoader("NoviceProgrammerClass"));
+    }
+
+    @Test
+    void testLocateSourceRealmMultipleImport() throws Exception {
+        ClassRealm mainRealm = this.world.newRealm("main");
+        ClassRealm werkflowRealm = this.world.newRealm("werkflow");
+        ClassRealm blissedRealm = this.world.newRealm("blissed");
+        mainRealm.importFrom("werkflow", "com.werken.werkflow");
+        mainRealm.importFrom("blissed", "com.werken.blissed");
+
+        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com.werken.werkflow.WerkflowEngine"));
+        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com.werken.werkflow.process.ProcessManager"));
+        assertSame(blissedRealm, mainRealm.getImportClassLoader("com.werken.blissed.Process"));
+        assertSame(blissedRealm, mainRealm.getImportClassLoader("com.werken.blissed.guard.BooleanGuard"));
+        assertSame(null, mainRealm.getImportClassLoader("java.lang.Object"));
+        assertSame(null, mainRealm.getImportClassLoader("NoviceProgrammerClass"));
+    }
+
+    @Test
+    void testLocateSourceRealmHierachy() throws Exception {
+        ClassRealm mainRealm = this.world.newRealm("main");
+        ClassRealm fooRealm = this.world.newRealm("foo");
+        ClassRealm fooBarRealm = this.world.newRealm("fooBar");
+        ClassRealm fooBarBazRealm = this.world.newRealm("fooBarBaz");
+        mainRealm.importFrom("foo", "foo");
+        mainRealm.importFrom("fooBar", "foo.bar");
+        mainRealm.importFrom("fooBarBaz", "foo.bar.baz");
+
+        assertSame(fooRealm, mainRealm.getImportClassLoader("foo.Goober"));
+        assertSame(fooRealm, mainRealm.getImportClassLoader("foo.cheese.Goober"));
+        assertSame(fooBarRealm, mainRealm.getImportClassLoader("foo.bar.Goober"));
+        assertSame(fooBarRealm, mainRealm.getImportClassLoader("foo.bar.cheese.Goober"));
+        assertSame(fooBarBazRealm, mainRealm.getImportClassLoader("foo.bar.baz.Goober"));
+        assertSame(fooBarBazRealm, mainRealm.getImportClassLoader("foo.bar.baz.cheese.Goober"));
+        assertSame(null, mainRealm.getImportClassLoader("java.lang.Object"));
+        assertSame(null, mainRealm.getImportClassLoader("NoviceProgrammerClass"));
+    }
+
+    @Test
+    void testLocateSourceRealmHierachyReverse() throws Exception {
+        ClassRealm fooBarBazRealm = this.world.newRealm("fooBarBaz");
+        ClassRealm fooBarRealm = this.world.newRealm("fooBar");
+        ClassRealm fooRealm = this.world.newRealm("foo");
+        ClassRealm mainRealm = this.world.newRealm("main");
+        mainRealm.importFrom("fooBarBaz", "foo.bar.baz");
+        mainRealm.importFrom("fooBar", "foo.bar");
+        mainRealm.importFrom("foo", "foo");
+
+        assertSame(fooRealm, mainRealm.getImportClassLoader("foo.Goober"));
+        assertSame(fooRealm, mainRealm.getImportClassLoader("foo.cheese.Goober"));
+        assertSame(fooBarRealm, mainRealm.getImportClassLoader("foo.bar.Goober"));
+        assertSame(fooBarRealm, mainRealm.getImportClassLoader("foo.bar.cheese.Goober"));
+        assertSame(fooBarBazRealm, mainRealm.getImportClassLoader("foo.bar.baz.Goober"));
+        assertSame(fooBarBazRealm, mainRealm.getImportClassLoader("foo.bar.baz.cheese.Goober"));
+        assertSame(null, mainRealm.getImportClassLoader("java.lang.Object"));
+        assertSame(null, mainRealm.getImportClassLoader("NoviceProgrammerClass"));
+    }
+
+    @Test
+    void testLoadClassSystemClass() throws Exception {
+        ClassRealm mainRealm = this.world.newRealm("main");
+        Class<?> cls = mainRealm.loadClass("java.lang.Object");
+        assertNotNull(cls);
+    }
+
+    @Test
+    void testLoadClassNonSystemClass() throws Exception {
+        ClassRealm mainRealm = this.world.newRealm("main");
+        try {
+            Class<?> c = mainRealm.loadClass("com.werken.projectz.UberThing");
+            System.out.println("c = " + c);
+            fail("A ClassNotFoundException should be thrown!");
+        } catch (ClassNotFoundException e) {
+            // expected and correct
+        }
+    }
+
+    @Test
+    void testLoadClassClassWorldsClass() throws Exception {
+        ClassRealm mainRealm = this.world.newRealm("main");
+        Class<?> cls = mainRealm.loadClass("org.codehaus.plexus.classworlds.ClassWorld");
+        assertNotNull(cls);
+        assertSame(ClassWorld.class, cls);
+    }
+
+    @Test
+    void testLoadClassLocal() throws Exception {
+        ClassRealm mainRealm = this.world.newRealm("main");
+        try {
+            mainRealm.loadClass("a.A");
+        } catch (ClassNotFoundException e) {
+            // expected and correct
+        }
+
+        mainRealm.addURL(getJarUrl("a.jar"));
+        Class<?> classA = mainRealm.loadClass("a.A");
+        assertNotNull(classA);
+
+        ClassRealm otherRealm = this.world.newRealm("other");
+        try {
+            otherRealm.loadClass("a.A");
+        } catch (ClassNotFoundException e) {
+            // expected and correct
+        }
+    }
+
+    @Test
+    void testLoadClassImported() throws Exception {
+        ClassRealm mainRealm = this.world.newRealm("main");
+        ClassRealm realmA = this.world.newRealm("realmA");
+
+        try {
+            realmA.loadClass("a.A");
+            fail("realmA.loadClass(a.A) should have thrown a ClassNotFoundException");
+        } catch (ClassNotFoundException e) {
+            // expected and correct
+        }
+
+        realmA.addURL(getJarUrl("a.jar"));
+
+        try {
+            mainRealm.loadClass("a.A");
+            fail("mainRealm.loadClass(a.A) should have thrown a ClassNotFoundException");
+        } catch (ClassNotFoundException e) {
+            // expected and correct
+        }
+
+        mainRealm.importFrom("realmA", "a");
+
+        Class<?> classA = realmA.loadClass("a.A");
+        assertNotNull(classA);
+        assertEquals(realmA, classA.getClassLoader());
+
+        Class<?> classMain = mainRealm.loadClass("a.A");
+        assertNotNull(classMain);
+        assertEquals(realmA, classMain.getClassLoader());
+        assertSame(classA, classMain);
+    }
+
+    @Test
+    void testLoadClassPackage() throws Exception {
+        ClassRealm realmA = this.world.newRealm("realmA");
+        realmA.addURL(getJarUrl("a.jar"));
+
+        Class<?> clazz = realmA.loadClass("a.A");
+        assertNotNull(clazz);
+        assertEquals("a.A", clazz.getName());
+
+        Package p = clazz.getPackage();
+        assertNotNull(p);
+        assertEquals("a", p.getName(), "p.getName()");
+    }
+
+    @Test
+    void testLoadClassComplex() throws Exception {
+        ClassRealm realmA = this.world.newRealm("realmA");
+        ClassRealm realmB = this.world.newRealm("realmB");
+        ClassRealm realmC = this.world.newRealm("realmC");
+        realmA.addURL(getJarUrl("a.jar"));
+        realmB.addURL(getJarUrl("b.jar"));
+        realmC.addURL(getJarUrl("c.jar"));
+        realmC.importFrom("realmA", "a");
+        realmC.importFrom("realmB", "b");
+        realmA.importFrom("realmC", "c");
+
+        Class<?> classAA = realmA.loadClass("a.A");
+        Class<?> classBB = realmB.loadClass("b.B");
+        Class<?> classCC = realmC.loadClass("c.C");
+        assertNotNull(classAA);
+        assertNotNull(classBB);
+        assertNotNull(classCC);
+        assertEquals(realmA, classAA.getClassLoader());
+        assertEquals(realmB, classBB.getClassLoader());
+        assertEquals(realmC, classCC.getClassLoader());
+
+        // load from C
+        Class<?> classAC = realmC.loadClass("a.A");
+        assertNotNull(classAC);
+        assertSame(classAA, classAC);
+        assertEquals(realmA, classAC.getClassLoader());
+
+        Class<?> classBC = realmC.loadClass("b.B");
+        assertNotNull(classBC);
+        assertSame(classBB, classBC);
+        assertEquals(realmB, classBC.getClassLoader());
+
+        // load from A
+        Class<?> classCA = realmA.loadClass("c.C");
+        assertNotNull(classCA);
+        assertSame(classCC, classCA);
+        assertEquals(realmC, classCA.getClassLoader());
+
+        try {
+            realmA.loadClass("b.B");
+            fail("throw ClassNotFoundException");
+        } catch (ClassNotFoundException e) {
+            // expected and correct
+        }
+
+        // load from B
+        try {
+            realmB.loadClass("a.A");
+            fail("throw ClassNotFoundException");
+        } catch (ClassNotFoundException e) {
+            // expected and correct
+        }
+
+        try {
+            realmB.loadClass("c.C");
+            fail("throw ClassNotFoundException");
+        } catch (ClassNotFoundException e) {
+            // expected and correct
+        }
+    }
+
+    @Test
+    void testLoadClassClassWorldsClassRepeatedly() throws Exception {
+        ClassRealm mainRealm = this.world.newRealm("main");
+        for (int i = 0; i < 100; i++) {
+            Class<?> cls = mainRealm.loadClass("org.codehaus.plexus.classworlds.ClassWorld");
+            assertNotNull(cls);
+            assertSame(ClassWorld.class, cls);
+        }
+    }
+
+    @Test
+    void testLoadClassWithModuleNameJava9() {
+        final ExtendedClassRealm mainRealm = new ExtendedClassRealm(world);
+        mainRealm.addURL(getJarUrl("a.jar"));
+        assertNotNull(mainRealm.simulateLoadClassFromModule("a.A"));
+    }
+
+    @Test
+    void testGetResourcesBaseBeforeSelf() throws Exception {
+        String resource = "common.properties";
+
+        ClassRealm base = this.world.newRealm("realmA");
+        base.addURL(getJarUrl("a.jar"));
+        URL baseUrl = base.getResource(resource);
+        assertNotNull(baseUrl);
+
+        ClassRealm sub = this.world.newRealm("realmB", base);
+        sub.addURL(getJarUrl("b.jar"));
+        URL subUrl = sub.getResource(resource);
+        assertNotNull(subUrl);
+        assertEquals(baseUrl, subUrl);
+
+        List<String> urls = new ArrayList<>();
+        for (URL url : Collections.list(sub.getResources(resource))) {
+            String path = url.toString();
+            path = path.substring(path.lastIndexOf('/', path.lastIndexOf(".jar!")));
+            urls.add(path);
+        }
+        assertEquals(Arrays.asList("/a.jar!/common.properties", "/b.jar!/common.properties"), urls);
+    }
+
+    @Test
+    void testGetResourcesSelfBeforeParent() throws Exception {
+        String resource = "common.properties";
+
+        ClassRealm parent = this.world.newRealm("realmA");
+        parent.addURL(getJarUrl("a.jar"));
+        URL parentUrl = parent.getResource(resource);
+        assertNotNull(parentUrl);
+
+        ClassRealm child = parent.createChildRealm("realmB");
+        child.addURL(getJarUrl("b.jar"));
+        URL childUrl = child.getResource(resource);
+        assertNotNull(childUrl);
+
+        List<URL> urls = Collections.list(child.getResources(resource));
+        assertNotNull(urls);
+        assertEquals(Arrays.asList(childUrl, parentUrl), urls);
+    }
+
+    /**
+     * Simulates new {@code java.lang.ClassLoader#findClass(String,String)} introduced with Java 9.
+     * It is reversed in terms of inheritance but enables to simulate the same behavior in these tests.
+     * @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html#findClass(java.lang.String,java.lang.String)">ClassLoader#findClass(String,String)</a>
+     */
+    private static class ExtendedClassRealm extends ClassRealm {
+
+        ExtendedClassRealm(final ClassWorld world) {
+            super(world, "java9", Thread.currentThread().getContextClassLoader());
+        }
+
+        public Class<?> simulateLoadClassFromModule(final String name) {
+            synchronized (getClassLoadingLock(name)) {
+                Class<?> c = findLoadedClass(name);
+                if (c == null) {
+                    c = findClass(null, name);
+                }
+                return c;
+            }
+        }
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/DefaultClassRealmTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/DefaultClassRealmTest.java
new file mode 100644
index 0000000000..c79468c973
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/DefaultClassRealmTest.java
@@ -0,0 +1,346 @@
+/*
+ * 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.codehaus.plexus.classworlds.realm;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+
+import org.codehaus.plexus.classworlds.AbstractClassWorldsTestCase;
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class DefaultClassRealmTest extends AbstractClassWorldsTestCase {
+    // ----------------------------------------------------------------------
+    // Class testing
+    // ----------------------------------------------------------------------
+
+    @Test
+    void testLoadClassFromRealm() throws Exception {
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);
+
+        mainRealm.addURL(getJarUrl("component0-1.0.jar"));
+
+        loadClass(mainRealm, "org.codehaus.plexus.Component0");
+    }
+
+    @Test
+    void testLoadClassFromChildRealmWhereClassIsLocatedInParentRealm() throws Exception {
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);
+
+        mainRealm.addURL(getJarUrl("component0-1.0.jar"));
+
+        ClassRealm childRealm = mainRealm.createChildRealm("child");
+
+        loadClass(childRealm, "org.codehaus.plexus.Component0");
+    }
+
+    @Test
+    void testLoadClassFromChildRealmWhereClassIsLocatedInGrantParentRealm() throws Exception {
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);
+
+        mainRealm.addURL(getJarUrl("component0-1.0.jar"));
+
+        ClassRealm childRealm = mainRealm.createChildRealm("child");
+
+        ClassRealm grandchildRealm = childRealm.createChildRealm("grandchild");
+
+        loadClass(grandchildRealm, "org.codehaus.plexus.Component0");
+    }
+
+    @Test
+    void testLoadClassFromChildRealmWhereClassIsLocatedInBothChildRealmAndParentRealm() throws Exception {
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "parent", null);
+
+        mainRealm.addURL(getJarUrl("component5-1.0.jar"));
+
+        ClassRealm childRealm = mainRealm.createChildRealm("child");
+
+        childRealm.addURL(getJarUrl("component5-2.0.jar"));
+
+        Class<?> cls = loadClass(childRealm, "test.Component5");
+
+        assertSame(childRealm, cls.getClassLoader());
+        assertEquals(1, cls.getMethods().length);
+        assertEquals("printNew", cls.getMethods()[0].getName());
+    }
+
+    @Test
+    void testLoadNonExistentClass() {
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);
+
+        mainRealm.addURL(getJarUrl("component0-1.0.jar"));
+
+        try {
+            mainRealm.loadClass("org.foo.bar.NonExistentClass");
+
+            fail("A ClassNotFoundException should have been thrown!");
+        } catch (ClassNotFoundException e) {
+            // expected
+        }
+    }
+
+    @Test
+    void testImport() throws Exception {
+        ClassWorld world = new ClassWorld();
+
+        ClassRealm r0 = world.newRealm("r0");
+
+        ClassRealm r1 = world.newRealm("r1");
+
+        r0.addURL(getJarUrl("component0-1.0.jar"));
+
+        r1.importFrom("r0", "org.codehaus.plexus");
+
+        loadClass(r1, "org.codehaus.plexus.Component0");
+    }
+
+    @Test
+    void testParentImport() throws Exception {
+        ClassWorld world = new ClassWorld();
+
+        ClassRealm parent = world.newRealm("parent");
+
+        ClassRealm child = world.newRealm("child");
+
+        parent.addURL(getJarUrl("component0-1.0.jar"));
+
+        child.setParentRealm(parent);
+
+        Class<?> type = loadClass(child, "org.codehaus.plexus.Component0");
+
+        child.importFromParent("non-existing");
+
+        assertSame(null, loadClassOrNull(child, "org.codehaus.plexus.Component0"));
+
+        child.importFromParent("org.codehaus.plexus");
+
+        assertSame(type, loadClass(child, "org.codehaus.plexus.Component0"));
+    }
+
+    @Test
+    void testLoadClassFromBaseClassLoaderBeforeSelf() throws Exception {
+        ClassWorld world = new ClassWorld();
+
+        ClassRealm base = world.newRealm("base");
+
+        base.addURL(getJarUrl("a.jar"));
+
+        ClassRealm child = world.newRealm("child", base);
+
+        child.addURL(getJarUrl("a.jar"));
+
+        Class<?> baseClass = loadClass(base, "a.A");
+        Class<?> childClass = loadClass(child, "a.A");
+
+        assertSame(base, baseClass.getClassLoader());
+        assertSame(base, childClass.getClassLoader());
+        assertSame(baseClass, childClass);
+    }
+
+    @Test
+    void testLoadClassFromRealmWithCircularClassReferences() throws Exception {
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);
+
+        mainRealm.addURL(getJarUrl("circular-0.1.jar"));
+
+        /*
+         * This was reported to fail with a ClassCircularityError in IBM JDK 1.5.0-SR2, 1.5.0-SR7 and 1.6.0-SR2. It
+         * works in IBM JDK 1.5.0-SR10 and 1.6.0-SR6.
+         */
+        loadClass(mainRealm, "A$C");
+    }
+
+    // ----------------------------------------------------------------------
+    // Resource testing
+    // ----------------------------------------------------------------------
+
+    @Test
+    void testResource() throws Exception {
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);
+
+        mainRealm.addURL(getJarUrl("component0-1.0.jar"));
+
+        getResource(mainRealm, "META-INF/plexus/components.xml");
+    }
+
+    @Test
+    void testMalformedResource() throws Exception {
+        URL jarUrl = getJarUrl("component0-1.0.jar");
+
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);
+
+        mainRealm.addURL(jarUrl);
+
+        ClassLoader officialClassLoader = new URLClassLoader(new URL[] {jarUrl});
+
+        String resource = "META-INF/plexus/components.xml";
+
+        assertNotNull(mainRealm.getResource(resource));
+        assertNotNull(officialClassLoader.getResource(resource));
+
+        /*
+         * NOTE: Resource names with a leading slash are invalid when passed to a class loader and must not be found!
+         * One can use a leading slash in Class.getResource() but not in ClassLoader.getResource().
+         */
+
+        assertSame(null, mainRealm.getResource("/" + resource));
+        assertSame(null, officialClassLoader.getResource("/" + resource));
+    }
+
+    @Test
+    void testFindResourceOnlyScansSelf() throws Exception {
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);
+
+        mainRealm.addURL(getJarUrl("a.jar"));
+
+        ClassRealm childRealm = mainRealm.createChildRealm("child");
+
+        childRealm.addURL(getJarUrl("b.jar"));
+
+        assertNotNull(childRealm.getResource("a.properties"));
+        assertNotNull(childRealm.getResource("b.properties"));
+
+        assertNull(childRealm.findResource("a.properties"));
+
+        assertNotNull(childRealm.findResource("b.properties"));
+    }
+
+    @Test
+    void testFindResourcesOnlyScansSelf() throws Exception {
+        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);
+
+        mainRealm.addURL(getJarUrl("a.jar"));
+
+        ClassRealm childRealm = mainRealm.createChildRealm("child");
+
+        childRealm.addURL(getJarUrl("b.jar"));
+
+        assertTrue(childRealm.getResources("a.properties").hasMoreElements());
+        assertTrue(childRealm.getResources("b.properties").hasMoreElements());
+
+        assertFalse(childRealm.findResources("a.properties").hasMoreElements());
+
+        assertTrue(childRealm.findResources("b.properties").hasMoreElements());
+    }
+
+    /** Should never deadlock. Ever */
+    @Test
+    void testParallelDeadlockClassRealm() throws InterruptedException {
+        for (int i = 0; i < 100; i++) {
+            doOneDeadlockAttempt();
+        }
+    }
+
+    private void doOneDeadlockAttempt() throws InterruptedException {
+        // Deadlock sample graciously ripped from http://docs.oracle.com/javase/7/docs/technotes/guides/lang/cl-mt.html
+        final ClassRealm cl1 = new ClassRealm(new ClassWorld(), "cl1", null);
+        final ClassRealm cl2 = new ClassRealm(new ClassWorld(), "cl2", cl1);
+        cl1.setParentRealm(cl2);
+        cl1.addURL(getJarUrl("deadlock.jar"));
+        cl2.addURL(getJarUrl("deadlock.jar"));
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        Runnable r1 = () -> {
+            try {
+                latch.await();
+                cl1.loadClass("deadlock.A");
+            } catch (ClassNotFoundException | InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        };
+
+        Runnable r2 = () -> {
+            try {
+                latch.await();
+                cl1.loadClass("deadlock.C");
+            } catch (ClassNotFoundException | InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        };
+
+        Thread thread = new Thread(r1);
+        thread.start();
+        Thread thread1 = new Thread(r2);
+        thread1.start();
+        latch.countDown();
+        thread.join();
+        thread1.join();
+    }
+
+    // ----------------------------------------------------------------------
+    //
+    // ----------------------------------------------------------------------
+
+    private Class<?> loadClassOrNull(ClassRealm realm, String name) {
+        try {
+            return loadClass(realm, name);
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private Class<?> loadClass(ClassRealm realm, String name) throws ClassNotFoundException {
+        Class<?> cls = realm.loadClass(name);
+
+        /*
+         * NOTE: Load the class both directly from the realm and indirectly from an (ordinary) child class loader which
+         * uses the specified class realm for parent delegation. The child class loader itself has no additional class
+         * path entries but relies entirely on the provided class realm. Hence, the created child class loader should in
+         * theory be able to load exactly the same classes/resources as the underlying class realm. In practice, it will
+         * test that class realms properly integrate into the standard Java class loader hierarchy.
+         */
+        ClassLoader childLoader = new URLClassLoader(new URL[0], realm);
+        assertEquals(cls, childLoader.loadClass(name));
+
+        return cls;
+    }
+
+    private void getResource(ClassRealm realm, String name) throws Exception {
+        ClassLoader childLoader = new URLClassLoader(new URL[0], realm);
+        assertNotNull(realm.getResource(name));
+        assertEquals(realm.getResource(name), childLoader.getResource(name));
+        assertEquals(Collections.list(realm.getResources(name)), Collections.list(childLoader.getResources(name)));
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/EntryTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/EntryTest.java
new file mode 100644
index 0000000000..f0115df603
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/EntryTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.codehaus.plexus.classworlds.realm;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+import org.codehaus.plexus.classworlds.AbstractClassWorldsTestCase;
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author <a href="bwalding@jakarta.org">Ben Walding</a>
+ */
+class EntryTest extends AbstractClassWorldsTestCase {
+
+    @Test
+    void testCompareTo() throws Exception {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r = cw.newRealm("test1");
+
+        Entry entry1 = new Entry(r, "org.test");
+        Entry entry2 = new Entry(r, "org.test.impl");
+
+        assertTrue(entry1.compareTo(entry2) > 0, "org.test > org.test.impl");
+    }
+
+    /**
+     * Tests the equality is realm independant
+     */
+    @Test
+    void testEquals() throws DuplicateRealmException {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r1 = cw.newRealm("test1");
+        ClassRealm r2 = cw.newRealm("test2");
+
+        Entry entry1 = new Entry(r1, "org.test");
+        Entry entry2 = new Entry(r2, "org.test");
+
+        assertEquals(entry1, entry2, "entry1 == entry2");
+        assertEquals(entry1.hashCode(), entry2.hashCode(), "entry1.hashCode() == entry2.hashCode()");
+    }
+
+    @Test
+    void testMatchesClassByPackageImport() throws Exception {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r = cw.newRealm("test1");
+
+        Entry entry = new Entry(r, "org.test");
+
+        assertTrue(entry.matches("org.test.MyClass"));
+        assertTrue(entry.matches("org.test.MyClass$NestedClass"));
+        assertTrue(entry.matches("org.test.MyClassUtils"));
+        assertTrue(entry.matches("org.test.impl.MyClass"));
+        assertFalse(entry.matches("org.tests.AnotherClass"));
+    }
+
+    @Test
+    void testMatchesClassByClassImport() throws Exception {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r = cw.newRealm("test1");
+
+        Entry entry = new Entry(r, "org.test.MyClass");
+
+        assertTrue(entry.matches("org.test.MyClass"));
+        assertTrue(entry.matches("org.test.MyClass$NestedClass"));
+        assertFalse(entry.matches("org.test.MyClassUtils"));
+        assertFalse(entry.matches("org.test.AnotherClass"));
+    }
+
+    @Test
+    void testMatchesResourceByPackageImport() throws Exception {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r = cw.newRealm("test1");
+
+        Entry entry = new Entry(r, "org.test");
+
+        assertTrue(entry.matches("org/test/MyClass.class"));
+        assertTrue(entry.matches("org/test/MyClass$NestedClass.class"));
+        assertTrue(entry.matches("org/test/MyClasses.properties"));
+        assertTrue(entry.matches("org/test/impl/MyClass.class"));
+        assertFalse(entry.matches("org/tests/AnotherClass.class"));
+    }
+
+    @Test
+    void testMatchesResourceByClassImport() throws Exception {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r = cw.newRealm("test1");
+
+        Entry entry = new Entry(r, "org.test.MyClass");
+
+        assertTrue(entry.matches("org/test/MyClass.class"));
+        assertTrue(entry.matches("org/test/MyClass$NestedClass.class"));
+        assertFalse(entry.matches("org/test/MyClass.properties"));
+        assertFalse(entry.matches("org/test/AnotherClass"));
+    }
+
+    @Test
+    void testMatchesAllImport() throws Exception {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r = cw.newRealm("test1");
+
+        Entry entry = new Entry(r, "");
+
+        assertTrue(entry.matches("org.test.MyClass"));
+        assertTrue(entry.matches("org.test.MyClass$NestedClass"));
+        assertTrue(entry.matches("org/test/MyClass.class"));
+        assertTrue(entry.matches("org/test/MyClass.properties"));
+    }
+
+    @Test
+    void testMatchesResourceByResourceImport() throws Exception {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r = cw.newRealm("test1");
+
+        Entry entry1 = new Entry(r, "some.properties");
+
+        assertTrue(entry1.matches("some.properties"));
+        assertFalse(entry1.matches("other.properties"));
+
+        Entry entry2 = new Entry(r, "org/test/some.properties");
+
+        assertTrue(entry2.matches("org/test/some.properties"));
+        assertFalse(entry2.matches("org/test/other.properties"));
+    }
+
+    @Test
+    void testMatchesClassByExactPackageImport() throws Exception {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r = cw.newRealm("test1");
+
+        Entry entry = new Entry(r, "org.test.*");
+
+        assertTrue(entry.matches("org.test.MyClass"));
+        assertTrue(entry.matches("org.test.MyClass$NestedClass"));
+        assertTrue(entry.matches("org.test.MyClassUtils"));
+        assertFalse(entry.matches("org.test.impl.MyClass"));
+        assertFalse(entry.matches("org.tests.AnotherClass"));
+    }
+
+    @Test
+    void testMatchesResourceByExactPackageImport() throws Exception {
+        ClassWorld cw = new ClassWorld();
+        ClassRealm r = cw.newRealm("test1");
+
+        Entry entry = new Entry(r, "org.test.*");
+
+        assertTrue(entry.matches("org/test/MyClass.class"));
+        assertTrue(entry.matches("org/test/MyClass$NestedClass.class"));
+        assertTrue(entry.matches("org/test/MyClasses.properties"));
+        assertFalse(entry.matches("org/test/impl/MyClass.class"));
+        assertFalse(entry.matches("org/tests/AnotherClass.class"));
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/FilteredClassRealmTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/FilteredClassRealmTest.java
new file mode 100644
index 0000000000..a85076da34
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/FilteredClassRealmTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.codehaus.plexus.classworlds.realm;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import org.codehaus.plexus.classworlds.AbstractClassWorldsTestCase;
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class FilteredClassRealmTest extends AbstractClassWorldsTestCase {
+    private ClassWorld world;
+    private ClassRealm realmA;
+
+    @BeforeEach
+    public void setUp() throws DuplicateRealmException {
+        this.world = new ClassWorld();
+        // only allow loading resources whose names start with "a."
+        Set<String> allowedResourcePrefixes = new HashSet<>();
+        allowedResourcePrefixes.add("a.");
+        allowedResourcePrefixes.add("a/Aa");
+        realmA = this.world.newRealm(
+                "realmA",
+                getClass().getClassLoader(),
+                s -> allowedResourcePrefixes.stream().anyMatch(s::startsWith));
+    }
+
+    @Test
+    void testLoadResources() throws Exception {
+        realmA.addURL(getJarUrl("a.jar"));
+        assertNull(realmA.getResource("common.properties"));
+        assertFalse(realmA.getResources("common.properties").hasMoreElements());
+
+        assertNotNull(realmA.getResource("a.properties"));
+        assertTrue(realmA.getResources("a.properties").hasMoreElements());
+    }
+
+    @Test
+    void testLoadClass() throws ClassNotFoundException {
+        assertThrows(ClassNotFoundException.class, () -> realmA.loadClass("a.Aa"));
+        realmA.addURL(getJarUrl("a.jar"));
+
+        assertNotNull(realmA.loadClass("a.Aa"));
+        assertThrows(ClassNotFoundException.class, () -> realmA.loadClass("a.A"));
+
+        assertNotNull(realmA.loadClass("a.Aa"));
+        assertThrows(ClassNotFoundException.class, () -> realmA.loadClass("a.A"));
+    }
+
+    @Test
+    void testLoadClassWithModule() throws IOException {
+        try (ExtendedFilteredClassRealm realmA = new ExtendedFilteredClassRealm(world, s -> s.startsWith("a/Aa"))) {
+            realmA.addURL(getJarUrl("a.jar"));
+            assertNotNull(realmA.simulateLoadClassFromModule("a.Aa"));
+            assertNull(realmA.simulateLoadClassFromModule("a.A"));
+        }
+    }
+
+    /**
+     * Simulates new {@code java.lang.ClassLoader#findClass(String,String)} introduced with Java 9.
+     * It is reversed in terms of inheritance but enables to simulate the same behavior in these tests.
+     * @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html#findClass(java.lang.String,java.lang.String)">ClassLoader#findClass(String,String)</a>
+     * @see ClassRealmImplTest.ExtendedClassRealm
+     */
+    static class ExtendedFilteredClassRealm extends FilteredClassRealm {
+
+        ExtendedFilteredClassRealm(final ClassWorld world, Predicate<String> filter) {
+            super(filter, world, "java9", Thread.currentThread().getContextClassLoader());
+        }
+
+        public Class<?> simulateLoadClassFromModule(final String name) {
+            synchronized (getClassLoadingLock(name)) {
+                Class<?> c = findLoadedClass(name);
+                if (c == null) {
+                    c = findClass(null, name);
+                }
+                return c;
+            }
+        }
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/strategy/StrategyFactoryTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/strategy/StrategyFactoryTest.java
new file mode 100644
index 0000000000..6c45242707
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/strategy/StrategyFactoryTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.codehaus.plexus.classworlds.strategy;
+
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class StrategyFactoryTest {
+
+    private ClassRealm realm;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        realm = new ClassWorld().newRealm("test");
+    }
+
+    @Test
+    void defaultStrategyIsSelfFirst() {
+        assertInstanceOf(SelfFirstStrategy.class, StrategyFactory.getStrategy(realm));
+    }
+
+    @Test
+    void selfFirstHint() {
+        assertInstanceOf(SelfFirstStrategy.class, StrategyFactory.getStrategy(realm, "self-first"));
+    }
+
+    @Test
+    void defaultHintIsSelfFirst() {
+        assertInstanceOf(SelfFirstStrategy.class, StrategyFactory.getStrategy(realm, "default"));
+    }
+
+    @Test
+    void nullHintIsSelfFirst() {
+        assertInstanceOf(SelfFirstStrategy.class, StrategyFactory.getStrategy(realm, null));
+    }
+
+    @Test
+    void emptyHintIsSelfFirst() {
+        assertInstanceOf(SelfFirstStrategy.class, StrategyFactory.getStrategy(realm, ""));
+    }
+
+    @Test
+    void parentFirstHint() {
+        assertInstanceOf(ParentFirstStrategy.class, StrategyFactory.getStrategy(realm, "parent-first"));
+    }
+
+    @Test
+    void osgiHint() {
+        assertInstanceOf(OsgiBundleStrategy.class, StrategyFactory.getStrategy(realm, "osgi"));
+    }
+
+    @Test
+    void unknownHintThrows() {
+        IllegalArgumentException ex =
+                assertThrows(IllegalArgumentException.class, () -> StrategyFactory.getStrategy(realm, "bogus"));
+        assertInstanceOf(IllegalArgumentException.class, ex);
+    }
+}
diff --git a/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/strategy/StrategyTest.java b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/strategy/StrategyTest.java
new file mode 100644
index 0000000000..d8ed59bb7c
--- /dev/null
+++ b/impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/strategy/StrategyTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.codehaus.plexus.classworlds.strategy;
+
+/*
+ * Copyright 2001-2006 Codehaus 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.
+ */
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+
+import org.codehaus.plexus.classworlds.AbstractClassWorldsTestCase;
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+// jars within jars
+// hierarchy vs graph
+
+class StrategyTest extends AbstractClassWorldsTestCase {
+    private ClassRealm realm;
+
+    private Strategy strategy;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        this.realm = new ClassWorld().newRealm("realm");
+        this.strategy = this.realm.getStrategy();
+        realm.addURL(getJarUrl("component0-1.0.jar"));
+    }
+
+    @Test
+    void testLoadingOfApplicationClass() throws Exception {
+        assertNotNull(strategy.loadClass("org.codehaus.plexus.Component0"));
+    }
+
+    @Test
+    void testLoadingOfApplicationClassThenDoingItAgain() throws Exception {
+        Class<?> c = strategy.loadClass("org.codehaus.plexus.Component0");
+
+        assertNotNull(c);
+
+        c = strategy.loadClass("org.codehaus.plexus.Component0");
+
+        assertNotNull(c);
+    }
+
+    @Test
+    void testLoadingOfSystemClass() throws Exception {
+        assertNotNull(strategy.getRealm().loadClass("java.lang.Object"));
+    }
+
+    @Test
+    void testLoadingOfNonExistentClass() {
+        try {
+            strategy.loadClass("org.codehaus.plexus.NonExistentComponent");
+
+            fail("Should have thrown a ClassNotFoundException!");
+        } catch (ClassNotFoundException e) {
+            // do nothing
+        }
+    }
+
+    @Test
+    void testGetApplicationResource() throws Exception {
+        URL resource = strategy.getResource("META-INF/plexus/components.xml");
+
+        assertNotNull(resource);
+
+        String content = getContent(resource.openStream());
+
+        assertTrue(content.startsWith("<component-set>"));
+    }
+
+    @Test
+    void testGetSystemResource() {
+        assumeTrue(
+                getJavaVersion() < 9.0,
+                "Due to strong encapsulation you cannot get the java/lang/Object.class as resource since Java 9");
+
+        URL resource = strategy.getRealm().getResource("java/lang/Object.class");
+
+        assertNotNull(resource);
+    }
+
+    @Test
+    void testFindResources() throws Exception {
+        realm.addURL(getJarUrl("component1-1.0.jar"));
+
+        Enumeration<URL> e = strategy.getResources("META-INF/plexus/components.xml");
+        assertNotNull(e);
+
+        int resourceCount = 0;
+        while (e.hasMoreElements()) {
+            e.nextElement();
+            resourceCount++;
+        }
+        assertEquals(2, resourceCount);
+    }
+
+    protected String getContent(InputStream in) throws Exception {
+        byte[] buffer = new byte[1024];
+
+        int read;
+
+        StringBuilder content = new StringBuilder();
+
+        while ((read = in.read(buffer, 0, 1024)) >= 0) {
+            content.append(new String(buffer, 0, read));
+        }
+
+        return content.toString();
+    }
+
+    private double getJavaVersion() {
+        return Double.parseDouble(System.getProperty("java.specification.version"));
+    }
+}
diff --git a/impl/maven-classworlds/src/test/test-data/a.properties b/impl/maven-classworlds/src/test/test-data/a.properties
new file mode 100644
index 0000000000..d0e2fad851
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/a.properties
@@ -0,0 +1,18 @@
+################################################################################
+#
+#  Copyright 2001-2006 The Codehaus 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.
+#
+
+a properties
diff --git a/impl/maven-classworlds/src/test/test-data/dupe-main.conf b/impl/maven-classworlds/src/test/test-data/dupe-main.conf
new file mode 100644
index 0000000000..ddd111f9a7
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/dupe-main.conf
@@ -0,0 +1,3 @@
+
+main is com.werken.Foo from foo
+main is com.werken.Bar from bar
diff --git a/impl/maven-classworlds/src/test/test-data/dupe-realm.conf b/impl/maven-classworlds/src/test/test-data/dupe-realm.conf
new file mode 100644
index 0000000000..345f51f0e1
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/dupe-realm.conf
@@ -0,0 +1,9 @@
+
+[cheese]
+
+[dupe.realm]
+
+[potatoes]
+
+[dupe.realm]
+
diff --git a/impl/maven-classworlds/src/test/test-data/early-import.conf b/impl/maven-classworlds/src/test/test-data/early-import.conf
new file mode 100644
index 0000000000..23378ecf00
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/early-import.conf
@@ -0,0 +1,2 @@
+
+import org.xml.sax from root
diff --git a/impl/maven-classworlds/src/test/test-data/inheritance.conf b/impl/maven-classworlds/src/test/test-data/inheritance.conf
new file mode 100644
index 0000000000..46f72107e5
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/inheritance.conf
@@ -0,0 +1,18 @@
+# ------------------------------------------------------------
+# Define the main entry-point
+# ------------------------------------------------------------
+
+main is org.apache.maven.app.App from root.maven
+
+# ------------------------------------------------------------
+# Start defining realms
+# ------------------------------------------------------------
+
+[root]
+load ${basedir}/target/test-jars/a.jar
+
+[root.maven]
+load ${basedir}/target/test-jars/b.jar
+
+[root.maven.plugin]
+load ${basedir}/target/test-jars/c.jar
diff --git a/impl/maven-classworlds/src/test/test-data/launch-noclass.conf b/impl/maven-classworlds/src/test/test-data/launch-noclass.conf
new file mode 100644
index 0000000000..439e24b55b
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/launch-noclass.conf
@@ -0,0 +1,6 @@
+
+main is b.Goober from app
+
+[app]
+  load ${basedir}/target/test-jars/a.jar
+  load ${basedir}/target/test-jars/b.jar
diff --git a/impl/maven-classworlds/src/test/test-data/launch-nomethod.conf b/impl/maven-classworlds/src/test/test-data/launch-nomethod.conf
new file mode 100644
index 0000000000..674fc499bd
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/launch-nomethod.conf
@@ -0,0 +1,7 @@
+
+main is c.C from app
+
+[app]
+  load ${basedir}/target/test-jars/a.jar
+  load ${basedir}/target/test-jars/b.jar
+  load ${basedir}/target/test-jars/c.jar
diff --git a/impl/maven-classworlds/src/test/test-data/nested.properties b/impl/maven-classworlds/src/test/test-data/nested.properties
new file mode 100644
index 0000000000..0462a15532
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/nested.properties
@@ -0,0 +1,18 @@
+################################################################################
+#
+#  Copyright 2001-2006 The Codehaus 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.
+#
+
+nested.properties
diff --git a/impl/maven-classworlds/src/test/test-data/optionally-existent.conf b/impl/maven-classworlds/src/test/test-data/optionally-existent.conf
new file mode 100644
index 0000000000..fe102574c9
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/optionally-existent.conf
@@ -0,0 +1,14 @@
+
+# ------------------------------------------------------------
+# Define the main entry-point
+# ------------------------------------------------------------
+
+main is org.apache.maven.app.App from opt
+
+# ------------------------------------------------------------
+# Start defining realms
+# ------------------------------------------------------------
+
+[opt]
+  optionally ${basedir}/target/test-lib/jakarta.xml.bind-api-4.0.2.jar
+
diff --git a/impl/maven-classworlds/src/test/test-data/optionally-nonexistent.conf b/impl/maven-classworlds/src/test/test-data/optionally-nonexistent.conf
new file mode 100644
index 0000000000..4048e077e6
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/optionally-nonexistent.conf
@@ -0,0 +1,13 @@
+
+# ------------------------------------------------------------
+# Define the main entry-point
+# ------------------------------------------------------------
+
+main is org.apache.maven.app.App from opt
+
+# ------------------------------------------------------------
+# Start defining realms
+# ------------------------------------------------------------
+
+[opt]
+  optionally /non/existant/path/*.jar
diff --git a/impl/maven-classworlds/src/test/test-data/realm-syntax.conf b/impl/maven-classworlds/src/test/test-data/realm-syntax.conf
new file mode 100644
index 0000000000..27b9e73f7d
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/realm-syntax.conf
@@ -0,0 +1,3 @@
+
+[foo
+]
diff --git a/impl/maven-classworlds/src/test/test-data/resources/classworlds.conf b/impl/maven-classworlds/src/test/test-data/resources/classworlds.conf
new file mode 100644
index 0000000000..d60fb1f5eb
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/resources/classworlds.conf
@@ -0,0 +1,12 @@
+# ------------------------------------------------------------
+# Define the main entry-point
+# ------------------------------------------------------------
+
+main is foo from root
+
+# ------------------------------------------------------------
+# Start defining realms
+# ------------------------------------------------------------
+
+[root]
+load ${basedir}/target/test-jars/resources/werkflow.jar
diff --git a/impl/maven-classworlds/src/test/test-data/set-using-existent.conf b/impl/maven-classworlds/src/test/test-data/set-using-existent.conf
new file mode 100644
index 0000000000..11415b8ebf
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/set-using-existent.conf
@@ -0,0 +1,19 @@
+
+# ------------------------------------------------------------
+# Define the main entry-point
+# ------------------------------------------------------------
+
+main is org.apache.maven.app.App from opt
+
+# ------------------------------------------------------------
+# Set properties
+# ------------------------------------------------------------
+set set.using.existent using ${basedir}/src/test/test-data/set-using-existent.properties
+set set.using.default using ${basedir}/src/test/test-data/set-using-existent.properties default testSetUsingExistentDefault
+
+# ------------------------------------------------------------
+# Start defining realms
+# ------------------------------------------------------------
+
+[opt]
+
diff --git a/impl/maven-classworlds/src/test/test-data/set-using-existent.properties b/impl/maven-classworlds/src/test/test-data/set-using-existent.properties
new file mode 100644
index 0000000000..6434aef94a
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/set-using-existent.properties
@@ -0,0 +1,19 @@
+################################################################################
+#
+#  Copyright 2001-2006 The Codehaus 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.
+#
+
+set.using.existent=testSetUsingExistent
+
diff --git a/impl/maven-classworlds/src/test/test-data/set-using-missing.conf b/impl/maven-classworlds/src/test/test-data/set-using-missing.conf
new file mode 100644
index 0000000000..6f8ccc53d2
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/set-using-missing.conf
@@ -0,0 +1,19 @@
+
+# ------------------------------------------------------------
+# Define the main entry-point
+# ------------------------------------------------------------
+
+main is org.apache.maven.app.App from opt
+
+# ------------------------------------------------------------
+# Set properties
+# ------------------------------------------------------------
+set set.using.missing default testSetUsingMissingDefault
+set set.using.filtered.default default ${user.home}/m2
+
+# ------------------------------------------------------------
+# Start defining realms
+# ------------------------------------------------------------
+
+[opt]
+
diff --git a/impl/maven-classworlds/src/test/test-data/set-using-nonexistent.conf b/impl/maven-classworlds/src/test/test-data/set-using-nonexistent.conf
new file mode 100644
index 0000000000..4640cf52c2
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/set-using-nonexistent.conf
@@ -0,0 +1,19 @@
+
+# ------------------------------------------------------------
+# Define the main entry-point
+# ------------------------------------------------------------
+
+main is org.apache.maven.app.App from opt
+
+# ------------------------------------------------------------
+# Set properties
+# ------------------------------------------------------------
+set set.using.nonexistent using ${basedir}/src/test/test-data/set-using-nonexistent.properties
+set set.using.nonexistent.default using ${basedir}/src/test/test-data/set-using-nonexistent.properties default testSetUsingNonExistentDefault
+
+# ------------------------------------------------------------
+# Start defining realms
+# ------------------------------------------------------------
+
+[opt]
+
diff --git a/impl/maven-classworlds/src/test/test-data/unhandled.conf b/impl/maven-classworlds/src/test/test-data/unhandled.conf
new file mode 100644
index 0000000000..3f808fe665
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/unhandled.conf
@@ -0,0 +1,2 @@
+
+foo
diff --git a/impl/maven-classworlds/src/test/test-data/valid-enh-launch-exitCode.conf b/impl/maven-classworlds/src/test/test-data/valid-enh-launch-exitCode.conf
new file mode 100644
index 0000000000..a88204a5ed
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/valid-enh-launch-exitCode.conf
@@ -0,0 +1,6 @@
+
+main is b.Bb from app
+
+[app]
+  load ${basedir}/target/test-jars/a.jar
+  load ${basedir}/target/test-jars/b.jar
diff --git a/impl/maven-classworlds/src/test/test-data/valid-enh-launch.conf b/impl/maven-classworlds/src/test/test-data/valid-enh-launch.conf
new file mode 100644
index 0000000000..02bb60a912
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/valid-enh-launch.conf
@@ -0,0 +1,6 @@
+
+main is b.B from app
+
+[app]
+  load ${basedir}/target/test-jars/a.jar
+  load ${basedir}/target/test-jars/b.jar
diff --git a/impl/maven-classworlds/src/test/test-data/valid-from-from-from.conf b/impl/maven-classworlds/src/test/test-data/valid-from-from-from.conf
new file mode 100644
index 0000000000..49895b093c
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/valid-from-from-from.conf
@@ -0,0 +1,24 @@
+
+# ------------------------------------------------------------
+# Define the main entry-point
+# ------------------------------------------------------------
+
+main is com.from.from.from.Main from from
+
+# ------------------------------------------------------------
+# Start defining realms
+# ------------------------------------------------------------
+
+[from]
+  load ${basedir}/target/test-jars/from-from-0.0.1-from-load-import.jar
+
+[ant]
+  import com.from.from.from from from
+  load ${basedir}/target/test-lib/ant-1.10.14.jar
+
+[maven]
+  import com.from.from.from from from
+  load ${basedir}/target/test-lib/log4j-api-2.23.1.jar
+
+[glob]
+  load ${basedir}/target/test-jars/*.jar
diff --git a/impl/maven-classworlds/src/test/test-data/valid-launch-exitCode.conf b/impl/maven-classworlds/src/test/test-data/valid-launch-exitCode.conf
new file mode 100644
index 0000000000..20ada89658
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/valid-launch-exitCode.conf
@@ -0,0 +1,5 @@
+
+main is a.Aa from app
+
+[app]
+  load ${basedir}/target/test-jars/a.jar
diff --git a/impl/maven-classworlds/src/test/test-data/valid-launch.conf b/impl/maven-classworlds/src/test/test-data/valid-launch.conf
new file mode 100644
index 0000000000..4752d09ca5
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/valid-launch.conf
@@ -0,0 +1,5 @@
+
+main is a.A from app
+
+[app]
+  load ${basedir}/target/test-jars/a.jar
diff --git a/impl/maven-classworlds/src/test/test-data/valid.conf b/impl/maven-classworlds/src/test/test-data/valid.conf
new file mode 100644
index 0000000000..e85024bf00
--- /dev/null
+++ b/impl/maven-classworlds/src/test/test-data/valid.conf
@@ -0,0 +1,24 @@
+
+# ------------------------------------------------------------
+# Define the main entry-point
+# ------------------------------------------------------------
+
+main is org.apache.maven.app.App from maven
+
+# ------------------------------------------------------------
+# Start defining realms
+# ------------------------------------------------------------
+
+[xml]
+  load ${basedir}/target/test-lib/jakarta.xml.bind-api-4.0.2.jar
+
+[ant]
+  import jakarta.xml.bind from xml
+  load ${basedir}/target/test-lib/ant-1.10.14.jar
+
+[maven]
+  import jakarta.xml.bind from xml
+  load ${basedir}/target/test-lib/log4j-api-2.23.1.jar
+
+[glob]
+  load ${basedir}/target/test-jars/*.jar
diff --git a/impl/maven-cli/pom.xml b/impl/maven-cli/pom.xml
index 06bd4c72dd..6d91bf9da4 100644
--- a/impl/maven-cli/pom.xml
+++ b/impl/maven-cli/pom.xml
@@ -150,8 +150,8 @@ under the License.
     </dependency>
 
     <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-classworlds</artifactId>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-classworlds</artifactId>
     </dependency>
     <dependency>
       <groupId>org.codehaus.plexus</groupId>
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
index f7e5593198..cbb8d25b06 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
@@ -23,6 +23,7 @@
 import java.io.OutputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 
 import org.apache.maven.api.annotations.Nullable;
 import org.apache.maven.api.cli.Invoker;
@@ -74,8 +75,13 @@ public static int main(String[] args, ClassWorld world) throws IOException {
 
     private static int delegateMain(String mainClass, String[] args, ClassWorld world) throws IOException {
         try {
-            Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(mainClass);
-            Method method = clazz.getMethod("main", String[].class, ClassWorld.class);
+            ClassLoader cl = Thread.currentThread().getContextClassLoader();
+            Class<?> clazz = cl.loadClass(mainClass);
+            Class<?> cwClass = cl.loadClass(ClassWorld.class.getName());
+            Method method = clazz.getMethod("main", String[].class, cwClass);
+            if (!Modifier.isStatic(method.getModifiers()) || !Modifier.isPublic(method.getModifiers())) {
+                throw new NoSuchMethodException(mainClass + ".main(String[], ClassWorld)");
+            }
             return (int) method.invoke(null, args, world);
         } catch (ClassNotFoundException e) {
             throw new IOException("Cannot find maven.mainClass: " + mainClass, e);
diff --git a/impl/maven-core/pom.xml b/impl/maven-core/pom.xml
index 8a3719d63b..5b76715677 100644
--- a/impl/maven-core/pom.xml
+++ b/impl/maven-core/pom.xml
@@ -173,8 +173,8 @@ under the License.
 
     <!-- Sisu/Eclipse DI -->
     <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-classworlds</artifactId>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-classworlds</artifactId>
     </dependency>
     <dependency>
       <groupId>javax.inject</groupId>
@@ -485,6 +485,40 @@ under the License.
               <exclude>org.apache.maven.toolchain.ToolchainManagerPrivate</exclude>
               <exclude>org.apache.maven.toolchain.ToolchainPrivate</exclude>
               <exclude>org.apache.maven.toolchain.ToolchainsBuilder</exclude>
+              <!-- ClassRealm API changes for Maven 4 - return type changes from Plexus ClassRealm to Maven ClassRealm -->
+              <exclude>org.apache.maven.classrealm.ClassRealmManager#createExtensionRealm(org.apache.maven.model.Plugin,java.util.List):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.classrealm.ClassRealmManager#createPluginRealm(org.apache.maven.model.Plugin,java.lang.ClassLoader,java.util.List,java.util.Map,java.util.List):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.classrealm.ClassRealmManager#createProjectRealm(org.apache.maven.model.Model,java.util.List):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.classrealm.ClassRealmManager#getCoreRealm():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.classrealm.ClassRealmManager#getMavenApiRealm():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.classrealm.ClassRealmManagerDelegate#setupRealm(org.codehaus.plexus.classworlds.realm.ClassRealm,org.apache.maven.classrealm.ClassRealmRequest):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.classrealm.DefaultClassRealmManager#createExtensionRealm(org.apache.maven.model.Plugin,java.util.List):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.classrealm.DefaultClassRealmManager#createPluginRealm(org.apache.maven.model.Plugin,java.lang.ClassLoader,java.util.List,java.util.Map,java.util.List):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.classrealm.DefaultClassRealmManager#createProjectRealm(org.apache.maven.model.Model,java.util.List):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.classrealm.DefaultClassRealmManager#getCoreRealm():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.classrealm.DefaultClassRealmManager#getMavenApiRealm():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.plugin.BuildPluginManager#getPluginRealm(org.apache.maven.execution.MavenSession,org.apache.maven.plugin.descriptor.PluginDescriptor):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.plugin.DefaultBuildPluginManager#getPluginRealm(org.apache.maven.execution.MavenSession,org.apache.maven.plugin.descriptor.PluginDescriptor):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.plugin.ExtensionRealmCache#put(org.apache.maven.plugin.ExtensionRealmCache$Key,org.codehaus.plexus.classworlds.realm.ClassRealm,org.apache.maven.project.ExtensionDescriptor,java.util.List):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.ExtensionRealmCache$CacheRecord#getRealm():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.plugin.PluginContainerException#getPluginRealm():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.plugin.PluginContainerException#PluginContainerException(org.apache.maven.model.Plugin,org.codehaus.plexus.classworlds.realm.ClassRealm,java.lang.String,org.codehaus.plexus.configuration.PlexusConfigurationException):CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.PluginContainerException#PluginContainerException(org.apache.maven.plugin.descriptor.MojoDescriptor,org.codehaus.plexus.classworlds.realm.ClassRealm,java.lang.String,java.lang.Throwable):CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.PluginContainerException#PluginContainerException(org.apache.maven.model.Plugin,org.codehaus.plexus.classworlds.realm.ClassRealm,java.lang.String,org.codehaus.plexus.component.repository.exception.ComponentRepositoryException):CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.PluginContainerException#PluginContainerException(org.apache.maven.model.Plugin,org.codehaus.plexus.classworlds.realm.ClassRealm,java.lang.String,java.lang.Throwable):CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.PluginContainerException#PluginContainerException(org.apache.maven.plugin.descriptor.MojoDescriptor,org.codehaus.plexus.classworlds.realm.ClassRealm,java.lang.String,org.codehaus.plexus.component.repository.exception.ComponentLookupException):CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.PluginManagerException#PluginManagerException(org.apache.maven.plugin.descriptor.MojoDescriptor,org.apache.maven.project.MavenProject,java.lang.String,org.codehaus.plexus.classworlds.realm.NoSuchRealmException):CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.PluginRealmCache#put(org.apache.maven.plugin.PluginRealmCache$Key,org.codehaus.plexus.classworlds.realm.ClassRealm,java.util.List):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.PluginRealmCache$CacheRecord#getRealm():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.plugin.PluginRealmCache$CacheRecord#PluginRealmCache$CacheRecord(org.codehaus.plexus.classworlds.realm.ClassRealm,java.util.List):CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.project.MavenProject#getClassRealm():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.project.MavenProject#setClassRealm(org.codehaus.plexus.classworlds.realm.ClassRealm):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.project.ProjectRealmCache#put(org.apache.maven.project.ProjectRealmCache$Key,org.codehaus.plexus.classworlds.realm.ClassRealm,org.eclipse.aether.graph.DependencyFilter):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.project.ProjectRealmCache$CacheRecord#getRealm():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <!-- Additional cache-related method removals -->
+              <exclude>org.apache.maven.plugin.DefaultExtensionRealmCache#put(org.apache.maven.plugin.ExtensionRealmCache$Key,org.codehaus.plexus.classworlds.realm.ClassRealm,org.apache.maven.project.ExtensionDescriptor,java.util.List):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.DefaultPluginRealmCache#put(org.apache.maven.plugin.PluginRealmCache$Key,org.codehaus.plexus.classworlds.realm.ClassRealm,java.util.List):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.project.DefaultProjectRealmCache#put(org.apache.maven.project.ProjectRealmCache$Key,org.codehaus.plexus.classworlds.realm.ClassRealm,org.eclipse.aether.graph.DependencyFilter):METHOD_REMOVED</exclude>
             </excludes>
           </parameter>
         </configuration>
diff --git a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleDependencyResolver.java b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleDependencyResolver.java
index e25472ec92..d1ac674a7e 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleDependencyResolver.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleDependencyResolver.java
@@ -46,6 +46,7 @@
 import org.apache.maven.project.ProjectDependenciesResolver;
 import org.apache.maven.project.artifact.InvalidDependencyVersionException;
 import org.apache.maven.project.artifact.ProjectArtifactsCache;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
 import org.eclipse.aether.graph.Dependency;
 import org.eclipse.aether.graph.DependencyFilter;
 import org.eclipse.aether.graph.DependencyNode;
@@ -112,9 +113,12 @@ public void resolveProjectDependencies(
             throws LifecycleExecutionException {
         ClassLoader tccl = Thread.currentThread().getContextClassLoader();
         try {
-            ClassLoader projectRealm = project.getClassRealm();
-            if (projectRealm != null && projectRealm != tccl) {
-                Thread.currentThread().setContextClassLoader(projectRealm);
+            ClassRealm projectRealm = project.getClassRealm();
+            if (projectRealm != null) {
+                ClassLoader projectClassLoader = projectRealm;
+                if (projectClassLoader != tccl) {
+                    Thread.currentThread().setContextClassLoader(projectClassLoader);
+                }
             }
 
             if (project.getDependencyArtifacts() == null) {
diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java
index d881c31b38..124607c148 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java
@@ -430,6 +430,7 @@ private void createPluginRealm(
 
         pluginArtifacts = toMavenArtifacts(result);
 
+        // Classic classpath-based plugin
         pluginRealm = classRealmManager.createPluginRealm(
                 plugin, parent, null, foreignImports, toAetherArtifacts(pluginArtifacts));
 
@@ -488,7 +489,7 @@ private Map<String, ClassLoader> calcImports(
             MavenProject project, ClassLoader parent, List<String> imports, boolean v4api) {
         Map<String, ClassLoader> foreignImports = new HashMap<>();
 
-        ClassLoader projectRealm = project.getClassRealm();
+        ClassRealm projectRealm = project.getClassRealm();
         if (projectRealm != null) {
             foreignImports.put("", projectRealm);
         } else {
@@ -566,6 +567,7 @@ private <T> T loadV4Mojo(
         org.apache.maven.api.plugin.Log log = new DefaultLog(
                 LoggerFactory.getLogger(mojoExecution.getMojoDescriptor().getImplementation()));
         try {
+            // Discover components from the effective ClassLoader (module layer loader for modular plugins)
             Injector injector = Injector.create();
             injector.discover(pluginRealm);
             // Add known classes
@@ -579,7 +581,7 @@ private <T> T loadV4Mojo(
             services.forEach((itf, svc) -> injector.bindSupplier((Class<Service>) itf, (Supplier<Service>) svc));
 
             mojo = mojoInterface.cast(injector.getInstance(
-                    Key.of(mojoDescriptor.getImplementationClass(), mojoDescriptor.getRoleHint())));
+                    Key.of(pluginRealm.loadClass(mojoDescriptor.getImplementation()), mojoDescriptor.getRoleHint())));
 
         } catch (Exception e) {
             throw new PluginContainerException(mojoDescriptor, pluginRealm, "Unable to lookup Mojo", e);
diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuildingHelper.java b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuildingHelper.java
index 724a0c88b7..131dec8aed 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuildingHelper.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuildingHelper.java
@@ -279,13 +279,16 @@ private void disposeRealm(ClassRealm realm) {
 
     @Override
     public void selectProjectRealm(MavenProject project) {
-        ClassLoader projectRealm = project.getClassRealm();
+        ClassRealm projectRealm = project.getClassRealm();
 
+        ClassLoader classLoader;
         if (projectRealm == null) {
-            projectRealm = classRealmManager.getCoreRealm();
+            classLoader = classRealmManager.getCoreRealm();
+        } else {
+            classLoader = projectRealm;
         }
 
-        Thread.currentThread().setContextClassLoader(projectRealm);
+        Thread.currentThread().setContextClassLoader(classLoader);
     }
 
     private static boolean containsExpression(String value) {
diff --git a/impl/maven-core/src/main/resources/META-INF/maven/extension.xml b/impl/maven-core/src/main/resources/META-INF/maven/extension.xml
index 56667ed69e..570fece8f4 100644
--- a/impl/maven-core/src/main/resources/META-INF/maven/extension.xml
+++ b/impl/maven-core/src/main/resources/META-INF/maven/extension.xml
@@ -77,12 +77,9 @@ under the License.
     <exportedPackage>org.eclipse.aether.version</exportedPackage>
     <exportedPackage>org.eclipse.aether.util</exportedPackage>
 
-    <!-- plexus-classworlds -->
+    <!-- maven-classworlds -->
     <exportedPackage>org.codehaus.plexus.classworlds</exportedPackage>
 
-    <!-- classworlds (for legacy code) -->
-    <exportedPackage>org.codehaus.classworlds</exportedPackage>
-
     <!-- plexus-utils (for DOM-type fields in maven-model) -->
     <exportedPackage>org.codehaus.plexus.util.xml.Xpp3Dom</exportedPackage>
     <exportedPackage>org.codehaus.plexus.util.xml.Xpp3DomBuilder</exportedPackage>
@@ -154,7 +151,7 @@ under the License.
     <exportedArtifact>org.apache.maven:maven-api-xml</exportedArtifact>
 
     <exportedArtifact>classworlds:classworlds</exportedArtifact>
-    <exportedArtifact>org.codehaus.plexus:plexus-classworlds</exportedArtifact>
+    <exportedArtifact>org.apache.maven:maven-classworlds</exportedArtifact>
     <exportedArtifact>org.codehaus.plexus:plexus-component-api</exportedArtifact>
     <exportedArtifact>org.codehaus.plexus:plexus-container-default</exportedArtifact>
     <exportedArtifact>plexus:plexus-container-default</exportedArtifact>
diff --git a/impl/maven-core/src/test/java/org/apache/maven/classrealm/DefaultClassRealmManagerTest.java b/impl/maven-core/src/test/java/org/apache/maven/classrealm/DefaultClassRealmManagerTest.java
index 411a483b22..d604c1cb60 100644
--- a/impl/maven-core/src/test/java/org/apache/maven/classrealm/DefaultClassRealmManagerTest.java
+++ b/impl/maven-core/src/test/java/org/apache/maven/classrealm/DefaultClassRealmManagerTest.java
@@ -29,6 +29,7 @@
 import org.codehaus.plexus.DefaultPlexusContainer;
 import org.codehaus.plexus.PlexusContainer;
 import org.codehaus.plexus.PlexusContainerException;
+import org.codehaus.plexus.classworlds.ClassWorld;
 import org.codehaus.plexus.classworlds.realm.ClassRealm;
 import org.eclipse.aether.artifact.Artifact;
 import org.junit.jupiter.api.Test;
@@ -58,7 +59,8 @@ private DefaultClassRealmManager newDefaultClassRealmManager(PlexusContainer con
         return new DefaultClassRealmManager(
                 new DefaultCoreRealm(container),
                 new ArrayList<ClassRealmManagerDelegate>(),
-                new CoreExports(new ClassRealm(null, "test", null), new HashSet<String>(), exportedPackages));
+                new CoreExports(
+                        new ClassRealm(new ClassWorld(), "test", null), new HashSet<String>(), exportedPackages));
     }
 
     private List<Artifact> newTestArtifactList() {
diff --git a/impl/pom.xml b/impl/pom.xml
index b41717c555..d05a9ce287 100644
--- a/impl/pom.xml
+++ b/impl/pom.xml
@@ -31,6 +31,7 @@ under the License.
   <name>Maven 4 Implementation Modules</name>
 
   <modules>
+    <module>maven-classworlds</module>
     <module>maven-support</module>
     <module>maven-impl</module>
     <module>maven-di</module>
diff --git a/its/core-it-suite/src/test/resources/mng-5530-mojo-execution-scope/extension/src/main/java/org/apache/maven/its/mng5530/mojoexecutionscope/extension/TestClassRealmManagerDelegate.java b/its/core-it-suite/src/test/resources/mng-5530-mojo-execution-scope/extension/src/main/java/org/apache/maven/its/mng5530/mojoexecutionscope/extension/TestClassRealmManagerDelegate.java
index 6c064d2731..805e9a8ba6 100644
--- a/its/core-it-suite/src/test/resources/mng-5530-mojo-execution-scope/extension/src/main/java/org/apache/maven/its/mng5530/mojoexecutionscope/extension/TestClassRealmManagerDelegate.java
+++ b/its/core-it-suite/src/test/resources/mng-5530-mojo-execution-scope/extension/src/main/java/org/apache/maven/its/mng5530/mojoexecutionscope/extension/TestClassRealmManagerDelegate.java
@@ -35,10 +35,10 @@
 
 import javax.inject.Named;
 
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
 import org.apache.maven.classrealm.ClassRealmManagerDelegate;
 import org.apache.maven.classrealm.ClassRealmRequest;
 import org.apache.maven.classrealm.ClassRealmRequest.RealmType;
-import org.codehaus.plexus.classworlds.realm.ClassRealm;
 
 @Named
 public class TestClassRealmManagerDelegate implements ClassRealmManagerDelegate {
diff --git a/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions-no-descriptor/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java b/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions-no-descriptor/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java
index edb262ce2d..287c936c7e 100644
--- a/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions-no-descriptor/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java
+++ b/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions-no-descriptor/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java
@@ -18,9 +18,9 @@
  */
 package org.apache.maven.its.core_extensions;
 
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
 import org.apache.maven.classrealm.ClassRealmManagerDelegate;
 import org.apache.maven.classrealm.ClassRealmRequest;
-import org.codehaus.plexus.classworlds.realm.ClassRealm;
 import org.codehaus.plexus.component.annotations.Component;
 
 @Component(role = ClassRealmManagerDelegate.class, hint = "TestClassRealmManagerDelegate")
diff --git a/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java b/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java
index edb262ce2d..287c936c7e 100644
--- a/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java
+++ b/its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java
@@ -18,9 +18,9 @@
  */
 package org.apache.maven.its.core_extensions;
 
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
 import org.apache.maven.classrealm.ClassRealmManagerDelegate;
 import org.apache.maven.classrealm.ClassRealmRequest;
-import org.codehaus.plexus.classworlds.realm.ClassRealm;
 import org.codehaus.plexus.component.annotations.Component;
 
 @Component(role = ClassRealmManagerDelegate.class, hint = "TestClassRealmManagerDelegate")
diff --git a/pom.xml b/pom.xml
index 3a0c0f2eda..5e38f93636 100644
--- a/pom.xml
+++ b/pom.xml
@@ -144,7 +144,6 @@ under the License.
     <!-- various versions -->
     <asmVersion>9.11.1</asmVersion>
     <byteBuddyVersion>1.18.15</byteBuddyVersion>
-    <classWorldsVersion>2.12.1</classWorldsVersion>
     <commonsCliVersion>1.11.0</commonsCliVersion>
     <domtripVersion>1.8.0</domtripVersion>
     <guiceVersion>5.1.0</guiceVersion>
@@ -400,6 +399,10 @@ under the License.
             <groupId>org.apache.maven</groupId>
             <artifactId>maven-xml-impl</artifactId>
           </exclusion>
+          <exclusion>
+            <groupId>org.codehaus.plexus</groupId>
+            <artifactId>plexus-classworlds</artifactId>
+          </exclusion>
         </exclusions>
       </dependency>
       <dependency>
@@ -423,9 +426,9 @@ under the License.
         <version>1</version>
       </dependency>
       <dependency>
-        <groupId>org.codehaus.plexus</groupId>
-        <artifactId>plexus-classworlds</artifactId>
-        <version>${classWorldsVersion}</version>
+        <groupId>org.apache.maven</groupId>
+        <artifactId>maven-classworlds</artifactId>
+        <version>${project.version}</version>
       </dependency>
       <dependency>
         <groupId>org.codehaus.plexus</groupId>
@@ -648,6 +651,10 @@ under the License.
             <groupId>com.google.inject</groupId>
             <artifactId>guice</artifactId>
           </exclusion>
+          <exclusion>
+            <groupId>org.codehaus.plexus</groupId>
+            <artifactId>plexus-classworlds</artifactId>
+          </exclusion>
         </exclusions>
       </dependency>
       <dependency>
@@ -1364,7 +1371,7 @@ under the License.
           <plugin>
             <groupId>org.codehaus.mojo</groupId>
             <artifactId>versions-maven-plugin</artifactId>
-            <version>2.23.0</version>
+            <version>2.22.0</version>
             <configuration>
               <generateBackupPoms>false</generateBackupPoms>
               <allowMajorUpdates>false</allowMajorUpdates>
diff --git a/src/graph/ReactorGraph.java b/src/graph/ReactorGraph.java
index 4dc29fe734..162f14d603 100755
--- a/src/graph/ReactorGraph.java
+++ b/src/graph/ReactorGraph.java
@@ -42,7 +42,7 @@ public class ReactorGraph {
         CLUSTER_PATTERNS.put("JLine", Pattern.compile("^org\\.jline:.*"));
         CLUSTER_PATTERNS.put("Maven 4 API", Pattern.compile("^org\\.apache\\.maven:maven-api-(?!impl).*"));
         CLUSTER_PATTERNS.put("Maven Resolver 2", Pattern.compile("^org\\.apache\\.maven\\.resolver:.*"));
-        CLUSTER_PATTERNS.put("Maven 4 Implementation", Pattern.compile("^org\\.apache\\.maven:maven-(support|impl|di|core|cli|xml|jline|logging|executor|testing):.*"));
+        CLUSTER_PATTERNS.put("Maven 4 Implementation", Pattern.compile("^org\\.apache\\.maven:maven-(support|impl|di|core|cli|xml|jline|logging|executor|testing|classworlds):.*"));
         CLUSTER_PATTERNS.put("Maven 3 Compatibility", Pattern.compile("^org\\.apache\\.maven:maven-(artifact|builder-support|compat|embedder|model|model-builder|plugin-api|repository-metadata|resolver-provider|settings|settings-builder|toolchain-builder|toolchain-model):.*"));
         CLUSTER_PATTERNS.put("Sisu", Pattern.compile("(^org\\.eclipse\\.sisu:.*)|(.*:guice:.*)|(.*:javax.inject:.*)|(.*:javax.annotation-api:.*)|(.*:aopalliance:.*)"));
         CLUSTER_PATTERNS.put("Plexus", Pattern.compile("^org\\.codehaus\\.plexus:.*"));