UpgradeTool add semantics upgrader and OH version (#5379)

* upgrade semantic tags

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
Mark Herwege
2026-05-29 22:23:42 +02:00
committed by GitHub
parent 64cb8b8145
commit 1976decbb9
11 changed files with 510 additions and 69 deletions
+10
View File
@@ -43,6 +43,11 @@
<artifactId>org.openhab.core.persistence</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.semantics</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.storage.json</artifactId>
@@ -92,6 +97,11 @@
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-artifact</artifactId>
<version>${maven.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
@@ -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<Upgrader> 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<UpgradeRecord> upgradeRecords = null;
private static @Nullable JsonStorage<VersionRecord> 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<UpgradeRecord> 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<UpgradeRecord> 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<UpgradeRecord> 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<UpgradeRecord> records = upgradeRecords;
if (records != null) {
UpgradeRecord upgradeRecord = records.get(upgrader);
JsonStorage<UpgradeRecord> 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<UpgradeRecord> 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<VersionRecord> 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<UpgradeRecord> 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<VersionRecord> 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;
}
};
}
@@ -30,6 +30,10 @@ public interface Upgrader {
String getDescription();
default @Nullable String getTargetVersion() {
return null;
}
/**
* Executes the upgrade process.
*
@@ -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
@@ -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
@@ -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)) {
@@ -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)) {
@@ -38,6 +38,8 @@ import org.slf4j.LoggerFactory;
* configuration that has no strategy defined.
* See <a href="https://github.com/openhab/openhab-core/pull/4682">openhab/openhab-core#4682</a>.
*
* @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<String> defaults = serviceConfigDTO.defaults;
if (defaults != null) {
Collection<PersistenceItemConfigurationDTO> configs = serviceConfigDTO.configs;
configs.forEach(config -> {
Collection<String> 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<PersistenceServiceConfigurationDTO> persistenceStorage, String serviceId) {
PersistenceServiceConfigurationDTO serviceConfigDTO = Objects.requireNonNull(persistenceStorage.get(serviceId));
Collection<String> defaults = serviceConfigDTO.defaults;
if (defaults != null) {
Collection<PersistenceItemConfigurationDTO> configs = serviceConfigDTO.configs;
configs.forEach(config -> {
Collection<String> 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<String> installedPersistenceAddons(Path userdataPath) throws IOException {
Path addonsConfigPath = userdataPath.resolve("config/org/openhab/addons.config");
if (Files.notExists(addonsConfigPath)) {
@@ -33,6 +33,8 @@ import org.slf4j.LoggerFactory;
* {@code commandFromItemScript} and {@code stateFromItemScript}.
* See <a href="https://github.com/openhab/openhab-core/pull/4058">openhab/openhab-core#4058</a>.
*
* @since 4.2.0
*
* @author Florian Hotze - Initial contribution
* @author Jimmy Tanagra - Refactored into a separate class
*/
@@ -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<String> 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<String, SemanticTag> defaultTags = (new DefaultSemanticTagProvider()).getAll().stream()
.collect(Collectors.toMap(SemanticTag::getName, Function.identity()));
Map<String, SemanticTag> customTags = Map.of();
Set<String> changedTags = new HashSet<>(TAGS_CHANGED_CLASS);
if (canUpdateDb) {
JsonStorage<SemanticTagDTO> 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<ManagedItemProvider.PersistedItem> 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<String, SemanticTag> allSemanticTags = Stream.of(defaultTags, customTags)
.flatMap(map -> map.entrySet().stream()).collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue, (defaultTag, customTag) -> defaultTag));
Set<String> itemTagNames = itemStorage.getValues().stream().filter(Objects::nonNull).map(item -> item.tags)
.filter(Objects::nonNull).flatMap(tags -> tags.stream()).collect(Collectors.toSet());
Set<String> 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<String> tags = item != null ? item.tags : null;
if (item == null || tags == null || tags.isEmpty()) {
continue;
}
Set<String> newTags = new HashSet<>(tags);
Set<String> locationTags = new HashSet<>(tags.stream()
.filter(tag -> "Location".equals(tagClass(allSemanticTags.get(tag)))).collect(Collectors.toSet()));
Set<String> equipmentTags = new HashSet<>(tags.stream()
.filter(tag -> "Equipment".equals(tagClass(allSemanticTags.get(tag)))).collect(Collectors.toSet()));
Set<String> pointTags = new HashSet<>(tags.stream()
.filter(tag -> "Point".equals(tagClass(allSemanticTags.get(tag)))).collect(Collectors.toSet()));
Set<String> propertyTags = new HashSet<>(tags.stream()
.filter(tag -> "Property".equals(tagClass(allSemanticTags.get(tag)))).collect(Collectors.toSet()));
Map<String, Set<String>> tagSets = Map.of("Location", locationTags, "Equipment", equipmentTags, "Point",
pointTags, "Property", propertyTags);
for (String tagClass : tagSets.keySet()) {
Set<String> 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<String> 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());
}
}
@@ -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