Add Yaml configuration tags list to map upgrader

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
pull/4718/head
Jimmy Tanagra 2025-04-28 16:06:44 +10:00 committed by Kai Kreuzer
parent 2515b05ee3
commit 3e0d14fed4
3 changed files with 213 additions and 1 deletions

View File

@ -17,6 +17,10 @@
<name>openHAB Core :: Tools :: Upgrade tool</name>
<description>A tool for upgrading openHAB</description>
<properties>
<jackson.version>2.18.2</jackson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
@ -73,6 +77,18 @@
<artifactId>org.eclipse.jdt.annotation</artifactId>
<version>2.2.600</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>

View File

@ -50,7 +50,8 @@ public class UpgradeTool {
private static final List<Upgrader> UPGRADERS = List.of( //
new ItemUnitToMetadataUpgrader(), //
new JSProfileUpgrader(), //
new ScriptProfileUpgrader() //
new ScriptProfileUpgrader(), //
new YamlConfigurationV1TagsUpgrader() // Added in 5.0
);
private static final Logger logger = LoggerFactory.getLogger(UpgradeTool.class);

View File

@ -0,0 +1,195 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.tools.internal;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.tools.Upgrader;
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.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
/**
* The {@link YamlConfigurationV1TagsUpgrader} upgrades YAML Tags Configuration from List to Map.
*
* Convert list to map format for tags in V1 configuration files.
*
* Input file criteria:
* - Search only in CONF/tags/, or in the given directory, and its subdirectories
* - Contains a version key with value 1
* - it must contain a tags key that is a list
* - The tags list must contain a uid key
* - If the above criteria are not met, the file will not be modified
*
* Output file will
* - Retain `version: 1`
* - convert tags list to a map with uid as key and the rest as map
* - Preserve the order of the tags
* - other keys will be unchanged
* - A backup of the original file will be created with the extension `.yaml.org`
* - If an .org file already exists, append a number to the end, e.g. `.org.1`
*
* @since 5.0.0
*
* @author Jimmy Tanagra - Initial contribution
*/
@NonNullByDefault
public class YamlConfigurationV1TagsUpgrader implements Upgrader {
private static final String VERSION = "version";
private final Logger logger = LoggerFactory.getLogger(YamlConfigurationV1TagsUpgrader.class);
private final YAMLFactory yamlFactory;
private final ObjectMapper objectMapper;
public YamlConfigurationV1TagsUpgrader() {
// match the options used in {@link YamlModelRepositoryImpl}
yamlFactory = YAMLFactory.builder() //
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) // omit "---" at file start
.disable(YAMLGenerator.Feature.SPLIT_LINES) // do not split long lines
.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR) // indent arrays
.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) // use quotes only where necessary
.enable(YAMLParser.Feature.PARSE_BOOLEAN_LIKE_WORDS_AS_STRINGS).build(); // do not parse ON/OFF/... as
// booleans
objectMapper = new ObjectMapper(yamlFactory);
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);
}
@Override
public String getName() {
return "yamlTagsListToMap";
}
@Override
public String getDescription() {
return "Upgrade YAML 'tags' list to map format on V1 configuration files";
}
@Override
public boolean execute(String userdataDir, String confDir) {
String confEnv = System.getenv("OPENHAB_CONF");
// If confDir is set to OPENHAB_CONF, look inside /tags/ subdirectory
// otherwise use the given confDir as is
if (confEnv != null && !confEnv.isBlank() && confEnv.equals(confDir)) {
confDir = Path.of(confEnv, "tags").toString();
}
Path configPath = Path.of(confDir).toAbsolutePath();
logger.info("Upgrading YAML tags configurations in '{}'", configPath);
try {
Files.walkFileTree(configPath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(@NonNullByDefault({}) Path file,
@NonNullByDefault({}) BasicFileAttributes attrs) throws IOException {
if (attrs.isRegularFile()) {
Path relativePath = configPath.relativize(file);
String modelName = relativePath.toString();
if (!relativePath.startsWith("automation") && modelName.endsWith(".yaml")) {
logger.info("Checking {}", file);
convertTagsListToMap(file);
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(@NonNullByDefault({}) Path file,
@NonNullByDefault({}) IOException exc) throws IOException {
logger.warn("Failed to process {}: {}", file.toAbsolutePath(), exc.getClass().getSimpleName());
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
logger.error("Failed to walk through the directory {}: {}", configPath, e.getMessage());
return false;
}
return true;
}
private void convertTagsListToMap(Path filePath) {
try {
JsonNode fileContent = objectMapper.readTree(filePath.toFile());
JsonNode versionNode = fileContent.get(VERSION);
if (versionNode == null || !versionNode.canConvertToInt() || versionNode.asInt() != 1) {
return;
}
JsonNode tagsNode = fileContent.get("tags");
if (tagsNode == null || !tagsNode.isArray()) {
return;
}
logger.info("Found v1 yaml file with tags list {}", filePath);
fileContent.properties().forEach(entry -> {
String key = entry.getKey();
JsonNode node = entry.getValue();
if (key.equals("tags")) {
ObjectNode tagsMap = objectMapper.createObjectNode();
for (JsonNode tag : node) {
if (tag.hasNonNull("uid")) {
String uid = tag.get("uid").asText();
((ObjectNode) tag).remove("uid");
tagsMap.set(uid, tag);
} else {
logger.warn("Tag {} does not have a uid, skipping", tag);
}
}
((ObjectNode) fileContent).set(key, tagsMap);
}
});
String output = objectMapper.writeValueAsString(fileContent);
saveFile(filePath, output);
} catch (IOException e) {
logger.error("Failed to read YAML file {}: {}", filePath, e.getMessage());
return;
}
}
private void saveFile(Path filePath, String content) {
Path backupPath = filePath.resolveSibling(filePath.getFileName() + ".org");
int i = 1;
while (Files.exists(backupPath)) {
backupPath = filePath.resolveSibling(filePath.getFileName() + ".org." + i);
i++;
}
try {
Files.move(filePath, backupPath);
Files.writeString(filePath, content);
logger.info("Converted {} to map format, and the original file saved as {}", filePath, backupPath);
} catch (IOException e) {
logger.error("Failed to save YAML file {}: {}", filePath, e.getMessage());
}
}
}