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:20 UTC

[sling-org-apache-sling-installer-provider-installhook] 01/24: SLING-7790 Initial implementation

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 f813390c44954337b5acf5e9a80fc61d883cb22f
Author: Georg Henzler <ge...@netcentric.biz>
AuthorDate: Fri Jul 27 17:56:18 2018 +0200

    SLING-7790 Initial implementation
---
 .gitignore                                         |   5 +
 README.md                                          |   9 +
 pom.xml                                            | 102 ++++++
 .../provider/installhook/OsgiInstallerHook.java    | 354 +++++++++++++++++++++
 .../installhook/OsigInstallerListener.java         |  87 +++++
 5 files changed, 557 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8b0fe0c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/target/
+.project
+.classpath
+.settings/
+.DS_Store
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fdde553
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+[<img src="http://sling.apache.org/res/logos/sling.png"/>](http://sling.apache.org)
+
+ [![Build Status](https://builds.apache.org/buildStatus/icon?job=org.apache.sling.installer.provider.installhook-1.8)](https://builds.apache.org/view/S-Z/view/Sling/job/org.apache.sling.installer.provider.installhook-1.8) [![Test Status](https://img.shields.io/jenkins/t/https/builds.apache.org/view/S-Z/view/Sling/job/org.apache.sling.installer.provider.installhook-1.8.svg)](https://builds.apache.org/view/S-Z/view/Sling/job/org.apache.sling.installer.provider.installhook-1.8/test_results_ [...]
+
+# Apache Sling JCR Installer Install Hook
+
+This module is part of the [Apache Sling](https://sling.apache.org) project.
+
+Allows to synchronously install bundles and configurations as contained in a vault package. 
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..820c8bb
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor 
+	license agreements. See the NOTICE file distributed with this work for additional 
+	information regarding copyright ownership. The ASF licenses this file to 
+	you under the Apache License, Version 2.0 (the "License"); you may not use 
+	this file except in compliance with the License. You may obtain a copy of 
+	the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required 
+	by applicable law or agreed to in writing, software distributed under the 
+	License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 
+	OF ANY KIND, either express or implied. See the License for the specific 
+	language governing permissions and limitations under the License. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.sling</groupId>
+		<artifactId>sling</artifactId>
+		<version>30</version>
+		<relativePath />
+	</parent>
+
+	<artifactId>org.apache.sling.installer.provider.installhook</artifactId>
+	<version>1.0.0-SNAPSHOT</version>
+	<packaging>jar</packaging>
+
+	<name>Sling Installer Vault Package Install Hook</name>
+	<description>
+        Can be used in packages to ensure installation of bundles/configs during package installation (circumventing )
+    </description>
+
+	<properties>
+		<jackrabbit.version>2.10.0</jackrabbit.version>
+		<sling.java.version>7</sling.java.version>
+		<filevault.version>3.1.18</filevault.version>
+		<jackrabbit.version>2.10.1</jackrabbit.version>
+	</properties>
+
+	<build>
+		<plugins>
+			<plugin>
+				<artifactId>maven-jar-plugin</artifactId>
+				<configuration>
+					<archive>
+						<manifest>
+							<mainClass>org.apache.sling.installer.provider.installhook.OsgiInstallerHook</mainClass>
+						</manifest>
+					</archive>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+		</dependency>
+	
+		<dependency>
+			<groupId>org.apache.jackrabbit.vault</groupId>
+			<artifactId>org.apache.jackrabbit.vault</artifactId>
+			<version>${filevault.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.jackrabbit</groupId>
+			<artifactId>jackrabbit-jcr-commons</artifactId>
+			<version>${jackrabbit.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>javax.jcr</groupId>
+			<artifactId>jcr</artifactId>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>commons-lang</groupId>
+			<artifactId>commons-lang</artifactId>
+			<version>2.5</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.installer.core</artifactId>
+			<version>3.6.8</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.osgi</groupId>
+			<artifactId>osgi.core</artifactId>
+			<scope>provided</scope>
+		</dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.cmpn</artifactId>
+            <scope>provided</scope>
+        </dependency>		
+		
+	</dependencies>
+
+</project>
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
new file mode 100644
index 0000000..de615ce
--- /dev/null
+++ b/src/main/java/org/apache/sling/installer/provider/installhook/OsgiInstallerHook.java
@@ -0,0 +1,354 @@
+/*
+    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.installer.provider.installhook;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.jackrabbit.vault.fs.api.ProgressTrackerListener;
+import org.apache.jackrabbit.vault.fs.io.Archive;
+import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
+import org.apache.jackrabbit.vault.fs.io.ImportOptions;
+import org.apache.jackrabbit.vault.packaging.InstallContext;
+import org.apache.jackrabbit.vault.packaging.InstallHook;
+import org.apache.jackrabbit.vault.packaging.PackageException;
+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.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+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 String PACKAGE_PROPERTY_MAX_WAIT_IN_SEC = "maxWaitForOsgiInstallerInSec";
+	private static final int DEFAULT_MAX_WAIT_IN_SEC = 120;
+
+	private static final String MANIFEST_BUNDLE_SYMBOLIC_NAME = "Bundle-SymbolicName";
+	private static final String MANIFEST_BUNDLE_VERSION = "Bundle-Version";
+
+	private static final String JCR_CONTENT = "jcr:content";
+	private static final String JCR_CONTENT_DATA = JCR_CONTENT + "/jcr:data";
+	private static final String JCR_LAST_MODIFIED = "jcr:lastModified";
+	private static final String JCR_CONTENT_LAST_MODIFIED = JCR_CONTENT + "/" + JCR_LAST_MODIFIED;
+
+	public static final String URL_SCHEME = "jcrinstall";
+	public static final String CONFIG_SUFFIX = ".config";
+
+	private InstallHookLogger logger = new InstallHookLogger();
+
+	@Override
+	public void execute(InstallContext context) throws PackageException {
+
+		ServiceReference<OsgiInstaller> osgiInstallerServiceRef = null;
+		ServiceReference<ConfigurationAdmin> configAdminServiceRef = null;
+		ServiceRegistration<InstallationListener> hookInstallationListenerServiceRegistration = null;
+		try {
+			switch (context.getPhase()) {
+			case INSTALLED:
+				ImportOptions options = context.getOptions();
+				logger.setOptions(options);
+				VaultPackage vaultPackage = context.getPackage();
+
+				logger.log(getClass().getSimpleName() + " is active in " + vaultPackage.getId());
+
+				List<BundleInPackage> bundleResources = new ArrayList<>();
+				List<String> configResourcePaths = new ArrayList<>();
+				Archive archive = vaultPackage.getArchive();
+				collectResources(archive, archive.getRoot(), "", bundleResources, configResourcePaths);
+
+				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);
+
+				configAdminServiceRef = getBundleContext().getServiceReference(ConfigurationAdmin.class);
+				ConfigurationAdmin confAdmin = (ConfigurationAdmin) getBundleContext()
+						.getService(configAdminServiceRef);
+
+				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);
+
+				OsigInstallerListener hookInstallationListener = new OsigInstallerListener(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 = vaultPackage.getProperties()
+						.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 apply env vars: " + e, e);
+		} finally {
+			if (osgiInstallerServiceRef != null) {
+				getBundleContext().ungetService(osgiInstallerServiceRef);
+			}
+			if (configAdminServiceRef != null) {
+				getBundleContext().ungetService(configAdminServiceRef);
+			}
+
+			if (hookInstallationListenerServiceRegistration != null) {
+				hookInstallationListenerServiceRegistration.unregister();
+			}
+		}
+	}
+
+	private Set<String> getConfigPidsToInstall(List<String> configResourcePaths, Session session,
+			List<InstallableResource> installableResources, ConfigurationAdmin confAdmin)
+			throws IOException, InvalidSyntaxException, PathNotFoundException, 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 PathNotFoundException, 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;
+	}
+
+	private void collectResources(Archive archive, Entry entry, String dirPath, List<BundleInPackage> bundleResources,
+			List<String> configResources) {
+		String entryName = entry.getName();
+
+		if (entryName.endsWith(".jar") && dirPath.contains("/install")) {
+
+			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);
+				String bundlePath = StringUtils.substringAfter(dirPath + entryName, "/jcr_root");
+				bundleResources.add(new BundleInPackage(bundlePath, symbolicName, version));
+			} catch (Exception e) {
+				throw new IllegalStateException(
+						"Could not read symbolic name and version from manifest of bundle " + entryName);
+			}
+		}
+
+		if (entryName.endsWith(CONFIG_SUFFIX) && dirPath.contains("/config")) {
+			String configPath = StringUtils.substringAfter(dirPath + entryName, "/jcr_root");
+			configResources.add(configPath);
+		}
+
+		for (Entry child : entry.getChildren()) {
+			collectResources(archive, child, dirPath + entryName + "/", bundleResources, configResources);
+		}
+	}
+
+	private 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, null);
+	}
+
+	// always get fresh bundle context to avoid "Dynamic class loader has already
+	// been deactivated" exceptions
+	public 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 + "]";
+		}
+
+	}
+
+	class InstallHookLogger {
+
+		private ImportOptions options;
+
+		public void setOptions(ImportOptions options) {
+			this.options = options;
+		}
+
+		public void logError(Logger logger, String message, Throwable throwable) {
+			ProgressTrackerListener listener = options.getListener();
+			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) {
+			ProgressTrackerListener listener = options.getListener();
+			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/OsigInstallerListener.java b/src/main/java/org/apache/sling/installer/provider/installhook/OsigInstallerListener.java
new file mode 100644
index 0000000..8cb83e7
--- /dev/null
+++ b/src/main/java/org/apache/sling/installer/provider/installhook/OsigInstallerListener.java
@@ -0,0 +1,87 @@
+/*
+    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.installer.provider.installhook;
+
+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;
+import org.apache.sling.installer.api.tasks.TaskResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OsigInstallerListener implements InstallationListener {
+
+	private static final Logger LOG = LoggerFactory.getLogger(OsigInstallerListener.class);
+
+	private static final String ENTITY_ID_PREFIX_BUNDLE = "bundle:";
+	private static final String ENTITY_ID_PREFIX_CONFIG = "config:";
+
+	private final Set<String> requiredBundleSymbolicNames;
+	private final Set<String> requiredConfigPids;
+	private final Set<String> installedBundleSymbolicNames = new HashSet<>();
+	private final Set<String> installedConfigPids = new HashSet<>();
+
+	public OsigInstallerListener(Set<String> requiredBundleSymbolicNames, Set<String> requiredConfigPids) {
+		this.requiredBundleSymbolicNames = requiredBundleSymbolicNames;
+		this.requiredConfigPids = requiredConfigPids;
+	}
+
+	@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();
+
+			LOG.debug("Received event about processed entityId {}", entityId);
+
+			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);
+			}
+		}
+	}
+
+	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);
+
+		LOG.trace("requiredConfigPids: {}", requiredConfigPids);
+		LOG.trace("installedConfigPids: {}", installedConfigPids);
+		HashSet<String> configsLeftToInstall = new HashSet<String>(requiredConfigPids);
+		requiredConfigPids.removeAll(installedConfigPids);
+		LOG.debug("configsLeftToInstall: {}", configsLeftToInstall);
+
+		return bundlesLeftToInstall.isEmpty() && configsLeftToInstall.isEmpty();
+	}
+
+}