[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
parent
3e94dd6e30
commit
c4e1b14d00
|
@ -38,6 +38,12 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.core.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.core.test</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,6 @@ package org.openhab.core.addon.marketplace.internal.community;
|
||||||
|
|
||||||
import static org.openhab.core.addon.Addon.CODE_MATURITY_LEVELS;
|
import static org.openhab.core.addon.Addon.CODE_MATURITY_LEVELS;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -23,37 +22,31 @@ import java.net.URLConnection;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Dictionary;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.core.addon.Addon;
|
import org.openhab.core.addon.Addon;
|
||||||
import org.openhab.core.addon.AddonEventFactory;
|
|
||||||
import org.openhab.core.addon.AddonService;
|
import org.openhab.core.addon.AddonService;
|
||||||
import org.openhab.core.addon.AddonType;
|
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.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;
|
||||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO.DiscoursePosterInfo;
|
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.DiscourseTopicItem;
|
||||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO.DiscourseUser;
|
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;
|
||||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseTopicResponseDTO.DiscoursePostLink;
|
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.config.core.ConfigurableService;
|
||||||
import org.openhab.core.events.Event;
|
|
||||||
import org.openhab.core.events.EventPublisher;
|
import org.openhab.core.events.EventPublisher;
|
||||||
|
import org.openhab.core.storage.StorageService;
|
||||||
import org.osgi.framework.Constants;
|
import org.osgi.framework.Constants;
|
||||||
import org.osgi.service.cm.Configuration;
|
|
||||||
import org.osgi.service.cm.ConfigurationAdmin;
|
import org.osgi.service.cm.ConfigurationAdmin;
|
||||||
import org.osgi.service.component.annotations.Activate;
|
import org.osgi.service.component.annotations.Activate;
|
||||||
import org.osgi.service.component.annotations.Component;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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
|
* @author Yannick Schaus - Initial contribution
|
||||||
*/
|
*/
|
||||||
@Component(immediate = true, configurationPid = "org.openhab.marketplace", //
|
@Component(immediate = true, configurationPid = CommunityMarketplaceAddonService.SERVICE_PID, //
|
||||||
property = Constants.SERVICE_PID + "=org.openhab.marketplace")
|
property = Constants.SERVICE_PID + "="
|
||||||
@ConfigurableService(category = "system", label = "Community Marketplace", description_uri = CommunityMarketplaceAddonService.CONFIG_URI)
|
+ CommunityMarketplaceAddonService.SERVICE_PID, service = AddonService.class)
|
||||||
|
@ConfigurableService(category = "system", label = CommunityMarketplaceAddonService.SERVICE_NAME, description_uri = CommunityMarketplaceAddonService.CONFIG_URI)
|
||||||
@NonNullByDefault
|
@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 JAR_CONTENT_TYPE = "application/vnd.openhab.bundle";
|
||||||
public static final String KAR_CONTENT_TYPE = "application/vnd.openhab.feature;type=karfile";
|
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 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";
|
public static final String BLOCKLIBRARIES_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=blocks";
|
||||||
|
|
||||||
// constants for the configuration properties
|
// 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_URI = "system:marketplace";
|
||||||
static final String CONFIG_API_KEY = "apiKey";
|
static final String CONFIG_API_KEY = "apiKey";
|
||||||
static final String CONFIG_SHOW_UNPUBLISHED_ENTRIES_KEY = "showUnpublished";
|
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_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_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 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 JSON_CODE_MARKUP_START = "<pre><code class=\"lang-json\">";
|
||||||
private static final String YAML_CODE_MARKUP_START = "<pre><code class=\"lang-yaml\">";
|
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 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 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 @Nullable String apiKey = null;
|
||||||
private boolean showUnpublished = false;
|
private boolean showUnpublished = false;
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
public CommunityMarketplaceAddonService(final @Reference EventPublisher eventPublisher,
|
public CommunityMarketplaceAddonService(final @Reference EventPublisher eventPublisher,
|
||||||
@Reference ConfigurationAdmin configurationAdmin) {
|
@Reference ConfigurationAdmin configurationAdmin, @Reference StorageService storageService,
|
||||||
this.eventPublisher = eventPublisher;
|
Map<String, Object> config) {
|
||||||
this.configurationAdmin = configurationAdmin;
|
super(eventPublisher, configurationAdmin, storageService, SERVICE_PID);
|
||||||
}
|
|
||||||
|
|
||||||
@Activate
|
|
||||||
protected void activate(Map<String, Object> config) {
|
|
||||||
modified(config);
|
modified(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Modified
|
@Modified
|
||||||
void modified(@Nullable Map<String, Object> config) {
|
public void modified(@Nullable Map<String, Object> config) {
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
this.apiKey = (String) config.get(CONFIG_API_KEY);
|
this.apiKey = (String) config.get(CONFIG_API_KEY);
|
||||||
Object showUnpublishedConfigValue = config.get(CONFIG_SHOW_UNPUBLISHED_ENTRIES_KEY);
|
Object showUnpublishedConfigValue = config.get(CONFIG_SHOW_UNPUBLISHED_ENTRIES_KEY);
|
||||||
this.showUnpublished = showUnpublishedConfigValue != null
|
this.showUnpublished = showUnpublishedConfigValue != null
|
||||||
&& "true".equals(showUnpublishedConfigValue.toString());
|
&& "true".equals(showUnpublishedConfigValue.toString());
|
||||||
|
cachedRemoteAddons.invalidateValue();
|
||||||
|
refreshSource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
|
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
|
||||||
protected void addAddonHandler(MarketplaceAddonHandler handler) {
|
protected void addAddonHandler(MarketplaceAddonHandler handler) {
|
||||||
this.addonHandlers.add(handler);
|
this.addonHandlers.add(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void removeAddonHandler(MarketplaceAddonHandler handler) {
|
protected void removeAddonHandler(MarketplaceAddonHandler handler) {
|
||||||
this.addonHandlers.remove(handler);
|
this.addonHandlers.remove(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return "marketplace";
|
return SERVICE_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "Community Marketplace";
|
return SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refreshSource() {
|
protected List<Addon> getRemoteAddons() {
|
||||||
}
|
List<Addon> addons = new ArrayList<>();
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Addon> getAddons(@Nullable Locale locale) {
|
|
||||||
if (!remoteEnabled()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<DiscourseCategoryResponseDTO> pages = new ArrayList<>();
|
List<DiscourseCategoryResponseDTO> pages = new ArrayList<>();
|
||||||
|
|
||||||
|
@ -190,11 +162,11 @@ public class CommunityMarketplaceAddonService implements AddonService {
|
||||||
|
|
||||||
try (Reader reader = new InputStreamReader(connection.getInputStream())) {
|
try (Reader reader = new InputStreamReader(connection.getInputStream())) {
|
||||||
DiscourseCategoryResponseDTO parsed = gson.fromJson(reader, DiscourseCategoryResponseDTO.class);
|
DiscourseCategoryResponseDTO parsed = gson.fromJson(reader, DiscourseCategoryResponseDTO.class);
|
||||||
if (parsed.topic_list.topics.length != 0) {
|
if (parsed.topicList.topics.length != 0) {
|
||||||
pages.add(parsed);
|
pages.add(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.topic_list.more_topics_url != null) {
|
if (parsed.topicList.moreTopicsUrl != null) {
|
||||||
// Discourse URL for next page is wrong
|
// Discourse URL for next page is wrong
|
||||||
url = new URL(COMMUNITY_MARKETPLACE_URL + "?page=" + pageNb++);
|
url = new URL(COMMUNITY_MARKETPLACE_URL + "?page=" + pageNb++);
|
||||||
} else {
|
} else {
|
||||||
|
@ -204,24 +176,31 @@ public class CommunityMarketplaceAddonService implements AddonService {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DiscourseUser> users = pages.stream().flatMap(p -> Stream.of(p.users)).collect(Collectors.toList());
|
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))
|
.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) {
|
} catch (Exception e) {
|
||||||
logger.error("Unable to retrieve marketplace add-ons", e);
|
logger.error("Unable to retrieve marketplace add-ons", e);
|
||||||
return List.of();
|
|
||||||
}
|
}
|
||||||
|
return addons;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Addon getAddon(String id, @Nullable Locale locale) {
|
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()) {
|
if (!remoteEnabled()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
URL url;
|
// retrieve from remote
|
||||||
try {
|
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();
|
URLConnection connection = url.openConnection();
|
||||||
connection.addRequestProperty("Accept", "application/json");
|
connection.addRequestProperty("Accept", "application/json");
|
||||||
if (this.apiKey != null) {
|
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
|
@Override
|
||||||
public @Nullable String getAddonId(URI addonURI) {
|
public @Nullable String getAddonId(URI addonURI) {
|
||||||
if (addonURI.toString().startsWith(COMMUNITY_TOPIC_URL)) {
|
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]));
|
List<String> tags = Arrays.asList(Objects.requireNonNullElse(topic.tags, new String[0]));
|
||||||
|
|
||||||
String id = ADDON_ID_PREFIX + topic.id.toString();
|
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 type = (addonType != null) ? addonType.getId() : "";
|
||||||
String contentType = getContentType(topic.category_id, tags);
|
String contentType = getContentType(topic.categoryId, tags);
|
||||||
|
|
||||||
String title = topic.title;
|
String title = topic.title;
|
||||||
String link = COMMUNITY_TOPIC_URL + topic.id.toString();
|
String link = COMMUNITY_TOPIC_URL + topic.id.toString();
|
||||||
int likeCount = topic.like_count;
|
int likeCount = topic.likeCount;
|
||||||
int views = topic.views;
|
int views = topic.views;
|
||||||
int postsCount = topic.posts_count;
|
int postsCount = topic.postsCount;
|
||||||
Date createdDate = topic.created_at;
|
Date createdDate = topic.createdAt;
|
||||||
String author = "";
|
String author = "";
|
||||||
for (DiscoursePosterInfo posterInfo : topic.posters) {
|
for (DiscoursePosterInfo posterInfo : topic.posters) {
|
||||||
if (posterInfo.description.contains("Original Poster")) {
|
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()
|
boolean installed = addonHandlers.stream()
|
||||||
.anyMatch(handler -> handler.supports(type, contentType) && handler.isInstalled(id));
|
.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)
|
.withAuthor(author).withProperties(properties).withLabel(title).withInstalled(installed)
|
||||||
.withMaturity(maturity).withLink(link).build();
|
.withMaturity(maturity).withLink(link).build();
|
||||||
}
|
}
|
||||||
|
@ -399,16 +327,16 @@ public class CommunityMarketplaceAddonService implements AddonService {
|
||||||
String id = ADDON_ID_PREFIX + topic.id.toString();
|
String id = ADDON_ID_PREFIX + topic.id.toString();
|
||||||
List<String> tags = Arrays.asList(Objects.requireNonNullElse(topic.tags, new String[0]));
|
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 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 views = topic.views;
|
||||||
int postsCount = topic.posts_count;
|
int postsCount = topic.postsCount;
|
||||||
Date createdDate = topic.post_stream.posts[0].created_at;
|
Date createdDate = topic.postStream.posts[0].createdAt;
|
||||||
Date updatedDate = topic.post_stream.posts[0].updated_at;
|
Date updatedDate = topic.postStream.posts[0].updatedAt;
|
||||||
Date lastPostedDate = topic.last_posted;
|
Date lastPostedDate = topic.lastPosted;
|
||||||
|
|
||||||
String maturity = tags.stream().filter(CODE_MATURITY_LEVELS::contains).findAny().orElse(null);
|
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("posts_count", postsCount);
|
||||||
properties.put("tags", tags.toArray(String[]::new));
|
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
|
// try to extract contents or links
|
||||||
if (topic.post_stream.posts[0].link_counts != null) {
|
if (topic.postStream.posts[0].linkCounts != null) {
|
||||||
for (DiscoursePostLink postLink : topic.post_stream.posts[0].link_counts) {
|
for (DiscoursePostLink postLink : topic.postStream.posts[0].linkCounts) {
|
||||||
if (postLink.url.endsWith(".jar")) {
|
if (postLink.url.endsWith(".jar")) {
|
||||||
properties.put("jar_download_url", postLink.url);
|
properties.put("jar_download_url", postLink.url);
|
||||||
}
|
}
|
||||||
|
@ -453,43 +381,14 @@ public class CommunityMarketplaceAddonService implements AddonService {
|
||||||
properties.put("yaml_content", unescapeEntities(yamlContent));
|
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()
|
boolean installed = addonHandlers.stream()
|
||||||
.anyMatch(handler -> handler.supports(type, contentType) && handler.isInstalled(id));
|
.anyMatch(handler -> handler.supports(type, contentType) && handler.isInstalled(id));
|
||||||
|
|
||||||
return Addon.create(id).withType(type).withContentType(contentType).withLabel(topic.title)
|
return Addon.create(id).withType(type).withContentType(contentType).withLabel(topic.title)
|
||||||
.withImageLink(topic.image_url).withLink(COMMUNITY_TOPIC_URL + topic.id.toString())
|
.withImageLink(topic.imageUrl).withLink(COMMUNITY_TOPIC_URL + topic.id.toString())
|
||||||
.withAuthor(topic.post_stream.posts[0].display_username).withMaturity(maturity)
|
.withAuthor(topic.postStream.posts[0].displayUsername).withMaturity(maturity)
|
||||||
.withDetailedDescription(detailedDescription).withInstalled(installed).withProperties(properties)
|
.withDetailedDescription(detailedDescription).withInstalled(installed).withProperties(properties)
|
||||||
.build();
|
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ package org.openhab.core.addon.marketplace.internal.community.model;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DTO class mapped to the Discourse category topic list API.
|
* A DTO class mapped to the Discourse category topic list API.
|
||||||
*
|
*
|
||||||
|
@ -21,38 +23,48 @@ import java.util.Date;
|
||||||
*/
|
*/
|
||||||
public class DiscourseCategoryResponseDTO {
|
public class DiscourseCategoryResponseDTO {
|
||||||
public DiscourseUser[] users;
|
public DiscourseUser[] users;
|
||||||
public DiscourseTopicList topic_list;
|
@SerializedName("topic_list")
|
||||||
|
public DiscourseTopicList topicList;
|
||||||
|
|
||||||
public class DiscourseUser {
|
public static class DiscourseUser {
|
||||||
public Integer id;
|
public Integer id;
|
||||||
public String username;
|
public String username;
|
||||||
public String name;
|
public String name;
|
||||||
public String avatar_template;
|
@SerializedName("avatar_template")
|
||||||
|
public String avatarTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DiscourseTopicList {
|
public static class DiscourseTopicList {
|
||||||
public String more_topics_url;
|
@SerializedName("more_topics_url")
|
||||||
public Integer per_page;
|
public String moreTopicsUrl;
|
||||||
|
@SerializedName("per_page")
|
||||||
|
public Integer perPage;
|
||||||
public DiscourseTopicItem[] topics;
|
public DiscourseTopicItem[] topics;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DiscoursePosterInfo {
|
public static class DiscoursePosterInfo {
|
||||||
public String extras;
|
public String extras;
|
||||||
public String description;
|
public String description;
|
||||||
public Integer user_id;
|
@SerializedName("user_id")
|
||||||
|
public Integer userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DiscourseTopicItem {
|
public static class DiscourseTopicItem {
|
||||||
public Integer id;
|
public Integer id;
|
||||||
public String title;
|
public String title;
|
||||||
public String slug;
|
public String slug;
|
||||||
public String[] tags;
|
public String[] tags;
|
||||||
public Integer posts_count;
|
@SerializedName("posts_count")
|
||||||
public String image_url;
|
public Integer postsCount;
|
||||||
public Date created_at;
|
@SerializedName("image_url")
|
||||||
public Integer like_count;
|
public String imageUrl;
|
||||||
|
@SerializedName("created_at")
|
||||||
|
public Date createdAt;
|
||||||
|
@SerializedName("like_count")
|
||||||
|
public Integer likeCount;
|
||||||
public Integer views;
|
public Integer views;
|
||||||
public Integer category_id;
|
@SerializedName("category_id")
|
||||||
|
public Integer categoryId;
|
||||||
public DiscoursePosterInfo[] posters;
|
public DiscoursePosterInfo[] posters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ package org.openhab.core.addon.marketplace.internal.community.model;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DTO class mapped to the Discourse topic API.
|
* A DTO class mapped to the Discourse topic API.
|
||||||
*
|
*
|
||||||
|
@ -22,57 +24,72 @@ import java.util.Date;
|
||||||
public class DiscourseTopicResponseDTO {
|
public class DiscourseTopicResponseDTO {
|
||||||
public Integer id;
|
public Integer id;
|
||||||
|
|
||||||
public DiscoursePostStream post_stream;
|
@SerializedName("post_stream")
|
||||||
|
public DiscoursePostStream postStream;
|
||||||
|
|
||||||
public String title;
|
public String title;
|
||||||
public Integer posts_count;
|
@SerializedName("posts_count")
|
||||||
public String image_url;
|
public Integer postsCount;
|
||||||
|
@SerializedName("image_url")
|
||||||
|
public String imageUrl;
|
||||||
|
|
||||||
public Date created_at;
|
@SerializedName("created_at")
|
||||||
public Date updated_at;
|
public Date createdAt;
|
||||||
public Date last_posted;
|
@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 Integer views;
|
||||||
|
|
||||||
public String[] tags;
|
public String[] tags;
|
||||||
public Integer category_id;
|
@SerializedName("category_id")
|
||||||
|
public Integer categoryId;
|
||||||
|
|
||||||
public DiscourseTopicDetails details;
|
public DiscourseTopicDetails details;
|
||||||
|
|
||||||
public class DiscoursePostAuthor {
|
public static class DiscoursePostAuthor {
|
||||||
public Integer id;
|
public Integer id;
|
||||||
public String username;
|
public String username;
|
||||||
public String avatar_template;
|
@SerializedName("avatar_template")
|
||||||
|
public String avatarTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DiscoursePostLink {
|
public static class DiscoursePostLink {
|
||||||
public String url;
|
public String url;
|
||||||
public Boolean internal;
|
public Boolean internal;
|
||||||
public Integer clicks;
|
public Integer clicks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DiscoursePostStream {
|
public static class DiscoursePostStream {
|
||||||
public DiscoursePost[] posts;
|
public DiscoursePost[] posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DiscoursePost {
|
public static class DiscoursePost {
|
||||||
public Integer id;
|
public Integer id;
|
||||||
|
|
||||||
public String username;
|
public String username;
|
||||||
public String display_username;
|
@SerializedName("display_username")
|
||||||
|
public String displayUsername;
|
||||||
|
|
||||||
public Date created_at;
|
@SerializedName("created_at")
|
||||||
public Date updated_at;
|
public Date createdAt;
|
||||||
|
@SerializedName("updated_at")
|
||||||
|
public Date updatedAt;
|
||||||
|
|
||||||
public String cooked;
|
public String cooked;
|
||||||
|
|
||||||
public DiscoursePostLink[] link_counts;
|
@SerializedName("link_counts")
|
||||||
|
public DiscoursePostLink[] linkCounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DiscourseTopicDetails {
|
public static class DiscourseTopicDetails {
|
||||||
public DiscoursePostAuthor created_by;
|
@SerializedName("created_by")
|
||||||
public DiscoursePostAuthor last_poster;
|
public DiscoursePostAuthor createdBy;
|
||||||
|
@SerializedName("last_poster")
|
||||||
|
public DiscoursePostAuthor lastPoster;
|
||||||
|
|
||||||
public DiscoursePostLink[] links;
|
public DiscoursePostLink[] links;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,33 +19,25 @@ import java.lang.reflect.Type;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Dictionary;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.core.addon.Addon;
|
import org.openhab.core.addon.Addon;
|
||||||
import org.openhab.core.addon.AddonEventFactory;
|
|
||||||
import org.openhab.core.addon.AddonService;
|
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.MarketplaceAddonHandler;
|
||||||
import org.openhab.core.addon.marketplace.MarketplaceHandlerException;
|
|
||||||
import org.openhab.core.addon.marketplace.internal.json.model.AddonEntryDTO;
|
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.config.core.ConfigurableService;
|
||||||
import org.openhab.core.events.Event;
|
|
||||||
import org.openhab.core.events.EventPublisher;
|
import org.openhab.core.events.EventPublisher;
|
||||||
|
import org.openhab.core.storage.StorageService;
|
||||||
import org.osgi.framework.Constants;
|
import org.osgi.framework.Constants;
|
||||||
import org.osgi.service.cm.Configuration;
|
|
||||||
import org.osgi.service.cm.ConfigurationAdmin;
|
import org.osgi.service.cm.ConfigurationAdmin;
|
||||||
import org.osgi.service.component.annotations.Activate;
|
import org.osgi.service.component.annotations.Activate;
|
||||||
import org.osgi.service.component.annotations.Component;
|
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.Reference;
|
||||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||||
import org.osgi.service.component.annotations.ReferencePolicy;
|
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;
|
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 Yannick Schaus - Initial contribution
|
||||||
* @author Jan N. Klug - Refactored for JSON marketplaces
|
* @author Jan N. Klug - Refactored for JSON marketplaces
|
||||||
*/
|
*/
|
||||||
@Component(immediate = true, configurationPid = { "org.openhab.jsonaddonservice" }, //
|
@Component(immediate = true, configurationPid = JsonAddonService.SERVICE_PID, //
|
||||||
property = Constants.SERVICE_PID + "=org.openhab.jsonaddonservice")
|
property = Constants.SERVICE_PID + "=" + JsonAddonService.SERVICE_PID, service = AddonService.class)
|
||||||
@ConfigurableService(category = "system", label = JsonAddonService.SERVICE_NAME, description_uri = JsonAddonService.CONFIG_URI)
|
@ConfigurableService(category = "system", label = JsonAddonService.SERVICE_NAME, description_uri = JsonAddonService.CONFIG_URI)
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class JsonAddonService implements AddonService {
|
public class JsonAddonService extends AbstractRemoteAddonService {
|
||||||
private final Logger logger = LoggerFactory.getLogger(JsonAddonService.class);
|
|
||||||
|
|
||||||
static final String SERVICE_NAME = "Json 3rd Party Add-on Service";
|
static final String SERVICE_NAME = "Json 3rd Party Add-on Service";
|
||||||
static final String CONFIG_URI = "system:jsonaddonservice";
|
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 SERVICE_ID = "json";
|
||||||
private static final String ADDON_ID_PREFIX = SERVICE_ID + ":";
|
private static final String ADDON_ID_PREFIX = SERVICE_ID + ":";
|
||||||
|
|
||||||
private static final String CONFIG_URLS = "urls";
|
private static final String CONFIG_URLS = "urls";
|
||||||
private static final String CONFIG_SHOW_UNSTABLE = "showUnstable";
|
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 boolean showUnstable = false;
|
||||||
|
|
||||||
private final EventPublisher eventPublisher;
|
|
||||||
private final ConfigurationAdmin configurationAdmin;
|
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
public JsonAddonService(@Reference EventPublisher eventPublisher, @Reference ConfigurationAdmin configurationAdmin,
|
public JsonAddonService(@Reference EventPublisher eventPublisher, @Reference StorageService storageService,
|
||||||
Map<String, Object> config) {
|
@Reference ConfigurationAdmin configurationAdmin, Map<String, Object> config) {
|
||||||
this.eventPublisher = eventPublisher;
|
super(eventPublisher, configurationAdmin, storageService, SERVICE_PID);
|
||||||
this.configurationAdmin = configurationAdmin;
|
|
||||||
modified(config);
|
modified(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Modified
|
@Modified
|
||||||
public void modified(Map<String, Object> config) {
|
public void modified(@Nullable Map<String, Object> config) {
|
||||||
String urls = Objects.requireNonNullElse((String) config.get(CONFIG_URLS), "");
|
if (config != null) {
|
||||||
addonserviceUrls = Arrays.asList(urls.split("\\|"));
|
String urls = Objects.requireNonNullElse((String) config.get(CONFIG_URLS), "");
|
||||||
showUnstable = (Boolean) config.getOrDefault(CONFIG_SHOW_UNSTABLE, false);
|
addonServiceUrls = Arrays.asList(urls.split("\\|"));
|
||||||
refreshSource();
|
showUnstable = (Boolean) config.getOrDefault(CONFIG_SHOW_UNSTABLE, false);
|
||||||
|
cachedRemoteAddons.invalidateValue();
|
||||||
|
refreshSource();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
|
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
|
||||||
protected void addAddonHandler(MarketplaceAddonHandler handler) {
|
protected void addAddonHandler(MarketplaceAddonHandler handler) {
|
||||||
this.addonHandlers.add(handler);
|
this.addonHandlers.add(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void removeAddonHandler(MarketplaceAddonHandler handler) {
|
protected void removeAddonHandler(MarketplaceAddonHandler handler) {
|
||||||
this.addonHandlers.remove(handler);
|
this.addonHandlers.remove(handler);
|
||||||
}
|
}
|
||||||
|
@ -140,13 +113,8 @@ public class JsonAddonService implements AddonService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void refreshSource() {
|
protected List<Addon> getRemoteAddons() {
|
||||||
if (!remoteEnabled()) {
|
return addonServiceUrls.stream().map(urlString -> {
|
||||||
cachedAddons = List.of();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedAddons = (List<AddonEntryDTO>) addonserviceUrls.stream().map(urlString -> {
|
|
||||||
try {
|
try {
|
||||||
URL url = new URL(urlString);
|
URL url = new URL(urlString);
|
||||||
URLConnection connection = url.openConnection();
|
URLConnection connection = url.openConnection();
|
||||||
|
@ -158,72 +126,15 @@ public class JsonAddonService implements AddonService {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return List.of();
|
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());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Addon> getAddons(@Nullable Locale locale) {
|
|
||||||
refreshSource();
|
|
||||||
return cachedAddons.stream().map(this::fromAddonEntry).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Addon getAddon(String id, @Nullable Locale locale) {
|
public @Nullable Addon getAddon(String id, @Nullable Locale locale) {
|
||||||
String remoteId = id.replace(ADDON_ID_PREFIX, "");
|
String fullId = ADDON_ID_PREFIX + id;
|
||||||
return cachedAddons.stream().filter(e -> remoteId.equals(e.id)).map(this::fromAddonEntry).findAny()
|
return cachedAddons.stream().filter(e -> fullId.equals(e.getId())).findAny().orElse(null);
|
||||||
.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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -251,35 +162,6 @@ public class JsonAddonService implements AddonService {
|
||||||
.withDetailedDescription(addonEntry.description).withContentType(addonEntry.contentType)
|
.withDetailedDescription(addonEntry.description).withContentType(addonEntry.contentType)
|
||||||
.withAuthor(addonEntry.author).withVersion(addonEntry.version).withLabel(addonEntry.title)
|
.withAuthor(addonEntry.author).withVersion(addonEntry.version).withLabel(addonEntry.title)
|
||||||
.withMaturity(addonEntry.maturity).withProperties(properties).withLink(addonEntry.link)
|
.withMaturity(addonEntry.maturity).withProperties(properties).withLink(addonEntry.link)
|
||||||
.withConfigDescriptionURI(addonEntry.configDescriptionURI).build();
|
.withImageLink(addonEntry.imageUrl).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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,5 +31,7 @@ public class AddonEntryDTO {
|
||||||
public String maturity = "unstable";
|
public String maturity = "unstable";
|
||||||
@SerializedName("content_type")
|
@SerializedName("content_type")
|
||||||
public String contentType = "";
|
public String contentType = "";
|
||||||
|
@SerializedName("image_url")
|
||||||
|
public String imageUrl;
|
||||||
public String url = "";
|
public String url = "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue