From fbf683a5d7ac704128cb4811e9e1bdf600794cb1 Mon Sep 17 00:00:00 2001 From: Jimmy Tanagra Date: Sun, 27 Apr 2025 22:43:14 +1000 Subject: [PATCH 1/7] Upgrade Tool: Split upgrade commands into individual classes Signed-off-by: Jimmy Tanagra --- .../org/openhab/core/tools/UpgradeTool.java | 142 +++++++++++++--- .../java/org/openhab/core/tools/Upgrader.java | 40 +++++ ...r.java => ItemUnitToMetadataUpgrader.java} | 153 +++--------------- .../tools/internal/JSProfileUpgrader.java | 84 ++++++++++ .../tools/internal/ScriptProfileUpgrader.java | 85 ++++++++++ 5 files changed, 346 insertions(+), 158 deletions(-) create mode 100644 tools/upgradetool/src/main/java/org/openhab/core/tools/Upgrader.java rename tools/upgradetool/src/main/java/org/openhab/core/tools/internal/{Upgrader.java => ItemUnitToMetadataUpgrader.java} (55%) create mode 100644 tools/upgradetool/src/main/java/org/openhab/core/tools/internal/JSProfileUpgrader.java create mode 100644 tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ScriptProfileUpgrader.java 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..a72c8b199c8 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,47 @@ 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 List UPGRADERS = List.of( // + new ItemUnitToMetadataUpgrader(), // + new JSProfileUpgrader(), // + new ScriptProfileUpgrader() // + ); + + 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") + .numberOfArgs(1).build()); + options.addOption( + Option.builder().longOpt(OPT_CONF_DIR).desc("CONF directory to process").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 +80,110 @@ 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) + String userdataDir = commandLine.hasOption(OPT_USERDATA_DIR) ? commandLine.getOptionValue(OPT_USERDATA_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."); + if (userdataDir == null || userdataDir.isBlank()) { + println("Please either set the environment variable ${OPENHAB_USERDATA} or provide a directory through the --userdata option."); System.exit(0); - } else { - boolean force = commandLine.hasOption(OPT_FORCE); + return; + } + + String confDir = commandLine.hasOption(OPT_CONF_DIR) ? commandLine.getOptionValue(OPT_CONF_DIR) + : System.getenv("OPENHAB_CONF"); + if (confDir == null || confDir.isBlank()) { + println("Please either set the environment variable ${OPENHAB_CONF} or provide a directory through the --conf option."); + System.exit(0); + return; + } + + 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 upgradeJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.tools.UpgradeTool"); + upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); - 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(userdataDir, confDir)) { + 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); } + + private static @Nullable String lastExecuted(String upgrader) { + JsonStorage records = upgradeRecords; + if (records != null) { + UpgradeRecord upgradeRecord = records.get(upgrader); + if (upgradeRecord != null) { + return upgradeRecord.executionDate; + } + } else { + logger.error("Upgrade records storage is not initialized."); + } + return null; + } + + private static void updateUpgradeRecord(String upgrader) { + JsonStorage records = upgradeRecords; + if (records != null) { + records.put(upgrader, new UpgradeRecord(ZonedDateTime.now())); + records.flush(); + } else { + logger.error("Upgrade records storage is not initialized."); + } + } + + // 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..3683b7b37e2 --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/Upgrader.java @@ -0,0 +1,40 @@ +/* + * 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 org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * 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 userdataDir the OPENHAB_USERDATA directory for the upgrade, + * or a custom path given by the user as --userdata argument + * @param confDir 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(String userdataDir, String confDir); +} 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..853daa65ec8 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,57 @@ 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.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"); - - upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); + @Override + public String getName() { + return "itemCopyUnitToMetadata"; // keep the old name for backwards compatibility } - 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); - return false; - } - return true; + @Override + public String getDescription() { + return "Copy item unit from state description to metadata"; } - public void itemCopyUnitToMetadata() { + @Override + public boolean execute(String userdataDir, String confDir) { 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 = Path.of(userdataDir, "jsondb", "org.openhab.core.items.Item.json"); + Path metadataJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.items.Metadata.json"); + Path linkJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json"); + Path thingJsonDatabasePath = Path.of(userdataDir, "jsondb", "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 +82,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 +101,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 +172,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..fa26315fae7 --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/JSProfileUpgrader.java @@ -0,0 +1,84 @@ +/* + * 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.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(String userdataDir, String confDir) { + Path linkJsonDatabasePath = Path.of(userdataDir, "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 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..699739ce214 --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ScriptProfileUpgrader.java @@ -0,0 +1,85 @@ +/* + * 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.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(String userdataDir, String confDir) { + Path linkJsonDatabasePath = Path.of(userdataDir, "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; + } +} From 60844f89065218898c17f93c64254575e804283e Mon Sep 17 00:00:00 2001 From: Jimmy Tanagra Date: Mon, 28 Apr 2025 16:06:44 +1000 Subject: [PATCH 2/7] Add Yaml configuration tags list to map upgrader Signed-off-by: Jimmy Tanagra --- tools/upgradetool/pom.xml | 16 ++ .../org/openhab/core/tools/UpgradeTool.java | 3 +- .../YamlConfigurationV1TagsUpgrader.java | 195 ++++++++++++++++++ 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 tools/upgradetool/src/main/java/org/openhab/core/tools/internal/YamlConfigurationV1TagsUpgrader.java 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 a72c8b199c8..2d419d84ee1 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 00000000000..76bbfe4334b --- /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()); + } + } +} From d8b10b26f170a6a603fea335adf74546b8e437bb Mon Sep 17 00:00:00 2001 From: Jimmy Tanagra Date: Wed, 30 Apr 2025 19:45:41 +1000 Subject: [PATCH 3/7] Log the exception message instead of class name Signed-off-by: Jimmy Tanagra --- .../core/tools/internal/YamlConfigurationV1TagsUpgrader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 76bbfe4334b..1cbf97f0d2b 100644 --- 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 @@ -125,7 +125,7 @@ public FileVisitResult visitFile(@NonNullByDefault({}) Path file, @Override public FileVisitResult visitFileFailed(@NonNullByDefault({}) Path file, @NonNullByDefault({}) IOException exc) throws IOException { - logger.warn("Failed to process {}: {}", file.toAbsolutePath(), exc.getClass().getSimpleName()); + logger.warn("Failed to process {}: {}", file.toAbsolutePath(), exc.getMessage()); return FileVisitResult.CONTINUE; } }); From dd0fefbff49a8b41be3870b3b4b9536d9c93bcba Mon Sep 17 00:00:00 2001 From: Jimmy Tanagra Date: Sun, 11 May 2025 20:42:06 +1000 Subject: [PATCH 4/7] allow unspecified userdata dir when --command is given Signed-off-by: Jimmy Tanagra --- .../org/openhab/core/tools/UpgradeTool.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) 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 2d419d84ee1..f8eee3e924e 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 @@ -95,9 +95,17 @@ public static void main(String[] args) { System.setProperty(org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, loglevel); + 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); + } + String userdataDir = commandLine.hasOption(OPT_USERDATA_DIR) ? commandLine.getOptionValue(OPT_USERDATA_DIR) : System.getenv("OPENHAB_USERDATA"); - if (userdataDir == null || userdataDir.isBlank()) { + if (command == null && (userdataDir == null || userdataDir.isBlank())) { println("Please either set the environment variable ${OPENHAB_USERDATA} or provide a directory through the --userdata option."); System.exit(0); return; @@ -111,14 +119,6 @@ public static void main(String[] args) { return; } - 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 upgradeJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.tools.UpgradeTool"); upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); @@ -135,7 +135,13 @@ public static void main(String[] args) { try { logger.info("Executing {}: {}", upgraderName, upgrader.getDescription()); if (upgrader.execute(userdataDir, confDir)) { - updateUpgradeRecord(upgraderName); + if (userdataDir != null) { + updateUpgradeRecord(upgraderName); + } else { + logger.warn( + "The Upgrade record for '{}' is not updated because user data directory wasn't specified", + upgraderName); + } } } catch (Exception e) { logger.error("Error executing upgrader {}: {}", upgraderName, e.getMessage()); From de9a253b1f3f1ea1dc2aad471ccd167710c1b979 Mon Sep 17 00:00:00 2001 From: Jimmy Tanagra Date: Sun, 11 May 2025 21:12:56 +1000 Subject: [PATCH 5/7] print the userdata and conf directories Signed-off-by: Jimmy Tanagra --- .../src/main/java/org/openhab/core/tools/UpgradeTool.java | 4 ++++ 1 file changed, 4 insertions(+) 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 f8eee3e924e..131274cda8c 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 @@ -109,6 +109,8 @@ public static void main(String[] args) { println("Please either set the environment variable ${OPENHAB_USERDATA} or provide a directory through the --userdata option."); System.exit(0); return; + } else if (userdataDir != null) { + logger.info("Using userdataDir: {}", userdataDir); } String confDir = commandLine.hasOption(OPT_CONF_DIR) ? commandLine.getOptionValue(OPT_CONF_DIR) @@ -117,6 +119,8 @@ public static void main(String[] args) { println("Please either set the environment variable ${OPENHAB_CONF} or provide a directory through the --conf option."); System.exit(0); return; + } else { + logger.info("Using confDir: {}", confDir); } Path upgradeJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.tools.UpgradeTool"); From 4577733f3324d53daa21028e5ec584d29098e80c Mon Sep 17 00:00:00 2001 From: Jimmy Tanagra Date: Sun, 11 May 2025 22:02:54 +1000 Subject: [PATCH 6/7] inform about quoting paths, fix missing userdata dir Signed-off-by: Jimmy Tanagra --- .../org/openhab/core/tools/UpgradeTool.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) 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 131274cda8c..5c21531cd8e 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 @@ -60,10 +60,12 @@ public class UpgradeTool { private static Options getOptions() { Options options = new Options(); - options.addOption(Option.builder().longOpt(OPT_USERDATA_DIR).desc("USERDATA directory to process") + 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_CONF_DIR).desc("CONF directory to process").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()); @@ -111,6 +113,8 @@ public static void main(String[] args) { return; } else if (userdataDir != null) { logger.info("Using userdataDir: {}", userdataDir); + Path upgradeJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.tools.UpgradeTool"); + upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); } String confDir = commandLine.hasOption(OPT_CONF_DIR) ? commandLine.getOptionValue(OPT_CONF_DIR) @@ -123,9 +127,6 @@ public static void main(String[] args) { logger.info("Using confDir: {}", confDir); } - Path upgradeJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.tools.UpgradeTool"); - upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); - UPGRADERS.forEach(upgrader -> { String upgraderName = upgrader.getName(); if (command != null && !upgraderName.equals(command)) { @@ -139,13 +140,7 @@ public static void main(String[] args) { try { logger.info("Executing {}: {}", upgraderName, upgrader.getDescription()); if (upgrader.execute(userdataDir, confDir)) { - if (userdataDir != null) { - updateUpgradeRecord(upgraderName); - } else { - logger.warn( - "The Upgrade record for '{}' is not updated because user data directory wasn't specified", - upgraderName); - } + updateUpgradeRecord(upgraderName); } } catch (Exception e) { logger.error("Error executing upgrader {}: {}", upgraderName, e.getMessage()); From d05d5ef6ca88c93f35fce5c9a60ee86c31325863 Mon Sep 17 00:00:00 2001 From: Jimmy Tanagra Date: Mon, 12 May 2025 11:11:37 +1000 Subject: [PATCH 7/7] refactor directory handling Signed-off-by: Jimmy Tanagra --- .../org/openhab/core/tools/UpgradeTool.java | 68 ++++++++++++------- .../java/org/openhab/core/tools/Upgrader.java | 11 +-- .../internal/ItemUnitToMetadataUpgrader.java | 17 +++-- .../tools/internal/JSProfileUpgrader.java | 12 +++- .../tools/internal/ScriptProfileUpgrader.java | 13 +++- .../YamlConfigurationV1TagsUpgrader.java | 38 +++++++---- 6 files changed, 107 insertions(+), 52 deletions(-) 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 5c21531cd8e..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 @@ -47,6 +47,9 @@ public class UpgradeTool { 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(), // @@ -105,26 +108,15 @@ public static void main(String[] args) { System.exit(0); } - String userdataDir = commandLine.hasOption(OPT_USERDATA_DIR) ? commandLine.getOptionValue(OPT_USERDATA_DIR) - : System.getenv("OPENHAB_USERDATA"); - if (command == null && (userdataDir == null || userdataDir.isBlank())) { - println("Please either set the environment variable ${OPENHAB_USERDATA} or provide a directory through the --userdata option."); - System.exit(0); - return; - } else if (userdataDir != null) { - logger.info("Using userdataDir: {}", userdataDir); - Path upgradeJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.tools.UpgradeTool"); - upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); - } + Path userdataPath = getPath("userdata", commandLine, OPT_USERDATA_DIR, ENV_USERDATA); + Path confPath = getPath("conf", commandLine, OPT_CONF_DIR, ENV_CONF); - String confDir = commandLine.hasOption(OPT_CONF_DIR) ? commandLine.getOptionValue(OPT_CONF_DIR) - : System.getenv("OPENHAB_CONF"); - if (confDir == null || confDir.isBlank()) { - println("Please either set the environment variable ${OPENHAB_CONF} or provide a directory through the --conf option."); - System.exit(0); - return; + 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 { - logger.info("Using confDir: {}", confDir); + logger.warn("Upgrade records storage is not initialized."); } UPGRADERS.forEach(upgrader -> { @@ -139,7 +131,7 @@ public static void main(String[] args) { } try { logger.info("Executing {}: {}", upgraderName, upgrader.getDescription()); - if (upgrader.execute(userdataDir, confDir)) { + if (upgrader.execute(userdataPath, confPath)) { updateUpgradeRecord(upgraderName); } } catch (Exception e) { @@ -154,6 +146,40 @@ public static void main(String[] args) { 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) { @@ -161,8 +187,6 @@ public static void main(String[] args) { if (upgradeRecord != null) { return upgradeRecord.executionDate; } - } else { - logger.error("Upgrade records storage is not initialized."); } return null; } @@ -172,8 +196,6 @@ private static void updateUpgradeRecord(String upgrader) { if (records != null) { records.put(upgrader, new UpgradeRecord(ZonedDateTime.now())); records.flush(); - } else { - logger.error("Upgrade records storage is not initialized."); } } 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 index 3683b7b37e2..17c9e8c7c0e 100644 --- a/tools/upgradetool/src/main/java/org/openhab/core/tools/Upgrader.java +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/Upgrader.java @@ -12,11 +12,14 @@ */ 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 @@ -30,11 +33,11 @@ public interface Upgrader { /** * Executes the upgrade process. * - * @param userdataDir the OPENHAB_USERDATA directory for the upgrade, + * @param userdataPath the OPENHAB_USERDATA directory for the upgrade, * or a custom path given by the user as --userdata argument - * @param confDir the OPENHAB_CONF directory for the upgrade, + * @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(String userdataDir, String confDir); + boolean execute(@Nullable Path userdataPath, @Nullable Path confPath); } diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ItemUnitToMetadataUpgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ItemUnitToMetadataUpgrader.java index 853daa65ec8..09f4dd1646e 100644 --- a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ItemUnitToMetadataUpgrader.java +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/ItemUnitToMetadataUpgrader.java @@ -24,6 +24,7 @@ import javax.measure.Unit; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.items.ManagedItemProvider; import org.openhab.core.items.Metadata; import org.openhab.core.items.MetadataKey; @@ -57,13 +58,19 @@ public String getDescription() { } @Override - public boolean execute(String userdataDir, String confDir) { + 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"); boolean noLink; - Path itemJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.items.Item.json"); - Path metadataJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.items.Metadata.json"); - Path linkJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json"); - Path thingJsonDatabasePath = Path.of(userdataDir, "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)) { 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 index fa26315fae7..0c428ca8ece 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -47,8 +48,15 @@ public String getDescription() { } @Override - public boolean execute(String userdataDir, String confDir) { - Path linkJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json"); + 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)) { 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 index 699739ce214..daf15cce7e8 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -31,7 +32,7 @@ * 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 */ @@ -50,8 +51,14 @@ public String getDescription() { } @Override - public boolean execute(String userdataDir, String confDir) { - Path linkJsonDatabasePath = Path.of(userdataDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json"); + 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)) { 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 index 1cbf97f0d2b..cfdc9179986 100644 --- 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 @@ -20,6 +20,7 @@ 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; @@ -37,16 +38,16 @@ /** * 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 @@ -54,9 +55,9 @@ * - 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 @@ -96,16 +97,22 @@ public String getDescription() { } @Override - public boolean execute(String userdataDir, String confDir) { + 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 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(); + // 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"); } - Path configPath = Path.of(confDir).toAbsolutePath(); - logger.info("Upgrading YAML tags configurations in '{}'", configPath); + 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 @@ -115,7 +122,6 @@ public FileVisitResult visitFile(@NonNullByDefault({}) Path file, Path relativePath = configPath.relativize(file); String modelName = relativePath.toString(); if (!relativePath.startsWith("automation") && modelName.endsWith(".yaml")) { - logger.info("Checking {}", file); convertTagsListToMap(file); } } @@ -142,15 +148,17 @@ private void convertTagsListToMap(Path filePath) { 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.info("Found v1 yaml file with tags list {}", filePath); + logger.debug("{} found containing v1 yaml file with a 'tags' array", filePath); fileContent.properties().forEach(entry -> { String key = entry.getKey(); JsonNode node = entry.getValue(); @@ -187,7 +195,7 @@ private void saveFile(Path filePath, String content) { try { Files.move(filePath, backupPath); Files.writeString(filePath, content); - logger.info("Converted {} to map format, and the original file saved as {}", filePath, backupPath); + 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()); }