diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/file/SemanticTagFile.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/file/SemanticTagFile.java new file mode 100644 index 00000000000..9c9eba8ab23 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/file/SemanticTagFile.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.semantics.internal.file; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.semantics.dto.SemanticTagDTO; + +/** + * The {@link SemanticTagFile} maps a configuration file containing a list of semantic tags. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class SemanticTagFile { + + private int version = 1; + private List tags = List.of(); + + public int getVersion() { + return version; + } + + public List getTags() { + return tags; + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/file/YamlFileSemanticTagProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/file/YamlFileSemanticTagProvider.java new file mode 100644 index 00000000000..5c272f1fa88 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/file/YamlFileSemanticTagProvider.java @@ -0,0 +1,146 @@ +/** + * 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.semantics.internal.file; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.AbstractProvider; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagProvider; +import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.dto.SemanticTagDTOMapper; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +/** + * {@link YamlFileSemanticTagProvider} 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, YamlFileSemanticTagProvider.class }) +public class YamlFileSemanticTagProvider extends AbstractProvider + implements SemanticTagProvider, WatchService.WatchEventListener { + + private static final String TAGS_DIRECTORY = "tags"; + + private final Logger logger = LoggerFactory.getLogger(YamlFileSemanticTagProvider.class); + + private final WatchService watchService; + private final Path watchPath; + private final ObjectMapper yamlReader; + private final Map> tags = new ConcurrentHashMap<>(); + + @Activate + public YamlFileSemanticTagProvider( + @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) { + this.watchService = watchService; + this.watchPath = watchService.getWatchPath().resolve(TAGS_DIRECTORY); + watchService.registerListener(this, watchPath); + + yamlReader = new ObjectMapper(new YAMLFactory()); + yamlReader.findAndRegisterModules(); + + // Load all YAML files + try (Stream stream = Files.walk(watchPath)) { + stream.filter(path -> !Files.isDirectory(path) && path.toFile().getName().endsWith(".yaml")) + .forEach(path -> tags.put(path, readYamlFile(path).values().stream().toList())); + } catch (IOException e) { + } + } + + @Deactivate + public void deactivate() { + watchService.unregisterListener(this); + tags.clear(); + } + + @Override + public Collection getAll() { + return tags.values().stream().flatMap(List::stream).sorted(Comparator.comparing(SemanticTag::getUID)).toList(); + } + + @Override + public synchronized void processWatchEvent(Kind kind, Path path) { + Path finalPath = watchPath.resolve(path); + logger.debug("processWatchEvent {} {}", kind, finalPath.toFile().getAbsolutePath()); + if (Files.isDirectory(finalPath) || !finalPath.toFile().getName().endsWith(".yaml")) { + logger.debug("processWatchEvent {} ignored", finalPath.toFile().getAbsolutePath()); + return; + } + Map oldTags; + Map newTags; + if (kind == WatchService.Kind.DELETE) { + newTags = Map.of(); + oldTags = Objects.requireNonNullElse(tags.remove(finalPath), List. of()).stream() + .collect(Collectors.toMap(SemanticTag::getUID, tag -> tag)); + } else { + oldTags = Objects.requireNonNullElse(tags.get(finalPath), List. of()).stream() + .collect(Collectors.toMap(SemanticTag::getUID, tag -> tag)); + newTags = readYamlFile(finalPath); + tags.put(finalPath, newTags.values().stream().toList()); + } + Collection removed = oldTags.keySet().stream().filter(tagId -> !newTags.containsKey(tagId)) + .sorted(Comparator.reverseOrder()).toList(); + Collection added = newTags.keySet().stream().filter(tagId -> !oldTags.containsKey(tagId)) + .sorted(Comparator.naturalOrder()).toList(); + Collection updated = oldTags.keySet().stream().filter(tagId -> newTags.containsKey(tagId)).toList(); + + removed.forEach(tagId -> notifyListenersAboutRemovedElement(Objects.requireNonNull(oldTags.get(tagId)))); + added.forEach(tagId -> notifyListenersAboutAddedElement(Objects.requireNonNull(newTags.get(tagId)))); + updated.forEach(tagId -> notifyListenersAboutUpdatedElement(Objects.requireNonNull(oldTags.get(tagId)), + Objects.requireNonNull(newTags.get(tagId)))); + } + + private Map readYamlFile(Path path) { + logger.debug("readYamlFile {}", path.toFile().getAbsolutePath()); + try { + SemanticTagFile tagsFile = yamlReader.readValue(path.toFile(), SemanticTagFile.class); + Map tags = new HashMap<>(); + tagsFile.getTags().forEach(dto -> { + SemanticTag tag = SemanticTagDTOMapper.map(dto); + if (tag != null) { + tags.put(tag.getUID(), tag); + } + }); + return tags; + } catch (IOException e) { + logger.warn("Failed to read Yaml file {}: {}", path.toFile().getAbsolutePath(), e.getMessage()); + } + return Map.of(); + } +} diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 615a415ab7b..ba8ed3ae829 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -55,6 +55,7 @@ mvn:org.openhab.core.bundles/org.openhab.core.persistence/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.semantics/${project.version} openhab.tp-asm + openhab.tp-jackson mvn:org.openhab.core.bundles/org.openhab.core.thing/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.transform/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.audio/${project.version}