diff --git a/bom/openhab-core/pom.xml b/bom/openhab-core/pom.xml index e8f72da9320..d1f2fd3163c 100644 --- a/bom/openhab-core/pom.xml +++ b/bom/openhab-core/pom.xml @@ -496,6 +496,12 @@ <version>${project.version}</version> <scope>compile</scope> </dependency> + <dependency> + <groupId>org.openhab.core.bundles</groupId> + <artifactId>org.openhab.core.model.yaml</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + </dependency> <dependency> <groupId>org.openhab.core.bundles</groupId> <artifactId>org.openhab.core.ui</artifactId> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" output="target/classes" path="src/main/java"> + <attributes> + <attribute name="optional" value="true"/> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"> + <attributes> + <attribute name="maven.pomderived" value="true"/> + <attribute name="annotationpath" value="target/dependency"/> + </attributes> + </classpathentry> + <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> + <attributes> + <attribute name="maven.pomderived" value="true"/> + <attribute name="annotationpath" value="target/dependency"/> + </attributes> + </classpathentry> + <classpathentry kind="src" output="target/test-classes" path="src/test/java"> + <attributes> + <attribute name="optional" value="true"/> + <attribute name="maven.pomderived" value="true"/> + <attribute name="test" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="output" path="target/classes"/> +</classpath> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>org.openhab.core.model.yaml</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.m2e.core.maven2Builder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + <nature>org.eclipse.m2e.core.maven2Nature</nature> + </natures> +</projectDescription> 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 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.openhab.core.bundles</groupId> + <artifactId>org.openhab.core.reactor.bundles</artifactId> + <version>4.1.0-SNAPSHOT</version> + </parent> + + <artifactId>org.openhab.core.model.yaml</artifactId> + + <name>openHAB Core :: Bundles :: Model YAML</name> + + <dependencies> + <dependency> + <groupId>org.openhab.core.bundles</groupId> + <artifactId>org.openhab.core</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.openhab.core.bundles</groupId> + <artifactId>org.openhab.core.semantics</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> +</project> 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<? extends YamlElement> getElements(); + + @Override + public int getVersion() { + return version; + } + + @Override + public boolean isValid() { + // Checking duplicated elements + List<? extends YamlElement> 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<? extends YamlElement> 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<T extends YamlElement> { + + /** + * 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<? extends YamlElement> 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<? extends YamlElement> 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<? extends YamlElement> 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<? extends AbstractYamlFile> getFileClass(); + + /** + * Get the DTO class to be used for each object of this model type. + * + * @return the DTO element class + */ + Class<T> 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<String, List<YamlModelListener<?>>> listeners = new ConcurrentHashMap<>(); + private final Map<Path, List<? extends YamlElement>> 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<String, ? extends YamlElement> oldObjects; + Map<String, ? extends YamlElement> newObjects; + if (kind == WatchService.Kind.DELETE) { + newObjects = Map.of(); + + List<? extends YamlElement> 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<? extends YamlElement> newListObjects = yamlData.getElements(); + newObjects = newListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj)); + + List<? extends YamlElement> 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<? extends YamlElement> 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<? extends AbstractYamlFile> 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<Path> 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<YamlModelListener<?>> 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<String> 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<SemanticTag> + implements SemanticTagProvider, YamlModelListener<YamlSemanticTag> { + + private final Logger logger = LoggerFactory.getLogger(YamlSemanticTagProvider.class); + + private final Set<SemanticTag> tags = new TreeSet<>(Comparator.comparing(SemanticTag::getUID)); + + @Activate + public YamlSemanticTagProvider() { + } + + @Deactivate + public void deactivate() { + tags.clear(); + } + + @Override + public Collection<SemanticTag> getAll() { + return tags; + } + + @Override + public String getRootName() { + return "tags"; + } + + @Override + public Class<? extends AbstractYamlFile> getFileClass() { + return YamlSemanticTags.class; + } + + @Override + public Class<YamlSemanticTag> getElementClass() { + return YamlSemanticTag.class; + } + + @Override + public void addedModel(String modelName, Collection<? extends YamlElement> elements) { + List<SemanticTag> 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<? extends YamlElement> elements) { + List<SemanticTag> 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<? extends YamlElement> elements) { + List<SemanticTag> 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<YamlSemanticTag> tags = List.of(); + + @Override + public List<? extends YamlElement> 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<Metadata> private final Map<String, Metadata> 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<SemanticTag> { + + 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 70000056d92..29f7257094f 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -99,6 +99,7 @@ <module>org.openhab.core.model.thing</module> <module>org.openhab.core.model.thing.ide</module> <module>org.openhab.core.model.thing.runtime</module> + <module>org.openhab.core.model.yaml</module> <module>org.openhab.core.storage.json</module> <module>org.openhab.core.test</module> <module>org.openhab.core.test.magic</module> diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 5cdcf866a1e..c8d8c0b2dec 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -367,6 +367,13 @@ <bundle>mvn:org.openhab.core.bundles/org.openhab.core.model.lsp/${project.version}</bundle> </feature> + <feature name="openhab-core-model-yaml" version="${project.version}"> + <feature>openhab-core-base</feature> + <bundle>mvn:org.openhab.core.bundles/org.openhab.core.model.yaml/${project.version}</bundle> + <requirement>openhab.tp;filter:="(feature=jackson)"</requirement> + <feature dependency="true">openhab.tp-jackson</feature> + </feature> + <feature name="openhab-core-storage-json" version="${project.version}"> <feature>openhab-core-base</feature> @@ -411,6 +418,7 @@ <feature>openhab-core-model-script</feature> <feature>openhab-core-model-sitemap</feature> <feature>openhab-core-model-thing</feature> + <feature>openhab-core-model-yaml</feature> <feature>openhab-core-ui-icon</feature> <feature>openhab-core-storage-json</feature> <feature>openhab-runtime-certificate</feature>