diff --git a/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd b/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd index 21142133d17..c32e01a70e4 100644 --- a/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd +++ b/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd @@ -7,35 +7,36 @@ - - - - - - - - - - Comma-separated list of two-letter ISO country codes. - - - - - 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 <type>.<name> - - - - - - - - + + + + + + + + + + + Comma-separated list of two-letter ISO country codes. + + + - The id is used to construct the UID of this add-on to <type>-<name> + 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 <type>.<name> - - - + + + + + + + + + + The id is used to construct the UID of this add-on to <type>-<name> + + + @@ -80,4 +81,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.addon/schema/addon-info-list-1.0.0.xsd b/bundles/org.openhab.core.addon/schema/addon-info-list-1.0.0.xsd new file mode 100644 index 00000000000..4f720dc9815 --- /dev/null +++ b/bundles/org.openhab.core.addon/schema/addon-info-list-1.0.0.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonDiscoveryMethod.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonDiscoveryMethod.java new file mode 100644 index 00000000000..88eb8669819 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonDiscoveryMethod.java @@ -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 matchProperties; + + public String getServiceType() { + return serviceType.toLowerCase(); + } + + public String getMdnsServiceType() { + String mdnsServiceType = this.mdnsServiceType; + return mdnsServiceType != null ? mdnsServiceType : ""; + } + + public List getMatchProperties() { + List 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 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); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java index 8de5d828f70..8e45c47c918 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java @@ -37,6 +37,7 @@ public class AddonInfo implements Identifiable { 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 { private final @Nullable String configDescriptionURI; private final String serviceId; private @Nullable String sourceBundle; + private @Nullable List discoveryMethods; - private AddonInfo(String id, String type, String name, String description, @Nullable String connection, - List 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 countries, @Nullable String configDescriptionURI, + @Nullable String serviceId, @Nullable String sourceBundle, + @Nullable List discoveryMethods) throws IllegalArgumentException { // mandatory fields if (id.isBlank()) { throw new IllegalArgumentException("The ID must neither be null nor empty!"); @@ -64,6 +67,7 @@ private AddonInfo(String id, String type, String name, String description, @Null } 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 @@ private AddonInfo(String id, String type, String name, String description, @Null this.configDescriptionURI = configDescriptionURI; this.serviceId = Objects.requireNonNullElse(serviceId, type + "." + id); this.sourceBundle = sourceBundle; + this.discoveryMethods = discoveryMethods; } /** @@ -82,7 +87,7 @@ private AddonInfo(String id, String type, String name, String description, @Null */ @Override public String getUID() { - return type + Addon.ADDON_SEPARATOR + id; + return uid; } /** @@ -142,6 +147,11 @@ public List getCountries() { return countries; } + public List getDiscoveryMethods() { + List 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 static class Builder { 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 static class Builder { private @Nullable String configDescriptionURI = ""; private @Nullable String serviceId; private @Nullable String sourceBundle; + private @Nullable List discoveryMethods; private Builder(String id, String type) { this.id = id; @@ -170,6 +182,7 @@ private Builder(String id, String type) { 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 @@ private Builder(AddonInfo addonInfo) { 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 Builder withSourceBundle(@Nullable String sourceBundle) { return this; } + public Builder withDiscoveryMethods(@Nullable List discoveryMethods) { + this.discoveryMethods = discoveryMethods; + return this; + } + /** * Build an {@link AddonInfo} from this builder * @@ -226,8 +250,8 @@ public Builder withSourceBundle(@Nullable String sourceBundle) { * @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); } } } diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoList.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoList.java new file mode 100644 index 00000000000..6638d9dec49 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoList.java @@ -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 addons; + + public List getAddons() { + List addons = this.addons; + return addons != null ? addons : List.of(); + } + + public AddonInfoList setAddons(@Nullable List addons) { + this.addons = addons; + return this; + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoProvider.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoProvider.java index edd8b095883..b444451afb4 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoProvider.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoProvider.java @@ -31,15 +31,15 @@ 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. diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoRegistry.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoRegistry.java index 678c329ca2d..338a3cbfad6 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoRegistry.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoRegistry.java @@ -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 @@ protected void addAddonInfoProvider(AddonInfoProvider addonInfoProvider) { 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. + *

+ * 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. + *

+ * 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 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 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. * diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonMatchProperty.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonMatchProperty.java new file mode 100644 index 00000000000..ac5ccebe9b3 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonMatchProperty.java @@ -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); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonDiscoveryMethodConverter.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonDiscoveryMethodConverter.java new file mode 100644 index 00000000000..39110721aec --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonDiscoveryMethodConverter.java @@ -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 { + + 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 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); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoAddonsXmlProvider.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoAddonsXmlProvider.java new file mode 100644 index 00000000000..aeae88583bf --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoAddonsXmlProvider.java @@ -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 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 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 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)); + } + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoConverter.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoConverter.java index 257dfff682f..2e1db84409a 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoConverter.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoConverter.java @@ -18,6 +18,7 @@ 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 @@ * @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 { @@ -107,6 +109,11 @@ public AddonInfoConverter() { 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 diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListConverter.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListConverter.java new file mode 100644 index 00000000000..60a93ad0027 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListConverter.java @@ -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 { + + 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 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); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListReader.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListReader.java new file mode 100644 index 00000000000..a733e169492 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListReader.java @@ -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. + *

+ * 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 { + + /** + * 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); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoReader.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoReader.java index a33cdd09cd8..47cad85e943 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoReader.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoReader.java @@ -15,6 +15,8 @@ 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.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 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. *

* This reader uses {@code XStream} and {@code StAX} to parse and convert the XML document. @@ -59,12 +62,15 @@ public AddonInfoReader() { @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 @@ protected void registerAliases(XStream xstream) { 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); } } diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonMatchPropertyConverter.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonMatchPropertyConverter.java new file mode 100644 index 00000000000..f8f5321d9de --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonMatchPropertyConverter.java @@ -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 { + + 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); + } +} diff --git a/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoListReaderTest.java b/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoListReaderTest.java new file mode 100644 index 00000000000..66c8485230f --- /dev/null +++ b/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoListReaderTest.java @@ -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 = + "" + + " " + + " automation" + + " Groovy Scripting" + + " This adds a Groovy script engine." + + " none" + + " " + + " " + + " mdns" + + " _printer._tcp.local." + + " " + + " " + + " rp" + + " .*" + + " " + + " " + + " ty" + + " hp (.*)" + + " " + + " " + + " " + + " " + + " upnp" + + " " + + " " + + " modelName" + + " Philips hue bridge" + + " " + + " " + + " " + + " " + + " " + + ""; + // @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 addonsInfos = addons.getAddons(); + assertEquals(1, addonsInfos.size()); + AddonInfo addon = addonsInfos.get(0); + assertNotNull(addon); + List 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 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()); + } +} diff --git a/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoRegistryMergeTest.java b/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoRegistryMergeTest.java new file mode 100644 index 00000000000..335dc1012ec --- /dev/null +++ b/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoRegistryMergeTest.java @@ -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()); + } +} diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/XmlDocumentReader.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/XmlDocumentReader.java index caa8bfd343a..5c2754c6f6f 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/XmlDocumentReader.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/XmlDocumentReader.java @@ -19,6 +19,7 @@ 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 @@ protected void configureSecurity(XStream xstream) { 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); + } } diff --git a/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java b/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java index 1531c2ab4b8..3e109b7a5a9 100644 --- a/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java +++ b/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java @@ -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 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 void assertThatAddonInfoIsReadProperly() throws Exception { 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 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 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()); }); } diff --git a/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml b/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml index 4d351403227..9e5db0944bd 100644 --- a/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml +++ b/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml @@ -30,4 +30,21 @@ + + + + mdns + _hue._tcp.local. + + + upnp + + + modelName + Philips hue bridge + + + + +