Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Community Marketplace Add-on Service - initial contribution #2405

Merged
merged 14 commits into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bom/openhab-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,12 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.addon.marketplace</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>

</project>
29 changes: 29 additions & 0 deletions bundles/org.openhab.core.addon.marketplace/.classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="annotationpath" value="target/dependency"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="annotationpath" value="target/dependency"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
23 changes: 23 additions & 0 deletions bundles/org.openhab.core.addon.marketplace/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.addon.marketplace</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
14 changes: 14 additions & 0 deletions bundles/org.openhab.core.addon.marketplace/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
This content is produced and maintained by the openHAB project.

* Project home: https://www.openhab.org

== Declared Project Licenses

This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.

== Source Code

https://github.com/openhab/openhab-core

43 changes: 43 additions & 0 deletions bundles/org.openhab.core.addon.marketplace/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.reactor.bundles</artifactId>
<version>3.2.0-SNAPSHOT</version>
</parent>

<artifactId>org.openhab.core.addon.marketplace</artifactId>

<name>openHAB Core :: Bundles :: Community Marketplace Add-on Service</name>

<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.core</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.ui</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.addon.marketplace;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.addon.Addon;

/**
* This interface can be implemented by services that want to register as handlers for specific marketplace add-on
* content types and content types.
* In a system there should always only be exactly one handler responsible for a given type+contentType
* combination. If
* multiple handers support it, it is undefined which one will be called.
* This mechanism allows solutions to add support for specific formats (e.g. Karaf features) that are not supported by
* openHAB out of the box.
* It also allows to decide which add-on types are made available at all.
*
* @author Kai Kreuzer - Initial contribution and API
* @author Yannick Schaus - refactoring
*
*/
@NonNullByDefault
public interface MarketplaceAddonHandler {

/**
* Tells whether this handler supports a given add-on.
*
* @param addon the add-on in question
* @return true, if the add-on is supported, false otherwise
*/
boolean supports(String type, String contentType);

/**
* Tells whether a given add-on is currently installed.
* Note: This method is only called, if the hander claimed support for the add-on before.
*
* @param id the id of the add-on in question
* @return true, if the add-on is installed, false otherwise
*/
boolean isInstalled(String id);

/**
* Installs a given add-on.
* Note: This method is only called, if the hander claimed support for the add-on before.
*
* @param addon the add-on to install
* @throws MarketplaceHandlerException if the installation failed for some reason
*/
void install(Addon addon) throws MarketplaceHandlerException;

/**
* Uninstalls a given add-on.
* Note: This method is only called, if the hander claimed support for the add-on before.
*
* @param addon the add-on to uninstall
* @throws MarketplaceHandlerException if the uninstallation failed for some reason
*/
void uninstall(Addon addon) throws MarketplaceHandlerException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.addon.marketplace;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.OpenHAB;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Handle the management of bundles related to marketplace add-ons that resists OSGi cache cleanups.
*
* These operations cache incoming bundle files locally in a structure under the user data folder, and can make sure the
* bundles are re-installed if they are present in the local cache but not installed in the OSGi framework.
* They can be used by marketplace handler implementations dealing with OSGi bundles.
*
* @author Yannick Schaus - Initial contribution and API
*
*/
@NonNullByDefault
public abstract class MarketplaceBundleInstaller {
private final Logger logger = LoggerFactory.getLogger(MarketplaceBundleInstaller.class);

private static final String BUNDLE_CACHE_PATH = OpenHAB.getUserDataFolder() + File.separator + "marketplace"
+ File.separator + "bundles";

/**
* Downloads a bundle file from a remote source and puts it in the local cache with the add-on ID.
*
* @param addonId the add-on ID
* @param sourceUrl the (online) source where the .jar file can be found
* @throws MarketplaceHandlerException
*/
protected void addBundleToCache(String addonId, URL sourceUrl) throws MarketplaceHandlerException {
try {
String fileName = new File(sourceUrl.toURI().getPath()).getName();
File addonFile = new File(getAddonCacheDirectory(addonId), fileName);
addonFile.getParentFile().mkdirs();

InputStream source = sourceUrl.openStream();
Path outputPath = Path.of(addonFile.toURI());
Files.copy(source, outputPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException | URISyntaxException e) {
throw new MarketplaceHandlerException("Cannot copy bundle to local cache: " + e.getMessage());
}
}

/**
* Installs a bundle from its ID by looking up in the local cache
*
* @param bundleContext the {@link BundleContext} to use to install the bundle
* @param addonId the add-on ID
* @throws MarketplaceHandlerException
*/
protected void installFromCache(BundleContext bundleContext, String addonId) throws MarketplaceHandlerException {
File addonPath = getAddonCacheDirectory(addonId);
if (addonPath.exists() && addonPath.isDirectory()) {
File[] bundleFiles = addonPath.listFiles();
if (bundleFiles.length != 1) {
throw new MarketplaceHandlerException(
"The local cache folder doesn't contain a single file: " + addonPath.toString());
}

try (FileInputStream fileInputStream = new FileInputStream(bundleFiles[0])) {
Bundle bundle = bundleContext.installBundle(addonId, fileInputStream);
try {
bundle.start();
} catch (BundleException e) {
logger.warn("The marketplace bundle was successfully installed but doesn't start: {}",
e.getMessage());
}

} catch (IOException | BundleException e) {
throw new MarketplaceHandlerException(
"Cannot install bundle from marketplace cache: " + e.getMessage());
}
}
}

/**
* Determines whether a bundle associated to the given add-on ID is installed
*
* @param bundleContext the {@link BundleContext} to use to look up the bundle
* @param addonId the add-on ID
*/
protected boolean isBundleInstalled(BundleContext bundleContext, String addonId) {
return bundleContext.getBundle(addonId) != null;
}

/**
* Uninstalls a bundle associated to the given add-on ID. Also removes it from the local cache.
*
* @param bundleContext the {@link BundleContext} to use to look up the bundle
* @param addonId the add-on ID
*/
protected void uninstallBundle(BundleContext bundleContext, String addonId) throws MarketplaceHandlerException {
File addonPath = getAddonCacheDirectory(addonId);
if (addonPath.exists() && addonPath.isDirectory()) {
for (File bundleFile : addonPath.listFiles()) {
bundleFile.delete();
}
}
addonPath.delete();

if (isBundleInstalled(bundleContext, addonId)) {
Bundle bundle = bundleContext.getBundle(addonId);
try {
bundle.stop();
bundle.uninstall();
} catch (BundleException e) {
throw new MarketplaceHandlerException("Failed uninstalling bundle: " + e.getMessage());
}
}
}

/**
* Iterates over the local cache entries and re-installs bundles that are missing
*
* @param bundleContext the {@link BundleContext} to use to look up the bundles
*/
protected void ensureCachedBundlesAreInstalled(BundleContext bundleContext) {
File addonPath = new File(BUNDLE_CACHE_PATH);
if (addonPath.exists() && addonPath.isDirectory()) {
for (File bundleFile : addonPath.listFiles()) {
if (bundleFile.isDirectory()) {
String addonId = "marketplace:" + bundleFile.getName();
if (!isBundleInstalled(bundleContext, addonId)) {
logger.info("Reinstalling missing marketplace bundle: {}", addonId);
try {
installFromCache(bundleContext, addonId);
} catch (MarketplaceHandlerException e) {
logger.warn("Failed reinstalling add-on from cache", e);
}
}
}
bundleFile.delete();
}
}
}

private File getAddonCacheDirectory(String addonId) {
return new File(BUNDLE_CACHE_PATH + File.separator + addonId.replace("marketplace:", ""));
}
}
Loading