[addonservices] Add version filtering (#2811)
Signed-off-by: Jan N. Klug <github@klug.nrw>pull/2861/head
parent
c2ba4dcd16
commit
4577562f08
|
@ -27,6 +27,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.openhab.core.addon.Addon;
|
||||
import org.openhab.core.addon.AddonEventFactory;
|
||||
import org.openhab.core.addon.AddonService;
|
||||
|
@ -37,6 +38,7 @@ 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.framework.FrameworkUtil;
|
||||
import org.osgi.service.cm.Configuration;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -52,6 +54,9 @@ import com.google.gson.GsonBuilder;
|
|||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractRemoteAddonService implements AddonService {
|
||||
static final String CONFIG_REMOTE_ENABLED = "remote";
|
||||
static final String CONFIG_INCLUDE_INCOMPATIBLE = "includeIncompatible";
|
||||
|
||||
protected static final Map<String, AddonType> TAG_ADDON_TYPE_MAP = Map.of( //
|
||||
"automation", new AddonType("automation", "Automation"), //
|
||||
"binding", new AddonType("binding", "Bindings"), //
|
||||
|
@ -61,6 +66,8 @@ public abstract class AbstractRemoteAddonService implements AddonService {
|
|||
"ui", new AddonType("ui", "User Interfaces"), //
|
||||
"voice", new AddonType("voice", "Voice"));
|
||||
|
||||
protected final BundleVersion coreVersion;
|
||||
|
||||
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;
|
||||
|
@ -78,6 +85,11 @@ public abstract class AbstractRemoteAddonService implements AddonService {
|
|||
this.eventPublisher = eventPublisher;
|
||||
this.configurationAdmin = configurationAdmin;
|
||||
this.installedAddonStorage = storageService.getStorage(servicePid);
|
||||
this.coreVersion = getCoreVersion();
|
||||
}
|
||||
|
||||
protected BundleVersion getCoreVersion() {
|
||||
return new BundleVersion(FrameworkUtil.getBundle(OpenHAB.class).getVersion().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -102,6 +114,10 @@ public abstract class AbstractRemoteAddonService implements AddonService {
|
|||
// check real installation status based on handlers
|
||||
addons.forEach(addon -> addon.setInstalled(addonHandlers.stream().anyMatch(h -> h.isInstalled(addon.getId()))));
|
||||
|
||||
// remove incompatible add-ons if not enabled
|
||||
boolean showIncompatible = includeIncompatible();
|
||||
addons.removeIf(addon -> !addon.getCompatible() && !showIncompatible);
|
||||
|
||||
cachedAddons = addons;
|
||||
this.installedAddons = installedAddons;
|
||||
}
|
||||
|
@ -216,12 +232,26 @@ public abstract class AbstractRemoteAddonService implements AddonService {
|
|||
// 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);
|
||||
return ConfigParser.valueAsOrElse(properties.get(CONFIG_REMOTE_ENABLED), Boolean.class, true);
|
||||
} catch (IOException e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean includeIncompatible() {
|
||||
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 false (default is show compatible only)
|
||||
return true;
|
||||
}
|
||||
return ConfigParser.valueAsOrElse(properties.get(CONFIG_INCLUDE_INCOMPATIBLE), Boolean.class, false);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void postInstalledEvent(String extensionId) {
|
||||
Event event = AddonEventFactory.createAddonInstalledEvent(extensionId);
|
||||
eventPublisher.post(event);
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* 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.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link BundleVersion} wraps a bundle version and provides a method to compare them
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BundleVersion {
|
||||
private static final Pattern VERSION_PATTERN = Pattern.compile(
|
||||
"(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<micro>\\d+)(\\.((?<rc>RC)|(?<milestone>M))?(?<qualifier>\\d+))?");
|
||||
public static final Pattern RANGE_PATTERN = Pattern.compile(
|
||||
"\\[(?<start>\\d+\\.\\d+(?<startmicro>\\.\\d+(\\.\\w+)?)?);(?<end>\\d+\\.\\d+(?<endmicro>\\.\\d+(\\.\\w+)?)?)(?<endtype>[)\\]])");
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(BundleVersion.class);
|
||||
|
||||
private final int major;
|
||||
private final int minor;
|
||||
private final int micro;
|
||||
private final @Nullable Long qualifier;
|
||||
|
||||
public BundleVersion(String version) {
|
||||
Matcher matcher = VERSION_PATTERN.matcher(version);
|
||||
if (matcher.matches()) {
|
||||
this.major = Integer.parseInt(matcher.group("major"));
|
||||
this.minor = Integer.parseInt(matcher.group("minor"));
|
||||
this.micro = Integer.parseInt(matcher.group("micro"));
|
||||
String qualifier = matcher.group("qualifier");
|
||||
if (qualifier != null) {
|
||||
long intQualifier = Long.parseLong(qualifier);
|
||||
if (matcher.group("rc") != null) {
|
||||
// we can safely assume that there are less than Integer.MAX_VALUE milestones
|
||||
// so RCs are always newer than milestones
|
||||
// since snapshot qualifiers are larger than 10*Integer.MAX_VALUE they are
|
||||
// still considered newer
|
||||
this.qualifier = intQualifier + Integer.MAX_VALUE;
|
||||
} else {
|
||||
this.qualifier = intQualifier;
|
||||
}
|
||||
} else {
|
||||
this.qualifier = null;
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Input does not match pattern");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if this version is within the provided range
|
||||
*
|
||||
* @param range a Maven like version range
|
||||
* @return {@code true} if this version is inside range, {@code false} otherwise
|
||||
* @throws IllegalArgumentException if {@code range} does not represent a valid range
|
||||
*/
|
||||
public boolean inRange(@Nullable String range) throws IllegalArgumentException {
|
||||
if (range == null || range.isBlank()) {
|
||||
// if no range is given, we assume the range covers everything
|
||||
return true;
|
||||
}
|
||||
Matcher matcher = RANGE_PATTERN.matcher(range);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException(range + "is not a valid version range");
|
||||
}
|
||||
String startString = matcher.group("startmicro") != null ? matcher.group("start")
|
||||
: matcher.group("start") + ".0";
|
||||
BundleVersion startVersion = new BundleVersion(startString);
|
||||
if (this.compareTo(startVersion) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String endString = matcher.group("endmicro") != null ? matcher.group("end") : matcher.group("stop") + ".0";
|
||||
boolean inclusive = "]".equals(matcher.group("endtype"));
|
||||
BundleVersion endVersion = new BundleVersion(endString);
|
||||
int comparison = this.compareTo(endVersion);
|
||||
return (inclusive && comparison == 0) || comparison < 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
BundleVersion version = (BundleVersion) o;
|
||||
return major == version.major && minor == version.minor && micro == version.micro
|
||||
&& Objects.equals(qualifier, version.qualifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(major, minor, micro, qualifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two bundle versions
|
||||
*
|
||||
* @param other the other bundle version
|
||||
* @return a positive integer if this version is newer than the other version, a negative number if this version is
|
||||
* older than the other version and 0 if the versions are equal
|
||||
*/
|
||||
public int compareTo(BundleVersion other) {
|
||||
int result = major - other.major;
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = minor - other.minor;
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = micro - other.micro;
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (Objects.equals(qualifier, other.qualifier)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// the release is always newer than a milestone or snapshot
|
||||
if (qualifier == null) { // we are the release
|
||||
return 1;
|
||||
}
|
||||
if (other.qualifier == null) { // the other is the release
|
||||
return -1;
|
||||
}
|
||||
|
||||
// both versions are milestones, we can compare them
|
||||
return Long.compare(qualifier, other.qualifier);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
@ -36,6 +37,7 @@ import org.openhab.core.addon.Addon;
|
|||
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.BundleVersion;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
|
||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO;
|
||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO.DiscoursePosterInfo;
|
||||
|
@ -43,6 +45,7 @@ import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCate
|
|||
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.EventPublisher;
|
||||
import org.openhab.core.storage.StorageService;
|
||||
|
@ -80,6 +83,7 @@ public class CommunityMarketplaceAddonService extends AbstractRemoteAddonService
|
|||
static final String CONFIG_URI = "system:marketplace";
|
||||
static final String CONFIG_API_KEY = "apiKey";
|
||||
static final String CONFIG_SHOW_UNPUBLISHED_ENTRIES_KEY = "showUnpublished";
|
||||
static final String CONFIG_ENABLED_KEY = "enabled";
|
||||
|
||||
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";
|
||||
|
@ -103,6 +107,7 @@ public class CommunityMarketplaceAddonService extends AbstractRemoteAddonService
|
|||
|
||||
private @Nullable String apiKey = null;
|
||||
private boolean showUnpublished = false;
|
||||
private boolean enabled = true;
|
||||
|
||||
@Activate
|
||||
public CommunityMarketplaceAddonService(final @Reference EventPublisher eventPublisher,
|
||||
|
@ -116,9 +121,9 @@ public class CommunityMarketplaceAddonService extends AbstractRemoteAddonService
|
|||
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());
|
||||
this.showUnpublished = ConfigParser.valueAsOrElse(config.get(CONFIG_SHOW_UNPUBLISHED_ENTRIES_KEY),
|
||||
Boolean.class, false);
|
||||
this.enabled = ConfigParser.valueAsOrElse(config.get(CONFIG_ENABLED_KEY), Boolean.class, true);
|
||||
cachedRemoteAddons.invalidateValue();
|
||||
refreshSource();
|
||||
}
|
||||
|
@ -147,6 +152,10 @@ public class CommunityMarketplaceAddonService extends AbstractRemoteAddonService
|
|||
|
||||
@Override
|
||||
protected List<Addon> getRemoteAddons() {
|
||||
if (!enabled) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<Addon> addons = new ArrayList<>();
|
||||
try {
|
||||
List<DiscourseCategoryResponseDTO> pages = new ArrayList<>();
|
||||
|
@ -277,6 +286,23 @@ public class CommunityMarketplaceAddonService extends AbstractRemoteAddonService
|
|||
String contentType = getContentType(topic.categoryId, tags);
|
||||
|
||||
String title = topic.title;
|
||||
boolean compatible = true;
|
||||
|
||||
int compatibilityStart = topic.title.lastIndexOf("["); // version range always starts with [
|
||||
if (topic.title.lastIndexOf(" ") < compatibilityStart) { // check includes [ not present
|
||||
String potentialRange = topic.title.substring(compatibilityStart + 1);
|
||||
Matcher matcher = BundleVersion.RANGE_PATTERN.matcher(potentialRange);
|
||||
if (matcher.matches()) {
|
||||
try {
|
||||
compatible = coreVersion.inRange(potentialRange);
|
||||
title = topic.title.substring(0, compatibilityStart).trim();
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.debug("Failed to determine compatibility for addon {}: {}", topic.title, e.getMessage());
|
||||
compatible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String link = COMMUNITY_TOPIC_URL + topic.id.toString();
|
||||
int likeCount = topic.likeCount;
|
||||
int views = topic.views;
|
||||
|
@ -303,7 +329,7 @@ public class CommunityMarketplaceAddonService extends AbstractRemoteAddonService
|
|||
|
||||
return Addon.create(id).withType(type).withContentType(contentType).withImageLink(topic.imageUrl)
|
||||
.withAuthor(author).withProperties(properties).withLabel(title).withInstalled(installed)
|
||||
.withMaturity(maturity).withLink(link).build();
|
||||
.withMaturity(maturity).withCompatible(compatible).withLink(link).build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -45,6 +45,8 @@ 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.reflect.TypeToken;
|
||||
|
||||
|
@ -69,6 +71,8 @@ public class JsonAddonService extends AbstractRemoteAddonService {
|
|||
private static final String CONFIG_URLS = "urls";
|
||||
private static final String CONFIG_SHOW_UNSTABLE = "showUnstable";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(JsonAddonService.class);
|
||||
|
||||
private List<String> addonServiceUrls = List.of();
|
||||
private boolean showUnstable = false;
|
||||
|
||||
|
@ -158,11 +162,19 @@ public class JsonAddonService extends AbstractRemoteAddonService {
|
|||
properties.put("yaml_download_url", addonEntry.url);
|
||||
}
|
||||
|
||||
boolean compatible = true;
|
||||
try {
|
||||
compatible = coreVersion.inRange(addonEntry.compatibleVersions);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.debug("Failed to determine compatibility for addon {}: {}", addonEntry.id, e.getMessage());
|
||||
}
|
||||
|
||||
return Addon.create(fullId).withType(addonEntry.type).withInstalled(installed)
|
||||
.withDetailedDescription(addonEntry.description).withContentType(addonEntry.contentType)
|
||||
.withAuthor(addonEntry.author).withVersion(addonEntry.version).withLabel(addonEntry.title)
|
||||
.withMaturity(addonEntry.maturity).withProperties(properties).withLink(addonEntry.link)
|
||||
.withImageLink(addonEntry.imageUrl).withConfigDescriptionURI(addonEntry.configDescriptionURI)
|
||||
.withLoggerPackages(addonEntry.loggerPackages).build();
|
||||
.withCompatible(compatible).withMaturity(addonEntry.maturity).withProperties(properties)
|
||||
.withLink(addonEntry.link).withImageLink(addonEntry.imageUrl)
|
||||
.withConfigDescriptionURI(addonEntry.configDescriptionURI).withLoggerPackages(addonEntry.loggerPackages)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ public class AddonEntryDTO {
|
|||
public String title = "";
|
||||
public String link = "";
|
||||
public String version = "";
|
||||
@SerializedName("compatible_versions")
|
||||
public String compatibleVersions = "";
|
||||
public String author = "";
|
||||
public String configDescriptionURI = "";
|
||||
public String maturity = "unstable";
|
||||
|
|
|
@ -6,6 +6,12 @@
|
|||
https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="system:marketplace">
|
||||
<parameter name="enable" type="boolean">
|
||||
<label>Enable Community Marketplace</label>
|
||||
<default>true</default>
|
||||
<description>If set to false no add-ons from the community marketplace will be shown. Already installed add-ons will
|
||||
still be available.</description>
|
||||
</parameter>
|
||||
<parameter name="showUnpublished" type="boolean">
|
||||
<label>Show Unpublished Entries</label>
|
||||
<default>false</default>
|
||||
|
@ -29,9 +35,9 @@
|
|||
thus harm your system.</description>
|
||||
</parameter>
|
||||
<parameter name="showUnstable" type="boolean">
|
||||
<label>Show Non-Stable Bundles</label>
|
||||
<label>Show Non-Stable Add-ons</label>
|
||||
<default>false</default>
|
||||
<description>Include entries which have not been tagged as "stable". These bundles should be used for testing
|
||||
<description>Include entries which have not been tagged as "stable". These add-ons should be used for testing
|
||||
purposes only and are not considered production-system ready.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
system.config.jsonaddonservice.showUnstable.label = Show Non-Stable Bundles
|
||||
system.config.jsonaddonservice.showUnstable.description = Include entries which have not been tagged as "stable". These bundles should be used for testing purposes only and are not considered production-system ready.
|
||||
system.config.jsonaddonservice.showUnstable.label = Show Non-Stable Add-ons
|
||||
system.config.jsonaddonservice.showUnstable.description = Include entries which have not been tagged as "stable". These add-ons should be used for testing purposes only and are not considered production-system ready.
|
||||
system.config.jsonaddonservice.urls.label = Add-on Service URLs
|
||||
system.config.jsonaddonservice.urls.description = Pipe (|) separated list of URLS that provide 3rd party add-on services via Json files. Warning: Bundles distributed over 3rd party add-on services may lack proper review and can potentially contain malicious code and thus harm your system.
|
||||
system.config.marketplace.apiKey.label = API Key for community.openhab.org
|
||||
|
|
|
@ -10,21 +10,28 @@
|
|||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.test;
|
||||
package org.openhab.core.addon.marketplace;
|
||||
|
||||
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 static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.openhab.core.addon.marketplace.AbstractRemoteAddonService.CONFIG_REMOTE_ENABLED;
|
||||
import static org.openhab.core.addon.marketplace.test.TestAddonService.ALL_ADDON_COUNT;
|
||||
import static org.openhab.core.addon.marketplace.test.TestAddonService.COMPATIBLE_ADDON_COUNT;
|
||||
import static org.openhab.core.addon.marketplace.test.TestAddonService.INSTALL_EXCEPTION_ADDON;
|
||||
import static org.openhab.core.addon.marketplace.test.TestAddonService.SERVICE_PID;
|
||||
import static org.openhab.core.addon.marketplace.test.TestAddonService.TEST_ADDON;
|
||||
import static org.openhab.core.addon.marketplace.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;
|
||||
|
@ -35,6 +42,8 @@ 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.addon.marketplace.test.TestAddonHandler;
|
||||
import org.openhab.core.addon.marketplace.test.TestAddonService;
|
||||
import org.openhab.core.events.Event;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.openhab.core.storage.Storage;
|
||||
|
@ -87,24 +96,24 @@ public class AbstractRemoteAddonServiceTest {
|
|||
addonService.addAddonHandler(addonHandler);
|
||||
|
||||
List<Addon> addons = addonService.getAddons(null);
|
||||
Assertions.assertEquals(0, addons.size());
|
||||
assertThat(addons, empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoteDisabledBlocksRemoteCalls() {
|
||||
properties.put("remote", false);
|
||||
List<Addon> addons = addonService.getAddons(null);
|
||||
Assertions.assertEquals(0, addons.size());
|
||||
Assertions.assertEquals(0, addonService.getRemoteCalls());
|
||||
assertThat(addons, empty());
|
||||
assertThat(addonService.getRemoteCalls(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddonResultsAreCached() {
|
||||
List<Addon> addons = addonService.getAddons(null);
|
||||
Assertions.assertEquals(TestAddonService.REMOTE_ADDONS.size(), addons.size());
|
||||
assertThat(addons, hasSize(COMPATIBLE_ADDON_COUNT));
|
||||
addons = addonService.getAddons(null);
|
||||
Assertions.assertEquals(TestAddonService.REMOTE_ADDONS.size(), addons.size());
|
||||
Assertions.assertEquals(1, addonService.getRemoteCalls());
|
||||
assertThat(addons, hasSize(COMPATIBLE_ADDON_COUNT));
|
||||
assertThat(addonService.getRemoteCalls(), is(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -113,8 +122,8 @@ public class AbstractRemoteAddonServiceTest {
|
|||
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());
|
||||
assertThat(addon, notNullValue());
|
||||
assertThat(addon.isInstalled(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -124,15 +133,26 @@ public class AbstractRemoteAddonServiceTest {
|
|||
|
||||
// check all addons are present
|
||||
List<Addon> addons = addonService.getAddons(null);
|
||||
Assertions.assertEquals(TestAddonService.REMOTE_ADDONS.size(), addons.size());
|
||||
assertThat(addons, hasSize(COMPATIBLE_ADDON_COUNT));
|
||||
|
||||
// disable remote repo
|
||||
properties.put("remote", false);
|
||||
properties.put(CONFIG_REMOTE_ENABLED, 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());
|
||||
assertThat(addons, hasSize(1));
|
||||
assertThat(addons.get(0).getId(), is(getFullAddonId(TEST_ADDON)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncompatibleAddonsNotIncludedByDefault() {
|
||||
assertThat(addonService.getAddons(null), hasSize(COMPATIBLE_ADDON_COUNT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncompatibleAddonsAreIncludedIfRequested() {
|
||||
properties.put("includeIncompatible", true);
|
||||
assertThat(addonService.getAddons(null), hasSize(ALL_ADDON_COUNT));
|
||||
}
|
||||
|
||||
// installation tests
|
||||
|
@ -242,22 +262,21 @@ public class AbstractRemoteAddonServiceTest {
|
|||
Event event = eventCaptor.getValue();
|
||||
String topic = "openhab/addons/" + expectedEventTopic;
|
||||
|
||||
Assertions.assertEquals(topic, event.getTopic());
|
||||
assertThat(event.getTopic(), is(topic));
|
||||
|
||||
// assert addon handler was called (by checking it's installed status)
|
||||
Assertions.assertEquals(installStatus, addonHandler.isInstalled(getFullAddonId(id)));
|
||||
assertThat(addonHandler.isInstalled(getFullAddonId(id)), is(installStatus));
|
||||
|
||||
// assert is present in storage if installed or missing if uninstalled
|
||||
Assertions.assertEquals(installStatus, storage.containsKey(id));
|
||||
assertThat(storage.containsKey(id), is(installStatus));
|
||||
|
||||
// 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());
|
||||
assertThat(addon, notNullValue());
|
||||
assertThat(addon.isInstalled(), is(installStatus));
|
||||
} else {
|
||||
Assertions.assertNull(addon);
|
||||
assertThat(addon, nullValue());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* 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 static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
/**
|
||||
* The {@link BundleVersionTest} contains tests for the {@link BundleVersion} class
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.WARN)
|
||||
public class BundleVersionTest {
|
||||
|
||||
private static Stream<Arguments> provideCompareVersionsArguments() {
|
||||
return Stream.of( //
|
||||
Arguments.of("3.1.0", "3.1.0", Result.EQUAL), // same versions are equal
|
||||
Arguments.of("3.1.0", "3.0.2", Result.NEWER), // minor version is more important than micro
|
||||
Arguments.of("3.7.0", "4.0.1.202105311711", Result.OLDER), // major version is more important than minor
|
||||
Arguments.of("3.9.1.M1", "3.9.0.M5", Result.NEWER), // micro version is more important than qualifier
|
||||
Arguments.of("3.0.0.202105311032", "3.0.0.202106011144", Result.OLDER), // snapshots
|
||||
Arguments.of("3.1.0.M3", "3.1.0.M1", Result.NEWER), // milestones are compared numerically
|
||||
Arguments.of("3.1.0.M1", "3.1.0.197705310021", Result.OLDER), // snapshot is newer than milestone
|
||||
Arguments.of("3.3.0", "3.3.0.202206302115", Result.NEWER), // release is newer than snapshot
|
||||
Arguments.of("3.3.0", "3.3.0.RC1", Result.NEWER), // releases are newer than release candidates
|
||||
Arguments.of("3.3.0.M5", "3.3.0.RC1", Result.OLDER), // milestones are older than release candidates
|
||||
Arguments.of("3.3.0.RC2", "3.3.0.202305201715", Result.OLDER) // snapshots are newer than release
|
||||
// candidates
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIllegalRangeThrowsException() {
|
||||
BundleVersion bundleVersion = new BundleVersion("3.1.0");
|
||||
assertThrows(IllegalArgumentException.class, () -> bundleVersion.inRange("illegal"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideCompareVersionsArguments")
|
||||
public void testCompareVersions(String v1, String v2, Result result) {
|
||||
BundleVersion version1 = new BundleVersion(v1);
|
||||
BundleVersion version2 = new BundleVersion(v2);
|
||||
switch (result) {
|
||||
case OLDER:
|
||||
assertThat(version1.compareTo(version2), lessThan(0));
|
||||
break;
|
||||
case NEWER:
|
||||
assertThat(version1.compareTo(version2), greaterThan(0));
|
||||
break;
|
||||
case EQUAL:
|
||||
assertThat(version1.compareTo(version2), is(0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> provideInRangeArguments() {
|
||||
return Stream.of(Arguments.of("[3.1.0;3.2.1)", true), // in range
|
||||
Arguments.of("[3.1.0;3.2.0)", false), // at end of range, non-inclusive
|
||||
Arguments.of("[3.1.0;3.2.0]", true), // at end of range, inclusive
|
||||
Arguments.of("[3.1.0;3.1.5)", false), // above range
|
||||
Arguments.of("[3.3.0;3.4.0)", false), // below range
|
||||
Arguments.of("", true), // empty range assumes in range
|
||||
Arguments.of(null, true));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideInRangeArguments")
|
||||
public void inRangeTest(@Nullable String range, boolean result) {
|
||||
BundleVersion frameworkVersion = new BundleVersion("3.2.0");
|
||||
assertThat(frameworkVersion.inRange(range), is(result));
|
||||
}
|
||||
|
||||
private enum Result {
|
||||
OLDER,
|
||||
NEWER,
|
||||
EQUAL
|
||||
}
|
||||
}
|
|
@ -10,10 +10,10 @@
|
|||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.test;
|
||||
package org.openhab.core.addon.marketplace.test;
|
||||
|
||||
import static org.openhab.core.addon.test.TestAddonService.INSTALL_EXCEPTION_ADDON;
|
||||
import static org.openhab.core.addon.test.TestAddonService.UNINSTALL_EXCEPTION_ADDON;
|
||||
import static org.openhab.core.addon.marketplace.test.TestAddonService.INSTALL_EXCEPTION_ADDON;
|
||||
import static org.openhab.core.addon.marketplace.test.TestAddonService.UNINSTALL_EXCEPTION_ADDON;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
|
@ -10,7 +10,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.test;
|
||||
package org.openhab.core.addon.marketplace.test;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
@ -22,6 +22,7 @@ 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.BundleVersion;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceHandlerException;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
|
@ -38,10 +39,14 @@ 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 INCOMPATIBLE_VERSION = "incompatibleVersion";
|
||||
|
||||
public static final String SERVICE_PID = "testAddonService";
|
||||
public static final Set<String> REMOTE_ADDONS = Set.of(TEST_ADDON, INSTALL_EXCEPTION_ADDON,
|
||||
UNINSTALL_EXCEPTION_ADDON);
|
||||
UNINSTALL_EXCEPTION_ADDON, INCOMPATIBLE_VERSION);
|
||||
|
||||
public static final int COMPATIBLE_ADDON_COUNT = REMOTE_ADDONS.size() - 1;
|
||||
public static final int ALL_ADDON_COUNT = REMOTE_ADDONS.size();
|
||||
|
||||
private int remoteCalls = 0;
|
||||
|
||||
|
@ -50,6 +55,11 @@ public class TestAddonService extends AbstractRemoteAddonService {
|
|||
super(eventPublisher, configurationAdmin, storageService, SERVICE_PID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BundleVersion getCoreVersion() {
|
||||
return new BundleVersion("3.2.0");
|
||||
}
|
||||
|
||||
public void addAddonHandler(MarketplaceAddonHandler handler) {
|
||||
this.addonHandlers.add(handler);
|
||||
}
|
||||
|
@ -63,7 +73,8 @@ public class TestAddonService extends AbstractRemoteAddonService {
|
|||
remoteCalls++;
|
||||
return REMOTE_ADDONS.stream()
|
||||
.map(id -> Addon.create(SERVICE_PID + ":" + id).withType("binding")
|
||||
.withContentType(TestAddonHandler.TEST_ADDON_CONTENT_TYPE).build())
|
||||
.withContentType(TestAddonHandler.TEST_ADDON_CONTENT_TYPE)
|
||||
.withCompatible(!id.equals(INCOMPATIBLE_VERSION)).build())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ public class Addon {
|
|||
private final String label;
|
||||
private final String version;
|
||||
private final @Nullable String maturity;
|
||||
private boolean compatible;
|
||||
private final String contentType;
|
||||
private final @Nullable String link;
|
||||
private final String author;
|
||||
|
@ -58,6 +59,7 @@ public class Addon {
|
|||
* @param label the label of the add-on
|
||||
* @param version the version of the add-on
|
||||
* @param maturity the maturity level of this version
|
||||
* @param compatible if this add-on is compatible with the current core version
|
||||
* @param contentType the content type of the add-on
|
||||
* @param link the link to find more information about the add-on (may be null)
|
||||
* @param author the author of the add-on
|
||||
|
@ -76,8 +78,8 @@ public class Addon {
|
|||
* @param properties a {@link Map} containing addition information
|
||||
* @param loggerPackages a {@link List} containing the package names belonging to this add-on
|
||||
*/
|
||||
private Addon(String id, String type, String label, String version, @Nullable String maturity, String contentType,
|
||||
@Nullable String link, String author, boolean verifiedAuthor, boolean installed,
|
||||
private Addon(String id, String type, String label, String version, @Nullable String maturity, boolean compatible,
|
||||
String contentType, @Nullable String link, String author, boolean verifiedAuthor, boolean installed,
|
||||
@Nullable String description, @Nullable String detailedDescription, String configDescriptionURI,
|
||||
String keywords, String countries, @Nullable String license, String connection,
|
||||
@Nullable String backgroundColor, @Nullable String imageLink, @Nullable Map<String, Object> properties,
|
||||
|
@ -86,6 +88,7 @@ public class Addon {
|
|||
this.label = label;
|
||||
this.version = version;
|
||||
this.maturity = maturity;
|
||||
this.compatible = compatible;
|
||||
this.contentType = contentType;
|
||||
this.description = description;
|
||||
this.detailedDescription = detailedDescription;
|
||||
|
@ -161,6 +164,13 @@ public class Addon {
|
|||
return maturity;
|
||||
}
|
||||
|
||||
/**
|
||||
* The (expected) compatibility of this add-on
|
||||
*/
|
||||
public boolean getCompatible() {
|
||||
return compatible;
|
||||
}
|
||||
|
||||
/**
|
||||
* The content type of the add-on
|
||||
*/
|
||||
|
@ -268,6 +278,7 @@ public class Addon {
|
|||
private String label;
|
||||
private String version = "";
|
||||
private @Nullable String maturity;
|
||||
private boolean compatible = true;
|
||||
private String contentType;
|
||||
private @Nullable String link;
|
||||
private String author = "";
|
||||
|
@ -305,6 +316,11 @@ public class Addon {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder withCompatible(boolean compatible) {
|
||||
this.compatible = compatible;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
return this;
|
||||
|
@ -397,9 +413,9 @@ public class Addon {
|
|||
}
|
||||
|
||||
public Addon build() {
|
||||
return new Addon(id, type, label, version, maturity, contentType, link, author, verifiedAuthor, installed,
|
||||
description, detailedDescription, configDescriptionURI, keywords, countries, license, connection,
|
||||
backgroundColor, imageLink, properties.isEmpty() ? null : properties, loggerPackages);
|
||||
return new Addon(id, type, label, version, maturity, compatible, contentType, link, author, verifiedAuthor,
|
||||
installed, description, detailedDescription, configDescriptionURI, keywords, countries, license,
|
||||
connection, backgroundColor, imageLink, properties.isEmpty() ? null : properties, loggerPackages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,12 @@
|
|||
<description>Defines whether openHAB should access the remote repository for add-on installation.</description>
|
||||
<default>true</default>
|
||||
</parameter>
|
||||
<parameter name="includeIncompatible" type="boolean">
|
||||
<label>Include (Potentially) Incompatible Add-ons</label>
|
||||
<description>Some add-on services may provide add-ons where compatibility with the currently running system is not
|
||||
expected. Enabling this option will include these entries in the list of available add-ons.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
Loading…
Reference in New Issue