/*******************************************************************************
* Copyright (c) 2008, 2011 VMware Inc. and others
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware Inc. - initial contribution
* EclipseSource - Bug 358442 Change InstallArtifact graph from a tree to a DAG
*******************************************************************************/
package org.eclipse.virgo.kernel.install.artifact.internal.bundle;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.virgo.kernel.artifact.fs.ArtifactFSEntry;
import org.eclipse.virgo.kernel.deployer.core.internal.BlockingAbortableSignal;
import org.eclipse.virgo.kernel.install.artifact.ArtifactIdentity;
import org.eclipse.virgo.kernel.install.artifact.ArtifactIdentityDeterminer;
import org.eclipse.virgo.kernel.install.artifact.ArtifactStorage;
import org.eclipse.virgo.kernel.install.artifact.BundleInstallArtifact;
import org.eclipse.virgo.kernel.install.artifact.InstallArtifact;
import org.eclipse.virgo.kernel.install.artifact.PlanInstallArtifact;
import org.eclipse.virgo.kernel.install.artifact.internal.AbstractInstallArtifact;
import org.eclipse.virgo.kernel.install.artifact.internal.ArtifactStateMonitor;
import org.eclipse.virgo.kernel.install.artifact.internal.InstallArtifactRefreshHandler;
import org.eclipse.virgo.kernel.install.artifact.internal.scoping.ArtifactIdentityScoper;
import org.eclipse.virgo.kernel.osgi.quasi.QuasiBundle;
import org.eclipse.virgo.medic.eventlog.EventLogger;
import org.eclipse.virgo.nano.core.AbortableSignal;
import org.eclipse.virgo.nano.deployer.api.core.DeployerLogEvents;
import org.eclipse.virgo.nano.deployer.api.core.DeploymentException;
import org.eclipse.virgo.nano.serviceability.NonNull;
import org.eclipse.virgo.util.common.GraphNode;
import org.eclipse.virgo.util.io.FileCopyUtils;
import org.eclipse.virgo.util.io.IOUtils;
import org.eclipse.virgo.util.osgi.manifest.BundleManifest;
import org.eclipse.virgo.util.osgi.manifest.BundleManifestFactory;
import org.eclipse.virgo.util.osgi.manifest.BundleSymbolicName;
import org.eclipse.virgo.util.osgi.manifest.ExportedPackage;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link StandardBundleInstallArtifact} is the default implementation of {@link BundleInstallArtifact}.
* <p />
*
* <strong>Concurrent Semantics</strong><br />
*
* This class is thread safe.
*
*/
final class StandardBundleInstallArtifact extends AbstractInstallArtifact implements BundleInstallArtifact {
private static final String DEFAULTED_BSN = "org-eclipse-virgo-kernel-DefaultedBSN";
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
private static final String EQUINOX_SYSTEM_BUNDLE_NAME = "org.eclipse.osgi";
private static final String RESERVED_SYSTEM_BUNDLE_NAME = "system.bundle";
private static final long REFRESH_RESTART_WAIT_PERIOD = 60;
private final Object monitor = new Object();
private final ArtifactStorage artifactStorage;
private final BundleDriver bundleDriver;
private final InstallArtifactRefreshHandler refreshHandler;
private final ArtifactIdentityDeterminer identityDeterminer;
private BundleManifest bundleManifest;
private QuasiBundle quasiBundle;
private Bundle cachedBundle;
private File cachedBundleFile;
/**
* Construct a {@link StandardBundleInstallArtifact} with the given type and {@link ArtifactStorage}, none of which
* may be <code>null</code>.
*
* @param artifactIdentifier
* @param bundleManifest
* @param artifactStorage the bundle artifact storage
* @param bundleDriver a {@link BundleDriver} for manipulating the bundle
* @param artifactStateMonitor
* @param refreshHandler
* @param repositoryName
* @param eventLogger
* @param identityDeterminer
*/
public StandardBundleInstallArtifact(@NonNull ArtifactIdentity artifactIdentifier, @NonNull BundleManifest bundleManifest,
@NonNull ArtifactStorage artifactStorage, @NonNull BundleDriver bundleDriver, @NonNull ArtifactStateMonitor artifactStateMonitor,
@NonNull InstallArtifactRefreshHandler refreshHandler, String repositoryName, EventLogger eventLogger,
ArtifactIdentityDeterminer identityDeterminer) {
super(artifactIdentifier, artifactStorage, artifactStateMonitor, repositoryName, eventLogger);
this.artifactStorage = artifactStorage;
this.bundleManifest = bundleManifest;
this.bundleDriver = bundleDriver;
this.refreshHandler = refreshHandler;
this.identityDeterminer = identityDeterminer;
synchronizeBundleSymbolicNameWithIdentity();
}
private void synchronizeBundleSymbolicNameWithIdentity() {
BundleManifest bundleManifest = this.bundleManifest;
BundleSymbolicName bundleSymbolicName = bundleManifest.getBundleSymbolicName();
if (!getName().equals(bundleSymbolicName.getSymbolicName())) {
bundleSymbolicName.setSymbolicName(getName());
bundleManifest.setHeader(DEFAULTED_BSN, "true");
}
}
/**
* {@inheritDoc}
*/
public BundleManifest getBundleManifest() throws IOException {
synchronized (this.monitor) {
if (this.bundleManifest == null) {
this.bundleManifest = getManifestFromArtifactFS();
}
return this.bundleManifest;
}
}
private BundleManifest getManifestFromArtifactFS() throws IOException {
ArtifactFSEntry manifestEntry = this.artifactStorage.getArtifactFS().getEntry(MANIFEST_ENTRY_NAME);
if (manifestEntry != null && manifestEntry.exists()) {
try (Reader manifestReader = new InputStreamReader(manifestEntry.getInputStream(), UTF_8)) {
return BundleManifestFactory.createBundleManifest(manifestReader);
}
} else {
return BundleManifestFactory.createBundleManifest();
}
}
/**
* {@inheritDoc}
*/
public QuasiBundle getQuasiBundle() {
synchronized (this.monitor) {
return this.quasiBundle;
}
}
/**
* {@inheritDoc}
*/
public void setQuasiBundle(QuasiBundle quasiBundle) {
synchronized (this.monitor) {
this.quasiBundle = quasiBundle;
}
}
/**
* {@inheritDoc}
*/
public Bundle getBundle() {
synchronized (this.monitor) {
return this.quasiBundle == null ? this.cachedBundle : this.quasiBundle.getBundle();
}
}
private File getBundleFile() {
synchronized (this.monitor) {
return this.quasiBundle == null ? this.cachedBundleFile : this.quasiBundle.getBundleFile();
}
}
/**
* {@inheritDoc}
*/
@Override
public State getState() {
// Before returning the state, ensure any bundle is set into the bundle state monitor.
monitorBundle();
State state = super.getState();
// avoid exposing inappropriate states for fragments
return (isFragment() && (state == State.STARTING || state == State.ACTIVE || state == State.STOPPING)) ? State.RESOLVED : state;
}
/**
* {@inheritDoc}
*/
@Override
public void endInstall() throws DeploymentException {
monitorBundle();
super.endInstall();
cacheAndDelete();
}
private void monitorBundle() {
synchronized (this.monitor) {
Bundle bundle = getBundle();
if (bundle != null) {
this.bundleDriver.setBundle(bundle);
}
}
}
/**
* Cache the <code>Bundle</code> contained within the <code>quasiBundle</code> and set the <code>quasiBundle</code>
* instance to <code>null</code>. This is a fix for this PR: https://bugs.eclipse.org/bugs/show_bug.cgi?id=424872
*/
private void cacheAndDelete() {
synchronized (this.monitor) {
if (this.quasiBundle == null) {
return;
}
this.cachedBundle = this.quasiBundle.getBundle();
this.cachedBundleFile = this.quasiBundle.getBundleFile();
this.quasiBundle = null;
}
}
/**
* {@inheritDoc}
*/
@Override
public void pushThreadContext() {
this.bundleDriver.pushThreadContext();
}
/**
* {@inheritDoc}
*/
@Override
public void popThreadContext() {
this.bundleDriver.popThreadContext();
}
/**
* Track an unsolicited start of the bundle.
*/
void trackStart() {
AbortableSignal signal = createStateMonitorSignal(null);
this.bundleDriver.trackStart(signal);
}
@Override
public void beginInstall() throws DeploymentException {
if (isFragmentOnSystemBundle()) {
throw new DeploymentException("Deploying fragments of the system bundle is not supported");
}
super.beginInstall();
}
private boolean isFragment() {
return this.bundleManifest.getFragmentHost().getBundleSymbolicName() != null;
}
private boolean isFragmentOnSystemBundle() {
String fragmentHost = this.bundleManifest.getFragmentHost().getBundleSymbolicName();
if (fragmentHost != null) {
return fragmentHost.equals(EQUINOX_SYSTEM_BUNDLE_NAME) || fragmentHost.equals(RESERVED_SYSTEM_BUNDLE_NAME);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public void start(AbortableSignal signal) throws DeploymentException {
if (!hasStartingParent()) {
topLevelStart();
}
/*
* Do not call super.start(signal) as it is essential that the starting event is driven under the bundle
* lifecycle event so the listeners see a suitable bundle state.
*/
pushThreadContext();
try {
driveDoStart(signal);
} finally {
popThreadContext();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void doStart(AbortableSignal signal) throws DeploymentException {
this.bundleDriver.start(signal);
}
/**
* {@inheritDoc}
*/
@Override
public void stop() throws DeploymentException {
if (this.getBundle().getState() == Bundle.ACTIVE && shouldStop()) {
/*
* Do not call super.stop() as it is essential that stopping and stopped events are driven under the bundle
* lifecycle events so the listeners see a suitable bundle state, however we must ensure that we ignore
* requests if the bundle is already stopping to prevent stop being performed more than once.
*/
pushThreadContext();
try {
doStop();
} catch (DeploymentException e) {
getStateMonitor().onStopFailed(this, e);
} finally {
popThreadContext();
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void doStop() throws DeploymentException {
this.bundleDriver.stop();
}
/**
* {@inheritDoc}
*/
@Override
public void uninstall() throws DeploymentException {
super.uninstall();
}
@Override
protected void doUninstall() throws DeploymentException {
this.bundleDriver.uninstall();
}
private boolean isScoped() {
return this.getScopeName() != null;
}
private boolean stopBundleIfNecessary() throws DeploymentException {
int bundleState = this.getBundle().getState();
boolean stopBundle = bundleState == Bundle.STARTING || bundleState == Bundle.ACTIVE;
if (stopBundle) {
stop();
return true;
}
return false;
}
private boolean completeUpdateAndRefresh(boolean startRequired) {
try {
boolean refreshed = this.bundleDriver.update(bundleManifest, this.artifactStorage.getArtifactFS().getFile());
if (refreshed) {
if (startRequired) {
BlockingAbortableSignal blockingSignal = new BlockingAbortableSignal(true);
start(blockingSignal);
try {
refreshed = blockingSignal.checkComplete();
} catch (DeploymentException e) {
refreshed = false;
}
}
}
return refreshed;
} catch (Exception e) {
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean doRefresh() throws DeploymentException {
BundleManifest currentBundleManifest;
synchronized (this.monitor) {
currentBundleManifest = this.bundleManifest;
}
BundleManifest newBundleManifest;
try {
newBundleManifest = getManifestFromArtifactFS();
} catch (IOException ioe) {
throw new DeploymentException("Failed to read new bundle manifest during refresh", ioe);
}
ArtifactIdentity newIdentity = this.identityDeterminer.determineIdentity(this.artifactStorage.getArtifactFS().getFile(), getScopeName());
if (newIdentity == null) {
throw new DeploymentException("Failed to determine new identity during refresh");
}
newIdentity = ArtifactIdentityScoper.scopeArtifactIdentity(newIdentity);
if (!isNameAndVersionUnchanged(newIdentity)) {
return false;
}
/*
* To avoid this module's bundle from being stopped and started by each of update and refresh packages, stop it
* if necessary and restart it later if we had to stop it.
*/
boolean bundleStopped = stopBundleIfNecessary();
synchronized (this.monitor) {
this.bundleManifest = newBundleManifest;
}
synchronizeBundleSymbolicNameWithIdentity();
if (!refreshScope()) {
synchronized (this.monitor) {
this.bundleManifest = currentBundleManifest;
}
startIfNecessary(bundleStopped);
return false;
}
if (isScoped() && !isExportPackageUnchanged(currentBundleManifest, newBundleManifest)) {
this.eventLogger.log(DeployerLogEvents.CANNOT_REFRESH_BUNDLE_AS_SCOPED_AND_EXPORTS_CHANGED, getName(), getVersion());
synchronized (this.monitor) {
this.bundleManifest = currentBundleManifest;
}
startIfNecessary(bundleStopped);
return false;
}
boolean refreshSuccessful = completeUpdateAndRefresh(bundleStopped);
if (!refreshSuccessful) {
synchronized (this.monitor) {
this.bundleManifest = currentBundleManifest;
}
startIfNecessary(bundleStopped);
}
return refreshSuccessful;
}
private void startIfNecessary(boolean bundleStopped) throws DeploymentException {
if (bundleStopped) {
BlockingAbortableSignal signal = new BlockingAbortableSignal(true);
start(signal);
signal.awaitCompletion(REFRESH_RESTART_WAIT_PERIOD);
}
}
private boolean isExportPackageUnchanged(BundleManifest currentBundleManifest, BundleManifest newBundleManifest) {
Set<ExportedPackage> previousExportedPackageSet = getExportedPackageSet(currentBundleManifest.getExportPackage().getExportedPackages());
Set<ExportedPackage> newExportedPackageSet = getExportedPackageSet(newBundleManifest.getExportPackage().getExportedPackages());
return newExportedPackageSet.equals(previousExportedPackageSet);
}
private boolean isNameAndVersionUnchanged(ArtifactIdentity newIdentity) {
if (newIdentity.getName().equals(getName()) && newIdentity.getVersion().equals(getVersion())) {
return true;
}
this.eventLogger.log(DeployerLogEvents.CANNOT_REFRESH_BUNDLE_IDENTITY_CHANGED, getName(), getVersion(), newIdentity.getName(),
newIdentity.getVersion());
return false;
}
// TODO DAG - think what to do with shared subgraphs.
// If this bundle belongs to a plan, run the subtree of any scoped plan containing the bundle through the refresh
// subpipeline.
private boolean refreshScope() {
boolean refreshed;
PlanInstallArtifact scopedAncestor = getScopedAncestor();
if (scopedAncestor != null) {
refreshed = scopedAncestor.refreshScope();
} else {
refreshed = this.refreshHandler.refresh(this);
}
return refreshed;
}
PlanInstallArtifact getScopedAncestor() {
// If the bundle belongs to a scoped plan, that plan may be found by searching up any line of ancestors.
List<GraphNode<InstallArtifact>> ancestors = getGraph().getParents();
while (!ancestors.isEmpty()) {
GraphNode<InstallArtifact> ancestor = ancestors.get(0);
InstallArtifact ancestorArtifact = ancestor.getValue();
PlanInstallArtifact planAncestor = (PlanInstallArtifact) ancestorArtifact;
if (planAncestor.isScoped()) {
return planAncestor;
} else {
ancestor = ancestors.get(0);
ancestors = ancestor.getParents();
}
}
return null;
}
private Set<ExportedPackage> getExportedPackageSet(List<ExportedPackage> exportedPackages) {
Set<ExportedPackage> packageExports = new HashSet<ExportedPackage>();
for (ExportedPackage exportPackage : exportedPackages) {
packageExports.add(exportPackage);
}
return packageExports;
}
public void deleteEntry(String targetPath) {
deleteEntry(getBundleFile(), targetPath);
getArtifactFS().getEntry(targetPath).delete();
}
private void deleteEntry(File root, String path) {
if (root.isDirectory()) {
File f = new File(root, path);
if (f.exists() && !f.delete()) {
logger.warn("Unable to delete resource at {}", path);
}
} else {
logger.warn("Unable to delete resource in non-directory location at {}", root.getAbsolutePath());
}
}
public void updateEntry(URI inputPath, String targetPath) {
updateEntry(getBundleFile(), inputPath, targetPath);
updateEntry(getArtifactFS().getEntry(targetPath), inputPath);
}
private void updateEntry(ArtifactFSEntry entry, URI inputPath) {
doUpdate(inputPath, entry.getOutputStream(), entry.getArtifactFS().getFile().getAbsolutePath() + File.separatorChar + entry.getPath());
}
private void updateEntry(File root, URI inputPath, String targetPath) {
try {
if (root.isDirectory()) {
FileOutputStream out = null;
try {
out = new FileOutputStream(new File(root, targetPath));
doUpdate(inputPath, out, targetPath);
} finally {
IOUtils.closeQuietly(out);
}
} else {
logger.warn("Unable to update resource in non-directory location at {}", root.getAbsolutePath());
}
} catch (Exception e) {
logger.warn("Unable to update resource at {} with resource at {}", targetPath, inputPath.toASCIIString());
}
}
private void doUpdate(URI input, OutputStream output, String targetPath) {
try {
InputStream in = input.toURL().openStream();
FileCopyUtils.copy(in, output);
} catch (Exception e) {
logger.warn("Unable to update resource at {} with resource at {}", targetPath, input.toASCIIString());
}
}
}