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());
+ }
+ }
+}