Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[addonservices] Add version filtering #2811

Merged
merged 11 commits into from
Mar 20, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

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;
Expand All @@ -37,6 +38,7 @@
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;
Expand All @@ -52,6 +54,9 @@
*/
@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"), //
Expand All @@ -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;
Expand All @@ -78,6 +85,11 @@ public AbstractRemoteAddonService(EventPublisher eventPublisher, ConfigurationAd
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
Expand All @@ -102,6 +114,10 @@ public void refreshSource() {
// 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;
}
Expand Down Expand Up @@ -216,12 +232,26 @@ protected boolean remoteEnabled() {
// 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* 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");
}
}

public boolean inRange(@Nullable String range) {
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()) {
logger.warn("Argument {} does not define a valid version range. Assuming inRange", range);
J-N-K marked this conversation as resolved.
Show resolved Hide resolved
return true;
}
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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;

Expand All @@ -36,13 +37,15 @@
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;
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO.DiscourseTopicItem;
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponseDTO.DiscourseUser;
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseTopicResponseDTO;
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseTopicResponseDTO.DiscoursePostLink;
import org.openhab.core.config.core.ConfigParser;
import org.openhab.core.config.core.ConfigurableService;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.storage.StorageService;
Expand Down Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -116,9 +121,9 @@ public CommunityMarketplaceAddonService(final @Reference EventPublisher eventPub
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();
}
Expand Down Expand Up @@ -147,6 +152,10 @@ public String getName() {

@Override
protected List<Addon> getRemoteAddons() {
if (!enabled) {
return List.of();
}

List<Addon> addons = new ArrayList<>();
try {
List<DiscourseCategoryResponseDTO> pages = new ArrayList<>();
Expand Down Expand Up @@ -276,7 +285,23 @@ private Addon convertTopicItemToAddon(DiscourseTopicItem topic, List<DiscourseUs
String type = (addonType != null) ? addonType.getId() : "";
String contentType = getContentType(topic.categoryId, tags);

String title = topic.title;
String title;
boolean compatible = true;

int compatibilityStart = topic.title.lastIndexOf("["); // version range always starts with [
J-N-K marked this conversation as resolved.
Show resolved Hide resolved
if (compatibilityStart == -1) {
title = topic.title;
} else {
String potentialRange = topic.title.substring(compatibilityStart + 1);
Matcher matcher = BundleVersion.RANGE_PATTERN.matcher(potentialRange);
if (matcher.matches()) {
compatible = coreVersion.inRange(potentialRange);
title = topic.title.substring(0, compatibilityStart);
J-N-K marked this conversation as resolved.
Show resolved Hide resolved
} else {
title = topic.title;
}
}

String link = COMMUNITY_TOPIC_URL + topic.id.toString();
int likeCount = topic.likeCount;
int views = topic.views;
Expand All @@ -303,7 +328,7 @@ private Addon convertTopicItemToAddon(DiscourseTopicItem topic, List<DiscourseUs

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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,9 @@ private Addon fromAddonEntry(AddonEntryDTO addonEntry) {
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(coreVersion.inRange(addonEntry.compatibleVersions)).withMaturity(addonEntry.maturity)
.withProperties(properties).withLink(addonEntry.link).withImageLink(addonEntry.imageUrl)
.withConfigDescriptionURI(addonEntry.configDescriptionURI).withLoggerPackages(addonEntry.loggerPackages)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
J-N-K marked this conversation as resolved.
Show resolved Hide resolved
<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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading