blob: a8d910b44752ee83b74f9a0845936e7de42de7cf [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.InstallationListener;
import org.apache.sling.settings.SlingSettingsService;
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 String PACKAGE_PROP_INSTALL_PATH_REGEX = "installPathRegex";
public static final int DEFAULT_PRIORITY_INSTALL_HOOK = 2000;
private static final int DEFAULT_MAX_WAIT_IN_SEC = 60;
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 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);
}
}
}
}