[addonservices] allow uninstalling of removed addons and fix other issues (#2607)

* [addonservices] allow uninstalling of removed addons

Signed-off-by: Jan N. Klug <github@klug.nrw>
pull/2737/head
J-N-K 2022-02-06 09:48:55 +01:00 committed by GitHub
parent 3e94dd6e30
commit c4e1b14d00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 835 additions and 339 deletions

View File

@ -38,6 +38,12 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,230 @@
/**
* Copyright (c) 2010-2022 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.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Dictionary;
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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.Addon;
import org.openhab.core.addon.AddonEventFactory;
import org.openhab.core.addon.AddonService;
import org.openhab.core.addon.AddonType;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.config.core.ConfigParser;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link AbstractRemoteAddonService} implements basic functionality of a remote add-on-service
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractRemoteAddonService implements AddonService {
protected static final Map<String, AddonType> TAG_ADDON_TYPE_MAP = Map.of( //
"automation", new AddonType("automation", "Automation"), //
"binding", new AddonType("binding", "Bindings"), //
"misc", new AddonType("misc", "Misc"), //
"persistence", new AddonType("persistence", "Persistence"), //
"transformation", new AddonType("transformation", "Transformations"), //
"ui", new AddonType("ui", "User Interfaces"), //
"voice", new AddonType("voice", "Voice"));
protected final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").create();
protected final Set<MarketplaceAddonHandler> addonHandlers = new HashSet<>();
protected final Storage<String> installedAddonStorage;
protected final EventPublisher eventPublisher;
protected final ConfigurationAdmin configurationAdmin;
protected final ExpiringCache<List<Addon>> cachedRemoteAddons = new ExpiringCache<>(Duration.ofMinutes(15),
this::getRemoteAddons);
protected List<Addon> cachedAddons = List.of();
protected List<String> installedAddons = List.of();
public AbstractRemoteAddonService(EventPublisher eventPublisher, ConfigurationAdmin configurationAdmin,
StorageService storageService, String servicePid) {
this.eventPublisher = eventPublisher;
this.configurationAdmin = configurationAdmin;
this.installedAddonStorage = storageService.getStorage(servicePid);
}
@Override
public void refreshSource() {
List<Addon> addons = new ArrayList<>();
installedAddonStorage.stream().map(e -> Objects.requireNonNull(gson.fromJson(e.getValue(), Addon.class)))
.forEach(addons::add);
// create lookup list to make sure installed addons take precedence
List<String> installedAddons = addons.stream().map(Addon::getId).collect(Collectors.toList());
if (remoteEnabled()) {
List<Addon> remoteAddons = Objects.requireNonNullElse(cachedRemoteAddons.getValue(), List.of());
remoteAddons.stream().filter(a -> !installedAddons.contains(a.getId())).forEach(addons::add);
}
// check real installation status based on handlers
addons.forEach(addon -> addon.setInstalled(addonHandlers.stream().anyMatch(h -> h.isInstalled(addon.getId()))));
cachedAddons = addons;
this.installedAddons = installedAddons;
}
/**
* Add an {@link MarketplaceAddonHandler) to this service
*
* This needs to be implemented by the addon-services because the handlers are references to OSGi services and
* the @Reference annotation is not inherited.
* It is added here to make sure that implementations comply with that.
*
* @param handler the handler that shall be added
*/
protected abstract void addAddonHandler(MarketplaceAddonHandler handler);
/**
* Remove an {@link MarketplaceAddonHandler) from this service
*
* This needs to be implemented by the addon-services because the handlers are references to OSGi services and
* unbind methods can't be inherited.
* It is added here to make sure that implementations comply with that.
*
* @param handler the handler that shall be removed
*/
protected abstract void removeAddonHandler(MarketplaceAddonHandler handler);
/**
* get all addons from remote
*
* @return a list of {@link Addon} that are available on the remote side
*/
protected abstract List<Addon> getRemoteAddons();
@Override
public List<Addon> getAddons(@Nullable Locale locale) {
refreshSource();
return cachedAddons;
}
@Override
public abstract @Nullable Addon getAddon(String id, @Nullable Locale locale);
@Override
public List<AddonType> getTypes(@Nullable Locale locale) {
return new ArrayList<>(TAG_ADDON_TYPE_MAP.values());
}
@Override
public void install(String id) {
Addon addon = getAddon(id, null);
if (addon != null) {
for (MarketplaceAddonHandler handler : addonHandlers) {
if (handler.supports(addon.getType(), addon.getContentType())) {
if (!handler.isInstalled(addon.getId())) {
try {
handler.install(addon);
installedAddonStorage.put(id, gson.toJson(addon));
refreshSource();
postInstalledEvent(addon.getId());
} catch (MarketplaceHandlerException e) {
postFailureEvent(addon.getId(), e.getMessage());
}
} else {
postFailureEvent(addon.getId(), "Add-on is already installed.");
}
return;
}
}
}
postFailureEvent(id, "Add-on not known.");
}
@Override
public void uninstall(String id) {
Addon addon = getAddon(id, null);
if (addon != null) {
for (MarketplaceAddonHandler handler : addonHandlers) {
if (handler.supports(addon.getType(), addon.getContentType())) {
if (handler.isInstalled(addon.getId())) {
try {
handler.uninstall(addon);
installedAddonStorage.remove(id);
refreshSource();
postUninstalledEvent(addon.getId());
} catch (MarketplaceHandlerException e) {
postFailureEvent(addon.getId(), e.getMessage());
}
} else {
installedAddonStorage.remove(id);
postFailureEvent(addon.getId(), "Add-on is not installed.");
}
return;
}
}
}
postFailureEvent(id, "Add-on not known.");
}
@Override
public abstract @Nullable String getAddonId(URI addonURI);
/**
* check if remote services are enabled
*
* @return true if network access is allowed
*/
protected boolean remoteEnabled() {
try {
Configuration configuration = configurationAdmin.getConfiguration("org.openhab.addons", null);
Dictionary<String, Object> properties = configuration.getProperties();
if (properties == null) {
// if we can't determine a set property, we use true (default is remote enabled)
return true;
}
return ConfigParser.valueAsOrElse(properties.get("remote"), Boolean.class, true);
} catch (IOException e) {
return true;
}
}
private void postInstalledEvent(String extensionId) {
Event event = AddonEventFactory.createAddonInstalledEvent(extensionId);
eventPublisher.post(event);
}
private void postUninstalledEvent(String extensionId) {
Event event = AddonEventFactory.createAddonUninstalledEvent(extensionId);
eventPublisher.post(event);
}
private void postFailureEvent(String extensionId, @Nullable String msg) {
Event event = AddonEventFactory.createAddonFailureEvent(extensionId, msg);
eventPublisher.post(event);
}
}

View File

@ -14,7 +14,6 @@ package org.openhab.core.addon.marketplace.internal.community;
import static org.openhab.core.addon.Addon.CODE_MATURITY_LEVELS;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
@ -23,37 +22,31 @@ import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Dictionary;
import java.util.HashMap;
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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.Addon;
import org.openhab.core.addon.AddonEventFactory;
import org.openhab.core.addon.AddonService;
import org.openhab.core.addon.AddonType;
import org.openhab.core.addon.marketplace.AbstractRemoteAddonService;
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
import org.openhab.core.addon.marketplace.MarketplaceHandlerException;
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.ConfigParser;
import org.openhab.core.config.core.ConfigurableService;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.storage.StorageService;
import org.osgi.framework.Constants;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@ -64,19 +57,17 @@ import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* This class is a {@link AddonService} retrieving posts on community.openhab.org (Discourse).
* This class is an {@link org.openhab.core.addon.AddonService} retrieving posts on community.openhab.org (Discourse).
*
* @author Yannick Schaus - Initial contribution
*/
@Component(immediate = true, configurationPid = "org.openhab.marketplace", //
property = Constants.SERVICE_PID + "=org.openhab.marketplace")
@ConfigurableService(category = "system", label = "Community Marketplace", description_uri = CommunityMarketplaceAddonService.CONFIG_URI)
@Component(immediate = true, configurationPid = CommunityMarketplaceAddonService.SERVICE_PID, //
property = Constants.SERVICE_PID + "="
+ CommunityMarketplaceAddonService.SERVICE_PID, service = AddonService.class)
@ConfigurableService(category = "system", label = CommunityMarketplaceAddonService.SERVICE_NAME, description_uri = CommunityMarketplaceAddonService.CONFIG_URI)
@NonNullByDefault
public class CommunityMarketplaceAddonService implements AddonService {
public class CommunityMarketplaceAddonService extends AbstractRemoteAddonService {
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";
@ -84,16 +75,18 @@ public class CommunityMarketplaceAddonService implements AddonService {
public static final String BLOCKLIBRARIES_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=blocks";
// constants for the configuration properties
static final String SERVICE_NAME = "Community Marketplace";
static final String SERVICE_PID = "org.openhab.marketplace";
static final String CONFIG_URI = "system:marketplace";
static final String CONFIG_API_KEY = "apiKey";
static final String CONFIG_SHOW_UNPUBLISHED_ENTRIES_KEY = "showUnpublished";
private static final String CFG_REMOTE = "remote";
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/";
private static final String ADDON_ID_PREFIX = "marketplace:";
private static final String SERVICE_ID = "marketplace";
private static final String ADDON_ID_PREFIX = SERVICE_ID + ":";
private static final String JSON_CODE_MARKUP_START = "<pre><code class=\"lang-json\">";
private static final String YAML_CODE_MARKUP_START = "<pre><code class=\"lang-yaml\">";
@ -106,76 +99,55 @@ public class CommunityMarketplaceAddonService implements AddonService {
private static final String PUBLISHED_TAG = "published";
private static final Map<String, AddonType> TAG_ADDON_TYPE_MAP = Map.of( //
"automation", new AddonType("automation", "Automation"), //
"binding", new AddonType("binding", "Bindings"), //
"misc", new AddonType("misc", "Misc"), //
"persistence", new AddonType("persistence", "Persistence"), //
"transformation", new AddonType("transformation", "Transformations"), //
"ui", new AddonType("ui", "User Interfaces"), //
"voice", new AddonType("voice", "Voice"));
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<MarketplaceAddonHandler> addonHandlers = new HashSet<>();
private final EventPublisher eventPublisher;
private final ConfigurationAdmin configurationAdmin;
private @Nullable String apiKey = null;
private boolean showUnpublished = false;
@Activate
public CommunityMarketplaceAddonService(final @Reference EventPublisher eventPublisher,
@Reference ConfigurationAdmin configurationAdmin) {
this.eventPublisher = eventPublisher;
this.configurationAdmin = configurationAdmin;
}
@Activate
protected void activate(Map<String, Object> config) {
@Reference ConfigurationAdmin configurationAdmin, @Reference StorageService storageService,
Map<String, Object> config) {
super(eventPublisher, configurationAdmin, storageService, SERVICE_PID);
modified(config);
}
@Modified
void modified(@Nullable Map<String, Object> config) {
public void modified(@Nullable Map<String, Object> config) {
if (config != null) {
this.apiKey = (String) config.get(CONFIG_API_KEY);
Object showUnpublishedConfigValue = config.get(CONFIG_SHOW_UNPUBLISHED_ENTRIES_KEY);
this.showUnpublished = showUnpublishedConfigValue != null
&& "true".equals(showUnpublishedConfigValue.toString());
cachedRemoteAddons.invalidateValue();
refreshSource();
}
}
@Override
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
protected void addAddonHandler(MarketplaceAddonHandler handler) {
this.addonHandlers.add(handler);
}
@Override
protected void removeAddonHandler(MarketplaceAddonHandler handler) {
this.addonHandlers.remove(handler);
}
@Override
public String getId() {
return "marketplace";
return SERVICE_ID;
}
@Override
public String getName() {
return "Community Marketplace";
return SERVICE_NAME;
}
@Override
public void refreshSource() {
}
@Override
public List<Addon> getAddons(@Nullable Locale locale) {
if (!remoteEnabled()) {
return List.of();
}
protected List<Addon> getRemoteAddons() {
List<Addon> addons = new ArrayList<>();
try {
List<DiscourseCategoryResponseDTO> pages = new ArrayList<>();
@ -190,11 +162,11 @@ public class CommunityMarketplaceAddonService implements AddonService {
try (Reader reader = new InputStreamReader(connection.getInputStream())) {
DiscourseCategoryResponseDTO parsed = gson.fromJson(reader, DiscourseCategoryResponseDTO.class);
if (parsed.topic_list.topics.length != 0) {
if (parsed.topicList.topics.length != 0) {
pages.add(parsed);
}
if (parsed.topic_list.more_topics_url != null) {
if (parsed.topicList.moreTopicsUrl != null) {
// Discourse URL for next page is wrong
url = new URL(COMMUNITY_MARKETPLACE_URL + "?page=" + pageNb++);
} else {
@ -204,24 +176,31 @@ public class CommunityMarketplaceAddonService implements AddonService {
}
List<DiscourseUser> users = pages.stream().flatMap(p -> Stream.of(p.users)).collect(Collectors.toList());
return pages.stream().flatMap(p -> Stream.of(p.topic_list.topics))
pages.stream().flatMap(p -> Stream.of(p.topicList.topics))
.filter(t -> showUnpublished || Arrays.asList(t.tags).contains(PUBLISHED_TAG))
.map(t -> convertTopicItemToAddon(t, users)).collect(Collectors.toList());
.map(t -> convertTopicItemToAddon(t, users)).forEach(addons::add);
} catch (Exception e) {
logger.error("Unable to retrieve marketplace add-ons", e);
return List.of();
}
return addons;
}
@Override
public @Nullable Addon getAddon(String id, @Nullable Locale locale) {
String fullId = ADDON_ID_PREFIX + id;
// check if it is an installed add-on (cachedAddons also contains possibly incomplete results from the remote
// side, we need to retrieve them from Discourse)
if (installedAddons.contains(id)) {
return cachedAddons.stream().filter(e -> fullId.equals(e.getId())).findAny().orElse(null);
}
if (!remoteEnabled()) {
return null;
}
URL url;
// retrieve from remote
try {
url = new URL(String.format("%s%s", COMMUNITY_TOPIC_URL, id.replace(ADDON_ID_PREFIX, "")));
URL url = new URL(String.format("%s%s", COMMUNITY_TOPIC_URL, id.replace(ADDON_ID_PREFIX, "")));
URLConnection connection = url.openConnection();
connection.addRequestProperty("Accept", "application/json");
if (this.apiKey != null) {
@ -237,57 +216,6 @@ public class CommunityMarketplaceAddonService implements AddonService {
}
}
@Override
public List<AddonType> getTypes(@Nullable Locale locale) {
return new ArrayList<>(TAG_ADDON_TYPE_MAP.values());
}
@Override
public void install(String id) {
Addon addon = getAddon(id, null);
if (addon != null) {
for (MarketplaceAddonHandler handler : addonHandlers) {
if (handler.supports(addon.getType(), addon.getContentType())) {
if (!handler.isInstalled(addon.getId())) {
try {
handler.install(addon);
postInstalledEvent(id);
} catch (MarketplaceHandlerException e) {
postFailureEvent(id, e.getMessage());
}
} else {
postFailureEvent(id, "Add-on is already installed.");
}
return;
}
}
}
postFailureEvent(id, "Add-on not known.");
}
@Override
public void uninstall(String id) {
Addon addon = getAddon(id, null);
if (addon != null) {
for (MarketplaceAddonHandler handler : addonHandlers) {
if (handler.supports(addon.getType(), addon.getContentType())) {
if (handler.isInstalled(addon.getId())) {
try {
handler.uninstall(addon);
postUninstalledEvent(id);
} catch (MarketplaceHandlerException e) {
postFailureEvent(id, e.getMessage());
}
} else {
postFailureEvent(id, "Add-on is not installed.");
}
return;
}
}
}
postFailureEvent(id, "Add-on not known.");
}
@Override
public @Nullable String getAddonId(URI addonURI) {
if (addonURI.toString().startsWith(COMMUNITY_TOPIC_URL)) {
@ -344,20 +272,20 @@ public class CommunityMarketplaceAddonService implements AddonService {
List<String> tags = Arrays.asList(Objects.requireNonNullElse(topic.tags, new String[0]));
String id = ADDON_ID_PREFIX + topic.id.toString();
AddonType addonType = getAddonType(topic.category_id, tags);
AddonType addonType = getAddonType(topic.categoryId, tags);
String type = (addonType != null) ? addonType.getId() : "";
String contentType = getContentType(topic.category_id, tags);
String contentType = getContentType(topic.categoryId, tags);
String title = topic.title;
String link = COMMUNITY_TOPIC_URL + topic.id.toString();
int likeCount = topic.like_count;
int likeCount = topic.likeCount;
int views = topic.views;
int postsCount = topic.posts_count;
Date createdDate = topic.created_at;
int postsCount = topic.postsCount;
Date createdDate = topic.createdAt;
String author = "";
for (DiscoursePosterInfo posterInfo : topic.posters) {
if (posterInfo.description.contains("Original Poster")) {
author = users.stream().filter(u -> u.id.equals(posterInfo.user_id)).findFirst().get().name;
author = users.stream().filter(u -> u.id.equals(posterInfo.userId)).findFirst().get().name;
}
}
@ -373,7 +301,7 @@ public class CommunityMarketplaceAddonService implements AddonService {
boolean installed = addonHandlers.stream()
.anyMatch(handler -> handler.supports(type, contentType) && handler.isInstalled(id));
return Addon.create(id).withType(type).withContentType(contentType).withImageLink(topic.image_url)
return Addon.create(id).withType(type).withContentType(contentType).withImageLink(topic.imageUrl)
.withAuthor(author).withProperties(properties).withLabel(title).withInstalled(installed)
.withMaturity(maturity).withLink(link).build();
}
@ -399,16 +327,16 @@ public class CommunityMarketplaceAddonService implements AddonService {
String id = ADDON_ID_PREFIX + topic.id.toString();
List<String> tags = Arrays.asList(Objects.requireNonNullElse(topic.tags, new String[0]));
AddonType addonType = getAddonType(topic.category_id, tags);
AddonType addonType = getAddonType(topic.categoryId, tags);
String type = (addonType != null) ? addonType.getId() : "";
String contentType = getContentType(topic.category_id, tags);
String contentType = getContentType(topic.categoryId, tags);
int likeCount = topic.like_count;
int likeCount = topic.likeCount;
int views = topic.views;
int postsCount = topic.posts_count;
Date createdDate = topic.post_stream.posts[0].created_at;
Date updatedDate = topic.post_stream.posts[0].updated_at;
Date lastPostedDate = topic.last_posted;
int postsCount = topic.postsCount;
Date createdDate = topic.postStream.posts[0].createdAt;
Date updatedDate = topic.postStream.posts[0].updatedAt;
Date lastPostedDate = topic.lastPosted;
String maturity = tags.stream().filter(CODE_MATURITY_LEVELS::contains).findAny().orElse(null);
@ -421,11 +349,11 @@ public class CommunityMarketplaceAddonService implements AddonService {
properties.put("posts_count", postsCount);
properties.put("tags", tags.toArray(String[]::new));
String detailedDescription = topic.post_stream.posts[0].cooked;
String detailedDescription = topic.postStream.posts[0].cooked;
// try to extract contents or links
if (topic.post_stream.posts[0].link_counts != null) {
for (DiscoursePostLink postLink : topic.post_stream.posts[0].link_counts) {
if (topic.postStream.posts[0].linkCounts != null) {
for (DiscoursePostLink postLink : topic.postStream.posts[0].linkCounts) {
if (postLink.url.endsWith(".jar")) {
properties.put("jar_download_url", postLink.url);
}
@ -453,43 +381,14 @@ public class CommunityMarketplaceAddonService implements AddonService {
properties.put("yaml_content", unescapeEntities(yamlContent));
}
// try to use an handler to determine if the add-on is installed
// try to use a handler to determine if the add-on is installed
boolean installed = addonHandlers.stream()
.anyMatch(handler -> handler.supports(type, contentType) && handler.isInstalled(id));
return Addon.create(id).withType(type).withContentType(contentType).withLabel(topic.title)
.withImageLink(topic.image_url).withLink(COMMUNITY_TOPIC_URL + topic.id.toString())
.withAuthor(topic.post_stream.posts[0].display_username).withMaturity(maturity)
.withImageLink(topic.imageUrl).withLink(COMMUNITY_TOPIC_URL + topic.id.toString())
.withAuthor(topic.postStream.posts[0].displayUsername).withMaturity(maturity)
.withDetailedDescription(detailedDescription).withInstalled(installed).withProperties(properties)
.build();
}
private void postInstalledEvent(String extensionId) {
Event event = AddonEventFactory.createAddonInstalledEvent(extensionId);
eventPublisher.post(event);
}
private void postUninstalledEvent(String extensionId) {
Event event = AddonEventFactory.createAddonUninstalledEvent(extensionId);
eventPublisher.post(event);
}
private void postFailureEvent(String extensionId, @Nullable String msg) {
Event event = AddonEventFactory.createAddonFailureEvent(extensionId, msg);
eventPublisher.post(event);
}
private boolean remoteEnabled() {
try {
Configuration configuration = configurationAdmin.getConfiguration("org.openhab.addons", null);
Dictionary<String, Object> properties = configuration.getProperties();
if (properties == null) {
// if we can't determine a set property, we use true (default is remote enabled)
return true;
}
return ConfigParser.valueAsOrElse(properties.get(CFG_REMOTE), Boolean.class, true);
} catch (IOException e) {
return true;
}
}
}

View File

@ -14,6 +14,8 @@ package org.openhab.core.addon.marketplace.internal.community.model;
import java.util.Date;
import com.google.gson.annotations.SerializedName;
/**
* A DTO class mapped to the Discourse category topic list API.
*
@ -21,38 +23,48 @@ import java.util.Date;
*/
public class DiscourseCategoryResponseDTO {
public DiscourseUser[] users;
public DiscourseTopicList topic_list;
@SerializedName("topic_list")
public DiscourseTopicList topicList;
public class DiscourseUser {
public static class DiscourseUser {
public Integer id;
public String username;
public String name;
public String avatar_template;
@SerializedName("avatar_template")
public String avatarTemplate;
}
public class DiscourseTopicList {
public String more_topics_url;
public Integer per_page;
public static class DiscourseTopicList {
@SerializedName("more_topics_url")
public String moreTopicsUrl;
@SerializedName("per_page")
public Integer perPage;
public DiscourseTopicItem[] topics;
}
public class DiscoursePosterInfo {
public static class DiscoursePosterInfo {
public String extras;
public String description;
public Integer user_id;
@SerializedName("user_id")
public Integer userId;
}
public class DiscourseTopicItem {
public static class DiscourseTopicItem {
public Integer id;
public String title;
public String slug;
public String[] tags;
public Integer posts_count;
public String image_url;
public Date created_at;
public Integer like_count;
@SerializedName("posts_count")
public Integer postsCount;
@SerializedName("image_url")
public String imageUrl;
@SerializedName("created_at")
public Date createdAt;
@SerializedName("like_count")
public Integer likeCount;
public Integer views;
public Integer category_id;
@SerializedName("category_id")
public Integer categoryId;
public DiscoursePosterInfo[] posters;
}
}

View File

@ -14,6 +14,8 @@ package org.openhab.core.addon.marketplace.internal.community.model;
import java.util.Date;
import com.google.gson.annotations.SerializedName;
/**
* A DTO class mapped to the Discourse topic API.
*
@ -22,57 +24,72 @@ import java.util.Date;
public class DiscourseTopicResponseDTO {
public Integer id;
public DiscoursePostStream post_stream;
@SerializedName("post_stream")
public DiscoursePostStream postStream;
public String title;
public Integer posts_count;
public String image_url;
@SerializedName("posts_count")
public Integer postsCount;
@SerializedName("image_url")
public String imageUrl;
public Date created_at;
public Date updated_at;
public Date last_posted;
@SerializedName("created_at")
public Date createdAt;
@SerializedName("updated_at")
public Date updatedAt;
@SerializedName("last_posted")
public Date lastPosted;
public Integer like_count;
@SerializedName("like_count")
public Integer likeCount;
public Integer views;
public String[] tags;
public Integer category_id;
@SerializedName("category_id")
public Integer categoryId;
public DiscourseTopicDetails details;
public class DiscoursePostAuthor {
public static class DiscoursePostAuthor {
public Integer id;
public String username;
public String avatar_template;
@SerializedName("avatar_template")
public String avatarTemplate;
}
public class DiscoursePostLink {
public static class DiscoursePostLink {
public String url;
public Boolean internal;
public Integer clicks;
}
public class DiscoursePostStream {
public static class DiscoursePostStream {
public DiscoursePost[] posts;
}
public class DiscoursePost {
public static class DiscoursePost {
public Integer id;
public String username;
public String display_username;
@SerializedName("display_username")
public String displayUsername;
public Date created_at;
public Date updated_at;
@SerializedName("created_at")
public Date createdAt;
@SerializedName("updated_at")
public Date updatedAt;
public String cooked;
public DiscoursePostLink[] link_counts;
@SerializedName("link_counts")
public DiscoursePostLink[] linkCounts;
}
public class DiscourseTopicDetails {
public DiscoursePostAuthor created_by;
public DiscoursePostAuthor last_poster;
public static class DiscourseTopicDetails {
@SerializedName("created_by")
public DiscoursePostAuthor createdBy;
@SerializedName("last_poster")
public DiscoursePostAuthor lastPoster;
public DiscoursePostLink[] links;
}

View File

@ -19,33 +19,25 @@ import java.lang.reflect.Type;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashMap;
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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.Addon;
import org.openhab.core.addon.AddonEventFactory;
import org.openhab.core.addon.AddonService;
import org.openhab.core.addon.AddonType;
import org.openhab.core.addon.marketplace.AbstractRemoteAddonService;
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
import org.openhab.core.addon.marketplace.MarketplaceHandlerException;
import org.openhab.core.addon.marketplace.internal.json.model.AddonEntryDTO;
import org.openhab.core.config.core.ConfigParser;
import org.openhab.core.config.core.ConfigurableService;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.storage.StorageService;
import org.osgi.framework.Constants;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@ -53,77 +45,58 @@ import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
/**
* This class is a {@link AddonService} retrieving JSON marketplace information.
* This class implements an {@link org.openhab.core.addon.AddonService} retrieving JSON marketplace information.
*
* @author Yannick Schaus - Initial contribution
* @author Jan N. Klug - Refactored for JSON marketplaces
*/
@Component(immediate = true, configurationPid = { "org.openhab.jsonaddonservice" }, //
property = Constants.SERVICE_PID + "=org.openhab.jsonaddonservice")
@Component(immediate = true, configurationPid = JsonAddonService.SERVICE_PID, //
property = Constants.SERVICE_PID + "=" + JsonAddonService.SERVICE_PID, service = AddonService.class)
@ConfigurableService(category = "system", label = JsonAddonService.SERVICE_NAME, description_uri = JsonAddonService.CONFIG_URI)
@NonNullByDefault
public class JsonAddonService implements AddonService {
private final Logger logger = LoggerFactory.getLogger(JsonAddonService.class);
public class JsonAddonService extends AbstractRemoteAddonService {
static final String SERVICE_NAME = "Json 3rd Party Add-on Service";
static final String CONFIG_URI = "system:jsonaddonservice";
static final String SERVICE_PID = "org.openhab.jsonaddonservice";
private static final String SERVICE_ID = "json";
private static final String ADDON_ID_PREFIX = SERVICE_ID + ":";
private static final String CONFIG_URLS = "urls";
private static final String CONFIG_SHOW_UNSTABLE = "showUnstable";
private static final String CFG_REMOTE = "remote";
private static final Map<String, AddonType> TAG_ADDON_TYPE_MAP = Map.of( //
"automation", new AddonType("automation", "Automation"), //
"binding", new AddonType("binding", "Bindings"), //
"misc", new AddonType("misc", "Misc"), //
"persistence", new AddonType("persistence", "Persistence"), //
"transformation", new AddonType("transformation", "Transformations"), //
"ui", new AddonType("ui", "User Interfaces"), //
"voice", new AddonType("voice", "Voice"));
private final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").create();
private final Set<MarketplaceAddonHandler> addonHandlers = new HashSet<>();
private List<String> addonserviceUrls = List.of();
private List<AddonEntryDTO> cachedAddons = List.of();
private List<String> addonServiceUrls = List.of();
private boolean showUnstable = false;
private final EventPublisher eventPublisher;
private final ConfigurationAdmin configurationAdmin;
@Activate
public JsonAddonService(@Reference EventPublisher eventPublisher, @Reference ConfigurationAdmin configurationAdmin,
Map<String, Object> config) {
this.eventPublisher = eventPublisher;
this.configurationAdmin = configurationAdmin;
public JsonAddonService(@Reference EventPublisher eventPublisher, @Reference StorageService storageService,
@Reference ConfigurationAdmin configurationAdmin, Map<String, Object> config) {
super(eventPublisher, configurationAdmin, storageService, SERVICE_PID);
modified(config);
}
@Modified
public void modified(Map<String, Object> config) {
String urls = Objects.requireNonNullElse((String) config.get(CONFIG_URLS), "");
addonserviceUrls = Arrays.asList(urls.split("\\|"));
showUnstable = (Boolean) config.getOrDefault(CONFIG_SHOW_UNSTABLE, false);
refreshSource();
public void modified(@Nullable Map<String, Object> config) {
if (config != null) {
String urls = Objects.requireNonNullElse((String) config.get(CONFIG_URLS), "");
addonServiceUrls = Arrays.asList(urls.split("\\|"));
showUnstable = (Boolean) config.getOrDefault(CONFIG_SHOW_UNSTABLE, false);
cachedRemoteAddons.invalidateValue();
refreshSource();
}
}
@Override
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
protected void addAddonHandler(MarketplaceAddonHandler handler) {
this.addonHandlers.add(handler);
}
@Override
protected void removeAddonHandler(MarketplaceAddonHandler handler) {
this.addonHandlers.remove(handler);
}
@ -140,13 +113,8 @@ public class JsonAddonService implements AddonService {
@Override
@SuppressWarnings("unchecked")
public void refreshSource() {
if (!remoteEnabled()) {
cachedAddons = List.of();
return;
}
cachedAddons = (List<AddonEntryDTO>) addonserviceUrls.stream().map(urlString -> {
protected List<Addon> getRemoteAddons() {
return addonServiceUrls.stream().map(urlString -> {
try {
URL url = new URL(urlString);
URLConnection connection = url.openConnection();
@ -158,72 +126,15 @@ public class JsonAddonService implements AddonService {
} catch (IOException e) {
return List.of();
}
}).flatMap(List::stream).filter(e -> showUnstable || "stable".equals(((AddonEntryDTO) e).maturity))
}).flatMap(List::stream).filter(Objects::nonNull).map(e -> (AddonEntryDTO) e)
.filter(e -> showUnstable || "stable".equals(e.maturity)).map(this::fromAddonEntry)
.collect(Collectors.toList());
}
@Override
public List<Addon> getAddons(@Nullable Locale locale) {
refreshSource();
return cachedAddons.stream().map(this::fromAddonEntry).collect(Collectors.toList());
}
@Override
public @Nullable Addon getAddon(String id, @Nullable Locale locale) {
String remoteId = id.replace(ADDON_ID_PREFIX, "");
return cachedAddons.stream().filter(e -> remoteId.equals(e.id)).map(this::fromAddonEntry).findAny()
.orElse(null);
}
@Override
public List<AddonType> getTypes(@Nullable Locale locale) {
return new ArrayList<>(TAG_ADDON_TYPE_MAP.values());
}
@Override
public void install(String id) {
Addon addon = getAddon(id, null);
if (addon != null) {
for (MarketplaceAddonHandler handler : addonHandlers) {
if (handler.supports(addon.getType(), addon.getContentType())) {
if (!handler.isInstalled(addon.getId())) {
try {
handler.install(addon);
postInstalledEvent(addon.getId());
} catch (MarketplaceHandlerException e) {
postFailureEvent(addon.getId(), e.getMessage());
}
} else {
postFailureEvent(addon.getId(), "Add-on is already installed.");
}
return;
}
}
}
postFailureEvent(id, "Add-on not known.");
}
@Override
public void uninstall(String id) {
Addon addon = getAddon(id, null);
if (addon != null) {
for (MarketplaceAddonHandler handler : addonHandlers) {
if (handler.supports(addon.getType(), addon.getContentType())) {
if (handler.isInstalled(addon.getId())) {
try {
handler.uninstall(addon);
postUninstalledEvent(addon.getId());
} catch (MarketplaceHandlerException e) {
postFailureEvent(addon.getId(), e.getMessage());
}
} else {
postFailureEvent(addon.getId(), "Add-on is not installed.");
}
return;
}
}
}
postFailureEvent(id, "Add-on not known.");
String fullId = ADDON_ID_PREFIX + id;
return cachedAddons.stream().filter(e -> fullId.equals(e.getId())).findAny().orElse(null);
}
@Override
@ -251,35 +162,6 @@ public class JsonAddonService implements AddonService {
.withDetailedDescription(addonEntry.description).withContentType(addonEntry.contentType)
.withAuthor(addonEntry.author).withVersion(addonEntry.version).withLabel(addonEntry.title)
.withMaturity(addonEntry.maturity).withProperties(properties).withLink(addonEntry.link)
.withConfigDescriptionURI(addonEntry.configDescriptionURI).build();
}
private void postInstalledEvent(String extensionId) {
Event event = AddonEventFactory.createAddonInstalledEvent(extensionId);
eventPublisher.post(event);
}
private void postUninstalledEvent(String extensionId) {
Event event = AddonEventFactory.createAddonUninstalledEvent(extensionId);
eventPublisher.post(event);
}
private void postFailureEvent(String extensionId, @Nullable String msg) {
Event event = AddonEventFactory.createAddonFailureEvent(extensionId, msg);
eventPublisher.post(event);
}
private boolean remoteEnabled() {
try {
Configuration configuration = configurationAdmin.getConfiguration("org.openhab.addons", null);
Dictionary<String, Object> properties = configuration.getProperties();
if (properties == null) {
// if we can't determine a set property, we use true (default is remote enabled)
return true;
}
return ConfigParser.valueAsOrElse(properties.get(CFG_REMOTE), Boolean.class, true);
} catch (IOException e) {
return true;
}
.withImageLink(addonEntry.imageUrl).withConfigDescriptionURI(addonEntry.configDescriptionURI).build();
}
}

View File

@ -31,5 +31,7 @@ public class AddonEntryDTO {
public String maturity = "unstable";
@SerializedName("content_type")
public String contentType = "";
@SerializedName("image_url")
public String imageUrl;
public String url = "";
}

View File

@ -0,0 +1,255 @@
/**
* Copyright (c) 2010-2022 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.test;
import static org.openhab.core.addon.test.TestAddonService.INSTALL_EXCEPTION_ADDON;
import static org.openhab.core.addon.test.TestAddonService.SERVICE_PID;
import static org.openhab.core.addon.test.TestAddonService.TEST_ADDON;
import static org.openhab.core.addon.test.TestAddonService.UNINSTALL_EXCEPTION_ADDON;
import java.io.IOException;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.core.addon.Addon;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
import org.openhab.core.test.storage.VolatileStorage;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
/**
* The {@link AbstractRemoteAddonServiceTest} contains tests for the
* {@link org.openhab.core.addon.marketplace.AbstractRemoteAddonService}
*
* @author Jan N. Klug - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.WARN)
@NonNullByDefault
public class AbstractRemoteAddonServiceTest {
private @Mock @NonNullByDefault({}) StorageService storageService;
private @Mock @NonNullByDefault({}) ConfigurationAdmin configurationAdmin;
private @Mock @NonNullByDefault({}) EventPublisher eventPublisher;
private @Mock @NonNullByDefault({}) Configuration configuration;
private @NonNullByDefault({}) Storage<String> storage;
private @NonNullByDefault({}) TestAddonService addonService;
private @NonNullByDefault({}) TestAddonHandler addonHandler;
private final Dictionary<String, Object> properties = new Hashtable<>();
@BeforeEach
public void initialize() throws IOException {
storage = new VolatileStorage<>();
Mockito.doReturn(storage).when(storageService).getStorage(SERVICE_PID);
Mockito.doReturn(configuration).when(configurationAdmin).getConfiguration("org.openhab.addons", null);
Mockito.doReturn(properties).when(configuration).getProperties();
addonHandler = new TestAddonHandler();
addonService = new TestAddonService(eventPublisher, configurationAdmin, storageService);
addonService.addAddonHandler(addonHandler);
}
// general tests
@Test
public void testRemoteDisabledBlocksRemoteCalls() {
properties.put("remote", false);
List<Addon> addons = addonService.getAddons(null);
Assertions.assertEquals(0, addons.size());
Assertions.assertEquals(0, addonService.getRemoteCalls());
}
@Test
public void testAddonResultsAreCached() {
List<Addon> addons = addonService.getAddons(null);
Assertions.assertEquals(TestAddonService.REMOTE_ADDONS.size(), addons.size());
addons = addonService.getAddons(null);
Assertions.assertEquals(TestAddonService.REMOTE_ADDONS.size(), addons.size());
Assertions.assertEquals(1, addonService.getRemoteCalls());
}
@Test
public void testAddonIsReportedAsInstalledIfStorageEntryMissing() {
addonService.setInstalled(TEST_ADDON);
List<Addon> addons = addonService.getAddons(null);
Addon addon = addons.stream().filter(a -> getFullAddonId(TEST_ADDON).equals(a.getId())).findAny().orElse(null);
Objects.requireNonNull(addon);
Assertions.assertTrue(addon.isInstalled());
}
@Test
public void testInstalledAddonIsStillPresentAfterRemoteIsDisabledOrMissing() {
addonService.setInstalled(TEST_ADDON);
addonService.addToStorage(TEST_ADDON);
// check all addons are present
List<Addon> addons = addonService.getAddons(null);
Assertions.assertEquals(TestAddonService.REMOTE_ADDONS.size(), addons.size());
// disable remote repo
properties.put("remote", false);
// check only the installed addon is present
addons = addonService.getAddons(null);
Assertions.assertEquals(1, addons.size());
Assertions.assertEquals(getFullAddonId(TEST_ADDON), addons.get(0).getId());
}
// installation tests
@Test
public void testAddonInstall() {
addonService.getAddons(null);
addonService.install(TEST_ADDON);
checkResult(TEST_ADDON, getFullAddonId(TEST_ADDON) + "/installed", true, true);
}
@Test
public void testAddonInstallFailsWithHandlerException() {
addonService.getAddons(null);
addonService.install(INSTALL_EXCEPTION_ADDON);
checkResult(INSTALL_EXCEPTION_ADDON, getFullAddonId(INSTALL_EXCEPTION_ADDON) + "/failed", false, true);
}
@Test
public void testAddonInstallFailsOnInstalledAddon() {
addonService.setInstalled(TEST_ADDON);
addonService.addToStorage(TEST_ADDON);
addonService.getAddons(null);
addonService.install(TEST_ADDON);
checkResult(TEST_ADDON, getFullAddonId(TEST_ADDON) + "/failed", true, true);
}
@Test
public void testAddonInstallFailsOnUnknownAddon() {
addonService.getAddons(null);
addonService.install("unknown");
checkResult("unknown", "unknown/failed", false, false);
}
// uninstallation tests
@Test
public void testAddonUninstall() {
addonService.setInstalled(TEST_ADDON);
addonService.addToStorage(TEST_ADDON);
addonService.getAddons(null);
addonService.uninstall(TEST_ADDON);
checkResult(TEST_ADDON, getFullAddonId(TEST_ADDON) + "/uninstalled", false, true);
}
@Test
public void testAddonUninstallFailsWithHandlerException() {
addonService.setInstalled(UNINSTALL_EXCEPTION_ADDON);
addonService.addToStorage(UNINSTALL_EXCEPTION_ADDON);
addonService.getAddons(null);
addonService.uninstall(UNINSTALL_EXCEPTION_ADDON);
checkResult(UNINSTALL_EXCEPTION_ADDON, getFullAddonId(UNINSTALL_EXCEPTION_ADDON) + "/failed", true, true);
}
@Test
public void testAddonUninstallFailsOnUninstalledAddon() {
addonService.getAddons(null);
addonService.uninstall(TEST_ADDON);
checkResult(TEST_ADDON, getFullAddonId(TEST_ADDON) + "/failed", false, true);
}
@Test
public void testAddonUninstallFailsOnUnknownAddon() {
addonService.getAddons(null);
addonService.uninstall("unknown");
checkResult("unknown", "unknown/failed", false, false);
}
@Test
public void testAddonUninstallRemovesStorageEntryOnUninstalledAddon() {
addonService.addToStorage(TEST_ADDON);
addonService.getAddons(null);
addonService.uninstall(TEST_ADDON);
checkResult(TEST_ADDON, getFullAddonId(TEST_ADDON) + "/failed", false, true);
}
/**
* checks that a proper event is posted, the presence in storage and installation status in handler
*
* @param id add-on id (without service-prefix)
* @param expectedEventTopic the expected event (e.g. installed)
* @param installStatus the expected installation status of the add-on
* @param present if the addon is expected to be present after the test
*/
private void checkResult(String id, String expectedEventTopic, boolean installStatus, boolean present) {
// assert expected event is posted
ArgumentCaptor<Event> eventCaptor = ArgumentCaptor.forClass(Event.class);
Mockito.verify(eventPublisher).post(eventCaptor.capture());
Event event = eventCaptor.getValue();
String topic = "openhab/addons/" + expectedEventTopic;
Assertions.assertEquals(topic, event.getTopic());
// assert addon handler was called (by checking it's installed status)
Assertions.assertEquals(installStatus, addonHandler.isInstalled(getFullAddonId(id)));
// assert is present in storage if installed or missing if uninstalled
Assertions.assertEquals(installStatus, storage.containsKey(id));
// assert correct installation status is reported for addon
Addon addon = addonService.getAddon(id, null);
if (present) {
Assertions.assertNotNull(addon);
Objects.requireNonNull(addon);
Assertions.assertEquals(installStatus, addon.isInstalled());
} else {
Assertions.assertNull(addon);
}
}
private String getFullAddonId(String id) {
return SERVICE_PID + ":" + id;
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2022 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.test;
import static org.openhab.core.addon.test.TestAddonService.INSTALL_EXCEPTION_ADDON;
import static org.openhab.core.addon.test.TestAddonService.UNINSTALL_EXCEPTION_ADDON;
import java.util.HashSet;
import java.util.Set;
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;
/**
* The {@link TestAddonHandler} is a
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class TestAddonHandler implements MarketplaceAddonHandler {
private static final Set<String> SUPPORTED_ADDON_TYPES = Set.of("binding", "automation");
public static final String TEST_ADDON_CONTENT_TYPE = "testAddonContentType";
private final Set<String> installedAddons = new HashSet<>();
@Override
public boolean supports(String type, String contentType) {
return SUPPORTED_ADDON_TYPES.contains(type) && TEST_ADDON_CONTENT_TYPE.equals(contentType);
}
@Override
public boolean isInstalled(String id) {
return installedAddons.contains(id);
}
@Override
public void install(Addon addon) throws MarketplaceHandlerException {
if (addon.getId().endsWith(":" + INSTALL_EXCEPTION_ADDON)) {
throw new MarketplaceHandlerException("Installation failed", null);
}
installedAddons.add(addon.getId());
}
@Override
public void uninstall(Addon addon) throws MarketplaceHandlerException {
if (addon.getId().endsWith(":" + UNINSTALL_EXCEPTION_ADDON)) {
throw new MarketplaceHandlerException("Uninstallation failed", null);
}
installedAddons.remove(addon.getId());
}
}

View File

@ -0,0 +1,130 @@
/**
* Copyright (c) 2010-2022 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.test;
import java.net.URI;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.Addon;
import org.openhab.core.addon.marketplace.AbstractRemoteAddonService;
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
import org.openhab.core.addon.marketplace.MarketplaceHandlerException;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.storage.StorageService;
import org.osgi.service.cm.ConfigurationAdmin;
/**
* The {@link TestAddonService} is a
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class TestAddonService extends AbstractRemoteAddonService {
public static final String TEST_ADDON = "testAddon";
public static final String INSTALL_EXCEPTION_ADDON = "installException";
public static final String UNINSTALL_EXCEPTION_ADDON = "uninstallException";
public static final String SERVICE_PID = "testAddonService";
public static final Set<String> REMOTE_ADDONS = Set.of(TEST_ADDON, INSTALL_EXCEPTION_ADDON,
UNINSTALL_EXCEPTION_ADDON);
private int remoteCalls = 0;
public TestAddonService(EventPublisher eventPublisher, ConfigurationAdmin configurationAdmin,
StorageService storageService) {
super(eventPublisher, configurationAdmin, storageService, SERVICE_PID);
}
public void addAddonHandler(MarketplaceAddonHandler handler) {
this.addonHandlers.add(handler);
}
public void removeAddonHandler(MarketplaceAddonHandler handler) {
this.addonHandlers.remove(handler);
}
@Override
protected List<Addon> getRemoteAddons() {
remoteCalls++;
return REMOTE_ADDONS.stream()
.map(id -> Addon.create(SERVICE_PID + ":" + id).withType("binding")
.withContentType(TestAddonHandler.TEST_ADDON_CONTENT_TYPE).build())
.collect(Collectors.toList());
}
@Override
public String getId() {
return SERVICE_PID;
}
@Override
public String getName() {
return "Test Addon Service";
}
@Override
public @Nullable Addon getAddon(String id, @Nullable Locale locale) {
String remoteId = SERVICE_PID + ":" + id;
return cachedAddons.stream().filter(a -> remoteId.equals(a.getId())).findAny().orElse(null);
}
@Override
public @Nullable String getAddonId(URI addonURI) {
return null;
}
/**
* get the number of remote calls issued by the addon service
*
* @return number of calls
*/
public int getRemoteCalls() {
return remoteCalls;
}
/**
* this installs an addon to the service without calling the install method
*
* @param id id of the addon to install
*/
public void setInstalled(String id) {
Addon addon = Addon.create(SERVICE_PID + ":" + id).withType("binding")
.withContentType(TestAddonHandler.TEST_ADDON_CONTENT_TYPE).build();
addonHandlers.forEach(addonHandler -> {
try {
addonHandler.install(addon);
} catch (MarketplaceHandlerException e) {
// ignore
}
});
}
/**
* add to installedStorage
*
* @param id id of the addon to add
*/
public void addToStorage(String id) {
Addon addon = Addon.create(SERVICE_PID + ":" + id).withType("binding")
.withContentType(TestAddonHandler.TEST_ADDON_CONTENT_TYPE).build();
addon.setInstalled(true);
installedAddonStorage.put(id, gson.toJson(addon));
}
}