Skip to content

Commit 0eecf01

Browse files
committed
Add include dependency change processing
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
1 parent 7743285 commit 0eecf01

5 files changed

Lines changed: 111 additions & 33 deletions

File tree

bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTracker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import org.eclipse.jdt.annotation.NonNullByDefault;
2929
import org.openhab.core.automation.module.script.ScriptDependencyTracker;
30-
import org.openhab.core.automation.module.script.rulesupport.internal.loader.BidiSetBag;
30+
import org.openhab.core.common.BidiSetBag;
3131
import org.openhab.core.service.WatchService;
3232
import org.slf4j.Logger;
3333
import org.slf4j.LoggerFactory;

bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepositoryImpl.java

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
import org.eclipse.jdt.annotation.NonNullByDefault;
3838
import org.eclipse.jdt.annotation.Nullable;
39+
import org.openhab.core.common.BidiSetBag;
3940
import org.openhab.core.model.yaml.YamlElement;
4041
import org.openhab.core.model.yaml.YamlElementName;
4142
import org.openhab.core.model.yaml.YamlModelListener;
@@ -107,6 +108,10 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
107108
// all model nodes, ordered by model name (full path as string) and type
108109
private final Map<String, YamlModelWrapper> modelCache = new ConcurrentHashMap<>();
109110

111+
// keep track of include files so we can reload the main model when they change
112+
// Bidirectional Map of modelName <-> include path by this model
113+
private final BidiSetBag<String, Path> modelIncludes = new BidiSetBag<>();
114+
110115
@Activate
111116
public YamlModelRepositoryImpl(@Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) {
112117
YAMLFactory yamlFactory = YAMLFactory.builder() //
@@ -154,6 +159,7 @@ public FileVisitResult visitFileFailed(@NonNullByDefault({}) Path file,
154159
@Deactivate
155160
public void deactivate() {
156161
watchService.unregisterListener(this);
162+
modelIncludes.clear();
157163
}
158164

159165
// The method is "synchronized" to avoid concurrent files processing
@@ -162,7 +168,21 @@ public void deactivate() {
162168
public synchronized void processWatchEvent(Kind kind, Path fullPath) {
163169
Path relativePath = watchPath.relativize(fullPath);
164170
String modelName = relativePath.toString();
165-
if (relativePath.startsWith("automation") || !modelName.endsWith(".yaml") || modelName.endsWith(".inc.yaml")) {
171+
172+
if (relativePath.startsWith("automation")) {
173+
return;
174+
}
175+
176+
// always clear the list of includes if it's a model
177+
// if it loads correctly, it will be re-populated
178+
modelIncludes.removeKey(modelName);
179+
180+
// check here because include files can have any extension
181+
if (processIncludeFile(kind, fullPath)) {
182+
return;
183+
}
184+
185+
if (!modelName.endsWith(".yaml") || modelName.endsWith(".inc.yaml")) {
166186
logger.trace("Ignored {}", fullPath);
167187
return;
168188
}
@@ -171,7 +191,10 @@ public synchronized void processWatchEvent(Kind kind, Path fullPath) {
171191
if (kind == WatchService.Kind.DELETE) {
172192
removeModel(modelName);
173193
} else if (!Files.isHidden(fullPath) && Files.isReadable(fullPath) && !Files.isDirectory(fullPath)) {
174-
JsonNode fileContent = objectMapper.valueToTree(YamlPreprocessor.load(fullPath));
194+
Object yamlObject = YamlPreprocessor.load(fullPath, includePath -> {
195+
modelIncludes.put(modelName, includePath);
196+
});
197+
JsonNode fileContent = objectMapper.valueToTree(yamlObject);
175198

176199
// check version
177200
JsonNode versionNode = fileContent.get(VERSION);
@@ -299,13 +322,44 @@ public synchronized void processWatchEvent(Kind kind, Path fullPath) {
299322
}
300323
}
301324

325+
private boolean processIncludeFile(Kind kind, Path fullPath) {
326+
boolean logged = false;
327+
328+
Set<String> dependingModels = modelIncludes.getKeys(fullPath);
329+
330+
if (dependingModels.isEmpty()) {
331+
return false;
332+
}
333+
334+
logger.info("An include file '{}' was {}", fullPath, switch (kind) {
335+
case WatchService.Kind.CREATE -> "created";
336+
case WatchService.Kind.DELETE -> "deleted";
337+
case WatchService.Kind.MODIFY -> "modified";
338+
default -> "unknown";
339+
});
340+
341+
dependingModels.forEach(modelName -> {
342+
Path modelPath = watchPath.resolve(modelName);
343+
try {
344+
// reprocess the model that depends on this include file
345+
processWatchEvent(WatchService.Kind.MODIFY, modelPath);
346+
} catch (Exception e) {
347+
logger.warn("Failed to reprocess model {} after include file change: {}", modelName, e.getMessage());
348+
}
349+
});
350+
351+
return true;
352+
}
353+
302354
@SuppressWarnings({ "rawtypes", "unchecked" })
303355
private void removeModel(String modelName) {
304356
YamlModelWrapper removedModel = modelCache.remove(modelName);
305357
if (removedModel == null) {
306358
return;
307359
}
308360
logger.info("Removing YAML model {}", modelName);
361+
modelIncludes.removeKey(modelName);
362+
309363
int version = removedModel.getVersion();
310364
for (Map.Entry<String, @Nullable JsonNode> modelEntry : removedModel.getNodes().entrySet()) {
311365
String elementName = modelEntry.getKey();

bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/util/preprocessor/YamlPreprocessor.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Set;
25+
import java.util.function.Consumer;
2526
import java.util.stream.Collectors;
2627
import java.util.stream.Stream;
2728

@@ -57,12 +58,13 @@ public class YamlPreprocessor {
5758

5859
private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(YamlPreprocessor.class);
5960

60-
public static Object load(Path file) throws IOException {
61-
return load(file, new HashMap<>(), new HashSet<>());
61+
public static Object load(Path file, Consumer<Path> includeCallback) throws IOException {
62+
return load(file, new HashMap<>(), new HashSet<>(), includeCallback);
6263
}
6364

6465
@SuppressWarnings("unchecked")
65-
static Object load(Path file, Map<String, String> variables, Set<Path> includeStack) throws IOException {
66+
static Object load(Path file, Map<String, String> variables, Set<Path> includeStack, Consumer<Path> includeCallback)
67+
throws IOException {
6668
LOGGER.debug("Loading file({}): {} with given vars {}", includeStack.size(), file, variables);
6769

6870
Set<Path> includeStackBranch = new HashSet<>(includeStack);
@@ -94,7 +96,8 @@ static Object load(Path file, Map<String, String> variables, Set<Path> includeSt
9496
dataMap.remove(VARIABLES_KEY); // we've already extracted the variables in the first pass
9597
LOGGER.trace("Loaded data from {}: {}", file, dataMap);
9698

97-
dataMap = (Map<String, Object>) processIncludes(file, dataMap, combinedVars, includeStackBranch);
99+
dataMap = (Map<String, Object>) processIncludes(file, dataMap, combinedVars, includeStackBranch,
100+
includeCallback);
98101
LOGGER.trace("Loaded includes from {}: {}", file, dataMap);
99102
Map<String, Object> packages = (Map<String, Object>) dataMap.remove(PACKAGES_KEY);
100103
dataMap = mergePackages(dataMap, packages);
@@ -161,30 +164,33 @@ private static void addSpecialVariables(Map<String, String> variables, Path file
161164
* This method is called recursively for nested objects.
162165
*/
163166
@SuppressWarnings("unchecked")
164-
private static Object processIncludes(Path file, Object data, Map<String, String> variables,
165-
Set<Path> includeStack) {
167+
private static Object processIncludes(Path file, Object data, Map<String, String> variables, Set<Path> includeStack,
168+
Consumer<Path> includeCallback) {
166169
if (data instanceof IncludeObject includeObject) {
167-
return loadIncludeFile(file, includeObject, variables, includeStack);
170+
return loadIncludeFile(file, includeObject, variables, includeStack, includeCallback);
168171
} else if (data instanceof Map) {
169172
Map<String, Object> dataMap = (Map<String, Object>) data;
170173
return dataMap.entrySet().stream()
171174
.collect(Collectors.toMap(Map.Entry::getKey,
172-
entry -> processIncludes(file, entry.getValue(), variables, includeStack),
175+
entry -> processIncludes(file, entry.getValue(), variables, includeStack, includeCallback),
173176
(existing, replacement) -> replacement, LinkedHashMap::new));
174177
} else if (data instanceof List) {
175178
List<Object> dataList = (List<Object>) data;
176-
return dataList.stream().map(value -> processIncludes(file, value, variables, includeStack)).toList();
179+
return dataList.stream()
180+
.map(value -> processIncludes(file, value, variables, includeStack, includeCallback)).toList();
177181
}
178182
return data;
179183
}
180184

181185
private static Object loadIncludeFile(Path file, IncludeObject includeObject, Map<String, String> variables,
182-
Set<Path> includeStack) {
186+
Set<Path> includeStack, Consumer<Path> includeCallback) {
183187
Path includeFile = file.resolveSibling(includeObject.fileName());
184188
Map<String, String> includeVars = new HashMap<>(variables);
185189
includeVars.putAll(includeObject.vars());
186190
try {
187-
return load(includeFile, includeVars, includeStack);
191+
Object loadedFile = load(includeFile, includeVars, includeStack, includeCallback);
192+
includeCallback.accept(includeFile);
193+
return loadedFile;
188194
} catch (IOException e) {
189195
LOGGER.warn("Error loading include file {}", e.getMessage());
190196
return Map.of();

bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/util/preprocessor/YamlPreprocessorTest.java

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ public void booleanParserTest() throws IOException {
7575

7676
@Test
7777
void anchorsTest() throws IOException {
78-
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(SOURCE_PATH.resolve("anchors.yaml"));
78+
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(SOURCE_PATH.resolve("anchors.yaml"),
79+
this::emptyCallback);
7980
assertThat(data.get("baz"), equalTo("bar"));
8081
assertThat(data.get("bar"), equalTo("qux"));
8182
}
@@ -93,15 +94,15 @@ void getNestedValueTest() {
9394
// from the resulting data structure
9495
void extraElementsRemovedTest() throws IOException {
9596
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor
96-
.load(SOURCE_PATH.resolve("extraElementsRemoved.yaml"));
97+
.load(SOURCE_PATH.resolve("extraElementsRemoved.yaml"), this::emptyCallback);
9798
assertNull(data.get("variables"));
9899
assertNull(data.get("packages"));
99100
}
100101

101102
@Test
102103
public void simpleVariableSubstitutionsTest() throws IOException {
103104
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor
104-
.load(SOURCE_PATH.resolve("simpleVariableSubstitutions.yaml"));
105+
.load(SOURCE_PATH.resolve("simpleVariableSubstitutions.yaml"), this::emptyCallback);
105106
assertThat(data.get("plainkey"), equalTo("value1"));
106107
assertThat(data.get("dynamickey"), equalTo("dynamicvalue"));
107108

@@ -117,14 +118,14 @@ public void simpleVariableSubstitutionsTest() throws IOException {
117118
@Test
118119
public void nestedVariablesTest() throws IOException {
119120
Map<String, String> data = (Map<String, String>) YamlPreprocessor
120-
.load(SOURCE_PATH.resolve("nestedVariables.yaml"));
121+
.load(SOURCE_PATH.resolve("nestedVariables.yaml"), this::emptyCallback);
121122
assertThat(data.get("key"), equalTo("value"));
122123
}
123124

124125
@Test
125126
void variableSyntaxTest() throws IOException {
126127
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor
127-
.load(SOURCE_PATH.resolve("variableSyntax.yaml"));
128+
.load(SOURCE_PATH.resolve("variableSyntax.yaml"), this::emptyCallback);
128129

129130
assertThat(data.get("empty_no_default"), equalTo(""));
130131
assertThat(data.get("absent_no_default"), equalTo(""));
@@ -149,24 +150,24 @@ void variableSyntaxTest() throws IOException {
149150

150151
@Test
151152
void include1DeepTest() throws IOException {
152-
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor
153-
.load(SOURCE_PATH.resolve("include1Deep.yaml"));
153+
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(SOURCE_PATH.resolve("include1Deep.yaml"),
154+
this::emptyCallback);
154155

155156
assertThat(YamlPreprocessor.getNestedValue(data, "toplevel", "includedkey"), equalTo("value"));
156157
}
157158

158159
@Test
159160
void include2DeepTest() throws IOException {
160-
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor
161-
.load(SOURCE_PATH.resolve("include2Deep.yaml"));
161+
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(SOURCE_PATH.resolve("include2Deep.yaml"),
162+
this::emptyCallback);
162163

163164
assertThat(YamlPreprocessor.getNestedValue(data, "toplevel", "level1", "level2"), equalTo("foo"));
164165
}
165166

166167
@Test
167168
void predefinedVarsTest() throws IOException {
168169
Path sourcePath = SOURCE_PATH.resolve("predefinedVars.yaml");
169-
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(sourcePath);
170+
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(sourcePath, this::emptyCallback);
170171

171172
assertThat(data.get("file"), equalTo(sourcePath.toAbsolutePath().toString()));
172173
assertThat(data.get("filename"), equalTo("predefinedVars"));
@@ -177,7 +178,7 @@ void predefinedVarsTest() throws IOException {
177178
@Test
178179
void predefinedVarsNotOverridableTest() throws IOException {
179180
Path sourcePath = SOURCE_PATH.resolve("predefinedVarsNotOverridable.yaml");
180-
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(sourcePath);
181+
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(sourcePath, this::emptyCallback);
181182

182183
Path file = sourcePath;
183184
assertThat(data.get("file"), equalTo(file.toAbsolutePath().toString()));
@@ -197,7 +198,7 @@ void predefinedVarsNotOverridableTest() throws IOException {
197198
@Test
198199
void circularInclusionTest() throws IOException {
199200
try {
200-
YamlPreprocessor.load(SOURCE_PATH.resolve("circularInclusion.yaml"));
201+
YamlPreprocessor.load(SOURCE_PATH.resolve("circularInclusion.yaml"), this::emptyCallback);
201202
fail("Expected an exception to be thrown");
202203
} catch (YAMLException e) {
203204
assertThat(e.getMessage(), containsString("Circular inclusion detected"));
@@ -207,30 +208,31 @@ void circularInclusionTest() throws IOException {
207208
@Test
208209
void includedTopLevelVarsTest() throws IOException {
209210
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor
210-
.load(SOURCE_PATH.resolve("includedTopLevelVars.yaml"));
211+
.load(SOURCE_PATH.resolve("includedTopLevelVars.yaml"), this::emptyCallback);
211212

212213
assertThat(YamlPreprocessor.getNestedValue(data, "toplevel", "level1"), equalTo("set_at_toplevel"));
213214
}
214215

215216
@Test
216217
void includedTopLevelFileVarsTest() throws IOException {
217218
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor
218-
.load(SOURCE_PATH.resolve("includedTopLevelFileVars.yaml"));
219+
.load(SOURCE_PATH.resolve("includedTopLevelFileVars.yaml"), this::emptyCallback);
219220

220221
assertThat(YamlPreprocessor.getNestedValue(data, "toplevel", "level1"), equalTo("set_at_include_level"));
221222
}
222223

223224
@Test
224225
void varsPropagate2LevelsTest() throws IOException {
225226
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor
226-
.load(SOURCE_PATH.resolve("varsPropagate2Levels.yaml"));
227+
.load(SOURCE_PATH.resolve("varsPropagate2Levels.yaml"), this::emptyCallback);
227228

228229
assertThat(YamlPreprocessor.getNestedValue(data, "toplevel", "data", "data"), equalTo("toplevel"));
229230
}
230231

231232
@Test
232233
void packagesTest() throws IOException {
233-
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(SOURCE_PATH.resolve("packages.yaml"));
234+
Map<String, Object> data = (Map<String, Object>) YamlPreprocessor.load(SOURCE_PATH.resolve("packages.yaml"),
235+
this::emptyCallback);
234236

235237
// defined in the package
236238
assertThat(YamlPreprocessor.getNestedValue(data, "things", "thing1", "label"), equalTo("label1"));
@@ -249,4 +251,9 @@ void packagesTest() throws IOException {
249251

250252
assertThat(YamlPreprocessor.getNestedValue(data, "list", "test1"), equalTo(List.of("main1", "package1")));
251253
}
254+
255+
void emptyCallback(Path includeFile) {
256+
// This method is intentionally left empty to satisfy the callback interface
257+
// in the tests where no action is needed on include files.
258+
}
252259
}

bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/BidiSetBag.java renamed to bundles/org.openhab.core/src/main/java/org/openhab/core/common/BidiSetBag.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*
1111
* SPDX-License-Identifier: EPL-2.0
1212
*/
13-
package org.openhab.core.automation.module.script.rulesupport.internal.loader;
13+
package org.openhab.core.common;
1414

1515
import java.util.Collections;
1616
import java.util.HashMap;
@@ -27,6 +27,7 @@
2727
*
2828
* @author Jonathan Gilbert - Initial contribution
2929
* @author Jan N. Klug - Make implementation thread-safe
30+
* @author Jimmy Tanagra - Remove map when empty
3031
* @param <K> Type of Key
3132
* @param <V> Type of Value
3233
*/
@@ -75,7 +76,7 @@ public Set<V> removeKey(K key) {
7576
for (V value : values) {
7677
valueToKeys.computeIfPresent(value, (k, v) -> {
7778
v.remove(key);
78-
return v;
79+
return v.isEmpty() ? null : v;
7980
});
8081
}
8182
return values;
@@ -95,7 +96,7 @@ public Set<K> removeValue(V value) {
9596
for (K key : keys) {
9697
keyToValues.computeIfPresent(key, (k, v) -> {
9798
v.remove(value);
98-
return v;
99+
return v.isEmpty() ? null : v;
99100
});
100101
}
101102
return keys;
@@ -106,4 +107,14 @@ public Set<K> removeValue(V value) {
106107
lock.writeLock().unlock();
107108
}
108109
}
110+
111+
public void clear() {
112+
lock.writeLock().lock();
113+
try {
114+
keyToValues.clear();
115+
valueToKeys.clear();
116+
} finally {
117+
lock.writeLock().unlock();
118+
}
119+
}
109120
}

0 commit comments

Comments
 (0)