mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-29 12:34:22 +02:00
Introduce YAML configuration version 2 (#4648)
* Introduce YAML configuration version 2 Version 2 uses map to store elements while version 1 uses table. Version 1 is still supported. Unit tests have been extended to cover version 1 and 2. Related to #3666 Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
+15
@@ -29,6 +29,7 @@ import org.openhab.core.model.yaml.internal.YamlModelRepositoryImpl;
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Jan N. Klug - Refactoring and improvements to JavaDoc
|
||||
* @author Laurent Garnier - Added methods setId and cloneWithoutId
|
||||
*/
|
||||
public interface YamlElement {
|
||||
|
||||
@@ -48,6 +49,20 @@ public interface YamlElement {
|
||||
@NonNull
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Set the identifier of this element.
|
||||
*
|
||||
* @param id the identifier as a string
|
||||
*/
|
||||
void setId(@NonNull String id);
|
||||
|
||||
/**
|
||||
* Clone this element putting the identifier to null.
|
||||
*
|
||||
* @return the cloned element
|
||||
*/
|
||||
YamlElement cloneWithoutId();
|
||||
|
||||
/**
|
||||
* Check if this element is valid and should be included in the model.
|
||||
* <p />
|
||||
|
||||
+183
-53
@@ -15,6 +15,7 @@ package org.openhab.core.model.yaml.internal;
|
||||
import static org.openhab.core.service.WatchService.Kind.CREATE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -48,7 +49,9 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -66,11 +69,13 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Jan N. Klug - Refactored for multiple types per file and add modifying possibility
|
||||
* @author Laurent Garnier - Introduce version 2 using map instead of table
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true)
|
||||
public class YamlModelRepositoryImpl implements WatchService.WatchEventListener, YamlModelRepository {
|
||||
private static final int DEFAULT_MODEL_VERSION = 1;
|
||||
private static final int DEFAULT_MODEL_VERSION = 2;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(YamlModelRepositoryImpl.class);
|
||||
|
||||
private final WatchService watchService;
|
||||
@@ -94,6 +99,8 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
objectMapper.findAndRegisterModules();
|
||||
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
objectMapper.setSerializationInclusion(Include.NON_NULL);
|
||||
objectMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
|
||||
|
||||
this.watchService = watchService;
|
||||
watchService.registerListener(this, Path.of(""));
|
||||
@@ -150,12 +157,22 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
if (removedModel == null) {
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<String, List<JsonNode>> modelEntry : removedModel.getNodes().entrySet()) {
|
||||
for (Map.Entry<String, List<JsonNode>> modelEntry : removedModel.getNodesV1().entrySet()) {
|
||||
String elementName = modelEntry.getKey();
|
||||
List<JsonNode> removedNodes = modelEntry.getValue();
|
||||
if (!removedNodes.isEmpty()) {
|
||||
getElementListeners(elementName).forEach(listener -> {
|
||||
List removedElements = parseJsonNodes(removedNodes, listener.getElementClass());
|
||||
List removedElements = parseJsonNodesV1(removedNodes, listener.getElementClass());
|
||||
listener.removedModel(modelName, removedElements);
|
||||
});
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, @Nullable JsonNode> modelEntry : removedModel.getNodes().entrySet()) {
|
||||
String elementName = modelEntry.getKey();
|
||||
JsonNode removedMapNode = modelEntry.getValue();
|
||||
if (removedMapNode != null) {
|
||||
getElementListeners(elementName).forEach(listener -> {
|
||||
List removedElements = parseJsonMapNode(removedMapNode, listener.getElementClass());
|
||||
listener.removedModel(modelName, removedElements);
|
||||
});
|
||||
}
|
||||
@@ -175,9 +192,10 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
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);
|
||||
if (modelVersion < 1 || modelVersion > DEFAULT_MODEL_VERSION) {
|
||||
logger.warn(
|
||||
"Model {} has version {}, but only versions between 1 and {} are supported. Ignoring it.",
|
||||
modelName, modelVersion, DEFAULT_MODEL_VERSION);
|
||||
return;
|
||||
}
|
||||
JsonNode readOnlyNode = fileContent.get("readOnly");
|
||||
@@ -192,23 +210,37 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
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> newNodeV1Elements = new ArrayList<>();
|
||||
JsonNode newNodeElements = null;
|
||||
|
||||
if (modelVersion == 1) {
|
||||
if (!node.isArray()) {
|
||||
// all processable sub-elements are arrays
|
||||
logger.trace("Element {} in model {} is not an array, ignoring it", elementName, modelName);
|
||||
continue;
|
||||
}
|
||||
node.elements().forEachRemaining(newNodeV1Elements::add);
|
||||
} else {
|
||||
if (!node.isContainerNode() || node.isArray()) {
|
||||
// all processable sub-elements are container nodes (not array)
|
||||
logger.trace("Element {} in model {} is not a container object, ignoring it", elementName,
|
||||
modelName);
|
||||
continue;
|
||||
}
|
||||
newNodeElements = node;
|
||||
}
|
||||
|
||||
List<JsonNode> oldNodeElements = model.getNodes().getOrDefault(elementName, List.of());
|
||||
List<JsonNode> newNodeElements = new ArrayList<>();
|
||||
node.elements().forEachRemaining(newNodeElements::add);
|
||||
List<JsonNode> oldNodeV1Elements = model.getNodesV1().getOrDefault(elementName, List.of());
|
||||
JsonNode oldNodeElements = model.getNodes().get(elementName);
|
||||
|
||||
for (YamlModelListener<?> elementListener : getElementListeners(elementName)) {
|
||||
Class<? extends YamlElement> elementClass = elementListener.getElementClass();
|
||||
|
||||
Map<String, ? extends YamlElement> oldElements = listToMap(
|
||||
parseJsonNodes(oldNodeElements, elementClass));
|
||||
parseJsonNodes(oldNodeV1Elements, oldNodeElements, elementClass));
|
||||
Map<String, ? extends YamlElement> newElements = listToMap(
|
||||
parseJsonNodes(newNodeElements, elementClass));
|
||||
parseJsonNodes(newNodeV1Elements, newNodeElements, elementClass));
|
||||
|
||||
List addedElements = newElements.values().stream()
|
||||
.filter(e -> !oldElements.containsKey(e.getId())).toList();
|
||||
@@ -230,7 +262,11 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
}
|
||||
|
||||
// replace cache
|
||||
model.getNodes().put(elementName, newNodeElements);
|
||||
if (modelVersion == 1) {
|
||||
model.getNodesV1().put(elementName, newNodeV1Elements);
|
||||
} else {
|
||||
model.getNodes().put(elementName, newNodeElements);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.trace("Ignored {}", fullPath);
|
||||
@@ -255,11 +291,12 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
// 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()) {
|
||||
List<JsonNode> modelNodes = model.getValue().getNodesV1().getOrDefault(elementName, List.of());
|
||||
JsonNode modelMapNode = model.getValue().getNodes().get(elementName);
|
||||
if (modelNodes.isEmpty() && modelMapNode == null) {
|
||||
continue;
|
||||
}
|
||||
List modelElements = parseJsonNodes(modelNodes, elementClass);
|
||||
List modelElements = parseJsonNodes(modelNodes, modelMapNode, elementClass);
|
||||
listener.addedModel(modelName, modelElements);
|
||||
}
|
||||
}
|
||||
@@ -271,11 +308,12 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
@Override
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void addElementToModel(String modelName, YamlElement element) {
|
||||
String id = element.getId();
|
||||
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());
|
||||
id, element.getClass());
|
||||
return;
|
||||
}
|
||||
String elementName = annotation.value();
|
||||
@@ -286,16 +324,31 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
return;
|
||||
}
|
||||
|
||||
List<JsonNode> modelNodes = model.getNodes().computeIfAbsent(elementName, k -> new CopyOnWriteArrayList<>());
|
||||
JsonNode newNode = objectMapper.convertValue(element, JsonNode.class);
|
||||
modelNodes.add(newNode);
|
||||
List<JsonNode> addedNodes = new ArrayList<>();
|
||||
JsonNode mapAddedNode = null;
|
||||
if (model.getVersion() == 1) {
|
||||
List<JsonNode> modelNodes = model.getNodesV1().computeIfAbsent(elementName,
|
||||
k -> new CopyOnWriteArrayList<>());
|
||||
JsonNode newNode = objectMapper.convertValue(element, JsonNode.class);
|
||||
modelNodes.add(newNode);
|
||||
addedNodes.add(newNode);
|
||||
} else {
|
||||
mapAddedNode = objectMapper.valueToTree(Map.of(id, element.cloneWithoutId()));
|
||||
JsonNode modelMapNode = model.getNodes().get(elementName);
|
||||
if (modelMapNode == null) {
|
||||
model.getNodes().put(elementName, mapAddedNode);
|
||||
} else {
|
||||
JsonNode newNode = objectMapper.convertValue(element.cloneWithoutId(), JsonNode.class);
|
||||
((ObjectNode) modelMapNode).set(id, newNode);
|
||||
}
|
||||
}
|
||||
// notify listeners
|
||||
getElementListeners(elementName).forEach(l -> {
|
||||
List newElements = parseJsonNodes(List.of(newNode), l.getElementClass());
|
||||
for (YamlModelListener<?> l : getElementListeners(elementName)) {
|
||||
List newElements = parseJsonNodes(addedNodes, mapAddedNode, l.getElementClass());
|
||||
if (!newElements.isEmpty()) {
|
||||
l.addedModel(modelName, newElements);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
writeModel(modelName);
|
||||
}
|
||||
@@ -303,11 +356,12 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
@Override
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void removeElementFromModel(String modelName, YamlElement element) {
|
||||
String id = element.getId();
|
||||
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());
|
||||
id, element.getClass());
|
||||
return;
|
||||
}
|
||||
String elementName = annotation.value();
|
||||
@@ -321,25 +375,35 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
return;
|
||||
}
|
||||
|
||||
List<JsonNode> modelNodes = model.getNodes().get(elementName);
|
||||
if (modelNodes == null) {
|
||||
List<JsonNode> modelNodes = model.getNodesV1().get(elementName);
|
||||
JsonNode modelMapNode = model.getNodes().get(elementName);
|
||||
if (modelNodes == null && modelMapNode == 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) {
|
||||
JsonNode toRemove = modelNodes == null ? null : findNodeById(modelNodes, element.getClass(), id);
|
||||
boolean found = modelMapNode == null ? false : modelMapNode.has(id);
|
||||
if (toRemove == null && !found) {
|
||||
logger.warn("Failed to remove {} from model {} because element is not in model.", element, modelName);
|
||||
return;
|
||||
}
|
||||
modelNodes.remove(toRemove);
|
||||
List<JsonNode> removedNodes = new ArrayList<>();
|
||||
JsonNode mapRemovedNode = null;
|
||||
if (toRemove != null) {
|
||||
modelNodes.remove(toRemove);
|
||||
removedNodes.add(toRemove);
|
||||
} else if (found) {
|
||||
((ObjectNode) modelMapNode).remove(id);
|
||||
mapRemovedNode = objectMapper.valueToTree(Map.of(id, element.cloneWithoutId()));
|
||||
}
|
||||
// notify listeners
|
||||
getElementListeners(elementName).forEach(l -> {
|
||||
List newElements = parseJsonNodes(List.of(toRemove), l.getElementClass());
|
||||
if (!newElements.isEmpty()) {
|
||||
l.addedModel(modelName, newElements);
|
||||
for (YamlModelListener<?> l : getElementListeners(elementName)) {
|
||||
List oldElements = parseJsonNodes(removedNodes, mapRemovedNode, l.getElementClass());
|
||||
if (!oldElements.isEmpty()) {
|
||||
l.removedModel(modelName, oldElements);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
writeModel(modelName);
|
||||
}
|
||||
@@ -347,11 +411,12 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
@Override
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void updateElementInModel(String modelName, YamlElement element) {
|
||||
String id = element.getId();
|
||||
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());
|
||||
id, element.getClass());
|
||||
return;
|
||||
}
|
||||
String elementName = annotation.value();
|
||||
@@ -365,27 +430,37 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
return;
|
||||
}
|
||||
|
||||
List<JsonNode> modelNodes = model.getNodes().get(elementName);
|
||||
if (modelNodes == null) {
|
||||
List<JsonNode> modelNodes = model.getNodesV1().get(elementName);
|
||||
JsonNode modelMapNode = model.getNodes().get(elementName);
|
||||
if (modelNodes == null && modelMapNode == 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) {
|
||||
JsonNode oldElement = modelNodes == null ? null : findNodeById(modelNodes, element.getClass(), id);
|
||||
boolean found = modelMapNode == null ? false : modelMapNode.has(id);
|
||||
if (oldElement == null && !found) {
|
||||
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);
|
||||
List<JsonNode> updatedNodes = new ArrayList<>();
|
||||
JsonNode mapUpdatedNode = null;
|
||||
if (oldElement != null) {
|
||||
JsonNode newNode = objectMapper.convertValue(element, JsonNode.class);
|
||||
modelNodes.set(modelNodes.indexOf(oldElement), newNode);
|
||||
updatedNodes.add(newNode);
|
||||
} else if (found) {
|
||||
JsonNode newNode = objectMapper.convertValue(element.cloneWithoutId(), JsonNode.class);
|
||||
((ObjectNode) modelMapNode).set(id, newNode);
|
||||
mapUpdatedNode = objectMapper.valueToTree(Map.of(id, element.cloneWithoutId()));
|
||||
}
|
||||
// notify listeners
|
||||
getElementListeners(elementName).forEach(l -> {
|
||||
List newElements = parseJsonNodes(List.of(newNode), l.getElementClass());
|
||||
for (YamlModelListener<?> l : getElementListeners(elementName)) {
|
||||
List newElements = parseJsonNodes(updatedNodes, mapUpdatedNode, l.getElementClass());
|
||||
if (!newElements.isEmpty()) {
|
||||
l.updatedModel(modelName, newElements);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
writeModel(modelName);
|
||||
}
|
||||
@@ -408,10 +483,18 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
|
||||
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);
|
||||
if (model.getVersion() == 1) {
|
||||
for (Map.Entry<String, List<JsonNode>> elementNodes : model.getNodesV1().entrySet()) {
|
||||
ArrayNode arrayNode = nodeFactory.arrayNode();
|
||||
elementNodes.getValue().forEach(arrayNode::add);
|
||||
rootNode.set(elementNodes.getKey(), arrayNode);
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<String, @Nullable JsonNode> elementNodes : model.getNodes().entrySet()) {
|
||||
if (elementNodes.getValue() != null) {
|
||||
rootNode.set(elementNodes.getKey(), elementNodes.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -433,6 +516,7 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
return Objects.requireNonNull(elementListeners.computeIfAbsent(elementName, k -> new CopyOnWriteArrayList<>()));
|
||||
}
|
||||
|
||||
// To be used only for version 1
|
||||
private <T extends YamlElement> @Nullable JsonNode findNodeById(List<JsonNode> nodes, Class<T> elementClass,
|
||||
String id) {
|
||||
return nodes.stream().filter(node -> {
|
||||
@@ -445,11 +529,13 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
return elements.stream().collect(Collectors.toMap(YamlElement::getId, e -> e));
|
||||
}
|
||||
|
||||
private <T extends YamlElement> List<T> parseJsonNodes(List<JsonNode> nodes, Class<T> elementClass) {
|
||||
// To be used only for version 1
|
||||
private <T extends YamlElement> List<T> parseJsonNodesV1(List<JsonNode> nodes, Class<T> elementClass) {
|
||||
return nodes.stream().map(nE -> parseJsonNode(nE, elementClass)).flatMap(Optional::stream)
|
||||
.filter(YamlElement::isValid).toList();
|
||||
}
|
||||
|
||||
// To be used only for version 1
|
||||
private <T extends YamlElement> Optional<T> parseJsonNode(JsonNode node, Class<T> elementClass) {
|
||||
try {
|
||||
return Optional.of(objectMapper.treeToValue(node, elementClass));
|
||||
@@ -458,4 +544,48 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// To be not used for version 1
|
||||
private <T extends YamlElement> List<T> parseJsonMapNode(@Nullable JsonNode mapNode, Class<T> elementClass) {
|
||||
List<T> elements = new ArrayList<>();
|
||||
if (mapNode != null) {
|
||||
Iterator<String> it = mapNode.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
String id = it.next();
|
||||
@Nullable
|
||||
T elt = null;
|
||||
JsonNode node = mapNode.get(id);
|
||||
if (node.isEmpty()) {
|
||||
try {
|
||||
elt = elementClass.getDeclaredConstructor().newInstance();
|
||||
elt.setId(id);
|
||||
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
|
||||
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
||||
logger.warn("Could not create new instance of {}", elementClass);
|
||||
}
|
||||
|
||||
} else {
|
||||
try {
|
||||
elt = objectMapper.treeToValue(node, elementClass);
|
||||
elt.setId(id);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("Could not parse element {} to {}: {}", node, elementClass, e.getMessage());
|
||||
}
|
||||
}
|
||||
if (elt != null && elt.isValid()) {
|
||||
elements.add(elt);
|
||||
}
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
// Usable whatever the format version
|
||||
private <T extends YamlElement> List<T> parseJsonNodes(List<JsonNode> nodes, @Nullable JsonNode mapNode,
|
||||
Class<T> elementClass) {
|
||||
List<T> elements = new ArrayList<>();
|
||||
elements.addAll(parseJsonNodesV1(nodes, elementClass));
|
||||
elements.addAll(parseJsonMapNode(mapNode, elementClass));
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
|
||||
+25
-2
@@ -17,6 +17,7 @@ import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
@@ -24,12 +25,20 @@ 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
|
||||
* @author Laurent Garnier - Introduce version 2 using map instead of table
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class YamlModelWrapper {
|
||||
private final int version;
|
||||
private final boolean readOnly;
|
||||
private final Map<String, List<JsonNode>> nodes = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* Nodes as a list in version 1
|
||||
*/
|
||||
private final Map<String, List<JsonNode>> nodesV1 = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* Nodes as a map in version >= 2
|
||||
*/
|
||||
private final Map<String, @Nullable JsonNode> nodes = new ConcurrentHashMap<>();
|
||||
|
||||
public YamlModelWrapper(int version, boolean readOnly) {
|
||||
this.version = version;
|
||||
@@ -44,7 +53,21 @@ public class YamlModelWrapper {
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
public Map<String, List<JsonNode>> getNodes() {
|
||||
/**
|
||||
* Get the nodes for version 1
|
||||
*
|
||||
* @return the nodes
|
||||
*/
|
||||
public Map<String, List<JsonNode>> getNodesV1() {
|
||||
return nodesV1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nodes for version >= 2
|
||||
*
|
||||
* @return the nodes
|
||||
*/
|
||||
public Map<String, @Nullable JsonNode> getNodes() {
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-3
@@ -27,9 +27,10 @@ import org.slf4j.LoggerFactory;
|
||||
* in a YAML configuration file.
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Laurent Garnier - Added methods setId and cloneWithoutId
|
||||
*/
|
||||
@YamlElementName("tags")
|
||||
public class YamlSemanticTagDTO implements YamlElement {
|
||||
public class YamlSemanticTagDTO implements YamlElement, Cloneable {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(YamlSemanticTagDTO.class);
|
||||
|
||||
@@ -43,12 +44,30 @@ public class YamlSemanticTagDTO implements YamlElement {
|
||||
|
||||
@Override
|
||||
public @NonNull String getId() {
|
||||
return uid;
|
||||
return uid == null ? "" : uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(@NonNull String id) {
|
||||
uid = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public YamlElement cloneWithoutId() {
|
||||
YamlSemanticTagDTO copy;
|
||||
try {
|
||||
copy = (YamlSemanticTagDTO) super.clone();
|
||||
copy.uid = null;
|
||||
return copy;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
// Will never happen
|
||||
return new YamlSemanticTagDTO();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
if (uid == null) {
|
||||
if (uid == null || uid.isBlank()) {
|
||||
logger.debug("uid missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
+242
-27
@@ -14,6 +14,7 @@ package org.openhab.core.model.yaml.internal;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
@@ -37,7 +38,6 @@ 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;
|
||||
@@ -48,6 +48,7 @@ import org.yaml.snakeyaml.Yaml;
|
||||
* The {@link YamlModelRepositoryImplTest} contains tests for the {@link YamlModelRepositoryImpl} class.
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
* @author Laurent Garnier - Extended tests to cover version 2
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@@ -56,10 +57,13 @@ public class YamlModelRepositoryImplTest {
|
||||
private static final Path SOURCE_PATH = Path.of("src/test/resources/model");
|
||||
private static final String MODEL_NAME = "model";
|
||||
private static final Path MODEL_PATH = Path.of(MODEL_NAME + ".yaml");
|
||||
private static final String MODEL2_NAME = "model2";
|
||||
private static final Path MODEL2_PATH = Path.of(MODEL2_NAME + ".yaml");
|
||||
|
||||
private @Mock @NonNullByDefault({}) WatchService watchServiceMock;
|
||||
private @TempDir @NonNullByDefault({}) Path watchPath;
|
||||
private @NonNullByDefault({}) Path fullModelPath;
|
||||
private @NonNullByDefault({}) Path fullModel2Path;
|
||||
|
||||
private @Mock @NonNullByDefault({}) YamlModelListener<@NonNull FirstTypeDTO> firstTypeListener;
|
||||
private @Mock @NonNullByDefault({}) YamlModelListener<@NonNull SecondTypeDTO> secondTypeListener1;
|
||||
@@ -72,6 +76,7 @@ public class YamlModelRepositoryImplTest {
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
fullModelPath = watchPath.resolve(MODEL_PATH);
|
||||
fullModel2Path = watchPath.resolve(MODEL2_PATH);
|
||||
when(watchServiceMock.getWatchPath()).thenReturn(watchPath);
|
||||
|
||||
when(firstTypeListener.getElementClass()).thenReturn(FirstTypeDTO.class);
|
||||
@@ -82,6 +87,7 @@ public class YamlModelRepositoryImplTest {
|
||||
@Test
|
||||
public void testFileAddedAfterListeners() throws IOException {
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"), fullModelPath);
|
||||
Files.copy(SOURCE_PATH.resolve("modelV2FileAddedOrRemoved.yaml"), fullModel2Path);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
@@ -89,54 +95,109 @@ public class YamlModelRepositoryImplTest {
|
||||
modelRepository.addYamlModelListener(secondTypeListener2);
|
||||
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL2_PATH);
|
||||
|
||||
verify(firstTypeListener).addedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener).addedModel(eq(MODEL2_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).addedModel(eq(MODEL2_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).addedModel(eq(MODEL2_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();
|
||||
List<Collection<FirstTypeDTO>> firstTypeCaptorValues = firstTypeCaptor.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(2));
|
||||
List<Collection<SecondTypeDTO>> secondTypeCaptor1Values = secondTypeCaptor1.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(2));
|
||||
List<Collection<SecondTypeDTO>> secondTypeCaptor2Values = secondTypeCaptor2.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(2));
|
||||
|
||||
Collection<FirstTypeDTO> firstTypeElements = firstTypeCaptorValues.getFirst();
|
||||
Collection<SecondTypeDTO> secondTypeElements1 = secondTypeCaptor1Values.getFirst();
|
||||
Collection<SecondTypeDTO> secondTypeElements2 = secondTypeCaptor2Values.getFirst();
|
||||
|
||||
assertThat(firstTypeElements, hasSize(2));
|
||||
assertThat(firstTypeElements,
|
||||
containsInAnyOrder(new FirstTypeDTO("First1", "Description1"), new FirstTypeDTO("First2", null)));
|
||||
assertThat(secondTypeElements1, hasSize(1));
|
||||
assertThat(secondTypeElements1, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
assertThat(secondTypeElements2, hasSize(1));
|
||||
assertThat(secondTypeElements2, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
|
||||
firstTypeElements = firstTypeCaptorValues.get(1);
|
||||
secondTypeElements1 = secondTypeCaptor1Values.get(1);
|
||||
secondTypeElements2 = secondTypeCaptor2Values.get(1);
|
||||
|
||||
assertThat(firstTypeElements, hasSize(2));
|
||||
assertThat(firstTypeElements,
|
||||
containsInAnyOrder(new FirstTypeDTO("First1", "Description1"), new FirstTypeDTO("First2", null)));
|
||||
assertThat(secondTypeElements1, hasSize(1));
|
||||
assertThat(secondTypeElements1, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
assertThat(secondTypeElements2, hasSize(1));
|
||||
assertThat(secondTypeElements2, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileAddedBeforeListeners() throws IOException {
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"), fullModelPath);
|
||||
Files.copy(SOURCE_PATH.resolve("modelV2FileAddedOrRemoved.yaml"), fullModel2Path);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL2_PATH);
|
||||
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
modelRepository.addYamlModelListener(secondTypeListener1);
|
||||
modelRepository.addYamlModelListener(secondTypeListener2);
|
||||
|
||||
verify(firstTypeListener).addedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener).addedModel(eq(MODEL2_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).addedModel(eq(MODEL2_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).addedModel(eq(MODEL2_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();
|
||||
List<Collection<FirstTypeDTO>> firstTypeCaptorValues = firstTypeCaptor.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(2));
|
||||
List<Collection<SecondTypeDTO>> secondTypeCaptor1Values = secondTypeCaptor1.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(2));
|
||||
List<Collection<SecondTypeDTO>> secondTypeCaptor2Values = secondTypeCaptor2.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(2));
|
||||
|
||||
Collection<FirstTypeDTO> firstTypeElements = firstTypeCaptorValues.getFirst();
|
||||
Collection<SecondTypeDTO> secondTypeElements1 = secondTypeCaptor1Values.getFirst();
|
||||
Collection<SecondTypeDTO> secondTypeElements2 = secondTypeCaptor2Values.getFirst();
|
||||
|
||||
assertThat(firstTypeElements, hasSize(2));
|
||||
assertThat(firstTypeElements,
|
||||
containsInAnyOrder(new FirstTypeDTO("First1", "Description1"), new FirstTypeDTO("First2", null)));
|
||||
assertThat(secondTypeElements1, hasSize(1));
|
||||
assertThat(secondTypeElements1, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
assertThat(secondTypeElements2, hasSize(1));
|
||||
assertThat(secondTypeElements2, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
|
||||
firstTypeElements = firstTypeCaptorValues.get(1);
|
||||
secondTypeElements1 = secondTypeCaptor1Values.get(1);
|
||||
secondTypeElements2 = secondTypeCaptor2Values.get(1);
|
||||
|
||||
assertThat(firstTypeElements, hasSize(2));
|
||||
assertThat(firstTypeElements,
|
||||
containsInAnyOrder(new FirstTypeDTO("First1", "Description1"), new FirstTypeDTO("First2", null)));
|
||||
assertThat(secondTypeElements1, hasSize(1));
|
||||
assertThat(secondTypeElements1, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
assertThat(secondTypeElements2, hasSize(1));
|
||||
assertThat(secondTypeElements2, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
}
|
||||
|
||||
@@ -145,6 +206,7 @@ public class YamlModelRepositoryImplTest {
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
|
||||
// File in v1 format
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileUpdatePost.yaml"), fullModelPath);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
verify(firstTypeListener).addedModel(eq(MODEL_NAME), any());
|
||||
@@ -155,18 +217,48 @@ public class YamlModelRepositoryImplTest {
|
||||
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));
|
||||
// File in v2 format
|
||||
Files.copy(SOURCE_PATH.resolve("modelV2FileUpdatePost.yaml"), fullModel2Path);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL2_PATH);
|
||||
verify(firstTypeListener).addedModel(eq(MODEL2_NAME), any());
|
||||
|
||||
// added originally
|
||||
assertThat(arguments.getFirst(), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
Files.copy(SOURCE_PATH.resolve("modelV2FileUpdatePre.yaml"), fullModel2Path,
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.MODIFY, MODEL2_PATH);
|
||||
verify(firstTypeListener, times(2)).addedModel(eq(MODEL2_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener).updatedModel(eq(MODEL2_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener).removedModel(eq(MODEL2_NAME), firstTypeCaptor.capture());
|
||||
|
||||
List<Collection<FirstTypeDTO>> firstTypeCaptorValues = firstTypeCaptor.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(8));
|
||||
|
||||
// added originally - first file
|
||||
assertThat(firstTypeCaptorValues.getFirst(), hasSize(3));
|
||||
assertThat(firstTypeCaptorValues.getFirst(), 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")));
|
||||
// added by update - first file
|
||||
assertThat(firstTypeCaptorValues.get(1), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(1), contains(new FirstTypeDTO("Fourth", "Fourth original")));
|
||||
// updated by update - first file
|
||||
assertThat(firstTypeCaptorValues.get(2), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(2), contains(new FirstTypeDTO("Second", "Second modified")));
|
||||
// removed by update - first file
|
||||
assertThat(firstTypeCaptorValues.get(3), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(3), contains(new FirstTypeDTO("Third", "Third original")));
|
||||
|
||||
// added originally -second file
|
||||
assertThat(firstTypeCaptorValues.get(4), hasSize(3));
|
||||
assertThat(firstTypeCaptorValues.get(4), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
new FirstTypeDTO("Second", "Second original"), new FirstTypeDTO("Third", "Third original")));
|
||||
// added by update -second file
|
||||
assertThat(firstTypeCaptorValues.get(5), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(5), contains(new FirstTypeDTO("Fourth", "Fourth original")));
|
||||
// updated by update -second file
|
||||
assertThat(firstTypeCaptorValues.get(6), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(6), contains(new FirstTypeDTO("Second", "Second modified")));
|
||||
// removed by update -second file
|
||||
assertThat(firstTypeCaptorValues.get(7), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(7), contains(new FirstTypeDTO("Third", "Third original")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -175,6 +267,7 @@ public class YamlModelRepositoryImplTest {
|
||||
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
|
||||
// File in v1 format
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileUpdatePost.yaml"), fullModelPath);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.DELETE, MODEL_PATH);
|
||||
@@ -183,14 +276,34 @@ public class YamlModelRepositoryImplTest {
|
||||
verify(firstTypeListener, never()).updatedModel(any(), any());
|
||||
verify(firstTypeListener).removedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
|
||||
List<Collection<FirstTypeDTO>> arguments = firstTypeCaptor.getAllValues();
|
||||
assertThat(arguments, hasSize(2));
|
||||
// File in v2 format
|
||||
Files.copy(SOURCE_PATH.resolve("modelV2FileUpdatePost.yaml"), fullModel2Path);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL2_PATH);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.DELETE, MODEL2_PATH);
|
||||
|
||||
// all are added
|
||||
assertThat(arguments.getFirst(), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
verify(firstTypeListener).addedModel(eq(MODEL2_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener, never()).updatedModel(any(), any());
|
||||
verify(firstTypeListener).removedModel(eq(MODEL2_NAME), firstTypeCaptor.capture());
|
||||
|
||||
List<Collection<FirstTypeDTO>> firstTypeCaptorValues = firstTypeCaptor.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(4));
|
||||
|
||||
// all are added - first file
|
||||
assertThat(firstTypeCaptorValues.getFirst(), hasSize(3));
|
||||
assertThat(firstTypeCaptorValues.getFirst(), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
new FirstTypeDTO("Second", "Second original"), new FirstTypeDTO("Third", "Third original")));
|
||||
// all are removed
|
||||
assertThat(arguments.getFirst(), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
// all are removed - first file
|
||||
assertThat(firstTypeCaptorValues.get(1), hasSize(3));
|
||||
assertThat(firstTypeCaptorValues.get(1), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
new FirstTypeDTO("Second", "Second original"), new FirstTypeDTO("Third", "Third original")));
|
||||
|
||||
// all are added - second file
|
||||
assertThat(firstTypeCaptorValues.get(2), hasSize(3));
|
||||
assertThat(firstTypeCaptorValues.get(2), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
new FirstTypeDTO("Second", "Second original"), new FirstTypeDTO("Third", "Third original")));
|
||||
// all are removed - second file
|
||||
assertThat(firstTypeCaptorValues.get(3), hasSize(3));
|
||||
assertThat(firstTypeCaptorValues.get(3), containsInAnyOrder(new FirstTypeDTO("First", "First original"),
|
||||
new FirstTypeDTO("Second", "Second original"), new FirstTypeDTO("Third", "Third original")));
|
||||
}
|
||||
|
||||
@@ -199,16 +312,57 @@ public class YamlModelRepositoryImplTest {
|
||||
Files.copy(SOURCE_PATH.resolve("modifyModelInitialContent.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
modelRepository.addYamlModelListener(secondTypeListener1);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
FirstTypeDTO added = new FirstTypeDTO("element3", "description3");
|
||||
modelRepository.addElementToModel(MODEL_NAME, added);
|
||||
SecondTypeDTO added2 = new SecondTypeDTO("elt1", "My label");
|
||||
modelRepository.addElementToModel(MODEL_NAME, added2);
|
||||
|
||||
verify(firstTypeListener, times(2)).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());
|
||||
|
||||
String actualFileContent = Files.readString(fullModelPath);
|
||||
String expectedFileContent = Files.readString(SOURCE_PATH.resolve("addToModelExpectedContent.yaml"));
|
||||
Yaml yaml = new Yaml();
|
||||
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent.replaceAll("\r\n", "\n"))));
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent)));
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modifyModelV2InitialContent.yaml"), fullModel2Path);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL2_PATH);
|
||||
modelRepository.addElementToModel(MODEL2_NAME, added);
|
||||
modelRepository.addElementToModel(MODEL2_NAME, added2);
|
||||
verify(firstTypeListener, times(2)).addedModel(eq(MODEL2_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener, never()).updatedModel(any(), any());
|
||||
verify(firstTypeListener, never()).removedModel(any(), any());
|
||||
verify(secondTypeListener1).addedModel(eq(MODEL2_NAME), secondTypeCaptor1.capture());
|
||||
verify(secondTypeListener1, never()).updatedModel(any(), any());
|
||||
verify(secondTypeListener1, never()).removedModel(any(), any());
|
||||
|
||||
actualFileContent = Files.readString(fullModel2Path);
|
||||
expectedFileContent = Files.readString(SOURCE_PATH.resolve("addToModelV2ExpectedContent.yaml"));
|
||||
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent)));
|
||||
|
||||
List<Collection<FirstTypeDTO>> firstTypeCaptorValues = firstTypeCaptor.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(4));
|
||||
assertThat(firstTypeCaptorValues.get(1), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(1), contains(new FirstTypeDTO("element3", "description3")));
|
||||
assertThat(firstTypeCaptorValues.get(3), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(3), contains(new FirstTypeDTO("element3", "description3")));
|
||||
|
||||
List<Collection<SecondTypeDTO>> secondTypeCaptor1Values = secondTypeCaptor1.getAllValues();
|
||||
assertThat(secondTypeCaptor1Values, hasSize(2));
|
||||
assertThat(secondTypeCaptor1Values.getFirst(), hasSize(1));
|
||||
assertThat(secondTypeCaptor1Values.getFirst(), contains(new SecondTypeDTO("elt1", "My label")));
|
||||
assertThat(secondTypeCaptor1Values.get(1), hasSize(1));
|
||||
assertThat(secondTypeCaptor1Values.get(1), contains(new SecondTypeDTO("elt1", "My label")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -216,16 +370,40 @@ public class YamlModelRepositoryImplTest {
|
||||
Files.copy(SOURCE_PATH.resolve("modifyModelInitialContent.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
FirstTypeDTO updated = new FirstTypeDTO("element1", "newDescription1");
|
||||
modelRepository.updateElementInModel(MODEL_NAME, updated);
|
||||
|
||||
verify(firstTypeListener).addedModel(eq(MODEL_NAME), any());
|
||||
verify(firstTypeListener).updatedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener, never()).removedModel(any(), any());
|
||||
|
||||
String actualFileContent = Files.readString(fullModelPath);
|
||||
String expectedFileContent = Files.readString(SOURCE_PATH.resolve("updateInModelExpectedContent.yaml"));
|
||||
Yaml yaml = new Yaml();
|
||||
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent.replaceAll("\r\n", "\n"))));
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent)));
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modifyModelV2InitialContent.yaml"), fullModel2Path);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL2_PATH);
|
||||
modelRepository.updateElementInModel(MODEL2_NAME, updated);
|
||||
verify(firstTypeListener).addedModel(eq(MODEL2_NAME), any());
|
||||
verify(firstTypeListener).updatedModel(eq(MODEL2_NAME), firstTypeCaptor.capture());
|
||||
verify(firstTypeListener, never()).removedModel(any(), any());
|
||||
|
||||
actualFileContent = Files.readString(fullModel2Path);
|
||||
expectedFileContent = Files.readString(SOURCE_PATH.resolve("updateInModelV2ExpectedContent.yaml"));
|
||||
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent)));
|
||||
|
||||
List<Collection<FirstTypeDTO>> firstTypeCaptorValues = firstTypeCaptor.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(2));
|
||||
assertThat(firstTypeCaptorValues.getFirst(), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.getFirst(), contains(new FirstTypeDTO("element1", "newDescription1")));
|
||||
assertThat(firstTypeCaptorValues.get(1), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(1), contains(new FirstTypeDTO("element1", "newDescription1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -236,12 +414,37 @@ public class YamlModelRepositoryImplTest {
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
FirstTypeDTO removed = new FirstTypeDTO("element1", "description1");
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
modelRepository.removeElementFromModel(MODEL_NAME, removed);
|
||||
|
||||
verify(firstTypeListener).addedModel(eq(MODEL_NAME), any());
|
||||
verify(firstTypeListener, never()).updatedModel(any(), any());
|
||||
verify(firstTypeListener).removedModel(eq(MODEL_NAME), firstTypeCaptor.capture());
|
||||
|
||||
String actualFileContent = Files.readString(fullModelPath);
|
||||
String expectedFileContent = Files.readString(SOURCE_PATH.resolve("removeFromModelExpectedContent.yaml"));
|
||||
Yaml yaml = new Yaml();
|
||||
|
||||
assertThat(actualFileContent, is(expectedFileContent.replaceAll("\r\n", "\n")));
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent)));
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modifyModelV2InitialContent.yaml"), fullModel2Path);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL2_PATH);
|
||||
modelRepository.removeElementFromModel(MODEL2_NAME, removed);
|
||||
verify(firstTypeListener).addedModel(eq(MODEL2_NAME), any());
|
||||
verify(firstTypeListener, never()).updatedModel(any(), any());
|
||||
verify(firstTypeListener).removedModel(eq(MODEL2_NAME), firstTypeCaptor.capture());
|
||||
|
||||
actualFileContent = Files.readString(fullModel2Path);
|
||||
expectedFileContent = Files.readString(SOURCE_PATH.resolve("removeFromModelV2ExpectedContent.yaml"));
|
||||
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent)));
|
||||
|
||||
List<Collection<FirstTypeDTO>> firstTypeCaptorValues = firstTypeCaptor.getAllValues();
|
||||
assertThat(firstTypeCaptorValues, hasSize(2));
|
||||
assertThat(firstTypeCaptorValues.getFirst(), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.getFirst(), contains(new FirstTypeDTO("element1", "description1")));
|
||||
assertThat(firstTypeCaptorValues.get(1), hasSize(1));
|
||||
assertThat(firstTypeCaptorValues.get(1), contains(new FirstTypeDTO("element1", "description1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -262,7 +465,19 @@ public class YamlModelRepositoryImplTest {
|
||||
|
||||
String actualFileContent = Files.readString(fullModelPath);
|
||||
String expectedFileContent = Files.readString(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"));
|
||||
Yaml yaml = new Yaml();
|
||||
|
||||
assertThat(actualFileContent, is(expectedFileContent));
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent)));
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modelV2FileAddedOrRemoved.yaml"), fullModel2Path);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL2_PATH);
|
||||
modelRepository.addElementToModel(MODEL2_NAME, added);
|
||||
modelRepository.removeElementFromModel(MODEL2_NAME, removed);
|
||||
modelRepository.updateElementInModel(MODEL2_NAME, updated);
|
||||
|
||||
actualFileContent = Files.readString(fullModel2Path);
|
||||
expectedFileContent = Files.readString(SOURCE_PATH.resolve("modelV2FileAddedOrRemoved.yaml"));
|
||||
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent)));
|
||||
}
|
||||
}
|
||||
|
||||
+20
-3
@@ -24,7 +24,7 @@ import org.openhab.core.model.yaml.YamlElementName;
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@YamlElementName("firstType")
|
||||
public class FirstTypeDTO implements YamlElement {
|
||||
public class FirstTypeDTO implements YamlElement, Cloneable {
|
||||
public String uid;
|
||||
public String description;
|
||||
|
||||
@@ -38,12 +38,29 @@ public class FirstTypeDTO implements YamlElement {
|
||||
|
||||
@Override
|
||||
public @NonNull String getId() {
|
||||
return uid;
|
||||
return uid == null ? "" : uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(@NonNull String id) {
|
||||
uid = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public YamlElement cloneWithoutId() {
|
||||
FirstTypeDTO copy;
|
||||
try {
|
||||
copy = (FirstTypeDTO) super.clone();
|
||||
copy.uid = null;
|
||||
return copy;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
return new FirstTypeDTO();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return uid != null;
|
||||
return uid != null && !uid.isBlank();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+20
-3
@@ -24,7 +24,7 @@ import org.openhab.core.model.yaml.YamlElementName;
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@YamlElementName("secondType")
|
||||
public class SecondTypeDTO implements YamlElement {
|
||||
public class SecondTypeDTO implements YamlElement, Cloneable {
|
||||
public String id;
|
||||
public String label;
|
||||
|
||||
@@ -38,12 +38,29 @@ public class SecondTypeDTO implements YamlElement {
|
||||
|
||||
@Override
|
||||
public @NonNull String getId() {
|
||||
return id;
|
||||
return id == null ? "" : id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(@NonNull String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public YamlElement cloneWithoutId() {
|
||||
SecondTypeDTO copy;
|
||||
try {
|
||||
copy = (SecondTypeDTO) super.clone();
|
||||
copy.id = null;
|
||||
return copy;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
return new SecondTypeDTO();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return id != null;
|
||||
return id != null && !id.isBlank();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
@@ -7,3 +7,6 @@ firstType:
|
||||
description: description2
|
||||
- uid: element3
|
||||
description: description3
|
||||
secondType:
|
||||
- id: elt1
|
||||
label: My label
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
version: 2
|
||||
readOnly: false
|
||||
firstType:
|
||||
element1:
|
||||
description: description1
|
||||
element2:
|
||||
description: description2
|
||||
element3:
|
||||
description: description3
|
||||
secondType:
|
||||
elt1:
|
||||
label: My label
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# A YAML test file in version 2 with different types
|
||||
# The structure is valid but also contains invalid elements
|
||||
|
||||
version: 2
|
||||
|
||||
# known first type
|
||||
firstType:
|
||||
# a valid element with uid and description
|
||||
First1:
|
||||
description: Description1
|
||||
|
||||
# a valid element with uid only
|
||||
First2:
|
||||
|
||||
# known second type
|
||||
secondType:
|
||||
# a valid element with id and label
|
||||
Second1:
|
||||
label: Label1
|
||||
|
||||
# unknown third type
|
||||
thirdType:
|
||||
Foobar:
|
||||
|
||||
nonArrayElement: Test
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# A YAML test file in version 2 for updating the model
|
||||
|
||||
version: 2
|
||||
|
||||
firstType:
|
||||
First:
|
||||
description: First original
|
||||
Second:
|
||||
description: Second original
|
||||
Third:
|
||||
description: Third original
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# A YAML test file in version 2 for updating the model
|
||||
|
||||
version: 2
|
||||
|
||||
firstType:
|
||||
First:
|
||||
description: First original
|
||||
Second:
|
||||
description: Second modified
|
||||
Fourth:
|
||||
description: Fourth original
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
readOnly: false
|
||||
firstType:
|
||||
element1:
|
||||
description: description1
|
||||
element2:
|
||||
description: description2
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
version: 2
|
||||
readOnly: false
|
||||
firstType:
|
||||
element2:
|
||||
description: description2
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
readOnly: false
|
||||
firstType:
|
||||
element1:
|
||||
description: newDescription1
|
||||
element2:
|
||||
description: description2
|
||||
Reference in New Issue
Block a user