diff --git a/tools/upgradetool/pom.xml b/tools/upgradetool/pom.xml
index 4d8ace94c..2ad1619a2 100644
--- a/tools/upgradetool/pom.xml
+++ b/tools/upgradetool/pom.xml
@@ -43,6 +43,11 @@
org.openhab.core.persistence
${project.version}
+
+ org.openhab.core.bundles
+ org.openhab.core.semantics
+ ${project.version}
+
org.openhab.core.bundles
org.openhab.core.storage.json
@@ -92,6 +97,11 @@
${jackson.version}
compile
+
+ org.apache.maven
+ maven-artifact
+ ${maven.version}
+
org.slf4j
slf4j-api
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 380147e28..b0ec60570 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,11 +12,14 @@
*/
package org.openhab.core.tools;
+import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.List;
+import java.util.Properties;
import java.util.Set;
import org.apache.commons.cli.CommandLine;
@@ -25,6 +28,7 @@ import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.help.HelpFormatter;
+import org.apache.maven.artifact.versioning.ComparableVersion;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.storage.json.internal.JsonStorage;
@@ -38,6 +42,8 @@ import org.slf4j.LoggerFactory;
* @author Jan N. Klug - Initial contribution
* @author Jimmy Tanagra - Refactor upgraders into individual classes
* @author Mark Herwege - Added persistence strategy upgrader
+ * @author Mark Herwege - Added semantic tag upgrader
+ * @author Mark Herwege - Track OH versions and let upgraders set a target version
*/
@NonNullByDefault
public class UpgradeTool {
@@ -46,24 +52,31 @@ public class UpgradeTool {
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_OH_VERSION = "version";
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 String UPGRADE_TOOL_VERSION_KEY = "UpgradeTool";
+
private static final List UPGRADERS = List.of( //
- new ItemUnitToMetadataUpgrader(), //
- new JSProfileUpgrader(), //
- new ScriptProfileUpgrader(), //
- new YamlConfigurationV1TagsUpgrader(), // Added in 5.0
- new HomeAssistantAddonUpgrader(), // Added in 5.1
- new HomieAddonUpgrader(), // Added in 5.1
- new PersistenceUpgrader() // Added in 5.1
+ new ItemUnitToMetadataUpgrader(), // Since 4.0.0
+ new JSProfileUpgrader(), // Since 4.0.0
+ new ScriptProfileUpgrader(), // Since 4.2.0
+ new YamlConfigurationV1TagsUpgrader(), // Since 5.0
+ new HomeAssistantAddonUpgrader(), // Since 5.1
+ new HomieAddonUpgrader(), // Since 5.1
+ new PersistenceUpgrader(), // Since 5.1
+ new SemanticTagUpgrader() // Since 5.2
);
private static final Logger LOGGER = LoggerFactory.getLogger(UpgradeTool.class);
private static @Nullable JsonStorage upgradeRecords = null;
+ private static @Nullable JsonStorage ohVersionRecords = null;
+
+ private static VersionRecord ohTargetVersion = new VersionRecord();
private static Options getOptions() {
Options options = new Options();
@@ -74,6 +87,9 @@ public class UpgradeTool {
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).get());
+ options.addOption(Option.builder().longOpt(OPT_OH_VERSION).desc(
+ "openHAB target version. Upgraders will be executed again if they have been executed in an upgrade to an earlier openHAB version and there are new changes.")
+ .numberOfArgs(1).get());
options.addOption(Option.builder().longOpt(OPT_COMMAND).numberOfArgs(1)
.desc("command to execute (executes all if omitted)").get());
options.addOption(Option.builder().longOpt(OPT_LIST_COMMANDS).desc("list available commands").get());
@@ -83,7 +99,7 @@ public class UpgradeTool {
return options;
}
- public static void main(String[] args) {
+ public static void main(String[] args) throws IOException {
Options options = getOptions();
try {
CommandLine commandLine = new DefaultParser().parse(options, args);
@@ -117,22 +133,39 @@ public class UpgradeTool {
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());
+ upgradeRecords = getUpgradeRecordsStorage(userdataPath);
+ Path ohVersionJsonDatabasePath = userdataPath
+ .resolve(Path.of("jsondb", "org.openhab.core.tools.ohVersion.json"));
+ ohVersionRecords = new JsonStorage<>(ohVersionJsonDatabasePath.toFile(), null, 5, 0, 0, List.of());
+ ohTargetVersion = commandLine.hasOption(OPT_OH_VERSION)
+ ? new VersionRecord(commandLine.getOptionValue(OPT_OH_VERSION))
+ : getTargetVersion(userdataPath);
} else {
- LOGGER.warn("Upgrade records storage is not initialized.");
+ LOGGER.warn("USERDATA not initialized. Unable to retrieve and save upgrade records.");
}
-
UPGRADERS.forEach(upgrader -> {
String upgraderName = upgrader.getName();
if (command != null && !upgraderName.equals(command)) {
return;
}
- if (!force && lastExecuted(upgraderName) instanceof String executionDate) {
- LOGGER.info("Already executed '{}' on {}. Use '--force' to execute it again.", upgraderName,
- executionDate);
- return;
+ if (!force) {
+ if (upgrader.getTargetVersion() instanceof String targetVersion) {
+ if (lastExecutedVersion(upgraderName) instanceof String executionVersion) {
+ if (compareVersions(executionVersion, targetVersion) >= 0) {
+ LOGGER.info("Already executed '{}' to version {}. Use '--force' to execute it again.",
+ upgraderName, executionVersion);
+ return;
+ }
+ } else if (lastExecuted(upgraderName) instanceof String executionDate) {
+ LOGGER.info("Already executed '{}' on {}. Use '--force' to execute it again.", upgraderName,
+ executionDate);
+ return;
+ }
+ } else if (lastExecuted(upgraderName) instanceof String executionDate) {
+ LOGGER.info("Already executed '{}' on {}. Use '--force' to execute it again.", upgraderName,
+ executionDate);
+ return;
+ }
}
try {
LOGGER.info("Executing {}: {}", upgraderName, upgrader.getDescription());
@@ -143,6 +176,12 @@ public class UpgradeTool {
LOGGER.error("Error executing upgrader {}: {}", upgraderName, e.getMessage());
}
});
+
+ // Only save version record if all upgraders have been executed
+ if (command == null) {
+ updateVersionRecord();
+ }
+
} catch (ParseException e) {
HelpFormatter formatter = HelpFormatter.builder().get();
try {
@@ -155,6 +194,32 @@ public class UpgradeTool {
System.exit(0);
}
+ private static JsonStorage getUpgradeRecordsStorage(Path userdataPath) {
+ // The old storage name did not have a json extension. If the new storage name does not exist yet, create it and
+ // move the data from old to new.
+ Path upgradeJsonDatabasePath = userdataPath
+ .resolve(Path.of("jsondb", "org.openhab.core.tools.UpgradeTool.json"));
+ JsonStorage upgradeRecordStorage = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5,
+ 0, 0, List.of());
+ if (!Files.exists(upgradeJsonDatabasePath)) {
+ Path oldUpgradeJsonDatabasePath = userdataPath
+ .resolve(Path.of("jsondb", "org.openhab.core.tools.UpgradeTool"));
+ if (Files.isReadable(oldUpgradeJsonDatabasePath)) {
+ JsonStorage oldUpgradeRecordStorage = new JsonStorage<>(
+ oldUpgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of());
+ oldUpgradeRecordStorage.stream()
+ .forEach(entry -> upgradeRecordStorage.put(entry.getKey(), entry.getValue()));
+ upgradeRecordStorage.flush();
+ try {
+ Files.delete(oldUpgradeJsonDatabasePath);
+ } catch (IOException e) {
+ LOGGER.debug("Failed to delete upgrade tool storage with old name.");
+ }
+ }
+ }
+ return upgradeRecordStorage;
+ }
+
/**
* 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').
@@ -189,12 +254,57 @@ public class UpgradeTool {
}
}
+ private static VersionRecord getTargetVersion(Path userdataPath) {
+ Path versionFilePath = userdataPath.resolve(Path.of("etc", "version.properties"));
+ Properties prop = new Properties();
+ try (FileInputStream fis = new FileInputStream(versionFilePath.toFile())) {
+ prop.load(fis);
+ String build = prop.getProperty("build-no");
+ String distro = prop.getProperty("openhab-distro");
+ String core = prop.getProperty("openhab-core");
+ String addons = prop.getProperty("openhab-addons");
+ String karaf = prop.getProperty("karaf");
+ return new VersionRecord(build, distro, core, addons, karaf);
+ } catch (IOException e) {
+ LOGGER.warn(
+ "Cannot retrieve OH core version from '{}' file. Some tasks may fail. You can provide the target version through the --version option.",
+ versionFilePath);
+ }
+ return new VersionRecord();
+ }
+
+ private static int compareVersions(String versionA, String versionB) {
+ ComparableVersion version1 = new ComparableVersion(versionA);
+ ComparableVersion version2 = new ComparableVersion(versionB);
+ return version1.compareTo(version2);
+ }
+
private static @Nullable String lastExecuted(String upgrader) {
- JsonStorage records = upgradeRecords;
- if (records != null) {
- UpgradeRecord upgradeRecord = records.get(upgrader);
+ JsonStorage upgrades = upgradeRecords;
+ if (upgrades != null) {
+ UpgradeRecord upgradeRecord = upgrades.get(upgrader);
if (upgradeRecord != null) {
- return upgradeRecord.executionDate;
+ return upgradeRecord.executionDate();
+ }
+ }
+ return null;
+ }
+
+ private static @Nullable String lastExecutedVersion(String upgrader) {
+ JsonStorage upgrades = upgradeRecords;
+ if (upgrades != null && upgrades.get(upgrader) instanceof UpgradeRecord upgradeRecord) {
+ String executionVersion = upgradeRecord.executionVersion();
+ if (executionVersion != null && !executionVersion.isEmpty()) {
+ return executionVersion;
+ }
+ // Legacy records may lack an executionVersion; in that case, fall back to the
+ // global core version recorded by the upgrade tool.
+ JsonStorage versions = ohVersionRecords;
+ if (versions != null) {
+ VersionRecord versionRecord = versions.get(UPGRADE_TOOL_VERSION_KEY);
+ if (versionRecord != null) {
+ return versionRecord.core();
+ }
}
}
return null;
@@ -203,7 +313,15 @@ public class UpgradeTool {
private static void updateUpgradeRecord(String upgrader) {
JsonStorage records = upgradeRecords;
if (records != null) {
- records.put(upgrader, new UpgradeRecord(ZonedDateTime.now()));
+ records.put(upgrader, new UpgradeRecord(ZonedDateTime.now(), ohTargetVersion.core()));
+ records.flush();
+ }
+ }
+
+ private static void updateVersionRecord() {
+ JsonStorage records = ohVersionRecords;
+ if (records != null && ohTargetVersion.isDefined()) {
+ records.put(UPGRADE_TOOL_VERSION_KEY, ohTargetVersion);
records.flush();
}
}
@@ -216,11 +334,35 @@ public class UpgradeTool {
}
}
- private static class UpgradeRecord {
- public final String executionDate;
-
- public UpgradeRecord(ZonedDateTime executionDate) {
- this.executionDate = executionDate.toString();
+ private record UpgradeRecord(String executionDate, @Nullable String executionVersion) {
+ public UpgradeRecord(ZonedDateTime executionDate, @Nullable String executionVersion) {
+ this(executionDate.toString(), executionVersion);
}
}
+
+ private record VersionRecord(String executionDate, @Nullable String build, @Nullable String distro,
+ @Nullable String core, @Nullable String addons, @Nullable String karaf) {
+
+ protected VersionRecord() {
+ this(null);
+ }
+
+ protected VersionRecord(@Nullable String core) {
+ this(null, null, core, null, null);
+ }
+
+ protected VersionRecord(@Nullable String build, @Nullable String distro, @Nullable String core,
+ @Nullable String addons, @Nullable String karaf) {
+ this(ZonedDateTime.now(), build, distro, core, addons, karaf);
+ }
+
+ protected VersionRecord(ZonedDateTime executionDate, @Nullable String build, @Nullable String distro,
+ @Nullable String core, @Nullable String addons, @Nullable String karaf) {
+ this(executionDate.toString(), build, distro, core, addons, karaf);
+ }
+
+ protected boolean isDefined() {
+ return core != null;
+ }
+ };
}
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 956b070d8..69f16708c 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
@@ -30,6 +30,10 @@ public interface Upgrader {
String getDescription();
+ default @Nullable String getTargetVersion() {
+ return null;
+ }
+
/**
* Executes the upgrade process.
*
diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/HomeAssistantAddonUpgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/HomeAssistantAddonUpgrader.java
index b58b2e086..c4232a355 100644
--- a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/HomeAssistantAddonUpgrader.java
+++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/HomeAssistantAddonUpgrader.java
@@ -20,6 +20,8 @@ import org.openhab.core.tools.ExtractedAddonUpgrader;
* installed, and if Home Assistant things exist, and if so installs the
* Home Assistant addon.
*
+ * @since 5.1.0
+ *
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault
diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/HomieAddonUpgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/HomieAddonUpgrader.java
index a9d8ad681..e60fb26d8 100644
--- a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/HomieAddonUpgrader.java
+++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/HomieAddonUpgrader.java
@@ -20,6 +20,8 @@ import org.openhab.core.tools.ExtractedAddonUpgrader;
* installed, and if Home Assistant things exist, and if so installs the
* Home Assistant addon.
*
+ * @since 5.1.0
+ *
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault
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 4355927b4..84579dbac 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
@@ -64,13 +64,13 @@ public class ItemUnitToMetadataUpgrader implements Upgrader {
return false;
}
- userdataPath = userdataPath.resolve("jsondb");
+ Path dataPath = userdataPath.resolve("jsondb");
boolean noLink;
- 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");
+ Path itemJsonDatabasePath = dataPath.resolve("org.openhab.core.items.Item.json");
+ Path metadataJsonDatabasePath = dataPath.resolve("org.openhab.core.items.Metadata.json");
+ Path linkJsonDatabasePath = dataPath.resolve("org.openhab.core.thing.link.ItemChannelLink.json");
+ Path thingJsonDatabasePath = dataPath.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 5fc0c985c..d3f94c283 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
@@ -30,6 +30,8 @@ import org.slf4j.LoggerFactory;
/**
* The {@link JSProfileUpgrader} upgrades JS Profile configurations
*
+ * @since 4.0.0
+ *
* @author Jan N. Klug - Initial contribution
* @author Jimmy Tanagra - Refactored into a separate class
*/
@@ -54,9 +56,9 @@ public class JSProfileUpgrader implements Upgrader {
return false;
}
- userdataPath = userdataPath.resolve("jsondb");
+ Path dataPath = userdataPath.resolve("jsondb");
- Path linkJsonDatabasePath = userdataPath.resolve("org.openhab.core.thing.link.ItemChannelLink.json");
+ Path linkJsonDatabasePath = dataPath.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/PersistenceUpgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/PersistenceUpgrader.java
index 40aa31633..147746028 100644
--- a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/PersistenceUpgrader.java
+++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/PersistenceUpgrader.java
@@ -38,6 +38,8 @@ import org.slf4j.LoggerFactory;
* configuration that has no strategy defined.
* See openhab/openhab-core#4682.
*
+ * @since 5.1.0
+ *
* @author Mark Herwege - Initial Contribution
*/
@NonNullByDefault
@@ -123,28 +125,32 @@ public class PersistenceUpgrader implements Upgrader {
// Update existing managed configurations
persistenceStorageKeys.forEach(serviceId -> {
- PersistenceServiceConfigurationDTO serviceConfigDTO = Objects
- .requireNonNull(persistenceStorage.get(serviceId));
- Collection defaults = serviceConfigDTO.defaults;
- if (defaults != null) {
- Collection configs = serviceConfigDTO.configs;
- configs.forEach(config -> {
- Collection strategies = config.strategies;
- if (strategies.isEmpty()) {
- config.strategies = defaults;
- }
- });
- serviceConfigDTO.defaults = null;
-
- persistenceStorage.put(serviceId, serviceConfigDTO);
- logger.info("{}: updated strategy configurations and removed default strategies", serviceId);
- }
+ updateConfig(persistenceStorage, serviceId);
});
persistenceStorage.flush();
return true;
}
+ @SuppressWarnings("deprecation")
+ private void updateConfig(JsonStorage persistenceStorage, String serviceId) {
+ PersistenceServiceConfigurationDTO serviceConfigDTO = Objects.requireNonNull(persistenceStorage.get(serviceId));
+ Collection defaults = serviceConfigDTO.defaults;
+ if (defaults != null) {
+ Collection configs = serviceConfigDTO.configs;
+ configs.forEach(config -> {
+ Collection strategies = config.strategies;
+ if (strategies.isEmpty()) {
+ config.strategies = defaults;
+ }
+ });
+ serviceConfigDTO.defaults = null;
+
+ persistenceStorage.put(serviceId, serviceConfigDTO);
+ logger.info("{}: updated strategy configurations and removed default strategies", serviceId);
+ }
+ }
+
private List installedPersistenceAddons(Path userdataPath) throws IOException {
Path addonsConfigPath = userdataPath.resolve("config/org/openhab/addons.config");
if (Files.notExists(addonsConfigPath)) {
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 1c7aecd14..f1d21ad4f 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
@@ -33,6 +33,8 @@ import org.slf4j.LoggerFactory;
* {@code commandFromItemScript} and {@code stateFromItemScript}.
* See openhab/openhab-core#4058.
*
+ * @since 4.2.0
+ *
* @author Florian Hotze - Initial contribution
* @author Jimmy Tanagra - Refactored into a separate class
*/
diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/SemanticTagUpgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/SemanticTagUpgrader.java
new file mode 100644
index 000000000..a833828b7
--- /dev/null
+++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/SemanticTagUpgrader.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2010-2026 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.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.items.ManagedItemProvider;
+import org.openhab.core.semantics.SemanticTag;
+import org.openhab.core.semantics.SemanticTagImpl;
+import org.openhab.core.semantics.dto.SemanticTagDTO;
+import org.openhab.core.semantics.dto.SemanticTagDTOMapper;
+import org.openhab.core.semantics.model.DefaultSemanticTagProvider;
+import org.openhab.core.storage.json.internal.JsonStorage;
+import org.openhab.core.tools.Upgrader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SemanticTagUpgrader} removes custom semantic tags that are now part of standard semantic tags.
+ * The custom tags will also be checked for their position in the hierarchy. If hierarchy in the default tags has
+ * changed, and a custom tag depends on it, the custom tag parent will be adjusted.
+ * Semantic tags applied to items will be cleaned. If there is more than one semantic tag by class, only one will be
+ * kept and the others will be renamed to be non-semantic. Preference is given to keep the tags that moved class. If
+ * there is a Property tag, a Point tag will be added if missing.
+ * With these adjustments, the model can be further corrected from the UI without issues of double tags of the same
+ * class not being shown in the UI.
+ * This upgrader only considers managed tags and items.
+ *
+ * @since 5.2.0
+ *
+ * @author Mark Herwege - Initial contribution
+ */
+@NonNullByDefault
+public class SemanticTagUpgrader implements Upgrader {
+ private final Logger logger = LoggerFactory.getLogger(SemanticTagUpgrader.class);
+
+ private static final String TARGET_VERSION = "5.0.0";
+
+ // Tags known to have changed class in 5.0
+ private static final Set TAGS_CHANGED_CLASS = Set.of("LowBattery", "OpenLevel", "OpenState", "Tampered");
+
+ private static final String DEFAULT_POINT_TAG = "Status";
+
+ @Override
+ public String getName() {
+ return "deduplicateSemanticTags";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Correct custom semantic tags and item semantic tags for changes in provided default semantic tags";
+ }
+
+ @Override
+ public @Nullable String getTargetVersion() {
+ return TARGET_VERSION;
+ }
+
+ @Override
+ public boolean execute(@Nullable Path userdataPath, @Nullable Path confPath) {
+ if (userdataPath == null) {
+ logger.error("{} skipped: no userdata directory found.", getName());
+ return false;
+ }
+
+ Path semanticsJsonDatabasePath = userdataPath
+ .resolve(Path.of("jsondb", "org.openhab.core.semantics.SemanticTag.json"));
+ logger.info("Deduplicating custom semantic tags '{}'", semanticsJsonDatabasePath);
+
+ boolean dbUpdated = false;
+ boolean canUpdateDb = true;
+ if (!Files.exists(semanticsJsonDatabasePath)) {
+ canUpdateDb = false;
+ dbUpdated = true;
+ logger.info("Semantic tags database '{}' does not exist, no tags to update.", semanticsJsonDatabasePath);
+ } else if (!Files.isWritable(semanticsJsonDatabasePath)) {
+ canUpdateDb = false;
+ logger.warn(
+ "Cannot update semantic tags database '{}', update may be incomplete, check path and access rights.",
+ semanticsJsonDatabasePath);
+ }
+
+ Map defaultTags = (new DefaultSemanticTagProvider()).getAll().stream()
+ .collect(Collectors.toMap(SemanticTag::getName, Function.identity()));
+ Map customTags = Map.of();
+
+ Set changedTags = new HashSet<>(TAGS_CHANGED_CLASS);
+
+ if (canUpdateDb) {
+ JsonStorage semanticTagStorage = new JsonStorage<>(semanticsJsonDatabasePath.toFile(), null,
+ 5, 0, 0, List.of());
+
+ // Remove duplicate tags from custom tag store
+ for (String tagKey : List.copyOf(semanticTagStorage.getKeys())) {
+ SemanticTag tag = SemanticTagDTOMapper.map(semanticTagStorage.get(tagKey));
+ if (tag == null) {
+ continue;
+ }
+
+ String tagUID = tag.getUID();
+ String tagName = tag.getName();
+ if (defaultTags.keySet().contains(tagName)) {
+ logger.info(" Removed duplicate tag '{}' with UID '{}' from custom tags", tagName, tagUID);
+ semanticTagStorage.remove(tagUID);
+ changedTags.add(tagName);
+ }
+ }
+ semanticTagStorage.flush();
+
+ // Rewrite parent relationships if position in hierarchy has changed
+ for (String tagKey : List.copyOf(semanticTagStorage.getKeys())) {
+ SemanticTag tag = SemanticTagDTOMapper.map(semanticTagStorage.get(tagKey));
+ if (tag == null) {
+ continue;
+ }
+
+ String tagUID = tag.getUID();
+ String[] hierarchy = tagUID.split("_");
+ if (hierarchy.length <= 2) {
+ // No need to check for update if only semantic class and tag
+ continue;
+ }
+
+ String tagName = tag.getName();
+ for (int i = hierarchy.length - 2; i > 0; i--) {
+ String parentName = hierarchy[i];
+ SemanticTag newParent = defaultTags.get(parentName);
+ if (newParent != null) {
+ String newParentUID = newParent.getUID();
+ String childUID = String.join("_", Arrays.copyOfRange(hierarchy, i + 1, hierarchy.length));
+ String newUID = newParentUID + "_" + childUID;
+ if (!tagUID.equals(newUID)) {
+ SemanticTag newTag = new SemanticTagImpl(newUID, tag.getLabel(), tag.getDescription(),
+ tag.getSynonyms());
+ semanticTagStorage.put(newUID, SemanticTagDTOMapper.map(newTag));
+ semanticTagStorage.remove(tagUID);
+ changedTags.add(tagName);
+ logger.info(" Updated custom tag '{}' parent from '{}' to '{}'", tagName,
+ tag.getParentUID(), newTag.getParentUID());
+ break;
+ }
+ }
+ }
+ }
+ semanticTagStorage.flush();
+ dbUpdated = true;
+
+ customTags = semanticTagStorage.getValues().stream().map(tag -> SemanticTagDTOMapper.map(tag))
+ .filter(Objects::nonNull).collect(Collectors.toMap(SemanticTag::getName, Function.identity()));
+ }
+
+ Path itemJsonDatabasePath = userdataPath.resolve(Path.of("jsondb", "org.openhab.core.items.Item.json"));
+ logger.info("Cleaning item semantic tags '{}'", itemJsonDatabasePath);
+
+ if (!Files.exists(itemJsonDatabasePath)) {
+ logger.info("No item database '{}', no managed items to update.", itemJsonDatabasePath);
+ return dbUpdated;
+ } else if (!Files.isWritable(itemJsonDatabasePath)) {
+ logger.warn("Cannot access item database '{}', update may be incomplete, check path and access rights.",
+ itemJsonDatabasePath);
+ return false;
+ }
+ JsonStorage itemStorage = new JsonStorage<>(itemJsonDatabasePath.toFile(),
+ null, 5, 0, 0, List.of());
+
+ // Adjust item tags, make sure only 1 tag of every class exists, rename other tags
+ Map allSemanticTags = Stream.of(defaultTags, customTags)
+ .flatMap(map -> map.entrySet().stream()).collect(Collectors.toMap(Map.Entry::getKey,
+ Map.Entry::getValue, (defaultTag, customTag) -> defaultTag));
+ Set itemTagNames = itemStorage.getValues().stream().filter(Objects::nonNull).map(item -> item.tags)
+ .filter(Objects::nonNull).flatMap(tags -> tags.stream()).collect(Collectors.toSet());
+ Set allTagNames = Stream.of(allSemanticTags.keySet(), itemTagNames).flatMap(tags -> tags.stream())
+ .collect(Collectors.toSet());
+
+ for (String itemKey : itemStorage.getKeys()) {
+ ManagedItemProvider.PersistedItem item = itemStorage.get(itemKey);
+ Set tags = item != null ? item.tags : null;
+ if (item == null || tags == null || tags.isEmpty()) {
+ continue;
+ }
+
+ Set newTags = new HashSet<>(tags);
+ Set locationTags = new HashSet<>(tags.stream()
+ .filter(tag -> "Location".equals(tagClass(allSemanticTags.get(tag)))).collect(Collectors.toSet()));
+ Set equipmentTags = new HashSet<>(tags.stream()
+ .filter(tag -> "Equipment".equals(tagClass(allSemanticTags.get(tag)))).collect(Collectors.toSet()));
+ Set pointTags = new HashSet<>(tags.stream()
+ .filter(tag -> "Point".equals(tagClass(allSemanticTags.get(tag)))).collect(Collectors.toSet()));
+ Set propertyTags = new HashSet<>(tags.stream()
+ .filter(tag -> "Property".equals(tagClass(allSemanticTags.get(tag)))).collect(Collectors.toSet()));
+
+ Map> tagSets = Map.of("Location", locationTags, "Equipment", equipmentTags, "Point",
+ pointTags, "Property", propertyTags);
+ for (String tagClass : tagSets.keySet()) {
+ Set tagSet = tagSets.get(tagClass);
+ while (tagSet != null && tagSet.size() > 1) {
+ String tagToRemove = tagSet.stream().filter(tag -> !changedTags.contains(tag)).findAny()
+ .orElse(tagSet.iterator().next());
+ if (tagToRemove != null) {
+ String tagToAdd = newTagName(tagToRemove, allTagNames);
+ newTags.remove(tagToRemove);
+ newTags.add(tagToAdd);
+ tagSet.remove(tagToRemove);
+ logger.info(" Multiple {} tags on item '{}', renamed '{}' to '{}'", tagClass, itemKey,
+ tagToRemove, tagToAdd);
+ }
+ }
+ if (tagSet != null && tagSet.size() == 1 && "Property".equals(tagClass) && pointTags.isEmpty()) {
+ // When there is a Property tag, there should also be a Point tag, add it if missing.
+ // Status is a neutral one, easy to reconfigure if necessary.
+ newTags.add(DEFAULT_POINT_TAG);
+ logger.info(" Item '{}' has Property tag '{}' without Point tag, added Point tag '{}'", itemKey,
+ tagSet.iterator().next(), DEFAULT_POINT_TAG);
+ }
+ }
+
+ if (!newTags.equals(tags)) {
+ item.tags = newTags;
+ itemStorage.put(itemKey, item);
+ }
+ }
+ itemStorage.flush();
+
+ return dbUpdated;
+ }
+
+ private String newTagName(String oldTagName, Set allTagNames) {
+ String tagName = oldTagName;
+ int i = 1;
+ while (allTagNames.contains(tagName)) {
+ tagName = oldTagName + String.valueOf(i);
+ i++;
+ }
+ return tagName;
+ }
+
+ private String tagClass(String tagUID) {
+ int idx = tagUID.indexOf("_");
+ return (idx >= 0 ? tagUID.substring(0, idx) : tagUID).trim();
+ }
+
+ private @Nullable String tagClass(@Nullable SemanticTag tag) {
+ if (tag == null) {
+ return null;
+ }
+ return tagClass(tag.getUID());
+ }
+}
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 bb3afe204..e7d5bcb91 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
@@ -27,13 +27,14 @@ import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonInclude.Value;
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.YAMLMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
/**
@@ -66,24 +67,26 @@ public class YamlConfigurationV1TagsUpgrader implements Upgrader {
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() //
+ objectMapper = YAMLMapper.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.setDefaultPropertyInclusion(Include.NON_NULL);
- objectMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
+ .enable(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS) // use quotes for numbers stored as
+ // strings
+ .enable(YAMLParser.Feature.PARSE_BOOLEAN_LIKE_WORDS_AS_STRINGS) // do not parse ON/OFF/... as booleans
+ .findAndAddModules() //
+ .visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) //
+ .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) //
+ // Correct compatible behavior with {@link YamlModelRepositoryImpl} for jackson > 2.8, don't use
+ // Include.ALWAYS as second argument (which would have been the behavior earlier)
+ .defaultPropertyInclusion(Value.construct(Include.NON_NULL, Include.NON_NULL)) //
+ .enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN) //
+ .build();
}
@Override
@@ -104,15 +107,13 @@ public class YamlConfigurationV1TagsUpgrader implements Upgrader {
}
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");
- }
+ // If confPath is set to OPENHAB_CONF, look inside /tags/ subdirectory otherwise use the given confPath as is.
+ // Make configPath "effectively final" inside the lambda below.
+ Path configPath = confEnv != null && !confEnv.isBlank() && Path.of(confEnv).toAbsolutePath().equals(confPath)
+ ? confPath.resolve("tags")
+ : confPath;
+ 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