You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by kw...@apache.org on 2019/02/25 12:38:28 UTC

[sling-org-apache-sling-installer-provider-installhook] 09/24: SLING-7790 use of InfoProvider, additional package properties for configuration

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

kwin pushed a commit to branch feature/SLING-8291_expose-error
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-installer-provider-installhook.git

commit 9a95ab7d4f7325e64f792c5cea4bf6445d4f0c43
Author: georg.henzler <ge...@netcentric.biz>
AuthorDate: Thu Oct 4 23:59:10 2018 +0200

    SLING-7790 use of InfoProvider, additional package properties for
    configuration
---
 .../provider/installhook/OsgiInstallerHook.java    | 784 ++++++++++++---------
 .../installhook/OsgiInstallerListener.java         |  91 +--
 .../installhook/OsgiInstallerHookTest.java         | 159 +++--
 .../installhook/OsgiInstallerListenerTest.java     |  87 ++-
 4 files changed, 625 insertions(+), 496 deletions(-)

diff --git a/src/main/java/org/apache/sling/installer/provider/installhook/OsgiInstallerHook.java b/src/main/java/org/apache/sling/installer/provider/installhook/OsgiInstallerHook.java
index a8d910b..fd27716 100644
--- a/src/main/java/org/apache/sling/installer/provider/installhook/OsgiInstallerHook.java
+++ b/src/main/java/org/apache/sling/installer/provider/installhook/OsgiInstallerHook.java
@@ -50,357 +50,467 @@ import org.apache.jackrabbit.vault.packaging.VaultPackage;
 import org.apache.sling.installer.api.InstallableResource;
 import org.apache.sling.installer.api.OsgiInstaller;
 import org.apache.sling.installer.api.event.InstallationListener;
+import org.apache.sling.installer.api.info.InfoProvider;
+import org.apache.sling.installer.api.info.InstallationState;
+import org.apache.sling.installer.api.info.Resource;
+import org.apache.sling.installer.api.info.ResourceGroup;
+import org.apache.sling.installer.api.tasks.ResourceState;
 import org.apache.sling.settings.SlingSettingsService;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleListener;
 import org.osgi.framework.FrameworkUtil;
 import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
-import org.osgi.service.cm.Configuration;
-import org.osgi.service.cm.ConfigurationAdmin;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class OsgiInstallerHook implements InstallHook {
 
-	private static final Logger LOG = LoggerFactory.getLogger(OsgiInstallerHook.class);
+    private static final Logger LOG = LoggerFactory.getLogger(OsgiInstallerHook.class);
 
-	private static final String PACKAGE_PROPERTY_MAX_WAIT_IN_SEC = "maxWaitForOsgiInstallerInSec";
-	private static final String PACKAGE_PROP_INSTALL_PATH_REGEX = "installPathRegex";
+    private static final String PACKAGE_PROP_INSTALL_PATH_REGEX = "installPathRegex";
+    private static final String PACKAGE_PROPERTY_MAX_WAIT_IN_SEC = "maxWaitForOsgiInstallerInSec";
+    private static final String PACKAGE_PROPERTY_INSTALL_PRIORITY = "osgiInstallerPriority";
+    private static final String PACKAGE_PROPERTY_WAIT_FOR_OSGI_EVENTS_QUIET_IN_SEC = "waitForOsgiEventsQuietInSec";
 
-	public static final int DEFAULT_PRIORITY_INSTALL_HOOK = 2000;
-	private static final int DEFAULT_MAX_WAIT_IN_SEC = 60;
+    public static final int DEFAULT_PRIORITY_INSTALL_HOOK = 2000;
+    private static final int DEFAULT_MAX_WAIT_IN_SEC = 60;
+    private static final int DEFAULT_WAIT_FOR_OSGI_EVENTS_QUIET_IN_SEC = 1;
 
-	public static final String URL_SCHEME = "jcrinstall";
-	public static final String CONFIG_SUFFIX = ".config";
-	public static final String JAR_SUFFIX = ".jar";
+    public static final String URL_SCHEME = "jcrinstall";
+    public static final String CONFIG_SUFFIX = ".config";
+    public static final String JAR_SUFFIX = ".jar";
+
+    private static final String ENTITY_ID_PREFIX_BUNDLE = "bundle:";
+
+    private static final String MANIFEST_BUNDLE_SYMBOLIC_NAME = "Bundle-SymbolicName";
+    private static final String MANIFEST_BUNDLE_VERSION = "Bundle-Version";
+    private static final String FOLDER_META_INF = "META-INF";
+
+    static final String JCR_CONTENT = "jcr:content";
+    static final String JCR_CONTENT_DATA = JCR_CONTENT + "/jcr:data";
+    static final String JCR_LAST_MODIFIED = "jcr:lastModified";
+    static final String JCR_CONTENT_LAST_MODIFIED = JCR_CONTENT + "/" + JCR_LAST_MODIFIED;
+
+    public static final String DOT = ".";
+
+    InstallHookLogger logger = new InstallHookLogger();
+
+    @Override
+    public void execute(InstallContext context) throws PackageException {
+
+        VaultPackage vaultPackage = context.getPackage();
+        PackageProperties packageProperties = vaultPackage.getProperties();
+        String installPathRegex = packageProperties.getProperty(PACKAGE_PROP_INSTALL_PATH_REGEX);
+
+        ServiceReference<OsgiInstaller> osgiInstallerServiceRef = null;
+        ServiceReference<SlingSettingsService> slingSettingsServiceRef = null;
+        ServiceRegistration<InstallationListener> hookInstallationListenerServiceRegistration = null;
+
+        ServiceReference<InfoProvider> infoProviderServiceRef = null;
+
+        try {
+            switch (context.getPhase()) {
+            case PREPARE:
+                if (StringUtils.isBlank(installPathRegex)) {
+                    throw new IllegalArgumentException(
+                            "When using OSGi installer install hook for synchronous installation, the package property "
+                                    + PACKAGE_PROP_INSTALL_PATH_REGEX + " has to be provided.");
+                }
+                break;
+            case INSTALLED:
+                ImportOptions options = context.getOptions();
+                logger.setOptions(options);
+
+                logger.log(getClass().getSimpleName() + " is active in " + vaultPackage.getId());
+
+                List<BundleInPackage> bundleResources = new ArrayList<>();
+                List<String> configResourcePaths = new ArrayList<>();
+                Archive archive = vaultPackage.getArchive();
+
+                infoProviderServiceRef = getBundleContext().getServiceReference(InfoProvider.class);
+                InfoProvider infoProvider = (InfoProvider) getBundleContext().getService(infoProviderServiceRef);
+                InstallationState installationState = infoProvider.getInstallationState();
+
+                slingSettingsServiceRef = getBundleContext().getServiceReference(SlingSettingsService.class);
+                SlingSettingsService slingSettingsService = (SlingSettingsService) getBundleContext().getService(slingSettingsServiceRef);
+                Set<String> runModes = slingSettingsService.getRunModes();
+
+                collectResources(archive, archive.getRoot(), "", bundleResources, configResourcePaths, installPathRegex,
+                        runModes);
+
+                logger.log("Bundles in package " + bundleResources);
+
+                Session session = context.getSession();
+
+                Map<String, InstallableResource> bundlesToInstallByUrl = getBundlesToInstall(bundleResources, session, installationState,
+                        packageProperties);
+                Map<String, InstallableResource> configsToInstallByUrl = getConfigsToInstall(configResourcePaths, session,
+                        installationState, packageProperties);
+
+                if (bundlesToInstallByUrl.isEmpty() && configsToInstallByUrl.isEmpty()) {
+                    logger.log("No installable resources that are not installed yet found.");
+                    return;
+                }
+
+                logger.log("Installing " + bundlesToInstallByUrl.size() + " bundles and "
+                        + configsToInstallByUrl.size() + " configs");
+                osgiInstallerServiceRef = getBundleContext().getServiceReference(OsgiInstaller.class);
+                OsgiInstaller osgiInstaller = getBundleContext().getService(osgiInstallerServiceRef);
+
+                OsgiInstallerListener hookInstallationListener = new OsgiInstallerListener(bundlesToInstallByUrl.keySet(),
+                        configsToInstallByUrl.keySet());
+                hookInstallationListenerServiceRegistration = getBundleContext()
+                        .registerService(InstallationListener.class, hookInstallationListener, null);
+
+                List<InstallableResource> resourcesToUpdate = new ArrayList<>();
+                resourcesToUpdate.addAll(bundlesToInstallByUrl.values());
+                resourcesToUpdate.addAll(configsToInstallByUrl.values());
+                logger.log("Updating resources " + resourcesToUpdate);
+                osgiInstaller.updateResources(URL_SCHEME, resourcesToUpdate.toArray(new InstallableResource[resourcesToUpdate.size()]),
+                        null);
+
+                int maxWaitForOsgiInstallerInSec = getNumericPackageProperty(packageProperties, PACKAGE_PROPERTY_MAX_WAIT_IN_SEC,
+                        DEFAULT_MAX_WAIT_IN_SEC);
+
+                long startTime = System.currentTimeMillis();
+                int bundlesLeftToInstall = 0;
+                int configsLeftToInstall = 0;
+                while ((bundlesLeftToInstall = hookInstallationListener.bundlesLeftToInstall()) > 0
+                        || (configsLeftToInstall = hookInstallationListener.configsLeftToInstall()) > 0) {
+                    if ((System.currentTimeMillis() - startTime) > maxWaitForOsgiInstallerInSec * 1000) {
+                        logger.log("Installable resources " + resourcesToUpdate
+                                + " could not be installed even after waiting " + maxWaitForOsgiInstallerInSec + "sec");
+                        break;
+                    }
+                    logger.log("Waiting for " + bundlesLeftToInstall + " bundles / " + configsLeftToInstall + " configs to be installed");
+                    Thread.sleep(1000);
+                }
+                if (bundlesLeftToInstall == 0 && configsLeftToInstall == 0) {
+                    logger.log("All " + bundlesToInstallByUrl.size() + " bundles / " + configsToInstallByUrl.size()
+                            + " configs have been successfully installed");
+                }
+
+                int waitForOsgiEventsQuietInSec = getNumericPackageProperty(packageProperties,
+                        PACKAGE_PROPERTY_WAIT_FOR_OSGI_EVENTS_QUIET_IN_SEC, DEFAULT_WAIT_FOR_OSGI_EVENTS_QUIET_IN_SEC);
+                waitForServiceChanges(waitForOsgiEventsQuietInSec);
+
+                break;
+            default:
+                break;
+            }
+        } catch (Exception e) {
+            throw new PackageException("Could not execute install hook to for synchronous installation: " + e, e);
+        } finally {
+            if (osgiInstallerServiceRef != null) {
+                getBundleContext().ungetService(osgiInstallerServiceRef);
+            }
+            if (slingSettingsServiceRef != null) {
+                getBundleContext().ungetService(slingSettingsServiceRef);
+            }
+            if (infoProviderServiceRef != null) {
+                getBundleContext().ungetService(infoProviderServiceRef);
+            }
+
+            if (hookInstallationListenerServiceRegistration != null) {
+                hookInstallationListenerServiceRegistration.unregister();
+            }
+        }
+    }
+
+    private int getNumericPackageProperty(PackageProperties packageProperties, String propertyName, int defaultVal) {
+        String strVal = packageProperties.getProperty(propertyName);
+        int intVal = strVal != null ? Integer.parseInt(strVal) : defaultVal;
+        return intVal;
+    }
+
+    private Map<String, InstallableResource> getBundlesToInstall(List<BundleInPackage> bundlesInPackage, Session session,
+            InstallationState installationState, PackageProperties packageProperties) throws RepositoryException, IOException {
+        Map<String, InstallableResource> installableResources = new HashMap<>();
+        Iterator<BundleInPackage> bundlesIt = bundlesInPackage.iterator();
+        while (bundlesIt.hasNext()) {
+            BundleInPackage bundle = bundlesIt.next();
+
+            List<Resource> currentInstallerBundleResources = getBundleResources(installationState, bundle.symbolicName);
+
+            boolean needsInstallation = false;
+            if (currentInstallerBundleResources.isEmpty()) {
+                needsInstallation = true;
+            } else if (currentInstallerBundleResources.size() == 1) {
+                Resource resource = currentInstallerBundleResources.get(0);
+
+                if (resource.getState() == ResourceState.INSTALLED) {
+                    String currentlyActiveBundleVersion = resource.getVersion().toString();
+                    if (!StringUtils.equals(currentlyActiveBundleVersion, bundle.version)) {
+                        logger.log("Bundle " + bundle.symbolicName + " is installed with version "
+                                + currentlyActiveBundleVersion + " but package contains version " + bundle.version);
+                        needsInstallation = true;
+                    } else {
+                        logger.log("Bundle " + bundle.symbolicName + " is already installed with version "
+                                + currentlyActiveBundleVersion + " that matches " + bundle.version + " as provided in package");
+                    }
+                } else {
+                    logger.log("Bundle " + bundle.symbolicName + " is not in state INSTALLED but in " + resource.getState());
+                    needsInstallation = true;
+                }
+
+            } else {
+                logger.log("Bundle " + bundle.symbolicName + " exists with multiple installer resources");
+                boolean installedBundleResourceFound = false;
+                for (Resource resource : currentInstallerBundleResources) {
+                    logger.log("Resource " + resource);
+                    if (resource.getState() == ResourceState.INSTALLED
+                            && StringUtils.equals(resource.getVersion().toString(), bundle.version)) {
+                        installedBundleResourceFound = true;
+                    }
+                }
+                if (!installedBundleResourceFound) {
+                    needsInstallation = true;
+                }
+
+            }
+
+            if (needsInstallation) {
+                logger.log("Bundle " + bundle.symbolicName + " requires installation");
+                Node node = session.getNode(bundle.path);
+                InstallableResource installableResource = convert(node, bundle.path, packageProperties);
+                String bundleUrl = URL_SCHEME + ":" + bundle.path;
+                installableResources.put(bundleUrl, installableResource);
+            }
+        }
+        return installableResources;
+    }
+
+    private List<Resource> getBundleResources(InstallationState installationState, String symbolicId) {
+
+        List<Resource> bundleResources = new ArrayList<Resource>();
+
+        List<ResourceGroup> allGroups = new ArrayList<ResourceGroup>();
+        allGroups.addAll(installationState.getInstalledResources());
+        allGroups.addAll(installationState.getActiveResources());
+        for (ResourceGroup resourceGroup : allGroups) {
+            List<Resource> resources = resourceGroup.getResources();
+            for (Resource resource : resources) {
+                if (StringUtils.equals(resource.getEntityId(), ENTITY_ID_PREFIX_BUNDLE + symbolicId)) {
+                    bundleResources.add(resource);
+                }
+            }
+        }
+        return bundleResources;
+    }
+
+    private Map<String, InstallableResource> getConfigsToInstall(List<String> configResourcePaths, Session session,
+            InstallationState installationState, PackageProperties packageProperties)
+            throws IOException, InvalidSyntaxException, RepositoryException {
+        Map<String, InstallableResource> configsToInstallByUrl = new HashMap<>();
+        for (String configResourcePath : configResourcePaths) {
+            boolean needsInstallation = false;
+
+            String configUrl = URL_SCHEME + ":" + configResourcePath;
+            boolean configFound = false;
+            List<ResourceGroup> installedResources = installationState.getInstalledResources();
+            for (ResourceGroup resourceGroup : installedResources) {
+                for (Resource resource : resourceGroup.getResources()) {
+                    if (StringUtils.equals(configUrl, resource.getURL())) {
+                        configFound = true;
+                        logger.log("Config " + configResourcePath + " is already installed");
+                    }
+                }
+            }
+            if (!configFound) {
+                logger.log("Config " + configResourcePath + " has not been installed");
+                needsInstallation = true;
+            }
+
+            if (needsInstallation) {
+
+                Node node = session.getNode(configResourcePath);
+                InstallableResource installableResource = convert(node, configResourcePath, packageProperties);
+
+                configsToInstallByUrl.put(configUrl, installableResource);
+            }
+        }
+        return configsToInstallByUrl;
+    }
+
+    void collectResources(Archive archive, Entry entry, String dirPath, List<BundleInPackage> bundleResources,
+            List<String> configResources, String installPathRegex, Set<String> actualRunmodes) {
+        String entryName = entry.getName();
+        if (entryName.equals(FOLDER_META_INF)) {
+            return;
+        }
+
+        String dirPathWithoutJcrRoot = StringUtils.substringAfter(dirPath, "/jcr_root");
+        String entryPath = dirPathWithoutJcrRoot + entryName;
+        String dirPathWithoutSlash = StringUtils.chomp(dirPathWithoutJcrRoot, "/");
+
+        boolean runmodesMatch;
+        if (dirPathWithoutSlash.contains(DOT)) {
+            String[] bits = dirPathWithoutSlash.split("\\" + DOT, 2);
+            List<String> runmodesOfResource = Arrays.asList(bits[1].split("\\" + DOT));
+            Set<String> matchingRunmodes = new HashSet<String>(runmodesOfResource);
+            matchingRunmodes.retainAll(actualRunmodes);
+            LOG.debug("Entry with runmode(s): entryPath={} runmodesOfResource={} actualRunmodes={} matchingRunmodes={}",
+                    entryPath, runmodesOfResource, actualRunmodes, matchingRunmodes);
+            runmodesMatch = matchingRunmodes.size() == runmodesOfResource.size();
+            if (!runmodesMatch) {
+                logger.log("Skipping installation of  " + entryPath
+                        + " because the path is not matching all actual runmodes " + actualRunmodes);
+            }
+        } else {
+            runmodesMatch = true;
+        }
+
+        if (entryPath.matches(installPathRegex) && runmodesMatch) {
+
+            if (entryName.endsWith(CONFIG_SUFFIX)) {
+                configResources.add(entryPath);
+            } else if (entryName.endsWith(JAR_SUFFIX)) {
+                try (InputStream entryInputStream = archive.getInputSource(entry).getByteStream();
+                        JarInputStream jarInputStream = new JarInputStream(entryInputStream)) {
+                    Manifest manifest = jarInputStream.getManifest();
+                    String symbolicName = manifest.getMainAttributes().getValue(MANIFEST_BUNDLE_SYMBOLIC_NAME);
+                    String version = manifest.getMainAttributes().getValue(MANIFEST_BUNDLE_VERSION);
+
+                    bundleResources.add(new BundleInPackage(entryPath, symbolicName, version));
+                } catch (Exception e) {
+                    throw new IllegalStateException(
+                            "Could not read symbolic name and version from manifest of bundle " + entryName);
+                }
+            }
+
+        }
+
+        for (Entry child : entry.getChildren()) {
+            collectResources(archive, child, dirPath + entryName + "/", bundleResources, configResources,
+                    installPathRegex, actualRunmodes);
+        }
+    }
+
+    InstallableResource convert(final Node node, final String path, PackageProperties packageProperties)
+            throws IOException, RepositoryException {
+        LOG.trace("Converting {} at path {}", node, path);
+        final String digest = String.valueOf(node.getProperty(JCR_CONTENT_LAST_MODIFIED).getDate().getTimeInMillis());
+        final InputStream is = node.getProperty(JCR_CONTENT_DATA).getStream();
+        final Dictionary<String, Object> dict = new Hashtable<String, Object>();
+        dict.put(InstallableResource.INSTALLATION_HINT, node.getParent().getName());
+        int priority = getNumericPackageProperty(packageProperties, PACKAGE_PROPERTY_INSTALL_PRIORITY, DEFAULT_PRIORITY_INSTALL_HOOK);
+        return new InstallableResource(path, is, dict, digest, null, priority);
+    }
+
+    private void waitForServiceChanges(int waitForOsgiEventsQuietInSec) {
+        if (waitForOsgiEventsQuietInSec <= 0) {
+            return;
+        }
+        InstallerHookOsgiEventListener osgiListener = new InstallerHookOsgiEventListener();
+        BundleContext bundleContext = getBundleContext();
+        bundleContext.addServiceListener(osgiListener);
+        bundleContext.addBundleListener(osgiListener);
+
+        long waitStart = System.currentTimeMillis();
+        osgiListener.waitUntilQuiet(waitForOsgiEventsQuietInSec);
+        logger.log("Waited " + (System.currentTimeMillis() - waitStart) + "ms in total for OSGi events to become quiet (for at least "
+                + waitForOsgiEventsQuietInSec + "sec)");
+
+        bundleContext.removeServiceListener(osgiListener);
+        bundleContext.removeBundleListener(osgiListener);
+
+    }
+
+    // always get fresh bundle context to avoid "Dynamic class loader has already
+    // been deactivated" exceptions
+    private BundleContext getBundleContext() {
+        // use the vault bundle to hook into the OSGi world
+        Bundle currentBundle = FrameworkUtil.getBundle(InstallHook.class);
+        if (currentBundle == null) {
+            throw new IllegalStateException(
+                    "The class " + InstallHook.class + " was not loaded through a bundle classloader");
+        }
+        BundleContext bundleContext = currentBundle.getBundleContext();
+        if (bundleContext == null) {
+            throw new IllegalStateException("Could not get bundle context for bundle " + currentBundle);
+        }
+        return bundleContext;
+    }
+
+    class BundleInPackage {
+        final String path;
+        final String symbolicName;
+        final String version;
+
+        public BundleInPackage(String path, String symbolicName, String version) {
+            super();
+            this.path = path;
+            this.symbolicName = symbolicName;
+            this.version = version;
+        }
+
+        @Override
+        public String toString() {
+            return "BundleInPackage [path=" + path + ", symbolicId=" + symbolicName + ", version=" + version + "]";
+        }
+
+    }
+
+    static class InstallHookLogger {
+
+        private ProgressTrackerListener listener;
+
+        public void setOptions(ImportOptions options) {
+            this.listener = options.getListener();
+        }
+
+        public void logError(Logger logger, String message, Throwable throwable) {
+            if (listener != null) {
+                listener.onMessage(ProgressTrackerListener.Mode.TEXT, "ERROR: " + message, "");
+            }
+            logger.error(message, throwable);
+        }
+
+        public void log(String message) {
+            log(LOG, message);
+        }
+
+        public void log(Logger logger, String message) {
+            if (listener != null) {
+                listener.onMessage(ProgressTrackerListener.Mode.TEXT, message, "");
+                logger.debug(message);
+            } else {
+                logger.info(message);
+            }
+        }
+    }
+
+    static class InstallerHookOsgiEventListener implements ServiceListener, BundleListener {
+
+        private long lastEventTimestamp = System.currentTimeMillis();
+
+        @Override
+        public void serviceChanged(ServiceEvent event) {
+            lastEventTimestamp = System.currentTimeMillis();
+            LOG.trace("Service changed event {}", event);
+        }
+
+        @Override
+        public void bundleChanged(BundleEvent event) {
+            lastEventTimestamp = System.currentTimeMillis();
+            LOG.trace("Bundle changed event {}", event);
+        }
+
+        public void waitUntilQuiet(long waitInSec) {
+            try {
+                while (System.currentTimeMillis() - lastEventTimestamp < waitInSec * 1000) {
+                    Thread.sleep(100);
+                }
+            } catch (InterruptedException e) {
+                LOG.warn("Wait for OSGi events was interrupted");
+            }
+        }
+    }
 
-	private static final String MANIFEST_BUNDLE_SYMBOLIC_NAME = "Bundle-SymbolicName";
-	private static final String MANIFEST_BUNDLE_VERSION = "Bundle-Version";
-	private static final String FOLDER_META_INF = "META-INF";
-
-	static final String JCR_CONTENT = "jcr:content";
-	static final String JCR_CONTENT_DATA = JCR_CONTENT + "/jcr:data";
-	static final String JCR_LAST_MODIFIED = "jcr:lastModified";
-	static final String JCR_CONTENT_LAST_MODIFIED = JCR_CONTENT + "/" + JCR_LAST_MODIFIED;
-
-	public static final String DOT = ".";
-
-	InstallHookLogger logger = new InstallHookLogger();
-
-	@Override
-	public void execute(InstallContext context) throws PackageException {
-
-		VaultPackage vaultPackage = context.getPackage();
-		PackageProperties packageProperties = vaultPackage.getProperties();
-		String installPathRegex = packageProperties.getProperty(PACKAGE_PROP_INSTALL_PATH_REGEX);
-
-		ServiceReference<OsgiInstaller> osgiInstallerServiceRef = null;
-		ServiceReference<ConfigurationAdmin> configAdminServiceRef = null;
-		ServiceReference<SlingSettingsService> slingSettingsServiceRef = null;
-		ServiceRegistration<InstallationListener> hookInstallationListenerServiceRegistration = null;
-
-		try {
-			switch (context.getPhase()) {
-			case PREPARE:
-				if (StringUtils.isBlank(installPathRegex)) {
-					throw new IllegalArgumentException(
-							"When using OSGi installer install hook for synchronous installation, the package property "
-									+ PACKAGE_PROP_INSTALL_PATH_REGEX + " has to be provided.");
-				}
-				break;
-			case INSTALLED:
-				ImportOptions options = context.getOptions();
-				logger.setOptions(options);
-
-				logger.log(getClass().getSimpleName() + " is active in " + vaultPackage.getId());
-
-				List<BundleInPackage> bundleResources = new ArrayList<>();
-				List<String> configResourcePaths = new ArrayList<>();
-				Archive archive = vaultPackage.getArchive();
-
-				configAdminServiceRef = getBundleContext().getServiceReference(ConfigurationAdmin.class);
-				ConfigurationAdmin confAdmin = (ConfigurationAdmin) getBundleContext()
-						.getService(configAdminServiceRef);
-
-				slingSettingsServiceRef = getBundleContext().getServiceReference(SlingSettingsService.class);
-				SlingSettingsService slingSettingsService = (SlingSettingsService) getBundleContext()
-						.getService(slingSettingsServiceRef);
-				Set<String> runModes = slingSettingsService.getRunModes();
-
-				collectResources(archive, archive.getRoot(), "", bundleResources, configResourcePaths, installPathRegex,
-						runModes);
-
-				logger.log("Bundles in package " + bundleResources);
-
-				Map<String, String> bundleVersionsBySymbolicId = new HashMap<>();
-				for (Bundle bundle : getBundleContext().getBundles()) {
-					bundleVersionsBySymbolicId.put(bundle.getSymbolicName(), bundle.getVersion().toString());
-				}
-
-				Session session = context.getSession();
-
-				List<InstallableResource> installableResources = new ArrayList<>();
-
-				Set<String> bundleSymbolicNamesToInstall = getBundlesToInstall(bundleResources,
-						bundleVersionsBySymbolicId, session, installableResources);
-
-				Set<String> configPidsToInstall = getConfigPidsToInstall(configResourcePaths, session,
-						installableResources, confAdmin);
-
-				if (installableResources.isEmpty()) {
-					logger.log("No installable resources that are not installed yet found.");
-					return;
-				}
-
-				logger.log("Installing " + bundleSymbolicNamesToInstall.size() + " bundles and "
-						+ configPidsToInstall.size() + " configs");
-				osgiInstallerServiceRef = getBundleContext().getServiceReference(OsgiInstaller.class);
-				OsgiInstaller osgiInstaller = getBundleContext().getService(osgiInstallerServiceRef);
-
-				OsgiInstallerListener hookInstallationListener = new OsgiInstallerListener(bundleSymbolicNamesToInstall,
-						configPidsToInstall);
-				hookInstallationListenerServiceRegistration = getBundleContext()
-						.registerService(InstallationListener.class, hookInstallationListener, null);
-
-				logger.log("Update resources " + installableResources);
-				osgiInstaller.updateResources(URL_SCHEME,
-						installableResources.toArray(new InstallableResource[installableResources.size()]), null);
-
-				String maxWaitForOsgiInstallerInSecStr = packageProperties
-						.getProperty(PACKAGE_PROPERTY_MAX_WAIT_IN_SEC);
-				int maxWaitForOsgiInstallerInSec = maxWaitForOsgiInstallerInSecStr != null
-						? Integer.parseInt(maxWaitForOsgiInstallerInSecStr)
-						: DEFAULT_MAX_WAIT_IN_SEC;
-
-				long startTime = System.currentTimeMillis();
-				while (!hookInstallationListener.isDone()) {
-					if ((System.currentTimeMillis() - startTime) > maxWaitForOsgiInstallerInSec * 1000) {
-						logger.log("Installable resources " + installableResources
-								+ " could not be installed even after waiting " + maxWaitForOsgiInstallerInSec + "sec");
-						break;
-					}
-					logger.log("Waiting for " + installableResources.size() + " to be installed");
-					Thread.sleep(1000);
-				}
-
-				break;
-			default:
-				break;
-			}
-		} catch (Exception e) {
-			throw new PackageException("Could not execute install hook to for synchronous installation: " + e, e);
-		} finally {
-			if (osgiInstallerServiceRef != null) {
-				getBundleContext().ungetService(osgiInstallerServiceRef);
-			}
-			if (configAdminServiceRef != null) {
-				getBundleContext().ungetService(configAdminServiceRef);
-			}
-			if (slingSettingsServiceRef != null) {
-				getBundleContext().ungetService(slingSettingsServiceRef);
-			}
-
-			if (hookInstallationListenerServiceRegistration != null) {
-				hookInstallationListenerServiceRegistration.unregister();
-			}
-		}
-	}
-
-	private Set<String> getConfigPidsToInstall(List<String> configResourcePaths, Session session,
-			List<InstallableResource> installableResources, ConfigurationAdmin confAdmin)
-			throws IOException, InvalidSyntaxException, RepositoryException {
-		Set<String> configIdsToInstall = new HashSet<>();
-		for (String configResourcePath : configResourcePaths) {
-			boolean needsInstallation = false;
-			String configIdToInstall = StringUtils
-					.substringBefore(StringUtils.substringAfterLast(configResourcePath, "/"), CONFIG_SUFFIX);
-			if (!configIdToInstall.contains("-")) {
-				// non-factory configs
-				Configuration[] activeConfigs = confAdmin.listConfigurations("(service.pid=" + configIdToInstall + ")");
-				if (activeConfigs == null) {
-					logger.log("Config PID " + configIdToInstall + " requires installation");
-
-					needsInstallation = true;
-				}
-			} else {
-				// non-factory configs
-				String factoryPid = StringUtils.substringBefore(configIdToInstall, "-");
-				Configuration[] activeConfigs = confAdmin.listConfigurations("(service.factoryPid=" + factoryPid + ")");
-				if (activeConfigs == null) {
-					logger.log("There is not a single config for factory PID " + factoryPid + " in system, "
-							+ configIdToInstall + " requires installation");
-					needsInstallation = true;
-				}
-			}
-
-			if (needsInstallation) {
-				Node node = session.getNode(configResourcePath);
-				InstallableResource installableResource = convert(node, configResourcePath);
-				installableResources.add(installableResource);
-				configIdsToInstall.add(configIdToInstall);
-			}
-		}
-		return configIdsToInstall;
-	}
-
-	private Set<String> getBundlesToInstall(List<BundleInPackage> bundleResources,
-			Map<String, String> bundleVersionsBySymbolicId, Session session,
-			List<InstallableResource> installableResources) throws RepositoryException, IOException {
-		Set<String> bundleSymbolicNamesToInstall = new HashSet<>();
-		Iterator<BundleInPackage> bundlesIt = bundleResources.iterator();
-		while (bundlesIt.hasNext()) {
-			BundleInPackage bundle = bundlesIt.next();
-
-			String currentlyActiveBundleVersion = bundleVersionsBySymbolicId.get(bundle.symbolicName);
-			boolean needsInstallation = false;
-			if (currentlyActiveBundleVersion == null) {
-				logger.log("Bundle " + bundle.symbolicName + " is not installed");
-				needsInstallation = true;
-			} else if (!currentlyActiveBundleVersion.equals(bundle.version)) {
-				logger.log("Bundle " + bundle.symbolicName + " is installed with version "
-						+ currentlyActiveBundleVersion + " but package contains version " + bundle.version);
-				needsInstallation = true;
-			} else {
-				logger.log("Bundle " + bundle.symbolicName + " is already installed with version "
-						+ currentlyActiveBundleVersion);
-			}
-			if (needsInstallation) {
-				logger.log("Bundle " + bundle.symbolicName + " requires installation");
-				Node node = session.getNode(bundle.path);
-				InstallableResource installableResource = convert(node, bundle.path);
-				installableResources.add(installableResource);
-				bundleSymbolicNamesToInstall.add(bundle.symbolicName);
-			}
-		}
-		return bundleSymbolicNamesToInstall;
-	}
-
-	void collectResources(Archive archive, Entry entry, String dirPath, List<BundleInPackage> bundleResources,
-			List<String> configResources, String installPathRegex, Set<String> actualRunmodes) {
-		String entryName = entry.getName();
-		if (entryName.equals(FOLDER_META_INF)) {
-			return;
-		}
-
-		String dirPathWithoutJcrRoot = StringUtils.substringAfter(dirPath, "/jcr_root");
-		String entryPath = dirPathWithoutJcrRoot + entryName;
-		String dirPathWithoutSlash = StringUtils.chomp(dirPathWithoutJcrRoot, "/");
-
-		boolean runmodesMatch;
-		if (dirPathWithoutSlash.contains(DOT)) {
-			String[] bits = dirPathWithoutSlash.split("\\" + DOT, 2);
-			List<String> runmodesOfResource = Arrays.asList(bits[1].split("\\" + DOT));
-			Set<String> matchingRunmodes = new HashSet<String>(runmodesOfResource);
-			matchingRunmodes.retainAll(actualRunmodes);
-			LOG.debug("Entry with runmode(s): entryPath={} runmodesOfResource={} actualRunmodes={} matchingRunmodes={}",
-					entryPath, runmodesOfResource, actualRunmodes, matchingRunmodes);
-			runmodesMatch = matchingRunmodes.size() == runmodesOfResource.size();
-			if (!runmodesMatch) {
-				logger.log("Skipping installation of  " + entryPath
-						+ " because the path is not matching all actual runmodes " + actualRunmodes);
-			}
-		} else {
-			runmodesMatch = true;
-		}
-
-		if (entryPath.matches(installPathRegex) && runmodesMatch) {
-
-			if (entryName.endsWith(CONFIG_SUFFIX)) {
-				configResources.add(entryPath);
-			} else if (entryName.endsWith(JAR_SUFFIX)) {
-				try (InputStream entryInputStream = archive.getInputSource(entry).getByteStream();
-						JarInputStream jarInputStream = new JarInputStream(entryInputStream)) {
-					Manifest manifest = jarInputStream.getManifest();
-					String symbolicName = manifest.getMainAttributes().getValue(MANIFEST_BUNDLE_SYMBOLIC_NAME);
-					String version = manifest.getMainAttributes().getValue(MANIFEST_BUNDLE_VERSION);
-
-					bundleResources.add(new BundleInPackage(entryPath, symbolicName, version));
-				} catch (Exception e) {
-					throw new IllegalStateException(
-							"Could not read symbolic name and version from manifest of bundle " + entryName);
-				}
-			}
-
-		}
-
-		for (Entry child : entry.getChildren()) {
-			collectResources(archive, child, dirPath + entryName + "/", bundleResources, configResources,
-					installPathRegex, actualRunmodes);
-		}
-	}
-
-	InstallableResource convert(final Node node, final String path) throws IOException, RepositoryException {
-		logger.log("Converting " + node + " at path " + path);
-		final String digest = String.valueOf(node.getProperty(JCR_CONTENT_LAST_MODIFIED).getDate().getTimeInMillis());
-		final InputStream is = node.getProperty(JCR_CONTENT_DATA).getStream();
-		final Dictionary<String, Object> dict = new Hashtable<String, Object>();
-		dict.put(InstallableResource.INSTALLATION_HINT, node.getParent().getName());
-		return new InstallableResource(path, is, dict, digest, null, DEFAULT_PRIORITY_INSTALL_HOOK);
-	}
-
-	// always get fresh bundle context to avoid "Dynamic class loader has already
-	// been deactivated" exceptions
-	private BundleContext getBundleContext() {
-		// use the vault bundle to hook into the OSGi world
-		Bundle currentBundle = FrameworkUtil.getBundle(InstallHook.class);
-		if (currentBundle == null) {
-			throw new IllegalStateException(
-					"The class " + InstallHook.class + " was not loaded through a bundle classloader");
-		}
-		BundleContext bundleContext = currentBundle.getBundleContext();
-		if (bundleContext == null) {
-			throw new IllegalStateException("Could not get bundle context for bundle " + currentBundle);
-		}
-		return bundleContext;
-	}
-
-	class BundleInPackage {
-		final String path;
-		final String symbolicName;
-		final String version;
-
-		public BundleInPackage(String path, String symbolicName, String version) {
-			super();
-			this.path = path;
-			this.symbolicName = symbolicName;
-			this.version = version;
-		}
-
-		@Override
-		public String toString() {
-			return "BundleInPackage [path=" + path + ", symbolicId=" + symbolicName + ", version=" + version + "]";
-		}
-
-	}
-
-	static class InstallHookLogger {
-
-		private ProgressTrackerListener listener;
-
-		public void setOptions(ImportOptions options) {
-			this.listener = options.getListener();
-		}
-
-		public void logError(Logger logger, String message, Throwable throwable) {
-			if (listener != null) {
-				listener.onMessage(ProgressTrackerListener.Mode.TEXT, "ERROR: " + message, "");
-			}
-			logger.error(message, throwable);
-		}
-
-		public void log(String message) {
-			log(LOG, message);
-		}
-
-		public void log(Logger logger, String message) {
-			if (listener != null) {
-				listener.onMessage(ProgressTrackerListener.Mode.TEXT, message, "");
-				logger.debug(message);
-			} else {
-				logger.info(message);
-			}
-		}
-	}
 }
diff --git a/src/main/java/org/apache/sling/installer/provider/installhook/OsgiInstallerListener.java b/src/main/java/org/apache/sling/installer/provider/installhook/OsgiInstallerListener.java
index c0b1ace..4dd6b5a 100644
--- a/src/main/java/org/apache/sling/installer/provider/installhook/OsgiInstallerListener.java
+++ b/src/main/java/org/apache/sling/installer/provider/installhook/OsgiInstallerListener.java
@@ -18,10 +18,10 @@
 */
 package org.apache.sling.installer.provider.installhook;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
-import org.apache.commons.lang.StringUtils;
 import org.apache.sling.installer.api.event.InstallationEvent;
 import org.apache.sling.installer.api.event.InstallationEvent.TYPE;
 import org.apache.sling.installer.api.event.InstallationListener;
@@ -31,57 +31,60 @@ import org.slf4j.LoggerFactory;
 
 public class OsgiInstallerListener implements InstallationListener {
 
-	private static final Logger LOG = LoggerFactory.getLogger(OsgiInstallerListener.class);
+    private static final Logger LOG = LoggerFactory.getLogger(OsgiInstallerListener.class);
 
-	static final String ENTITY_ID_PREFIX_BUNDLE = "bundle:";
-	static final String ENTITY_ID_PREFIX_CONFIG = "config:";
+    private final Set<String> initialBundleUrlsToInstall;
+    private final Set<String> initialConfigUrlsToInstall;
 
-	private final Set<String> requiredBundleSymbolicNames;
-	private final Set<String> requiredConfigPids;
-	private final Set<String> installedBundleSymbolicNames = new HashSet<>();
-	private final Set<String> installedConfigPids = new HashSet<>();
+    private final Set<String> bundleUrlsToInstall;
+    private final Set<String> configUrlsToInstall;
 
-	public OsgiInstallerListener(Set<String> requiredBundleSymbolicNames, Set<String> requiredConfigPids) {
-		this.requiredBundleSymbolicNames = requiredBundleSymbolicNames;
-		this.requiredConfigPids = requiredConfigPids;
-	}
+    public OsgiInstallerListener(Set<String> bundleUrlsToInstall, Set<String> configUrlsToInstall) {
+        this.initialBundleUrlsToInstall = bundleUrlsToInstall;
+        this.initialConfigUrlsToInstall = configUrlsToInstall;
 
-	@Override
-	public void onEvent(InstallationEvent installationEvent) {
-		if (installationEvent.getType() == TYPE.PROCESSED) {
-			Object sourceRaw = installationEvent.getSource();
-			if (!(sourceRaw instanceof TaskResource)) {
-				throw new IllegalStateException("Expected source of type " + TaskResource.class.getName());
-			}
-			TaskResource source = (TaskResource) sourceRaw;
-			String entityId = source.getEntityId();
+        this.bundleUrlsToInstall = Collections.synchronizedSet(new HashSet<>(initialBundleUrlsToInstall));
+        this.configUrlsToInstall = Collections.synchronizedSet(new HashSet<>(initialConfigUrlsToInstall));
+    }
 
-			LOG.debug("Received event about processed entityId {}", entityId);
+    @Override
+    public void onEvent(InstallationEvent installationEvent) {
+        if (installationEvent.getType() == TYPE.PROCESSED) {
+            Object sourceRaw = installationEvent.getSource();
+            if (!(sourceRaw instanceof TaskResource)) {
+                throw new IllegalStateException("Expected source of type " + TaskResource.class.getName());
+            }
+            TaskResource source = (TaskResource) sourceRaw;
+            String entityId = source.getEntityId();
+            String url = source.getURL();
 
-			if (entityId.startsWith(ENTITY_ID_PREFIX_BUNDLE)) {
-				String installedBundleSymbolicName = StringUtils.substringAfter(entityId, ENTITY_ID_PREFIX_BUNDLE);
-				installedBundleSymbolicNames.add(installedBundleSymbolicName);
-			} else if (entityId.startsWith(ENTITY_ID_PREFIX_CONFIG)) {
-				String installedConfigPid = StringUtils.substringAfter(entityId, ENTITY_ID_PREFIX_CONFIG);
-				installedConfigPids.add(installedConfigPid);
-			}
-		}
-	}
+            LOG.trace("Received event about processed entityId={} url={}", entityId, url);
 
-	public boolean isDone() {
-		LOG.trace("requiredBundleSymbolicNames: {}", requiredBundleSymbolicNames);
-		LOG.trace("installedBundleSymbolicNames: {}", installedBundleSymbolicNames);
-		HashSet<String> bundlesLeftToInstall = new HashSet<String>(requiredBundleSymbolicNames);
-		bundlesLeftToInstall.removeAll(installedBundleSymbolicNames);
-		LOG.debug("bundlesLeftToInstall: {}", bundlesLeftToInstall);
+            if (bundleUrlsToInstall.contains(url)) {
+                LOG.debug("Received event for bundle installed with url={}", url);
+                bundleUrlsToInstall.remove(url);
+            }
+            if (configUrlsToInstall.contains(url)) {
+                LOG.debug("Received event for config installed with url={}", url);
+                configUrlsToInstall.remove(url);
+            }
+        }
+    }
 
-		LOG.trace("requiredConfigPids: {}", requiredConfigPids);
-		LOG.trace("installedConfigPids: {}", installedConfigPids);
-		HashSet<String> configsLeftToInstall = new HashSet<String>(requiredConfigPids);
-		configsLeftToInstall.removeAll(installedConfigPids);
-		LOG.debug("configsLeftToInstall: {}", configsLeftToInstall);
+    public int bundlesLeftToInstall() {
+        if (LOG.isTraceEnabled()) {
+            LOG.trace("initialBundleUrlsToInstall: {}", initialBundleUrlsToInstall);
+            LOG.trace("bundleUrlsToInstall: {}", bundleUrlsToInstall);
+        }
+        return bundleUrlsToInstall.size();
+    }
 
-		return bundlesLeftToInstall.isEmpty() && configsLeftToInstall.isEmpty();
-	}
+    public int configsLeftToInstall() {
+        if (LOG.isTraceEnabled()) {
+            LOG.trace("initialConfigUrlsToInstall: {}", initialConfigUrlsToInstall);
+            LOG.trace("configUrlsToInstall: {}", configUrlsToInstall);
+        }
+        return configUrlsToInstall.size();
+    }
 
 }
diff --git a/src/test/java/org/apache/sling/installer/provider/installhook/OsgiInstallerHookTest.java b/src/test/java/org/apache/sling/installer/provider/installhook/OsgiInstallerHookTest.java
index 7d347d1..156fbfe 100644
--- a/src/test/java/org/apache/sling/installer/provider/installhook/OsgiInstallerHookTest.java
+++ b/src/test/java/org/apache/sling/installer/provider/installhook/OsgiInstallerHookTest.java
@@ -36,6 +36,7 @@ import javax.jcr.RepositoryException;
 
 import org.apache.jackrabbit.vault.fs.io.Archive;
 import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
+import org.apache.jackrabbit.vault.packaging.PackageProperties;
 import org.apache.sling.installer.api.InstallableResource;
 import org.apache.sling.installer.provider.installhook.OsgiInstallerHook.BundleInPackage;
 import org.junit.Before;
@@ -47,83 +48,85 @@ import org.mockito.junit.MockitoJUnitRunner;
 @RunWith(MockitoJUnitRunner.class)
 public class OsgiInstallerHookTest {
 
-	OsgiInstallerHook osgiInstallerHook;
-	
-	@Mock
-	Node node;
-
-	@Mock
-	Node parentNode;
-	
-	@Mock
-	Property lastModifiedProperty;
-	
-	@Mock
-	Property contentDataProperty;	
-
-	@Mock
-	Archive archive;	
-	
-	@Mock
-	Entry entry;	
-	
-	Calendar now;
-
-	@Before
-	public void setup() throws RepositoryException  {
-		osgiInstallerHook = new OsgiInstallerHook();
-		
-		now = Calendar.getInstance();
-		when(node.getProperty(OsgiInstallerHook.JCR_CONTENT_LAST_MODIFIED)).thenReturn(lastModifiedProperty);
-		when(lastModifiedProperty.getDate()).thenReturn(now);
-		when(node.getProperty(OsgiInstallerHook.JCR_CONTENT_DATA)).thenReturn(contentDataProperty);
-		when(node.getParent()).thenReturn(parentNode);
-		
-	}
-	
-	@Test
-	public void testConvert() throws IOException, RepositoryException {
-		
-		File pathToTest = new File("/apps/myproj/install/mybundle.jar");
-		when(parentNode.getName()).thenReturn(pathToTest.getParentFile().getName());
-		
-		InstallableResource installableResource = osgiInstallerHook.convert(node, pathToTest.getAbsolutePath());
-
-		assertEquals(String.valueOf(now.getTimeInMillis()), installableResource.getDigest());
-		assertEquals(pathToTest.getParentFile().getName(), installableResource.getDictionary().get(InstallableResource.INSTALLATION_HINT));
-		
-	}
-	
-	public void testCollectResources() throws IOException, RepositoryException {
-		
-		File pathToTest = new File("/apps/myproj/install.author/myconfig.config");
-		String dirPath = "/jcr_root" + pathToTest.getParentFile().getPath() + "/";
-
-		when(entry.getName()).thenReturn(pathToTest.getName());
-		
-		List<BundleInPackage> bundleResources = new ArrayList<BundleInPackage>();
-		List<String> configResources = new ArrayList<String>();
-
-		osgiInstallerHook.collectResources(archive, entry, dirPath, bundleResources, configResources, 
-				"/apps/other.*", new HashSet<String>(Arrays.asList("author", "dev")));
-				
-		assertTrue(bundleResources.isEmpty());
-		assertTrue(configResources.isEmpty());
-
-		osgiInstallerHook.collectResources(archive, entry, dirPath, bundleResources, configResources, 
-				"/apps/myproj.*", new HashSet<String>(Arrays.asList("publish", "dev")));
-				
-		assertTrue(bundleResources.isEmpty());
-		assertTrue(configResources.isEmpty());
-		
-		
-		osgiInstallerHook.collectResources(archive, entry, dirPath, bundleResources, configResources, 
-				"/apps/myproj.*", new HashSet<String>(Arrays.asList("author", "dev")));
-		
-		assertTrue(bundleResources.isEmpty());
-		assertEquals(1, configResources.size());
-		assertEquals(pathToTest.getAbsolutePath(), configResources.get(0));
-		
-	}
+    OsgiInstallerHook osgiInstallerHook;
+
+    @Mock
+    Node node;
+
+    @Mock
+    Node parentNode;
+
+    @Mock
+    Property lastModifiedProperty;
+
+    @Mock
+    Property contentDataProperty;
+
+    @Mock
+    Archive archive;
+
+    @Mock
+    PackageProperties packageProperties;
+
+    @Mock
+    Entry entry;
+
+    Calendar now;
+
+    @Before
+    public void setup() throws RepositoryException {
+        osgiInstallerHook = new OsgiInstallerHook();
+
+        now = Calendar.getInstance();
+        when(node.getProperty(OsgiInstallerHook.JCR_CONTENT_LAST_MODIFIED)).thenReturn(lastModifiedProperty);
+        when(lastModifiedProperty.getDate()).thenReturn(now);
+        when(node.getProperty(OsgiInstallerHook.JCR_CONTENT_DATA)).thenReturn(contentDataProperty);
+        when(node.getParent()).thenReturn(parentNode);
+
+    }
+
+    @Test
+    public void testConvert() throws IOException, RepositoryException {
+
+        File pathToTest = new File("/apps/myproj/install/mybundle.jar");
+        when(parentNode.getName()).thenReturn(pathToTest.getParentFile().getName());
+
+        InstallableResource installableResource = osgiInstallerHook.convert(node, pathToTest.getAbsolutePath(), packageProperties);
+
+        assertEquals(String.valueOf(now.getTimeInMillis()), installableResource.getDigest());
+        assertEquals(pathToTest.getParentFile().getName(), installableResource.getDictionary().get(InstallableResource.INSTALLATION_HINT));
+
+    }
+
+    public void testCollectResources() throws IOException, RepositoryException {
+
+        File pathToTest = new File("/apps/myproj/install.author/myconfig.config");
+        String dirPath = "/jcr_root" + pathToTest.getParentFile().getPath() + "/";
+
+        when(entry.getName()).thenReturn(pathToTest.getName());
+
+        List<BundleInPackage> bundleResources = new ArrayList<BundleInPackage>();
+        List<String> configResources = new ArrayList<String>();
+
+        osgiInstallerHook.collectResources(archive, entry, dirPath, bundleResources, configResources,
+                "/apps/other.*", new HashSet<String>(Arrays.asList("author", "dev")));
+
+        assertTrue(bundleResources.isEmpty());
+        assertTrue(configResources.isEmpty());
+
+        osgiInstallerHook.collectResources(archive, entry, dirPath, bundleResources, configResources,
+                "/apps/myproj.*", new HashSet<String>(Arrays.asList("publish", "dev")));
+
+        assertTrue(bundleResources.isEmpty());
+        assertTrue(configResources.isEmpty());
+
+        osgiInstallerHook.collectResources(archive, entry, dirPath, bundleResources, configResources,
+                "/apps/myproj.*", new HashSet<String>(Arrays.asList("author", "dev")));
+
+        assertTrue(bundleResources.isEmpty());
+        assertEquals(1, configResources.size());
+        assertEquals(pathToTest.getAbsolutePath(), configResources.get(0));
+
+    }
 
 }
diff --git a/src/test/java/org/apache/sling/installer/provider/installhook/OsgiInstallerListenerTest.java b/src/test/java/org/apache/sling/installer/provider/installhook/OsgiInstallerListenerTest.java
index 6cd02ea..90c755b 100644
--- a/src/test/java/org/apache/sling/installer/provider/installhook/OsgiInstallerListenerTest.java
+++ b/src/test/java/org/apache/sling/installer/provider/installhook/OsgiInstallerListenerTest.java
@@ -18,12 +18,10 @@
 */
 package org.apache.sling.installer.provider.installhook;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -34,39 +32,54 @@ import org.junit.Test;
 
 public class OsgiInstallerListenerTest {
 
-	private OsgiInstallerListener osgiInstallerListener;
-	
-	@Test
-	public void testOsgiInstallerListener() {
-		
-		String bundleSymbolicId1 = "org.prj.bundle1";
-		String bundleSymbolicId2 = "org.prj.bundle2";
-		Set<String> requiredBundleSymbolicNames = new HashSet<String>(Arrays.asList(bundleSymbolicId1, bundleSymbolicId2));
-		String configPid1 = "org.prj.config1";
-		String configPid2 = "org.prj.config2";
-		Set<String> requiredConfigPids = new HashSet<String>(Arrays.asList(configPid1, configPid2));
-
-		osgiInstallerListener = new OsgiInstallerListener(requiredBundleSymbolicNames, requiredConfigPids);
-
-		assertFalse(osgiInstallerListener.isDone());
-		osgiInstallerListener.onEvent(getInstallationEventMock(OsgiInstallerListener.ENTITY_ID_PREFIX_BUNDLE + bundleSymbolicId1));
-		assertFalse(osgiInstallerListener.isDone());
-		osgiInstallerListener.onEvent(getInstallationEventMock(OsgiInstallerListener.ENTITY_ID_PREFIX_BUNDLE + bundleSymbolicId2));
-		assertFalse(osgiInstallerListener.isDone());
-		osgiInstallerListener.onEvent(getInstallationEventMock(OsgiInstallerListener.ENTITY_ID_PREFIX_CONFIG + configPid1));
-		assertFalse(osgiInstallerListener.isDone());
-		osgiInstallerListener.onEvent(getInstallationEventMock(OsgiInstallerListener.ENTITY_ID_PREFIX_CONFIG + configPid2));
-		assertTrue(osgiInstallerListener.isDone());
-		
-	}
-	
-	public InstallationEvent getInstallationEventMock(String entityId) {
-		InstallationEvent event = mock(InstallationEvent.class);
-		when(event.getType()).thenReturn(TYPE.PROCESSED);
-		TaskResource taskResource = mock(TaskResource.class);
-		when(event.getSource()).thenReturn(taskResource);
-		when(taskResource.getEntityId()).thenReturn(entityId);
-		return event;
-	}
+    private OsgiInstallerListener osgiInstallerListener;
+
+    @Test
+    public void testOsgiInstallerListener() {
+
+        Set<String> bundleUrlsToInstall = new HashSet<String>();
+        String bundleUrl1 = "jcrinstall:/apps/myproj/install/mybundle1.jar";
+        bundleUrlsToInstall.add(bundleUrl1);
+        String bundleUrl2 = "jcrinstall:/apps/myproj/install/mybundle2.jar";
+        bundleUrlsToInstall.add(bundleUrl2);
+
+        Set<String> configUrlsToInstall = new HashSet<String>();
+        String configUrl1 = "jcrinstall:/apps/myproj/config/conf1.config";
+        configUrlsToInstall.add(configUrl1);
+        String configUrl2 = "jcrinstall:/apps/myproj/config/conf2.config";
+        configUrlsToInstall.add(configUrl2);
+
+        osgiInstallerListener = new OsgiInstallerListener(bundleUrlsToInstall, configUrlsToInstall);
+
+        assertEquals(2, osgiInstallerListener.bundlesLeftToInstall());
+        assertEquals(2, osgiInstallerListener.configsLeftToInstall());
+
+        osgiInstallerListener.onEvent(getInstallationEventMock(bundleUrl1));
+        assertEquals(1, osgiInstallerListener.bundlesLeftToInstall());
+        assertEquals(2, osgiInstallerListener.configsLeftToInstall());
+
+        osgiInstallerListener.onEvent(getInstallationEventMock(bundleUrl2));
+        assertEquals(0, osgiInstallerListener.bundlesLeftToInstall());
+        assertEquals(2, osgiInstallerListener.configsLeftToInstall());
+
+        osgiInstallerListener.onEvent(getInstallationEventMock(configUrl1));
+        assertEquals(0, osgiInstallerListener.bundlesLeftToInstall());
+        assertEquals(1, osgiInstallerListener.configsLeftToInstall());
+
+        osgiInstallerListener.onEvent(getInstallationEventMock(configUrl2));
+        assertEquals(0, osgiInstallerListener.bundlesLeftToInstall());
+        assertEquals(0, osgiInstallerListener.configsLeftToInstall());
+
+    }
+
+    public InstallationEvent getInstallationEventMock(String url) {
+        InstallationEvent event = mock(InstallationEvent.class);
+        when(event.getType()).thenReturn(TYPE.PROCESSED);
+        TaskResource taskResource = mock(TaskResource.class);
+        when(event.getSource()).thenReturn(taskResource);
+        when(taskResource.getURL()).thenReturn(url);
+        when(taskResource.getEntityId()).thenReturn("dummyEntityId");
+        return event;
+    }
 
 }