Support 'keywords' field in addon.xml schema (#5352)

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green
2026-02-14 20:04:06 +01:00
committed by GitHub
parent 767e809e5a
commit 70144e3cc5
13 changed files with 58 additions and 7 deletions
@@ -144,6 +144,7 @@ public class EclipseAddonService implements AddonService {
if (addonInfo != null) {
// only enrich if this add-on is installed, otherwise wrong data might be added
addon = addon.withLabel(addonInfo.getName()).withDescription(addonInfo.getDescription())
.withKeywords(Objects.requireNonNullElse(addonInfo.getKeywords(), ""))
.withConnection(addonInfo.getConnection()).withCountries(addonInfo.getCountries())
.withLink(getDefaultDocumentationLink(type, name))
.withConfigDescriptionURI(addonInfo.getConfigDescriptionURI());
@@ -209,7 +209,7 @@ public class JsonAddonService extends AbstractRemoteAddonService {
.withDetailedDescription(addonEntry.description).withContentType(addonEntry.contentType)
.withAuthor(addonEntry.author).withVersion(addonEntry.version).withLabel(addonEntry.title)
.withCompatible(compatible).withMaturity(addonEntry.maturity).withProperties(properties)
.withLink(addonEntry.link).withImageLink(addonEntry.imageUrl)
.withLink(addonEntry.link).withImageLink(addonEntry.imageUrl).withKeywords(addonEntry.keywords)
.withConfigDescriptionURI(addonEntry.configDescriptionURI).withLoggerPackages(addonEntry.loggerPackages)
.withConnection(addonEntry.connection).withCountries(addonEntry.countries).build();
}
@@ -26,6 +26,7 @@ public class AddonEntryDTO {
public String id = "";
public String type = "";
public String description = "";
public String keywords = "";
public String title = "";
public String link = "";
public String version = "";
@@ -13,6 +13,11 @@
<xs:element name="type" type="addon:addonType"/>
<xs:element name="name" type="xs:string"/>
<xs:element name="description" type="xs:string"/>
<xs:element name="keywords" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>Comma-separated list of key words for search.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="connection" type="addon:connectionType" minOccurs="0"/>
<xs:element name="countries" type="addon:countryType" minOccurs="0">
<xs:annotation>
@@ -41,6 +41,7 @@ public class AddonInfo implements Identifiable<String> {
private final String uid;
private final String name;
private final String description;
private final @Nullable String keywords;
private final @Nullable String connection;
private final List<String> countries;
private final @Nullable String configDescriptionURI;
@@ -49,8 +50,8 @@ public class AddonInfo implements Identifiable<String> {
private @Nullable List<AddonDiscoveryMethod> discoveryMethods;
private AddonInfo(String id, String type, @Nullable String uid, String name, String description,
@Nullable String connection, List<String> countries, @Nullable String configDescriptionURI,
@Nullable String serviceId, @Nullable String sourceBundle,
@Nullable String keywords, @Nullable String connection, List<String> countries,
@Nullable String configDescriptionURI, @Nullable String serviceId, @Nullable String sourceBundle,
@Nullable List<AddonDiscoveryMethod> discoveryMethods) throws IllegalArgumentException {
// mandatory fields
if (id.isBlank()) {
@@ -73,6 +74,7 @@ public class AddonInfo implements Identifiable<String> {
this.description = description;
// optional fields
this.keywords = keywords;
this.connection = connection;
this.countries = countries;
this.configDescriptionURI = configDescriptionURI;
@@ -127,6 +129,15 @@ public class AddonInfo implements Identifiable<String> {
return description;
}
/**
* Returns a comma-separated list of keywords related to the add-on. e.g. "bluetooth".
*
* @return a comma-separated list of keywords, or null if no keywords string available
*/
public @Nullable String getKeywords() {
return keywords;
}
/**
* Returns the link to a concrete {@link org.openhab.core.config.core.ConfigDescription}.
*
@@ -168,6 +179,7 @@ public class AddonInfo implements Identifiable<String> {
private @Nullable String uid;
private String name = "";
private String description = "";
private @Nullable String keywords;
private @Nullable String connection;
private List<String> countries = List.of();
private @Nullable String configDescriptionURI = "";
@@ -186,6 +198,7 @@ public class AddonInfo implements Identifiable<String> {
this.uid = addonInfo.uid;
this.name = addonInfo.name;
this.description = addonInfo.description;
this.keywords = addonInfo.keywords;
this.connection = addonInfo.connection;
this.countries = addonInfo.countries;
this.configDescriptionURI = addonInfo.configDescriptionURI;
@@ -209,6 +222,11 @@ public class AddonInfo implements Identifiable<String> {
return this;
}
public Builder withKeywords(@Nullable String keywords) {
this.keywords = keywords;
return this;
}
public Builder withConnection(@Nullable String connection) {
this.connection = connection;
return this;
@@ -251,8 +269,8 @@ public class AddonInfo implements Identifiable<String> {
* @throws IllegalArgumentException if any of the information in this builder is invalid
*/
public AddonInfo build() throws IllegalArgumentException {
return new AddonInfo(id, type, uid, name, description, connection, countries, configDescriptionURI,
serviceId, sourceBundle, discoveryMethods);
return new AddonInfo(id, type, uid, name, description, keywords, connection, countries,
configDescriptionURI, serviceId, sourceBundle, discoveryMethods);
}
}
}
@@ -12,6 +12,7 @@
*/
package org.openhab.core.addon;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
@@ -98,6 +99,16 @@ public class AddonInfoRegistry {
if (a.getDescription().isBlank()) {
builder.withDescription(b.getDescription());
}
Set<String> keywords = new HashSet<>();
if (a.getKeywords() instanceof String ka) {
Arrays.stream(ka.split(",")).map(String::trim).filter(s -> !s.isEmpty()).forEach(keywords::add);
}
if (b.getKeywords() instanceof String kb) {
Arrays.stream(kb.split(",")).map(String::trim).filter(s -> !s.isEmpty()).forEach(keywords::add);
}
if (!keywords.isEmpty()) {
builder.withKeywords(keywords.stream().collect(Collectors.joining(",")));
}
if (a.getConnection() == null && b.getConnection() != null) {
builder.withConnection(b.getConnection());
}
@@ -157,6 +157,7 @@ public class JarFileAddonService extends BundleTracker<Bundle> implements AddonS
.withConnection(addonInfo.getConnection()).withCountries(addonInfo.getCountries())
.withConfigDescriptionURI(addonInfo.getConfigDescriptionURI())
.withDescription(Objects.requireNonNullElse(addonInfo.getDescription(), bundle.getSymbolicName()))
.withKeywords(Objects.requireNonNullElse(addonInfo.getKeywords(), ""))
.withContentType(ADDONS_CONTENT_TYPE).withLoggerPackages(List.of(bundle.getSymbolicName())).build();
}
@@ -87,6 +87,8 @@ public class AddonInfoConverter extends GenericUnmarshaller<AddonInfoXmlResult>
"Add-on description is null or empty");
AddonInfo.Builder addonInfo = AddonInfo.builder(id, type).withName(name).withDescription(description);
addonInfo.withKeywords((String) nodeIterator.nextValue("keywords", false));
addonInfo.withConnection((String) nodeIterator.nextValue("connection", false));
addonInfo.withCountries((String) nodeIterator.nextValue("countries", false));
addonInfo.withServiceId((String) nodeIterator.nextValue("service-id", false));
@@ -82,6 +82,7 @@ public class AddonInfoListReader extends XmlDocumentReader<AddonInfoList> {
xstream.alias("name", NodeValue.class);
xstream.alias("description", NodeValue.class);
xstream.alias("type", NodeValue.class);
xstream.alias("keywords", NodeValue.class);
xstream.alias("connection", NodeValue.class);
xstream.alias("countries", NodeValue.class);
xstream.alias("config-description", ConfigDescription.class);
@@ -81,6 +81,7 @@ public class AddonInfoReader extends XmlDocumentReader<AddonInfoXmlResult> {
xstream.alias("name", NodeValue.class);
xstream.alias("description", NodeValue.class);
xstream.alias("type", NodeValue.class);
xstream.alias("keywords", NodeValue.class);
xstream.alias("connection", NodeValue.class);
xstream.alias("countries", NodeValue.class);
xstream.alias("config-description", ConfigDescription.class);
@@ -37,6 +37,7 @@ class AddonInfoListReaderTest {
+ " <type>automation</type>"
+ " <name>Groovy Scripting</name>"
+ " <description>This adds a Groovy script engine.</description>"
+ " <keywords>The,quick,brown,fox</keywords>"
+ " <connection>none</connection>"
+ " <discovery-methods>"
+ " <discovery-method>"
@@ -86,6 +87,7 @@ class AddonInfoListReaderTest {
assertEquals(1, addonsInfos.size());
AddonInfo addon = addonsInfos.getFirst();
assertNotNull(addon);
assertEquals("The,quick,brown,fox", addon.getKeywords());
List<AddonDiscoveryMethod> discoveryMethods = addon.getDiscoveryMethods();
assertNotNull(discoveryMethods);
assertEquals(2, discoveryMethods.size());
@@ -13,6 +13,7 @@
package org.openhab.core.addon;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.List;
@@ -47,7 +48,7 @@ class AddonInfoRegistryMergeTest {
}
private AddonInfoProvider createAddonInfoProvider0() {
AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-zero")
AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-zero").withKeywords("the,quick")
.withDescription("description-zero").build();
AddonInfoProvider provider = mock(AddonInfoProvider.class);
when(provider.getAddonInfo(anyString(), any(Locale.class))).thenReturn(null);
@@ -60,7 +61,7 @@ class AddonInfoRegistryMergeTest {
private AddonInfoProvider createAddonInfoProvider1() {
AddonDiscoveryMethod discoveryMethod = new AddonDiscoveryMethod().setServiceType("mdns")
.setParameters(List.of(new AddonParameter("mdnsServiceType", "_hue._tcp.local.")));
AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-one")
AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-one").withKeywords("brown,fox")
.withDescription("description-one").withCountries("GB,NL").withConnection("local")
.withDiscoveryMethods(List.of(discoveryMethod)).build();
AddonInfoProvider provider = mock(AddonInfoProvider.class);
@@ -150,6 +151,11 @@ class AddonInfoRegistryMergeTest {
assertNotEquals("http://www.openhab.org", addonInfo.getConfigDescriptionURI());
assertEquals("binding.hue", addonInfo.getServiceId());
assertEquals(1, addonInfo.getDiscoveryMethods().size());
assertTrue(addonInfo.getKeywords().contains("the"));
assertTrue(addonInfo.getKeywords().contains("quick"));
assertTrue(addonInfo.getKeywords().contains("brown"));
assertTrue(addonInfo.getKeywords().contains("fox"));
}
/**
@@ -21,6 +21,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import org.apache.karaf.features.Feature;
import org.apache.karaf.features.FeaturesService;
@@ -135,6 +136,7 @@ public class KarafAddonService implements AddonService {
if (addonInfo != null) {
addon = addon.withLabel(addonInfo.getName()).withDescription(addonInfo.getDescription())
.withKeywords(Objects.requireNonNullElse(addonInfo.getKeywords(), ""))
.withConnection(addonInfo.getConnection()).withCountries(addonInfo.getCountries())
.withLink(getDefaultDocumentationLink(type, name))
.withConfigDescriptionURI(addonInfo.getConfigDescriptionURI());