refactored upgrade services

This commit is contained in:
YevhenBondarenko 2024-11-28 15:17:10 +01:00
parent 9a90c381d1
commit b61d7b6b17
15 changed files with 82 additions and 368 deletions

View File

@ -24,7 +24,7 @@ import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.install.DatabaseEntitiesUpgradeService; import org.thingsboard.server.service.install.DatabaseEntitiesUpgradeService;
import org.thingsboard.server.service.install.DatabaseSchemaVersionService; import org.thingsboard.server.service.install.DatabaseSchemaSettingsService;
import org.thingsboard.server.service.install.EntityDatabaseSchemaService; import org.thingsboard.server.service.install.EntityDatabaseSchemaService;
import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.install.NoSqlKeyspaceService; import org.thingsboard.server.service.install.NoSqlKeyspaceService;
@ -89,7 +89,7 @@ public class ThingsboardInstallService {
private InstallScripts installScripts; private InstallScripts installScripts;
@Autowired @Autowired
private DatabaseSchemaVersionService databaseSchemaVersionService; private DatabaseSchemaSettingsService databaseSchemaVersionService;
public void performInstall() { public void performInstall() {
try { try {
@ -102,21 +102,15 @@ public class ThingsboardInstallService {
} else if (upgradeFromVersion.equals("3.6.2-images")) { } else if (upgradeFromVersion.equals("3.6.2-images")) {
installScripts.updateImages(); installScripts.updateImages();
} else { } else {
upgradeFromVersion = databaseSchemaVersionService.validateSchemaSettings(upgradeFromVersion); databaseSchemaVersionService.validateSchemaSettings();
cacheCleanupService.clearCache(upgradeFromVersion); //TODO DON'T FORGET to update SUPPORTED_VERSIONS_FROM in DatabaseSchemaVersionService,
switch (upgradeFromVersion) { // this list should include last version and can include previous versions without upgrade
case "3.8.0": String fromVersion = databaseSchemaVersionService.getDbSchemaVersion();
log.info("Upgrading ThingsBoard from version 3.8.0 to 3.8.1 ..."); String toVersion = databaseSchemaVersionService.getPackageSchemaVersion();
case "3.8.1": cacheCleanupService.clearCache(fromVersion, toVersion);
log.info("Upgrading ThingsBoard from version 3.8.1 to 3.9.0 ..."); databaseEntitiesUpgradeService.upgradeDatabase(fromVersion, toVersion);
databaseEntitiesUpgradeService.upgradeDatabase("3.8.1"); // dataUpdateService.updateData(fromVersion, toVersion);
installScripts.updateResourcesUsage(); installScripts.updateResourcesUsage();
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache (include previous versions without upgrade)
//TODO DON'T FORGET to update SUPPORTED_VERSIONS_FROM, this list should include last version and can include previous versions without upgrade
break;
default:
throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
}
entityDatabaseSchemaService.createDatabaseSchema(false); entityDatabaseSchemaService.createDatabaseSchema(false);
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions(); entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();

View File

@ -1,117 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
@Slf4j
public abstract class AbstractSqlTsDatabaseUpgradeService {
protected static final String CALL_REGEX = "call ";
protected static final String DROP_TABLE = "DROP TABLE ";
protected static final String DROP_PROCEDURE_IF_EXISTS = "DROP PROCEDURE IF EXISTS ";
protected static final String TS_KV_SQL = "ts_kv.sql";
protected static final String PATH_TO_USERS_PUBLIC_FOLDER = "C:\\Users\\Public";
protected static final String THINGSBOARD_WINDOWS_UPGRADE_DIR = "THINGSBOARD_WINDOWS_UPGRADE_DIR";
@Value("${spring.datasource.url}")
protected String dbUrl;
@Value("${spring.datasource.username}")
protected String dbUserName;
@Value("${spring.datasource.password}")
protected String dbPassword;
@Autowired
protected InstallScripts installScripts;
protected abstract void loadSql(Connection conn, String fileName, String version);
protected void loadFunctions(Path sqlFile, Connection conn) throws Exception {
String sql = new String(Files.readAllBytes(sqlFile), StandardCharsets.UTF_8);
conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
}
protected boolean checkVersion(Connection conn) {
boolean versionValid = false;
try {
Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT current_setting('server_version_num')");
resultSet.next();
if(resultSet.getLong(1) > 110000) {
versionValid = true;
}
statement.close();
} catch (Exception e) {
log.info("Failed to check current PostgreSQL version due to: {}", e.getMessage());
}
return versionValid;
}
protected boolean isOldSchema(Connection conn, long fromVersion) {
boolean isOldSchema = true;
try {
Statement statement = conn.createStatement();
statement.execute("CREATE TABLE IF NOT EXISTS tb_schema_settings ( schema_version bigint NOT NULL, CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version));");
Thread.sleep(1000);
ResultSet resultSet = statement.executeQuery("SELECT schema_version FROM tb_schema_settings;");
if (resultSet.next()) {
isOldSchema = resultSet.getLong(1) <= fromVersion;
} else {
resultSet.close();
statement.execute("INSERT INTO tb_schema_settings (schema_version) VALUES (" + fromVersion + ")");
}
statement.close();
} catch (InterruptedException | SQLException e) {
log.info("Failed to check current PostgreSQL schema due to: {}", e.getMessage());
}
return isOldSchema;
}
protected void executeQuery(Connection conn, String query) {
try {
Statement statement = conn.createStatement();
statement.execute(query); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
SQLWarning warnings = statement.getWarnings();
if (warnings != null) {
log.info("{}", warnings.getMessage());
SQLWarning nextWarning = warnings.getNextWarning();
while (nextWarning != null) {
log.info("{}", nextWarning.getMessage());
nextWarning = nextWarning.getNextWarning();
}
}
Thread.sleep(2000);
log.info("Successfully executed query: {}", query);
} catch (InterruptedException | SQLException e) {
log.error("Failed to execute query: {} due to: {}", query, e.getMessage());
throw new RuntimeException("Failed to execute query:" + query + " due to: ", e);
}
}
}

View File

@ -1,37 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.NoSqlTsDao;
@Service
@NoSqlTsDao
@Profile("install")
@Slf4j
public class CassandraTsDatabaseUpgradeService extends AbstractCassandraDatabaseUpgradeService implements DatabaseTsUpgradeService {
@Override
public void upgradeDatabase(String fromVersion) throws Exception {
switch (fromVersion) {
default:
throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
}
}
}

View File

@ -17,6 +17,6 @@ package org.thingsboard.server.service.install;
public interface DatabaseEntitiesUpgradeService { public interface DatabaseEntitiesUpgradeService {
void upgradeDatabase(String fromVersion) throws Exception; void upgradeDatabase(String fromVersion, String toVersion) throws Exception;
} }

View File

@ -15,10 +15,14 @@
*/ */
package org.thingsboard.server.service.install; package org.thingsboard.server.service.install;
public interface DatabaseSchemaVersionService { public interface DatabaseSchemaSettingsService {
String validateSchemaSettings(String upgradeFromVersion); void validateSchemaSettings();
void createSchemaSettings(); void createSchemaSettings();
void updateSchemaVersion(); void updateSchemaVersion();
String getPackageSchemaVersion();
String getDbSchemaVersion();
} }

View File

@ -1,22 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install;
public interface DatabaseTsUpgradeService {
void upgradeDatabase(String fromVersion) throws Exception;
}

View File

@ -22,7 +22,6 @@ import org.springframework.context.annotation.Profile;
import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.service.install.update.DefaultDataUpdateService; import org.thingsboard.server.service.install.update.DefaultDataUpdateService;
import java.util.List; import java.util.List;
@ -31,7 +30,7 @@ import java.util.List;
@Profile("install") @Profile("install")
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class DefaultDatabaseSchemaVersionService implements DatabaseSchemaVersionService { public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSettingsService {
private static final String CURRENT_PRODUCT = "CE"; private static final String CURRENT_PRODUCT = "CE";
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("3.8.0", "3.8.1"); private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("3.8.0", "3.8.1");
@ -39,52 +38,35 @@ public class DefaultDatabaseSchemaVersionService implements DatabaseSchemaVersio
private final BuildProperties buildProperties; private final BuildProperties buildProperties;
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private String packageSchemaVersion;
private String schemaVersionFromDb;
@Override @Override
public String validateSchemaSettings(String upgradeFromVersion) { public void validateSchemaSettings() {
//TODO: remove after release (3.9.0) //TODO: remove after release (3.9.0)
createProductIfNotExists(); createProductIfNotExists();
if (StringUtils.isNotEmpty(upgradeFromVersion) && DefaultDataUpdateService.getEnv("SKIP_SCHEMA_VERSION_CHECK", false)) { String dbSchemaVersion = getDbSchemaVersion();
if (DefaultDataUpdateService.getEnv("SKIP_SCHEMA_VERSION_CHECK", false)) {
log.info("Skipped DB schema version check due to SKIP_SCHEMA_VERSION_CHECK set to 'true'."); log.info("Skipped DB schema version check due to SKIP_SCHEMA_VERSION_CHECK set to 'true'.");
return upgradeFromVersion; return;
}
if (dbSchemaVersion.equals(getPackageSchemaVersion())) {
onSchemaSettingsError("Upgrade failed: database already upgraded to current version. You can set SKIP_SCHEMA_VERSION_CHECK to 'true' if force re-upgrade needed.");
}
if (!SUPPORTED_VERSIONS_FOR_UPGRADE.contains(dbSchemaVersion)) {
onSchemaSettingsError(String.format("Upgrade failed: database version '%s' is not supported for upgrade. Supported versions are: %s.",
dbSchemaVersion, SUPPORTED_VERSIONS_FOR_UPGRADE
));
} }
String product = getProductFromDb(); String product = getProductFromDb();
if (!CURRENT_PRODUCT.equals(product)) { if (!CURRENT_PRODUCT.equals(product)) {
onSchemaSettingsError(String.format("Upgrade failed: can't upgrade ThingsBoard %s database using ThingsBoard %s.", product, CURRENT_PRODUCT)); onSchemaSettingsError(String.format("Upgrade failed: can't upgrade ThingsBoard %s database using ThingsBoard %s.", product, CURRENT_PRODUCT));
} }
Long schemaVersionFromDb = getSchemaVersionFromDb();
if (schemaVersionFromDb == null) {
onSchemaSettingsError("Upgrade failed: the database schema version is missing.");
}
long currentSchemaVersion = getCurrentSchemaVersion();
if (currentSchemaVersion == schemaVersionFromDb) {
onSchemaSettingsError("Upgrade failed: database already upgraded to current version. You can set SKIP_SCHEMA_VERSION_CHECK to 'true' if force re-upgrade needed.");
}
long major = schemaVersionFromDb / 1000000;
long minor = (schemaVersionFromDb % 1000000) / 1000;
long patch = schemaVersionFromDb % 1000;
String currentSchemaVersionFromDb = major + "." + minor + "." + patch;
if (!SUPPORTED_VERSIONS_FOR_UPGRADE.contains(currentSchemaVersionFromDb)) {
onSchemaSettingsError(String.format("Upgrade failed: database version '%s' is not supported for upgrade. Supported versions are: %s.",
currentSchemaVersionFromDb, SUPPORTED_VERSIONS_FOR_UPGRADE
));
}
if (StringUtils.isEmpty(upgradeFromVersion)) {
upgradeFromVersion = currentSchemaVersionFromDb;
} else if (!SUPPORTED_VERSIONS_FOR_UPGRADE.contains(upgradeFromVersion)) {
onSchemaSettingsError(String.format("Upgrade failed: 'versionFrom' '%s' is not supported for upgrade. Supported versions are: %s.",
upgradeFromVersion, SUPPORTED_VERSIONS_FOR_UPGRADE));
}
return upgradeFromVersion;
} }
@Deprecated(forRemoval = true, since = "3.9.0") @Deprecated(forRemoval = true, since = "3.9.0")
@ -104,13 +86,39 @@ public class DefaultDatabaseSchemaVersionService implements DatabaseSchemaVersio
public void createSchemaSettings() { public void createSchemaSettings() {
Long schemaVersion = getSchemaVersionFromDb(); Long schemaVersion = getSchemaVersionFromDb();
if (schemaVersion == null) { if (schemaVersion == null) {
jdbcTemplate.execute("INSERT INTO tb_schema_settings (schema_version, product) VALUES (" + getCurrentSchemaVersion() + ", '" + CURRENT_PRODUCT + "')"); jdbcTemplate.execute("INSERT INTO tb_schema_settings (schema_version, product) VALUES (" + getPackageSchemaVersionForDb() + ", '" + CURRENT_PRODUCT + "')");
} }
} }
@Override @Override
public void updateSchemaVersion() { public void updateSchemaVersion() {
jdbcTemplate.execute("UPDATE tb_schema_settings SET schema_version = " + getCurrentSchemaVersion()); jdbcTemplate.execute("UPDATE tb_schema_settings SET schema_version = " + getPackageSchemaVersionForDb());
}
@Override
public String getPackageSchemaVersion() {
if (packageSchemaVersion == null) {
packageSchemaVersion = buildProperties.getVersion().replaceAll("[^\\d.]", "");
}
return packageSchemaVersion;
}
@Override
public String getDbSchemaVersion() {
if (schemaVersionFromDb == null) {
Long version = getSchemaVersionFromDb();
if (version == null) {
onSchemaSettingsError("Upgrade failed: the database schema version is missing.");
}
@SuppressWarnings("DataFlowIssue")
long major = version / 1000000;
long minor = (version % 1000000) / 1000;
long patch = version % 1000;
schemaVersionFromDb = major + "." + minor + "." + patch;
}
return schemaVersionFromDb;
} }
private Long getSchemaVersionFromDb() { private Long getSchemaVersionFromDb() {
@ -121,8 +129,8 @@ public class DefaultDatabaseSchemaVersionService implements DatabaseSchemaVersio
return jdbcTemplate.queryForList("SELECT product FROM tb_schema_settings", String.class).stream().findFirst().orElse(null); return jdbcTemplate.queryForList("SELECT product FROM tb_schema_settings", String.class).stream().findFirst().orElse(null);
} }
private long getCurrentSchemaVersion() { private long getPackageSchemaVersionForDb() {
String[] versionParts = buildProperties.getVersion().replaceAll("[^\\d.]", "").split("\\."); String[] versionParts = getPackageSchemaVersion().split("\\.");
long major = Integer.parseInt(versionParts[0]); long major = Integer.parseInt(versionParts[0]);
long minor = Integer.parseInt(versionParts[1]); long minor = Integer.parseInt(versionParts[1]);

View File

@ -51,11 +51,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
} }
@Override @Override
public void upgradeDatabase(String fromVersion) { public void upgradeDatabase(String fromVersion, String toVersion) {
switch (fromVersion) { log.info("Updating schema from version {} to {} ...", fromVersion, toVersion);
case "3.8.1" -> loadSql(getSchemaUpdateFile(fromVersion)); loadSql(getSchemaUpdateFile("basic"));
default -> throw new RuntimeException("Unsupported fromVersion '" + fromVersion + "'"); log.info("Schema updated.");
}
} }
private Path getSchemaUpdateFile(String version) { private Path getSchemaUpdateFile(String version) {

View File

@ -1,51 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.SqlTsDao;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
@Service
@Profile("install")
@Slf4j
@SqlTsDao
public class SqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeService implements DatabaseTsUpgradeService {
@Override
public void upgradeDatabase(String fromVersion) throws Exception {
switch (fromVersion) {
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
}
@Override
protected void loadSql(Connection conn, String fileName, String version) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", version, fileName);
try {
loadFunctions(schemaUpdateFile, conn);
log.info("Functions successfully loaded!");
} catch (Exception e) {
log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
}
}
}

View File

@ -1,55 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
@Service
@Profile("install")
@Slf4j
@TimescaleDBTsDao
public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeService implements DatabaseTsUpgradeService {
@Autowired
private InstallScripts installScripts;
@Override
public void upgradeDatabase(String fromVersion) throws Exception {
switch (fromVersion) {
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
}
@Override
protected void loadSql(Connection conn, String fileName, String version) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", version, fileName);
try {
loadFunctions(schemaUpdateFile, conn);
log.info("Functions successfully loaded!");
} catch (Exception e) {
log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
}
}
}

View File

@ -17,6 +17,6 @@ package org.thingsboard.server.service.install.update;
public interface CacheCleanupService { public interface CacheCleanupService {
void clearCache(String fromVersion) throws Exception; void clearCache(String from, String to) throws Exception;
} }

View File

@ -17,7 +17,7 @@ package org.thingsboard.server.service.install.update;
public interface DataUpdateService { public interface DataUpdateService {
void updateData(String fromVersion) throws Exception; void updateData(String fromVersion, String toVersion) throws Exception;
void upgradeRuleNodes(); void upgradeRuleNodes();
} }

View File

@ -42,16 +42,9 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
* to discover which tables were changed * to discover which tables were changed
* */ * */
@Override @Override
public void clearCache(String fromVersion) throws Exception { public void clearCache(String from, String to) throws Exception {
switch (fromVersion) { log.info("Clearing cache to upgrade from version {} to {}", from, to);
case "3.8.0":
case "3.8.1":
log.info("Clearing cache to upgrade from version {} to 3.9.0", fromVersion);
clearAllCaches(); clearAllCaches();
break;
default:
//Do nothing, since cache cleanup is optional.
}
} }
void clearAllCaches() { void clearAllCaches() {

View File

@ -58,11 +58,9 @@ public class DefaultDataUpdateService implements DataUpdateService {
JpaExecutorService jpaExecutorService; JpaExecutorService jpaExecutorService;
@Override @Override
public void updateData(String fromVersion) throws Exception { public void updateData(String fromVersion, String toVersion) throws Exception {
switch (fromVersion) { log.info("Updating data from version {} to {} ...", fromVersion, toVersion);
default: log.info("Data updated.");
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
}
} }
@Override @Override