From 4569eea51905d0c0c66af91581ee0452b980cb88 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Thu, 23 Sep 2021 17:07:34 +0200 Subject: [PATCH] Extend marketplace to accept kar and some improvements (#2490) Also-by: Wouter Born Signed-off-by: Jan N. Klug --- bom/openhab-core/pom.xml | 6 + .../.classpath | 29 +++ .../.project | 23 +++ .../NOTICE | 14 ++ .../pom.xml | 32 +++ .../community/CommunityKarafAddonHandler.java | 190 ++++++++++++++++++ .../marketplace/MarketplaceAddonHandler.java | 5 +- .../MarketplaceBundleInstaller.java | 98 ++++----- .../MarketplaceHandlerException.java | 5 +- .../CommunityBundleAddonHandler.java | 35 +--- .../CommunityMarketplaceAddonService.java | 147 ++++++++++---- .../CommunityRuleTemplateAddonHandler.java | 26 ++- .../CommunityUIWidgetAddonHandler.java | 11 +- ...java => DiscourseCategoryResponseDTO.java} | 2 +- ...se.java => DiscourseTopicResponseDTO.java} | 2 +- .../sample/internal/SampleAddonService.java | 5 +- .../java/org/openhab/core/addon/Addon.java | 49 ++++- bundles/pom.xml | 1 + .../openhab-core/src/main/feature/feature.xml | 2 + 19 files changed, 532 insertions(+), 150 deletions(-) create mode 100644 bundles/org.openhab.core.addon.marketplace.karaf/.classpath create mode 100644 bundles/org.openhab.core.addon.marketplace.karaf/.project create mode 100644 bundles/org.openhab.core.addon.marketplace.karaf/NOTICE create mode 100644 bundles/org.openhab.core.addon.marketplace.karaf/pom.xml create mode 100644 bundles/org.openhab.core.addon.marketplace.karaf/src/main/java/org/openhab/core/addon/marketplace/karaf/internal/community/CommunityKarafAddonHandler.java rename bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/{DiscourseCategoryResponse.java => DiscourseCategoryResponseDTO.java} (97%) rename bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/{DiscourseTopicResponse.java => DiscourseTopicResponseDTO.java} (97%) diff --git a/bom/openhab-core/pom.xml b/bom/openhab-core/pom.xml index db1c79e682..00c4a1dff1 100644 --- a/bom/openhab-core/pom.xml +++ b/bom/openhab-core/pom.xml @@ -514,6 +514,12 @@ ${project.version} compile + + org.openhab.core.bundles + org.openhab.core.addon.marketplace.karaf + ${project.version} + compile + diff --git a/bundles/org.openhab.core.addon.marketplace.karaf/.classpath b/bundles/org.openhab.core.addon.marketplace.karaf/.classpath new file mode 100644 index 0000000000..4244343f8a --- /dev/null +++ b/bundles/org.openhab.core.addon.marketplace.karaf/.classpath @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.addon.marketplace.karaf/.project b/bundles/org.openhab.core.addon.marketplace.karaf/.project new file mode 100644 index 0000000000..889e37cc3a --- /dev/null +++ b/bundles/org.openhab.core.addon.marketplace.karaf/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.addon.marketplace.karaf + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.core.addon.marketplace.karaf/NOTICE b/bundles/org.openhab.core.addon.marketplace.karaf/NOTICE new file mode 100644 index 0000000000..6c17d0d8a4 --- /dev/null +++ b/bundles/org.openhab.core.addon.marketplace.karaf/NOTICE @@ -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 + diff --git a/bundles/org.openhab.core.addon.marketplace.karaf/pom.xml b/bundles/org.openhab.core.addon.marketplace.karaf/pom.xml new file mode 100644 index 0000000000..a0046176d4 --- /dev/null +++ b/bundles/org.openhab.core.addon.marketplace.karaf/pom.xml @@ -0,0 +1,32 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 3.2.0-SNAPSHOT + + + org.openhab.core.addon.marketplace.karaf + + openHAB Core :: Bundles :: Community Marketplace Add-on Service :: Karaf + + + + org.apache.karaf.kar + org.apache.karaf.kar.core + ${karaf.tooling.version} + compile + + + org.openhab.core.bundles + org.openhab.core.addon.marketplace + ${project.version} + compile + + + + diff --git a/bundles/org.openhab.core.addon.marketplace.karaf/src/main/java/org/openhab/core/addon/marketplace/karaf/internal/community/CommunityKarafAddonHandler.java b/bundles/org.openhab.core.addon.marketplace.karaf/src/main/java/org/openhab/core/addon/marketplace/karaf/internal/community/CommunityKarafAddonHandler.java new file mode 100644 index 0000000000..f48c39d370 --- /dev/null +++ b/bundles/org.openhab.core.addon.marketplace.karaf/src/main/java/org/openhab/core/addon/marketplace/karaf/internal/community/CommunityKarafAddonHandler.java @@ -0,0 +1,190 @@ +/** + * 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.karaf.internal.community; + +import static org.openhab.core.addon.marketplace.internal.community.CommunityMarketplaceAddonService.KAR_CONTENT_TYPE; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.karaf.kar.KarService; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.OpenHAB; +import org.openhab.core.addon.Addon; +import org.openhab.core.addon.marketplace.MarketplaceAddonHandler; +import org.openhab.core.addon.marketplace.MarketplaceHandlerException; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link CommunityKarafAddonHandler} implementation, which handles add-ons as KAR files and installs them + * using the {@link KarService}. + * + * @author Kai Kreuzer - Initial contribution and API + * @author Yannick Schaus - refactoring + * @author Jan N. Klug - refactor to support kar files + * + */ +@Component(immediate = true) +@NonNullByDefault +public class CommunityKarafAddonHandler implements MarketplaceAddonHandler { + private static final Path KAR_CACHE_PATH = Path.of(OpenHAB.getUserDataFolder(), "marketplace", "kar"); + private static final List SUPPORTED_EXT_TYPES = List.of("automation", "binding", "io", "persistence", + "transformation", "ui", "voice"); + private static final String KAR_DOWNLOAD_URL_PROPERTY = "kar_download_url"; + private static final String KAR_EXTENSION = ".kar"; + + private final Logger logger = LoggerFactory.getLogger(CommunityKarafAddonHandler.class); + + private final KarService karService; + + @Activate + public CommunityKarafAddonHandler(@Reference KarService karService) { + this.karService = karService; + ensureCachedKarsAreInstalled(); + } + + @Override + public boolean supports(String type, String contentType) { + return SUPPORTED_EXT_TYPES.contains(type) && KAR_CONTENT_TYPE.equals(contentType); + } + + private Stream karFilesStream(Path addonDirectory) throws IOException { + return Files.isDirectory(addonDirectory) ? Files.list(addonDirectory).map(Path::getFileName) + .filter(path -> path.toString().endsWith(KAR_EXTENSION)) : Stream.empty(); + } + + private String pathToKarRepoName(Path path) { + String fileName = path.getFileName().toString(); + return fileName.substring(0, fileName.length() - KAR_EXTENSION.length()); + } + + @Override + @SuppressWarnings("null") + public boolean isInstalled(String addonId) { + try { + Path addonDirectory = getAddonCacheDirectory(addonId); + List repositories = karService.list(); + return karFilesStream(addonDirectory).findFirst().map(this::pathToKarRepoName).map(repositories::contains) + .orElse(false); + } catch (Exception e) { + logger.warn("Failed to determine installation status for {}: ", addonId, e); + } + + return false; + } + + @Override + public void install(Addon addon) throws MarketplaceHandlerException { + try { + URL sourceUrl = new URL((String) addon.getProperties().get(KAR_DOWNLOAD_URL_PROPERTY)); + addKarToCache(addon.getId(), sourceUrl); + installFromCache(addon.getId()); + } catch (MalformedURLException e) { + throw new MarketplaceHandlerException("Malformed source URL: " + e.getMessage(), e); + } + } + + @Override + public void uninstall(Addon addon) throws MarketplaceHandlerException { + try { + Path addonPath = getAddonCacheDirectory(addon.getId()); + List repositories = karService.list(); + for (Path path : karFilesStream(addonPath).collect(Collectors.toList())) { + String karRepoName = pathToKarRepoName(path); + if (repositories.contains(karRepoName)) { + karService.uninstall(karRepoName); + } + Files.delete(addonPath.resolve(path)); + } + Files.delete(addonPath); + } catch (Exception e) { + throw new MarketplaceHandlerException("Failed uninstalling KAR: " + e.getMessage(), e); + } + } + + /** + * Downloads a KAR 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 KAR file can be found + * @throws MarketplaceHandlerException on error + */ + private void addKarToCache(String addonId, URL sourceUrl) throws MarketplaceHandlerException { + try { + String fileName = new File(sourceUrl.toURI().getPath()).getName(); + Path addonFile = getAddonCacheDirectory(addonId).resolve(fileName); + Files.createDirectories(addonFile.getParent()); + InputStream source = sourceUrl.openStream(); + Files.copy(source, addonFile, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException | URISyntaxException e) { + throw new MarketplaceHandlerException("Cannot copy KAR to local cache: " + e.getMessage(), e); + } + } + + private void installFromCache(String addonId) throws MarketplaceHandlerException { + Path addonPath = getAddonCacheDirectory(addonId); + if (Files.isDirectory(addonPath)) { + try { + List karFiles = Files.list(addonPath).collect(Collectors.toList()); + if (karFiles.size() != 1) { + throw new MarketplaceHandlerException( + "The local cache folder doesn't contain a single file: " + addonPath, null); + } + try { + karService.install(karFiles.get(0).toUri(), false); + } catch (Exception e) { + throw new MarketplaceHandlerException( + "Cannot install KAR from marketplace cache: " + e.getMessage(), e); + } + } catch (IOException e) { + throw new MarketplaceHandlerException("Could not list files in cache directory " + addonPath, e); + } + } + } + + private void ensureCachedKarsAreInstalled() { + try { + if (Files.isDirectory(KAR_CACHE_PATH)) { + Files.list(KAR_CACHE_PATH).filter(Files::isDirectory).map(p -> "marketplace:" + p.getFileName()) + .filter(addonId -> !isInstalled(addonId)).forEach(addonId -> { + logger.info("Reinstalling missing marketplace KAR: {}", addonId); + try { + installFromCache(addonId); + } catch (MarketplaceHandlerException e) { + logger.warn("Failed reinstalling add-on from cache", e); + } + }); + } + } catch (IOException e) { + logger.warn("Failed to re-install KARs: {}", e.getMessage()); + } + } + + private Path getAddonCacheDirectory(String addonId) { + return KAR_CACHE_PATH.resolve(addonId.replace("marketplace:", "")); + } +} diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceAddonHandler.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceAddonHandler.java index 3337297b6c..7981a584a2 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceAddonHandler.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceAddonHandler.java @@ -35,8 +35,9 @@ 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 + * @param type the type of the add-on in question + * @param contentType the content type of the add-on on question + * @return true, if the addon type and contentType are supported, false otherwise */ boolean supports(String type, String contentType); diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceBundleInstaller.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceBundleInstaller.java index 45574137f8..5b029bcd44 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceBundleInstaller.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceBundleInstaller.java @@ -21,6 +21,8 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.OpenHAB; @@ -44,8 +46,7 @@ import org.slf4j.LoggerFactory; 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"; + private static final Path BUNDLE_CACHE_PATH = Path.of(OpenHAB.getUserDataFolder(), "marketplace", "bundles"); /** * Downloads a bundle file from a remote source and puts it in the local cache with the add-on ID. @@ -57,14 +58,13 @@ public abstract class MarketplaceBundleInstaller { 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(); + Path addonFile = getAddonCacheDirectory(addonId).resolve(fileName); + Files.createDirectories(addonFile.getParent()); InputStream source = sourceUrl.openStream(); - Path outputPath = Path.of(addonFile.toURI()); - Files.copy(source, outputPath, StandardCopyOption.REPLACE_EXISTING); + Files.copy(source, addonFile, StandardCopyOption.REPLACE_EXISTING); } catch (IOException | URISyntaxException e) { - throw new MarketplaceHandlerException("Cannot copy bundle to local cache: " + e.getMessage()); + throw new MarketplaceHandlerException("Cannot copy bundle to local cache: " + e.getMessage(), e); } } @@ -76,26 +76,27 @@ public abstract class MarketplaceBundleInstaller { * @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()); + Path addonPath = getAddonCacheDirectory(addonId); + if (Files.isDirectory(addonPath)) { + try { + List bundleFiles = Files.list(addonPath).collect(Collectors.toList()); + if (bundleFiles.size() != 1) { + throw new MarketplaceHandlerException( + "The local cache folder doesn't contain a single file: " + addonPath, null); } + try (FileInputStream fileInputStream = new FileInputStream(bundleFiles.get(0).toFile())) { + 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()); + throw new MarketplaceHandlerException("Cannot install bundle from marketplace cache: " + e.getMessage(), + e); } } } @@ -117,21 +118,24 @@ public abstract class MarketplaceBundleInstaller { * @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(); + try { + Path addonPath = getAddonCacheDirectory(addonId); + if (Files.isDirectory(addonPath)) { + for (Path bundleFile : Files.list(addonPath).collect(Collectors.toList())) { + Files.delete(bundleFile); + } } + Files.delete(addonPath); + } catch (IOException e) { + throw new MarketplaceHandlerException("Failed to delete bundle-files: " + e.getMessage(), e); } - 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()); + throw new MarketplaceHandlerException("Failed uninstalling bundle: " + e.getMessage(), e); } } } @@ -142,26 +146,24 @@ public abstract class MarketplaceBundleInstaller { * @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(); + try { + if (Files.isDirectory(BUNDLE_CACHE_PATH)) { + Files.list(BUNDLE_CACHE_PATH).filter(Files::isDirectory).map(p -> "marketplace:" + p.getFileName()) + .filter(addonId -> !isBundleInstalled(bundleContext, addonId)).forEach(addonId -> { + logger.info("Reinstalling missing marketplace bundle: {}", addonId); + try { + installFromCache(bundleContext, addonId); + } catch (MarketplaceHandlerException e) { + logger.warn("Failed reinstalling add-on from cache", e); + } + }); } + } catch (IOException e) { + logger.warn("Failed to re-install bundles: {}", e.getMessage()); } } - private File getAddonCacheDirectory(String addonId) { - return new File(BUNDLE_CACHE_PATH + File.separator + addonId.replace("marketplace:", "")); + private Path getAddonCacheDirectory(String addonId) { + return BUNDLE_CACHE_PATH.resolve(addonId.replace("marketplace:", "")); } } diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceHandlerException.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceHandlerException.java index 0758d630ce..b0eaa1e71a 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceHandlerException.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceHandlerException.java @@ -13,6 +13,7 @@ package org.openhab.core.addon.marketplace; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; /** * This is an exception that can be thrown by {@link MarketplaceAddonHandler}s if some operation fails. @@ -30,7 +31,7 @@ public class MarketplaceHandlerException extends Exception { * * @param message A message describing the issue */ - public MarketplaceHandlerException(String message) { - super(message); + public MarketplaceHandlerException(String message, @Nullable Throwable cause) { + super(message, cause); } } diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityBundleAddonHandler.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityBundleAddonHandler.java index e6facd6d00..cf6277de57 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityBundleAddonHandler.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityBundleAddonHandler.java @@ -12,12 +12,13 @@ */ package org.openhab.core.addon.marketplace.internal.community; +import static org.openhab.core.addon.marketplace.internal.community.CommunityMarketplaceAddonService.JAR_CONTENT_TYPE; + import java.net.MalformedURLException; import java.net.URL; -import java.util.Arrays; import java.util.List; -import java.util.Map; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.addon.Addon; import org.openhab.core.addon.marketplace.MarketplaceAddonHandler; import org.openhab.core.addon.marketplace.MarketplaceBundleInstaller; @@ -25,9 +26,6 @@ import org.openhab.core.addon.marketplace.MarketplaceHandlerException; import org.osgi.framework.BundleContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Deactivate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A {@link MarketplaceAddonHandler} implementation, which handles add-ons as jar files (specifically, OSGi @@ -40,34 +38,24 @@ import org.slf4j.LoggerFactory; * */ @Component(immediate = true) +@NonNullByDefault public class CommunityBundleAddonHandler extends MarketplaceBundleInstaller implements MarketplaceAddonHandler { - - // add-on types supported by this handler - private static final List SUPPORTED_EXT_TYPES = Arrays.asList("binding"); - - private static final String BUNDLE_CONTENTTYPE = "application/vnd.openhab.bundle"; - + private static final List SUPPORTED_EXT_TYPES = List.of("automation", "binding", "io", "persistence", + "transformation", "ui", "voice"); private static final String JAR_DOWNLOAD_URL_PROPERTY = "jar_download_url"; - private final Logger logger = LoggerFactory.getLogger(CommunityBundleAddonHandler.class); - - private BundleContext bundleContext; + private final BundleContext bundleContext; @Activate - protected void activate(BundleContext bundleContext, Map config) { + public CommunityBundleAddonHandler(BundleContext bundleContext) { this.bundleContext = bundleContext; ensureCachedBundlesAreInstalled(bundleContext); } - @Deactivate - protected void deactivate() { - this.bundleContext = null; - } - @Override public boolean supports(String type, String contentType) { // we support only certain extension types, and only as pure OSGi bundles - return SUPPORTED_EXT_TYPES.contains(type) && contentType.equals(BUNDLE_CONTENTTYPE); + return SUPPORTED_EXT_TYPES.contains(type) && contentType.equals(JAR_CONTENT_TYPE); } @Override @@ -77,13 +65,12 @@ public class CommunityBundleAddonHandler extends MarketplaceBundleInstaller impl @Override public void install(Addon addon) throws MarketplaceHandlerException { - URL sourceUrl; try { - sourceUrl = new URL((String) addon.getProperties().get(JAR_DOWNLOAD_URL_PROPERTY)); + URL sourceUrl = new URL((String) addon.getProperties().get(JAR_DOWNLOAD_URL_PROPERTY)); addBundleToCache(addon.getId(), sourceUrl); installFromCache(bundleContext, addon.getId()); } catch (MalformedURLException e) { - throw new MarketplaceHandlerException("Malformed source URL: " + e.getMessage()); + throw new MarketplaceHandlerException("Malformed source URL: " + e.getMessage(), e); } } diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java index 6b4fe6da8e..20a187f3ed 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java @@ -12,6 +12,8 @@ */ package org.openhab.core.addon.marketplace.internal.community; +import static org.openhab.core.addon.Addon.CODE_MATURITY_LEVELS; + import java.io.InputStreamReader; import java.io.Reader; import java.net.URI; @@ -25,6 +27,7 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -36,12 +39,12 @@ import org.openhab.core.addon.AddonService; import org.openhab.core.addon.AddonType; import org.openhab.core.addon.marketplace.MarketplaceAddonHandler; import org.openhab.core.addon.marketplace.MarketplaceHandlerException; -import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponse; -import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponse.DiscoursePosterInfo; -import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponse.DiscourseTopicItem; -import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponse.DiscourseUser; -import org.openhab.core.addon.marketplace.internal.community.model.DiscourseTopicResponse; -import org.openhab.core.addon.marketplace.internal.community.model.DiscourseTopicResponse.DiscoursePostLink; +import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO; +import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO.DiscoursePosterInfo; +import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO.DiscourseTopicItem; +import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO.DiscourseUser; +import org.openhab.core.addon.marketplace.internal.community.model.DiscourseTopicResponseDTO; +import org.openhab.core.addon.marketplace.internal.community.model.DiscourseTopicResponseDTO.DiscoursePostLink; import org.openhab.core.config.core.ConfigurableService; import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; @@ -68,14 +71,16 @@ import com.google.gson.GsonBuilder; property = Constants.SERVICE_PID + "=org.openhab.marketplace") @ConfigurableService(category = "system", label = "Community Marketplace", description_uri = CommunityMarketplaceAddonService.CONFIG_URI) public class CommunityMarketplaceAddonService implements AddonService { + public static final String JAR_CONTENT_TYPE = "application/vnd.openhab.bundle"; + public static final String KAR_CONTENT_TYPE = "application/vnd.openhab.feature;type=karfile"; + public static final String RULETEMPLATES_CONTENT_TYPE = "application/vnd.openhab.ruletemplate"; + public static final String UIWIDGETS_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=widget"; // constants for the configuration properties static final String CONFIG_URI = "system:marketplace"; static final String CONFIG_API_KEY = "apiKey"; static final String CONFIG_SHOW_UNPUBLISHED_ENTRIES_KEY = "showUnpublished"; - private final Logger logger = LoggerFactory.getLogger(CommunityMarketplaceAddonService.class); - private static final String COMMUNITY_BASE_URL = "https://community.openhab.org"; private static final String COMMUNITY_MARKETPLACE_URL = COMMUNITY_BASE_URL + "/c/marketplace/69/l/latest"; private static final String COMMUNITY_TOPIC_URL = COMMUNITY_BASE_URL + "/t/"; @@ -86,33 +91,31 @@ public class CommunityMarketplaceAddonService implements AddonService { private static final String YAML_CODE_MARKUP_START = "
";
     private static final String CODE_MARKUP_END = "
"; - private HashMap types = new HashMap(3); - private static final Integer BINDINGS_CATEGORY = 73; + private static final Integer BUNDLES_CATEGORY = 73; private static final Integer RULETEMPLATES_CATEGORY = 74; private static final Integer UIWIDGETS_CATEGORY = 75; private static final String PUBLISHED_TAG = "published"; - private HashMap contentTypes = new HashMap(3); - private static final String BINDINGS_CONTENT_TYPE = "application/vnd.openhab.bundle"; - private static final String RULETEMPLATES_CONTENT_TYPE = "application/vnd.openhab.ruletemplate"; - private static final String UIWIDGETS_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=widget"; + private static final Map TAG_ADDON_TYPE_MAP = Map.of( // + "automation", new AddonType("automation", "Automation"), // + "binding", new AddonType("binding", "Bindings"), // + "io", new AddonType("io", "I/O Services"), // + "persistence", new AddonType("persistence", "Persistence Services"), // + "transformation", new AddonType("transformation", "Transformations"), // + "ui", new AddonType("ui", "User Interfaces"), // + "voice", new AddonType("voice", "Voices")); + private final Logger logger = LoggerFactory.getLogger(CommunityMarketplaceAddonService.class); private final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").create(); - private final Set addonHandlers = new HashSet<>(); + private EventPublisher eventPublisher; private String apiKey = null; private boolean showUnpublished = false; @Activate protected void activate(Map config) { - types.put(BINDINGS_CATEGORY, new AddonType("binding", "Bindings")); - types.put(RULETEMPLATES_CATEGORY, new AddonType("automation", "Automation")); - types.put(UIWIDGETS_CATEGORY, new AddonType("ui", "User Interfaces")); - contentTypes.put("binding", BINDINGS_CONTENT_TYPE); - contentTypes.put("automation", RULETEMPLATES_CONTENT_TYPE); - contentTypes.put("ui", UIWIDGETS_CONTENT_TYPE); modified(config); } @@ -161,7 +164,7 @@ public class CommunityMarketplaceAddonService implements AddonService { @Override public List getAddons(Locale locale) { try { - List pages = new ArrayList(); + List pages = new ArrayList<>(); URL url = new URL(COMMUNITY_MARKETPLACE_URL); int pageNb = 1; @@ -173,7 +176,7 @@ public class CommunityMarketplaceAddonService implements AddonService { } try (Reader reader = new InputStreamReader(connection.getInputStream())) { - DiscourseCategoryResponse parsed = gson.fromJson(reader, DiscourseCategoryResponse.class); + DiscourseCategoryResponseDTO parsed = gson.fromJson(reader, DiscourseCategoryResponseDTO.class); pages.add(parsed); if (parsed.topic_list.more_topics_url != null) { @@ -191,7 +194,7 @@ public class CommunityMarketplaceAddonService implements AddonService { .map(t -> convertTopicItemToAddon(t, users)).collect(Collectors.toList()); } catch (Exception e) { logger.error("Unable to retrieve marketplace add-ons", e); - return new ArrayList(); + return List.of(); } } @@ -207,7 +210,7 @@ public class CommunityMarketplaceAddonService implements AddonService { } try (Reader reader = new InputStreamReader(connection.getInputStream())) { - DiscourseTopicResponse parsed = gson.fromJson(reader, DiscourseTopicResponse.class); + DiscourseTopicResponseDTO parsed = gson.fromJson(reader, DiscourseTopicResponseDTO.class); return convertTopicToAddon(parsed); } } catch (Exception e) { @@ -217,7 +220,7 @@ public class CommunityMarketplaceAddonService implements AddonService { @Override public List getTypes(Locale locale) { - return new ArrayList(types.values()); + return new ArrayList<>(TAG_ADDON_TYPE_MAP.values()); } @Override @@ -270,6 +273,45 @@ public class CommunityMarketplaceAddonService implements AddonService { return ""; } + private @Nullable AddonType getAddonType(@Nullable Integer category, String[] tags) { + // check if we can determine the addon type from the category + if (RULETEMPLATES_CATEGORY.equals(category)) { + return TAG_ADDON_TYPE_MAP.get("automation"); + } else if (UIWIDGETS_CATEGORY.equals(category)) { + return TAG_ADDON_TYPE_MAP.get("ui"); + } else if (BUNDLES_CATEGORY.equals(category)) { + // try to get it from tags if we have tags + for (String tag : tags) { + AddonType addonType = TAG_ADDON_TYPE_MAP.get(tag); + if (addonType != null) { + return addonType; + } + } + } + + // or return null + return null; + } + + private String getContentType(@Nullable Integer category, String[] tags) { + // check if we can determine the addon type from the category + if (RULETEMPLATES_CATEGORY.equals(category)) { + return RULETEMPLATES_CONTENT_TYPE; + } else if (UIWIDGETS_CATEGORY.equals(category)) { + return UIWIDGETS_CONTENT_TYPE; + } else if (BUNDLES_CATEGORY.equals(category)) { + if (Arrays.asList(tags).contains("kar")) { + return KAR_CONTENT_TYPE; + } else { + // default to plain jar bundle for addons + return JAR_CONTENT_TYPE; + } + } + + // empty string if content type could not be defined + return ""; + } + /** * Transforms a {@link DiscourseTopicItem} to a {@link Addon} * @@ -278,16 +320,18 @@ public class CommunityMarketplaceAddonService implements AddonService { */ private Addon convertTopicItemToAddon(DiscourseTopicItem topic, List users) { String id = ADDON_ID_PREFIX + topic.id.toString(); - AddonType addonType = types.get(topic.category_id); + String[] tags = Objects.requireNonNullElse(topic.tags, new String[0]); + + AddonType addonType = getAddonType(topic.category_id, tags); String type = (addonType != null) ? addonType.getId() : ""; - String contentType = (contentTypes.get(type) != null) ? contentTypes.get(type) : ""; + String contentType = getContentType(topic.category_id, tags); + String version = ""; String title = topic.title; String link = COMMUNITY_TOPIC_URL + topic.id.toString(); int likeCount = topic.like_count; int views = topic.views; int postsCount = topic.posts_count; - String[] tags = topic.tags; Date createdDate = topic.created_at; String author = ""; boolean verifiedAuthor = false; @@ -297,6 +341,14 @@ public class CommunityMarketplaceAddonService implements AddonService { } } + String maturity = null; + for (String tag : tags) { + if (CODE_MATURITY_LEVELS.contains(tag)) { + maturity = tag; + break; + } + } + HashMap properties = new HashMap<>(10); properties.put("created_at", createdDate); properties.put("like_count", likeCount); @@ -323,9 +375,9 @@ public class CommunityMarketplaceAddonService implements AddonService { String connection = ""; String backgroundColor = ""; String imageLink = topic.image_url; - Addon addon = new Addon(id, type, title, version, contentType, link, author, verifiedAuthor, installed, - description, detailedDescription, configDescriptionURI, keywords, countries, connection, - backgroundColor, imageLink, properties); + Addon addon = new Addon(id, type, title, version, maturity, contentType, link, author, verifiedAuthor, + installed, description, detailedDescription, configDescriptionURI, keywords, countries, null, + connection, backgroundColor, imageLink, properties); return addon; } @@ -341,29 +393,39 @@ public class CommunityMarketplaceAddonService implements AddonService { } /** - * Transforms a {@link DiscourseTopicResponse} to a {@link Addon} + * Transforms a {@link DiscourseTopicResponseDTO} to a {@link Addon} * * @param topic the topic * @return the list item */ - private Addon convertTopicToAddon(DiscourseTopicResponse topic) { + private Addon convertTopicToAddon(DiscourseTopicResponseDTO topic) { String id = ADDON_ID_PREFIX + topic.id.toString(); - AddonType addonType = types.get(topic.category_id); + String[] tags = Objects.requireNonNullElse(topic.tags, new String[0]); + + AddonType addonType = getAddonType(topic.category_id, tags); String type = (addonType != null) ? addonType.getId() : ""; - String contentType = contentTypes.get(type); + String contentType = getContentType(topic.category_id, tags); + String version = ""; String title = topic.title; String link = COMMUNITY_TOPIC_URL + topic.id.toString(); int likeCount = topic.like_count; int views = topic.views; int postsCount = topic.posts_count; - String[] tags = topic.tags; Date createdDate = topic.post_stream.posts[0].created_at; Date updatedDate = topic.post_stream.posts[0].updated_at; Date lastPostedDate = topic.last_posted; String author = topic.post_stream.posts[0].display_username; boolean verifiedAuthor = false; + String maturity = null; + for (String tag : tags) { + if (CODE_MATURITY_LEVELS.contains(tag)) { + maturity = tag; + break; + } + } + HashMap properties = new HashMap<>(10); properties.put("created_at", createdDate); properties.put("updated_at", updatedDate); @@ -382,6 +444,9 @@ public class CommunityMarketplaceAddonService implements AddonService { if (postLink.url.endsWith(".jar")) { properties.put("jar_download_url", postLink.url); } + if (postLink.url.endsWith(".kar")) { + properties.put("kar_download_url", postLink.url); + } if (postLink.url.endsWith(".json")) { properties.put("json_download_url", postLink.url); } @@ -406,7 +471,7 @@ public class CommunityMarketplaceAddonService implements AddonService { // try to use an handler to determine if the add-on is installed boolean installed = false; for (MarketplaceAddonHandler handler : addonHandlers) { - if (handler.supports(type, (contentType != null) ? contentType : "")) { + if (handler.supports(type, contentType)) { if (handler.isInstalled(id)) { installed = true; } @@ -418,9 +483,9 @@ public class CommunityMarketplaceAddonService implements AddonService { String countries = ""; String connection = ""; String backgroundColor = ""; - Addon addon = new Addon(id, type, title, version, contentType, link, author, verifiedAuthor, installed, - description, detailedDescription, configDescriptionURI, keywords, countries, connection, - backgroundColor, null, properties); + Addon addon = new Addon(id, type, title, version, maturity, contentType, link, author, verifiedAuthor, + installed, description, detailedDescription, configDescriptionURI, keywords, countries, null, + connection, backgroundColor, null, properties); return addon; } diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityRuleTemplateAddonHandler.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityRuleTemplateAddonHandler.java index 51106e9ae4..3c5171f8fe 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityRuleTemplateAddonHandler.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityRuleTemplateAddonHandler.java @@ -17,13 +17,14 @@ import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.addon.Addon; import org.openhab.core.addon.marketplace.MarketplaceAddonHandler; import org.openhab.core.addon.marketplace.MarketplaceHandlerException; import org.openhab.core.addon.marketplace.internal.automation.MarketplaceRuleTemplateProvider; import org.openhab.core.automation.template.RuleTemplateProvider; import org.openhab.core.storage.Storage; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; @@ -38,7 +39,8 @@ import org.slf4j.LoggerFactory; * @author Yannick Schaus - refactoring * */ -@Component +@Component(immediate = true) +@NonNullByDefault public class CommunityRuleTemplateAddonHandler implements MarketplaceAddonHandler { private static final String JSON_DOWNLOAD_URL_PROPERTY = "json_download_url"; private static final String YAML_DOWNLOAD_URL_PROPERTY = "yaml_download_url"; @@ -48,18 +50,14 @@ public class CommunityRuleTemplateAddonHandler implements MarketplaceAddonHandle private final Logger logger = LoggerFactory.getLogger(CommunityRuleTemplateAddonHandler.class); - private MarketplaceRuleTemplateProvider marketplaceRuleTemplateProvider; + private final MarketplaceRuleTemplateProvider marketplaceRuleTemplateProvider; - @Reference - protected void setMarketplaceRuleTemplateProvider(MarketplaceRuleTemplateProvider marketplaceRuleTemplateProvider) { + @Activate + public CommunityRuleTemplateAddonHandler( + @Reference MarketplaceRuleTemplateProvider marketplaceRuleTemplateProvider) { this.marketplaceRuleTemplateProvider = marketplaceRuleTemplateProvider; } - protected void unsetMarketplaceRuleTemplateProvider( - MarketplaceRuleTemplateProvider marketplaceRuleTemplateProvider) { - this.marketplaceRuleTemplateProvider = null; - } - @Override public boolean supports(String type, String contentType) { return "automation".equals(type) && RULETEMPLATES_CONTENT_TYPE.equals(contentType); @@ -81,18 +79,18 @@ public class CommunityRuleTemplateAddonHandler implements MarketplaceAddonHandle template = getTemplateFromURL((String) addon.getProperties().get(YAML_DOWNLOAD_URL_PROPERTY)); marketplaceRuleTemplateProvider.addTemplateAsYAML(addon.getId(), template); } else if (addon.getProperties().containsKey(JSON_CONTENT_PROPERTY)) { - template = (@NonNull String) addon.getProperties().get(JSON_CONTENT_PROPERTY); + template = (String) addon.getProperties().get(JSON_CONTENT_PROPERTY); marketplaceRuleTemplateProvider.addTemplateAsJSON(addon.getId(), template); } else if (addon.getProperties().containsKey(YAML_CONTENT_PROPERTY)) { - template = (@NonNull String) addon.getProperties().get(YAML_CONTENT_PROPERTY); + template = (String) addon.getProperties().get(YAML_CONTENT_PROPERTY); marketplaceRuleTemplateProvider.addTemplateAsYAML(addon.getId(), template); } } catch (IOException e) { logger.error("Rule template from marketplace cannot be downloaded: {}", e.getMessage()); - throw new MarketplaceHandlerException("Template cannot be downloaded."); + throw new MarketplaceHandlerException("Template cannot be downloaded.", e); } catch (Exception e) { logger.error("Rule template from marketplace is invalid: {}", e.getMessage()); - throw new MarketplaceHandlerException("Template is not valid."); + throw new MarketplaceHandlerException("Template is not valid.", e); } } diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityUIWidgetAddonHandler.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityUIWidgetAddonHandler.java index 9550db38e0..08ed879a5b 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityUIWidgetAddonHandler.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityUIWidgetAddonHandler.java @@ -18,7 +18,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.addon.Addon; import org.openhab.core.addon.marketplace.MarketplaceAddonHandler; import org.openhab.core.addon.marketplace.MarketplaceHandlerException; @@ -41,7 +41,8 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; * @author Yannick Schaus - Initial contribution and API * */ -@Component +@Component(immediate = true) +@NonNullByDefault public class CommunityUIWidgetAddonHandler implements MarketplaceAddonHandler { private static final String YAML_DOWNLOAD_URL_PROPERTY = "yaml_download_url"; private static final String YAML_CONTENT_PROPERTY = "yaml_content"; @@ -77,17 +78,17 @@ public class CommunityUIWidgetAddonHandler implements MarketplaceAddonHandler { if (addon.getProperties().containsKey(YAML_DOWNLOAD_URL_PROPERTY)) { widget = getWidgetFromURL((String) addon.getProperties().get(YAML_DOWNLOAD_URL_PROPERTY)); } else if (addon.getProperties().containsKey(YAML_CONTENT_PROPERTY)) { - widget = (@NonNull String) addon.getProperties().get(YAML_CONTENT_PROPERTY); + widget = (String) addon.getProperties().get(YAML_CONTENT_PROPERTY); } else { throw new IllegalArgumentException("Couldn't find the widget in the add-on entry"); } addWidgetAsYAML(addon.getId(), widget); } catch (IOException e) { logger.error("Widget from marketplace cannot be downloaded: {}", e.getMessage()); - throw new MarketplaceHandlerException("Widget cannot be downloaded."); + throw new MarketplaceHandlerException("Widget cannot be downloaded.", e); } catch (Exception e) { logger.error("Widget from marketplace is invalid: {}", e.getMessage()); - throw new MarketplaceHandlerException("Widget is not valid."); + throw new MarketplaceHandlerException("Widget is not valid.", e); } } diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseCategoryResponse.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseCategoryResponseDTO.java similarity index 97% rename from bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseCategoryResponse.java rename to bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseCategoryResponseDTO.java index 222b0ae882..df7fd91faf 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseCategoryResponse.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseCategoryResponseDTO.java @@ -20,7 +20,7 @@ import java.util.Date; * @author Yannick Schaus - Initial contribution * */ -public class DiscourseCategoryResponse { +public class DiscourseCategoryResponseDTO { public DiscourseUser[] users; public DiscourseTopicList topic_list; diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseTopicResponse.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseTopicResponseDTO.java similarity index 97% rename from bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseTopicResponse.java rename to bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseTopicResponseDTO.java index e3aad2a109..6062c842ba 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseTopicResponse.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/model/DiscourseTopicResponseDTO.java @@ -20,7 +20,7 @@ import java.util.Date; * @author Yannick Schaus - Initial contribution * */ -public class DiscourseTopicResponse { +public class DiscourseTopicResponseDTO { public Integer id; public DiscoursePostStream post_stream; diff --git a/bundles/org.openhab.core.addon.sample/src/main/java/org/openhab/core/addon/sample/internal/SampleAddonService.java b/bundles/org.openhab.core.addon.sample/src/main/java/org/openhab/core/addon/sample/internal/SampleAddonService.java index 91d59a8b0d..20f27043d2 100644 --- a/bundles/org.openhab.core.addon.sample/src/main/java/org/openhab/core/addon/sample/internal/SampleAddonService.java +++ b/bundles/org.openhab.core.addon.sample/src/main/java/org/openhab/core/addon/sample/internal/SampleAddonService.java @@ -80,8 +80,9 @@ public class SampleAddonService implements AddonService { String description = createDescription(); String imageLink = null; String backgroundColor = createRandomColor(); - Addon extension = new Addon(id, typeId, label, version, "example/vnd.openhab.addon", link, "John Doe", - false, installed, description, backgroundColor, imageLink, null, null, null, null, null, null); + Addon extension = new Addon(id, typeId, label, version, "stable", "example/vnd.openhab.addon", link, + "John Doe", false, installed, description, backgroundColor, imageLink, null, null, null, null, + null, null, null); extensions.put(extension.getId(), extension); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/addon/Addon.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/addon/Addon.java index a5582cd1a0..c118154d49 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/addon/Addon.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/addon/Addon.java @@ -13,6 +13,7 @@ package org.openhab.core.addon; import java.util.Map; +import java.util.Set; /** * This class defines an add-on. @@ -21,10 +22,12 @@ import java.util.Map; * @author Yannick Schaus - Add fields */ public class Addon { + public static final Set CODE_MATURITY_LEVELS = Set.of("alpha", "beta", "mature", "stable"); private final String id; private final String label; private final String version; + private final String maturity; private final String contentType; private final String link; private final String author; @@ -36,6 +39,7 @@ public class Addon { private final String configDescriptionURI; private final String keywords; private final String countries; + private final String license; private final String connection; private final String backgroundColor; private final String imageLink; @@ -56,8 +60,8 @@ public class Addon { */ public Addon(String id, String type, String label, String version, String contentType, String link, String author, boolean verifiedAuthor, boolean installed) { - this(id, type, label, version, contentType, link, author, verifiedAuthor, installed, null, null, null, null, - null, null, null, null, null); + this(id, type, label, version, null, contentType, link, author, verifiedAuthor, installed, null, null, null, + null, null, null, null, null, null, null); } /** @@ -67,28 +71,39 @@ public class Addon { * @param type the type id of the add-on * @param label the label of the add-on * @param version the version of the add-on + * @param maturity the maturity level of this version * @param contentType the content type of the add-on - * @param description the detailed description of the add-on (may be null) - * @param backgroundColor for displaying the add-on (may be null) - * @param link the link to find more information about the add-on (may be null) + * @param link the link to find more information about the add-on (may be null)* * @param author the author of the add-on * @param verifiedAuthor true, if the author is verified - * @param imageLink the link to an image (png/svg) (may be null) * @param installed true, if the add-on is installed, false otherwise + * @param description the description of the add-on (may be null) + * @param detailedDescription the detailed description of the add-on (may be null) + * @param configDescriptionURI the URI to the configuration description for this add-on + * @param keywords the keywords for this add-on + * @param countries a comma-separated list of ISO 3166 codes relevant to this add-on + * @param license the SPDX license identifier + * @param connection a string describing the type of connection (local or cloud, push or pull...) this add-on uses, + * if applicable. + * @param backgroundColor for displaying the add-on (may be null) + * @param imageLink the link to an image (png/svg) (may be null) + * @param properties a {@link Map} containing addition information */ - public Addon(String id, String type, String label, String version, String contentType, String link, String author, - boolean verifiedAuthor, boolean installed, String description, String detailedDescription, - String configDescriptionURI, String keywords, String countries, String connection, String backgroundColor, - String imageLink, Map properties) { + public Addon(String id, String type, String label, String version, String maturity, String contentType, String link, + String author, boolean verifiedAuthor, boolean installed, String description, String detailedDescription, + String configDescriptionURI, String keywords, String countries, String license, String connection, + String backgroundColor, String imageLink, Map properties) { this.id = id; this.label = label; this.version = version; + this.maturity = maturity; this.contentType = contentType; this.description = description; this.detailedDescription = detailedDescription; this.configDescriptionURI = configDescriptionURI; this.keywords = keywords; this.countries = countries; + this.license = license; this.connection = connection; this.backgroundColor = backgroundColor; this.link = link; @@ -149,6 +164,13 @@ public class Addon { return version; } + /** + * The maturity level of this version + */ + public String getMaturity() { + return maturity; + } + /** * The content type of the add-on */ @@ -191,6 +213,13 @@ public class Addon { return countries; } + /** + * The SPDX License identifier for this addon + */ + public String getLicense() { + return license; + } + /** * A string describing the type of connection (local or cloud, push or pull...) this add-on uses, if applicable. */ diff --git a/bundles/pom.xml b/bundles/pom.xml index ce4d656d39..101aeca02a 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -18,6 +18,7 @@ org.openhab.core.addon.marketplace + org.openhab.core.addon.marketplace.karaf org.openhab.core.auth.jaas org.openhab.core.auth.oauth2client org.openhab.core.automation diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 15499d28e5..c6af467b5b 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -65,8 +65,10 @@ + kar openhab-core-base mvn:org.openhab.core.bundles/org.openhab.core.addon.marketplace/${project.version} + mvn:org.openhab.core.bundles/org.openhab.core.addon.marketplace.karaf/${project.version} openhab.tp;filter:="(feature=jackson)" openhab.tp-jackson openhab-core-ui