Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Introduce metadata for all add-ons #3050

Merged
merged 8 commits into from
Jan 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bom/openhab-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.binding.xml</artifactId>
<artifactId>org.openhab.core.addon.xml</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.binding.xml</name>
<name>org.openhab.core.addon.xml</name>
<comment></comment>
<projects>
</projects>
Expand Down
70 changes: 70 additions & 0 deletions bundles/org.openhab.core.addon.xml/addon-1.0.0.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
targetNamespace="https://openhab.org/schemas/addon/v1.0.0">

<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="addonType"/>
<xs:element name="name" type="xs:string"/>
<xs:element name="description" type="xs:string"/>
<xs:element name="author" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>The organization maintaining the add-on (e.g. openHAB). Individual developer names should be avoided.</xs:documentation>
</xs:annotation>
</xs:element>
Comment on lines +15 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for commenting code already merged, but thought it would be easiest for proper context. Should it be mentioned that it's not needed to use "openHAB" as author here? At least it seems none of the add-ons in the openhab-addons repository have this, and the skeleton script doesn't create it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be the right time to drop this field now altogether - we more or less only kept it for backward-compatibility, but as we are breaking this now anyhow, I think we could remove it from the XSD.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or are we using it for Marketplace entries, @ghys?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The marketplace add-on service will build the add-on with what Discourse returns as the author of the topic as the add-on author:

return Addon.create(id).withType(type).withContentType(contentType).withImageLink(topic.imageUrl)
.withAuthor(author).withProperties(properties).withLabel(title).withInstalled(installed)
.withMaturity(maturity).withCompatible(compatible).withLink(link).build();

and the Karaf add-on service will put the constant "openHAB" in that field (and set it as "verified"):

Addon.Builder addon = Addon.create(id).withContentType(ADDONS_CONTENT_TYPE).withType(type)
.withVersion(feature.getVersion()).withAuthor(ADDONS_AUTHOR, true).withInstalled(isInstalled);

The author and the checkmark are displayed in the UI, notably in the information tables in each add-on page. For "verified" add-ons it's also displayed in the list.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ghys, this means that this field is definitely deprecated then.
@J-N-K Would you agree that this is a good moment to remove it from the XSD then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

<xs:element name="connection" type="connectionType" minOccurs="0"/>
<xs:element name="countries" type="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: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:element>

<xs:simpleType name="addonType">
<xs:restriction base="xs:string">
<xs:enumeration value="automation"/>
<xs:enumeration value="binding"/>
<xs:enumeration value="misc"/>
<xs:enumeration value="persistence"/>
<xs:enumeration value="transformation"/>
<xs:enumeration value="ui"/>
<xs:enumeration value="voice"/>
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="connectionType">
<xs:restriction base="xs:string">
<xs:enumeration value="local"/>
<xs:enumeration value="cloud"/>
<xs:enumeration value="cloudDiscovery"/>
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="countryType">
<xs:restriction base="xs:string">
<xs:pattern value="[a-z]{2}(,[a-z]{2})*"/>
</xs:restriction>
</xs:simpleType>

</xs:schema>
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<version>4.0.0-SNAPSHOT</version>
</parent>

<artifactId>org.openhab.core.binding.xml</artifactId>
<artifactId>org.openhab.core.addon.xml</artifactId>

<name>openHAB Core :: Bundles :: Binding XML</name>
<name>openHAB Core :: Bundles :: Add-on XML</name>

<dependencies>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* 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.xml.internal;

import java.net.URI;
import java.util.List;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionBuilder;
import org.openhab.core.config.xml.util.ConverterAttributeMapValidator;
import org.openhab.core.config.xml.util.GenericUnmarshaller;
import org.openhab.core.config.xml.util.NodeIterator;

import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;

/**
* The {@link AddonInfoConverter} is a concrete implementation of the {@code XStream} {@link Converter} interface used
* to convert add-on information within an XML document into a {@link AddonInfoXmlResult} object.
* This converter converts {@code addon} XML tags.
*
* @author Michael Grammling - Initial contribution
* @author Andre Fuechsel - Made author tag optional
* @author Jan N. Klug - Refactored to cover all add-ons
*/
@NonNullByDefault
public class AddonInfoConverter extends GenericUnmarshaller<AddonInfoXmlResult> {
private static final String CONFIG_DESCRIPTION_URI_PLACEHOLDER = "addonInfoConverter:placeHolder";
private final ConverterAttributeMapValidator attributeMapValidator;

public AddonInfoConverter() {
super(AddonInfoXmlResult.class);

attributeMapValidator = new ConverterAttributeMapValidator(Map.of("id", true, "schemaLocation", false));
}

private @Nullable ConfigDescription readConfigDescription(NodeIterator nodeIterator) {
Object nextNode = nodeIterator.next();

if (nextNode != null) {
if (nextNode instanceof ConfigDescription configDescription) {
return configDescription;
}

nodeIterator.revert();
}

return null;
}

@Override
public @Nullable Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
// read attributes
Map<String, String> attributes = attributeMapValidator.readValidatedAttributes(reader);

String id = requireNonEmpty(attributes.get("id"), "Add-on id attribute is null or empty");

// set automatically extracted URI for a possible 'config-description' section
context.put("config-description.uri", CONFIG_DESCRIPTION_URI_PLACEHOLDER);

// read values
List<?> nodes = (List<?>) context.convertAnother(context, List.class);
NodeIterator nodeIterator = new NodeIterator(nodes);

String type = requireNonEmpty((String) nodeIterator.nextValue("type", true), "Add-on type is null or empty");

String name = requireNonEmpty((String) nodeIterator.nextValue("name", true),
"Add-on name attribute is null or empty");
String description = requireNonEmpty((String) nodeIterator.nextValue("description", true),
"Add-on description is null or empty");

AddonInfo.Builder addonInfo = AddonInfo.builder(id, type).withName(name).withDescription(description);
addonInfo.withAuthor((String) nodeIterator.nextValue("author", false));
addonInfo.withConnection((String) nodeIterator.nextValue("connection", false));

addonInfo.withServiceId((String) nodeIterator.nextValue("service-id", false));

String configDescriptionURI = nodeIterator.nextAttribute("config-description-ref", "uri", false);
ConfigDescription configDescription = null;
if (configDescriptionURI == null) {
configDescription = readConfigDescription(nodeIterator);
if (configDescription != null) {
configDescriptionURI = configDescription.getUID().toString();
// if config description is missing the URI, recreate it with correct URI
if (CONFIG_DESCRIPTION_URI_PLACEHOLDER.equals(configDescriptionURI)) {
configDescriptionURI = type + ":" + id;
configDescription = ConfigDescriptionBuilder.create(URI.create(configDescriptionURI))
.withParameterGroups(configDescription.getParameterGroups())
.withParameters(configDescription.getParameters()).build();
}
}
}
addonInfo.withConfigDescriptionURI(configDescriptionURI);

nodeIterator.assertEndOfType();

// create object
return new AddonInfoXmlResult(addonInfo.build(), configDescription);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.binding.xml.internal;
package org.openhab.core.addon.xml.internal;

import java.util.List;

Expand All @@ -33,23 +33,24 @@
import com.thoughtworks.xstream.XStream;

/**
* The {@link BindingInfoReader} reads XML documents, which contain the {@code binding} XML tag,
* and converts them to {@link BindingInfoXmlResult} objects.
* The {@link AddonInfoReader} reads XML documents, which contain the {@code binding} 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.
*
* @author Michael Grammling - Initial contribution
* @author Alex Tugarev - Extended by options and filter criteria
* @author Chris Jackson - Add parameter groups
* @author Jan N. Klug - Refactored to cover all add-ons
*/
@NonNullByDefault
public class BindingInfoReader extends XmlDocumentReader<BindingInfoXmlResult> {
public class AddonInfoReader extends XmlDocumentReader<AddonInfoXmlResult> {

/**
* The default constructor of this class.
*/
public BindingInfoReader() {
ClassLoader classLoader = BindingInfoReader.class.getClassLoader();
public AddonInfoReader() {
ClassLoader classLoader = AddonInfoReader.class.getClassLoader();
if (classLoader != null) {
super.setClassLoader(classLoader);
}
Expand All @@ -59,7 +60,7 @@ public BindingInfoReader() {
protected void registerConverters(XStream xstream) {
xstream.registerConverter(new NodeAttributesConverter());
xstream.registerConverter(new NodeValueConverter());
xstream.registerConverter(new BindingInfoConverter());
xstream.registerConverter(new AddonInfoConverter());
xstream.registerConverter(new ConfigDescriptionConverter());
xstream.registerConverter(new ConfigDescriptionParameterConverter());
xstream.registerConverter(new ConfigDescriptionParameterGroupConverter());
Expand All @@ -68,11 +69,11 @@ protected void registerConverters(XStream xstream) {

@Override
protected void registerAliases(XStream xstream) {
xstream.alias("binding", BindingInfoXmlResult.class);
xstream.alias("addon", AddonInfoXmlResult.class);
xstream.alias("name", NodeValue.class);
xstream.alias("description", NodeValue.class);
xstream.alias("author", NodeValue.class);
xstream.alias("service-id", NodeValue.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What has happened to the service-id?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I planned to change it to config-id and later decided against that. I have reverted this changed (same answer below).

xstream.alias("type", NodeValue.class);
xstream.alias("config-description", ConfigDescription.class);
xstream.alias("config-description-ref", NodeAttributes.class);
xstream.alias("parameter", ConfigDescriptionParameter.class);
Expand All @@ -81,5 +82,6 @@ protected void registerAliases(XStream xstream) {
xstream.alias("option", NodeValue.class);
xstream.alias("filter", List.class);
xstream.alias("criteria", FilterCriteria.class);
xstream.alias("service-id", NodeValue.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.binding.xml.internal;
package org.openhab.core.addon.xml.internal;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.binding.BindingInfo;
import org.openhab.core.binding.BindingInfoProvider;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.xml.AbstractXmlConfigDescriptionProvider;
import org.openhab.core.config.xml.osgi.XmlDocumentProvider;
Expand All @@ -23,52 +21,51 @@
import org.slf4j.LoggerFactory;

/**
* The {@link BindingInfoXmlProvider} is responsible managing any created
* objects by a {@link BindingInfoReader} for a certain bundle.
* The {@link AddonInfoXmlProvider} is responsible managing any created
* objects by a {@link AddonInfoReader} for a certain bundle.
* <p>
* This implementation registers each {@link BindingInfo} object at the {@link XmlBindingInfoProvider} which is itself
* registered as {@link BindingInfoProvider} service at the <i>OSGi</i> service registry.
* This implementation registers each {@link AddonInfo} object at the {@link XmlAddonInfoProvider} which is itself
* registered as {@link AddonInfoProvider} service at the <i>OSGi</i> service registry.
* <p>
* If there is a {@link ConfigDescription} object within the {@link BindingInfoXmlResult} object, it is added to the
* If there is a {@link ConfigDescription} object within the {@link AddonInfoXmlResult} object, it is added to the
* {@link AbstractXmlConfigDescriptionProvider} which is itself registered as <i>OSGi</i> service at the service
* registry.
*
* @author Michael Grammling - Initial contribution
*
* @see BindingInfoXmlProviderFactory
* @author Jan N. Klug - Refactored to cover all add-ons
*/
@NonNullByDefault
public class BindingInfoXmlProvider implements XmlDocumentProvider<BindingInfoXmlResult> {
public class AddonInfoXmlProvider implements XmlDocumentProvider<AddonInfoXmlResult> {

private Logger logger = LoggerFactory.getLogger(BindingInfoXmlProvider.class);
private Logger logger = LoggerFactory.getLogger(AddonInfoXmlProvider.class);

private final Bundle bundle;

private final XmlBindingInfoProvider bindingInfoProvider;
private final XmlAddonInfoProvider addonInfoProvider;
private final AbstractXmlConfigDescriptionProvider configDescriptionProvider;

public BindingInfoXmlProvider(Bundle bundle, XmlBindingInfoProvider bindingInfoProvider,
public AddonInfoXmlProvider(Bundle bundle, XmlAddonInfoProvider addonInfoProvider,
AbstractXmlConfigDescriptionProvider configDescriptionProvider) throws IllegalArgumentException {
if (bundle == null) {
throw new IllegalArgumentException("The Bundle must not be null!");
}

if (bindingInfoProvider == null) {
throw new IllegalArgumentException("The XmlBindingInfoProvider must not be null!");
if (addonInfoProvider == null) {
throw new IllegalArgumentException("The XmlAddonInfoProvider must not be null!");
}

if (configDescriptionProvider == null) {
throw new IllegalArgumentException("The XmlConfigDescriptionProvider must not be null!");
}

this.bundle = bundle;
this.bindingInfoProvider = bindingInfoProvider;
this.addonInfoProvider = addonInfoProvider;
this.configDescriptionProvider = configDescriptionProvider;
}

@Override
public synchronized void addingObject(BindingInfoXmlResult bindingInfoXmlResult) {
ConfigDescription configDescription = bindingInfoXmlResult.getConfigDescription();
public synchronized void addingObject(AddonInfoXmlResult addonInfoXmlResult) {
ConfigDescription configDescription = addonInfoXmlResult.configDescription();

if (configDescription != null) {
try {
Expand All @@ -78,7 +75,7 @@ public synchronized void addingObject(BindingInfoXmlResult bindingInfoXmlResult)
}
}

bindingInfoProvider.add(bundle, bindingInfoXmlResult.getBindingInfo());
addonInfoProvider.add(bundle, addonInfoXmlResult.addonInfo());
}

@Override
Expand All @@ -88,7 +85,7 @@ public void addingFinished() {

@Override
public synchronized void release() {
this.bindingInfoProvider.removeAll(bundle);
this.addonInfoProvider.removeAll(bundle);
this.configDescriptionProvider.removeAll(bundle);
}
}
Loading