diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonService.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonService.java index 07b92ec525..df2bcf26d6 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonService.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonService.java @@ -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 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 addonHandlers = new HashSet<>(); protected final Storage 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 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); diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/BundleVersion.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/BundleVersion.java new file mode 100644 index 0000000000..ece936a46e --- /dev/null +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/BundleVersion.java @@ -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( + "(?\\d+)\\.(?\\d+)\\.(?\\d+)(\\.((?RC)|(?M))?(?\\d+))?"); + public static final Pattern RANGE_PATTERN = Pattern.compile( + "\\[(?\\d+\\.\\d+(?\\.\\d+(\\.\\w+)?)?);(?\\d+\\.\\d+(?\\.\\d+(\\.\\w+)?)?)(?[)\\]])"); + + 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); + } +} diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java index ab6a7538ee..4a7b0622aa 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java @@ -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 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 getRemoteAddons() { + if (!enabled) { + return List.of(); + } + List addons = new ArrayList<>(); try { List 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(); } /** diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/JsonAddonService.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/JsonAddonService.java index e11f9051b1..18ffd85d05 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/JsonAddonService.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/JsonAddonService.java @@ -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 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(); } } diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/model/AddonEntryDTO.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/model/AddonEntryDTO.java index 08366baa76..4b40f092e9 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/model/AddonEntryDTO.java +++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/model/AddonEntryDTO.java @@ -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"; diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/config/marketplace.xml b/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/config/marketplace.xml index 3a48b9692b..284d63b5c0 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/config/marketplace.xml +++ b/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/config/marketplace.xml @@ -6,6 +6,12 @@ https://openhab.org/schemas/config-description-1.0.0.xsd"> + + + true + If set to false no add-ons from the community marketplace will be shown. Already installed add-ons will + still be available. + false @@ -29,9 +35,9 @@ thus harm your system. - + false - Include entries which have not been tagged as "stable". These bundles should be used for testing + 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. diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/i18n/marketplace.properties b/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/i18n/marketplace.properties index 7f1f230383..1bf497aff5 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/i18n/marketplace.properties +++ b/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/i18n/marketplace.properties @@ -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 diff --git a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/test/AbstractRemoteAddonServiceTest.java b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonServiceTest.java similarity index 76% rename from bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/test/AbstractRemoteAddonServiceTest.java rename to bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonServiceTest.java index f11db90300..bc1a8ba25c 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/test/AbstractRemoteAddonServiceTest.java +++ b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonServiceTest.java @@ -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 addons = addonService.getAddons(null); - Assertions.assertEquals(0, addons.size()); + assertThat(addons, empty()); } @Test public void testRemoteDisabledBlocksRemoteCalls() { properties.put("remote", false); List 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 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 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 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()); } } diff --git a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/BundleVersionTest.java b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/BundleVersionTest.java new file mode 100644 index 0000000000..8af8096c71 --- /dev/null +++ b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/BundleVersionTest.java @@ -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 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 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 + } +} diff --git a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/test/TestAddonHandler.java b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonHandler.java similarity index 90% rename from bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/test/TestAddonHandler.java rename to bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonHandler.java index 33705fdafb..9d8c252e57 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/test/TestAddonHandler.java +++ b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonHandler.java @@ -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; diff --git a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/test/TestAddonService.java b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonService.java similarity index 86% rename from bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/test/TestAddonService.java rename to bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonService.java index 11c2a24a72..eb04ef10b5 100644 --- a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/test/TestAddonService.java +++ b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonService.java @@ -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 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()); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/addon/Addon.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/addon/Addon.java index 51b58b418f..4364c0e002 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/addon/Addon.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/addon/Addon.java @@ -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 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); } } } diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/config/addons.xml b/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml similarity index 64% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/config/addons.xml rename to bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml index 663a14c369..d036ea139f 100644 --- a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/config/addons.xml +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml @@ -11,6 +11,12 @@ Defines whether openHAB should access the remote repository for add-on installation. true + + + 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. + false + diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_cs.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_cs.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_cs.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_cs.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_de.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_de.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_de.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_de.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_el.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_el.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_el.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_el.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_es.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_es.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_es.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_es.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_fi.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_fi.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_fi.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_fi.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_fr.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_fr.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_fr.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_fr.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_he.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_he.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_he.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_he.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_hu.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_hu.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_hu.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_hu.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_it.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_it.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_it.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_it.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_nl.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_nl.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_nl.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_nl.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_pl.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_pl.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_pl.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_pl.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_pt.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_pt.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_pt.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_pt.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_ru.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_ru.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_ru.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_ru.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_uk.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_uk.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_uk.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_uk.properties diff --git a/bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_zh.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_zh.properties similarity index 100% rename from bundles/org.openhab.core.karaf/src/main/resources/OH-INF/i18n/addons_zh.properties rename to bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_zh.properties