diff --git a/tools/upgradetool/pom.xml b/tools/upgradetool/pom.xml index 8a68d763bad..e9d064227a2 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 063f11718b0..83a1889b923 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 @@ -12,8 +12,10 @@ */ package org.openhab.core.tools; -import static org.openhab.core.tools.internal.Upgrader.*; - +import java.io.PrintStream; +import java.nio.file.Path; +import java.time.ZonedDateTime; +import java.util.List; import java.util.Set; import org.apache.commons.cli.CommandLine; @@ -23,27 +25,53 @@ import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.tools.internal.Upgrader; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.storage.json.internal.JsonStorage; +import org.openhab.core.tools.internal.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link UpgradeTool} is a tool for upgrading openHAB to mitigate breaking changes * * @author Jan N. Klug - Initial contribution + * @author Jimmy Tanagra - Refactor upgraders into individual classes */ @NonNullByDefault public class UpgradeTool { private static final Set LOG_LEVELS = Set.of("TRACE", "DEBUG", "INFO", "WARN", "ERROR"); private static final String OPT_COMMAND = "command"; - private static final String OPT_DIR = "dir"; + private static final String OPT_LIST_COMMANDS = "list-commands"; + private static final String OPT_USERDATA_DIR = "userdata"; + private static final String OPT_CONF_DIR = "conf"; private static final String OPT_LOG = "log"; private static final String OPT_FORCE = "force"; + private static final String ENV_USERDATA = "OPENHAB_USERDATA"; + private static final String ENV_CONF = "OPENHAB_CONF"; + + private static final List UPGRADERS = List.of( // + new ItemUnitToMetadataUpgrader(), // + new JSProfileUpgrader(), // + new ScriptProfileUpgrader(), // + new YamlConfigurationV1TagsUpgrader() // Added in 5.0 + ); + + private static final Logger logger = LoggerFactory.getLogger(UpgradeTool.class); + private static @Nullable JsonStorage upgradeRecords = null; + private static Options getOptions() { Options options = new Options(); - options.addOption(Option.builder().longOpt(OPT_DIR).desc("directory to process").numberOfArgs(1).build()); + options.addOption(Option.builder().longOpt(OPT_USERDATA_DIR).desc( + "USERDATA directory to process. Enclose it in double quotes to ensure that any backslashes are not ignored by your command shell.") + .numberOfArgs(1).build()); + options.addOption(Option.builder().longOpt(OPT_CONF_DIR).desc( + "CONF directory to process. Enclose it in double quotes to ensure that any backslashes are not ignored by your command shell.") + .numberOfArgs(1).build()); options.addOption(Option.builder().longOpt(OPT_COMMAND).numberOfArgs(1) .desc("command to execute (executes all if omitted)").build()); + options.addOption(Option.builder().longOpt(OPT_LIST_COMMANDS).desc("list available commands").build()); options.addOption(Option.builder().longOpt(OPT_LOG).numberOfArgs(1).desc("log verbosity").build()); options.addOption(Option.builder().longOpt(OPT_FORCE).desc("force execution (even if already done)").build()); @@ -58,42 +86,132 @@ public static void main(String[] args) { String loglevel = commandLine.hasOption(OPT_LOG) ? commandLine.getOptionValue(OPT_LOG).toUpperCase() : "INFO"; if (!LOG_LEVELS.contains(loglevel)) { - System.out.println("Allowed log-levels are " + LOG_LEVELS); + println("Allowed log-levels are " + LOG_LEVELS); + System.exit(0); + } + + if (commandLine.hasOption(OPT_LIST_COMMANDS)) { + println("Available commands:"); + UPGRADERS.stream().forEach(upgrader -> { + println(" - " + upgrader.getName() + ": " + upgrader.getDescription()); + }); System.exit(0); } System.setProperty(org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, loglevel); - String baseDir = commandLine.hasOption(OPT_DIR) ? commandLine.getOptionValue(OPT_DIR) - : System.getenv("OPENHAB_USERDATA"); - if (baseDir == null || baseDir.isBlank()) { - System.out.println( - "Please either set the environment variable ${OPENHAB_USERDATA} or provide a directory through the --dir option."); + boolean force = commandLine.hasOption(OPT_FORCE); + String command = commandLine.hasOption(OPT_COMMAND) ? commandLine.getOptionValue(OPT_COMMAND) : null; + + if (command != null && UPGRADERS.stream().filter(u -> u.getName().equals(command)).findAny().isEmpty()) { + println("Unknown command: " + command); System.exit(0); + } + + Path userdataPath = getPath("userdata", commandLine, OPT_USERDATA_DIR, ENV_USERDATA); + Path confPath = getPath("conf", commandLine, OPT_CONF_DIR, ENV_CONF); + + if (userdataPath != null) { + Path upgradeJsonDatabasePath = userdataPath + .resolve(Path.of("jsondb", "org.openhab.core.tools.UpgradeTool")); + upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); } else { - boolean force = commandLine.hasOption(OPT_FORCE); + logger.warn("Upgrade records storage is not initialized."); + } - Upgrader upgrader = new Upgrader(baseDir, force); - if (!commandLine.hasOption(OPT_COMMAND) - || ITEM_COPY_UNIT_TO_METADATA.equals(commandLine.getOptionValue(OPT_COMMAND))) { - upgrader.itemCopyUnitToMetadata(); + UPGRADERS.forEach(upgrader -> { + String upgraderName = upgrader.getName(); + if (command != null && !upgraderName.equals(command)) { + return; } - if (!commandLine.hasOption(OPT_COMMAND) - || LINK_UPGRADE_JS_PROFILE.equals(commandLine.getOptionValue(OPT_COMMAND))) { - upgrader.linkUpgradeJsProfile(); + if (!force && lastExecuted(upgraderName) instanceof String executionDate) { + logger.info("Already executed '{}' on {}. Use '--force' to execute it again.", upgraderName, + executionDate); + return; } - if (!commandLine.hasOption(OPT_COMMAND) - || LINK_UPGRADE_SCRIPT_PROFILE.equals(commandLine.getOptionValue(OPT_COMMAND))) { - upgrader.linkUpgradeScriptProfile(); + try { + logger.info("Executing {}: {}", upgraderName, upgrader.getDescription()); + if (upgrader.execute(userdataPath, confPath)) { + updateUpgradeRecord(upgraderName); + } + } catch (Exception e) { + logger.error("Error executing upgrader {}: {}", upgraderName, e.getMessage()); } - } + }); } catch (ParseException e) { HelpFormatter formatter = new HelpFormatter(); - String commands = Set.of(ITEM_COPY_UNIT_TO_METADATA, LINK_UPGRADE_JS_PROFILE, LINK_UPGRADE_SCRIPT_PROFILE) - .toString(); - formatter.printHelp("upgradetool", "", options, "Available commands: " + commands, true); + formatter.printHelp("upgradetool", "", options, "", true); } System.exit(0); } + + /** + * Returns the path to the given directory, either from the command line or from the environment variable. + * If neither is set, it defaults to a relative subdirectory of the given pathName ('./userdata' or './conf'). + * + * @param pathName the name of the directory (e.g., "userdata" or "conf"). + * @param commandLine a CommandLine instance. + * @param option the command line option for the directory (e.g., "userdata" or "conf"). + * @param env the environment variable name for the directory (e.g., "OPENHAB_USERDATA" or "OPENHAB_CONF"). + * @return the absolute path to the directory, or null if it does not exist. + */ + private static @Nullable Path getPath(String pathName, CommandLine commandLine, String option, String env) { + Path path = Path.of(pathName); + + String optionValue = commandLine.getOptionValue(option); + String envValue = System.getenv(env); + + if (optionValue != null && !optionValue.isBlank()) { + path = Path.of(optionValue); + } else if (envValue != null && !envValue.isBlank()) { + path = Path.of(envValue); + } + + path = path.toAbsolutePath(); + + if (path.toFile().isDirectory()) { + return path; + } else { + logger.warn( + "The '{}' directory '{}' does not exist. Some tasks may fail. To set it, either set the environment variable ${{}} or provide a directory through the --{} option.", + pathName, path, env, option); + return null; + } + } + + private static @Nullable String lastExecuted(String upgrader) { + JsonStorage records = upgradeRecords; + if (records != null) { + UpgradeRecord upgradeRecord = records.get(upgrader); + if (upgradeRecord != null) { + return upgradeRecord.executionDate; + } + } + return null; + } + + private static void updateUpgradeRecord(String upgrader) { + JsonStorage records = upgradeRecords; + if (records != null) { + records.put(upgrader, new UpgradeRecord(ZonedDateTime.now())); + records.flush(); + } + } + + // to avoid compiler's null pointer warnings + private static void println(String message) { + PrintStream out = System.out; + if (out != null) { + out.println(message); + } + } + + private static class UpgradeRecord { + public final String executionDate; + + public UpgradeRecord(ZonedDateTime executionDate) { + this.executionDate = executionDate.toString(); + } + } } diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/Upgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/Upgrader.java new file mode 100644 index 00000000000..17c9e8c7c0e --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/Upgrader.java @@ -0,0 +1,43 @@ +/* + * 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; + +import java.nio.file.Path; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link Upgrader} provides an interface for upgrading openHAB configuration files. + * + * Implementing class MUST provide a no-argument constructor. + * + * @author Jimmy Tanagra - Initial contribution + */ +@NonNullByDefault +public interface Upgrader { + String getName(); + + String getDescription(); + + /** + * Executes the upgrade process. + * + * @param userdataPath the OPENHAB_USERDATA directory for the upgrade, + * or a custom path given by the user as --userdata argument + * @param confPath the OPENHAB_CONF directory for the upgrade, + * or a custom path given by the user as --conf argument + * @return true if the upgrade was successful, false otherwise + */ + boolean execute(@Nullable Path userdataPath, @Nullable Path confPath); +} diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/Upgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ItemUnitToMetadataUpgrader.java similarity index 55% rename from tools/upgradetool/src/main/java/org/openhab/core/tools/internal/Upgrader.java rename to tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ItemUnitToMetadataUpgrader.java index 20bca3443d8..09f4dd1646e 100644 --- a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/Upgrader.java +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ItemUnitToMetadataUpgrader.java @@ -18,77 +18,64 @@ import java.nio.file.Files; import java.nio.file.Path; -import java.time.ZonedDateTime; import java.util.List; import java.util.Objects; import javax.measure.Unit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.config.core.Configuration; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.items.ManagedItemProvider; import org.openhab.core.items.Metadata; import org.openhab.core.items.MetadataKey; import org.openhab.core.library.items.NumberItem; import org.openhab.core.storage.json.internal.JsonStorage; import org.openhab.core.thing.dto.ThingDTO; -import org.openhab.core.thing.internal.link.ItemChannelLinkConfigDescriptionProvider; import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.tools.Upgrader; import org.openhab.core.types.util.UnitUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The {@link Upgrader} contains the implementation of the upgrade methods + * The {@link ItemUnitToMetadataUpgrader} copies the unit from the item to the metadata. * * @author Jan N. Klug - Initial contribution - * @author Florian Hotze - Add script profile upgrade + * @author Jimmy Tanagra - Refactored into a separate class */ @NonNullByDefault -public class Upgrader { - public static final String ITEM_COPY_UNIT_TO_METADATA = "itemCopyUnitToMetadata"; - public static final String LINK_UPGRADE_JS_PROFILE = "linkUpgradeJsProfile"; - public static final String LINK_UPGRADE_SCRIPT_PROFILE = "linkUpgradeScriptProfile"; +public class ItemUnitToMetadataUpgrader implements Upgrader { + private final Logger logger = LoggerFactory.getLogger(ItemUnitToMetadataUpgrader.class); - private final Logger logger = LoggerFactory.getLogger(Upgrader.class); - private final String baseDir; - private final boolean force; - private final JsonStorage upgradeRecords; - - public Upgrader(String baseDir, boolean force) { - this.baseDir = baseDir; - this.force = force; - - Path upgradeJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.tools.UpgradeTool"); + @Override + public String getName() { + return "itemCopyUnitToMetadata"; // keep the old name for backwards compatibility + } - upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); + @Override + public String getDescription() { + return "Copy item unit from state description to metadata"; } - private boolean checkUpgradeRecord(String key) { - UpgradeRecord upgradeRecord = upgradeRecords.get(key); - if (upgradeRecord != null && !force) { - logger.info("Already executed '{}' on {}. Use '--force' to execute it again.", key, - upgradeRecord.executionDate); + @Override + public boolean execute(@Nullable Path userdataPath, @Nullable Path confPath) { + if (userdataPath == null) { + logger.error("{} skipped: no userdata directory found.", getName()); return false; } - return true; - } - public void itemCopyUnitToMetadata() { + userdataPath = userdataPath.resolve("jsondb"); boolean noLink; - if (!checkUpgradeRecord(ITEM_COPY_UNIT_TO_METADATA)) { - return; - } - Path itemJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.items.Item.json"); - Path metadataJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.items.Metadata.json"); - Path linkJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json"); - Path thingJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.thing.Thing.json"); + Path itemJsonDatabasePath = userdataPath.resolve("org.openhab.core.items.Item.json"); + Path metadataJsonDatabasePath = userdataPath.resolve("org.openhab.core.items.Metadata.json"); + Path linkJsonDatabasePath = userdataPath.resolve("org.openhab.core.thing.link.ItemChannelLink.json"); + Path thingJsonDatabasePath = userdataPath.resolve("org.openhab.core.thing.Thing.json"); logger.info("Copying item unit from state description to metadata in database '{}'", itemJsonDatabasePath); if (!Files.isReadable(itemJsonDatabasePath)) { logger.error("Cannot access item database '{}', check path and access rights.", itemJsonDatabasePath); - return; + return false; } if (!Files.isReadable(linkJsonDatabasePath) || !Files.isReadable(thingJsonDatabasePath)) { @@ -102,7 +89,7 @@ public void itemCopyUnitToMetadata() { if (!Files.isWritable(metadataJsonDatabasePath) && Files.exists(metadataJsonDatabasePath)) { logger.error("Cannot access metadata database '{}', check path and access rights.", metadataJsonDatabasePath); - return; + return false; } JsonStorage itemStorage = new JsonStorage<>(itemJsonDatabasePath.toFile(), @@ -121,7 +108,7 @@ public void itemCopyUnitToMetadata() { logger.debug("{}: Already contains a 'unit' metadata, skipping it", itemName); } else { String unit = null; - if (!noLink) { + if (linkStorage != null && thingStorage != null) { List links = linkStorage.getValues().stream().map(Objects::requireNonNull) .filter(link -> itemName.equals(link.getItemName())).toList(); // check if we can find the channel for these links @@ -192,98 +179,7 @@ public void itemCopyUnitToMetadata() { }); metadataStorage.flush(); - upgradeRecords.put(ITEM_COPY_UNIT_TO_METADATA, new UpgradeRecord(ZonedDateTime.now())); - upgradeRecords.flush(); - } - - public void linkUpgradeJsProfile() { - if (!checkUpgradeRecord(LINK_UPGRADE_JS_PROFILE)) { - return; - } - - Path linkJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json"); - logger.info("Upgrading JS profile configuration in database '{}'", linkJsonDatabasePath); - - if (!Files.isWritable(linkJsonDatabasePath)) { - logger.error("Cannot access link database '{}', check path and access rights.", linkJsonDatabasePath); - return; - } - JsonStorage linkStorage = new JsonStorage<>(linkJsonDatabasePath.toFile(), null, 5, 0, 0, - List.of()); - - List.copyOf(linkStorage.getKeys()).forEach(linkUid -> { - ItemChannelLink link = Objects.requireNonNull(linkStorage.get(linkUid)); - Configuration configuration = link.getConfiguration(); - String profileName = (String) configuration.get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE); - if ("transform:JS".equals(profileName)) { - String function = (String) configuration.get("function"); - if (function != null) { - configuration.put("toItemScript", function); - configuration.put("toHandlerScript", "|input"); - configuration.remove("function"); - configuration.remove("sourceFormat"); - - linkStorage.put(linkUid, link); - logger.info("{}: rewrote JS profile link to new format", linkUid); - } else { - logger.info("{}: link already has correct configuration", linkUid); - } - } - }); - - linkStorage.flush(); - upgradeRecords.put(LINK_UPGRADE_JS_PROFILE, new UpgradeRecord(ZonedDateTime.now())); - upgradeRecords.flush(); - } - - /** - * Upgrades the ItemChannelLink database for the separation of {@code toHandlerScript} into - * {@code commandFromItemScript} and {@code stateFromItemScript}. - * See openhab/openhab-core#4058. - */ - public void linkUpgradeScriptProfile() { - if (!checkUpgradeRecord(LINK_UPGRADE_SCRIPT_PROFILE)) { - return; - } - - Path linkJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json"); - logger.info("Upgrading script profile configuration in database '{}'", linkJsonDatabasePath); - - if (!Files.isWritable(linkJsonDatabasePath)) { - logger.error("Cannot access link database '{}', check path and access rights.", linkJsonDatabasePath); - return; - } - JsonStorage linkStorage = new JsonStorage<>(linkJsonDatabasePath.toFile(), null, 5, 0, 0, - List.of()); - - List.copyOf(linkStorage.getKeys()).forEach(linkUid -> { - ItemChannelLink link = Objects.requireNonNull(linkStorage.get(linkUid)); - Configuration configuration = link.getConfiguration(); - String profileName = (String) configuration.get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE); - if (profileName != null && profileName.startsWith("transform:")) { - String toHandlerScript = (String) configuration.get("toHandlerScript"); - if (toHandlerScript != null) { - configuration.put("commandFromItemScript", toHandlerScript); - configuration.remove("toHandlerScript"); - linkStorage.put(linkUid, link); - logger.info("{}: rewrote script profile link to new format", linkUid); - } else { - logger.info("{}: link already has correct configuration", linkUid); - } - } - }); - - linkStorage.flush(); - upgradeRecords.put(LINK_UPGRADE_SCRIPT_PROFILE, new UpgradeRecord(ZonedDateTime.now())); - upgradeRecords.flush(); - } - - private static class UpgradeRecord { - public final String executionDate; - - public UpgradeRecord(ZonedDateTime executionDate) { - this.executionDate = executionDate.toString(); - } + return true; } } diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/JSProfileUpgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/JSProfileUpgrader.java new file mode 100644 index 00000000000..0c428ca8ece --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/JSProfileUpgrader.java @@ -0,0 +1,92 @@ +/* + * 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.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.storage.json.internal.JsonStorage; +import org.openhab.core.thing.internal.link.ItemChannelLinkConfigDescriptionProvider; +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.tools.Upgrader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link JSProfileUpgrader} upgrades JS Profile configurations + * + * @author Jan N. Klug - Initial contribution + * @author Jimmy Tanagra - Refactored into a separate class + */ +@NonNullByDefault +public class JSProfileUpgrader implements Upgrader { + private final Logger logger = LoggerFactory.getLogger(JSProfileUpgrader.class); + + @Override + public String getName() { + return "linkUpgradeJSProfile"; // keep the old name for backwards compatibility + } + + @Override + public String getDescription() { + return "Upgrade JS profile configuration to new format"; + } + + @Override + public boolean execute(@Nullable Path userdataPath, @Nullable Path confPath) { + if (userdataPath == null) { + logger.error("{} skipped: no userdata directory found.", getName()); + return false; + } + + userdataPath = userdataPath.resolve("jsondb"); + + Path linkJsonDatabasePath = userdataPath.resolve("org.openhab.core.thing.link.ItemChannelLink.json"); + logger.info("Upgrading JS profile configuration in database '{}'", linkJsonDatabasePath); + + if (!Files.isWritable(linkJsonDatabasePath)) { + logger.error("Cannot access link database '{}', check path and access rights.", linkJsonDatabasePath); + return false; + } + JsonStorage linkStorage = new JsonStorage<>(linkJsonDatabasePath.toFile(), null, 5, 0, 0, + List.of()); + + List.copyOf(linkStorage.getKeys()).forEach(linkUid -> { + ItemChannelLink link = Objects.requireNonNull(linkStorage.get(linkUid)); + Configuration configuration = link.getConfiguration(); + String profileName = (String) configuration.get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE); + if ("transform:JS".equals(profileName)) { + String function = (String) configuration.get("function"); + if (function != null) { + configuration.put("toItemScript", function); + configuration.put("toHandlerScript", "|input"); + configuration.remove("function"); + configuration.remove("sourceFormat"); + + linkStorage.put(linkUid, link); + logger.info("{}: rewrote JS profile link to new format", linkUid); + } else { + logger.info("{}: link already has correct configuration", linkUid); + } + } + }); + + linkStorage.flush(); + return true; + } +} diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ScriptProfileUpgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ScriptProfileUpgrader.java new file mode 100644 index 00000000000..daf15cce7e8 --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ScriptProfileUpgrader.java @@ -0,0 +1,92 @@ +/* + * 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.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.storage.json.internal.JsonStorage; +import org.openhab.core.thing.internal.link.ItemChannelLinkConfigDescriptionProvider; +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.tools.Upgrader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ScriptProfileUpgrader} upgrades the ItemChannelLink database + * for the separation of {@code toHandlerScript} into + * {@code commandFromItemScript} and {@code stateFromItemScript}. + * See openhab/openhab-core#4058. + * + * @author Florian Hotze - Initial contribution + * @author Jimmy Tanagra - Refactored into a separate class + */ +@NonNullByDefault +public class ScriptProfileUpgrader implements Upgrader { + private final Logger logger = LoggerFactory.getLogger(ScriptProfileUpgrader.class); + + @Override + public String getName() { + return "linkUpgradeScriptProfile"; // keep the old name for backwards compatibility + } + + @Override + public String getDescription() { + return "Upgrade script profile configuration toHandlerScript to commandFromItemScript"; + } + + @Override + public boolean execute(@Nullable Path userdataPath, @Nullable Path confPath) { + if (userdataPath == null) { + logger.error("{} skipped: no userdata directory found.", getName()); + return false; + } + + Path linkJsonDatabasePath = userdataPath + .resolve(Path.of("jsondb", "org.openhab.core.thing.link.ItemChannelLink.json")); + logger.info("Upgrading script profile configuration in database '{}'", linkJsonDatabasePath); + + if (!Files.isWritable(linkJsonDatabasePath)) { + logger.error("Cannot access link database '{}', check path and access rights.", linkJsonDatabasePath); + return false; + } + JsonStorage linkStorage = new JsonStorage<>(linkJsonDatabasePath.toFile(), null, 5, 0, 0, + List.of()); + + List.copyOf(linkStorage.getKeys()).forEach(linkUid -> { + ItemChannelLink link = Objects.requireNonNull(linkStorage.get(linkUid)); + Configuration configuration = link.getConfiguration(); + String profileName = (String) configuration.get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE); + if (profileName != null && profileName.startsWith("transform:")) { + String toHandlerScript = (String) configuration.get("toHandlerScript"); + if (toHandlerScript != null) { + configuration.put("commandFromItemScript", toHandlerScript); + configuration.remove("toHandlerScript"); + + linkStorage.put(linkUid, link); + logger.info("{}: rewrote script profile link to new format", linkUid); + } else { + logger.info("{}: link already has correct configuration", linkUid); + } + } + }); + + linkStorage.flush(); + return true; + } +} 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 00000000000..cfdc9179986 --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/YamlConfigurationV1TagsUpgrader.java @@ -0,0 +1,203 @@ +/* + * 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.eclipse.jdt.annotation.Nullable; +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(@Nullable Path userdataPath, @Nullable Path confPath) { + if (confPath == null) { + logger.error("{} skipped: no conf directory found.", getName()); + return false; + } + + String confEnv = System.getenv("OPENHAB_CONF"); + // If confPath is set to OPENHAB_CONF, look inside /tags/ subdirectory + // otherwise use the given confPath as is + if (confEnv != null && !confEnv.isBlank() && Path.of(confEnv).toAbsolutePath().equals(confPath)) { + confPath = confPath.resolve("tags"); + } + + logger.info("Upgrading YAML tags configurations in '{}'", confPath); + + Path configPath = confPath; // make configPath "effectively final" inside the lambda below + 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")) { + 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.getMessage()); + 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) { + logger.debug("{} skipped: it doesn't contain a version key", filePath); + return; + } + + JsonNode tagsNode = fileContent.get("tags"); + if (tagsNode == null || !tagsNode.isArray()) { + logger.debug("{} skipped: it doesn't contain a 'tags' array.", filePath); + return; + } + + logger.debug("{} found containing v1 yaml file with a 'tags' array", 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()); + } + } +}