From 070de55b27390a0d431a2312f1ccd6d0363932a8 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sun, 10 Dec 2023 11:26:03 +0100 Subject: [PATCH] Add a YAML file provider for semantic tags (#3659) * Add a YAML file provider for semantic tags Files in folder conf/tags are loaded by this provider. Related to #3619 Signed-off-by: Laurent Garnier --- bom/openhab-core/pom.xml | 6 + .../org.openhab.core.model.yaml/.classpath | 29 +++ bundles/org.openhab.core.model.yaml/.project | 23 ++ bundles/org.openhab.core.model.yaml/NOTICE | 14 ++ bundles/org.openhab.core.model.yaml/pom.xml | 29 +++ .../core/model/yaml/AbstractYamlFile.java | 65 ++++++ .../openhab/core/model/yaml/YamlElement.java | 39 ++++ .../org/openhab/core/model/yaml/YamlFile.java | 48 +++++ .../core/model/yaml/YamlModelListener.java | 75 +++++++ .../core/model/yaml/YamlParseException.java | 39 ++++ .../yaml/internal/YamlModelRepository.java | 198 ++++++++++++++++++ .../internal/semantics/YamlSemanticTag.java | 66 ++++++ .../semantics/YamlSemanticTagProvider.java | 126 +++++++++++ .../internal/semantics/YamlSemanticTags.java | 36 ++++ .../internal/SemanticsMetadataProvider.java | 33 +++ bundles/pom.xml | 1 + .../openhab-core/src/main/feature/feature.xml | 8 + 17 files changed, 835 insertions(+) create mode 100644 bundles/org.openhab.core.model.yaml/.classpath create mode 100644 bundles/org.openhab.core.model.yaml/.project create mode 100644 bundles/org.openhab.core.model.yaml/NOTICE create mode 100644 bundles/org.openhab.core.model.yaml/pom.xml create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.java create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java diff --git a/bom/openhab-core/pom.xml b/bom/openhab-core/pom.xml index 4a96fcf9a91..8346e8dd549 100644 --- a/bom/openhab-core/pom.xml +++ b/bom/openhab-core/pom.xml @@ -514,6 +514,12 @@ ${project.version} compile + + org.openhab.core.bundles + org.openhab.core.model.yaml + ${project.version} + compile + org.openhab.core.bundles org.openhab.core.ui diff --git a/bundles/org.openhab.core.model.yaml/.classpath b/bundles/org.openhab.core.model.yaml/.classpath new file mode 100644 index 00000000000..58cd399d639 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/.classpath @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.model.yaml/.project b/bundles/org.openhab.core.model.yaml/.project new file mode 100644 index 00000000000..1b130d3a787 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.model.yaml + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.core.model.yaml/NOTICE b/bundles/org.openhab.core.model.yaml/NOTICE new file mode 100644 index 00000000000..19bf5f2cfa8 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/NOTICE @@ -0,0 +1,14 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-core + diff --git a/bundles/org.openhab.core.model.yaml/pom.xml b/bundles/org.openhab.core.model.yaml/pom.xml new file mode 100644 index 00000000000..00941ec4b21 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/pom.xml @@ -0,0 +1,29 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 4.1.0-SNAPSHOT + + + org.openhab.core.model.yaml + + openHAB Core :: Bundles :: Model YAML + + + + org.openhab.core.bundles + org.openhab.core + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.semantics + ${project.version} + + + diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java new file mode 100644 index 00000000000..d99c8151ca1 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java @@ -0,0 +1,65 @@ +/** + * 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.model.yaml; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AbstractYamlFile} is the DTO base class used to map a YAML configuration file. + * + * A YAML configuration file consists of a version and a list of elements. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractYamlFile implements YamlFile { + + private final Logger logger = LoggerFactory.getLogger(AbstractYamlFile.class); + + /** + * YAML file version + */ + public int version; + + @Override + public abstract List getElements(); + + @Override + public int getVersion() { + return version; + } + + @Override + public boolean isValid() { + // Checking duplicated elements + List elts = getElements(); + long nbDistinctIds = elts.stream().map(YamlElement::getId).distinct().count(); + if (nbDistinctIds < elts.size()) { + logger.debug("Elements with same ids detected in the file"); + return false; + } + + // Checking each element + for (int i = 0; i < elts.size(); i++) { + if (!elts.get(i).isValid()) { + logger.debug("Error in element {}", i + 1); + return false; + } + } + return true; + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java new file mode 100644 index 00000000000..2b1d8b16a03 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java @@ -0,0 +1,39 @@ +/** + * 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.model.yaml; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link YamlElement} interface offers an identifier and a check validity method + * to any element defined in a YAML configuration file. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface YamlElement { + + /** + * Get the identifier of the YAML element + * + * @return the identifier as a string + */ + String getId(); + + /** + * Check that the YAML element is valid + * + * @return true if all the checks are OK + */ + boolean isValid(); +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java new file mode 100644 index 00000000000..40f5ce4c9c8 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java @@ -0,0 +1,48 @@ +/** + * 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.model.yaml; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link YamlFile} is the interface to manage the generic content of a YAML configuration file. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface YamlFile { + + /** + * Get the list of elements present in the YAML file. + * + * @return the list of elements + */ + List getElements(); + + /** + * Get the version present in the YAML file. + * + * @return the version in the file + */ + int getVersion(); + + /** + * Check that the file content is valid. + * It includes the check of duplicated elements (same identifier) and the check of each element. + * + * @return true if all the checks are OK + */ + boolean isValid(); +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java new file mode 100644 index 00000000000..b15a8d9d639 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java @@ -0,0 +1,75 @@ +/** + * 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.model.yaml; + +import java.util.Collection; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link YamlModelListener} interface is responsible for managing a particular model type + * with data processed from YAML configuration files. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface YamlModelListener { + + /** + * Method called by the model repository when elements from a model are added. + * + * @param modelName the name of the model + * @param elements the collection of added elements + */ + void addedModel(String modelName, Collection elements); + + /** + * Method called by the model repository when elements from a model are updated. + * + * @param modelName the name of the model + * @param elements the collection of updated elements + */ + void updatedModel(String modelName, Collection elements); + + /** + * Method called by the model repository when elements from a model are removed. + * + * @param modelName the name of the model + * @param elements the collection of removed elements + */ + void removedModel(String modelName, Collection elements); + + /** + * Get the root name of this model type which is also the name of the root folder + * containing the user files for this model type. + * + * A path is unexpected. What is expected is for example "items" or "things". + * + * @return the model root name + */ + String getRootName(); + + /** + * Get the DTO class to be used for a file providing objects for this model type. + * + * @return the DTO file class + */ + Class getFileClass(); + + /** + * Get the DTO class to be used for each object of this model type. + * + * @return the DTO element class + */ + Class getElementClass(); +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java new file mode 100644 index 00000000000..bae53ba78d9 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java @@ -0,0 +1,39 @@ +/** + * 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.model.yaml; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link YamlParseException} is used when an error is detected when parsing the content + * of a YAML configuration file. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class YamlParseException extends Exception { + + private static final long serialVersionUID = 1L; + + public YamlParseException(String message) { + super(message); + } + + public YamlParseException(Throwable cause) { + super(cause); + } + + public YamlParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.java new file mode 100644 index 00000000000..a7d04138762 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.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.model.yaml.internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.model.yaml.AbstractYamlFile; +import org.openhab.core.model.yaml.YamlElement; +import org.openhab.core.model.yaml.YamlModelListener; +import org.openhab.core.model.yaml.YamlParseException; +import org.openhab.core.service.WatchService; +import org.openhab.core.service.WatchService.Kind; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +/** + * The {@link YamlModelRepository} is an OSGi service, that encapsulates all YAML file processing + * including file monitoring to detect created, updated and removed YAML configuration files. + * Data processed from these files are consumed by registered OSGi services that implement {@link YamlModelListener}. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true) +public class YamlModelRepository implements WatchService.WatchEventListener { + + private final Logger logger = LoggerFactory.getLogger(YamlModelRepository.class); + + private final WatchService watchService; + private final Path watchPath; + private final ObjectMapper yamlReader; + + private final Map>> listeners = new ConcurrentHashMap<>(); + private final Map> objects = new ConcurrentHashMap<>(); + + @Activate + public YamlModelRepository(@Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) { + this.watchService = watchService; + this.yamlReader = new ObjectMapper(new YAMLFactory()); + yamlReader.findAndRegisterModules(); + + watchService.registerListener(this, Path.of("")); + watchPath = watchService.getWatchPath(); + } + + @Deactivate + public void deactivate() { + watchService.unregisterListener(this); + } + + // The method is "synchronized" to avoid concurrent files processing + @Override + public synchronized void processWatchEvent(Kind kind, Path path) { + Path fullPath = watchPath.resolve(path); + String dirName = path.subpath(0, 1).toString(); + + if (Files.isDirectory(fullPath) || fullPath.toFile().isHidden() || !fullPath.toString().endsWith(".yaml")) { + logger.trace("Ignored {}", fullPath); + return; + } + + getListeners(dirName).forEach(listener -> processWatchEvent(dirName, kind, fullPath, listener)); + } + + private void processWatchEvent(String dirName, Kind kind, Path fullPath, YamlModelListener listener) { + logger.debug("processWatchEvent dirName={} kind={} fullPath={} listener={}", dirName, kind, fullPath, + listener.getClass().getSimpleName()); + Map oldObjects; + Map newObjects; + if (kind == WatchService.Kind.DELETE) { + newObjects = Map.of(); + + List oldListObjects = objects.remove(fullPath); + if (oldListObjects == null) { + oldListObjects = List.of(); + } + oldObjects = oldListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj)); + } else { + AbstractYamlFile yamlData; + try { + yamlData = readYamlFile(fullPath, listener.getFileClass()); + } catch (YamlParseException e) { + logger.warn("Failed to parse Yaml file {} with DTO class {}: {}", fullPath, + listener.getFileClass().getName(), e.getMessage()); + return; + } + List newListObjects = yamlData.getElements(); + newObjects = newListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj)); + + List oldListObjects = objects.get(fullPath); + if (oldListObjects == null) { + oldListObjects = List.of(); + } + oldObjects = oldListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj)); + + objects.put(fullPath, newListObjects); + } + + String modelName = fullPath.toFile().getName(); + modelName = modelName.substring(0, modelName.indexOf(".yaml")); + List listElements; + listElements = oldObjects.entrySet().stream() + .filter(entry -> entry.getValue().getClass().equals(listener.getElementClass()) + && !newObjects.containsKey(entry.getKey())) + .map(Map.Entry::getValue).toList(); + if (!listElements.isEmpty()) { + listener.removedModel(modelName, listElements); + } + + listElements = newObjects.entrySet().stream() + .filter(entry -> entry.getValue().getClass().equals(listener.getElementClass()) + && !oldObjects.containsKey(entry.getKey())) + .map(Map.Entry::getValue).toList(); + if (!listElements.isEmpty()) { + listener.addedModel(modelName, listElements); + } + + // Object is ignored if unchanged + listElements = newObjects.entrySet().stream() + .filter(entry -> entry.getValue().getClass().equals(listener.getElementClass()) + && oldObjects.containsKey(entry.getKey()) + && !entry.getValue().equals(oldObjects.get(entry.getKey()))) + .map(Map.Entry::getValue).toList(); + if (!listElements.isEmpty()) { + listener.updatedModel(modelName, listElements); + } + } + + private AbstractYamlFile readYamlFile(Path path, Class dtoClass) + throws YamlParseException { + logger.debug("readYamlFile {} with {}", path.toFile().getAbsolutePath(), dtoClass.getName()); + try { + AbstractYamlFile dto = yamlReader.readValue(path.toFile(), dtoClass); + if (!dto.isValid()) { + throw new YamlParseException("The file is not valid, some checks failed!"); + } + return dto; + } catch (IOException e) { + throw new YamlParseException(e); + } + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected void addYamlModelListener(YamlModelListener listener) { + String dirName = listener.getRootName(); + logger.debug("Adding model listener for {}", dirName); + getListeners(dirName).add(listener); + + // Load all existing YAML files + try (Stream stream = Files.walk(watchPath.resolve(dirName))) { + stream.forEach(path -> { + if (!Files.isDirectory(path) && !path.toFile().isHidden() && path.toString().endsWith(".yaml")) { + processWatchEvent(dirName, Kind.CREATE, path, listener); + } + }); + } catch (IOException ignored) { + } + } + + protected void removeYamlModelListener(YamlModelListener listener) { + String dirName = listener.getRootName(); + logger.debug("Removing model listener for {}", dirName); + getListeners(dirName).remove(listener); + } + + private List> getListeners(String dirName) { + return Objects.requireNonNull(listeners.computeIfAbsent(dirName, k -> new CopyOnWriteArrayList<>())); + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java new file mode 100644 index 00000000000..9d8f0865abd --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java @@ -0,0 +1,66 @@ +/** + * 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.model.yaml.internal.semantics; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.model.yaml.YamlElement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link YamlSemanticTag} is a data transfer object used to serialize a semantic tag + * in a YAML configuration file. + * + * @author Laurent Garnier - Initial contribution + */ +public class YamlSemanticTag implements YamlElement { + + private final Logger logger = LoggerFactory.getLogger(YamlSemanticTag.class); + + public String uid; + public String label; + public String description; + public List synonyms; + + public YamlSemanticTag() { + } + + @Override + public String getId() { + return uid; + } + + @Override + public boolean isValid() { + if (uid == null) { + logger.debug("uid missing"); + return false; + } + return true; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } else if (obj == null || getClass() != obj.getClass()) { + return false; + } + YamlSemanticTag that = (YamlSemanticTag) obj; + return Objects.equals(uid, that.uid) && Objects.equals(label, that.label) + && Objects.equals(description, that.description) && Objects.equals(synonyms, that.synonyms); + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java new file mode 100644 index 00000000000..4f13120b9a1 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java @@ -0,0 +1,126 @@ +/** + * 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.model.yaml.internal.semantics; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.AbstractProvider; +import org.openhab.core.model.yaml.AbstractYamlFile; +import org.openhab.core.model.yaml.YamlElement; +import org.openhab.core.model.yaml.YamlModelListener; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagProvider; +import org.openhab.core.semantics.SemanticTagRegistry; +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; + +/** + * {@link YamlSemanticTagProvider} is an OSGi service, that allows to define semantic tags + * in YAML configuration files in folder conf/tags. + * Files can be added, updated or removed at runtime. + * These semantic tags are automatically exposed to the {@link SemanticTagRegistry}. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { SemanticTagProvider.class, YamlSemanticTagProvider.class, + YamlModelListener.class }) +public class YamlSemanticTagProvider extends AbstractProvider + implements SemanticTagProvider, YamlModelListener { + + private final Logger logger = LoggerFactory.getLogger(YamlSemanticTagProvider.class); + + private final Set tags = new TreeSet<>(Comparator.comparing(SemanticTag::getUID)); + + @Activate + public YamlSemanticTagProvider() { + } + + @Deactivate + public void deactivate() { + tags.clear(); + } + + @Override + public Collection getAll() { + return tags; + } + + @Override + public String getRootName() { + return "tags"; + } + + @Override + public Class getFileClass() { + return YamlSemanticTags.class; + } + + @Override + public Class getElementClass() { + return YamlSemanticTag.class; + } + + @Override + public void addedModel(String modelName, Collection elements) { + List added = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e)) + .sorted(Comparator.comparing(SemanticTag::getUID)).toList(); + tags.addAll(added); + added.forEach(t -> { + logger.debug("model {} added tag {}", modelName, t.getUID()); + notifyListenersAboutAddedElement(t); + }); + } + + @Override + public void updatedModel(String modelName, Collection elements) { + List updated = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e)).toList(); + updated.forEach(t -> { + tags.stream().filter(tag -> tag.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldTag -> { + tags.remove(oldTag); + tags.add(t); + logger.debug("model {} updated tag {}", modelName, t.getUID()); + notifyListenersAboutUpdatedElement(oldTag, t); + }, () -> logger.debug("model {} tag {} not found", modelName, t.getUID())); + }); + } + + @Override + public void removedModel(String modelName, Collection elements) { + List removed = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e)) + .sorted(Comparator.comparing(SemanticTag::getUID).reversed()).toList(); + removed.forEach(t -> { + tags.stream().filter(tag -> tag.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldTag -> { + tags.remove(oldTag); + logger.debug("model {} removed tag {}", modelName, t.getUID()); + notifyListenersAboutRemovedElement(oldTag); + }, () -> logger.debug("model {} tag {} not found", modelName, t.getUID())); + }); + } + + private SemanticTag mapSemanticTag(YamlSemanticTag tagDTO) { + if (tagDTO.uid == null) { + throw new IllegalArgumentException("The argument 'tagDTO.uid' must not be null."); + } + return new SemanticTagImpl(tagDTO.uid, tagDTO.label, tagDTO.description, tagDTO.synonyms); + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java new file mode 100644 index 00000000000..3c2a4ee9609 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java @@ -0,0 +1,36 @@ +/** + * 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.model.yaml.internal.semantics; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.model.yaml.AbstractYamlFile; +import org.openhab.core.model.yaml.YamlElement; + +/** + * The {@link YamlSemanticTags} is a data transfer object used to serialize a list of semantic tags + * in a YAML configuration file. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class YamlSemanticTags extends AbstractYamlFile { + + public List tags = List.of(); + + @Override + public List getElements() { + return tags; + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java index 863594d15b8..93c842f06a4 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.common.registry.AbstractProvider; +import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; @@ -33,6 +34,7 @@ import org.openhab.core.semantics.Location; import org.openhab.core.semantics.Point; import org.openhab.core.semantics.Property; +import org.openhab.core.semantics.SemanticTag; import org.openhab.core.semantics.SemanticTagRegistry; import org.openhab.core.semantics.SemanticTags; import org.openhab.core.semantics.Tag; @@ -72,11 +74,16 @@ public class SemanticsMetadataProvider extends AbstractProvider private final Map semantics = new TreeMap<>(String::compareTo); private final ItemRegistry itemRegistry; + private final SemanticTagRegistry semanticTagRegistry; + + private SemanticTagRegistryChangeListener listener; @Activate public SemanticsMetadataProvider(final @Reference ItemRegistry itemRegistry, final @Reference SemanticTagRegistry semanticTagRegistry) { this.itemRegistry = itemRegistry; + this.semanticTagRegistry = semanticTagRegistry; + this.listener = new SemanticTagRegistryChangeListener(this); } @Activate @@ -86,10 +93,12 @@ protected void activate() { processItem(item); } itemRegistry.addRegistryChangeListener(this); + semanticTagRegistry.addRegistryChangeListener(listener); } @Deactivate protected void deactivate() { + semanticTagRegistry.removeRegistryChangeListener(listener); itemRegistry.removeRegistryChangeListener(this); semantics.clear(); } @@ -280,4 +289,28 @@ public void removed(Item item) { public void updated(Item oldItem, Item item) { processItem(item); } + + private class SemanticTagRegistryChangeListener implements RegistryChangeListener { + + private SemanticsMetadataProvider provider; + + public SemanticTagRegistryChangeListener(SemanticsMetadataProvider provider) { + this.provider = provider; + } + + @Override + public void added(SemanticTag element) { + provider.allItemsChanged(List.of()); + } + + @Override + public void removed(SemanticTag element) { + provider.allItemsChanged(List.of()); + } + + @Override + public void updated(SemanticTag oldElement, SemanticTag element) { + provider.allItemsChanged(List.of()); + } + } } diff --git a/bundles/pom.xml b/bundles/pom.xml index ad744143289..01799d77195 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -102,6 +102,7 @@ org.openhab.core.model.thing org.openhab.core.model.thing.ide org.openhab.core.model.thing.runtime + org.openhab.core.model.yaml org.openhab.core.storage.json org.openhab.core.test org.openhab.core.test.magic diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 2f919eb97a1..5062c2ead67 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -390,6 +390,13 @@ mvn:org.openhab.core.bundles/org.openhab.core.model.lsp/${project.version} + + openhab-core-base + mvn:org.openhab.core.bundles/org.openhab.core.model.yaml/${project.version} + openhab.tp;filter:="(feature=jackson)" + openhab.tp-jackson + + openhab-core-base @@ -434,6 +441,7 @@ openhab-core-model-script openhab-core-model-sitemap openhab-core-model-thing + openhab-core-model-yaml openhab-core-ui-icon openhab-core-storage-json openhab-runtime-certificate