AddonInfo extensions (#3865)

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
pull/3899/head
Andrew Fiddian-Green 2023-12-05 15:49:31 +00:00 committed by GitHub
parent aa305d90d0
commit cc9b70516a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1172 additions and 48 deletions

View File

@ -7,35 +7,36 @@
<xs:import namespace="https://openhab.org/schemas/config-description/v1.0.0"
schemaLocation="https://openhab.org/schemas/config-description-1.0.0.xsd"/>
<xs:element name="addon">
<xs:complexType>
<xs:sequence>
<xs:element name="type" type="addon:addonType"/>
<xs:element name="name" type="xs:string"/>
<xs:element name="description" type="xs:string"/>
<xs:element name="connection" type="addon:connectionType" minOccurs="0"/>
<xs:element name="countries" type="addon:countryType" minOccurs="0">
<xs:annotation>
<xs:documentation>Comma-separated list of two-letter ISO country codes.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="service-id" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>The ID (service.pid or component.name) of the main add-on service, which can be configured through OSGi configuration admin service. Should only be used in combination with a config description definition. The default value is &lt;type&gt;.&lt;name&gt;</xs:documentation>
</xs:annotation>
</xs:element>
<xs:choice minOccurs="0">
<xs:element name="config-description" type="config-description:configDescription"/>
<xs:element name="config-description-ref" type="config-description:configDescriptionRef"/>
</xs:choice>
</xs:sequence>
<xs:attribute name="id" type="config-description:idRestrictionPattern" use="required">
<xs:element name="addon" type="addon:addonInfo"/>
<xs:complexType name="addonInfo">
<xs:sequence>
<xs:element name="type" type="addon:addonType"/>
<xs:element name="name" type="xs:string"/>
<xs:element name="description" type="xs:string"/>
<xs:element name="connection" type="addon:connectionType" minOccurs="0"/>
<xs:element name="countries" type="addon:countryType" minOccurs="0">
<xs:annotation>
<xs:documentation>The id is used to construct the UID of this add-on to &lt;type&gt;-&lt;name&gt;</xs:documentation>
<xs:documentation>Comma-separated list of two-letter ISO country codes.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:element>
<xs:element name="service-id" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>The ID (service.pid or component.name) of the main add-on service, which can be configured through OSGi configuration admin service. Should only be used in combination with a config description definition. The default value is &lt;type&gt;.&lt;name&gt;</xs:documentation>
</xs:annotation>
</xs:element>
<xs:choice minOccurs="0">
<xs:element name="config-description" type="config-description:configDescription"/>
<xs:element name="config-description-ref" type="config-description:configDescriptionRef"/>
</xs:choice>
<xs:element name="discovery-methods" type="addon:discoveryMethodsType" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="config-description:idRestrictionPattern" use="required">
<xs:annotation>
<xs:documentation>The id is used to construct the UID of this add-on to &lt;type&gt;-&lt;name&gt;</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:simpleType name="addonType">
<xs:restriction base="xs:string">
@ -80,4 +81,31 @@
</xs:restriction>
</xs:simpleType>
<xs:complexType name="discoveryMethodsType">
<xs:sequence>
<xs:element type="addon:discoveryMethodType" name="discovery-method" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="discoveryMethodType">
<xs:sequence>
<xs:element type="xs:string" name="service-type"/>
<xs:element type="xs:string" name="mdns-service-type" minOccurs="0"/>
<xs:element type="addon:matchPropertiesType" name="match-properties" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="matchPropertiesType">
<xs:sequence>
<xs:element type="addon:matchPropertyType" name="match-property" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="matchPropertyType">
<xs:sequence>
<xs:element type="xs:string" name="name"/>
<xs:element type="xs:string" name="regex"/>
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xmlns:addon-info-list="https://openhab.org/schemas/addon-info-list/v1.0.0"
targetNamespace="https://openhab.org/schemas/addon-info-list/v1.0.0">
<xs:import namespace="https://openhab.org/schemas/addon/v1.0.0" schemaLocation="addon-1.0.0.xsd"/>
<xs:element name="addon-info-list">
<xs:complexType>
<xs:sequence>
<xs:element name="addons" type="addon-info-list:addons"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="addons">
<xs:sequence>
<xs:element name="addon" type="addon:addonInfo" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2023 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;
import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* DTO for serialization of a suggested addon discovery method.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class AddonDiscoveryMethod {
private @NonNullByDefault({}) String serviceType;
private @Nullable String mdnsServiceType;
private @Nullable List<AddonMatchProperty> matchProperties;
public String getServiceType() {
return serviceType.toLowerCase();
}
public String getMdnsServiceType() {
String mdnsServiceType = this.mdnsServiceType;
return mdnsServiceType != null ? mdnsServiceType : "";
}
public List<AddonMatchProperty> getMatchProperties() {
List<AddonMatchProperty> matchProperties = this.matchProperties;
return matchProperties != null ? matchProperties : List.of();
}
public AddonDiscoveryMethod setServiceType(String serviceType) {
this.serviceType = serviceType.toLowerCase();
return this;
}
public AddonDiscoveryMethod setMdnsServiceType(@Nullable String mdnsServiceType) {
this.mdnsServiceType = mdnsServiceType;
return this;
}
public AddonDiscoveryMethod setMatchProperties(@Nullable List<AddonMatchProperty> matchProperties) {
this.matchProperties = matchProperties;
return this;
}
@Override
public int hashCode() {
return Objects.hash(serviceType, mdnsServiceType, matchProperties);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
AddonDiscoveryMethod other = (AddonDiscoveryMethod) obj;
return Objects.equals(serviceType, other.serviceType) && Objects.equals(mdnsServiceType, other.mdnsServiceType)
&& Objects.equals(matchProperties, other.matchProperties);
}
}

View File

@ -37,6 +37,7 @@ public class AddonInfo implements Identifiable<String> {
private final String id;
private final String type;
private final String uid;
private final String name;
private final String description;
private final @Nullable String connection;
@ -44,10 +45,12 @@ public class AddonInfo implements Identifiable<String> {
private final @Nullable String configDescriptionURI;
private final String serviceId;
private @Nullable String sourceBundle;
private @Nullable List<AddonDiscoveryMethod> discoveryMethods;
private AddonInfo(String id, String type, String name, String description, @Nullable String connection,
List<String> countries, @Nullable String configDescriptionURI, @Nullable String serviceId,
@Nullable String sourceBundle) throws IllegalArgumentException {
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 List<AddonDiscoveryMethod> discoveryMethods) throws IllegalArgumentException {
// mandatory fields
if (id.isBlank()) {
throw new IllegalArgumentException("The ID must neither be null nor empty!");
@ -64,6 +67,7 @@ public class AddonInfo implements Identifiable<String> {
}
this.id = id;
this.type = type;
this.uid = uid != null ? uid : type + Addon.ADDON_SEPARATOR + id;
this.name = name;
this.description = description;
@ -73,6 +77,7 @@ public class AddonInfo implements Identifiable<String> {
this.configDescriptionURI = configDescriptionURI;
this.serviceId = Objects.requireNonNullElse(serviceId, type + "." + id);
this.sourceBundle = sourceBundle;
this.discoveryMethods = discoveryMethods;
}
/**
@ -82,7 +87,7 @@ public class AddonInfo implements Identifiable<String> {
*/
@Override
public String getUID() {
return type + Addon.ADDON_SEPARATOR + id;
return uid;
}
/**
@ -142,6 +147,11 @@ public class AddonInfo implements Identifiable<String> {
return countries;
}
public List<AddonDiscoveryMethod> getDiscoveryMethods() {
List<AddonDiscoveryMethod> discoveryMethods = this.discoveryMethods;
return discoveryMethods != null ? discoveryMethods : List.of();
}
public static Builder builder(String id, String type) {
return new Builder(id, type);
}
@ -154,6 +164,7 @@ public class AddonInfo implements Identifiable<String> {
private final String id;
private final String type;
private @Nullable String uid;
private String name = "";
private String description = "";
private @Nullable String connection;
@ -161,6 +172,7 @@ public class AddonInfo implements Identifiable<String> {
private @Nullable String configDescriptionURI = "";
private @Nullable String serviceId;
private @Nullable String sourceBundle;
private @Nullable List<AddonDiscoveryMethod> discoveryMethods;
private Builder(String id, String type) {
this.id = id;
@ -170,6 +182,7 @@ public class AddonInfo implements Identifiable<String> {
private Builder(AddonInfo addonInfo) {
this.id = addonInfo.id;
this.type = addonInfo.type;
this.uid = addonInfo.uid;
this.name = addonInfo.name;
this.description = addonInfo.description;
this.connection = addonInfo.connection;
@ -177,6 +190,12 @@ public class AddonInfo implements Identifiable<String> {
this.configDescriptionURI = addonInfo.configDescriptionURI;
this.serviceId = addonInfo.serviceId;
this.sourceBundle = addonInfo.sourceBundle;
this.discoveryMethods = addonInfo.discoveryMethods;
}
public Builder withUID(@Nullable String uid) {
this.uid = uid;
return this;
}
public Builder withName(String name) {
@ -219,6 +238,11 @@ public class AddonInfo implements Identifiable<String> {
return this;
}
public Builder withDiscoveryMethods(@Nullable List<AddonDiscoveryMethod> discoveryMethods) {
this.discoveryMethods = discoveryMethods;
return this;
}
/**
* Build an {@link AddonInfo} from this builder
*
@ -226,8 +250,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, name, description, connection, countries, configDescriptionURI, serviceId,
sourceBundle);
return new AddonInfo(id, type, uid, name, description, connection, countries, configDescriptionURI,
serviceId, sourceBundle, discoveryMethods);
}
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2023 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;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* DTO containing a list of {@code AddonInfo}
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class AddonInfoList {
protected @Nullable List<AddonInfo> addons;
public List<AddonInfo> getAddons() {
List<AddonInfo> addons = this.addons;
return addons != null ? addons : List.of();
}
public AddonInfoList setAddons(@Nullable List<AddonInfo> addons) {
this.addons = addons;
return this;
}
}

View File

@ -31,15 +31,15 @@ import org.eclipse.jdt.annotation.Nullable;
public interface AddonInfoProvider {
/**
* Returns the binding information for the specified binding ID and locale (language),
* Returns the binding information for the specified binding UID and locale (language),
* or {@code null} if no binding information could be found.
*
* @param id the ID to be looked for (could be null or empty)
* @param uid the UID to be looked for (could be null or empty)
* @param locale the locale to be used for the binding information (could be null)
* @return a localized binding information object (could be null)
*/
@Nullable
AddonInfo getAddonInfo(@Nullable String id, @Nullable Locale locale);
AddonInfo getAddonInfo(@Nullable String uid, @Nullable Locale locale);
/**
* Returns all binding information in the specified locale (language) this provider contains.

View File

@ -13,10 +13,13 @@
package org.openhab.core.addon;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -44,34 +47,90 @@ public class AddonInfoRegistry {
addonInfoProviders.add(addonInfoProvider);
}
protected void removeAddonInfoProvider(AddonInfoProvider addonInfoProvider) {
public void removeAddonInfoProvider(AddonInfoProvider addonInfoProvider) {
addonInfoProviders.remove(addonInfoProvider);
}
/**
* Returns the add-on information for the specified add-on ID, or {@code null} if no add-on information could be
* Returns the add-on information for the specified add-on UID, or {@code null} if no add-on information could be
* found.
*
* @param id the ID to be looked
* @param uid the UID to be looked
* @return a add-on information object (could be null)
*/
public @Nullable AddonInfo getAddonInfo(String id) {
return getAddonInfo(id, null);
public @Nullable AddonInfo getAddonInfo(String uid) {
return getAddonInfo(uid, null);
}
/**
* Returns the add-on information for the specified add-on ID and locale (language),
* Returns the add-on information for the specified add-on UID and locale (language),
* or {@code null} if no add-on information could be found.
* <p>
* If more than one provider provides information for the specified add-on UID and locale,
* it returns a new {@link AddonInfo} containing merged information from all such providers.
*
* @param id the ID to be looked for
* @param uid the UID to be looked for
* @param locale the locale to be used for the add-on information (could be null)
* @return a localized add-on information object (could be null)
*/
public @Nullable AddonInfo getAddonInfo(String id, @Nullable Locale locale) {
return addonInfoProviders.stream().map(p -> p.getAddonInfo(id, locale)).filter(Objects::nonNull).findAny()
.orElse(null);
public @Nullable AddonInfo getAddonInfo(String uid, @Nullable Locale locale) {
return addonInfoProviders.stream().map(p -> p.getAddonInfo(uid, locale)).filter(Objects::nonNull)
.collect(Collectors.groupingBy(a -> a == null ? "" : a.getUID(),
Collectors.collectingAndThen(Collectors.reducing(mergeAddonInfos), Optional::get)))
.get(uid);
}
/**
* A {@link BinaryOperator} to merge the field values from two {@link AddonInfo} objects into a third such object.
* <p>
* If the first object has a non-null field value the result object takes the first value, or if the second object
* has a non-null field value the result object takes the second value. Otherwise the field remains null.
*
* @param a the first {@link AddonInfo} (could be null)
* @param b the second {@link AddonInfo} (could be null)
* @return a new {@link AddonInfo} containing the combined field values (could be null)
*/
private static BinaryOperator<@Nullable AddonInfo> mergeAddonInfos = (a, b) -> {
if (a == null) {
return b;
} else if (b == null) {
return a;
}
AddonInfo.Builder builder = AddonInfo.builder(a);
if (a.getDescription().isEmpty()) {
builder.withDescription(b.getDescription());
}
if (a.getConnection() == null && b.getConnection() != null) {
builder.withConnection(b.getConnection());
}
Set<String> countries = new HashSet<>(a.getCountries());
countries.addAll(b.getCountries());
if (!countries.isEmpty()) {
builder.withCountries(countries.stream().toList());
}
String aConfigDescriptionURI = a.getConfigDescriptionURI();
if (aConfigDescriptionURI == null || aConfigDescriptionURI.isEmpty() && b.getConfigDescriptionURI() != null) {
builder.withConfigDescriptionURI(b.getConfigDescriptionURI());
}
if (a.getSourceBundle() == null && b.getSourceBundle() != null) {
builder.withSourceBundle(b.getSourceBundle());
}
String defaultServiceId = a.getType() + "." + a.getId();
if (defaultServiceId.equals(a.getServiceId()) && !defaultServiceId.equals(b.getServiceId())) {
builder.withServiceId(b.getServiceId());
}
String defaultUID = a.getType() + Addon.ADDON_SEPARATOR + a.getId();
if (defaultUID.equals(a.getUID()) && !defaultUID.equals(b.getUID())) {
builder.withUID(b.getUID());
}
Set<AddonDiscoveryMethod> discoveryMethods = new HashSet<>(a.getDiscoveryMethods());
discoveryMethods.addAll(b.getDiscoveryMethods());
if (!discoveryMethods.isEmpty()) {
builder.withDiscoveryMethods(discoveryMethods.stream().toList());
}
return builder.build();
};
/**
* Returns all add-on information this registry contains.
*

View File

@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2023 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;
import java.util.Objects;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* DTO for serialization of a property match regular expression.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class AddonMatchProperty {
private @NonNullByDefault({}) String name;
private @NonNullByDefault({}) String regex;
private transient @NonNullByDefault({}) Pattern pattern;
public AddonMatchProperty(String name, String regex) {
this.name = name;
this.regex = regex;
this.pattern = null;
}
public String getName() {
return name;
}
public Pattern getPattern() {
Pattern pattern = this.pattern;
if (pattern == null) {
this.pattern = Pattern.compile(regex);
}
return this.pattern;
}
public String getRegex() {
return regex;
}
@Override
public int hashCode() {
return Objects.hash(name, regex);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
AddonMatchProperty other = (AddonMatchProperty) obj;
return Objects.equals(name, other.name) && Objects.equals(regex, other.regex);
}
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2023 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.internal.xml;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonMatchProperty;
import org.openhab.core.config.core.xml.util.GenericUnmarshaller;
import org.openhab.core.config.core.xml.util.NodeIterator;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
/**
* The {@link AddonDiscoveryMethodConverter} is a concrete implementation of the {@code XStream} {@link Converter}
* interface used to convert add-on discovery method information within an XML document into a
* {@link AddonDiscoveryMethod} object.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class AddonDiscoveryMethodConverter extends GenericUnmarshaller<AddonDiscoveryMethod> {
public AddonDiscoveryMethodConverter() {
super(AddonDiscoveryMethod.class);
}
@Override
public @Nullable Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
List<?> nodes = (List<?>) context.convertAnother(context, List.class);
NodeIterator nodeIterator = new NodeIterator(nodes);
String serviceType = requireNonEmpty((String) nodeIterator.nextValue("service-type", true),
"Service type is null or empty");
String mdnsServiceType = (String) nodeIterator.nextValue("mdns-service-type", false);
Object object = nodeIterator.nextList("match-properties", false);
List<AddonMatchProperty> matchProperties = !(object instanceof List<?> list) ? null
: list.stream().filter(e -> (e instanceof AddonMatchProperty)).map(e -> ((AddonMatchProperty) e))
.toList();
nodeIterator.assertEndOfType();
return new AddonDiscoveryMethod().setServiceType(serviceType).setMdnsServiceType(mdnsServiceType)
.setMatchProperties(matchProperties);
}
}

View File

@ -0,0 +1,128 @@
/**
* Copyright (c) 2010-2023 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.internal.xml;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.OpenHAB;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.addon.AddonInfoProvider;
import org.openhab.core.addon.AddonMatchProperty;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.converters.ConversionException;
/**
* The {@link AddonInfoAddonsXmlProvider} reads all {@code userdata/addons/*.xml} files, each of which
* should contain a list of {@code addon} elements, and convert their combined contents into a list
* of {@link AddonInfo} objects can be accessed via the {@link AddonInfoProvider} interface.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
@Component(service = AddonInfoProvider.class, name = AddonInfoAddonsXmlProvider.SERVICE_NAME)
public class AddonInfoAddonsXmlProvider implements AddonInfoProvider {
public static final String SERVICE_NAME = "addons-info-provider";
private final Logger logger = LoggerFactory.getLogger(AddonInfoAddonsXmlProvider.class);
private final String folder = OpenHAB.getUserDataFolder() + File.separator + "addons";
private final Set<AddonInfo> addonInfos = new HashSet<>();
@Activate
public AddonInfoAddonsXmlProvider() {
initialize();
testAddonDeveloperRegexSyntax();
}
@Deactivate
public void deactivate() {
addonInfos.clear();
}
@Override
public @Nullable AddonInfo getAddonInfo(@Nullable String uid, @Nullable Locale locale) {
return addonInfos.stream().filter(a -> a.getUID().equals(uid)).findFirst().orElse(null);
}
@Override
public Set<AddonInfo> getAddonInfos(@Nullable Locale locale) {
return addonInfos;
}
private void initialize() {
AddonInfoListReader reader = new AddonInfoListReader();
Stream.of(new File(folder).listFiles()).filter(f -> f.isFile() && f.getName().endsWith(".xml")).forEach(f -> {
try {
String xml = Files.readString(f.toPath());
if (xml != null && !xml.isBlank()) {
addonInfos.addAll(reader.readFromXML(xml).getAddons().stream().collect(Collectors.toSet()));
} else {
logger.warn("File '{}' contents are null or empty", f.getName());
}
} catch (IOException e) {
logger.warn("File '{}' could not be read", f.getName());
} catch (ConversionException e) {
logger.warn("File '{}' has invalid content", f.getName());
} catch (XStreamException e) {
logger.warn("File '{}' could not be deserialized", f.getName());
}
});
}
/*
* The openhab-addons Maven build process checks individual developer addon.xml contributions
* against the 'addon-1.0.0.xsd' schema, but it can't check the discovery-method match-property
* regex syntax. Invalid regexes do throw exceptions at run-time, but the log can't identify the
* culprit addon. Ideally we need to add syntax checks to the Maven build; and this test is an
* interim solution.
*/
private void testAddonDeveloperRegexSyntax() {
List<String> patternErrors = new ArrayList<>();
for (AddonInfo addonInfo : addonInfos) {
for (AddonDiscoveryMethod discoveryMethod : addonInfo.getDiscoveryMethods()) {
for (AddonMatchProperty matchProperty : discoveryMethod.getMatchProperties()) {
try {
matchProperty.getPattern();
} catch (PatternSyntaxException e) {
patternErrors.add(String.format(
"Regex syntax error in org.openhab.%s.%s addon.xml => %s in \"%s\" position %d",
addonInfo.getType(), addonInfo.getId(), e.getDescription(), e.getPattern(),
e.getIndex()));
}
}
}
}
if (!patternErrors.isEmpty()) {
logger.warn("The following errors were found:\n\t{}", String.join("\n\t", patternErrors));
}
}
}

View File

@ -18,6 +18,7 @@ import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionBuilder;
@ -37,6 +38,7 @@ import com.thoughtworks.xstream.io.HierarchicalStreamReader;
* @author Michael Grammling - Initial contribution
* @author Andre Fuechsel - Made author tag optional
* @author Jan N. Klug - Refactored to cover all add-ons
* @author Andrew Fiddian-Green - Added discovery methods
*/
@NonNullByDefault
public class AddonInfoConverter extends GenericUnmarshaller<AddonInfoXmlResult> {
@ -107,6 +109,11 @@ public class AddonInfoConverter extends GenericUnmarshaller<AddonInfoXmlResult>
addonInfo.withConfigDescriptionURI(configDescriptionURI);
Object object = nodeIterator.nextList("discovery-methods", false);
addonInfo.withDiscoveryMethods(!(object instanceof List<?> list) ? null
: list.stream().filter(e -> (e instanceof AddonDiscoveryMethod)).map(e -> ((AddonDiscoveryMethod) e))
.toList());
nodeIterator.assertEndOfType();
// create object

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2023 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.internal.xml;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.addon.AddonInfoList;
import org.openhab.core.config.core.xml.util.GenericUnmarshaller;
import org.openhab.core.config.core.xml.util.NodeIterator;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
/**
* The {@link AddonInfoListConverter} is a concrete implementation of the {@code XStream} {@link Converter}
* interface used to convert a list of add-on information within an XML document into a list of {@link AddonInfo}
* objects.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class AddonInfoListConverter extends GenericUnmarshaller<AddonInfoList> {
public AddonInfoListConverter() {
super(AddonInfoList.class);
}
@Override
public @Nullable Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
List<?> nodes = (List<?>) context.convertAnother(context, List.class);
NodeIterator nodeIterator = new NodeIterator(nodes);
Object object = nodeIterator.nextList("addons", false);
List<AddonInfo> addons = (object instanceof List<?> list)
? list.stream().filter(e -> e != null).filter(e -> (e instanceof AddonInfoXmlResult))
.map(e -> (AddonInfoXmlResult) e).map(r -> r.addonInfo()).toList()
: null;
nodeIterator.assertEndOfType();
return new AddonInfoList().setAddons(addons);
}
}

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2023 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.internal.xml;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonInfoList;
import org.openhab.core.addon.AddonMatchProperty;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionParameterGroup;
import org.openhab.core.config.core.FilterCriteria;
import org.openhab.core.config.core.xml.ConfigDescriptionConverter;
import org.openhab.core.config.core.xml.ConfigDescriptionParameterConverter;
import org.openhab.core.config.core.xml.ConfigDescriptionParameterGroupConverter;
import org.openhab.core.config.core.xml.FilterCriteriaConverter;
import org.openhab.core.config.core.xml.util.NodeAttributes;
import org.openhab.core.config.core.xml.util.NodeAttributesConverter;
import org.openhab.core.config.core.xml.util.NodeList;
import org.openhab.core.config.core.xml.util.NodeListConverter;
import org.openhab.core.config.core.xml.util.NodeValue;
import org.openhab.core.config.core.xml.util.NodeValueConverter;
import org.openhab.core.config.core.xml.util.XmlDocumentReader;
import com.thoughtworks.xstream.XStream;
/**
* The {@link AddonInfoListReader} reads XML documents, which contain the {@code addon} XML tag, and converts them to
* a List of {@link AddonInfoXmlResult} objects.
* <p>
* This reader uses {@code XStream} and {@code StAX} to parse and convert the XML document.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class AddonInfoListReader extends XmlDocumentReader<AddonInfoList> {
/**
* The default constructor of this class.
*/
public AddonInfoListReader() {
ClassLoader classLoader = AddonInfoListReader.class.getClassLoader();
if (classLoader != null) {
super.setClassLoader(classLoader);
}
}
@Override
protected void registerConverters(XStream xstream) {
xstream.registerConverter(new NodeAttributesConverter());
xstream.registerConverter(new NodeListConverter());
xstream.registerConverter(new NodeValueConverter());
xstream.registerConverter(new AddonInfoListConverter());
xstream.registerConverter(new AddonInfoConverter());
xstream.registerConverter(new ConfigDescriptionConverter());
xstream.registerConverter(new ConfigDescriptionParameterConverter());
xstream.registerConverter(new ConfigDescriptionParameterGroupConverter());
xstream.registerConverter(new FilterCriteriaConverter());
xstream.registerConverter(new AddonDiscoveryMethodConverter());
xstream.registerConverter(new AddonMatchPropertyConverter());
}
@Override
protected void registerAliases(XStream xstream) {
xstream.alias("addon-info-list", AddonInfoList.class);
xstream.alias("addons", NodeList.class);
xstream.alias("addon", AddonInfoXmlResult.class);
xstream.alias("name", NodeValue.class);
xstream.alias("description", NodeValue.class);
xstream.alias("type", NodeValue.class);
xstream.alias("connection", NodeValue.class);
xstream.alias("countries", NodeValue.class);
xstream.alias("config-description", ConfigDescription.class);
xstream.alias("config-description-ref", NodeAttributes.class);
xstream.alias("parameter", ConfigDescriptionParameter.class);
xstream.alias("parameter-group", ConfigDescriptionParameterGroup.class);
xstream.alias("options", NodeList.class);
xstream.alias("option", NodeValue.class);
xstream.alias("filter", List.class);
xstream.alias("criteria", FilterCriteria.class);
xstream.alias("service-id", NodeValue.class);
xstream.alias("discovery-methods", NodeList.class);
xstream.alias("discovery-method", AddonDiscoveryMethod.class);
xstream.alias("service-type", NodeValue.class);
xstream.alias("mdns-service-type", NodeValue.class);
xstream.alias("match-properties", NodeList.class);
xstream.alias("match-property", AddonMatchProperty.class);
xstream.alias("regex", NodeValue.class);
}
}

View File

@ -15,6 +15,8 @@ package org.openhab.core.addon.internal.xml;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonMatchProperty;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionParameterGroup;
@ -26,6 +28,7 @@ import org.openhab.core.config.core.xml.FilterCriteriaConverter;
import org.openhab.core.config.core.xml.util.NodeAttributes;
import org.openhab.core.config.core.xml.util.NodeAttributesConverter;
import org.openhab.core.config.core.xml.util.NodeList;
import org.openhab.core.config.core.xml.util.NodeListConverter;
import org.openhab.core.config.core.xml.util.NodeValue;
import org.openhab.core.config.core.xml.util.NodeValueConverter;
import org.openhab.core.config.core.xml.util.XmlDocumentReader;
@ -33,7 +36,7 @@ import org.openhab.core.config.core.xml.util.XmlDocumentReader;
import com.thoughtworks.xstream.XStream;
/**
* The {@link AddonInfoReader} reads XML documents, which contain the {@code binding} XML tag,
* The {@link AddonInfoReader} reads XML documents, which contain the {@code addon} XML tag,
* and converts them to {@link AddonInfoXmlResult} objects.
* <p>
* This reader uses {@code XStream} and {@code StAX} to parse and convert the XML document.
@ -59,12 +62,15 @@ public class AddonInfoReader extends XmlDocumentReader<AddonInfoXmlResult> {
@Override
protected void registerConverters(XStream xstream) {
xstream.registerConverter(new NodeAttributesConverter());
xstream.registerConverter(new NodeListConverter());
xstream.registerConverter(new NodeValueConverter());
xstream.registerConverter(new AddonInfoConverter());
xstream.registerConverter(new ConfigDescriptionConverter());
xstream.registerConverter(new ConfigDescriptionParameterConverter());
xstream.registerConverter(new ConfigDescriptionParameterGroupConverter());
xstream.registerConverter(new FilterCriteriaConverter());
xstream.registerConverter(new AddonDiscoveryMethodConverter());
xstream.registerConverter(new AddonMatchPropertyConverter());
}
@Override
@ -84,5 +90,12 @@ public class AddonInfoReader extends XmlDocumentReader<AddonInfoXmlResult> {
xstream.alias("filter", List.class);
xstream.alias("criteria", FilterCriteria.class);
xstream.alias("service-id", NodeValue.class);
xstream.alias("discovery-methods", NodeList.class);
xstream.alias("discovery-method", AddonDiscoveryMethod.class);
xstream.alias("service-type", NodeValue.class);
xstream.alias("mdns-service-type", NodeValue.class);
xstream.alias("match-properties", NodeList.class);
xstream.alias("match-property", AddonMatchProperty.class);
xstream.alias("regex", NodeValue.class);
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2023 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.internal.xml;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.AddonMatchProperty;
import org.openhab.core.config.core.xml.util.GenericUnmarshaller;
import org.openhab.core.config.core.xml.util.NodeIterator;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
/**
* The {@link AddonMatchPropertyConverter} is a concrete implementation of the {@code XStream} {@link Converter}
* interface used to convert add-on discovery method match property information within an XML document into a
* {@link AddonMatchProperty} object.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class AddonMatchPropertyConverter extends GenericUnmarshaller<AddonMatchProperty> {
public AddonMatchPropertyConverter() {
super(AddonMatchProperty.class);
}
@Override
public @Nullable Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
List<?> nodes = (List<?>) context.convertAnother(context, List.class);
NodeIterator nodeIterator = new NodeIterator(nodes);
String name = requireNonEmpty((String) nodeIterator.nextValue("name", true), "Name is null or empty");
String regex = requireNonEmpty((String) nodeIterator.nextValue("regex", true), "Regex is null or empty");
nodeIterator.assertEndOfType();
return new AddonMatchProperty(name, regex);
}
}

View File

@ -0,0 +1,117 @@
/**
* Copyright (c) 2010-2023 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;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.addon.internal.xml.AddonInfoListReader;
/**
* JUnit tests for {@link AddonInfoListReader}.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
class AddonInfoListReaderTest {
// @formatter:off
private final String testXml =
"<addon-info-list><addons>"
+ " <addon:addon id=\"groovyscripting\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+ " xmlns:addon=\"https://openhab.org/schemas/addon/v1.0.0\""
+ " xsi:schemaLocation=\"https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd\">"
+ " <type>automation</type>"
+ " <name>Groovy Scripting</name>"
+ " <description>This adds a Groovy script engine.</description>"
+ " <connection>none</connection>"
+ " <discovery-methods>"
+ " <discovery-method>"
+ " <service-type>mdns</service-type>"
+ " <mdns-service-type>_printer._tcp.local.</mdns-service-type>"
+ " <match-properties>"
+ " <match-property>"
+ " <name>rp</name>"
+ " <regex>.*</regex>"
+ " </match-property>"
+ " <match-property>"
+ " <name>ty</name>"
+ " <regex>hp (.*)</regex>"
+ " </match-property>"
+ " </match-properties>"
+ " </discovery-method>"
+ " <discovery-method>"
+ " <service-type>upnp</service-type>"
+ " <match-properties>"
+ " <match-property>"
+ " <name>modelName</name>"
+ " <regex>Philips hue bridge</regex>"
+ " </match-property>"
+ " </match-properties>"
+ " </discovery-method>"
+ " </discovery-methods>"
+ " </addon:addon>"
+ "</addons></addon-info-list>";
// @formatter:on
@Test
void testAddonInfoListReader() {
AddonInfoList addons = null;
try {
AddonInfoListReader reader = new AddonInfoListReader();
addons = reader.readFromXML(testXml);
} catch (Exception e) {
fail(e);
}
assertNotNull(addons);
List<AddonInfo> addonsInfos = addons.getAddons();
assertEquals(1, addonsInfos.size());
AddonInfo addon = addonsInfos.get(0);
assertNotNull(addon);
List<AddonDiscoveryMethod> discoveryMethods = addon.getDiscoveryMethods();
assertNotNull(discoveryMethods);
assertEquals(2, discoveryMethods.size());
AddonDiscoveryMethod method = discoveryMethods.get(0);
assertNotNull(method);
assertEquals("mdns", method.getServiceType());
assertEquals("_printer._tcp.local.", method.getMdnsServiceType());
List<AddonMatchProperty> matchProperties = method.getMatchProperties();
assertNotNull(matchProperties);
assertEquals(2, matchProperties.size());
AddonMatchProperty property = matchProperties.get(0);
assertNotNull(property);
assertEquals("rp", property.getName());
assertEquals(".*", property.getRegex());
assertTrue(property.getPattern().matcher("the cat sat on the mat").matches());
method = discoveryMethods.get(1);
assertNotNull(method);
assertEquals("upnp", method.getServiceType());
assertEquals("", method.getMdnsServiceType());
matchProperties = method.getMatchProperties();
assertNotNull(matchProperties);
assertEquals(1, matchProperties.size());
property = matchProperties.get(0);
assertNotNull(property);
assertEquals("modelName", property.getName());
assertEquals("Philips hue bridge", property.getRegex());
assertTrue(property.getPattern().matcher("Philips hue bridge").matches());
}
}

View File

@ -0,0 +1,198 @@
/**
* Copyright (c) 2010-2023 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;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
/**
* JUnit test for the {@link AddonInfoRegistry} merge function.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
@TestInstance(Lifecycle.PER_CLASS)
class AddonInfoRegistryMergeTest {
private @Nullable AddonInfoProvider addonInfoProvider0;
private @Nullable AddonInfoProvider addonInfoProvider1;
private @Nullable AddonInfoProvider addonInfoProvider2;
@BeforeAll
void beforeAll() {
addonInfoProvider0 = createAddonInfoProvider0();
addonInfoProvider1 = createAddonInfoProvider1();
addonInfoProvider2 = createAddonInfoProvider2();
}
private AddonInfoProvider createAddonInfoProvider0() {
AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-zero")
.withDescription("description-zero").build();
AddonInfoProvider provider = mock(AddonInfoProvider.class);
when(provider.getAddonInfo(anyString(), any(Locale.class))).thenReturn(null);
when(provider.getAddonInfo(anyString(), eq(null))).thenReturn(null);
when(provider.getAddonInfo(eq("binding-hue"), any(Locale.class))).thenReturn(addonInfo);
when(provider.getAddonInfo(eq("binding-hue"), eq(null))).thenReturn(null);
return provider;
}
private AddonInfoProvider createAddonInfoProvider1() {
AddonDiscoveryMethod discoveryMethod = new AddonDiscoveryMethod().setServiceType("mdns")
.setMdnsServiceType("_hue._tcp.local.");
AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-one")
.withDescription("description-one").withCountries("GB,NL").withConnection("local")
.withDiscoveryMethods(List.of(discoveryMethod)).build();
AddonInfoProvider provider = mock(AddonInfoProvider.class);
when(provider.getAddonInfo(anyString(), any(Locale.class))).thenReturn(null);
when(provider.getAddonInfo(anyString(), eq(null))).thenReturn(null);
when(provider.getAddonInfo(eq("binding-hue"), any(Locale.class))).thenReturn(addonInfo);
when(provider.getAddonInfo(eq("binding-hue"), eq(null))).thenReturn(null);
return provider;
}
private AddonInfoProvider createAddonInfoProvider2() {
AddonDiscoveryMethod discoveryMethod = new AddonDiscoveryMethod().setServiceType("upnp")
.setMatchProperties(List.of(new AddonMatchProperty("modelName", "Philips hue bridge")));
AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-two")
.withDescription("description-two").withCountries("DE,FR").withSourceBundle("source-bundle")
.withServiceId("service-id").withConfigDescriptionURI("http://www.openhab.org")
.withDiscoveryMethods(List.of(discoveryMethod)).build();
AddonInfoProvider provider = mock(AddonInfoProvider.class);
when(provider.getAddonInfo(anyString(), any(Locale.class))).thenReturn(null);
when(provider.getAddonInfo(anyString(), eq(null))).thenReturn(null);
when(provider.getAddonInfo(eq("binding-hue"), any(Locale.class))).thenReturn(addonInfo);
when(provider.getAddonInfo(eq("binding-hue"), eq(null))).thenReturn(null);
return provider;
}
/**
* Test fetching a single addon-info from the registry with no merging.
*/
@Test
void testGetOneAddonInfo() {
AddonInfoRegistry registry = new AddonInfoRegistry();
assertNotNull(addonInfoProvider0);
registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider0));
AddonInfo addonInfo;
addonInfo = registry.getAddonInfo("aardvark", Locale.US);
assertNull(addonInfo);
addonInfo = registry.getAddonInfo("aardvark", null);
assertNull(addonInfo);
addonInfo = registry.getAddonInfo("binding-hue", null);
assertNull(addonInfo);
addonInfo = registry.getAddonInfo("binding-hue", Locale.US);
assertNotNull(addonInfo);
assertEquals("hue", addonInfo.getId());
assertEquals("binding", addonInfo.getType());
assertEquals("binding-hue", addonInfo.getUID());
assertTrue(addonInfo.getName().startsWith("name-"));
assertTrue(addonInfo.getDescription().startsWith("description-"));
assertNull(addonInfo.getSourceBundle());
assertNotEquals("local", addonInfo.getConnection());
assertEquals(0, addonInfo.getCountries().size());
assertNotEquals("http://www.openhab.org", addonInfo.getConfigDescriptionURI());
assertEquals("binding.hue", addonInfo.getServiceId());
assertEquals(0, addonInfo.getDiscoveryMethods().size());
}
/**
* Test fetching two addon-info's from the registry with merging.
*/
@Test
void testMergeAddonInfos2() {
AddonInfoRegistry registry = new AddonInfoRegistry();
assertNotNull(addonInfoProvider0);
registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider0));
assertNotNull(addonInfoProvider1);
registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider1));
AddonInfo addonInfo;
addonInfo = registry.getAddonInfo("aardvark", Locale.US);
assertNull(addonInfo);
addonInfo = registry.getAddonInfo("aardvark", null);
assertNull(addonInfo);
addonInfo = registry.getAddonInfo("binding-hue", null);
assertNull(addonInfo);
addonInfo = registry.getAddonInfo("binding-hue", Locale.US);
assertNotNull(addonInfo);
assertEquals("hue", addonInfo.getId());
assertEquals("binding", addonInfo.getType());
assertEquals("binding-hue", addonInfo.getUID());
assertTrue(addonInfo.getName().startsWith("name-"));
assertTrue(addonInfo.getDescription().startsWith("description-"));
assertNull(addonInfo.getSourceBundle());
assertEquals("local", addonInfo.getConnection());
assertEquals(2, addonInfo.getCountries().size());
assertNotEquals("http://www.openhab.org", addonInfo.getConfigDescriptionURI());
assertEquals("binding.hue", addonInfo.getServiceId());
assertEquals(1, addonInfo.getDiscoveryMethods().size());
}
/**
* Test fetching three addon-info's from the registry with full merging.
*/
@Test
void testMergeAddonInfos3() {
AddonInfoRegistry registry = new AddonInfoRegistry();
assertNotNull(addonInfoProvider0);
registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider0));
assertNotNull(addonInfoProvider1);
registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider1));
assertNotNull(addonInfoProvider2);
registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider2));
AddonInfo addonInfo;
addonInfo = registry.getAddonInfo("aardvark", Locale.US);
assertNull(addonInfo);
addonInfo = registry.getAddonInfo("aardvark", null);
assertNull(addonInfo);
addonInfo = registry.getAddonInfo("binding-hue", null);
assertNull(addonInfo);
addonInfo = registry.getAddonInfo("binding-hue", Locale.US);
assertNotNull(addonInfo);
assertEquals("hue", addonInfo.getId());
assertEquals("binding", addonInfo.getType());
assertEquals("binding-hue", addonInfo.getUID());
assertTrue(addonInfo.getName().startsWith("name-"));
assertTrue(addonInfo.getDescription().startsWith("description-"));
assertEquals("source-bundle", addonInfo.getSourceBundle());
assertEquals("local", addonInfo.getConnection());
assertEquals(4, addonInfo.getCountries().size());
assertEquals("http://www.openhab.org", addonInfo.getConfigDescriptionURI());
assertEquals("service-id", addonInfo.getServiceId());
assertEquals(2, addonInfo.getDiscoveryMethods().size());
}
}

View File

@ -19,6 +19,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.io.xml.StaxDriver;
@ -104,4 +105,18 @@ public abstract class XmlDocumentReader<@NonNull T> {
public @Nullable T readFromXML(URL xmlURL) throws ConversionException {
return (@Nullable T) xstream.fromXML(xmlURL);
}
/**
* Reads the XML document containing a specific XML tag from the specified xml string and converts it to the
* according object.
*
* @param xml a string containing the XML document to be read.
* @return the conversion result object (could be null).
* @throws XStreamException if the object cannot be deserialized.
* @throws ConversionException if the specified document contains invalid content
*/
@SuppressWarnings("unchecked")
public @Nullable T readFromXML(String xml) throws ConversionException {
return (@Nullable T) xstream.fromXML(xml);
}
}

View File

@ -12,8 +12,12 @@
*/
package org.openhab.core.addon.xml.test;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.net.URI;
import java.util.List;
@ -24,8 +28,10 @@ import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.addon.AddonInfoRegistry;
import org.openhab.core.addon.AddonMatchProperty;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
@ -66,6 +72,31 @@ public class AddonInfoTest extends JavaOSGiTest {
assertThat(addonInfo.getDescription(),
is("The hue Binding integrates the Philips hue system. It allows to control hue lights."));
assertThat(addonInfo.getName(), is("hue Binding"));
List<AddonDiscoveryMethod> discoveryMethods = addonInfo.getDiscoveryMethods();
assertNotNull(discoveryMethods);
assertEquals(2, discoveryMethods.size());
AddonDiscoveryMethod discoveryMethod = discoveryMethods.get(0);
assertNotNull(discoveryMethod);
assertEquals("mdns", discoveryMethod.getServiceType());
assertEquals("_hue._tcp.local.", discoveryMethod.getMdnsServiceType());
List<AddonMatchProperty> properties = discoveryMethod.getMatchProperties();
assertNotNull(properties);
assertEquals(0, properties.size());
discoveryMethod = discoveryMethods.get(1);
assertNotNull(discoveryMethod);
assertEquals("upnp", discoveryMethod.getServiceType());
assertEquals("", discoveryMethod.getMdnsServiceType());
properties = discoveryMethod.getMatchProperties();
assertNotNull(properties);
assertEquals(1, properties.size());
AddonMatchProperty property = properties.get(0);
assertNotNull(property);
assertEquals("modelName", property.getName());
assertEquals("Philips hue bridge", property.getRegex());
assertTrue(property.getPattern().matcher("Philips hue bridge").matches());
});
}

View File

@ -30,4 +30,21 @@
</config-description>
<!-- discovery methods -->
<discovery-methods>
<discovery-method>
<service-type>mdns</service-type>
<mdns-service-type>_hue._tcp.local.</mdns-service-type>
</discovery-method>
<discovery-method>
<service-type>upnp</service-type>
<match-properties>
<match-property>
<name>modelName</name>
<regex>Philips hue bridge</regex>
</match-property>
</match-properties>
</discovery-method>
</discovery-methods>
</addon:addon>