mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-10 13:21:53 +01:00
Improve YAML model repository (#4024)
Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
parent
d994c136db
commit
fe4cbe546e
@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
}
|
||||
}
|
@ -12,28 +12,54 @@
|
||||
*/
|
||||
package org.openhab.core.model.yaml;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.openhab.core.model.yaml.internal.YamlModelRepositoryImpl;
|
||||
|
||||
/**
|
||||
* The {@link YamlElement} interface offers an identifier and a check validity method
|
||||
* to any element defined in a YAML configuration file.
|
||||
* The {@link YamlElement} interface must be implemented by any classes that need to be handled by the
|
||||
* {@link YamlModelRepositoryImpl}.
|
||||
* <p />
|
||||
* Implementations
|
||||
* <ul>
|
||||
* <li>MUST have a default constructor to allow deserialization with Jackson</li>
|
||||
* <li>MUST provide {@code equals(Object other)} and {@code hashcode()} methods</li>
|
||||
* <li>MUST be annotated with {@link YamlElementName} containing the element name</li>
|
||||
* <li>SHOULD implement a proper {@code toString()} method</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Jan N. Klug - Refactoring and improvements to JavaDoc
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface YamlElement {
|
||||
|
||||
/**
|
||||
* Get the identifier of the YAML element
|
||||
* Get the identifier of this element.
|
||||
* <p />
|
||||
* <p />
|
||||
* Identifiers
|
||||
* <ul>
|
||||
* <li>MUST be non-null</li>
|
||||
* <li>MUST be unique within a model</li>
|
||||
* <li>SHOULD be unique across all models</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the identifier as a string
|
||||
*/
|
||||
@NonNull
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Check that the YAML element is valid
|
||||
* Check if this element is valid and should be included in the model.
|
||||
* <p />
|
||||
* <p />
|
||||
* Implementations
|
||||
* <ul>
|
||||
* <li>MUST check that at least {link #getId()} returns a non-null value</li>
|
||||
* <li>SHOULD log the reason of failed checks at WARN level</li>
|
||||
* <li>MAY perform additional checks</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return true if all the checks are OK
|
||||
* @return {@code true} if all the checks are completed successfully
|
||||
*/
|
||||
boolean isValid();
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* The {@link YamlElementName} is a required annotation for the inheritors of {@link YamlElement}. It specifies the root
|
||||
* element name in a YAML model that is described by the respective class. Code review MUST ensure that element names
|
||||
* are unique.
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface YamlElementName {
|
||||
String value();
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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();
|
||||
}
|
@ -19,6 +19,9 @@ 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.
|
||||
* <p />
|
||||
* Implementors are notified whenever a YAML model changed that contains elements of the given type. They MUST declare
|
||||
* at least {@link YamlModelListener} as service to automatically register them with the repository.
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
*/
|
||||
@ -26,20 +29,23 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
public interface YamlModelListener<T extends YamlElement> {
|
||||
|
||||
/**
|
||||
* Method called by the model repository when elements from a model are added.
|
||||
* Method called by the model repository when elements from a model are added. Only added elements are contained in
|
||||
* the collection. In case the listener is added after a model was read, this method is also called with all
|
||||
* elements from that model.
|
||||
*
|
||||
* @param modelName the name of the model
|
||||
* @param elements the collection of added elements
|
||||
*/
|
||||
void addedModel(String modelName, Collection<? extends YamlElement> elements);
|
||||
void addedModel(String modelName, Collection<T> elements);
|
||||
|
||||
/**
|
||||
* Method called by the model repository when elements from a model are updated.
|
||||
* Method called by the model repository when elements from a model are updated. Only changed elements are contained
|
||||
* in the collection.
|
||||
*
|
||||
* @param modelName the name of the model
|
||||
* @param elements the collection of updated elements
|
||||
*/
|
||||
void updatedModel(String modelName, Collection<? extends YamlElement> elements);
|
||||
void updatedModel(String modelName, Collection<T> elements);
|
||||
|
||||
/**
|
||||
* Method called by the model repository when elements from a model are removed.
|
||||
@ -47,27 +53,11 @@ public interface YamlModelListener<T extends YamlElement> {
|
||||
* @param modelName the name of the model
|
||||
* @param elements the collection of removed elements
|
||||
*/
|
||||
void removedModel(String modelName, Collection<? extends YamlElement> elements);
|
||||
void removedModel(String modelName, Collection<T> 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.
|
||||
* Get the DTO class to be used for each object of this model type. The DTO class MUST implement {@link YamlElement}
|
||||
* and fulfill all requirements defined for the interface.
|
||||
*
|
||||
* @return the DTO element class
|
||||
*/
|
||||
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 YamlModelRepository} defines methods to update elements in a YAML model.
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface YamlModelRepository {
|
||||
void addElementToModel(String modelName, YamlElement element);
|
||||
|
||||
void removeElementFromModel(String modelName, YamlElement element);
|
||||
|
||||
void updateElementInModel(String modelName, YamlElement element);
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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);
|
||||
}
|
||||
}
|
@ -1,199 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.info("Loading model '{}'", path.toFile().getName());
|
||||
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<>()));
|
||||
}
|
||||
}
|
@ -0,0 +1,428 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.model.yaml.YamlElement;
|
||||
import org.openhab.core.model.yaml.YamlElementName;
|
||||
import org.openhab.core.model.yaml.YamlModelListener;
|
||||
import org.openhab.core.model.yaml.YamlModelRepository;
|
||||
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.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
|
||||
|
||||
/**
|
||||
* The {@link YamlModelRepositoryImpl} 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
|
||||
* @author Jan N. Klug - Refactored for multiple types per file and add modifying possibility
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true)
|
||||
public class YamlModelRepositoryImpl implements WatchService.WatchEventListener, YamlModelRepository {
|
||||
private static final int DEFAULT_MODEL_VERSION = 1;
|
||||
private final Logger logger = LoggerFactory.getLogger(YamlModelRepositoryImpl.class);
|
||||
|
||||
private final WatchService watchService;
|
||||
private final Path watchPath;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final Map<String, List<YamlModelListener<?>>> elementListeners = new ConcurrentHashMap<>();
|
||||
// all model nodes, ordered by model name (full path as string) and type
|
||||
private final Map<String, YamlModelWrapper> modelCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Activate
|
||||
public YamlModelRepositoryImpl(@Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) {
|
||||
YAMLFactory yamlFactory = YAMLFactory.builder() //
|
||||
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) // omit "---" at file start
|
||||
.disable(YAMLGenerator.Feature.SPLIT_LINES) // do not split long lines
|
||||
.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR) // indent arrays
|
||||
.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) // use quotes only where necessary
|
||||
.build();
|
||||
this.objectMapper = new ObjectMapper(yamlFactory);
|
||||
objectMapper.findAndRegisterModules();
|
||||
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
|
||||
this.watchService = watchService;
|
||||
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
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public synchronized void processWatchEvent(Kind kind, Path path) {
|
||||
Path fullPath = watchPath.resolve(path);
|
||||
String pathString = path.toString();
|
||||
if (Files.isDirectory(fullPath) || fullPath.toFile().isHidden() || !pathString.endsWith(".yaml")) {
|
||||
logger.trace("Ignored {}", fullPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// strip extension for model name
|
||||
String modelName = pathString.substring(0, pathString.lastIndexOf("."));
|
||||
|
||||
if (kind == WatchService.Kind.DELETE) {
|
||||
logger.info("Removing YAML model {}", modelName);
|
||||
YamlModelWrapper removedModel = modelCache.remove(modelName);
|
||||
if (removedModel == null) {
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<String, List<JsonNode>> modelEntry : removedModel.getNodes().entrySet()) {
|
||||
String elementName = modelEntry.getKey();
|
||||
List<JsonNode> removedNodes = modelEntry.getValue();
|
||||
if (!removedNodes.isEmpty()) {
|
||||
getElementListeners(elementName).forEach(listener -> {
|
||||
List removedElements = parseJsonNodes(removedNodes, listener.getElementClass());
|
||||
listener.removedModel(modelName, removedElements);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (kind == Kind.CREATE) {
|
||||
logger.info("Adding YAML model {}", modelName);
|
||||
} else {
|
||||
logger.info("Updating YAML model {}", modelName);
|
||||
}
|
||||
try {
|
||||
JsonNode fileContent = objectMapper.readTree(fullPath.toFile());
|
||||
|
||||
// check version
|
||||
JsonNode versionNode = fileContent.get("version");
|
||||
if (versionNode == null || !versionNode.canConvertToInt()) {
|
||||
logger.warn("Version is missing or not a number in model {}. Ignoring it.", modelName);
|
||||
return;
|
||||
}
|
||||
int modelVersion = versionNode.asInt();
|
||||
if (modelVersion != DEFAULT_MODEL_VERSION) {
|
||||
logger.warn("Model {} has version {}, but only version 1 is supported. Ignoring it.", modelName,
|
||||
modelVersion);
|
||||
return;
|
||||
}
|
||||
JsonNode readOnlyNode = fileContent.get("readOnly");
|
||||
boolean readOnly = readOnlyNode == null || readOnlyNode.asBoolean(false);
|
||||
|
||||
YamlModelWrapper model = Objects.requireNonNull(
|
||||
modelCache.computeIfAbsent(modelName, k -> new YamlModelWrapper(modelVersion, readOnly)));
|
||||
|
||||
// get sub-elements
|
||||
Iterator<Map.Entry<String, JsonNode>> it = fileContent.fields();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<String, JsonNode> element = it.next();
|
||||
String elementName = element.getKey();
|
||||
JsonNode node = element.getValue();
|
||||
if (!node.isArray()) {
|
||||
// all processable sub-elements are arrays
|
||||
logger.trace("Element {} in model {} is not an array, ignoring it", elementName, modelName);
|
||||
continue;
|
||||
}
|
||||
|
||||
List<JsonNode> oldNodeElements = model.getNodes().getOrDefault(elementName, List.of());
|
||||
List<JsonNode> newNodeElements = new ArrayList<>();
|
||||
node.elements().forEachRemaining(newNodeElements::add);
|
||||
|
||||
for (YamlModelListener<?> elementListener : getElementListeners(elementName)) {
|
||||
Class<? extends YamlElement> elementClass = elementListener.getElementClass();
|
||||
|
||||
Map<String, ? extends YamlElement> oldElements = listToMap(
|
||||
parseJsonNodes(oldNodeElements, elementClass));
|
||||
Map<String, ? extends YamlElement> newElements = listToMap(
|
||||
parseJsonNodes(newNodeElements, elementClass));
|
||||
|
||||
List addedElements = newElements.values().stream()
|
||||
.filter(e -> !oldElements.containsKey(e.getId())).toList();
|
||||
List removedElements = oldElements.values().stream()
|
||||
.filter(e -> !newElements.containsKey(e.getId())).toList();
|
||||
List updatedElements = newElements.values().stream().filter(
|
||||
e -> oldElements.containsKey(e.getId()) && !e.equals(oldElements.get(e.getId())))
|
||||
.toList();
|
||||
|
||||
if (!addedElements.isEmpty()) {
|
||||
elementListener.addedModel(modelName, addedElements);
|
||||
}
|
||||
if (!removedElements.isEmpty()) {
|
||||
elementListener.removedModel(modelName, removedElements);
|
||||
}
|
||||
if (!updatedElements.isEmpty()) {
|
||||
elementListener.updatedModel(modelName, updatedElements);
|
||||
}
|
||||
}
|
||||
|
||||
// replace cache
|
||||
model.getNodes().put(elementName, newNodeElements);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to read {}: {}", modelName, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void addYamlModelListener(YamlModelListener<? extends YamlElement> listener) {
|
||||
Class<? extends YamlElement> elementClass = listener.getElementClass();
|
||||
YamlElementName annotation = elementClass.getAnnotation(YamlElementName.class);
|
||||
if (annotation == null) {
|
||||
logger.warn("Class {} is missing the mandatory YamlElementName annotation. This is a bug.", elementClass);
|
||||
return;
|
||||
}
|
||||
String elementName = annotation.value();
|
||||
getElementListeners(elementName).add(listener);
|
||||
|
||||
// iterate over all models and notify he new listener of already existing models with this type
|
||||
for (Map.Entry<String, YamlModelWrapper> model : modelCache.entrySet()) {
|
||||
String modelName = model.getKey();
|
||||
List<JsonNode> modelNodes = model.getValue().getNodes().get(elementName);
|
||||
if (modelNodes == null || modelNodes.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List modelElements = parseJsonNodes(modelNodes, elementClass);
|
||||
listener.addedModel(modelName, modelElements);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeYamlModelListener(YamlModelListener<? extends YamlElement> listener) {
|
||||
elementListeners.values().forEach(list -> list.remove(listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void addElementToModel(String modelName, YamlElement element) {
|
||||
YamlElementName annotation = element.getClass().getAnnotation(YamlElementName.class);
|
||||
if (annotation == null) {
|
||||
logger.warn(
|
||||
"Failed to add element {}. Class {}) is missing the mandatory YamlElementName annotation. This is a bug.",
|
||||
element.getId(), element.getClass());
|
||||
return;
|
||||
}
|
||||
String elementName = annotation.value();
|
||||
YamlModelWrapper model = Objects.requireNonNull(
|
||||
modelCache.computeIfAbsent(modelName, k -> new YamlModelWrapper(DEFAULT_MODEL_VERSION, false)));
|
||||
if (model.isReadOnly()) {
|
||||
logger.warn("Modifying {} is not allowed, model is marked read-only", modelName);
|
||||
return;
|
||||
}
|
||||
|
||||
List<JsonNode> modelNodes = model.getNodes().computeIfAbsent(elementName, k -> new CopyOnWriteArrayList<>());
|
||||
JsonNode newNode = objectMapper.convertValue(element, JsonNode.class);
|
||||
modelNodes.add(newNode);
|
||||
// notify listeners
|
||||
getElementListeners(elementName).forEach(l -> {
|
||||
List newElements = parseJsonNodes(List.of(newNode), l.getElementClass());
|
||||
if (!newElements.isEmpty()) {
|
||||
l.addedModel(modelName, newElements);
|
||||
}
|
||||
});
|
||||
|
||||
writeModel(modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void removeElementFromModel(String modelName, YamlElement element) {
|
||||
YamlElementName annotation = element.getClass().getAnnotation(YamlElementName.class);
|
||||
if (annotation == null) {
|
||||
logger.warn(
|
||||
"Failed to remove element {}. Class {}) is missing the mandatory YamlElementName annotation. This is a bug.",
|
||||
element.getId(), element.getClass());
|
||||
return;
|
||||
}
|
||||
String elementName = annotation.value();
|
||||
YamlModelWrapper model = modelCache.get(modelName);
|
||||
if (model == null) {
|
||||
logger.warn("Failed to remove {} from model {} because the model is not known.", element, modelName);
|
||||
return;
|
||||
}
|
||||
if (model.isReadOnly()) {
|
||||
logger.warn("Modifying {} is not allowed, model is marked read-only", modelName);
|
||||
return;
|
||||
}
|
||||
|
||||
List<JsonNode> modelNodes = model.getNodes().get(elementName);
|
||||
if (modelNodes == null) {
|
||||
logger.warn("Failed to remove {} from model {} because type {} is not known in the model.", element,
|
||||
modelName, elementName);
|
||||
return;
|
||||
}
|
||||
JsonNode toRemove = findNodeById(modelNodes, element.getClass(), element.getId());
|
||||
if (toRemove == null) {
|
||||
logger.warn("Failed to remove {} from model {} because element is not in model.", element, modelName);
|
||||
return;
|
||||
}
|
||||
modelNodes.remove(toRemove);
|
||||
// notify listeners
|
||||
getElementListeners(elementName).forEach(l -> {
|
||||
List newElements = parseJsonNodes(List.of(toRemove), l.getElementClass());
|
||||
if (!newElements.isEmpty()) {
|
||||
l.addedModel(modelName, newElements);
|
||||
}
|
||||
});
|
||||
|
||||
writeModel(modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void updateElementInModel(String modelName, YamlElement element) {
|
||||
YamlElementName annotation = element.getClass().getAnnotation(YamlElementName.class);
|
||||
if (annotation == null) {
|
||||
logger.warn(
|
||||
"Failed to update element {}. Class {}) is missing the mandatory YamlElementName annotation. This is a bug.",
|
||||
element.getId(), element.getClass());
|
||||
return;
|
||||
}
|
||||
String elementName = annotation.value();
|
||||
YamlModelWrapper model = modelCache.get(modelName);
|
||||
if (model == null) {
|
||||
logger.warn("Failed to update {} in model {} because the model is not known.", element, modelName);
|
||||
return;
|
||||
}
|
||||
if (model.isReadOnly()) {
|
||||
logger.warn("Modifying {} is not allowed, model is marked read-only", modelName);
|
||||
return;
|
||||
}
|
||||
|
||||
List<JsonNode> modelNodes = model.getNodes().get(elementName);
|
||||
if (modelNodes == null) {
|
||||
logger.warn("Failed to update {} in model {} because type {} is not known in the model.", element,
|
||||
modelName, elementName);
|
||||
return;
|
||||
}
|
||||
JsonNode oldElement = findNodeById(modelNodes, element.getClass(), element.getId());
|
||||
if (oldElement == null) {
|
||||
logger.warn("Failed to update {} in model {} because element is not in model.", element, modelName);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonNode newNode = objectMapper.convertValue(element, JsonNode.class);
|
||||
modelNodes.set(modelNodes.indexOf(oldElement), newNode);
|
||||
// notify listeners
|
||||
getElementListeners(elementName).forEach(l -> {
|
||||
List newElements = parseJsonNodes(List.of(newNode), l.getElementClass());
|
||||
if (!newElements.isEmpty()) {
|
||||
l.updatedModel(modelName, newElements);
|
||||
}
|
||||
});
|
||||
|
||||
writeModel(modelName);
|
||||
}
|
||||
|
||||
private void writeModel(String modelName) {
|
||||
YamlModelWrapper model = modelCache.get(modelName);
|
||||
if (model == null) {
|
||||
logger.warn("Failed to write model {} to disk because it is not known.", modelName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.isReadOnly()) {
|
||||
logger.warn("Failed to write model {} to disk because it is marked as read-only.", modelName);
|
||||
return;
|
||||
}
|
||||
|
||||
// create the model
|
||||
JsonNodeFactory nodeFactory = objectMapper.getNodeFactory();
|
||||
ObjectNode rootNode = nodeFactory.objectNode();
|
||||
|
||||
rootNode.put("version", model.getVersion());
|
||||
rootNode.put("readOnly", model.isReadOnly());
|
||||
for (Map.Entry<String, List<JsonNode>> elementNodes : model.getNodes().entrySet()) {
|
||||
ArrayNode arrayNode = nodeFactory.arrayNode();
|
||||
elementNodes.getValue().forEach(arrayNode::add);
|
||||
rootNode.set(elementNodes.getKey(), arrayNode);
|
||||
}
|
||||
|
||||
try {
|
||||
Path outFile = watchPath.resolve(modelName + ".yaml");
|
||||
String fileContent = objectMapper.writeValueAsString(rootNode);
|
||||
if (Files.exists(outFile) && !Files.isWritable(outFile)) {
|
||||
logger.warn("Failed writing model {}: model exists but is read-only.", modelName);
|
||||
return;
|
||||
}
|
||||
Files.writeString(outFile, fileContent);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("Failed to serialize model {}: {}", modelName, e.getMessage());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed writing model {}: {}", modelName, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private List<YamlModelListener<?>> getElementListeners(String elementName) {
|
||||
return Objects.requireNonNull(elementListeners.computeIfAbsent(elementName, k -> new CopyOnWriteArrayList<>()));
|
||||
}
|
||||
|
||||
private <T extends YamlElement> @Nullable JsonNode findNodeById(List<JsonNode> nodes, Class<T> elementClass,
|
||||
String id) {
|
||||
return nodes.stream().filter(node -> {
|
||||
Optional<T> parsedNode = parseJsonNode(node, elementClass);
|
||||
return parsedNode.filter(yamlDTO -> id.equals(yamlDTO.getId())).isPresent();
|
||||
}).findAny().orElse(null);
|
||||
}
|
||||
|
||||
private Map<String, ? extends YamlElement> listToMap(List<? extends YamlElement> elements) {
|
||||
return elements.stream().collect(Collectors.toMap(YamlElement::getId, e -> e));
|
||||
}
|
||||
|
||||
private <T extends YamlElement> List<T> parseJsonNodes(List<JsonNode> nodes, Class<T> elementClass) {
|
||||
return nodes.stream().map(nE -> parseJsonNode(nE, elementClass)).filter(Optional::isPresent).map(Optional::get)
|
||||
.filter(YamlElement::isValid).toList();
|
||||
}
|
||||
|
||||
private <T extends YamlElement> Optional<T> parseJsonNode(JsonNode node, Class<T> elementClass) {
|
||||
try {
|
||||
return Optional.of(objectMapper.treeToValue(node, elementClass));
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("Could not parse element {} to {}: {}", node, elementClass, e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* The {@link YamlModelWrapper} is used to store the information read from a model in the model cache.
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class YamlModelWrapper {
|
||||
private final int version;
|
||||
private final boolean readOnly;
|
||||
private final Map<String, List<JsonNode>> nodes = new ConcurrentHashMap<>();
|
||||
|
||||
public YamlModelWrapper(int version, boolean readOnly) {
|
||||
this.version = version;
|
||||
this.readOnly = readOnly;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
public Map<String, List<JsonNode>> getNodes() {
|
||||
return nodes;
|
||||
}
|
||||
}
|
@ -15,31 +15,34 @@ package org.openhab.core.model.yaml.internal.semantics;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.model.yaml.YamlElement;
|
||||
import org.openhab.core.model.yaml.YamlElementName;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link YamlSemanticTag} is a data transfer object used to serialize a semantic tag
|
||||
* The {@link YamlSemanticTagDTO} 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 {
|
||||
@YamlElementName("tags")
|
||||
public class YamlSemanticTagDTO implements YamlElement {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(YamlSemanticTag.class);
|
||||
private final Logger logger = LoggerFactory.getLogger(YamlSemanticTagDTO.class);
|
||||
|
||||
public String uid;
|
||||
public String label;
|
||||
public String description;
|
||||
public List<String> synonyms;
|
||||
|
||||
public YamlSemanticTag() {
|
||||
public YamlSemanticTagDTO() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
public @NonNull String getId() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
@ -59,7 +62,7 @@ public class YamlSemanticTag implements YamlElement {
|
||||
} else if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
YamlSemanticTag that = (YamlSemanticTag) obj;
|
||||
YamlSemanticTagDTO that = (YamlSemanticTagDTO) obj;
|
||||
return Objects.equals(uid, that.uid) && Objects.equals(label, that.label)
|
||||
&& Objects.equals(description, that.description) && Objects.equals(synonyms, that.synonyms);
|
||||
}
|
@ -20,13 +20,10 @@ 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;
|
||||
@ -37,7 +34,7 @@ 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}.
|
||||
* These semantic tags are automatically exposed to the {@link org.openhab.core.semantics.SemanticTagRegistry}.
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
*/
|
||||
@ -45,7 +42,7 @@ import org.slf4j.LoggerFactory;
|
||||
@Component(immediate = true, service = { SemanticTagProvider.class, YamlSemanticTagProvider.class,
|
||||
YamlModelListener.class })
|
||||
public class YamlSemanticTagProvider extends AbstractProvider<SemanticTag>
|
||||
implements SemanticTagProvider, YamlModelListener<YamlSemanticTag> {
|
||||
implements SemanticTagProvider, YamlModelListener<YamlSemanticTagDTO> {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(YamlSemanticTagProvider.class);
|
||||
|
||||
@ -66,23 +63,13 @@ public class YamlSemanticTagProvider extends AbstractProvider<SemanticTag>
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRootName() {
|
||||
return "tags";
|
||||
public Class<YamlSemanticTagDTO> getElementClass() {
|
||||
return YamlSemanticTagDTO.class;
|
||||
}
|
||||
|
||||
@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))
|
||||
public void addedModel(String modelName, Collection<YamlSemanticTagDTO> elements) {
|
||||
List<SemanticTag> added = elements.stream().map(this::mapSemanticTag)
|
||||
.sorted(Comparator.comparing(SemanticTag::getUID)).toList();
|
||||
tags.addAll(added);
|
||||
added.forEach(t -> {
|
||||
@ -92,8 +79,8 @@ public class YamlSemanticTagProvider extends AbstractProvider<SemanticTag>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatedModel(String modelName, Collection<? extends YamlElement> elements) {
|
||||
List<SemanticTag> updated = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e)).toList();
|
||||
public void updatedModel(String modelName, Collection<YamlSemanticTagDTO> elements) {
|
||||
List<SemanticTag> updated = elements.stream().map(this::mapSemanticTag).toList();
|
||||
updated.forEach(t -> {
|
||||
tags.stream().filter(tag -> tag.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldTag -> {
|
||||
tags.remove(oldTag);
|
||||
@ -105,8 +92,8 @@ public class YamlSemanticTagProvider extends AbstractProvider<SemanticTag>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removedModel(String modelName, Collection<? extends YamlElement> elements) {
|
||||
List<SemanticTag> removed = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e))
|
||||
public void removedModel(String modelName, Collection<YamlSemanticTagDTO> elements) {
|
||||
List<SemanticTag> removed = elements.stream().map(this::mapSemanticTag)
|
||||
.sorted(Comparator.comparing(SemanticTag::getUID).reversed()).toList();
|
||||
removed.forEach(t -> {
|
||||
tags.stream().filter(tag -> tag.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldTag -> {
|
||||
@ -117,10 +104,7 @@ public class YamlSemanticTagProvider extends AbstractProvider<SemanticTag>
|
||||
});
|
||||
}
|
||||
|
||||
private SemanticTag mapSemanticTag(YamlSemanticTag tagDTO) {
|
||||
if (tagDTO.uid == null) {
|
||||
throw new IllegalArgumentException("The argument 'tagDTO.uid' must not be null.");
|
||||
}
|
||||
private SemanticTag mapSemanticTag(YamlSemanticTagDTO tagDTO) {
|
||||
return new SemanticTagImpl(tagDTO.uid, tagDTO.label, tagDTO.description, tagDTO.synonyms);
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.openhab.core.model.yaml.YamlElement;
|
||||
import org.openhab.core.model.yaml.YamlModelListener;
|
||||
import org.openhab.core.model.yaml.test.FirstTypeDTO;
|
||||
import org.openhab.core.model.yaml.test.SecondTypeDTO;
|
||||
import org.openhab.core.service.WatchService;
|
||||
|
||||
/**
|
||||
* The {@link YamlModelRepositoryImplTest} contains tests for the {@link YamlModelRepositoryImpl} class.
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@NonNullByDefault
|
||||
public class YamlModelRepositoryImplTest {
|
||||
private static final Path SOURCE_PATH = Path.of("src/test/resources");
|
||||
private static final String MODEL_NAME = "model";
|
||||
private static final Path MODEL_PATH = Path.of(MODEL_NAME + ".yaml");
|
||||
|
||||
private @Mock @NonNullByDefault({}) WatchService watchServiceMock;
|
||||
private @TempDir @NonNullByDefault({}) Path watchPath;
|
||||
private @NonNullByDefault({}) Path fullModelPath;
|
||||
|
||||
private @Mock @NonNullByDefault({}) YamlModelListener<@NonNull FirstTypeDTO> firstTypeListener;
|
||||
private @Mock @NonNullByDefault({}) YamlModelListener<@NonNull SecondTypeDTO> secondTypeListener1;
|
||||
private @Mock @NonNullByDefault({}) YamlModelListener<@NonNull SecondTypeDTO> secondTypeListener2;
|
||||
|
||||
private @Captor @NonNullByDefault({}) ArgumentCaptor<Collection<FirstTypeDTO>> firstTypeCaptor;
|
||||
private @Captor @NonNullByDefault({}) ArgumentCaptor<Collection<SecondTypeDTO>> secondTypeCaptor1;
|
||||
private @Captor @NonNullByDefault({}) ArgumentCaptor<Collection<SecondTypeDTO>> secondTypeCaptor2;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
fullModelPath = watchPath.resolve(MODEL_PATH);
|
||||
when(watchServiceMock.getWatchPath()).thenReturn(watchPath);
|
||||
|
||||
when(firstTypeListener.getElementClass()).thenReturn(FirstTypeDTO.class);
|
||||
when(secondTypeListener1.getElementClass()).thenReturn(SecondTypeDTO.class);
|
||||
when(secondTypeListener2.getElementClass()).thenReturn(SecondTypeDTO.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileAddedAfterListeners() throws IOException {
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
modelRepository.addYamlModelListener(secondTypeListener1);
|
||||
modelRepository.addYamlModelListener(secondTypeListener2);
|
||||
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
verify(firstTypeListener).addedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener, never()).updatedModel(any(), any());
|
||||
verify(firstTypeListener, never()).removedModel(any(), any());
|
||||
verify(secondTypeListener1).addedModel(eq(MODEL_NAME), secondTypeCaptor1.capture());
|
||||
verify(secondTypeListener1, never()).updatedModel(any(), any());
|
||||
verify(secondTypeListener1, never()).removedModel(any(), any());
|
||||
verify(secondTypeListener2).addedModel(eq(MODEL_NAME), secondTypeCaptor2.capture());
|
||||
verify(secondTypeListener2, never()).updatedModel(any(), any());
|
||||
verify(secondTypeListener2, never()).removedModel(any(), any());
|
||||
|
||||
Collection<? extends YamlElement> firstTypeElements = firstTypeCaptor.getValue();
|
||||
Collection<? extends YamlElement> secondTypeElements1 = secondTypeCaptor1.getValue();
|
||||
Collection<? extends YamlElement> secondTypeElements2 = secondTypeCaptor2.getValue();
|
||||
|
||||
assertThat(firstTypeElements, hasSize(2));
|
||||
assertThat(secondTypeElements1, hasSize(1));
|
||||
assertThat(secondTypeElements2, hasSize(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileAddedBeforeListeners() throws IOException {
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"), fullModelPath);
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
modelRepository.addYamlModelListener(secondTypeListener1);
|
||||
modelRepository.addYamlModelListener(secondTypeListener2);
|
||||
|
||||
verify(firstTypeListener).addedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener, never()).updatedModel(any(), any());
|
||||
verify(firstTypeListener, never()).removedModel(any(), any());
|
||||
verify(secondTypeListener1).addedModel(eq(MODEL_NAME), secondTypeCaptor1.capture());
|
||||
verify(secondTypeListener1, never()).updatedModel(any(), any());
|
||||
verify(secondTypeListener1, never()).removedModel(any(), any());
|
||||
verify(secondTypeListener2).addedModel(eq(MODEL_NAME), secondTypeCaptor2.capture());
|
||||
verify(secondTypeListener2, never()).updatedModel(any(), any());
|
||||
verify(secondTypeListener2, never()).removedModel(any(), any());
|
||||
|
||||
Collection<FirstTypeDTO> firstTypeElements = firstTypeCaptor.getValue();
|
||||
Collection<SecondTypeDTO> secondTypeElements1 = secondTypeCaptor1.getValue();
|
||||
Collection<SecondTypeDTO> secondTypeElements2 = secondTypeCaptor2.getValue();
|
||||
|
||||
assertThat(firstTypeElements,
|
||||
containsInAnyOrder(new FirstTypeDTO("First1", "Description1"), new FirstTypeDTO("First2", null)));
|
||||
assertThat(secondTypeElements1, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
assertThat(secondTypeElements1, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileUpdated() throws IOException {
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileUpdatePost.yaml"), fullModelPath);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
verify(firstTypeListener).addedModel(eq(MODEL_NAME), any());
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileUpdatePre.yaml"), fullModelPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.MODIFY, MODEL_PATH);
|
||||
verify(firstTypeListener, times(2)).addedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener).updatedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener).removedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
|
||||
List<Collection<FirstTypeDTO>> arguments = firstTypeCaptor.getAllValues();
|
||||
assertThat(arguments, hasSize(4));
|
||||
|
||||
// added originally
|
||||
assertThat(arguments.get(0), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
new FirstTypeDTO("Second", "Second original"), new FirstTypeDTO("Third", "Third original")));
|
||||
// added by update
|
||||
assertThat(arguments.get(1), contains(new FirstTypeDTO("Fourth", "Fourth original")));
|
||||
// updated by update
|
||||
assertThat(arguments.get(2), contains(new FirstTypeDTO("Second", "Second modified")));
|
||||
// removed by update
|
||||
assertThat(arguments.get(3), contains(new FirstTypeDTO("Third", "Third original")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileRemoved() throws IOException {
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileUpdatePost.yaml"), fullModelPath);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.DELETE, MODEL_PATH);
|
||||
|
||||
verify(firstTypeListener).addedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener, never()).updatedModel(any(), any());
|
||||
verify(firstTypeListener).removedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
|
||||
List<Collection<FirstTypeDTO>> arguments = firstTypeCaptor.getAllValues();
|
||||
assertThat(arguments, hasSize(2));
|
||||
|
||||
// all are added
|
||||
assertThat(arguments.get(0), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
new FirstTypeDTO("Second", "Second original"), new FirstTypeDTO("Third", "Third original")));
|
||||
// all are removed
|
||||
assertThat(arguments.get(0), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
new FirstTypeDTO("Second", "Second original"), new FirstTypeDTO("Third", "Third original")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddElementToModel() throws IOException {
|
||||
Files.copy(SOURCE_PATH.resolve("modifyModelInitialContent.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
FirstTypeDTO added = new FirstTypeDTO("element3", "description3");
|
||||
modelRepository.addElementToModel(MODEL_NAME, added);
|
||||
|
||||
String actualFileContent = Files.readString(fullModelPath);
|
||||
String expectedFileContent = Files.readString(SOURCE_PATH.resolve("addToModelExpectedContent.yaml"));
|
||||
|
||||
assertThat(actualFileContent, is(expectedFileContent));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateElementInModel() throws IOException {
|
||||
Files.copy(SOURCE_PATH.resolve("modifyModelInitialContent.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
FirstTypeDTO updated = new FirstTypeDTO("element1", "newDescription1");
|
||||
modelRepository.updateElementInModel(MODEL_NAME, updated);
|
||||
|
||||
String actualFileContent = Files.readString(fullModelPath);
|
||||
String expectedFileContent = Files.readString(SOURCE_PATH.resolve("updateInModelExpectedContent.yaml"));
|
||||
|
||||
assertThat(actualFileContent, is(expectedFileContent));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveElementFromModel() throws IOException {
|
||||
Files.copy(SOURCE_PATH.resolve("modifyModelInitialContent.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
FirstTypeDTO removed = new FirstTypeDTO("element1", "description1");
|
||||
modelRepository.removeElementFromModel(MODEL_NAME, removed);
|
||||
|
||||
String actualFileContent = Files.readString(fullModelPath);
|
||||
String expectedFileContent = Files.readString(SOURCE_PATH.resolve("removeFromModelExpectedContent.yaml"));
|
||||
|
||||
assertThat(actualFileContent, is(expectedFileContent));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadOnlyModelNotUpdated() throws IOException {
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
FirstTypeDTO added = new FirstTypeDTO("element3", "description3");
|
||||
modelRepository.addElementToModel(MODEL_NAME, added);
|
||||
|
||||
FirstTypeDTO removed = new FirstTypeDTO("element1", "description1");
|
||||
modelRepository.removeElementFromModel(MODEL_NAME, removed);
|
||||
|
||||
FirstTypeDTO updated = new FirstTypeDTO("element2", "newDescription2");
|
||||
modelRepository.updateElementInModel(MODEL_NAME, updated);
|
||||
|
||||
String actualFileContent = Files.readString(fullModelPath);
|
||||
String expectedFileContent = Files.readString(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"));
|
||||
|
||||
assertThat(actualFileContent, is(expectedFileContent));
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.test;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.openhab.core.model.yaml.YamlElement;
|
||||
import org.openhab.core.model.yaml.YamlElementName;
|
||||
|
||||
/**
|
||||
* The {@link FirstTypeDTO} is a test type implementing {@link YamlElement}
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@YamlElementName("firstType")
|
||||
public class FirstTypeDTO implements YamlElement {
|
||||
public String uid;
|
||||
public String description;
|
||||
|
||||
public FirstTypeDTO() {
|
||||
}
|
||||
|
||||
public FirstTypeDTO(String uid, String description) {
|
||||
this.uid = uid;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getId() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return uid != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
FirstTypeDTO that = (FirstTypeDTO) o;
|
||||
return Objects.equals(uid, that.uid) && Objects.equals(description, that.description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(uid, description);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.test;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.openhab.core.model.yaml.YamlElement;
|
||||
import org.openhab.core.model.yaml.YamlElementName;
|
||||
|
||||
/**
|
||||
* The {@link SecondTypeDTO} is a test type implementing {@link YamlElement}
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@YamlElementName("secondType")
|
||||
public class SecondTypeDTO implements YamlElement {
|
||||
public String id;
|
||||
public String label;
|
||||
|
||||
public SecondTypeDTO() {
|
||||
}
|
||||
|
||||
public SecondTypeDTO(String id, String label) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return id != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
SecondTypeDTO that = (SecondTypeDTO) o;
|
||||
return Objects.equals(id, that.id) && Objects.equals(label, that.label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, label);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
version: 1
|
||||
readOnly: false
|
||||
firstType:
|
||||
- uid: element1
|
||||
description: description1
|
||||
- uid: element2
|
||||
description: description2
|
||||
- uid: element3
|
||||
description: description3
|
@ -0,0 +1,28 @@
|
||||
# A YAML test file with different types
|
||||
# The structure is valid but also contains invalid elements
|
||||
|
||||
version: 1
|
||||
|
||||
# known first type
|
||||
firstType:
|
||||
# a valid element with uid and description
|
||||
- uid: First1
|
||||
description: Description1
|
||||
|
||||
# a valid element with uid only
|
||||
- uid: First2
|
||||
|
||||
# an invalid element (missing uid)
|
||||
- description: Description3
|
||||
|
||||
# known second type
|
||||
secondType:
|
||||
# a valid element with id and label
|
||||
- id: Second1
|
||||
label: Label1
|
||||
|
||||
# unknown third type
|
||||
thirdType:
|
||||
- foo: Bar
|
||||
|
||||
nonArrayElement: Test
|
@ -0,0 +1,11 @@
|
||||
# A YAML test file for updating the model
|
||||
|
||||
version: 1
|
||||
|
||||
firstType:
|
||||
- uid: First
|
||||
description: First original
|
||||
- uid: Second
|
||||
description: Second original
|
||||
- uid: Third
|
||||
description: Third original
|
@ -0,0 +1,11 @@
|
||||
# A YAML test file for updating the model
|
||||
|
||||
version: 1
|
||||
|
||||
firstType:
|
||||
- uid: First
|
||||
description: First original
|
||||
- uid: Second
|
||||
description: Second modified
|
||||
- uid: Fourth
|
||||
description: Fourth original
|
@ -0,0 +1,7 @@
|
||||
version: 1
|
||||
readOnly: false
|
||||
firstType:
|
||||
- uid: element1
|
||||
description: description1
|
||||
- uid: element2
|
||||
description: description2
|
@ -0,0 +1,5 @@
|
||||
version: 1
|
||||
readOnly: false
|
||||
firstType:
|
||||
- uid: element2
|
||||
description: description2
|
@ -0,0 +1,7 @@
|
||||
version: 1
|
||||
readOnly: false
|
||||
firstType:
|
||||
- uid: element1
|
||||
description: newDescription1
|
||||
- uid: element2
|
||||
description: description2
|
@ -5,13 +5,14 @@
|
||||
<!-- These suppressions define which files to be suppressed for which checks. -->
|
||||
<suppress files=".+[\\/]internal[\\/].+\.java" checks="JavadocType|JavadocVariable|JavadocMethod|MissingJavadocFilterCheck"/>
|
||||
<suppress files=".+DTO\.java" checks="JavadocType|JavadocVariable|JavadocMethod|MissingJavadocFilterCheck|NullAnnotationsCheck" />
|
||||
<suppress files=".+[\\/]YamlElement\.java" checks="NullAnnotationsCheck" />
|
||||
<suppress files=".+Impl\.java" checks="JavadocType|JavadocVariable|JavadocMethod|MissingJavadocFilterCheck"/>
|
||||
<suppress files=".+[\\/]pom\.xml" checks="OnlyTabIndentationCheck"/>
|
||||
<suppress files=".+[\\/]OH-INF[\\/].+\.xml" checks="OhInfXmlLabelCheck"/>
|
||||
|
||||
<!-- All generated files will skip the author tag check -->
|
||||
<suppress files=".+[\\/]gen[\\/].+\.java" checks="AuthorTagCheck"/>
|
||||
<!-- Some checks will be supressed for test bundles -->
|
||||
<!-- Some checks will be suppressed for test bundles -->
|
||||
<suppress files=".+.test[\\/].+" checks="RequireBundleCheck|OutsideOfLibExternalLibrariesCheck|ManifestExternalLibrariesCheck|BuildPropertiesExternalLibrariesCheck"/>
|
||||
|
||||
<!-- openHAB Core specific suppressions-->
|
||||
|
Loading…
Reference in New Issue
Block a user