Skip to content

Commit

Permalink
[addonservices] Add version filtering (openhab#2811)
Browse files Browse the repository at this point in the history
Signed-off-by: Jan N. Klug <github@klug.nrw>
GitOrigin-RevId: 4577562
  • Loading branch information
J-N-K authored and splatch committed Jul 12, 2023
1 parent 19e80fb commit ce77308
Show file tree
Hide file tree
Showing 12 changed files with 436 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -86,6 +88,7 @@ private Addon(String id, String type, String label, String version, @Nullable St
this.label = label;
this.version = version;
this.maturity = maturity;
this.compatible = compatible;
this.contentType = contentType;
this.description = description;
this.detailedDescription = detailedDescription;
Expand Down Expand Up @@ -161,6 +164,13 @@ public String getVersion() {
return maturity;
}

/**
* The (expected) compatibility of this add-on
*/
public boolean getCompatible() {
return compatible;
}

/**
* The content type of the add-on
*/
Expand Down Expand Up @@ -268,6 +278,7 @@ public static class Builder {
private String label;
private String version = "";
private @Nullable String maturity;
private boolean compatible = true;
private String contentType;
private @Nullable String link;
private String author = "";
Expand Down Expand Up @@ -305,6 +316,11 @@ public Builder withMaturity(@Nullable String maturity) {
return this;
}

public Builder withCompatible(boolean compatible) {
this.compatible = compatible;
return this;
}

public Builder withContentType(String contentType) {
this.contentType = contentType;
return this;
Expand Down Expand Up @@ -397,9 +413,9 @@ public Builder withLoggerPackages(List<String> loggerPackages) {
}

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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>
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,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);
}
}
Loading

0 comments on commit ce77308

Please sign in to comment.