net.solarnetwork.node.setup.obr.OBRProvisionTask.java Source code

Java tutorial

Introduction

Here is the source code for net.solarnetwork.node.setup.obr.OBRProvisionTask.java

Source

/* ==================================================================
 * OBRProvisionTask.java - Apr 24, 2014 8:09:12 PM
 * 
 * Copyright 2007-2014 SolarNetwork.net Dev Team
 * 
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License as 
 * published by the Free Software Foundation; either version 2 of 
 * the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License 
 * along with this program; if not, write to the Free Software 
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 
 * 02111-1307 USA
 * ==================================================================
 */

package net.solarnetwork.node.setup.obr;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.codec.digest.DigestUtils;
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.framework.VersionRange;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.FrameworkWiring;
import org.osgi.service.obr.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import net.solarnetwork.node.SystemService;
import net.solarnetwork.node.backup.Backup;
import net.solarnetwork.node.backup.BackupManager;
import net.solarnetwork.node.setup.BundlePlugin;
import net.solarnetwork.node.setup.Plugin;

/**
 * Task to install plugins.
 * 
 * @author matt
 * @version 1.1
 */
public class OBRProvisionTask implements Callable<OBRPluginProvisionStatus> {

    private static final Logger LOG = LoggerFactory.getLogger(OBRProvisionTask.class);

    private final BundleContext bundleContext;
    private final OBRPluginProvisionStatus status;
    private Future<OBRPluginProvisionStatus> future;
    private final File directory;
    private final BackupManager backupManager;
    private final SystemService systemService;

    /**
     * Construct with a status.
     * 
     * @param bundleContext
     *        the BundleContext to manipulate bundles with
     * @param status
     *        the status, which defines the plugins to install
     * @param directory
     *        the directory to download plugins to
     * @param backupManager
     *        if provided, then a backup will be performed before provisioning
     *        any bundles
     * @param systemService
     *        if provided and
     *        {@link OBRPluginProvisionStatus#isRestartRequired()} is
     *        {@literal true} then perform a restart after the provision task
     *        completes
     */
    public OBRProvisionTask(BundleContext bundleContext, OBRPluginProvisionStatus status, File directory,
            BackupManager backupManager, SystemService systemService) {
        super();
        this.bundleContext = bundleContext;
        this.status = status;
        this.directory = directory;
        this.backupManager = backupManager;
        this.systemService = systemService;
        this.status.setBackupComplete(backupManager == null);
    }

    @Override
    public OBRPluginProvisionStatus call() throws Exception {
        try {
            status.setStatusMessage("Starting provisioning operation.");
            handleBackupBeforeProvisioningOperation();
            if (status.getPluginsToInstall() != null && status.getPluginsToInstall().size() > 0) {
                downloadPlugins(status.getPluginsToInstall());
            }
            if (status.getPluginsToRemove() != null && status.getPluginsToRemove().size() > 0) {
                removePlugins(status.getPluginsToRemove());
            }
            status.setStatusMessage("Provisioning operation complete.");
            return status;
        } catch (Exception e) {
            LOG.warn("Error in provision task: {}", e.getMessage(), e);
            status.setStatusMessage("Error in provisioning operation: " + e.getMessage());
            throw e;
        }
    }

    private void handleBackupBeforeProvisioningOperation() {
        // if we are actually going to provision something, let's make a backup
        if (backupManager != null && status.getOverallProgress() < 1) {
            status.setStatusMessage("Creating backup before provisioning operation.");
            LOG.info("Creating backup before provisioning operation.");
            try {
                Backup backup = backupManager.createBackup();
                if (backup != null) {
                    LOG.info("Created backup {} (size {})", backup.getKey(), backup.getSize());
                    status.setStatusMessage("Backup complete.");
                    status.setBackupComplete(Boolean.TRUE);
                }
            } catch (RuntimeException e) {
                status.setBackupComplete(Boolean.FALSE);
                LOG.warn("Error creating backup for provisioning operation {}", status.getProvisionID(), e);
            }
        }
    }

    /**
     * Find all installed bundles for a specific ID that are less than or equal
     * to a specific version.
     * 
     * @param symbolicName
     *        The bundle ID to look for.
     * @param maxVersion
     *        The maximum version to include in the result.
     * @return All found bundles whose symbolic name matches and has a version
     *         less than {@code maxVersion}, in largest to smallest order, or
     *         <em>null</em> if none found.
     */
    private List<Bundle> findBundlesOlderThanVersion(String symbolicName, Version maxVersion) {
        List<Bundle> olderBundles = null;
        Bundle[] bundles = bundleContext.getBundles();
        for (Bundle b : bundles) {
            if (b.getSymbolicName().equals(symbolicName) && b.getVersion().compareTo(maxVersion) < 1) {
                if (olderBundles == null) {
                    olderBundles = new ArrayList<Bundle>(2);
                }
                olderBundles.add(b);
            }
        }
        if (olderBundles != null) {
            Collections.sort(olderBundles, new Comparator<Bundle>() {

                @Override
                public int compare(Bundle o1, Bundle o2) {
                    return o2.getVersion().compareTo(o1.getVersion());
                }
            });
        }
        return olderBundles;
    }

    private void downloadPlugins(List<Plugin> plugins) throws InterruptedException {
        assert plugins != null;
        LOG.debug("Starting install of {} plugins", plugins.size());
        if (!directory.exists() && !directory.mkdirs()) {
            throw new RuntimeException("Unable to create plugin directory: " + directory.toString());
        }

        // This method will manually download the bundle for each resolved plugin, 
        // then install it and start it in the running OSGi platform. We don't
        // make use of the OBR RepositoryAdmin to do this because on SolarNode
        // the bundle's runtime area is held only in RAM (not persisted to disk)
        // but we want these downloaded bundles to be persisted to disk. Thus we
        // just do a bit of work here to download and start the bundles ourselves.

        List<Bundle> installedBundles = new ArrayList<Bundle>(plugins.size());

        // iterate backwards, to work our way up through deps to requested plugin
        for (ListIterator<Plugin> itr = plugins.listIterator(plugins.size()); itr.hasPrevious();) {
            Plugin plugin = itr.previous();
            assert plugin instanceof OBRResourcePlugin;
            LOG.debug("Starting install of plugin: {}", plugin.getUID());
            status.setStatusMessage("Starting install of plugin " + plugin.getUID());

            OBRResourcePlugin obrPlugin = (OBRResourcePlugin) plugin;
            Resource resource = obrPlugin.getResource();
            URL resourceURL = resource.getURL();
            String pluginFileName = StringUtils.getFilename(resourceURL.getPath());
            File outputFile = new File(directory, pluginFileName);
            String bundleSymbolicName = resource.getSymbolicName();

            // download to tmp file first, then we'll rename
            File tmpOutputFile = new File(directory, "." + pluginFileName);
            LOG.debug("Downloading plugin {} => {}", resourceURL, tmpOutputFile);
            try {
                FileCopyUtils.copy(resourceURL.openStream(), new FileOutputStream(tmpOutputFile));
            } catch (IOException e) {
                throw new RuntimeException("Unable to download plugin " + bundleSymbolicName, e);
            }

            moveTemporaryDownloadedPluginFile(resource, outputFile, tmpOutputFile);

            installDownloadedPlugin(resource, outputFile, installedBundles);

            LOG.debug("Installed plugin: {}", plugin.getUID());
            status.markPluginInstalled(plugin);
        }
        if (!installedBundles.isEmpty()) {
            Set<Bundle> toRefresh = findFragmentHostsForBundles(installedBundles);
            toRefresh.addAll(installedBundles);
            status.setStatusMessage("Refreshing OSGi framework.");
            FrameworkWiring fw = bundleContext.getBundle(0).adapt(FrameworkWiring.class);
            fw.refreshBundles(toRefresh);

            for (ListIterator<Bundle> itr = installedBundles.listIterator(); itr.hasNext();) {
                Bundle b = itr.next();
                boolean fragment = isFragment(b);
                status.setStatusMessage("Starting plugin: " + b.getSymbolicName());
                try {
                    if (!fragment && !(b.getState() == Bundle.ACTIVE || b.getState() == Bundle.STARTING)) {
                        b.start();
                    }
                    // bundles are in reverse order of plugins
                    Plugin p = plugins.get(plugins.size() - itr.nextIndex());
                    status.markPluginStarted(p);
                } catch (BundleException e) {
                    throw new RuntimeException(
                            "Unable to start plugin " + b.getSymbolicName() + " version " + b.getVersion(), e);
                }
            }
        }
        if (status.isRestartRequired() && systemService == null) {
            LOG.debug("Install of {} plugins complete; manual restart required", plugins.size());
            status.setStatusMessage("Install of " + plugins.size() + " plugins complete; manual restart required");
        } else if (status.isRestartRequired()) {
            LOG.debug("Install of {} plugins complete; restarting now", plugins.size());
            status.setStatusMessage("Install of " + plugins.size() + " plugins complete; restarting now");
            performRestart();
        } else {
            LOG.debug("Install of {} plugins complete", plugins.size());
            status.setStatusMessage("Install of " + plugins.size() + " plugins complete");
        }
    }

    private void performRestart() {
        if (systemService == null) {
            return;
        }
        // restart after a delay, to give time for status query
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // ignore
                }
                systemService.exit(true);
            }
        }).start();
    }

    private boolean isFragment(Bundle b) {
        BundleRevision r = b.adapt(BundleRevision.class);
        return (r != null && (r.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0);
    }

    private static final Pattern BUNDLE_VERSION_PATTERN = Pattern.compile("bundle-version\\s*=\\s*\"([^\"]+)",
            Pattern.CASE_INSENSITIVE);

    private Set<Bundle> findFragmentHostsForBundles(Collection<Bundle> toRefresh) {
        Set<Bundle> fragmentHosts = new HashSet<Bundle>();
        Bundle[] bundles = bundleContext.getBundles();
        for (Bundle b : toRefresh) {
            if (b.getState() == Bundle.UNINSTALLED) {
                continue;
            }
            String hostHeader = b.getHeaders().get(Constants.FRAGMENT_HOST);
            if (hostHeader == null) {
                continue;
            }
            String[] clauses = StringUtils.delimitedListToStringArray(hostHeader, ";");
            if (clauses == null || clauses.length < 1) {
                continue;
            }
            String hostSymbolicName = clauses[0];
            for (Bundle hostBundle : bundles) {
                if (hostBundle.getSymbolicName() != null && hostBundle.getSymbolicName().equals(hostSymbolicName)) {
                    VersionRange hostVersionRange = null;
                    if (clauses.length > 1) {
                        for (String clause : clauses) {
                            Matcher m = BUNDLE_VERSION_PATTERN.matcher(clause);
                            if (m.find()) {
                                String ver = m.group(1);
                                try {
                                    hostVersionRange = new org.osgi.framework.VersionRange(ver);
                                } catch (IllegalArgumentException e) {
                                    LOG.warn("Ignoring fragment bundle {} version range syntax error: {}",
                                            hostSymbolicName, e.getMessage());
                                }
                                break;
                            }
                        }
                    }
                    if (hostVersionRange == null || hostVersionRange.includes(hostBundle.getVersion())) {
                        LOG.debug("Found fragment {} host {} to refresh", b, hostBundle);
                        fragmentHosts.add(hostBundle);
                    }
                    continue;
                }
            }
        }
        return fragmentHosts;
    }

    private void moveTemporaryDownloadedPluginFile(Resource resource, File outputFile, File tmpOutputFile) {
        if (outputFile.exists()) {
            // if the file has not changed, just delete tmp file
            InputStream outputFileInputStream = null;
            InputStream tmpOutputFileInputStream = null;
            try {
                outputFileInputStream = new FileInputStream(outputFile);
                tmpOutputFileInputStream = new FileInputStream(tmpOutputFile);
                String outputFileHash = DigestUtils.sha1Hex(outputFileInputStream);
                String tmpOutputFileHash = DigestUtils.sha1Hex(tmpOutputFileInputStream);
                if (tmpOutputFileHash.equals(outputFileHash)) {
                    // file unchanged, so just delete tmp file
                    tmpOutputFile.delete();
                } else {
                    LOG.debug("Bundle {} version {} content updated", resource.getSymbolicName(),
                            resource.getVersion());
                    outputFile.delete();
                    tmpOutputFile.renameTo(outputFile);
                }
            } catch (IOException e) {
                throw new RuntimeException("Error downloading plugin " + resource.getSymbolicName(), e);
            } finally {
                if (outputFileInputStream != null) {
                    try {
                        outputFileInputStream.close();
                    } catch (IOException e) {
                        // ignore;
                    }
                }
                if (tmpOutputFileInputStream != null) {
                    try {
                        tmpOutputFileInputStream.close();
                    } catch (IOException e) {
                        // ignore
                    }
                }
            }
        } else {
            // rename tmp file
            tmpOutputFile.renameTo(outputFile);
        }
    }

    private boolean installDownloadedPlugin(Resource resource, File outputFile, List<Bundle> installedBundles) {
        final String bundleSymbolicName = resource.getSymbolicName();
        boolean refreshNeeded = false;
        try {
            URL newBundleURL = outputFile.toURI().toURL();
            List<Bundle> oldBundles = findBundlesOlderThanVersion(bundleSymbolicName, resource.getVersion());
            Bundle oldBundle = (oldBundles != null && oldBundles.size() > 0 ? oldBundles.get(0) : null);
            Version oldVersion = (oldBundle != null ? oldBundle.getVersion() : null);
            if (oldVersion != null && oldVersion.compareTo(resource.getVersion()) >= 0) {
                LOG.debug("Skipping install of plugin {} as version is unchanged at {}", bundleSymbolicName,
                        oldVersion);
            } else if (oldVersion != null) {
                InputStream in = null;
                try {
                    // only update bundles in runtime if restartRequired flag not set
                    if (!status.isRestartRequired()) {
                        LOG.debug("Upgrading plugin {} from {} to {}", bundleSymbolicName, oldVersion,
                                resource.getVersion());
                        in = new BufferedInputStream(new FileInputStream(outputFile));
                        oldBundle.update(in);
                    }

                    // try to delete the old version
                    File oldJar = new File(directory, bundleSymbolicName + "-" + oldVersion + ".jar");
                    if (!oldJar.delete()) {
                        LOG.warn("Error deleting old plugin " + oldJar.getName());
                    }

                    if (status.isRestartRequired()) {
                        LOG.debug("Upgraded plugin {} {} will be available after restart", bundleSymbolicName,
                                resource.getVersion());
                    } else {
                        installedBundles.add(oldBundle);
                        LOG.info("Upgraded plugin {} from version {} to {}", bundleSymbolicName, oldVersion,
                                resource.getVersion());
                        refreshNeeded = true;
                    }
                } catch (BundleException e) {
                    throw new RuntimeException("Unable to upgrade plugin " + bundleSymbolicName, e);
                } catch (FileNotFoundException e) {
                    throw new RuntimeException("Unable to upgrade plugin " + bundleSymbolicName, e);
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException e) {
                            // ignore
                        }
                    }
                }
            } else {
                if (status.isRestartRequired()) {
                    LOG.debug("Downloaded plugin {} version {} will be installed after restart", newBundleURL,
                            resource.getVersion());
                } else {
                    LOG.debug("Installing plugin {} version {}", newBundleURL, resource.getVersion());
                    Bundle newBundle = bundleContext.installBundle(newBundleURL.toString());
                    LOG.info("Installed plugin {} version {}", newBundle.getSymbolicName(), newBundle.getVersion());
                    installedBundles.add(newBundle);
                }
            }
        } catch (BundleException e) {
            throw new RuntimeException("Unable to install plugin " + bundleSymbolicName, e);
        } catch (MalformedURLException e) {
            throw new RuntimeException("Unable to install plugin " + bundleSymbolicName, e);
        }
        return refreshNeeded;
    }

    private void removePlugins(List<Plugin> plugins) {
        assert plugins != null;
        LOG.debug("Starting removal of {} plugins", plugins.size());

        final boolean restartRequired = status.isRestartRequired();
        boolean refreshNeeded = false;
        for (Plugin plugin : plugins) {
            assert plugin instanceof BundlePlugin;
            LOG.debug("Starting removal of plugin: {}", plugin.getUID());
            status.setStatusMessage("Starting removal of plugin " + plugin.getUID());
            BundlePlugin bundlePlugin = (BundlePlugin) plugin;
            Bundle oldBundle = bundlePlugin.getBundle();
            if (oldBundle != null) {
                Version oldVersion = oldBundle.getVersion();
                if (!restartRequired) {
                    LOG.debug("Removing plugin {} version {}", oldBundle.getSymbolicName(), oldVersion);
                    try {
                        oldBundle.uninstall();
                        refreshNeeded = true;
                    } catch (BundleException e) {
                        throw new RuntimeException("Unable to uninstall plugin " + oldBundle.getSymbolicName(), e);
                    }
                }
                File oldJar = new File(directory, oldBundle.getSymbolicName() + "-" + oldVersion + ".jar");
                if (!oldJar.delete()) {
                    LOG.warn("Error deleting plugin JAR " + oldJar.getName());
                }
            }

            LOG.debug("Removed plugin: {}", plugin.getUID());
            status.setStatusMessage("Removed plugin " + plugin.getUID());
            status.markPluginRemoved(plugin);
        }
        if (refreshNeeded) {
            status.setStatusMessage("Refreshing OSGi framework.");
            FrameworkWiring fw = bundleContext.getBundle(0).adapt(FrameworkWiring.class);
            fw.refreshBundles(null);
        }
        LOG.debug("Removal of {} plugins complete", plugins.size());
        if (restartRequired) {
            performRestart();
        }
    }

    public OBRPluginProvisionStatus getStatus() {
        return status;
    }

    Future<OBRPluginProvisionStatus> getFuture() {
        return future;
    }

    void setFuture(Future<OBRPluginProvisionStatus> future) {
        this.future = future;
    }

    public File getDirectory() {
        return directory;
    }

}