package org.openanzo.osgi.dirwatcher;
/*
* 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.
*/
/**
* -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.
*/
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.packageadmin.PackageAdmin;
import org.osgi.service.startlevel.StartLevel;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
class DirectoryWatcher extends Thread {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(DirectoryWatcher.class);
private final static String ALIAS_KEY = "_alias_factory_pid";
private final HashSet<File> watchedDirectories = new HashSet<File>();
private final HashSet<String> watchedDirectoriesPaths = new HashSet<String>();
private final Map<String, Long> currentManagedConfigs = new HashMap<String, Long>();
private final Map<String, Long> currentManagedBundles = new HashMap<String, Long>();
private final long poll;
private final int sl;
private final BundleContext context;
private final ConfigurationAdmin configAdmin;
private final PackageAdmin packageAdmin;
private final StartLevel startLevel;
private final Bundle systemBundle;
static final Marker LIFECYCLE_MARKER = MarkerFactory.getMarker("services");
protected DirectoryWatcher(ConfigurationAdmin configAdmin, PackageAdmin packageAdmin, StartLevel startLevel, Collection<String> directories, int sl, long poll, BundleContext context, Bundle systemBundle) {
super("DirectoyrWatcherForLevel:" + sl);
this.systemBundle = systemBundle;
this.configAdmin = configAdmin;
this.packageAdmin = packageAdmin;
this.context = context;
this.poll = poll;
this.sl = sl;
this.startLevel = startLevel;
for (String dir : directories) {
File directory = new File(dir.trim());
if (!directory.isDirectory() && !directory.isFile()) {
directory.mkdirs();
}
this.watchedDirectories.add(directory);
watchedDirectoriesPaths.add(directory.getAbsolutePath());
}
}
protected Collection<Bundle> installBundles() {
try {
Map<String, File> installed = new HashMap<String, File>();
Map<String, String> versions = new HashMap<String, String>();
Set<String> configs = new HashSet<String>();
traverse(installed, versions, configs, watchedDirectories);
Iterator<Map.Entry<String, Long>> currentIter = currentManagedBundles.entrySet().iterator();
while (currentIter.hasNext()) {
Map.Entry<String, Long> entry = currentIter.next();
File currentFile = installed.get(entry.getKey());
if (currentFile != null) {
if (currentFile.lastModified() <= entry.getValue()) {
currentIter.remove();
}
}
}
doConfigs(currentManagedConfigs, configs);
return doInstalled(currentManagedBundles, installed, versions);
} catch (Throwable e) {
log.error(LIFECYCLE_MARKER, "In main loop, we have serious trouble", e);
return Collections.<Bundle> emptySet();
}
}
/**
* Main run loop, will traverse the directory, and then handle the delta between installed and newly found/lost bundles and configurations.
*
*/
@Override
public void run() {
try {
while (!interrupted()) {
if (systemBundle == null || systemBundle.getState() == Bundle.ACTIVE) {
startBundles(installBundles());
}
Thread.sleep(poll);
}
} catch (InterruptedException e) {
return;
}
}
@SuppressWarnings("unchecked")
protected void startBundles(Collection<Bundle> starters) {
if (starters.size() != 0) {
for (Bundle bundle : starters) {
if (!isFragment(bundle)) {
try {
Dictionary headers = bundle.getHeaders();
Object level = headers.get("Bundle-StartLevel");
int bundleLevel = sl;
if (level != null) {
bundleLevel = Integer.parseInt(level.toString());
}
startLevel.setBundleStartLevel(bundle, bundleLevel);
// if (startLevel.getStartLevel() >= bundleLevel) {
bundle.start();
// }
} catch (BundleException e) {
log.error(LIFECYCLE_MARKER, "Error while starting a newly installed bundle " + bundle.getSymbolicName(), e);
}
}
}
}
}
/**
* Traverse the directory and fill the map with the found jars and configurations keyed by the abs file path.
*
* @param jars
* Returns the abspath -> file for found jars
* @param configs
* Returns the abspath -> file for found configurations
* @param jardir
* The directory to traverse
*/
private void traverse(Map<String, File> jars, Map<String, String> versions, Set<String> configs, Collection<File> jarDirs) {
for (File jardir : jarDirs) {
File list[] = jardir.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.isFile() && (pathname.getPath().endsWith(".jar") || pathname.getPath().endsWith(".cfg"));
}
});
if (list != null) {
for (File file : list) {
if (file.getPath().endsWith(".jar")) {
try {
JarFile jarFile = new JarFile(file);
Manifest manifest = jarFile.getManifest();
Attributes attributes = manifest.getMainAttributes();
String bundleName = attributes.getValue(Constants.BUNDLE_SYMBOLICNAME);
String version = attributes.getValue(Constants.BUNDLE_VERSION);
if (bundleName != null && version != null) {
String oldVers = versions.get(bundleName);
boolean newer = true;
if (oldVers != null) {
Version newVersion = new Version(version);
Version oldVersion = new Version(oldVers);
if (oldVersion.compareTo(newVersion) > -1) {
newer = false;
if (log.isDebugEnabled()) {
log.debug(LIFECYCLE_MARKER, "Bundle file with greater version already found: {}:{}>={}", new Object[] { bundleName, oldVers, version });
}
}
}
if (newer) {
jars.put(bundleName, file);
versions.put(bundleName, version);
}
}
} catch (IOException ioe) {
log.error(LIFECYCLE_MARKER, "Cannot load jar file:" + file.toString(), ioe);
}
} else if (file.getPath().endsWith(".cfg")) {
configs.add(file.getAbsolutePath());
}
}
}
}
}
/**
* Install bundles that were discovered and uninstall bundles that are gone from the current state.
*
* @param current
* A map location -> path that holds the current state
* @param discovered
* A set of paths that represent the just found bundles
*/
private Collection<Bundle> doInstalled(Map<String, Long> current, Map<String, File> discovered, Map<String, String> versioned) {
boolean refresh = false;
Bundle bundles[] = context.getBundles();
for (int i = 0; i < bundles.length; i++) {
Bundle bundle = bundles[i];
String symbolicName = bundle.getSymbolicName();
File file = discovered.remove(symbolicName);
if (file != null) {
// We have a bundle that is already installed
// so we know it
Version vers = new Version(versioned.get(symbolicName));
Version bundleVers = new Version((String) bundle.getHeaders().get(Constants.BUNDLE_VERSION));
int result = bundleVers.compareTo(vers);
if (log.isDebugEnabled()) {
log.debug(LIFECYCLE_MARKER, "Existing Bundle vs New Bundle:{} New:{} Old:{} Compare:{}", new Object[] { symbolicName, vers.toString(), bundleVers.toString(), Integer.toString(result) });
}
if (result < 0 || file.lastModified() > bundle.getLastModified() + 4000) {
try {
// We treat this as an update, it is modified,,
// different size, and it is present in the dir
// as well as in the list of bundles.
InputStream in = new FileInputStream(file);
bundle.update(in);
refresh = true;
in.close();
log.info(LIFECYCLE_MARKER, "Updating bundle {}:{} from {} to {}", new Object[] { bundle.getLocation(), symbolicName, bundleVers.toString(), vers.toString() });
// Fragments can not be started. All other
// bundles are always started because OSGi treats this
// as a noop when the bundle is already started
if (!isFragment(bundle)) {
try {
bundle.start();
} catch (Exception e) {
log.error(LIFECYCLE_MARKER, "Fail to start bundle {}", e, symbolicName);
}
}
} catch (Exception e) {
log.error(LIFECYCLE_MARKER, "Failed to update bundle ", e);
}
}
} else {
// Hmm. We found a bundle that looks like it came from our
// watched directory but we did not find it this round.
// Just remove it.
for (String watchedDirectory : watchedDirectoriesPaths) {
if (bundle.getLocation().startsWith(watchedDirectory)) {
try {
bundle.uninstall();
refresh = true;
log.error(LIFECYCLE_MARKER, "Uninstall bundle {}", symbolicName);
} catch (Exception e) {
log.debug(LIFECYCLE_MARKER, "Uninstalled failed {}:{}", bundle.getLocation(), symbolicName);
}
break;
}
}
}
}
Collection<Bundle> starters = new ArrayList<Bundle>();
for (Map.Entry<String, File> entry : discovered.entrySet()) {
try {
InputStream in = new FileInputStream(entry.getValue());
Bundle bundle = context.installBundle(entry.getKey(), in);
in.close();
// We do not start this bundle yet. We wait after
// refresh because this will minimize the disruption
// as well as temporary unresolved errors.
starters.add(bundle);
log.info(LIFECYCLE_MARKER, "Installed {}", entry.getKey());
} catch (Exception e) {
log.error(LIFECYCLE_MARKER, "failed to install/start bundle: {}", e, entry.getKey());
}
}
if (refresh) {
refresh();
}
return starters;
}
/**
* Handle the changes between the configurations already installed and the newly found/lost configurations.
*
* @param current
* Existing installed configurations abspath -> File
* @param discovered
* Newly found configurations
*/
private void doConfigs(Map<String, Long> current, Set<String> discovered) {
try {
// Set all old keys as inactive, we remove them
// when we find them to be active, will be left
// with the inactive ones.
Set<String> inactive = new HashSet<String>(current.keySet());
for (String path : discovered) {
File f = new File(path);
if (!current.containsKey(path)) {
// newly found entry, set the configuration immediately
Long l = Long.valueOf(f.lastModified());
if (setConfig(f)) {
// Remember it for the next round
current.put(path, l);
}
} else {
// Found an existing one.
// Check if it has been updated
long lastModified = f.lastModified();
long oldTime = (current.get(path)).longValue();
if (oldTime < lastModified) {
if (setConfig(f)) {
// Remember it for the next round.
current.put(path, Long.valueOf(lastModified));
}
}
}
// Mark this one as active
inactive.remove(path);
}
for (String path : inactive) {
File f = new File(path);
if (deleteConfig(f)) {
current.remove(path);
}
}
} catch (Exception ee) {
log.error(LIFECYCLE_MARKER, "Error Processing config: ", ee);
}
}
/**
* Set the configuration based on the config file.
*
* @param f
* Configuration file
* @return
* @throws Exception
*/
private boolean setConfig(File f) throws Exception {
Properties p = new Properties();
InputStream in = new FileInputStream(f);
p.load(in);
in.close();
String pid[] = parsePid(f.getName());
if (pid[1] != null) {
p.put(ALIAS_KEY, pid[1]);
}
Configuration config = getConfiguration(pid[0], pid[1]);
if (config.getBundleLocation() != null) {
config.setBundleLocation(null);
}
config.update(p);
return true;
}
/**
* Remove the configuration.
*
* @param f
* File where the configuration in whas defined.
* @return
* @throws Exception
*/
private boolean deleteConfig(File f) throws Exception {
String pid[] = parsePid(f.getName());
Configuration config = getConfiguration(pid[0], pid[1]);
config.delete();
return true;
}
private String[] parsePid(String path) {
String pid = path.substring(0, path.length() - 4);
int n = pid.indexOf('-');
if (n > 0) {
String factoryPid = pid.substring(n + 1);
pid = pid.substring(0, n);
return new String[] { pid, factoryPid };
} else {
return new String[] { pid, null };
}
}
private Configuration getConfiguration(String pid, String factoryPid) throws Exception {
if (factoryPid != null) {
String filter = "(|(" + ALIAS_KEY + "=" + factoryPid + ")(.alias_factory_pid=" + factoryPid + "))";
Configuration configs[] = configAdmin.listConfigurations(filter);
if (configs == null || configs.length == 0) {
return configAdmin.createFactoryConfiguration(pid, null);
} else {
return configs[0];
}
} else {
return configAdmin.getConfiguration(pid, null);
}
}
/**
* Check if a bundle is a fragment.
*
* @param bundle
* @return
*/
private boolean isFragment(Bundle bundle) {
if (packageAdmin != null) {
return packageAdmin.getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
}
return false;
}
/**
* Convenience to refresh the packages
*/
private void refresh() {
packageAdmin.refreshPackages(null);
}
protected void close() {
interrupt();
try {
join(10000);
} catch (InterruptedException ie) {
// Ignore
}
}
}