You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by pa...@apache.org on 2021/10/27 14:26:00 UTC

[sling-jspc-maven-plugin] branch issues/SLING-10887 created (now ce58058)

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

pauls pushed a change to branch issues/SLING-10887
in repository https://gitbox.apache.org/repos/asf/sling-jspc-maven-plugin.git.


      at ce58058  SLING-10887: allow to load dependencies from a feature

This branch includes the following new commits:

     new ce58058  SLING-10887: allow to load dependencies from a feature

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[sling-jspc-maven-plugin] 01/01: SLING-10887: allow to load dependencies from a feature

Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

pauls pushed a commit to branch issues/SLING-10887
in repository https://gitbox.apache.org/repos/asf/sling-jspc-maven-plugin.git

commit ce580583fb4aed137397719ec8f0d3d18232d128
Author: Karl Pauls <ka...@gmail.com>
AuthorDate: Wed Oct 27 16:25:49 2021 +0200

    SLING-10887: allow to load dependencies from a feature
---
 pom.xml                                            |  23 +-
 .../apache/sling/maven/jspc/FeatureSupport.java    | 187 +++++++++++
 .../java/org/apache/sling/maven/jspc/JspcMojo.java | 214 ++++++++++---
 .../sling/maven/jspc/TrackingClassLoader.java      |  43 ++-
 .../maven/jspc/classloader/ClassLoaderFacade.java  | 167 ++++++++++
 .../DynamicClassLoaderManagerFactory.java          | 105 +++++++
 .../classloader/DynamicClassLoaderManagerImpl.java | 116 +++++++
 .../jspc/classloader/PackageAdminClassLoader.java  | 344 +++++++++++++++++++++
 .../sling/maven/jspc/JspcMojoFeatureTest.java      | 212 +++++++++++++
 .../feature-test.json                              |   4 +
 .../jspc-maven-plugin-it-includes/pom-feature.xml  |  65 ++++
 .../0.0.1/jspc-maven-plugin-it-deps-0.0.1.jar      | Bin 1434 -> 1508 bytes
 12 files changed, 1437 insertions(+), 43 deletions(-)

diff --git a/pom.xml b/pom.xml
index 4876eff..08fb36a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -153,7 +153,24 @@
     </dependencyManagement>
 
     <dependencies>
-
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>7.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>1.2.28</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.cm.json</artifactId>
+            <version>1.0.6</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.scripting.jsp</artifactId>
@@ -269,5 +286,9 @@
             <scope>test</scope>
             <version>${maven.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-compat</artifactId>
+        </dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/maven/jspc/FeatureSupport.java b/src/main/java/org/apache/sling/maven/jspc/FeatureSupport.java
new file mode 100644
index 0000000..fb00432
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/jspc/FeatureSupport.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.maven.jspc;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.ArtifactProvider;
+import org.apache.sling.maven.jspc.classloader.DynamicClassLoaderManagerFactory;
+import org.apache.sling.maven.jspc.classloader.DynamicClassLoaderManagerImpl;
+import org.apache.sling.scripting.jsp.SlingTldLocationsCache;
+import org.apache.sling.scripting.jsp.jasper.compiler.TldLocationsCache;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+import org.osgi.framework.wiring.FrameworkWiring;
+import org.osgi.service.packageadmin.PackageAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.function.Function;
+
+
+public class FeatureSupport {
+    private final Framework framework;
+    private final ClassLoader loader;
+    private final TldLocationsCache locationsCache;
+    private final Feature feature;
+
+    public FeatureSupport(Framework framework, ClassLoader loader, TldLocationsCache locationsCache, Feature feature) {
+        this.framework = framework;
+        this.loader = loader;
+        this.locationsCache = locationsCache;
+        this.feature = feature;
+    }
+
+    public ClassLoader getClassLoader() {
+        return loader;
+    }
+
+    public TldLocationsCache getTldLocationsCache() {
+        return locationsCache;
+    }
+
+    public Feature getFeature() {
+        return feature;
+    }
+
+    public static FeatureSupport createFeatureSupport(Feature feature, ArtifactProvider provider, ClassLoader loader, boolean failOnUnresolvedBundles,
+            Function<Map<String, String>, Map<String, String>> frameworkPropertiesHandler) throws BundleException {
+        FrameworkFactory factory = ServiceLoader.load(FrameworkFactory.class, loader).iterator().next();
+
+        Map<String, String> properties = new HashMap<>(feature.getFrameworkProperties());
+
+        properties = frameworkPropertiesHandler.apply(properties);
+
+        Framework framework = factory.newFramework(properties);
+
+        framework.init();
+
+        try {
+
+            List<Bundle> bundles = install(framework, feature, provider);
+
+            framework.start();
+
+            for (Bundle bundle : bundles) {
+                if (bundle.getState() != Bundle.RESOLVED) {
+                    boolean resolved = framework.adapt(FrameworkWiring.class).resolveBundles(Arrays.asList(bundle));
+                    if (!resolved && failOnUnresolvedBundles) {
+                        throw new BundleException("Unable to resolve bundle: " + bundle.getLocation());
+                    }
+                }
+            }
+
+            ServiceTracker<PackageAdmin, PackageAdmin> tracker = new ServiceTracker<>(framework.getBundleContext(), PackageAdmin.class, null);
+            tracker.open();
+
+            PackageAdmin admin = tracker.waitForService(1000);
+
+            DynamicClassLoaderManagerImpl manager = new DynamicClassLoaderManagerImpl(framework.getBundleContext(), admin, loader,
+                    new DynamicClassLoaderManagerFactory(framework.getBundleContext(), admin));
+            SlingTldLocationsCache tldLocationsCache = new SlingTldLocationsCache(framework.getBundleContext());
+
+            FeatureSupport featureSupport = new FeatureSupport(framework, manager.getDynamicClassLoader(), tldLocationsCache, feature);
+
+            return featureSupport;
+        } catch (Throwable t) {
+            try {
+                framework.stop();
+                framework.waitForStop(10000);
+            } finally {
+                throw new RuntimeException(t);
+            }
+        }
+    }
+
+    private static List<Bundle> install(final Framework framework, final Feature feature, final ArtifactProvider provider) throws BundleException {
+        final BundleContext bc = framework.getBundleContext();
+        int defaultStartLevel = getProperty(bc, "felix.startlevel.bundle", 1);
+        Map<Integer, List<Artifact>> bundlesByStartOrder = feature.getBundles().getBundlesByStartOrder();
+        List<Bundle> bundles = new ArrayList<>();
+        for(final Integer startLevel : sortStartLevels(bundlesByStartOrder.keySet(), defaultStartLevel)) {
+            for(final Artifact bundleArtifact : bundlesByStartOrder.get(startLevel)) {
+                URL url = provider.provide(bundleArtifact.getId());
+                // use reference protocol if possible. This avoids copying the binary to the cache directory
+                // of the framework
+                String location = "";
+                if (url.getProtocol().equals("file")) {
+                    location = "reference:";
+                }
+                location = location + url.toString();
+
+                final Bundle bundle = bc.installBundle(location, null);
+                if (!isSystemBundleFragment(bundle) && getFragmentHostHeader(bundle) == null) {
+                    bundles.add(bundle);
+                }
+            }
+        }
+        return bundles;
+    }
+
+    private static int getProperty(BundleContext bc, String propName, int defaultValue) {
+        String val = bc.getProperty(propName);
+        if (val == null) {
+            return defaultValue;
+        } else {
+            return Integer.parseInt(val);
+        }
+    }
+
+    /**
+     * Sort the start levels in the ascending order. The only exception is the start level
+     * "0", which should be put at the position configured in {@code felix.startlevel.bundle}.
+     *
+     * @param startLevels integer start levels
+     * @return sorted start levels
+     */
+    private static Iterable<Integer> sortStartLevels(final Collection<Integer> startLevels, final int defaultStartLevel) {
+        final List<Integer> result = new ArrayList<>(startLevels);
+        Collections.sort(result, (o1, o2) -> {
+            int i1 = o1 == 0 ? defaultStartLevel : o1;
+            int i2 = o2 == 0 ? defaultStartLevel : o2;
+            return Integer.compare(i1, i2);
+        });
+        return result;
+    }
+
+    private static boolean isSystemBundleFragment(final Bundle installedBundle) {
+        final String fragmentHeader = getFragmentHostHeader(installedBundle);
+        return fragmentHeader != null
+                && fragmentHeader.indexOf(Constants.EXTENSION_DIRECTIVE) > 0;
+    }
+
+    private static String getFragmentHostHeader(final Bundle b) {
+        return b.getHeaders().get( Constants.FRAGMENT_HOST );
+    }
+
+    public void shutdown(long timeout) throws Exception {
+        framework.stop();
+        framework.waitForStop(timeout);
+    }
+}
diff --git a/src/main/java/org/apache/sling/maven/jspc/JspcMojo.java b/src/main/java/org/apache/sling/maven/jspc/JspcMojo.java
index b894452..aa61692 100644
--- a/src/main/java/org/apache/sling/maven/jspc/JspcMojo.java
+++ b/src/main/java/org/apache/sling/maven/jspc/JspcMojo.java
@@ -18,17 +18,21 @@ package org.apache.sling.maven.jspc;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.jar.JarFile;
 
 import javax.json.Json;
@@ -42,17 +46,29 @@ import javax.json.stream.JsonGenerator;
 import org.apache.commons.logging.impl.LogFactoryImpl;
 import org.apache.commons.logging.impl.SimpleLog;
 import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
 import org.apache.maven.artifact.DependencyResolutionRequiredException;
+import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
+import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
+import org.apache.maven.artifact.resolver.ArtifactResolutionException;
+import org.apache.maven.artifact.versioning.VersionRange;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.Dependency;
 import org.apache.maven.plugin.AbstractMojo;
 import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Component;
 import org.apache.maven.plugins.annotations.LifecyclePhase;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.plugins.annotations.ResolutionScope;
 import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.MavenProjectHelper;
 import org.apache.sling.commons.classloader.ClassLoaderWriter;
 import org.apache.sling.commons.compiler.JavaCompiler;
 import org.apache.sling.commons.compiler.impl.EclipseJavaCompiler;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.io.json.FeatureJSONReader;
 import org.apache.sling.scripting.jsp.jasper.Constants;
 import org.apache.sling.scripting.jsp.jasper.JasperException;
 import org.apache.sling.scripting.jsp.jasper.JspCompilationContext;
@@ -64,6 +80,7 @@ import org.apache.sling.scripting.jsp.jasper.compiler.TagPluginManager;
 import org.apache.sling.scripting.jsp.jasper.compiler.TldLocationsCache;
 import org.codehaus.plexus.util.DirectoryScanner;
 import org.codehaus.plexus.util.StringUtils;
+import org.apache.maven.artifact.resolver.ArtifactResolver;
 
 /**
  * The <code>JspcMojo</code> is implements the Sling Maven JspC goal
@@ -211,6 +228,21 @@ public class JspcMojo extends AbstractMojo implements Options {
     @Parameter
     private String[] excludes;
 
+    @Parameter
+    private FeatureDependency feature;
+
+    @Parameter(property = "session", defaultValue = "${session}", readonly = true, required = true)
+    protected MavenSession mavenSession;
+
+    @Component
+    protected MavenProjectHelper projectHelper;
+
+    @Component
+    ArtifactHandlerManager artifactHandlerManager;
+
+    @Component
+    ArtifactResolver artifactResolver;
+
     private String uriSourceRoot;
 
     private List<String> pages = new ArrayList<String>();
@@ -228,6 +260,8 @@ public class JspcMojo extends AbstractMojo implements Options {
      */
     private TldLocationsCache tldLocationsCache;
 
+    private FeatureSupport featureSupport;
+
     private JspConfig jspConfig;
 
     private TagPluginManager tagPluginManager;
@@ -236,6 +270,13 @@ public class JspcMojo extends AbstractMojo implements Options {
 
     private DependencyTracker dependencyTracker;
 
+    public static final class FeatureDependency {
+        public Artifact featureId;
+        public String featureFile;
+        public List<Dependency> dependencies;
+        public boolean failOnUnresolved;
+    }
+
     /*
      * (non-Javadoc)
      *
@@ -294,6 +335,15 @@ public class JspcMojo extends AbstractMojo implements Options {
             } else {
                 System.setProperty(Constants.JSP_PACKAGE_NAME_PROPERTY_NAME, previousJasperPackageName);
             }
+            if (featureSupport != null) {
+                try {
+                    featureSupport.shutdown(10000);
+                } catch (Exception e) {
+                    getLog().error(e);
+                }
+
+                featureSupport = null;
+            }
         }
 
         project.addCompileSourceRoot(outputDirectory);
@@ -325,11 +375,35 @@ public class JspcMojo extends AbstractMojo implements Options {
         }
 
         try {
+            if (featureSupport == null && feature != null) {
+                File target = null;
+                if (feature.featureId != null) {
+                    target = getOrResolveArtifact(project, mavenSession, artifactHandlerManager, artifactResolver, ArtifactId.fromMvnId(feature.featureId.getId())).getFile();
+                } else if (feature.featureFile != null && ! feature.featureFile.trim().isEmpty()){
+                    target = new File(feature.featureFile);
+                }
+                if (target != null && target.isFile()) {
+                    try (Reader reader = new InputStreamReader(Files.newInputStream(target.toPath()), "UTF-8")) {
+                        Feature assembled = FeatureJSONReader.read(reader, target.getAbsolutePath());
+                        featureSupport = FeatureSupport.createFeatureSupport(assembled, artifactId -> {
+                            try {
+                                return getOrResolveArtifact(project, mavenSession, artifactHandlerManager, artifactResolver, artifactId).getFile().toURI().toURL();
+                            } catch (Exception e) {
+                                throw new RuntimeException(e);
+                            }
+                        }, this.getClass().getClassLoader(), feature.failOnUnresolved, properties -> {
+                            properties.put(org.osgi.framework.Constants.FRAMEWORK_STORAGE_CLEAN, org.osgi.framework.Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT);
+                            properties.put(org.osgi.framework.Constants.FRAMEWORK_STORAGE, new File(project.getBuild().getDirectory(), "featuresupport").getAbsolutePath());
+                            properties.put(org.osgi.framework.Constants.FRAMEWORK_BOOTDELEGATION, "*");
+                            return properties;
+                        });
+                    }
+                }
+            }
             if (context == null) {
                 initServletContext();
             }
 
-
             if (printCompilationReport || generateCompilationReport) {
                 dependencyTracker = new DependencyTracker(
                         getLog(),
@@ -376,6 +450,14 @@ public class JspcMojo extends AbstractMojo implements Options {
                     try {
                         processFile(nextjsp);
                     } catch (JasperException e) {
+                        Throwable rootCause = e;
+                        while (rootCause instanceof JasperException
+                                && ((JasperException) rootCause).getRootCause() != null) {
+                            rootCause = ((JasperException) rootCause).getRootCause();
+                        }
+                        if (rootCause != e) {
+                            rootCause.printStackTrace();
+                        }
                         throw new RuntimeException(e);
                     }
                 }
@@ -383,18 +465,6 @@ public class JspcMojo extends AbstractMojo implements Options {
             if (dependencyTracker != null) {
                 dependencyTracker.processCompileDependencies();
             }
-
-        } catch (JasperException je) {
-            Throwable rootCause = je;
-            while (rootCause instanceof JasperException
-                && ((JasperException) rootCause).getRootCause() != null) {
-                rootCause = ((JasperException) rootCause).getRootCause();
-            }
-            if (rootCause != je) {
-                rootCause.printStackTrace();
-            }
-            throw je;
-
         } catch (/* IO */Exception ioe) {
             throw new JasperException(ioe);
         }
@@ -435,8 +505,11 @@ public class JspcMojo extends AbstractMojo implements Options {
             // just log otherwise
             getLog().error(je.getMessage());
 
-        } catch (Exception e) {
-            throw new JasperException(e);
+        } catch (Throwable e) {
+            if (failOnError) {
+                throw new JasperException(e);
+            }
+            getLog().error(e.getMessage(), e);
         }
     }
 
@@ -454,7 +527,7 @@ public class JspcMojo extends AbstractMojo implements Options {
             context.addAlternativeBaseURL(altUrl);
         }
 
-        tldLocationsCache = new JspCTldLocationsCache(context, true, loader);
+        tldLocationsCache = featureSupport != null ? featureSupport.getTldLocationsCache() : new JspCTldLocationsCache(context, true, loader);
 
         JavaCompiler compiler = new EclipseJavaCompiler();
         ClassLoaderWriter writer = new JspCClassLoaderWriter(loader, new File(outputDirectory));
@@ -478,32 +551,49 @@ public class JspcMojo extends AbstractMojo implements Options {
         final String targetDirectory = project.getBuild().getOutputDirectory();
         classPath.add(new File(targetDirectory).toURI().toURL());
 
-        // add artifacts from project
-        Set<Artifact> artifacts = project.getArtifacts();
-        jspcCompileArtifacts = new ArrayList<Artifact>(artifacts.size());
-        for (Artifact a: artifacts) {
-            final String scope = a.getScope();
-            if ("provided".equals(scope) || "runtime".equals(scope) || "compile".equals(scope)) {
-                // we need to exclude the javax.servlet.jsp API, otherwise the taglib parser causes problems (see note below)
-                if (containsProblematicPackage(a.getFile())) {
-                    continue;
+
+        jspcCompileArtifacts = new ArrayList<>();
+
+        if (featureSupport != null) {
+            if (feature.dependencies != null) {
+                for (Dependency dependency : feature.dependencies) {
+                    Artifact artifact = getOrResolveArtifact(project, mavenSession, artifactHandlerManager, artifactResolver, new ArtifactId(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), dependency.getClassifier(), dependency.getType()));
+                    classPath.add(artifact.getFile().toURI().toURL());
+                    jspcCompileArtifacts.add(artifact);
+                }
+            }
+            featureSupport.getFeature().getBundles().stream()
+                    .map(bundle -> bundle.getId())
+                    .map(dependency -> getOrResolveArtifact(project, mavenSession, artifactHandlerManager, artifactResolver, new ArtifactId(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), dependency.getClassifier(), dependency.getType())))
+                    .forEachOrdered(jspcCompileArtifacts::add);
+
+            loader = new TrackingClassLoader(classPath.toArray(new URL[classPath.size()]), featureSupport.getClassLoader());
+        } else {
+            // add artifacts from project
+            Set<Artifact> artifacts = project.getArtifacts();
+            for (Artifact a: artifacts) {
+                final String scope = a.getScope();
+                if ("provided".equals(scope) || "runtime".equals(scope) || "compile".equals(scope)) {
+                    // we need to exclude the javax.servlet.jsp API, otherwise the taglib parser causes problems (see note below)
+                    if (containsProblematicPackage(a.getFile())) {
+                        continue;
+                    }
+                    classPath.add(a.getFile().toURI().toURL());
+                    jspcCompileArtifacts.add(a);
                 }
-                classPath.add(a.getFile().toURI().toURL());
-                jspcCompileArtifacts.add(a);
             }
-        }
 
-        if (getLog().isDebugEnabled()) {
-            getLog().debug("Compiler classpath:");
-            for (URL u: classPath) {
-                getLog().debug("  " + u);
+            if (getLog().isDebugEnabled()) {
+                getLog().debug("Compiler classpath:");
+                for (URL u: classPath) {
+                    getLog().debug("  " + u);
+                }
             }
+            // this is dangerous to use this classloader as parent as the compilation will depend on the classes provided
+            // in the plugin dependencies. but if we omit this, we get errors by not being able to load the TagExtraInfo classes.
+            // this is because this plugin uses classes from the javax.servlet.jsp that are also loaded via the TLDs.
+            loader = new TrackingClassLoader(classPath.toArray(new URL[classPath.size()]), this.getClass().getClassLoader());
         }
-
-        // this is dangerous to use this classloader as parent as the compilation will depend on the classes provided
-        // in the plugin dependencies. but if we omit this, we get errors by not being able to load the TagExtraInfo classes.
-        // this is because this plugin uses classes from the javax.servlet.jsp that are also loaded via the TLDs.
-        loader = new TrackingClassLoader(classPath.toArray(new URL[classPath.size()]), this.getClass().getClassLoader());
     }
 
     /**
@@ -852,4 +942,54 @@ public class JspcMojo extends AbstractMojo implements Options {
         // Display the source fragment on errors for maven compilation
         return true;
     }
+
+    private static final ConcurrentHashMap<String, Artifact> cache = new ConcurrentHashMap<>();
+
+    public static Artifact getOrResolveArtifact(final MavenProject project,
+                                                final MavenSession session,
+                                                final ArtifactHandlerManager artifactHandlerManager,
+                                                final ArtifactResolver resolver,
+                                                final ArtifactId id) {
+        @SuppressWarnings("unchecked")
+        Artifact result = cache.get(id.toMvnId());
+        if ( result == null ) {
+            result = findArtifact(id, project.getAttachedArtifacts());
+            if ( result == null ) {
+                result = findArtifact(id, project.getArtifacts());
+                if ( result == null ) {
+                    final Artifact prjArtifact = new DefaultArtifact(id.getGroupId(),
+                            id.getArtifactId(),
+                            VersionRange.createFromVersion(id.getVersion()),
+                            Artifact.SCOPE_PROVIDED,
+                            id.getType(),
+                            id.getClassifier(),
+                            artifactHandlerManager.getArtifactHandler(id.getType()));
+                    try {
+                        resolver.resolve(prjArtifact, project.getRemoteArtifactRepositories(), session.getLocalRepository());
+                    } catch (final ArtifactResolutionException | ArtifactNotFoundException e) {
+                        throw new RuntimeException("Unable to get artifact for " + id.toMvnId(), e);
+                    }
+                    result = prjArtifact;
+                }
+            }
+            cache.put(id.toMvnId(), result);
+        }
+
+        return result;
+    }
+
+    private static Artifact findArtifact(final ArtifactId id, final Collection<Artifact> artifacts) {
+        if (artifacts != null) {
+            for(final Artifact artifact : artifacts) {
+                if ( artifact.getGroupId().equals(id.getGroupId())
+                        && artifact.getArtifactId().equals(id.getArtifactId())
+                        && artifact.getVersion().equals(id.getVersion())
+                        && artifact.getType().equals(id.getType())
+                        && ((id.getClassifier() == null && artifact.getClassifier() == null) || (id.getClassifier() != null && id.getClassifier().equals(artifact.getClassifier()))) ) {
+                    return artifact.getFile() == null ? null : artifact;
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/src/main/java/org/apache/sling/maven/jspc/TrackingClassLoader.java b/src/main/java/org/apache/sling/maven/jspc/TrackingClassLoader.java
index 989c47b..e721f34 100644
--- a/src/main/java/org/apache/sling/maven/jspc/TrackingClassLoader.java
+++ b/src/main/java/org/apache/sling/maven/jspc/TrackingClassLoader.java
@@ -16,15 +16,16 @@
  */
 package org.apache.sling.maven.jspc;
 
-import java.io.InputStream;
+import org.osgi.framework.BundleReference;
+
+import java.io.IOException;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.util.Collections;
+import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Set;
 
-import org.codehaus.plexus.util.StringUtils;
-
 /**
  * Classloader that tracks which classes are loaded.
  */
@@ -59,13 +60,46 @@ public class TrackingClassLoader extends URLClassLoader {
      */
     @Override
     public Class<?> loadClass(final String name) throws ClassNotFoundException {
-        final Class<?> c = super.loadClass(name);
+        return loadClass(name, false);
+    }
+
+    /**
+     * @see java.lang.ClassLoader#loadClass(java.lang.String,boolean)
+     */
+    @Override
+    public Class<?> loadClass(final String name, boolean resolve) throws ClassNotFoundException {
+        final Class<?> c = super.loadClass(name, resolve);
+        if (c.getClassLoader() instanceof BundleReference) {
+            this.classNames.add(name);
+            this.packageNames.add(c.getPackage().getName());
+        }
+        return c;
+    }
+
+    @Override
+    protected Class<?> findClass(String name) throws ClassNotFoundException {
+        final Class<?> c = super.findClass(name);
         this.classNames.add(name);
         this.packageNames.add(c.getPackage().getName());
         return c;
     }
 
     @Override
+    public URL getResource(String name) {
+        final URL url = super.getResource(name);
+        if (url != null && "bundle".equals(url.getProtocol()) && name.endsWith(".class")) {
+            int lastDot = name.lastIndexOf('.');
+            int lastSlash = name.lastIndexOf('/');
+            String className = name.substring(0, lastDot).replaceAll("/", ".");
+            classNames.add(className);
+            if (lastSlash > 0) {
+                packageNames.add(className.substring(0, lastSlash));
+            }
+        }
+        return url;
+    }
+
+    @Override
     public URL findResource(String name) {
         final URL url = super.findResource(name);
         if (url != null && name.endsWith(".class")) {
@@ -79,5 +113,4 @@ public class TrackingClassLoader extends URLClassLoader {
         }
         return url;
     }
-
 }
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/maven/jspc/classloader/ClassLoaderFacade.java b/src/main/java/org/apache/sling/maven/jspc/classloader/ClassLoaderFacade.java
new file mode 100644
index 0000000..22756d5
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/jspc/classloader/ClassLoaderFacade.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.maven.jspc.classloader;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+
+import org.apache.sling.commons.classloader.DynamicClassLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>ClassLoaderFacade</code> is a facade
+ * for the dynamic class loading.
+ * This class loader is returned to the clients of the
+ * dynamic class loader manager.
+ * This class loader delegates to other class loaders
+ * but caches its result for performance.
+ */
+public class ClassLoaderFacade extends ClassLoader implements DynamicClassLoader {
+
+    /** The logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
+
+    /** Dynamic class loader manager which manages the dynamic class loader providers for this facade. */
+    private final DynamicClassLoaderManagerImpl manager;
+
+    /** Used to log stack traces in slf4j for non-critical errors */
+    @SuppressWarnings("serial")
+    static class StackTraceProbe extends Exception {
+        StackTraceProbe(String reason) {
+            super(reason);
+        }
+    }
+
+    /**
+     * Constructor
+     */
+    public ClassLoaderFacade(final DynamicClassLoaderManagerImpl manager) {
+        this.manager = manager;
+    }
+
+    /** Return false if our manager is not active, and log the stack trace
+     *  when that happens, to ease troubleshooting.
+     */
+    private boolean checkManagerActive() {
+        if(!this.manager.isActive()) {
+            final String msg = "Dynamic class loader has already been deactivated.";
+            final StackTraceProbe p = new StackTraceProbe(msg);
+            logger.error(msg, p);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * @see java.lang.ClassLoader#getResource(java.lang.String)
+     */
+    @Override
+    public URL getResource(String name) {
+        if (!checkManagerActive()) {
+            return null;
+        }
+        final ClassLoader[] loaders = manager.getDynamicClassLoaders();
+        for(final ClassLoader cl : loaders) {
+            if ( cl != null ) {
+                try {
+                    final URL u = cl.getResource(name);
+                    if ( u != null ) {
+                        return u;
+                    }
+                } catch (Throwable t) {
+                    logger.error("Exception while querying class loader " + cl + " for resource " + name, t);
+                }
+            }
+        }
+        return null;
+    }
+
+    private final List<URL> EMPTY_LIST = Collections.emptyList();
+
+    /**
+     * @see java.lang.ClassLoader#getResources(java.lang.String)
+     */
+    @Override
+    public Enumeration<URL> getResources(String name) throws IOException {
+        if(!checkManagerActive()) {
+            return Collections.enumeration(EMPTY_LIST);
+        }
+        final ClassLoader[] loaders = manager.getDynamicClassLoaders();
+        final List<URL> resources = new ArrayList<URL>();
+        for(final ClassLoader cl : loaders) {
+            if ( cl != null ) {
+                try {
+                    final Enumeration<URL> e = cl.getResources(name);
+                    if ( e != null && e.hasMoreElements() ) {
+                        resources.addAll(Collections.list(e));
+                    }
+                } catch (Throwable t) {
+                    logger.error("Exception while querying class loader " + cl + " for resources " + name, t);
+                }
+            }
+        }
+        return Collections.enumeration(resources);
+    }
+
+    /**
+     * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean)
+     */
+    @Override
+    protected synchronized Class<?> loadClass(String name, boolean resolve)
+            throws ClassNotFoundException {
+        if(!checkManagerActive()) {
+            throw new ClassNotFoundException(name);
+        }
+        final ClassLoader[] loaders = manager.getDynamicClassLoaders();
+        for(final ClassLoader cl : loaders) {
+            if ( cl != null ) {
+                try {
+                    final Class<?> c = cl.loadClass(name);
+                    return c;
+                } catch (ClassNotFoundException cnfe) {
+                    // we just ignore this and try the next class loader
+                } catch (Throwable t) {
+                    logger.error("Exception while trying to load class " + name + " from class loader " + cl, t);
+                }
+            }
+        }
+        if (name.startsWith("sun.reflect.") || name.startsWith("jdk.internal.reflect.")) {
+            try {
+                return ClassLoader.getSystemClassLoader().loadClass(name);
+            } catch (ClassNotFoundException cnfe) {
+                // we just ignore this and throw our own exception
+            } catch (Throwable t) {
+                logger.error("Exception while trying to load class " + name + " from class loader " + ClassLoader.getSystemClassLoader(), t);
+            }
+        }
+        throw new ClassNotFoundException(name);
+    }
+
+    /**
+     * @see org.apache.sling.commons.classloader.DynamicClassLoader#isLive()
+     */
+    public boolean isLive() {
+        return this.manager.isActive();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/maven/jspc/classloader/DynamicClassLoaderManagerFactory.java b/src/main/java/org/apache/sling/maven/jspc/classloader/DynamicClassLoaderManagerFactory.java
new file mode 100644
index 0000000..589b883
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/jspc/classloader/DynamicClassLoaderManagerFactory.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.maven.jspc.classloader;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.packageadmin.ExportedPackage;
+import org.osgi.service.packageadmin.PackageAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is the service factory for the dynamic class loader manager.
+ */
+public class DynamicClassLoaderManagerFactory {
+
+    /** The logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** The package admin. */
+    private final PackageAdmin pckAdmin;
+
+    /** The bundle context. */
+    private final BundleContext context;
+
+    private final Set<Long> usedBundles = Collections.synchronizedSet(new HashSet<Long>());
+
+    private final Set<String> unresolvedPackages = Collections.synchronizedSet(new HashSet<String>());
+
+    /**
+     * Create a new service instance
+     * @param ctx The bundle context.
+     * @param pckAdmin The package admin.
+     */
+    public DynamicClassLoaderManagerFactory(final BundleContext ctx,
+                                            final PackageAdmin pckAdmin) {
+        this.context = ctx;
+        this.pckAdmin = pckAdmin;
+    }
+
+    /**
+     * Check if a bundle has been used for class loading.
+     * @param bundleId The bundle id.
+     * @return <code>true</code> if the bundle has been used.
+     */
+    public boolean isBundleUsed(final long bundleId) {
+        return usedBundles.contains(bundleId);
+    }
+
+    /**
+     * Notify that a bundle is used as a source for class loading.
+     * @param bundle The bundle.
+     */
+    public void addUsedBundle(final Bundle bundle) {
+        final long id = bundle.getBundleId();
+        this.usedBundles.add(id);
+    }
+
+    /**
+     * Notify that a package is not found during class loading.
+     * @param pckName The package name.
+     */
+    public void addUnresolvedPackage(final String pckName) {
+        this.unresolvedPackages.add(pckName);
+    }
+
+    /**
+     * Check if an exported package from the bundle has not been
+     * found during previous class loading attempts.
+     * @param bundle The bundle to check
+     * @return <code>true</code> if a package has not be found before
+     */
+    public boolean hasUnresolvedPackages(final Bundle bundle) {
+        if ( !this.unresolvedPackages.isEmpty() ) {
+            final ExportedPackage[] pcks = this.pckAdmin.getExportedPackages(bundle);
+            if ( pcks != null ) {
+                for(final ExportedPackage pck : pcks ) {
+                    if ( this.unresolvedPackages.contains(pck.getName()) ) {
+                        logger.debug("Bundle '{}' provides package '{}' which has been previously unsuccessfully requested!", bundle, pck.getName());
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/maven/jspc/classloader/DynamicClassLoaderManagerImpl.java b/src/main/java/org/apache/sling/maven/jspc/classloader/DynamicClassLoaderManagerImpl.java
new file mode 100644
index 0000000..9f4e597
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/jspc/classloader/DynamicClassLoaderManagerImpl.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.maven.jspc.classloader;
+
+import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
+import org.apache.sling.commons.classloader.DynamicClassLoaderProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.packageadmin.PackageAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is the default implementation of the dynamic class loader
+ * manager.
+ */
+public class DynamicClassLoaderManagerImpl
+        implements DynamicClassLoaderManager {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** The class loaders */
+    private final ClassLoader[] loaders;
+
+    /** The dynamic class loader. */
+    private final ClassLoaderFacade facade;
+
+    /** Is this still active? */
+    private volatile boolean active = true;
+
+    private final ServiceTracker deprecatedProviderTracker;
+
+    /**
+     * Create a new service instance
+     * @param ctx The bundle context of the class loader bundle
+     * @param pckAdmin The package admin.
+     * @param parent The parent class loader.
+     */
+    public DynamicClassLoaderManagerImpl(final BundleContext ctx,
+                                         final PackageAdmin pckAdmin,
+                                         final ClassLoader parent,
+                                         final DynamicClassLoaderManagerFactory factory) {
+        this.deprecatedProviderTracker = new ServiceTracker(ctx, DynamicClassLoaderProvider.class.getName(),
+                new ServiceTrackerCustomizer() {
+
+                    public void removedService(final ServiceReference serviceRef,
+                                               final Object paramObject) {
+                        ctx.ungetService(serviceRef);
+                    }
+
+                    public void modifiedService(final ServiceReference serviceRef,
+                                                final Object paramObject) {
+                        // nothing to do
+                    }
+
+                    public Object addingService(final ServiceReference serviceRef) {
+                        final Object obj = ctx.getService(serviceRef);
+                        if ( obj != null ) {
+                            logger.warn("Dynamic class loader does not support deprecated dynamic class loader providers: {} : {}",
+                                    serviceRef, obj);
+                        }
+                        return obj;
+                    }
+                });
+        this.deprecatedProviderTracker.open();
+        this.loaders = new ClassLoader[] {new PackageAdminClassLoader(pckAdmin, parent, factory)};
+        this.facade = new ClassLoaderFacade(this);
+    }
+
+    /**
+     * Deactivate this service.
+     */
+    public void deactivate() {
+        this.deprecatedProviderTracker.close();
+        this.active = false;
+    }
+
+    /**
+     * Check if this service is still active.
+     */
+    public boolean isActive() {
+        return this.active;
+    }
+
+    /**
+     * @see org.apache.sling.commons.classloader.DynamicClassLoaderManager#getDynamicClassLoader()
+     */
+    public ClassLoader getDynamicClassLoader() {
+        return this.facade;
+    }
+
+    /**
+     * Return the dynamic class loaders to use
+     * Currently this is just the package admin class loader.
+     */
+    public ClassLoader[] getDynamicClassLoaders() {
+        return this.loaders;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/maven/jspc/classloader/PackageAdminClassLoader.java b/src/main/java/org/apache/sling/maven/jspc/classloader/PackageAdminClassLoader.java
new file mode 100644
index 0000000..c0b959b
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/jspc/classloader/PackageAdminClassLoader.java
@@ -0,0 +1,344 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.maven.jspc.classloader;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.service.packageadmin.ExportedPackage;
+import org.osgi.service.packageadmin.PackageAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>PackageAdminClassLoader</code> loads
+ * classes and resources through the package admin service.
+ */
+class PackageAdminClassLoader extends ClassLoader {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(PackageAdminClassLoader.class);
+
+    /** The package admin service. */
+    private final PackageAdmin packageAdmin;
+
+    /** The manager factory. */
+    private final DynamicClassLoaderManagerFactory factory;
+
+    /** A cache for resolved classes. */
+    private Map<String, Class<?>> classCache = new ConcurrentHashMap<>();
+
+    /** Negative class cache. */
+    private Set<String> negativeClassCache = Collections.synchronizedSet(new HashSet<String>());
+
+    private Map<String, Bundle> packageProviders = new ConcurrentHashMap<>();
+
+    /** A cache for resolved urls. */
+    private Map<String, URL> urlCache = new ConcurrentHashMap<>();
+
+    public PackageAdminClassLoader(final PackageAdmin pckAdmin,
+                                   final ClassLoader parent,
+                                   final DynamicClassLoaderManagerFactory factory) {
+        super(parent);
+        this.packageAdmin = pckAdmin;
+        this.factory = factory;
+    }
+
+    /**
+     * Returns <code>true</code> if the <code>bundle</code> is to be considered
+     * active from the perspective of declarative services.
+     * <p>
+     * As of R4.1 a bundle may have lazy activation policy which means a bundle
+     * remains in the STARTING state until a class is loaded from that bundle
+     * (unless that class is declared to not cause the bundle to start).
+     *
+     * @param bundle The bundle check
+     * @return <code>true</code> if <code>bundle</code> is not <code>null</code>
+     *          and the bundle is either active or has lazy activation policy
+     *          and is in the starting state.
+     */
+    private boolean isBundleActive( final Bundle bundle ) {
+        if ( bundle != null ) {
+            if ( bundle.getState() == Bundle.ACTIVE ) {
+                return true;
+            }
+
+            if ( bundle.getState() == Bundle.STARTING ) {
+                // according to the spec the activationPolicy header is only
+                // set to request a bundle to be lazily activated. So in this
+                // simple check we just verify the header is set to assume
+                // the bundle is considered a lazily activated bundle
+                return bundle.getHeaders().get( Constants.BUNDLE_ACTIVATIONPOLICY ) != null;
+            }
+
+            if (bundle.getState() == Bundle.RESOLVED) {
+                return true;
+            }
+        }
+
+        // fall back: bundle is not considered active
+        return false;
+    }
+
+    /**
+     * Find the bundle for a given package.
+     * @param pckName The package name.
+     * @return The bundle or <code>null</code>
+     */
+    private Set<Bundle> findBundlesForPackage(final String pckName) {
+        final ExportedPackage[] exportedPackages = this.packageAdmin.getExportedPackages(pckName);
+        Set<Bundle> bundles = new LinkedHashSet<>();
+        if (exportedPackages != null) {
+            for (ExportedPackage exportedPackage : exportedPackages) {
+                if (!exportedPackage.isRemovalPending()) {
+                    Bundle bundle = exportedPackage.getExportingBundle();
+                    if (isBundleActive(bundle)) {
+                        bundles.add(bundle);
+                    }
+                }
+            }
+        }
+        return bundles;
+    }
+
+    /**
+     * Return the package from a resource.
+     * @param resource The resource path.
+     * @return The package name.
+     */
+    private String getPackageFromResource(final String resource) {
+        final int lastSlash = resource.lastIndexOf('/');
+        final String pckName = (lastSlash == -1 ? "" : resource.substring(0, lastSlash).replace('/', '.'));
+        return pckName;
+    }
+
+    /**
+     * Return the package from a class.
+     * @param name The class name.
+     * @return The package name.
+     */
+    private String getPackageFromClassName(final String name) {
+        final int lastDot = name.lastIndexOf('.');
+        final String pckName = (lastDot == -1 ? "" : name.substring(0, lastDot));
+        return pckName;
+    }
+
+    /**
+     * @see java.lang.ClassLoader#getResources(java.lang.String)
+     */
+    @Override
+    public Enumeration<URL> getResources(final String name) throws IOException {
+        Enumeration<URL> e = super.getResources(name);
+        if ( e == null || !e.hasMoreElements() ) {
+            String packageName = getPackageFromResource(name);
+            Bundle providingBundle = packageProviders.get(packageName);
+            if (providingBundle == null) {
+                for (Bundle bundle : findBundlesForPackage(getPackageFromResource(name))) {
+                    e = bundle.getResources(name);
+                    if (e != null) {
+                        packageProviders.put(packageName, bundle);
+                        this.factory.addUsedBundle(bundle);
+                        LOGGER.debug("Marking bundle {}:{} as the provider for API package {}.", bundle.getSymbolicName(), bundle
+                                .getVersion().toString(), packageName);
+                        return e;
+                    }
+                }
+            } else {
+                e = providingBundle.getResources(name);
+                if (e == null) {
+                    LOGGER.debug("Cannot find resources {} in bundle {}:{} which was marked as the provider for package {}.", name,
+                            providingBundle.getSymbolicName(), providingBundle.getVersion().toString(), packageName);
+                }
+            }
+        }
+        return e;
+    }
+
+    /**
+     * @see java.lang.ClassLoader#findResource(java.lang.String)
+     */
+    @Override
+    public URL getResource(final String name) {
+        final URL cachedURL = urlCache.get(name);
+        if ( cachedURL != null ) {
+            return cachedURL;
+        }
+        URL url = super.getResource(name);
+        if ( url == null ) {
+            String packageName = getPackageFromResource(name);
+            Bundle providingBundle = packageProviders.get(packageName);
+            if (providingBundle == null) {
+                Set<Bundle> bundles = findBundlesForPackage(getPackageFromResource(name));
+                for (Bundle bundle : bundles) {
+                    url = bundle.getResource(name);
+                    if (url != null) {
+                        urlCache.put(name, url);
+                        this.factory.addUsedBundle(bundle);
+                        packageProviders.put(packageName, bundle);
+                        LOGGER.debug("Marking bundle {}:{} as the provider for API package {}.", bundle.getSymbolicName(), bundle
+                                .getVersion().toString(), packageName);
+                        return url;
+                    }
+                }
+            } else {
+                url = providingBundle.getResource(name);
+                if (url == null) {
+                    LOGGER.debug("Cannot find resource {} in bundle {}:{} which was marked as the provider for package {}.", name,
+                            providingBundle.getSymbolicName(), providingBundle.getVersion().toString(), packageName);
+                }
+            }
+        }
+        return url;
+    }
+
+    /**
+     * @see java.lang.ClassLoader#findResource(java.lang.String)
+     */
+    @Override
+    public URL findResource(final String name) {
+        final URL cachedURL = urlCache.get(name);
+        if ( cachedURL != null ) {
+            return cachedURL;
+        }
+        URL url = super.findResource(name);
+        if ( url == null ) {
+            String packageName = getPackageFromResource(name);
+            Bundle providingBundle = packageProviders.get(packageName);
+            if (providingBundle == null) {
+                Set<Bundle> bundles = findBundlesForPackage(getPackageFromResource(name));
+                for (Bundle bundle : bundles) {
+                    url = bundle.getResource(name);
+                    if (url != null) {
+                        urlCache.put(name, url);
+                        this.factory.addUsedBundle(bundle);
+                        packageProviders.put(packageName, bundle);
+                        LOGGER.debug("Marking bundle {}:{} as the provider for API package {}.", bundle.getSymbolicName(), bundle
+                                .getVersion().toString(), packageName);
+                        return url;
+                    }
+                }
+            } else {
+                url = providingBundle.getResource(name);
+                if (url == null) {
+                    LOGGER.debug("Cannot find resource {} in bundle {}:{} which was marked as the provider for package {}.", name,
+                            providingBundle.getSymbolicName(), providingBundle.getVersion().toString(), packageName);
+                }
+            }
+        }
+        return url;
+    }
+
+    /**
+     * @see java.lang.ClassLoader#findClass(java.lang.String)
+     */
+    @Override
+    public Class<?> findClass(final String name) throws ClassNotFoundException {
+        final Class<?> cachedClass = this.classCache.get(name);
+        if ( cachedClass != null ) {
+            return cachedClass;
+        }
+        Class<?> clazz;
+        try {
+            clazz = super.findClass(name);
+        } catch (ClassNotFoundException cnfe) {
+            try {
+                clazz = getClassFromBundles(name);
+            } catch (ClassNotFoundException innerCNFE) {
+                throw innerCNFE;
+            }
+        }
+        if ( clazz == null ) {
+            throw new ClassNotFoundException("Class not found " + name);
+        }
+        this.classCache.put(name, clazz);
+        return clazz;
+    }
+
+    /**
+     * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean)
+     */
+    @Override
+    protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
+        final Class<?> cachedClass = this.classCache.get(name);
+        if ( cachedClass != null ) {
+            return cachedClass;
+        }
+        if ( negativeClassCache.contains(name) ) {
+            throw new ClassNotFoundException("Class not found " + name);
+        }
+        String packageName = getPackageFromClassName(name);
+        Class<?> clazz;
+        try {
+            clazz = super.loadClass(name, resolve);
+        } catch (final ClassNotFoundException cnfe) {
+            try {
+                clazz = getClassFromBundles(name);
+            } catch (ClassNotFoundException innerCNFE) {
+                negativeClassCache.add(name);
+                this.factory.addUnresolvedPackage(packageName);
+                throw innerCNFE;
+            }
+        }
+        if ( clazz == null ) {
+            negativeClassCache.add(name);
+            this.factory.addUnresolvedPackage(packageName);
+            throw new ClassNotFoundException("Class not found " + name);
+        }
+        this.classCache.put(name, clazz);
+        return clazz;
+    }
+
+    private Class<?> getClassFromBundles(String name) throws ClassNotFoundException {
+        Class<?> clazz = null;
+        String packageName = getPackageFromClassName(name);
+        Bundle providingBundle = packageProviders.get(packageName);
+        if (providingBundle == null) {
+            Set<Bundle> bundles = findBundlesForPackage(packageName);
+            for (Bundle bundle : bundles) {
+                try {
+                    clazz = bundle.loadClass(name);
+                    this.factory.addUsedBundle(bundle);
+                    packageProviders.put(packageName, bundle);
+                    LOGGER.debug("Marking bundle {}:{} as the provider for API package {}.", bundle.getSymbolicName(), bundle
+                            .getVersion().toString(), packageName);
+                    break;
+                } catch (ClassNotFoundException innerCNFE) {
+                    // do nothing; we need to loop over the bundles providing the class' package
+                }
+            }
+        } else {
+            try {
+                clazz = providingBundle.loadClass(name);
+                this.factory.addUsedBundle(providingBundle);
+            } catch (ClassNotFoundException icnfe) {
+                throw new ClassNotFoundException(String.format("Cannot find class %s in bundle %s:%s which was marked as the provider for" +
+                        " package %s.", name, providingBundle.getSymbolicName(), providingBundle.getVersion().toString(), packageName), icnfe);
+            }
+        }
+        return clazz;
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/maven/jspc/JspcMojoFeatureTest.java b/src/test/java/org/apache/sling/maven/jspc/JspcMojoFeatureTest.java
new file mode 100644
index 0000000..1b2f2b1
--- /dev/null
+++ b/src/test/java/org/apache/sling/maven/jspc/JspcMojoFeatureTest.java
@@ -0,0 +1,212 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.maven.jspc;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.artifact.repository.DefaultArtifactRepository;
+import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
+import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
+import org.apache.maven.artifact.resolver.ArtifactResolutionException;
+import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
+import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
+import org.apache.maven.artifact.resolver.ArtifactResolver;
+import org.apache.maven.artifact.resolver.DefaultArtifactResolver;
+import org.apache.maven.artifact.resolver.ResolutionListener;
+import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
+import org.apache.maven.execution.DefaultMavenExecutionRequest;
+import org.apache.maven.execution.MavenExecutionRequest;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.testing.MojoRule;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.ProjectBuilder;
+import org.apache.maven.project.ProjectBuildingRequest;
+import org.apache.maven.wagon.events.TransferListener;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.json.JsonString;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+
+public class JspcMojoFeatureTest {
+
+    private static final String INCLUDES_PROJECT = "src/test/resources/jspc-maven-plugin-it-includes";
+    private static final String REPOSITORY = "src/test/resources/jspc-maven-plugin-it-includes/repo";
+
+    @Rule
+    public MojoRule mojoRule = new MojoRule();
+
+    private MavenProject mavenProject;
+    private JspcMojo jspcMojo;
+    private File baseDir;
+
+    @Before
+    public void before() throws Exception {
+        baseDir = new File(INCLUDES_PROJECT);
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        request.setBaseDirectory(baseDir);
+        ProjectBuildingRequest configuration = request.getProjectBuildingRequest();
+        configuration.setResolveDependencies(true);
+        configuration.setLocalRepository(new DefaultArtifactRepository("project", "file://" + new File(REPOSITORY).getAbsolutePath(),
+                new DefaultRepositoryLayout()));
+        mavenProject = mojoRule.lookup(ProjectBuilder.class).build(new File(baseDir, "pom-feature.xml"), configuration).getProject();
+        Assert.assertNotNull(mavenProject);
+        MavenSession session = mojoRule.newMavenSession(mavenProject);
+        MojoExecution execution = mojoRule.newMojoExecution("jspc");
+        jspcMojo = (JspcMojo) mojoRule.lookupConfiguredMojo(session, execution);
+        jspcMojo.artifactResolver = new ArtifactResolver() {
+            @Override
+            public ArtifactResolutionResult resolve(ArtifactResolutionRequest request) {
+                return null;
+            }
+
+            @Override
+            public ArtifactResolutionResult resolveTransitively(Set<Artifact> artifacts, Artifact originatingArtifact, ArtifactRepository localRepository, List<ArtifactRepository> remoteRepositories, ArtifactMetadataSource source, ArtifactFilter filter) throws ArtifactResolutionException, ArtifactNotFoundException {
+                return null;
+            }
+
+            @Override
+            public ArtifactResolutionResult resolveTransitively(Set<Artifact> artifacts, Artifact originatingArtifact, Map<String, Artifact> managedVersions, ArtifactRepository localRepository, List<ArtifactRepository> remoteRepositories, ArtifactMetadataSource source) throws ArtifactResolutionException, ArtifactNotFoundException {
+                return null;
+            }
+
+            @Override
+            public ArtifactResolutionResult resolveTransitively(Set<Artifact> artifacts, Artifact originatingArtifact, Map<String, Artifact> managedVersions, ArtifactRepository localRepository, List<ArtifactRepository> remoteRepositories, ArtifactMetadataSource source, ArtifactFilter filter) throws ArtifactResolutionException, ArtifactNotFoundException {
+                return null;
+            }
+
+            @Override
+            public ArtifactResolutionResult resolveTransitively(Set<Artifact> artifacts, Artifact originatingArtifact, List<ArtifactRepository> remoteRepositories, ArtifactRepository localRepository, ArtifactMetadataSource source) throws ArtifactResolutionException, ArtifactNotFoundException {
+                return null;
+            }
+
+            @Override
+            public ArtifactResolutionResult resolveTransitively(Set<Artifact> artifacts, Artifact originatingArtifact, Map<String, Artifact> managedVersions, ArtifactRepository localRepository, List<ArtifactRepository> remoteRepositories, ArtifactMetadataSource source, ArtifactFilter filter, List<ResolutionListener> listeners) throws ArtifactResolutionException, ArtifactNotFoundException {
+                return null;
+            }
+
+            @Override
+            public ArtifactResolutionResult resolveTransitively(Set<Artifact> artifacts, Artifact originatingArtifact, List<ArtifactRepository> remoteRepositories, ArtifactRepository localRepository, ArtifactMetadataSource source, List<ResolutionListener> listeners) throws ArtifactResolutionException, ArtifactNotFoundException {
+                return null;
+            }
+
+            @Override
+            public void resolve(Artifact artifact, List<ArtifactRepository> remoteRepositories, ArtifactRepository localRepository) throws ArtifactResolutionException, ArtifactNotFoundException {
+                artifact.setFile(new File(REPOSITORY, artifact.getGroupId().replace('.','/') + '/' +
+                        artifact.getArtifactId() + "/" + artifact.getVersion() + "/" + artifact.getArtifactId() + "-" + artifact.getVersion() + ".jar"));
+            }
+
+            @Override
+            public void resolve(Artifact artifact, List<ArtifactRepository> remoteRepositories, ArtifactRepository localRepository, TransferListener downloadMonitor) throws ArtifactResolutionException, ArtifactNotFoundException {
+
+            }
+
+            @Override
+            public void resolveAlways(Artifact artifact, List<ArtifactRepository> remoteRepositories, ArtifactRepository localRepository) throws ArtifactResolutionException, ArtifactNotFoundException {
+
+            }
+        };
+    }
+
+    @After
+    public void after() {
+        FileUtils.deleteQuietly(new File(baseDir, "target"));
+    }
+
+    @Test
+    public void testIncludesFromClassPath() throws Exception {
+        jspcMojo.execute();
+        File generatedMain = new File(mavenProject.getBuild().getOutputDirectory() + File.separator + "main__002e__jsp.java");
+        assertTrue("Expected to find a generated main__002e__jsp.java file.", generatedMain.exists());
+        FileReader fileReader = new FileReader(generatedMain);
+        BufferedReader bufferedReader = new BufferedReader(fileReader);
+        Set<String> expectedContent = new HashSet<>(Arrays.asList("included-fs.jsp", "included-cp.jsp", "/libs/l1/l2/included-cp.jsp"));
+        String line = bufferedReader.readLine();
+        while (line != null) {
+            expectedContent.removeIf(line::contains);
+            line = bufferedReader.readLine();
+        }
+        assertTrue("Some files were not correctly included: " + expectedContent.toString(), expectedContent.isEmpty());
+    }
+
+    @Test
+    public void testCompilationReport() throws Exception {
+        jspcMojo.execute();
+        Path compilationReportPath = Paths.get(mavenProject.getBuild().getOutputDirectory(), "compilation_report.json");
+        try (JsonReader reader = Json.createReader(Files.newBufferedReader(compilationReportPath))) {
+            JsonObject compilationReport = reader.readObject();
+
+            JsonArray unusedDependencies = compilationReport.getJsonArray("unusedDependencies");
+            assertNotNull(unusedDependencies);
+            assertTrue(unusedDependencies.isEmpty());
+
+            JsonArray jspDependencies = compilationReport.getJsonArray("jspDependencies");
+            assertNotNull(jspDependencies);
+            assertEquals(1, jspDependencies.size());
+            JsonObject mainJsp = jspDependencies.getJsonObject(0);
+            assertEquals("src/main/scripts/main.jsp", mainJsp.getString("jsp"));
+            JsonArray mainJspDependencies = mainJsp.getJsonArray("dependencies");
+            assertNotNull(mainJspDependencies);
+            assertEquals(3, mainJspDependencies.size());
+
+            assertEquals(new HashSet<>(Arrays.asList("jspc-maven-plugin-it-includes-deps-0.0.1.jar:/libs/l1/l2/included-cp.jsp", "jspc" +
+                    "-maven-plugin-it-includes-deps-0.0.1.jar:/included-cp.jsp", "src/main/scripts/included-fs.jsp")),
+                    new HashSet<>(mainJspDependencies.getValuesAs(JsonString.class).stream().map(JsonString::getString).collect(
+                            Collectors.toList())));
+
+            JsonArray packageProviders = compilationReport.getJsonArray("packageProviders");
+            assertNotNull(packageProviders);
+            assertEquals(1, packageProviders.size());
+
+            JsonObject provider = packageProviders.getJsonObject(0);
+            assertEquals("org.apache.sling.maven.jspc.it", provider.getString("package"));
+            JsonArray providers = provider.getJsonArray("providers");
+            assertNotNull(providers);
+            assertEquals(1, providers.size());
+            assertEquals("org.apache.sling:jspc-maven-plugin-it-deps:jar:0.0.1", providers.getString(0));
+        }
+    }
+
+}
diff --git a/src/test/resources/jspc-maven-plugin-it-includes/feature-test.json b/src/test/resources/jspc-maven-plugin-it-includes/feature-test.json
new file mode 100644
index 0000000..b05f89e
--- /dev/null
+++ b/src/test/resources/jspc-maven-plugin-it-includes/feature-test.json
@@ -0,0 +1,4 @@
+{
+  "id": "org.foo:bar:slingosgifeature:0.0.1-SNAPSHOT",
+  "bundles": ["org.apache.sling:jspc-maven-plugin-it-deps:0.0.1"]
+}
\ No newline at end of file
diff --git a/src/test/resources/jspc-maven-plugin-it-includes/pom-feature.xml b/src/test/resources/jspc-maven-plugin-it-includes/pom-feature.xml
new file mode 100644
index 0000000..52f2556
--- /dev/null
+++ b/src/test/resources/jspc-maven-plugin-it-includes/pom-feature.xml
@@ -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.
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
+<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>
+
+    <groupId>org.apache.sling</groupId>
+    <artifactId>jspc-maven-plugin-it-feature</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>HTL Maven Plugin IT - Generate Java Classes</name>
+
+    <repositories>
+        <repository>
+            <id>project</id>
+            <url>file://${project.basedir}/repo</url>
+        </repository>
+    </repositories>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.sling</groupId>
+                <artifactId>jspc-maven-plugin</artifactId>
+                <configuration>
+                    <generateCompilationReport>true</generateCompilationReport>
+                    <feature>
+                        <featureFile>${project.basedir}/feature-test.json</featureFile>
+                        <dependencies>
+                            <dependency>
+                                <groupId>org.apache.sling</groupId>
+                                <artifactId>jspc-maven-plugin-it-includes-deps</artifactId>
+                                <version>0.0.1</version>
+                            </dependency>
+                        </dependencies>
+                    </feature>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jspc</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/src/test/resources/jspc-maven-plugin-it-includes/repo/org/apache/sling/jspc-maven-plugin-it-deps/0.0.1/jspc-maven-plugin-it-deps-0.0.1.jar b/src/test/resources/jspc-maven-plugin-it-includes/repo/org/apache/sling/jspc-maven-plugin-it-deps/0.0.1/jspc-maven-plugin-it-deps-0.0.1.jar
index cb589cd..d756b0a 100644
Binary files a/src/test/resources/jspc-maven-plugin-it-includes/repo/org/apache/sling/jspc-maven-plugin-it-deps/0.0.1/jspc-maven-plugin-it-deps-0.0.1.jar and b/src/test/resources/jspc-maven-plugin-it-includes/repo/org/apache/sling/jspc-maven-plugin-it-deps/0.0.1/jspc-maven-plugin-it-deps-0.0.1.jar differ