/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.extensionservice.marketplace.internal; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.io.IOUtils; import org.apache.commons.io.LineIterator; import org.eclipse.smarthome.extensionservice.marketplace.MarketplaceExtension; import org.eclipse.smarthome.extensionservice.marketplace.MarketplaceExtensionHandler; import org.eclipse.smarthome.extensionservice.marketplace.MarketplaceHandlerException; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A {@link MarketplaceExtensionHandler} implementation, which handles bindings as jar files (OSGi bundles) and installs * them through the standard OSGi bundle installation mechanism. * The information, which installed bundle corresponds to which extension is written to a file in the bundle's data * store. It is therefore wiped together with the bundles upon an OSGi "clean". * We might want to move this class into a separate bundle in future, when we add support for further extension types. * * @author Kai Kreuzer - Initial contribution and API * */ public class BindingExtensionHandler implements MarketplaceExtensionHandler { private static final String BINDING_FILE = "installedBindingsMap.csv"; private final Logger logger = LoggerFactory.getLogger(BindingExtensionHandler.class); private Map<String, Long> installedBindings; private BundleContext bundleContext; protected void activate(BundleContext bundleContext, Map<String, Object> config) { this.bundleContext = bundleContext; installedBindings = loadInstalledBindingsMap(); } protected void deactivate() { this.installedBindings = null; this.bundleContext = null; } @Override public boolean supports(MarketplaceExtension ext) { // we support only bindings as pure OSGi bundles return ext.getType().equals(MarketplaceExtension.EXT_TYPE_BINDING) && ext.getPackageFormat().equals(MarketplaceExtension.EXT_FORMAT_BUNDLE); } @Override public boolean isInstalled(MarketplaceExtension ext) { return installedBindings.containsKey(ext.getId()); } @Override public void install(MarketplaceExtension ext) throws MarketplaceHandlerException { String url = ext.getDownloadUrl(); try { Bundle bundle = bundleContext.installBundle(url); try { bundle.start(); } catch (BundleException e) { logger.warn("Installed bundle, but failed to start it: {}", e.getMessage()); } installedBindings.put(ext.getId(), bundle.getBundleId()); persistInstalledBindingsMap(installedBindings); } catch (BundleException e) { throw new MarketplaceHandlerException("Binding cannot be installed: " + e.getMessage()); } } @Override public void uninstall(MarketplaceExtension ext) throws MarketplaceHandlerException { Long id = installedBindings.get(ext.getId()); if (id != null) { Bundle bundle = bundleContext.getBundle(id); if (bundle != null) { try { bundle.stop(); bundle.uninstall(); installedBindings.remove(ext.getId()); persistInstalledBindingsMap(installedBindings); } catch (BundleException e) { throw new MarketplaceHandlerException("Failed deinstalling binding: " + e.getMessage()); } } else { // we do not have such a bundle, so let's remove it from our internal map installedBindings.remove(ext.getId()); persistInstalledBindingsMap(installedBindings); throw new MarketplaceHandlerException("Id not known."); } } else { throw new MarketplaceHandlerException("Id not known."); } } private Map<String, Long> loadInstalledBindingsMap() { File dataFile = bundleContext.getDataFile(BINDING_FILE); if (dataFile != null && dataFile.exists()) { try (FileReader reader = new FileReader(dataFile)) { LineIterator lineIterator = IOUtils.lineIterator(reader); Map<String, Long> map = new HashMap<>(); while (lineIterator.hasNext()) { String line = lineIterator.nextLine(); String[] parts = line.split(";"); if (parts.length == 2) { try { map.put(parts[0], Long.valueOf(parts[1])); } catch (NumberFormatException e) { logger.debug("Cannot parse '{}' as a number in file {} - ignoring it.", parts[1], dataFile.getName()); } } else { logger.debug("Invalid line in file {} - ignoring it:\n{}", dataFile.getName(), line); } } return map; } catch (IOException e) { logger.debug("File '{}' for installed bindings does not exist.", dataFile.getName()); // ignore and just return an empty map } } return new HashMap<>(); } private synchronized void persistInstalledBindingsMap(Map<String, Long> map) { File dataFile = bundleContext.getDataFile(BINDING_FILE); if (dataFile != null) { try (FileWriter writer = new FileWriter(dataFile)) { for (Entry<String, Long> entry : map.entrySet()) { writer.write(entry.getKey() + ";" + entry.getValue() + System.lineSeparator()); } } catch (IOException e) { logger.warn("Failed writing file '{}': {}", dataFile.getName(), e.getMessage()); } } else { logger.debug("System does not support bundle data files -> not persisting installed binding info"); } } }