diff --git a/tools/upgradetool/pom.xml b/tools/upgradetool/pom.xml index 8a68d763ba..e9d064227a 100644 --- a/tools/upgradetool/pom.xml +++ b/tools/upgradetool/pom.xml @@ -17,6 +17,10 @@ openHAB Core :: Tools :: Upgrade tool A tool for upgrading openHAB + + 2.18.2 + + org.openhab.core.bundles @@ -73,6 +77,18 @@ org.eclipse.jdt.annotation 2.2.600 + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + compile + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + compile + diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java index a72c8b199c..2d419d84ee 100644 --- a/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java @@ -50,7 +50,8 @@ public class UpgradeTool { private static final List 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); diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/YamlConfigurationV1TagsUpgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/YamlConfigurationV1TagsUpgrader.java new file mode 100644 index 0000000000..76bbfe4334 --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/YamlConfigurationV1TagsUpgrader.java @@ -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()); + } + } +}