blob: 629e48dca6eaf641728b03ac86c5ba339b6958ff [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.felix.fileinstall.internal;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import org.apache.felix.fileinstall.ArtifactInstaller;
import org.apache.felix.fileinstall.ArtifactListener;
import org.apache.felix.fileinstall.ArtifactTransformer;
import org.apache.felix.fileinstall.ArtifactUrlTransformer;
import org.apache.felix.fileinstall.internal.Util.Logger;
import org.apache.felix.utils.manifest.Clause;
import org.apache.felix.utils.manifest.Parser;
import org.apache.felix.utils.version.VersionRange;
import org.apache.felix.utils.version.VersionTable;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleException;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.framework.startlevel.BundleStartLevel;
import org.osgi.framework.startlevel.FrameworkStartLevel;
import org.osgi.framework.wiring.BundleRevision;
/**
* -DirectoryWatcher-
*
* This class runs a background task that checks a directory for new files or
* removed files. These files can be configuration files or jars.
* For jar files, its behavior is defined below:
* - If there are new jar files, it installs them and optionally starts them.
* - If it fails to install a jar, it does not try to install it again until
* the jar has been modified.
* - If it fail to start a bundle, it attempts to start it in following
* iterations until it succeeds or the corresponding jar is uninstalled.
* - If some jar files have been deleted, it uninstalls them.
* - If some jar files have been updated, it updates them.
* - If it fails to update a bundle, it tries to update it in following
* iterations until it is successful.
* - If any bundle gets updated or uninstalled, it refreshes the framework
* for the changes to take effect.
* - If it detects any new installations, uninstallations or updations,
* it tries to start all the managed bundle unless it has been configured
* to only install bundles.
*
* @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
*/
public class DirectoryWatcher extends Thread implements BundleListener
{
public final static String FILENAME = "felix.fileinstall.filename";
public final static String POLL = "felix.fileinstall.poll";
public final static String DIR = "felix.fileinstall.dir";
public final static String LOG_LEVEL = "felix.fileinstall.log.level";
public final static String LOG_DEFAULT = "felix.fileinstall.log.default";
public final static String TMPDIR = "felix.fileinstall.tmpdir";
public final static String FILTER = "felix.fileinstall.filter";
public final static String START_NEW_BUNDLES = "felix.fileinstall.bundles.new.start";
public final static String USE_START_TRANSIENT = "felix.fileinstall.bundles.startTransient";
public final static String USE_START_ACTIVATION_POLICY = "felix.fileinstall.bundles.startActivationPolicy";
public final static String NO_INITIAL_DELAY = "felix.fileinstall.noInitialDelay";
public final static String DISABLE_CONFIG_SAVE = "felix.fileinstall.disableConfigSave";
public final static String ENABLE_CONFIG_SAVE = "felix.fileinstall.enableConfigSave";
public final static String CONFIG_ENCODING = "felix.fileinstall.configEncoding";
public final static String START_LEVEL = "felix.fileinstall.start.level";
public final static String ACTIVE_LEVEL = "felix.fileinstall.active.level";
public final static String UPDATE_WITH_LISTENERS = "felix.fileinstall.bundles.updateWithListeners";
public final static String OPTIONAL_SCOPE = "felix.fileinstall.optionalImportRefreshScope";
public final static String FRAGMENT_SCOPE = "felix.fileinstall.fragmentRefreshScope";
public final static String DISABLE_NIO2 = "felix.fileinstall.disableNio2";
public final static String SUBDIR_MODE = "felix.fileinstall.subdir.mode";
public final static String SCOPE_NONE = "none";
public final static String SCOPE_MANAGED = "managed";
public final static String SCOPE_ALL = "all";
public final static String LOG_STDOUT = "stdout";
public final static String LOG_JUL = "jul";
final FileInstall fileInstall;
Map<String, String> properties;
File watchedDirectory;
File tmpDir;
long poll;
int logLevel;
boolean startBundles;
boolean useStartTransient;
boolean useStartActivationPolicy;
String filter;
BundleContext context;
private Bundle systemBundle;
String originatingFileName;
boolean noInitialDelay;
int startLevel;
int activeLevel;
boolean updateWithListeners;
String fragmentScope;
String optionalScope;
boolean disableNio2;
int frameworkStartLevel;
// Map of all installed artifacts
final Map<File, Artifact> currentManagedArtifacts = new HashMap<File, Artifact>();
// The scanner to report files changes
Scanner scanner;
// Represents files that could not be processed because of a missing artifact listener
final Set<File> processingFailures = new HashSet<File>();
// Represents installed artifacts which need to be started later because they failed to start
Set<Bundle> delayedStart = new HashSet<Bundle>();
// Represents consistently failing bundles
Set<Bundle> consistentlyFailingBundles = new HashSet<Bundle>();
// Represents artifacts that could not be installed
final Map<File, Artifact> installationFailures = new HashMap<File, Artifact>();
// flag (acces to which must be synchronized) that indicates wheter there's a change in state of system,
// which may result in an attempt to start the watched bundles
private AtomicBoolean stateChanged = new AtomicBoolean();
public DirectoryWatcher(FileInstall fileInstall, Map<String, String> properties, BundleContext context)
{
super("fileinstall-" + getThreadName(properties));
this.fileInstall = fileInstall;
this.properties = properties;
this.context = context;
systemBundle = context.getBundle(Constants.SYSTEM_BUNDLE_LOCATION);
poll = getLong(properties, POLL, 2000);
logLevel = getInt(properties, LOG_LEVEL, Util.getGlobalLogLevel(context));
originatingFileName = properties.get(FILENAME);
watchedDirectory = getFile(properties, DIR, new File("./load"));
verifyWatchedDir();
tmpDir = getFile(properties, TMPDIR, null);
prepareTempDir();
startBundles = getBoolean(properties, START_NEW_BUNDLES, true); // by default, we start bundles.
useStartTransient = getBoolean(properties, USE_START_TRANSIENT, false); // by default, we start bundles persistently.
useStartActivationPolicy = getBoolean(properties, USE_START_ACTIVATION_POLICY, true); // by default, we start bundles using activation policy.
filter = properties.get(FILTER);
noInitialDelay = getBoolean(properties, NO_INITIAL_DELAY, false);
startLevel = getInt(properties, START_LEVEL, 0); // by default, do not touch start level
activeLevel = getInt(properties, ACTIVE_LEVEL, 0); // by default, always scan
updateWithListeners = getBoolean(properties, UPDATE_WITH_LISTENERS, false); // Do not update bundles when listeners are updated
fragmentScope = properties.get(FRAGMENT_SCOPE);
optionalScope = properties.get(OPTIONAL_SCOPE);
disableNio2 = getBoolean(properties, DISABLE_NIO2, false);
this.context.addBundleListener(this);
if (disableNio2) {
scanner = new Scanner(watchedDirectory, filter, properties.get(SUBDIR_MODE));
} else {
try {
scanner = new WatcherScanner(context, watchedDirectory, filter, properties.get(SUBDIR_MODE));
} catch (Throwable t) {
scanner = new Scanner(watchedDirectory, filter, properties.get(SUBDIR_MODE));
}
}
}
private void verifyWatchedDir()
{
if (!watchedDirectory.exists())
{
// Issue #2069: Do not create the directory if it does not exist,
// instead, warn user and continue. We will automatically start
// monitoring the dir when it becomes available.
log(Logger.LOG_WARNING,
watchedDirectory + " does not exist, please create it.",
null);
}
else if (!watchedDirectory.isDirectory())
{
log(Logger.LOG_ERROR,
"Cannot use "
+ watchedDirectory
+ " because it's not a directory", null);
throw new RuntimeException(
"File Install can't monitor " + watchedDirectory + " because it is not a directory");
}
}
public static String getThreadName(Map<String, String> properties)
{
return (properties.get(DIR) != null ? properties.get(DIR) : "./load");
}
public Map<String, String> getProperties()
{
return properties;
}
public void start()
{
if (noInitialDelay)
{
log(Logger.LOG_DEBUG, "Starting initial scan", null);
initializeCurrentManagedBundles();
Set<File> files = scanner.scan(true);
if (files != null)
{
try
{
process(files);
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
}
super.start();
}
/**
* Main run loop, will traverse the directory, and then handle the delta
* between installed and newly found/lost bundles and configurations.
*
*/
public void run()
{
// We must wait for FileInstall to complete initialisation
// to avoid race conditions observed in FELIX-2791
try
{
fileInstall.lock.readLock().lockInterruptibly();
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
log(Logger.LOG_INFO, "Watcher for " + watchedDirectory + " exiting because of interruption.", e);
return;
}
try {
log(Logger.LOG_DEBUG,
"{" + POLL + " (ms) = " + poll + ", "
+ DIR + " = " + watchedDirectory.getAbsolutePath() + ", "
+ LOG_LEVEL + " = " + logLevel + ", "
+ START_NEW_BUNDLES + " = " + startBundles + ", "
+ TMPDIR + " = " + tmpDir + ", "
+ FILTER + " = " + filter + ", "
+ START_LEVEL + " = " + startLevel + "}", null
);
if (!noInitialDelay) {
try {
// enforce a delay before the first directory scan
Thread.sleep(poll);
} catch (InterruptedException e) {
log(Logger.LOG_DEBUG, "Watcher for " + watchedDirectory + " was interrupted while waiting "
+ poll + " milliseconds for initial directory scan.", e);
return;
}
initializeCurrentManagedBundles();
}
}
finally
{
fileInstall.lock.readLock().unlock();
}
while (!interrupted()) {
try {
FrameworkStartLevel startLevelSvc = systemBundle.adapt(FrameworkStartLevel.class);
// Don't access the disk when the framework is still in a startup phase.
if (startLevelSvc.getStartLevel() >= activeLevel
&& systemBundle.getState() == Bundle.ACTIVE) {
Set<File> files = scanner.scan(false);
// Check that there is a result. If not, this means that the directory can not be listed,
// so it's presumably not a valid directory (it may have been deleted by someone).
// In such case, just sleep
if (files != null) {
process(files);
}
}
synchronized (this) {
wait(poll);
}
} catch (InterruptedException e) {
interrupt();
return;
} catch (Throwable e) {
try {
context.getBundle();
} catch (IllegalStateException t) {
// FileInstall bundle has been uninstalled, exiting loop
return;
}
log(Logger.LOG_ERROR, "In main loop, we have serious trouble", e);
}
}
}
public void bundleChanged(BundleEvent bundleEvent)
{
int type = bundleEvent.getType();
if (type == BundleEvent.UNINSTALLED)
{
for (Iterator<?> it = getArtifacts().iterator(); it.hasNext();)
{
Artifact artifact = (Artifact) it.next();
if (artifact.getBundleId() == bundleEvent.getBundle().getBundleId())
{
log(Logger.LOG_DEBUG, "Bundle " + bundleEvent.getBundle().getBundleId()
+ " has been uninstalled", null);
it.remove();
break;
}
}
}
if (type == BundleEvent.INSTALLED || type == BundleEvent.RESOLVED || type == BundleEvent.UNINSTALLED ||
type == BundleEvent.UNRESOLVED || type == BundleEvent.UPDATED) {
setStateChanged(true);
}
}
private void process(Set<File> files) throws InterruptedException
{
fileInstall.lock.readLock().lockInterruptibly();
try
{
doProcess(files);
}
finally
{
fileInstall.lock.readLock().unlock();
}
}
private void doProcess(Set<File> files) throws InterruptedException
{
List<ArtifactListener> listeners = fileInstall.getListeners();
List<Artifact> deleted = new ArrayList<Artifact>();
List<Artifact> modified = new ArrayList<Artifact>();
List<Artifact> created = new ArrayList<Artifact>();
// Try to process again files that could not be processed
synchronized (processingFailures)
{
files.addAll(processingFailures);
processingFailures.clear();
}
for (File file : files) {
boolean exists = file.exists();
Artifact artifact = getArtifact(file);
// File has been deleted
if (!exists) {
if (artifact != null) {
deleteJaredDirectory(artifact);
deleteTransformedFile(artifact);
deleted.add(artifact);
}
}
// File exists
else {
File jar = file;
URL jaredUrl = null;
try {
jaredUrl = file.toURI().toURL();
} catch (MalformedURLException e) {
// Ignore, can't happen
}
// Jar up the directory if needed
if (file.isDirectory()) {
prepareTempDir();
try {
jar = new File(tmpDir, file.getName() + ".jar");
Util.jarDir(file, jar);
jaredUrl = new URL(JarDirUrlHandler.PROTOCOL, null, file.getPath());
} catch (IOException e) {
// Notify user of problem, won't retry until the dir is updated.
log(Logger.LOG_ERROR,
"Unable to create jar for: " + file.getAbsolutePath(), e);
continue;
}
}
// File has been modified
if (artifact != null) {
artifact.setChecksum(scanner.getChecksum(file));
// If there's no listener, this is because this artifact has been installed before
// fileinstall has been restarted. In this case, try to find a listener.
if (artifact.getListener() == null) {
ArtifactListener listener = findListener(jar, listeners);
// If no listener can handle this artifact, we need to defer the
// processing for this artifact until one is found
if (listener == null) {
synchronized (processingFailures) {
processingFailures.add(file);
}
continue;
}
artifact.setListener(listener);
}
// If the listener can not handle this file anymore,
// uninstall the artifact and try as if is was new
if (!listeners.contains(artifact.getListener()) || !artifact.getListener().canHandle(jar)) {
deleted.add(artifact);
}
// The listener is still ok
else {
deleteTransformedFile(artifact);
artifact.setJaredDirectory(jar);
artifact.setJaredUrl(jaredUrl);
if (transformArtifact(artifact)) {
modified.add(artifact);
} else {
deleteJaredDirectory(artifact);
deleted.add(artifact);
}
}
}
// File has been added
else {
// Find the listener
ArtifactListener listener = findListener(jar, listeners);
// If no listener can handle this artifact, we need to defer the
// processing for this artifact until one is found
if (listener == null) {
synchronized (processingFailures) {
processingFailures.add(file);
}
continue;
}
// Create the artifact
artifact = new Artifact();
artifact.setPath(file);
artifact.setJaredDirectory(jar);
artifact.setJaredUrl(jaredUrl);
artifact.setListener(listener);
artifact.setChecksum(scanner.getChecksum(file));
if (transformArtifact(artifact)) {
created.add(artifact);
} else {
deleteJaredDirectory(artifact);
}
}
}
}
// Handle deleted artifacts
// We do the operations in the following order:
// uninstall, update, install, refresh & start.
Collection<Bundle> uninstalledBundles = uninstall(deleted);
Collection<Bundle> updatedBundles = update(modified);
Collection<Bundle> installedBundles = install(created);
if (!uninstalledBundles.isEmpty() || !updatedBundles.isEmpty() || !installedBundles.isEmpty())
{
Set<Bundle> toRefresh = new HashSet<Bundle>();
toRefresh.addAll(uninstalledBundles);
toRefresh.addAll(updatedBundles);
toRefresh.addAll(installedBundles);
findBundlesWithFragmentsToRefresh(toRefresh);
findBundlesWithOptionalPackagesToRefresh(toRefresh);
if (toRefresh.size() > 0)
{
// Refresh if any bundle got uninstalled or updated.
refresh(toRefresh);
// set the state to reattempt starting managed bundles which aren't already STARTING or ACTIVE
setStateChanged(true);
}
}
if (startBundles) {
int startLevel = systemBundle.adapt(FrameworkStartLevel.class).getStartLevel();
boolean doStart = isStateChanged() || startLevel != frameworkStartLevel;
frameworkStartLevel = startLevel;
if (doStart)
{
// Try to start all the bundles that are not persistently stopped
startAllBundles();
delayedStart.addAll(installedBundles);
delayedStart.removeAll(uninstalledBundles);
// Try to start newly installed bundles, or bundles which we missed on a previous round
startBundles(delayedStart);
consistentlyFailingBundles.clear();
consistentlyFailingBundles.addAll(delayedStart);
// set the state as unchanged to not reattempt starting failed bundles
setStateChanged(false);
}
}
}
ArtifactListener findListener(File artifact, List<ArtifactListener> listeners)
{
for (ArtifactListener listener : listeners) {
if (listener.canHandle(artifact)) {
return listener;
}
}
return null;
}
boolean transformArtifact(Artifact artifact)
{
if (artifact.getListener() instanceof ArtifactTransformer)
{
prepareTempDir();
try
{
File transformed = ((ArtifactTransformer) artifact.getListener()).transform(artifact.getJaredDirectory(), tmpDir);
if (transformed != null)
{
artifact.setTransformed(transformed);
return true;
}
}
catch (Exception e)
{
log(Logger.LOG_WARNING,
"Unable to transform artifact: " + artifact.getPath().getAbsolutePath(), e);
}
return false;
}
else if (artifact.getListener() instanceof ArtifactUrlTransformer)
{
try
{
URL url = artifact.getJaredUrl();
URL transformed = ((ArtifactUrlTransformer) artifact.getListener()).transform(url);
if (transformed != null)
{
artifact.setTransformedUrl(transformed);
return true;
}
}
catch (Exception e)
{
log(Logger.LOG_WARNING,
"Unable to transform artifact: " + artifact.getPath().getAbsolutePath(), e);
}
return false;
}
return true;
}
private void deleteTransformedFile(Artifact artifact)
{
if (artifact.getTransformed() != null
&& !artifact.getTransformed().equals(artifact.getPath())
&& !artifact.getTransformed().delete())
{
log(Logger.LOG_WARNING,
"Unable to delete transformed artifact: " + artifact.getTransformed().getAbsolutePath(), null);
}
}
private void deleteJaredDirectory(Artifact artifact)
{
if (artifact.getJaredDirectory() != null
&& !artifact.getJaredDirectory().equals(artifact.getPath())
&& !artifact.getJaredDirectory().delete())
{
log(Logger.LOG_WARNING,
"Unable to delete jared artifact: " + artifact.getJaredDirectory().getAbsolutePath(), null);
}
}
private void prepareTempDir()
{
if (tmpDir == null)
{
File javaIoTmpdir = new File(System.getProperty("java.io.tmpdir"));
if (!javaIoTmpdir.exists() && !javaIoTmpdir.mkdirs()) {
throw new IllegalStateException("Unable to create temporary directory " + javaIoTmpdir);
}
Random random = new Random();
while (tmpDir == null)
{
File f = new File(javaIoTmpdir, "fileinstall-" + Long.toString(random.nextLong()));
if (!f.exists() && f.mkdirs())
{
tmpDir = f;
tmpDir.deleteOnExit();
}
}
}
else
{
prepareDir(tmpDir);
}
}
/**
* Create the watched directory, if not existing.
* Throws a runtime exception if the directory cannot be created,
* or if the provided File parameter does not refer to a directory.
*
* @param dir
* The directory File Install will monitor
*/
private void prepareDir(File dir)
{
if (!dir.exists() && !dir.mkdirs())
{
log(Logger.LOG_ERROR,
"Cannot create folder "
+ dir
+ ". Is the folder write-protected?", null);
throw new RuntimeException("Cannot create folder: " + dir);
}
if (!dir.isDirectory())
{
log(Logger.LOG_ERROR,
"Cannot use "
+ dir
+ " because it's not a directory", null);
throw new RuntimeException(
"Cannot start FileInstall using something that is not a directory");
}
}
/**
* Log a message and optional throwable. If there is a log service we use
* it, otherwise we log to the console
*
* @param message
* The message to log
* @param e
* The throwable to log
*/
void log(int msgLevel, String message, Throwable e)
{
Util.log(context, logLevel, msgLevel, message, e);
}
/**
* Check if a bundle is a fragment.
*/
boolean isFragment(Bundle bundle)
{
BundleRevision rev = bundle.adapt(BundleRevision.class);
return (rev.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0;
}
/**
* Convenience to refresh the packages
*/
void refresh(Collection<Bundle> bundles) throws InterruptedException
{
FileInstall.refresh(systemBundle, bundles);
}
/**
* Retrieve a property as a long.
*
* @param properties the properties to retrieve the value from
* @param property the name of the property to retrieve
* @param dflt the default value
* @return the property as a long or the default value
*/
int getInt(Map<String, String> properties, String property, int dflt)
{
String value = properties.get(property);
if (value != null)
{
try
{
return Integer.parseInt(value);
}
catch (Exception e)
{
log(Logger.LOG_WARNING, property + " set, but not a int: " + value, null);
}
}
return dflt;
}
/**
* Retrieve a property as a long.
*
* @param properties the properties to retrieve the value from
* @param property the name of the property to retrieve
* @param dflt the default value
* @return the property as a long or the default value
*/
long getLong(Map<String, String> properties, String property, long dflt)
{
String value = properties.get(property);
if (value != null)
{
try
{
return Long.parseLong(value);
}
catch (Exception e)
{
log(Logger.LOG_WARNING, property + " set, but not a long: " + value, null);
}
}
return dflt;
}
/**
* Retrieve a property as a File.
*
* @param properties the properties to retrieve the value from
* @param property the name of the property to retrieve
* @param dflt the default value
* @return the property as a File or the default value
*/
File getFile(Map<String, String> properties, String property, File dflt)
{
String value = properties.get(property);
if (value != null)
{
return new File(value);
}
return dflt;
}
/**
* Retrieve a property as a boolan.
*
* @param properties the properties to retrieve the value from
* @param property the name of the property to retrieve
* @param dflt the default value
* @return the property as a boolean or the default value
*/
boolean getBoolean(Map<String, String> properties, String property, boolean dflt)
{
String value = properties.get(property);
if (value != null)
{
return Boolean.valueOf(value);
}
return dflt;
}
public void close()
{
this.context.removeBundleListener(this);
interrupt();
for (Artifact artifact : getArtifacts()) {
deleteTransformedFile(artifact);
deleteJaredDirectory(artifact);
}
try
{
scanner.close();
}
catch (IOException e)
{
// Ignore
}
try
{
join(10000);
}
catch (InterruptedException ie)
{
// Ignore
}
}
/**
* This method goes through all the currently installed bundles
* and returns information about those bundles whose location
* refers to a file in our {@link #watchedDirectory}.
*/
private void initializeCurrentManagedBundles()
{
Bundle[] bundles = this.context.getBundles();
String watchedDirPath = watchedDirectory.toURI().normalize().getPath();
Map<File, Long> checksums = new HashMap<File, Long>();
Pattern filePattern = filter == null || filter.isEmpty() ? null : Pattern.compile(filter);
for (Bundle bundle : bundles) {
// Convert to a URI because the location of a bundle
// is typically a URI. At least, that's the case for
// autostart bundles and bundles installed by fileinstall.
// Normalisation is needed to ensure that we don't treat (e.g.)
// /tmp/foo and /tmp//foo differently.
String location = bundle.getLocation();
String path = null;
if (location != null &&
!location.equals(Constants.SYSTEM_BUNDLE_LOCATION)) {
URI uri;
try {
uri = new URI(bundle.getLocation()).normalize();
} catch (URISyntaxException e) {
// Let's try to interpret the location as a file path
uri = new File(location).toURI().normalize();
}
if (uri.isOpaque() && uri.getSchemeSpecificPart() != null) {
// blueprint:file:/tmp/foo/baa.jar -> file:/tmp/foo/baa.jar
// blueprint:mvn:foo.baa/baa/0.0.1 -> mvn:foo.baa/baa/0.0.1
// wrap:file:/tmp/foo/baa-1.0.jar$Symbolic-Name=baa&Version=1.0 -> file:/tmp/foo/baa-1.0.jar$Symbolic-Name=baa&Version1.0
final String schemeSpecificPart = uri.getSchemeSpecificPart();
// extract content behind the 'file:' protocol of scheme specific path
final int lastIndexOfFileProtocol = schemeSpecificPart.lastIndexOf("file:");
final int offsetFileProtocol = lastIndexOfFileProtocol >= 0 ? lastIndexOfFileProtocol + "file:".length() : 0;
final int firstIndexOfDollar = schemeSpecificPart.indexOf("$");
final int endOfPath = firstIndexOfDollar >= 0 ? firstIndexOfDollar : schemeSpecificPart.length();
// file:/tmp/foo/baa.jar -> /tmp/foo/baa.jar
// mvn:foo.baa/baa/0.0.1 -> mvn:foo.baa/baa/0.0.1
// file:/tmp/foo/baa-1.0.jar$Symbolic-Name=baa&Version=1.0 -> /tmp/foo/baa-1.0.jar
path = schemeSpecificPart.substring(offsetFileProtocol, endOfPath);
} else {
// file:/tmp/foo/baa.jar -> /tmp/foo/baa.jar
// mnv:foo.baa/baa/0.0.1 -> foo.baa/baa/0.0.1
path = uri.getPath();
}
}
if (path == null) {
// jar.getPath is null means we could not parse the location
// as a meaningful URI or file path.
// We can't do any meaningful processing for this bundle.
continue;
}
final int index = path.lastIndexOf('/');
if (index != -1 && path.startsWith(watchedDirPath)) {
final String fileName = path.substring(index + 1);
if (filePattern == null || filePattern.matcher(fileName).matches()) {
Artifact artifact = new Artifact();
artifact.setBundleId(bundle.getBundleId());
artifact.setChecksum(Util.loadChecksum(bundle, context));
artifact.setListener(null);
artifact.setPath(new File(path));
setArtifact(new File(path), artifact);
checksums.put(new File(path), artifact.getChecksum());
}
}
}
scanner.initialize(checksums);
}
/**
* This method installs a collection of artifacts.
* @param artifacts Collection of {@link Artifact}s to be installed
* @return List of Bundles just installed
*/
private Collection<Bundle> install(Collection<Artifact> artifacts)
{
List<Bundle> bundles = new ArrayList<Bundle>();
for (Artifact artifact : artifacts) {
Bundle bundle = install(artifact);
if (bundle != null) {
bundles.add(bundle);
}
}
return bundles;
}
/**
* This method uninstalls a collection of artifacts.
* @param artifacts Collection of {@link Artifact}s to be uninstalled
* @return Collection of Bundles that got uninstalled
*/
private Collection<Bundle> uninstall(Collection<Artifact> artifacts)
{
List<Bundle> bundles = new ArrayList<Bundle>();
for (Artifact artifact : artifacts) {
Bundle bundle = uninstall(artifact);
if (bundle != null) {
bundles.add(bundle);
}
}
return bundles;
}
/**
* This method updates a collection of artifacts.
*
* @param artifacts Collection of {@link Artifact}s to be updated.
* @return Collection of bundles that got updated
*/
private Collection<Bundle> update(Collection<Artifact> artifacts)
{
List<Bundle> bundles = new ArrayList<Bundle>();
for (Artifact artifact : artifacts) {
Bundle bundle = update(artifact);
if (bundle != null) {
bundles.add(bundle);
}
}
return bundles;
}
/**
* Install an artifact and return the bundle object.
* It uses {@link Artifact#getPath()} as location
* of the new bundle. Before installing a file,
* it sees if the file has been identified as a bad file in
* earlier run. If yes, then it compares to see if the file has changed
* since then. It installs the file if the file has changed.
* If the file has not been identified as a bad file in earlier run,
* then it always installs it.
*
* @param artifact the artifact to be installed
* @return Bundle object that was installed
*/
private Bundle install(Artifact artifact)
{
File path = artifact.getPath();
Bundle bundle = null;
AtomicBoolean modified = new AtomicBoolean();
try
{
// If the listener is an installer, ask for an update
if (artifact.getListener() instanceof ArtifactInstaller)
{
((ArtifactInstaller) artifact.getListener()).install(path);
}
// if the listener is an url transformer
else if (artifact.getListener() instanceof ArtifactUrlTransformer)
{
Artifact badArtifact = installationFailures.get(path);
if (badArtifact != null && badArtifact.getChecksum() == artifact.getChecksum())
{
return null; // Don't attempt to install it; nothing has changed.
}
URL transformed = artifact.getTransformedUrl();
String location = transformed.toString();
BufferedInputStream in = new BufferedInputStream(transformed.openStream());
bundle = installOrUpdateBundle(location, in, artifact.getChecksum(), modified);
artifact.setBundleId(bundle.getBundleId());
}
// if the listener is an artifact transformer
else if (artifact.getListener() instanceof ArtifactTransformer)
{
Artifact badArtifact = installationFailures.get(path);
if (badArtifact != null && badArtifact.getChecksum() == artifact.getChecksum())
{
return null; // Don't attempt to install it; nothing has changed.
}
File transformed = artifact.getTransformed();
String location = path.toURI().normalize().toString();
BufferedInputStream in = new BufferedInputStream(new FileInputStream(transformed != null ? transformed : path));
bundle = installOrUpdateBundle(location, in, artifact.getChecksum(), modified);
artifact.setBundleId(bundle.getBundleId());
}
installationFailures.remove(path);
setArtifact(path, artifact);
}
catch (Exception e)
{
log(Logger.LOG_ERROR, "Failed to install artifact: " + path, e);
// Add it our bad jars list, so that we don't
// attempt to install it again and again until the underlying
// jar has been modified.
installationFailures.put(path, artifact);
}
return modified.get() ? bundle : null;
}
private Bundle installOrUpdateBundle(
String bundleLocation, BufferedInputStream is, long checksum, AtomicBoolean modified)
throws IOException, BundleException
{
JarInputStream jar = null;
try {
is.mark(256 * 1024);
jar = new JarInputStream(is);
Manifest m = jar.getManifest();
if( m == null ) {
throw new BundleException(
"The bundle " + bundleLocation + " does not have a META-INF/MANIFEST.MF! " +
"Make sure, META-INF and MANIFEST.MF are the first 2 entries in your JAR!");
}
String sn = m.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
String vStr = m.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
Version v = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr);
Bundle[] bundles = context.getBundles();
for (Bundle b : bundles) {
if (b.getSymbolicName() != null && b.getSymbolicName().equals(sn)) {
vStr = b.getHeaders().get(Constants.BUNDLE_VERSION);
Version bv = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr);
if (v.equals(bv)) {
is.reset();
if (Util.loadChecksum(b, context) != checksum) {
log(Logger.LOG_WARNING,
"A bundle with the same symbolic name ("
+ sn + ") and version (" + vStr
+ ") is already installed. Updating this bundle instead.", null
);
stopTransient(b);
Util.storeChecksum(b, checksum, context);
b.update(is);
modified.set(true);
}
return b;
}
}
}
is.reset();
Util.log(context, Logger.LOG_INFO, "Installing bundle " + sn
+ " / " + v, null);
Bundle b = context.installBundle(bundleLocation, is);
Util.storeChecksum(b, checksum, context);
modified.set(true);
// Set default start level at install time, the user can override it if he wants
if (startLevel != 0) {
b.adapt(BundleStartLevel.class).setStartLevel(startLevel);
}
return b;
}
finally
{
if (jar != null)
{
jar.close();
}
}
}
/**
* Uninstall a jar file.
*/
private Bundle uninstall(Artifact artifact)
{
Bundle bundle = null;
try
{
File path = artifact.getPath();
// Find a listener for this artifact if needed
if (artifact.getListener() == null)
{
artifact.setListener(findListener(path, fileInstall.getListeners()));
}
// Forget this artifact
removeArtifact(path);
// Delete transformed file
deleteTransformedFile(artifact);
// if the listener is an installer, uninstall the artifact
if (artifact.getListener() instanceof ArtifactInstaller)
{
((ArtifactInstaller) artifact.getListener()).uninstall(path);
}
// else we need uninstall the bundle
else if (artifact.getBundleId() != 0)
{
// old can't be null because of the way we calculate deleted list.
bundle = context.getBundle(artifact.getBundleId());
if (bundle == null)
{
log(Logger.LOG_WARNING,
"Failed to uninstall bundle: "
+ path + " with id: "
+ artifact.getBundleId()
+ ". The bundle has already been uninstalled", null);
return null;
}
log(Logger.LOG_INFO,
"Uninstalling bundle "
+ bundle.getBundleId() + " ("
+ bundle.getSymbolicName() + ")", null);
bundle.uninstall();
}
}
catch (Exception e)
{
log(Logger.LOG_WARNING, "Failed to uninstall artifact: " + artifact.getPath(), e);
}
return bundle;
}
private Bundle update(Artifact artifact)
{
Bundle bundle = null;
try
{
File path = artifact.getPath();
// If the listener is an installer, ask for an update
if (artifact.getListener() instanceof ArtifactInstaller)
{
((ArtifactInstaller) artifact.getListener()).update(path);
}
// if the listener is an url transformer
else if (artifact.getListener() instanceof ArtifactUrlTransformer)
{
URL transformed = artifact.getTransformedUrl();
bundle = context.getBundle(artifact.getBundleId());
if (bundle == null)
{
log(Logger.LOG_WARNING,
"Failed to update bundle: "
+ path + " with ID "
+ artifact.getBundleId()
+ ". The bundle has been uninstalled", null);
return null;
}
Util.log(context, Logger.LOG_INFO, "Updating bundle " + bundle.getSymbolicName()
+ " / " + bundle.getVersion(), null);
stopTransient(bundle);
Util.storeChecksum(bundle, artifact.getChecksum(), context);
InputStream in = (transformed != null)
? transformed.openStream()
: new FileInputStream(path);
try
{
bundle.update(in);
}
finally
{
in.close();
}
}
// else we need to ask for an update on the bundle
else if (artifact.getListener() instanceof ArtifactTransformer)
{
File transformed = artifact.getTransformed();
bundle = context.getBundle(artifact.getBundleId());
if (bundle == null)
{
log(Logger.LOG_WARNING,
"Failed to update bundle: "
+ path + " with ID "
+ artifact.getBundleId()
+ ". The bundle has been uninstalled", null);
return null;
}
Util.log(context, Logger.LOG_INFO, "Updating bundle " + bundle.getSymbolicName()
+ " / " + bundle.getVersion(), null);
stopTransient(bundle);
Util.storeChecksum(bundle, artifact.getChecksum(), context);
InputStream in = new FileInputStream(transformed != null ? transformed : path);
try
{
bundle.update(in);
}
finally
{
in.close();
}
}
}
catch (Throwable t)
{
log(Logger.LOG_WARNING, "Failed to update artifact " + artifact.getPath(), t);
}
return bundle;
}
private void stopTransient(Bundle bundle) throws BundleException
{
// Stop the bundle transiently so that it will be restarted when startAllBundles() is called
// but this avoids the need to restart the bundle twice (once for the update and another one
// when refreshing packages).
if (startBundles)
{
if (!isFragment(bundle))
{
bundle.stop(Bundle.STOP_TRANSIENT);
}
}
}
/**
* Tries to start all the bundles which somehow got stopped transiently.
* The File Install component will only retry the start When {@link #USE_START_TRANSIENT}
* is set to true or when a bundle is persistently started. Persistently stopped bundles
* are ignored.
*/
private void startAllBundles()
{
FrameworkStartLevel startLevelSvc = systemBundle.adapt(FrameworkStartLevel.class);
Set<Bundle> bundles = new LinkedHashSet<>();
for (Artifact artifact : getArtifacts()) {
if (artifact.getBundleId() > 0) {
Bundle bundle = context.getBundle(artifact.getBundleId());
if (bundle != null) {
if (bundle.getState() != Bundle.STARTING && bundle.getState() != Bundle.ACTIVE
&& (useStartTransient || bundle.adapt(BundleStartLevel.class).isPersistentlyStarted())
&& startLevelSvc.getStartLevel() >= bundle.adapt(BundleStartLevel.class).getStartLevel()) {
bundles.add(bundle);
}
}
}
}
startBundles(bundles);
}
/**
* Starts a bundle and removes it from the Collection when successfully started.
*/
private void startBundles(Set<Bundle> bundles)
{
// Check if this is the consistent set of bundles which failed previously.
boolean logFailures = !consistentlyFailingBundles.equals(bundles);
for (Iterator<Bundle> b = bundles.iterator(); b.hasNext(); )
{
if (startBundle(b.next(), logFailures))
{
b.remove();
}
}
}
/**
* Start a bundle, if the framework's startlevel allows it.
* @param bundle the bundle to start.
* @return whether the bundle was started.
*/
private boolean startBundle(Bundle bundle, boolean logFailures)
{
// Fragments can never be started.
// Bundles can only be started transient when the start level of the framework is high
// enough. Persistent (i.e. non-transient) starts will simply make the framework start the
// bundle when the start level is high enough.
if (startBundles
&& bundle.getState() != Bundle.UNINSTALLED
&& !isFragment(bundle)
&& frameworkStartLevel >= bundle.adapt(BundleStartLevel.class).getStartLevel())
{
try
{
int options = useStartTransient ? Bundle.START_TRANSIENT : 0;
options |= useStartActivationPolicy ? Bundle.START_ACTIVATION_POLICY : 0;
bundle.start(options);
log(Logger.LOG_INFO, "Started bundle: " + bundle.getLocation(), null);
return true;
}
catch (BundleException e)
{
// Don't log this as an error, instead we start the bundle repeatedly.
if (logFailures)
{
log(Logger.LOG_WARNING, "Error while starting bundle: " + bundle.getLocation(), e);
}
}
}
return false;
}
protected Set<Bundle> getScopedBundles(String scope) {
// No bundles to check
if (SCOPE_NONE.equals(scope)) {
return new HashSet<Bundle>();
}
// Go through managed bundles
else if (SCOPE_MANAGED.equals(scope)) {
Set<Bundle> bundles = new HashSet<Bundle>();
for (Artifact artifact : getArtifacts()) {
if (artifact.getBundleId() > 0) {
Bundle bundle = context.getBundle(artifact.getBundleId());
if (bundle != null) {
bundles.add(bundle);
}
}
}
return bundles;
// Go through all bundles
} else {
return new HashSet<Bundle>(Arrays.asList(context.getBundles()));
}
}
protected void findBundlesWithFragmentsToRefresh(Set<Bundle> toRefresh) {
Set<Bundle> fragments = new HashSet<Bundle>();
Set<Bundle> bundles = getScopedBundles(fragmentScope);
for (Bundle b : toRefresh) {
if (b.getState() != Bundle.UNINSTALLED) {
String hostHeader = b.getHeaders().get(Constants.FRAGMENT_HOST);
if (hostHeader != null) {
Clause[] clauses = Parser.parseHeader(hostHeader);
if (clauses != null && clauses.length > 0) {
Clause path = clauses[0];
for (Bundle hostBundle : bundles) {
if (hostBundle.getSymbolicName() != null &&
hostBundle.getSymbolicName().equals(path.getName())) {
String ver = path.getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE);
if (ver != null) {
VersionRange v = VersionRange.parseVersionRange(ver);
if (v.contains(VersionTable.getVersion(hostBundle.getHeaders().get(Constants.BUNDLE_VERSION)))) {
fragments.add(hostBundle);
}
} else {
fragments.add(hostBundle);
}
}
}
}
}
}
}
toRefresh.addAll(fragments);
}
protected void findBundlesWithOptionalPackagesToRefresh(Set<Bundle> toRefresh) {
Set<Bundle> bundles = getScopedBundles(optionalScope);
// First pass: include all bundles contained in these features
bundles.removeAll(toRefresh);
if (bundles.isEmpty()) {
return;
}
// Second pass: for each bundle, check if there is any unresolved optional package that could be resolved
Map<Bundle, List<Clause>> imports = new HashMap<Bundle, List<Clause>>();
for (Iterator<Bundle> it = bundles.iterator(); it.hasNext(); ) {
Bundle b = it.next();
String importsStr = b.getHeaders().get(Constants.IMPORT_PACKAGE);
List<Clause> importsList = getOptionalImports(importsStr);
if (importsList.isEmpty()) {
it.remove();
} else {
imports.put(b, importsList);
}
}
if (bundles.isEmpty()) {
return;
}
// Third pass: compute a list of packages that are exported by our bundles and see if
// some exported packages can be wired to the optional imports
List<Clause> exports = new ArrayList<Clause>();
for (Bundle b : toRefresh) {
if (b.getState() != Bundle.UNINSTALLED) {
String exportsStr = b.getHeaders().get(Constants.EXPORT_PACKAGE);
if (exportsStr != null) {
Clause[] exportsList = Parser.parseHeader(exportsStr);
exports.addAll(Arrays.asList(exportsList));
}
}
}
for (Iterator<Bundle> it = bundles.iterator(); it.hasNext(); ) {
Bundle b = it.next();
List<Clause> importsList = imports.get(b);
for (Iterator<Clause> itpi = importsList.iterator(); itpi.hasNext(); ) {
Clause pi = itpi.next();
boolean matching = false;
for (Clause pe : exports) {
if (pi.getName().equals(pe.getName())) {
String evStr = pe.getAttribute(Constants.VERSION_ATTRIBUTE);
String ivStr = pi.getAttribute(Constants.VERSION_ATTRIBUTE);
Version exported = evStr != null ? Version.parseVersion(evStr) : Version.emptyVersion;
VersionRange imported = ivStr != null ? VersionRange.parseVersionRange(ivStr) : VersionRange.ANY_VERSION;
if (imported.contains(exported)) {
matching = true;
break;
}
}
}
if (!matching) {
itpi.remove();
}
}
if (importsList.isEmpty()) {
it.remove();
// } else {
// LOGGER.debug("Refreshing bundle {} ({}) to solve the following optional imports", b.getSymbolicName(), b.getBundleId());
// for (Clause p : importsList) {
// LOGGER.debug(" {}", p);
// }
//
}
}
toRefresh.addAll(bundles);
}
protected List<Clause> getOptionalImports(String importsStr)
{
Clause[] imports = Parser.parseHeader(importsStr);
List<Clause> result = new LinkedList<Clause>();
for (Clause anImport : imports)
{
String resolution = anImport.getDirective(Constants.RESOLUTION_DIRECTIVE);
if (Constants.RESOLUTION_OPTIONAL.equals(resolution))
{
result.add(anImport);
}
}
return result;
}
public void addListener(ArtifactListener listener, long stamp)
{
if (updateWithListeners)
{
for (Artifact artifact : getArtifacts())
{
if (artifact.getListener() == null && artifact.getBundleId() > 0)
{
Bundle bundle = context.getBundle(artifact.getBundleId());
if (bundle != null && bundle.getLastModified() < stamp)
{
File path = artifact.getPath();
if (listener.canHandle(path))
{
synchronized (processingFailures)
{
processingFailures.add(path);
}
}
}
}
}
}
synchronized (this)
{
this.notifyAll();
}
}
public void removeListener(ArtifactListener listener)
{
for (Artifact artifact : getArtifacts())
{
if (artifact.getListener() == listener)
{
artifact.setListener(null);
}
}
synchronized (this)
{
this.notifyAll();
}
}
private Artifact getArtifact(File file)
{
synchronized (currentManagedArtifacts)
{
return currentManagedArtifacts.get(file);
}
}
private List<Artifact> getArtifacts()
{
synchronized (currentManagedArtifacts)
{
return new ArrayList<Artifact>(currentManagedArtifacts.values());
}
}
private void setArtifact(File file, Artifact artifact)
{
synchronized (currentManagedArtifacts)
{
currentManagedArtifacts.put(file, artifact);
}
}
private void removeArtifact(File file)
{
synchronized (currentManagedArtifacts)
{
currentManagedArtifacts.remove(file);
}
}
private void setStateChanged(boolean changed) {
this.stateChanged.set(changed);
}
private boolean isStateChanged() {
return stateChanged.get();
}
}