mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-31 05:24:26 +02:00
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:
@@ -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,23 +133,40 @@ 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) {
|
||||
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());
|
||||
if (upgrader.execute(userdataPath, confPath)) {
|
||||
@@ -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;
|
||||
private record UpgradeRecord(String executionDate, @Nullable String executionVersion) {
|
||||
public UpgradeRecord(ZonedDateTime executionDate, @Nullable String executionVersion) {
|
||||
this(executionDate.toString(), executionVersion);
|
||||
}
|
||||
}
|
||||
|
||||
public UpgradeRecord(ZonedDateTime executionDate) {
|
||||
this.executionDate = executionDate.toString();
|
||||
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.
|
||||
*
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+5
-5
@@ -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)) {
|
||||
|
||||
+4
-2
@@ -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)) {
|
||||
|
||||
+12
-6
@@ -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,8 +125,16 @@ public class PersistenceUpgrader implements Upgrader {
|
||||
|
||||
// Update existing managed configurations
|
||||
persistenceStorageKeys.forEach(serviceId -> {
|
||||
PersistenceServiceConfigurationDTO serviceConfigDTO = Objects
|
||||
.requireNonNull(persistenceStorage.get(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;
|
||||
@@ -139,10 +149,6 @@ public class PersistenceUpgrader implements Upgrader {
|
||||
persistenceStorage.put(serviceId, serviceConfigDTO);
|
||||
logger.info("{}: updated strategy configurations and removed default strategies", serviceId);
|
||||
}
|
||||
});
|
||||
|
||||
persistenceStorage.flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<String> installedPersistenceAddons(Path userdataPath) throws IOException {
|
||||
|
||||
+2
@@ -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
|
||||
*/
|
||||
|
||||
+270
@@ -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());
|
||||
}
|
||||
}
|
||||
+20
-19
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user