/*
 * 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();
    }

}
