blob: c0bf2359ca8f6a1527d202dfd83f5c828705a926 [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.servicemix.kernel.filemonitor;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
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.Properties;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.packageadmin.PackageAdmin;
import org.osgi.service.prefs.Preferences;
import org.osgi.service.prefs.PreferencesService;
import org.osgi.util.tracker.ServiceTracker;
/**
* Watches a deploy directory for files that are added, updated or removed then
* processing them. Currently we support OSGi bundles, OSGi configuration files
* and expanded directories of OSGi bundles.
*
* @version $Revision: 1.1 $
*/
public class FileMonitor {
public final static String CONFIG_DIR = "org.apache.servicemix.filemonitor.configDir";
public final static String DEPLOY_DIR = "org.apache.servicemix.filemonitor.monitorDir";
public final static String GENERATED_JAR_DIR = "org.apache.servicemix.filemonitor.generatedJarDir";
public final static String SCAN_INTERVAL = "org.apache.servicemix.filemonitor.scanInterval";
public final static String PREFERENCE_KEY = "FileMonitor";
protected static final String ALIAS_KEY = "_alias_factory_pid";
private static final Log LOGGER = LogFactory.getLog(FileMonitor.class);
private FileMonitorActivator activator;
private File configDir;
private File deployDir;
private File generateDir;
private Scanner scanner = new Scanner();
private long scanInterval = 500L;
private boolean loggedConfigAdminWarning;
private List<Bundle> bundlesToStart = new ArrayList<Bundle>();
private List<Bundle> bundlesToUpdate = new ArrayList<Bundle>();
private Map<String, String> artifactToBundle = new HashMap<String, String>();
private final Set<String> pendingTransformationArtifacts = new HashSet<String>();
private final Set<Bundle> pendingStartBundles = new HashSet<Bundle>();
private ServiceListener deployerListener;
private BundleListener bundleListener;
private Executor executor;
private String pid;
public FileMonitor() {
String base = System.getProperty("servicemix.base", ".");
configDir = new File(base, "etc");
deployDir = new File(base, "deploy");
generateDir = new File(base, "data/generated-bundles");
}
@SuppressWarnings("unchecked")
public FileMonitor(FileMonitorActivator activator, Dictionary properties, String pid) {
this();
this.activator = activator;
this.pid = pid;
File value = getFileValue(properties, CONFIG_DIR);
if (value != null) {
configDir = value;
}
value = getFileValue(properties, DEPLOY_DIR);
if (value != null) {
deployDir = value;
}
value = getFileValue(properties, GENERATED_JAR_DIR);
if (value != null) {
generateDir = value;
}
Long i = getLongValue(properties, SCAN_INTERVAL);
if (i != null) {
scanInterval = i;
}
}
public void start() {
executor = Executors.newSingleThreadExecutor();
if (configDir != null) {
configDir.mkdirs();
}
deployDir.mkdirs();
generateDir.mkdirs();
List<File> dirs = new ArrayList<File>();
if (configDir != null) {
dirs.add(configDir);
}
dirs.add(deployDir);
scanner.setScanDirs(dirs);
scanner.setScanInterval(scanInterval);
// load the scan results for this monitor
loadScannerState();
scanner.addListener(new Scanner.BulkListener() {
public void filesChanged(List<String> filenames) throws Exception {
onFilesChanged(filenames);
}
});
LOGGER.info("Starting to monitor the deploy directory: " + deployDir + " every " + scanInterval
+ " millis");
if (configDir != null) {
LOGGER.info("Config directory is at: " + configDir);
}
LOGGER.info("Will generate bundles from expanded source directories to: " + generateDir);
executor.execute(new Runnable() {
public void run() {
scanner.start();
}
});
}
public void stop() {
// first stop the scanner
scanner.stop();
// load the scan results for this monitor
saveScannerState();
}
// Properties
// -------------------------------------------------------------------------
public BundleContext getContext() {
return activator.getContext();
}
public FileMonitorActivator getActivator() {
return activator;
}
public void setActivator(FileMonitorActivator activator) {
this.activator = activator;
}
public File getConfigDir() {
return configDir;
}
public void setConfigDir(File configDir) {
this.configDir = configDir;
}
public File getDeployDir() {
return deployDir;
}
public void setDeployDir(File deployDir) {
this.deployDir = deployDir;
}
public File getGenerateDir() {
return generateDir;
}
public void setGenerateDir(File generateDir) {
this.generateDir = generateDir;
}
public long getScanInterval() {
return scanInterval;
}
public void setScanInterval(long scanInterval) {
this.scanInterval = scanInterval;
}
// Implementation methods
// -------------------------------------------------------------------------
protected synchronized void onFilesChanged(Collection<String> filenames) {
bundlesToStart.clear();
bundlesToUpdate.clear();
Set<File> bundleJarsCreated = new HashSet<File>();
for (String filename : filenames) {
File file = new File(filename);
try {
LOGGER.debug("File changed: " + filename);
// Handle config files
if (isValidConfigFile(file)) {
if (file.exists()) {
updateConfiguration(file);
} else {
deleteConfiguration(file);
}
continue;
}
// Handle exploded artifacts removal
if (!file.exists() && file.getName().equals("MANIFEST.MF")) {
File parentFile = file.getParentFile();
if (parentFile.getName().equals("META-INF")) {
File bundleDir = parentFile.getParentFile();
if (isValidBundleSourceDirectory(bundleDir)) {
undeployBundle(bundleDir);
continue;
}
}
}
// Handle exploded artifacts add
File jardir = getExpandedBundleRootDirectory(file);
if (jardir != null) {
if (bundleJarsCreated.contains(jardir)) {
continue;
}
bundleJarsCreated.add(jardir);
file = createBundleJar(jardir);
}
// Transformation step
if (file.exists()) {
File f = transformArtifact(file);
if (f == null) {
LOGGER.warn("Unsupported deployment: " + filename);
rescheduleTransformation(file);
continue;
}
file = f;
} else {
String transformedFile = artifactToBundle.get(filename);
if (transformedFile != null) {
file = new File(transformedFile);
if (file.exists()) {
file.delete();
}
}
}
// Handle final bundles
if (isValidArtifactFile(file)) {
if (file.exists()) {
deployBundle(file);
} else {
undeployBundle(file);
}
}
} catch (Exception e) {
LOGGER.warn("Failed to process: " + file + ". Reason: " + e, e);
}
}
refreshPackagesAndStartOrUpdateBundles();
}
private void rescheduleTransformation(File file) {
synchronized (pendingTransformationArtifacts) {
pendingTransformationArtifacts.add(file.getAbsolutePath());
}
if (deployerListener == null) {
try {
String filter = "(" + Constants.OBJECTCLASS + "=" + DeploymentListener.class.getName() + ")";
deployerListener = new ServiceListener() {
public void serviceChanged(ServiceEvent event) {
executor.execute(new Runnable() {
public void run() {
Set<String> files;
synchronized (pendingTransformationArtifacts) {
files = new HashSet<String>(pendingTransformationArtifacts);
pendingTransformationArtifacts.clear();
}
onFilesChanged(files);
}
});
}
};
getContext().addServiceListener(deployerListener, filter);
} catch (InvalidSyntaxException e) {
// Ignore
}
}
}
private File transformArtifact(File file) throws Exception {
// Check registered deployers
ServiceReference[] srvRefs = getContext().getAllServiceReferences(DeploymentListener.class.getName(),
null);
if (srvRefs != null) {
for (ServiceReference sr : srvRefs) {
try {
DeploymentListener deploymentListener = (DeploymentListener)getContext().getService(sr);
if (deploymentListener.canHandle(file)) {
File transformedFile = deploymentListener.handle(file, getGenerateDir());
artifactToBundle.put(file.getAbsolutePath(), transformedFile.getAbsolutePath());
return transformedFile;
}
} finally {
getContext().ungetService(sr);
}
}
}
JarFile jar = null;
try {
// Handle OSGi bundles with the default deployer
if (file.getName().endsWith("txt") || file.getName().endsWith("xml")
|| file.getName().endsWith("properties")) {
// that's file type which is not supported as bundle and avoid
// exception in the log
return null;
}
jar = new JarFile(file);
Manifest m = jar.getManifest();
if (m.getMainAttributes().getValue(new Attributes.Name("Bundle-SymbolicName")) != null
&& m.getMainAttributes().getValue(new Attributes.Name("Bundle-Version")) != null) {
return file;
}
} catch (Exception e) {
LOGGER.debug("Error transforming artifact " + file.getName(), e);
} finally {
if (jar != null) {
jar.close();
}
}
return null;
}
protected void deployBundle(File file) throws IOException, BundleException {
LOGGER.info("Deploying: " + file.getCanonicalPath());
InputStream in = new FileInputStream(file);
try {
Bundle bundle = getBundleForJarFile(file);
if (bundle != null) {
bundlesToUpdate.add(bundle);
} else {
bundle = getContext().installBundle(file.getCanonicalFile().toURI().toString(), in);
if (!isBundleFragment(bundle)) {
bundlesToStart.add(bundle);
}
}
} finally {
closeQuietly(in);
}
}
protected void undeployBundle(File file) throws BundleException, IOException {
LOGGER.info("Undeploying: " + file.getCanonicalPath());
Bundle bundle = getBundleForJarFile(file);
if (bundle == null) {
LOGGER.warn("Could not find Bundle for file: " + file.getCanonicalPath());
} else {
if (!isBundleFragment(bundle)) {
bundle.stop();
}
bundle.uninstall();
}
}
protected Bundle getBundleForJarFile(File file) throws IOException {
String absoluteFilePath = file.getAbsoluteFile().toURI().toString();
Bundle bundles[] = getContext().getBundles();
for (Bundle bundle : bundles) {
String location = bundle.getLocation();
if (filePathsMatch(absoluteFilePath, location)) {
return bundle;
}
}
return null;
}
protected static boolean filePathsMatch(String p1, String p2) {
p1 = normalizeFilePath(p1);
p2 = normalizeFilePath(p2);
return (p1 != null && p1.equalsIgnoreCase(p2));
}
protected static String normalizeFilePath(String path) {
if (path != null) {
path = path.replaceFirst("file:/*", "");
path = path.replaceAll("[\\\\/]+", "/");
}
return path;
}
protected void updateConfiguration(File file) throws IOException, InvalidSyntaxException {
ConfigurationAdmin configurationAdmin = activator.getConfigurationAdmin();
if (configurationAdmin == null) {
if (!loggedConfigAdminWarning) {
LOGGER.warn("No ConfigurationAdmin so cannot deploy configurations");
loggedConfigAdminWarning = true;
}
} else {
Properties properties = new Properties();
InputStream in = new FileInputStream(file);
try {
properties.load(in);
interpolation(properties);
closeQuietly(in);
String[] pid = parsePid(file);
Hashtable<Object, Object> hashtable = new Hashtable<Object, Object>();
hashtable.putAll(properties);
if (pid[1] != null) {
hashtable.put(ALIAS_KEY, pid[1]);
}
Configuration config = getConfiguration(pid[0], pid[1]);
if (config.getBundleLocation() != null) {
config.setBundleLocation(null);
}
config.update(hashtable);
} finally {
closeQuietly(in);
}
}
}
protected void interpolation(Properties properties) {
for (Enumeration e = properties.propertyNames(); e.hasMoreElements();) {
String key = (String)e.nextElement();
String val = properties.getProperty(key);
Matcher matcher = Pattern.compile("\\$\\{([^}]+)\\}").matcher(val);
while (matcher.find()) {
String rep = System.getProperty(matcher.group(1));
if (rep != null) {
val = val.replace(matcher.group(0), rep);
matcher.reset(val);
}
}
properties.put(key, val);
}
}
protected void deleteConfiguration(File file) throws IOException, InvalidSyntaxException {
String[] pid = parsePid(file);
Configuration config = getConfiguration(pid[0], pid[1]);
config.delete();
}
protected Configuration getConfiguration(String pid, String factoryPid) throws IOException,
InvalidSyntaxException {
ConfigurationAdmin configurationAdmin = activator.getConfigurationAdmin();
if (factoryPid != null) {
Configuration[] configs = configurationAdmin.listConfigurations("(|(" + ALIAS_KEY + "=" + pid
+ ")(.alias_factory_pid="
+ factoryPid + "))");
if (configs == null || configs.length == 0) {
return configurationAdmin.createFactoryConfiguration(pid, null);
} else {
return configs[0];
}
} else {
return configurationAdmin.getConfiguration(pid, null);
}
}
protected String[] parsePid(File file) {
String path = file.getName();
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};
}
}
protected PackageAdmin getPackageAdmin() {
ServiceTracker packageAdminTracker = activator.getPackageAdminTracker();
if (packageAdminTracker != null) {
try {
return (PackageAdmin)packageAdminTracker.waitForService(5000L);
} catch (InterruptedException e) {
// ignore
}
}
return null;
}
protected boolean isBundleFragment(Bundle bundle) {
PackageAdmin packageAdmin = getPackageAdmin();
if (packageAdmin != null) {
return packageAdmin.getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
}
return false;
}
protected void refreshPackagesAndStartOrUpdateBundles() {
for (Bundle bundle : bundlesToUpdate) {
try {
bundle.update();
LOGGER.info("Updated: " + bundle);
} catch (BundleException e) {
LOGGER.warn("Failed to update bundle: " + bundle + ". Reason: " + e, e);
}
}
for (Bundle bundle : bundlesToStart) {
try {
bundle.start();
LOGGER.info("Started: " + bundle);
} catch (BundleException e) {
LOGGER.warn("Failed to start bundle: " + bundle + ". Reason: " + e, e);
rescheduleStart(bundle);
}
}
PackageAdmin packageAdmin = getPackageAdmin();
if (packageAdmin != null) {
packageAdmin.refreshPackages(null);
}
}
private void rescheduleStart(Bundle bundle) {
synchronized (pendingStartBundles) {
pendingStartBundles.add(bundle);
if (bundleListener == null) {
bundleListener = new BundleListener() {
public void bundleChanged(BundleEvent event) {
if (event.getType() == BundleEvent.RESOLVED) {
executor.execute(new Runnable() {
public void run() {
retryPendingStartBundles();
}
});
} else if (event.getType() == BundleEvent.UNINSTALLED) {
// bundle was uninstalled meanwhile, so remove
// it from the list of pending bundles
pendingStartBundles.remove(event.getBundle());
}
}
};
getContext().addBundleListener(bundleListener);
}
}
}
protected void retryPendingStartBundles() {
synchronized (pendingStartBundles) {
Set<Bundle> bundles = new HashSet<Bundle>();
bundles.addAll(pendingStartBundles);
pendingStartBundles.clear();
boolean success = false;
for (Bundle bundle : bundles) {
try {
bundle.start();
LOGGER.info("Pending bundle started: " + bundle);
success = true;
} catch (BundleException e) {
LOGGER.info("Pending bundle not started: " + bundle);
pendingStartBundles.add(bundle);
}
}
if (success) {
PackageAdmin packageAdmin = getPackageAdmin();
if (packageAdmin != null) {
packageAdmin.refreshPackages(null);
}
}
}
}
protected File createBundleJar(File dir) throws BundleException, IOException {
File destFile = new File(generateDir, dir.getName() + ".jar");
if (destFile.exists()) {
undeployBundle(destFile);
destFile.delete();
}
JarUtil.jarDir(dir.getPath(), destFile.getPath());
return destFile;
}
/**
* Returns the root directory of the expanded OSGi bundle if the file is
* part of an expanded OSGi bundle or null if it is not
*/
protected File getExpandedBundleRootDirectory(File file) throws IOException {
File parent = file.getParentFile();
if (file.isDirectory()) {
String rootPath = deployDir.getCanonicalPath();
if (file.getCanonicalPath().equals(rootPath)) {
return null;
}
if (containsManifest(file)) {
return file;
}
}
if (isValidBundleSourceDirectory(parent)) {
return getExpandedBundleRootDirectory(parent);
}
return null;
}
/**
* Returns true if the given directory is a valid child directory within the
* {@link #deployDir}
*/
protected boolean isValidBundleSourceDirectory(File dir) throws IOException {
if (dir != null) {
String parentPath = dir.getCanonicalPath();
String rootPath = deployDir.getCanonicalPath();
return !parentPath.equals(rootPath) && parentPath.startsWith(rootPath);
} else {
return false;
}
}
/**
* Returns true if the given directory is a valid child file within the
* {@link #deployDir} or a child of {@link #generateDir}
*/
protected boolean isValidArtifactFile(File file) throws IOException {
if (file != null) {
String filePath = file.getParentFile().getCanonicalPath();
String deployPath = deployDir.getCanonicalPath();
String generatePath = generateDir.getCanonicalPath();
return filePath.equals(deployPath) || filePath.startsWith(generatePath);
} else {
return false;
}
}
/**
* Returns true if the given directory is a valid child file within the
* {@link #configDir}
*/
protected boolean isValidConfigFile(File file) throws IOException {
if (file != null && file.getName().endsWith(".cfg")) {
String filePath = file.getParentFile().getCanonicalPath();
String configPath = configDir.getCanonicalPath();
return filePath.equals(configPath);
} else {
return false;
}
}
/**
* Returns true if the given directory contains a valid manifest file
*/
protected boolean containsManifest(File dir) {
File metaInfDir = new File(dir, "META-INF");
if (metaInfDir.exists() && metaInfDir.isDirectory()) {
File manifest = new File(metaInfDir, "MANIFEST.MF");
return manifest.exists() && !manifest.isDirectory();
}
return false;
}
@SuppressWarnings("unchecked")
protected File getFileValue(Dictionary properties, String key) {
Object value = properties.get(key);
if (value instanceof File) {
return (File)value;
} else if (value != null) {
return new File(value.toString());
}
return null;
}
@SuppressWarnings("unchecked")
protected Long getLongValue(Dictionary properties, String key) {
Object value = properties.get(key);
if (value instanceof Long) {
return (Long)value;
} else if (value != null) {
return Long.parseLong(value.toString());
}
return null;
}
protected void closeQuietly(Closeable in) {
try {
in.close();
} catch (IOException e) {
LOGGER.warn("Failed to close stream. " + e, e);
}
}
protected PreferencesService getPreferenceService() {
if (activator.getPreferenceServiceTracker() != null) {
try {
return (PreferencesService)activator.getPreferenceServiceTracker().waitForService(5000L);
} catch (InterruptedException e) {
// ignore
}
}
return null;
}
protected void saveScannerState() {
try {
Map<String, Long> results = scanner.getLastScanResults();
Preferences prefs = getPreferenceService().getUserPreferences(PREFERENCE_KEY).node(pid);
Iterator<String> it = results.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
Long value = results.get(key);
prefs.putLong(key, value);
}
prefs.flush();
} catch (Exception e) {
LOGGER.error("Error persisting FileMonitor state", e);
}
}
protected void loadScannerState() {
try {
Preferences prefs = getPreferenceService().getUserPreferences(PREFERENCE_KEY).node(pid);
Map<String, Long> lastResult = new HashMap<String, Long>();
for (String key : prefs.keys()) {
lastResult.put(key, prefs.getLong(key, -1));
}
scanner.setLastScanResults(lastResult);
} catch (Exception e) {
LOGGER.error("Error loading FileMonitor state", e);
}
}
}