Skip to content

Commit

Permalink
Add a YAML file provider for semantic tags
Browse files Browse the repository at this point in the history
Files in folder conf/tags are loaded by this provider.

Related to openhab#3619

Signed-off-by: Laurent Garnier <[email protected]>
  • Loading branch information
lolodomo committed Jun 17, 2023
1 parent 8eddad5 commit 8bc1921
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<SemanticTagDTO> tags = List.of();

public int getVersion() {
return version;
}

public List<SemanticTagDTO> getTags() {
return tags;
}
}
Original file line number Diff line number Diff line change
@@ -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<SemanticTag>
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<Path, List<SemanticTag>> 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<Path> 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<SemanticTag> 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<String, SemanticTag> oldTags;
Map<String, SemanticTag> newTags;
if (kind == WatchService.Kind.DELETE) {
newTags = Map.of();
oldTags = Objects.requireNonNullElse(tags.remove(finalPath), List.<SemanticTag> of()).stream()
.collect(Collectors.toMap(SemanticTag::getUID, tag -> tag));
} else {
oldTags = Objects.requireNonNullElse(tags.get(finalPath), List.<SemanticTag> of()).stream()
.collect(Collectors.toMap(SemanticTag::getUID, tag -> tag));
newTags = readYamlFile(finalPath);
tags.put(finalPath, newTags.values().stream().toList());
}
Collection<String> removed = oldTags.keySet().stream().filter(tagId -> !newTags.containsKey(tagId))
.sorted(Comparator.reverseOrder()).toList();
Collection<String> added = newTags.keySet().stream().filter(tagId -> !oldTags.containsKey(tagId))
.sorted(Comparator.naturalOrder()).toList();
Collection<String> 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<String, SemanticTag> readYamlFile(Path path) {
logger.debug("readYamlFile {}", path.toFile().getAbsolutePath());
try {
SemanticTagFile tagsFile = yamlReader.readValue(path.toFile(), SemanticTagFile.class);
Map<String, SemanticTag> 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();
}
}
1 change: 1 addition & 0 deletions features/karaf/openhab-core/src/main/feature/feature.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.persistence/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.semantics/${project.version}</bundle>
<feature dependency="true">openhab.tp-asm</feature>
<feature dependency="true">openhab.tp-jackson</feature>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.thing/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.transform/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.audio/${project.version}</bundle>
Expand Down

0 comments on commit 8bc1921

Please sign in to comment.