blob: 6ca0eec0688538c3d710a2746e5a2704917efd06 [file] [log] [blame]
/*
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.Arrays;
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.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.PackageProperties;
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.InstallationEvent;
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.installer.api.tasks.TaskResource;
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.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_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;
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";
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();
public OsgiInstallerHook() {
LOG.debug("Preloading classes to ensure to not run into a NoClassDefFoundError"
+ " due to a reloading dynamic classloader: {}, {}, {}, {}",
new Object[] { TaskResource.class, InstallationEvent.TYPE.class, ResourceState.class,
InstallerHookOsgiEventListener.class });
}
@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);
// the events are not always reliably received, also update listener explicitly with current installation state
hookInstallationListener.updateWith(infoProvider.getInstallationState().getInstalledResources());
}
if (bundlesLeftToInstall == 0 && configsLeftToInstall == 0) {
logger.log("All " + bundlesToInstallByUrl.size() + " bundles / " + configsToInstallByUrl.size()
+ " configs have been successfully installed in " + (System.currentTimeMillis() - startTime) + "ms");
}
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).getBinary().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.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");
}
}
}
}