mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-29 12:34:22 +02:00
YAML configuration: add basic version management (#4667)
Each YAML element provider can now define which YAML file versions it is applicable to and whether it is deprecated or not. Related to #3666 Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
+17
-1
@@ -18,12 +18,13 @@ 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.
|
||||
* with data processed from YAML configuration files having a version that is supported by this manager.
|
||||
* <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
|
||||
* @author Laurent Garnier - Added basic version management
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface YamlModelListener<T extends YamlElement> {
|
||||
@@ -62,4 +63,19 @@ public interface YamlModelListener<T extends YamlElement> {
|
||||
* @return the DTO element class
|
||||
*/
|
||||
Class<T> getElementClass();
|
||||
|
||||
/**
|
||||
* Indicates whether a YAML file version is supported.
|
||||
*
|
||||
* @param version the YAML file version to check
|
||||
* @return true if the version is supported
|
||||
*/
|
||||
boolean isVersionSupported(int version);
|
||||
|
||||
/**
|
||||
* Indicates whether this manager is deprecated.
|
||||
*
|
||||
* @return true if this manager is deprecated
|
||||
*/
|
||||
boolean isDeprecated();
|
||||
}
|
||||
|
||||
+28
-6
@@ -70,6 +70,7 @@ 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
|
||||
* @author Laurent Garnier - Added basic version management
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true)
|
||||
@@ -157,11 +158,12 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
if (removedModel == null) {
|
||||
return;
|
||||
}
|
||||
int version = removedModel.getVersion();
|
||||
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 -> {
|
||||
getElementListeners(elementName, version).forEach(listener -> {
|
||||
List removedElements = parseJsonNodesV1(removedNodes, listener.getElementClass());
|
||||
listener.removedModel(modelName, removedElements);
|
||||
});
|
||||
@@ -171,7 +173,7 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
String elementName = modelEntry.getKey();
|
||||
JsonNode removedMapNode = modelEntry.getValue();
|
||||
if (removedMapNode != null) {
|
||||
getElementListeners(elementName).forEach(listener -> {
|
||||
getElementListeners(elementName, version).forEach(listener -> {
|
||||
List removedElements = parseJsonMapNode(removedMapNode, listener.getElementClass());
|
||||
listener.removedModel(modelName, removedElements);
|
||||
});
|
||||
@@ -234,7 +236,7 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
List<JsonNode> oldNodeV1Elements = model.getNodesV1().getOrDefault(elementName, List.of());
|
||||
JsonNode oldNodeElements = model.getNodes().get(elementName);
|
||||
|
||||
for (YamlModelListener<?> elementListener : getElementListeners(elementName)) {
|
||||
for (YamlModelListener<?> elementListener : getElementListeners(elementName, modelVersion)) {
|
||||
Class<? extends YamlElement> elementClass = elementListener.getElementClass();
|
||||
|
||||
Map<String, ? extends YamlElement> oldElements = listToMap(
|
||||
@@ -250,6 +252,13 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
e -> oldElements.containsKey(e.getId()) && !e.equals(oldElements.get(e.getId())))
|
||||
.toList();
|
||||
|
||||
if (elementListener.isDeprecated()
|
||||
&& (!addedElements.isEmpty() || !updatedElements.isEmpty())) {
|
||||
logger.warn(
|
||||
"Element {} in model {} version {} is still supported but deprecated, please consider migrating your model to a more recent version",
|
||||
elementName, modelName, modelVersion);
|
||||
}
|
||||
|
||||
if (!addedElements.isEmpty()) {
|
||||
elementListener.addedModel(modelName, addedElements);
|
||||
}
|
||||
@@ -291,6 +300,15 @@ 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();
|
||||
int modelVersion = model.getValue().getVersion();
|
||||
if (!listener.isVersionSupported(modelVersion)) {
|
||||
continue;
|
||||
}
|
||||
if (listener.isDeprecated()) {
|
||||
logger.warn(
|
||||
"Element {} in model {} version {} is still supported but deprecated, please consider migrating your model to a more recent version",
|
||||
elementName, modelName, modelVersion);
|
||||
}
|
||||
List<JsonNode> modelNodes = model.getValue().getNodesV1().getOrDefault(elementName, List.of());
|
||||
JsonNode modelMapNode = model.getValue().getNodes().get(elementName);
|
||||
if (modelNodes.isEmpty() && modelMapNode == null) {
|
||||
@@ -343,7 +361,7 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
}
|
||||
}
|
||||
// notify listeners
|
||||
for (YamlModelListener<?> l : getElementListeners(elementName)) {
|
||||
for (YamlModelListener<?> l : getElementListeners(elementName, model.getVersion())) {
|
||||
List newElements = parseJsonNodes(addedNodes, mapAddedNode, l.getElementClass());
|
||||
if (!newElements.isEmpty()) {
|
||||
l.addedModel(modelName, newElements);
|
||||
@@ -398,7 +416,7 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
mapRemovedNode = objectMapper.valueToTree(Map.of(id, element.cloneWithoutId()));
|
||||
}
|
||||
// notify listeners
|
||||
for (YamlModelListener<?> l : getElementListeners(elementName)) {
|
||||
for (YamlModelListener<?> l : getElementListeners(elementName, model.getVersion())) {
|
||||
List oldElements = parseJsonNodes(removedNodes, mapRemovedNode, l.getElementClass());
|
||||
if (!oldElements.isEmpty()) {
|
||||
l.removedModel(modelName, oldElements);
|
||||
@@ -455,7 +473,7 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
mapUpdatedNode = objectMapper.valueToTree(Map.of(id, element.cloneWithoutId()));
|
||||
}
|
||||
// notify listeners
|
||||
for (YamlModelListener<?> l : getElementListeners(elementName)) {
|
||||
for (YamlModelListener<?> l : getElementListeners(elementName, model.getVersion())) {
|
||||
List newElements = parseJsonNodes(updatedNodes, mapUpdatedNode, l.getElementClass());
|
||||
if (!newElements.isEmpty()) {
|
||||
l.updatedModel(modelName, newElements);
|
||||
@@ -516,6 +534,10 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
||||
return Objects.requireNonNull(elementListeners.computeIfAbsent(elementName, k -> new CopyOnWriteArrayList<>()));
|
||||
}
|
||||
|
||||
private List<YamlModelListener<?>> getElementListeners(String elementName, int version) {
|
||||
return getElementListeners(elementName).stream().filter(l -> l.isVersionSupported(version)).toList();
|
||||
}
|
||||
|
||||
// To be used only for version 1
|
||||
private <T extends YamlElement> @Nullable JsonNode findNodeById(List<JsonNode> nodes, Class<T> elementClass,
|
||||
String id) {
|
||||
|
||||
+11
@@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory;
|
||||
* These semantic tags are automatically exposed to the {@link org.openhab.core.semantics.SemanticTagRegistry}.
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Laurent Garnier - Added basic version management
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true, service = { SemanticTagProvider.class, YamlSemanticTagProvider.class,
|
||||
@@ -107,4 +108,14 @@ public class YamlSemanticTagProvider extends AbstractProvider<SemanticTag>
|
||||
private SemanticTag mapSemanticTag(YamlSemanticTagDTO tagDTO) {
|
||||
return new SemanticTagImpl(tagDTO.uid, tagDTO.label, tagDTO.description, tagDTO.synonyms);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVersionSupported(int version) {
|
||||
return version >= 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeprecated() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+114
-1
@@ -15,8 +15,8 @@ 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.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -49,6 +49,7 @@ import org.yaml.snakeyaml.Yaml;
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
* @author Laurent Garnier - Extended tests to cover version 2
|
||||
* @author Laurent Garnier - Added one test for version management
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@@ -80,8 +81,14 @@ public class YamlModelRepositoryImplTest {
|
||||
when(watchServiceMock.getWatchPath()).thenReturn(watchPath);
|
||||
|
||||
when(firstTypeListener.getElementClass()).thenReturn(FirstTypeDTO.class);
|
||||
when(firstTypeListener.isVersionSupported(anyInt())).thenReturn(true);
|
||||
when(firstTypeListener.isDeprecated()).thenReturn(false);
|
||||
when(secondTypeListener1.getElementClass()).thenReturn(SecondTypeDTO.class);
|
||||
when(secondTypeListener1.isVersionSupported(anyInt())).thenReturn(true);
|
||||
when(secondTypeListener1.isDeprecated()).thenReturn(false);
|
||||
when(secondTypeListener2.getElementClass()).thenReturn(SecondTypeDTO.class);
|
||||
when(secondTypeListener2.isVersionSupported(anyInt())).thenReturn(true);
|
||||
when(secondTypeListener2.isDeprecated()).thenReturn(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -480,4 +487,110 @@ public class YamlModelRepositoryImplTest {
|
||||
|
||||
assertThat(yaml.load(actualFileContent), equalTo(yaml.load(expectedFileContent)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExistingProviderForVersion() throws IOException {
|
||||
// Provider supporting only version 1
|
||||
when(firstTypeListener.isVersionSupported(eq(1))).thenReturn(true);
|
||||
when(firstTypeListener.isVersionSupported(eq(2))).thenReturn(false);
|
||||
when(firstTypeListener.isDeprecated()).thenReturn(false);
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
|
||||
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());
|
||||
|
||||
Collection<FirstTypeDTO> firstTypeElements = firstTypeCaptor.getValue();
|
||||
assertThat(firstTypeElements, hasSize(2));
|
||||
assertThat(firstTypeElements,
|
||||
containsInAnyOrder(new FirstTypeDTO("First1", "Description1"), new FirstTypeDTO("First2", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExistingDeprecatedProviderForVersion() throws IOException {
|
||||
// Provider supporting only version 1 and deprecated
|
||||
when(firstTypeListener.isVersionSupported(eq(1))).thenReturn(true);
|
||||
when(firstTypeListener.isVersionSupported(eq(2))).thenReturn(false);
|
||||
when(firstTypeListener.isDeprecated()).thenReturn(true);
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
|
||||
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());
|
||||
|
||||
Collection<FirstTypeDTO> firstTypeElements = firstTypeCaptor.getValue();
|
||||
assertThat(firstTypeElements, hasSize(2));
|
||||
assertThat(firstTypeElements,
|
||||
containsInAnyOrder(new FirstTypeDTO("First1", "Description1"), new FirstTypeDTO("First2", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoProviderForVersion() throws IOException {
|
||||
// Provider supporting only version 2
|
||||
when(firstTypeListener.isVersionSupported(eq(1))).thenReturn(false);
|
||||
when(firstTypeListener.isVersionSupported(eq(2))).thenReturn(true);
|
||||
when(firstTypeListener.isDeprecated()).thenReturn(false);
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"), fullModelPath);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(firstTypeListener);
|
||||
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
|
||||
verify(firstTypeListener, never()).addedModel(any(), any());
|
||||
verify(firstTypeListener, never()).updatedModel(any(), any());
|
||||
verify(firstTypeListener, never()).removedModel(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDifferentProvidersDependingOnVersion() throws IOException {
|
||||
// secondTypeListener1 supports version 1 only
|
||||
when(secondTypeListener1.isVersionSupported(eq(1))).thenReturn(true);
|
||||
when(secondTypeListener1.isVersionSupported(eq(2))).thenReturn(false);
|
||||
when(secondTypeListener1.isDeprecated()).thenReturn(false);
|
||||
// secondTypeListener2 supports version 2 only
|
||||
when(secondTypeListener2.isVersionSupported(eq(1))).thenReturn(false);
|
||||
when(secondTypeListener2.isVersionSupported(eq(2))).thenReturn(true);
|
||||
when(secondTypeListener2.isDeprecated()).thenReturn(false);
|
||||
|
||||
Files.copy(SOURCE_PATH.resolve("modelFileAddedOrRemoved.yaml"), fullModelPath);
|
||||
Files.copy(SOURCE_PATH.resolve("modelV2FileAddedOrRemoved.yaml"), fullModel2Path);
|
||||
|
||||
YamlModelRepositoryImpl modelRepository = new YamlModelRepositoryImpl(watchServiceMock);
|
||||
modelRepository.addYamlModelListener(secondTypeListener1);
|
||||
modelRepository.addYamlModelListener(secondTypeListener2);
|
||||
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL_PATH);
|
||||
modelRepository.processWatchEvent(WatchService.Kind.CREATE, MODEL2_PATH);
|
||||
|
||||
verify(secondTypeListener1).addedModel(eq(MODEL_NAME), secondTypeCaptor1.capture());
|
||||
verify(secondTypeListener1, never()).addedModel(eq(MODEL2_NAME), any());
|
||||
verify(secondTypeListener1, never()).updatedModel(any(), any());
|
||||
verify(secondTypeListener1, never()).removedModel(any(), any());
|
||||
verify(secondTypeListener2).addedModel(eq(MODEL2_NAME), secondTypeCaptor2.capture());
|
||||
verify(secondTypeListener2, never()).addedModel(eq(MODEL_NAME), any());
|
||||
verify(secondTypeListener2, never()).updatedModel(any(), any());
|
||||
verify(secondTypeListener2, never()).removedModel(any(), any());
|
||||
|
||||
Collection<SecondTypeDTO> secondTypeElements = secondTypeCaptor1.getValue();
|
||||
assertThat(secondTypeElements, hasSize(1));
|
||||
assertThat(secondTypeElements, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
|
||||
secondTypeElements = secondTypeCaptor2.getValue();
|
||||
assertThat(secondTypeElements, hasSize(1));
|
||||
assertThat(secondTypeElements, contains(new SecondTypeDTO("Second1", "Label1")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user