128 changed files with 2220 additions and 1118 deletions
@ -0,0 +1,272 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.system; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.google.common.hash.Hashing; |
|||
import jakarta.annotation.PostConstruct; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; |
|||
import org.thingsboard.server.dao.widget.WidgetTypeService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; |
|||
import org.thingsboard.server.service.install.InstallScripts; |
|||
import org.thingsboard.server.service.install.update.DefaultDataUpdateService; |
|||
|
|||
import java.io.IOException; |
|||
import java.io.UncheckedIOException; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.NoSuchFileException; |
|||
import java.nio.file.Path; |
|||
import java.util.Objects; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
import java.util.stream.Stream; |
|||
|
|||
/** |
|||
* Runs at application startup and applies no-downtime data updates |
|||
* when the package PATCH version increases (e.g., 4.2.1.0 -> 4.2.1.1). |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
@TbCoreComponent |
|||
@RequiredArgsConstructor |
|||
public class SystemPatchApplier { |
|||
|
|||
private static final long ADVISORY_LOCK_ID = 7536891047216478431L; |
|||
|
|||
private final JdbcTemplate jdbcTemplate; |
|||
private final InstallScripts installScripts; |
|||
private final DatabaseSchemaSettingsService schemaSettingsService; |
|||
private final WidgetTypeService widgetTypeService; |
|||
|
|||
@PostConstruct |
|||
private void init() { |
|||
ExecutorService executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("system-patch-applier")); |
|||
executor.submit(() -> { |
|||
try { |
|||
applyPatchIfNeeded(); |
|||
} catch (Exception e) { |
|||
log.error("Failed to apply system data patch updates", e); |
|||
} finally { |
|||
executor.shutdown(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private void applyPatchIfNeeded() { |
|||
boolean skipVersionCheck = DefaultDataUpdateService.getEnv("SKIP_PATCH_VERSION_CHECK", false); |
|||
if (!skipVersionCheck && !isVersionChanged()) { |
|||
return; |
|||
} |
|||
|
|||
if (!acquireAdvisoryLock()) { |
|||
log.trace("Could not acquire advisory lock. Another node is processing patch updates."); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
int updated = updateWidgetTypes(); |
|||
log.info("Updated {} widget types", updated); |
|||
|
|||
schemaSettingsService.updateSchemaVersion(); |
|||
log.info("System data patch update completed successfully"); |
|||
|
|||
} finally { |
|||
releaseAdvisoryLock(); |
|||
} |
|||
} |
|||
|
|||
private boolean isVersionChanged() { |
|||
String packageVersion = schemaSettingsService.getPackageSchemaVersion(); |
|||
String dbVersion = schemaSettingsService.getDbSchemaVersion(); |
|||
|
|||
log.trace("Package version: {}, DB schema version: {}", packageVersion, dbVersion); |
|||
|
|||
VersionInfo packageVersionInfo = parseVersion(packageVersion); |
|||
VersionInfo dbVersionInfo = parseVersion(dbVersion); |
|||
|
|||
if (packageVersionInfo == null || dbVersionInfo == null) { |
|||
log.warn("Unable to parse versions. Package: {}, DB: {}", packageVersion, dbVersion); |
|||
return false; |
|||
} |
|||
|
|||
if (!isPatchVersionChanged(packageVersionInfo, dbVersionInfo)) { |
|||
return false; |
|||
} |
|||
|
|||
log.info("Patch version increased from {} to {}. Starting system data update.", dbVersion, packageVersion); |
|||
return true; |
|||
} |
|||
|
|||
private boolean isPatchVersionChanged(VersionInfo packageVersion, VersionInfo dbVersion) { |
|||
return packageVersion.major == dbVersion.major && packageVersion.minor == dbVersion.minor |
|||
&& packageVersion.maintenance == dbVersion.maintenance && packageVersion.patch > dbVersion.patch; |
|||
} |
|||
|
|||
private int updateWidgetTypes() { |
|||
AtomicInteger updated = new AtomicInteger(); |
|||
Path widgetTypesDir = installScripts.getWidgetTypesDir(); |
|||
|
|||
if (!Files.exists(widgetTypesDir)) { |
|||
log.trace("Widget types directory does not exist: {}", widgetTypesDir); |
|||
return 0; |
|||
} |
|||
|
|||
try (Stream<Path> dirStream = listDir(widgetTypesDir).filter(path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { |
|||
dirStream.forEach( |
|||
path -> { |
|||
try { |
|||
if (updateWidgetTypeFromFile(path)) { |
|||
updated.incrementAndGet(); |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("Unable to update widget type from json: [{}]", path.toString()); |
|||
throw new RuntimeException("Unable to update widget type from json", e); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
return updated.get(); |
|||
} |
|||
|
|||
private boolean updateWidgetTypeFromFile(Path filePath) { |
|||
JsonNode json = JacksonUtil.toJsonNode(filePath.toFile()); |
|||
WidgetTypeDetails fileWidgetType = JacksonUtil.treeToValue(json, WidgetTypeDetails.class); |
|||
String fqn = fileWidgetType.getFqn(); |
|||
|
|||
WidgetTypeDetails existingWidgetType = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, fqn); |
|||
if (existingWidgetType == null) { |
|||
// We expect only update here, so it's probably never happening, but for test purpose leave it like this:
|
|||
throw new RuntimeException("Widget type not found: " + fqn); |
|||
} |
|||
if (isWidgetTypeChanged(existingWidgetType, fileWidgetType)) { |
|||
existingWidgetType.setDescription(fileWidgetType.getDescription()); |
|||
existingWidgetType.setName(fileWidgetType.getName()); |
|||
existingWidgetType.setDescriptor(fileWidgetType.getDescriptor()); |
|||
widgetTypeService.saveWidgetType(existingWidgetType); |
|||
log.trace("Updated widget type: {}", fqn); |
|||
return true; |
|||
} |
|||
|
|||
log.trace("Widget type unchanged: {}", fqn); |
|||
return false; |
|||
} |
|||
|
|||
private boolean isWidgetTypeChanged(WidgetTypeDetails existing, WidgetTypeDetails file) { |
|||
if (!isDescriptorEqual(existing.getDescriptor(), file.getDescriptor())) { |
|||
return true; |
|||
} |
|||
|
|||
if (!Objects.equals(existing.getName(), file.getName())) { |
|||
return true; |
|||
} |
|||
|
|||
return !Objects.equals(existing.getDescription(), file.getDescription()); |
|||
} |
|||
|
|||
private boolean isDescriptorEqual(JsonNode desc1, JsonNode desc2) { |
|||
if (desc1 == null && desc2 == null) { |
|||
return true; |
|||
} |
|||
if (desc1 == null || desc2 == null) { |
|||
return false; |
|||
} |
|||
|
|||
try { |
|||
String hash1 = computeChecksum(desc1); |
|||
String hash2 = computeChecksum(desc2); |
|||
return Objects.equals(hash1, hash2); |
|||
} catch (Exception e) { |
|||
log.warn("Failed to compare descriptors using checksum, falling back to equals", e); |
|||
return desc1.equals(desc2); |
|||
} |
|||
} |
|||
|
|||
private String computeChecksum(JsonNode node) { |
|||
String canonicalString = JacksonUtil.toCanonicalString(node); |
|||
if (canonicalString == null) { |
|||
return null; |
|||
} |
|||
return Hashing.sha256().hashBytes(canonicalString.getBytes()).toString(); |
|||
} |
|||
|
|||
private boolean acquireAdvisoryLock() { |
|||
try { |
|||
Boolean acquired = jdbcTemplate.queryForObject( |
|||
"SELECT pg_try_advisory_lock(?)", |
|||
Boolean.class, |
|||
ADVISORY_LOCK_ID |
|||
); |
|||
if (Boolean.TRUE.equals(acquired)) { |
|||
log.trace("Acquired advisory lock"); |
|||
return true; |
|||
} |
|||
return false; |
|||
} catch (Exception e) { |
|||
log.error("Failed to acquire advisory lock", e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
private void releaseAdvisoryLock() { |
|||
try { |
|||
jdbcTemplate.queryForObject( |
|||
"SELECT pg_advisory_unlock(?)", |
|||
Boolean.class, |
|||
ADVISORY_LOCK_ID |
|||
); |
|||
log.debug("Released advisory lock"); |
|||
} catch (Exception e) { |
|||
log.error("Failed to release advisory lock", e); |
|||
} |
|||
} |
|||
|
|||
private VersionInfo parseVersion(String version) { |
|||
try { |
|||
String[] parts = version.split("\\."); |
|||
int major = Integer.parseInt(parts[0]); |
|||
int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; |
|||
int maintenance = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; |
|||
int patch = parts.length > 3 ? Integer.parseInt(parts[3]) : 0; |
|||
return new VersionInfo(major, minor, maintenance, patch); |
|||
} catch (Exception e) { |
|||
log.error("Failed to parse version: {}", version, e); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private Stream<Path> listDir(Path dir) { |
|||
try { |
|||
return Files.list(dir); |
|||
} catch (NoSuchFileException e) { |
|||
return Stream.empty(); |
|||
} catch (IOException e) { |
|||
throw new UncheckedIOException(e); |
|||
} |
|||
} |
|||
|
|||
public record VersionInfo(int major, int minor, int maintenance, int patch) {} |
|||
|
|||
} |
|||
@ -0,0 +1,410 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.system; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.junit.jupiter.api.io.TempDir; |
|||
import org.junit.jupiter.params.ParameterizedTest; |
|||
import org.junit.jupiter.params.provider.Arguments; |
|||
import org.junit.jupiter.params.provider.CsvSource; |
|||
import org.junit.jupiter.params.provider.MethodSource; |
|||
import org.mockito.InjectMocks; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.WidgetTypeId; |
|||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; |
|||
import org.thingsboard.server.dao.widget.WidgetTypeService; |
|||
import org.thingsboard.server.service.install.InstallScripts; |
|||
import org.thingsboard.server.service.system.SystemPatchApplier; |
|||
|
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.CountDownLatch; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicBoolean; |
|||
import java.util.stream.Stream; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.junit.jupiter.api.Assertions.assertFalse; |
|||
import static org.junit.jupiter.api.Assertions.assertNotEquals; |
|||
import static org.junit.jupiter.api.Assertions.assertNotNull; |
|||
import static org.junit.jupiter.api.Assertions.assertNull; |
|||
import static org.junit.jupiter.api.Assertions.assertThrows; |
|||
import static org.junit.jupiter.api.Assertions.assertTrue; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.anyLong; |
|||
import static org.mockito.ArgumentMatchers.anyString; |
|||
import static org.mockito.ArgumentMatchers.argThat; |
|||
import static org.mockito.ArgumentMatchers.contains; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class SystemPatchApplierTest { |
|||
|
|||
@Mock |
|||
private JdbcTemplate jdbcTemplate; |
|||
|
|||
@Mock |
|||
private InstallScripts installScripts; |
|||
|
|||
@Mock |
|||
private WidgetTypeService widgetTypeService; |
|||
|
|||
@InjectMocks |
|||
private SystemPatchApplier reconciler; |
|||
|
|||
@TempDir |
|||
Path tempDir; |
|||
|
|||
@ParameterizedTest(name = "Parse version {0} should return major={1}, minor={2}, patch={3}") |
|||
@CsvSource({ |
|||
"4.2.1, 4, 2, 1, 0", |
|||
"4.2.0, 4, 2, 0, 0", |
|||
"4.2, 4, 2, 0, 0", |
|||
"4.0.1.2, 4, 0, 1, 2", |
|||
"4, 4, 0, 0, 0", |
|||
"1.0.5.7, 1, 0, 5, 7", |
|||
"10.20.30.40, 10, 20, 30, 40", |
|||
"0.0.1, 0, 0, 1, 0" |
|||
}) |
|||
void testParseVersion(String versionString, int expectedMajor, int expectedMinor, int expectedMaintenance, int expectedPatch) { |
|||
SystemPatchApplier.VersionInfo version = ReflectionTestUtils.invokeMethod(reconciler, "parseVersion", versionString); |
|||
|
|||
assertNotNull(version, "Version should not be null for: " + versionString); |
|||
assertEquals(expectedMajor, version.major(), "Major version mismatch"); |
|||
assertEquals(expectedMinor, version.minor(), "Minor version mismatch"); |
|||
assertEquals(expectedMaintenance, version.maintenance(), "Maintenance version mismatch"); |
|||
assertEquals(expectedPatch, version.patch(), "Patch version mismatch"); |
|||
} |
|||
|
|||
@ParameterizedTest(name = "Parse invalid version: {0}") |
|||
@CsvSource({ |
|||
"invalid", |
|||
"a.b.c", |
|||
"1.2.y.x", |
|||
"''", |
|||
"1.x.3" |
|||
}) |
|||
void testParseInvalidVersion(String invalidVersion) { |
|||
SystemPatchApplier.VersionInfo version = ReflectionTestUtils.invokeMethod(reconciler, "parseVersion", invalidVersion); |
|||
assertNull(version, "Version should be null for invalid input: " + invalidVersion); |
|||
} |
|||
|
|||
@Test |
|||
void whenLockIsNotAcquired_thenAcquiredIsSuccess() { |
|||
when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())).thenReturn(true); |
|||
|
|||
Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); |
|||
|
|||
assertEquals(Boolean.TRUE, acquired); |
|||
verify(jdbcTemplate).queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong()); |
|||
} |
|||
|
|||
@Test |
|||
void whenLockIsAlreadyAcquired_thenAcquiredIsFailed() { |
|||
when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())).thenReturn(false); |
|||
|
|||
Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); |
|||
|
|||
assertNotEquals(Boolean.TRUE, acquired); |
|||
} |
|||
|
|||
@Test |
|||
void testReleaseAdvisoryLock() { |
|||
when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())) |
|||
.thenReturn(true); |
|||
|
|||
ReflectionTestUtils.invokeMethod(reconciler, "releaseAdvisoryLock"); |
|||
|
|||
verify(jdbcTemplate).queryForObject( |
|||
contains("pg_advisory_unlock"), eq(Boolean.class), anyLong()); |
|||
} |
|||
|
|||
@Test |
|||
void whenWidgetNotFound_thenThrowException() throws Exception { |
|||
Path widgetTypesDir = tempDir.resolve("widget_types"); |
|||
Files.createDirectories(widgetTypesDir); |
|||
when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); |
|||
|
|||
WidgetTypeDetails testWidget = createTestWidgetType("test_widget", "Test Widget"); |
|||
String json = JacksonUtil.toString(testWidget); |
|||
assertNotNull(json); |
|||
Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); |
|||
|
|||
when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")).thenReturn(null); |
|||
|
|||
assertThrows(RuntimeException.class, () -> ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes")); |
|||
} |
|||
|
|||
@Test |
|||
void whenDescriptorChanged_thenUpdateTheExistingWidget() throws Exception { |
|||
Path widgetTypesDir = tempDir.resolve("widget_types"); |
|||
Files.createDirectories(widgetTypesDir); |
|||
when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); |
|||
|
|||
WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "Test Widget"); |
|||
fileWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":2}")); |
|||
String json = JacksonUtil.toString(fileWidget); |
|||
assertNotNull(json); |
|||
Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); |
|||
|
|||
WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Test Widget"); |
|||
existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); |
|||
existingWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}")); |
|||
|
|||
when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")) |
|||
.thenReturn(existingWidget); |
|||
|
|||
Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); |
|||
|
|||
assertEquals(1, updated); |
|||
verify(widgetTypeService).saveWidgetType(argThat(w -> |
|||
w.getDescriptor().get("version").asInt() == 2 |
|||
)); |
|||
} |
|||
|
|||
@Test |
|||
void whenNameChanged_thenUpdateTheExistingWidget() throws Exception { |
|||
Path widgetTypesDir = tempDir.resolve("widget_types"); |
|||
Files.createDirectories(widgetTypesDir); |
|||
when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); |
|||
|
|||
WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "New Name"); |
|||
String json = JacksonUtil.toString(fileWidget); |
|||
assertNotNull(json); |
|||
Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); |
|||
|
|||
WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Old Name"); |
|||
existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); |
|||
|
|||
when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")) |
|||
.thenReturn(existingWidget); |
|||
|
|||
Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); |
|||
|
|||
assertEquals(1, updated); |
|||
verify(widgetTypeService).saveWidgetType(argThat(w -> "New Name".equals(w.getName()))); |
|||
} |
|||
|
|||
@Test |
|||
void whenNothingChanged_thenSkipTheUpdateOfTheExistingWidget() throws Exception { |
|||
Path widgetTypesDir = tempDir.resolve("widget_types"); |
|||
Files.createDirectories(widgetTypesDir); |
|||
when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); |
|||
|
|||
WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "Test Widget"); |
|||
String json = JacksonUtil.toString(fileWidget); |
|||
assertNotNull(json); |
|||
Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); |
|||
|
|||
WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Test Widget"); |
|||
existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); |
|||
|
|||
when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")) |
|||
.thenReturn(existingWidget); |
|||
|
|||
Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); |
|||
|
|||
assertEquals(0, updated); |
|||
verify(widgetTypeService, never()).saveWidgetType(any()); |
|||
} |
|||
|
|||
@ParameterizedTest(name = "{0}") |
|||
@MethodSource("provideDescriptorComparisonTestCases") |
|||
void testIfDescriptorsAreEqual(String testName, JsonNode desc1, JsonNode desc2, boolean expectedEqual) { |
|||
Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isDescriptorEqual", desc1, desc2); |
|||
assertEquals(expectedEqual, result, testName); |
|||
} |
|||
|
|||
@Test |
|||
void whenDescriptorChanged_thenReturnWidgetTypeChanged() { |
|||
WidgetTypeDetails existing = createTestWidgetType("test", "Test"); |
|||
existing.setDescriptor(JacksonUtil.toJsonNode("{\"version\":1}")); |
|||
|
|||
WidgetTypeDetails file = createTestWidgetType("test", "Test"); |
|||
file.setDescriptor(JacksonUtil.toJsonNode("{\"version\":2}")); |
|||
|
|||
boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); |
|||
assertTrue(result); |
|||
} |
|||
|
|||
@Test |
|||
void whenNameChanged_thenReturnWidgetTypeChanged() { |
|||
WidgetTypeDetails existing = createTestWidgetType("test", "Old Name"); |
|||
WidgetTypeDetails file = createTestWidgetType("test", "New Name"); |
|||
|
|||
boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); |
|||
assertTrue(result); |
|||
} |
|||
|
|||
@Test |
|||
void whenDescriptionChanged_thenReturnWidgetTypeChanged() { |
|||
WidgetTypeDetails existing = createTestWidgetType("test", "Test"); |
|||
existing.setDescription("Old description"); |
|||
|
|||
WidgetTypeDetails file = createTestWidgetType("test", "Test"); |
|||
file.setDescription("New description"); |
|||
|
|||
boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); |
|||
assertTrue(result); |
|||
} |
|||
|
|||
@Test |
|||
void whenWidgetTypeAreIdentical_thenNoUpdateIsPerformed() { |
|||
WidgetTypeDetails existing = createTestWidgetType("test", "Test"); |
|||
WidgetTypeDetails file = createTestWidgetType("test", "Test"); |
|||
|
|||
boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); |
|||
assertFalse(result); |
|||
} |
|||
|
|||
@Test |
|||
void whenLockIsHeldByOneThread_thenSecondThreadCannotAcquireLock() throws Exception { |
|||
CountDownLatch lockAcquired = new CountDownLatch(1); |
|||
CountDownLatch startSecondThread = new CountDownLatch(1); |
|||
CountDownLatch testComplete = new CountDownLatch(1); |
|||
|
|||
AtomicBoolean firstThreadAcquiredLock = new AtomicBoolean(false); |
|||
AtomicBoolean secondThreadAcquiredLock = new AtomicBoolean(false); |
|||
AtomicBoolean firstThreadSavedWidget = new AtomicBoolean(false); |
|||
AtomicBoolean secondThreadSavedWidget = new AtomicBoolean(false); |
|||
|
|||
Path widgetTypesDir = tempDir.resolve("widget_types"); |
|||
Files.createDirectories(widgetTypesDir); |
|||
when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); |
|||
|
|||
WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "Test Widget"); |
|||
fileWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":2}")); |
|||
String toString = JacksonUtil.toCanonicalString(fileWidget); |
|||
assertNotNull(toString); |
|||
Files.writeString(widgetTypesDir.resolve("test_widget.json"), toString); |
|||
|
|||
WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Test Widget"); |
|||
existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); |
|||
existingWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}")); |
|||
|
|||
when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")).thenReturn(existingWidget); |
|||
|
|||
when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())) |
|||
.thenReturn(true) |
|||
.thenReturn(false); |
|||
|
|||
when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())) |
|||
.thenReturn(true); |
|||
|
|||
// The first thread-acquires lock and performs update
|
|||
Thread firstThread = new Thread(() -> { |
|||
try { |
|||
Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); |
|||
firstThreadAcquiredLock.set(Boolean.TRUE.equals(acquired)); |
|||
|
|||
if (firstThreadAcquiredLock.get()) { |
|||
lockAcquired.countDown(); |
|||
startSecondThread.await(5, TimeUnit.SECONDS); |
|||
|
|||
// Simulate work while holding lock
|
|||
Thread.sleep(100); |
|||
|
|||
Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); |
|||
firstThreadSavedWidget.set(updated != null && updated > 0); |
|||
|
|||
ReflectionTestUtils.invokeMethod(reconciler, "releaseAdvisoryLock"); |
|||
} |
|||
} catch (Exception ignored) { |
|||
} finally { |
|||
testComplete.countDown(); |
|||
} |
|||
}); |
|||
|
|||
// Second thread - attempts to acquire lock but fails
|
|||
Thread secondThread = new Thread(() -> { |
|||
try { |
|||
lockAcquired.await(5, TimeUnit.SECONDS); |
|||
startSecondThread.countDown(); |
|||
|
|||
Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); |
|||
secondThreadAcquiredLock.set(Boolean.TRUE.equals(acquired)); |
|||
|
|||
if (secondThreadAcquiredLock.get()) { |
|||
Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); |
|||
secondThreadSavedWidget.set(updated != null && updated > 0); |
|||
|
|||
ReflectionTestUtils.invokeMethod(reconciler, "releaseAdvisoryLock"); |
|||
} |
|||
} catch (Exception ignored) {} |
|||
}); |
|||
|
|||
firstThread.start(); |
|||
secondThread.start(); |
|||
|
|||
assertTrue(testComplete.await(10, TimeUnit.SECONDS), "Test should complete within timeout"); |
|||
firstThread.join(1000); |
|||
secondThread.join(1000); |
|||
|
|||
assertTrue(firstThreadAcquiredLock.get(), "First thread should acquire lock"); |
|||
assertFalse(secondThreadAcquiredLock.get(), "Second thread should NOT acquire lock"); |
|||
assertTrue(firstThreadSavedWidget.get(), "First thread should save widget"); |
|||
assertFalse(secondThreadSavedWidget.get(), "Second thread should NOT save widget"); |
|||
|
|||
verify(widgetTypeService, times(1)).saveWidgetType(any()); |
|||
} |
|||
|
|||
private static Stream<Arguments> provideDescriptorComparisonTestCases() { |
|||
return Stream.of( |
|||
Arguments.of("Both null", null, null, true), |
|||
Arguments.of("First null", null, JacksonUtil.newObjectNode(), false), |
|||
Arguments.of("Second null", JacksonUtil.newObjectNode(), null, false), |
|||
Arguments.of("Same content", |
|||
JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), |
|||
JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), |
|||
true), |
|||
Arguments.of("Different content", |
|||
JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), |
|||
JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":2}"), |
|||
false), |
|||
Arguments.of("Different key order but same content", |
|||
JacksonUtil.toJsonNode("{\"version\":1,\"type\":\"latest\"}"), |
|||
JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), |
|||
true), |
|||
Arguments.of("Empty objects", |
|||
JacksonUtil.toJsonNode("{}"), |
|||
JacksonUtil.toJsonNode("{}"), |
|||
true) |
|||
); |
|||
} |
|||
|
|||
private WidgetTypeDetails createTestWidgetType(String fqn, String name) { |
|||
WidgetTypeDetails widget = new WidgetTypeDetails(); |
|||
widget.setFqn(fqn); |
|||
widget.setName(name); |
|||
widget.setDescription("Test description"); |
|||
widget.setTenantId(TenantId.SYS_TENANT_ID); |
|||
widget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\"}")); |
|||
return widget; |
|||
} |
|||
|
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.common.data.id; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
@Schema |
|||
public class CalculatedFieldLinkId extends UUIDBased implements EntityId { |
|||
|
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
@JsonCreator |
|||
public CalculatedFieldLinkId(@JsonProperty("id") UUID id) { |
|||
super(id); |
|||
} |
|||
|
|||
public static CalculatedFieldLinkId fromString(String calculatedFieldLinkId) { |
|||
return new CalculatedFieldLinkId(UUID.fromString(calculatedFieldLinkId)); |
|||
} |
|||
|
|||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD_LINK", allowableValues = "CALCULATED_FIELD_LINK") |
|||
@Override |
|||
public EntityType getEntityType() { |
|||
return EntityType.CALCULATED_FIELD_LINK; |
|||
} |
|||
|
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.dao.cf; |
|||
|
|||
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; |
|||
import org.thingsboard.server.common.data.id.CalculatedFieldId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.dao.Dao; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface CalculatedFieldLinkDao extends Dao<CalculatedFieldLink> { |
|||
|
|||
List<CalculatedFieldLink> findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId); |
|||
|
|||
List<CalculatedFieldLink> findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId); |
|||
|
|||
List<CalculatedFieldLink> findCalculatedFieldLinksByTenantId(TenantId tenantId); |
|||
|
|||
List<CalculatedFieldLink> findAll(); |
|||
|
|||
PageData<CalculatedFieldLink> findAll(PageLink pageLink); |
|||
|
|||
PageData<CalculatedFieldLink> findAllByTenantId(TenantId tenantId, PageLink pageLink); |
|||
|
|||
} |
|||
@ -1,79 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.dao.model.sql; |
|||
|
|||
import jakarta.persistence.Column; |
|||
import jakarta.persistence.Entity; |
|||
import jakarta.persistence.Table; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; |
|||
import org.thingsboard.server.common.data.id.CalculatedFieldId; |
|||
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; |
|||
import org.thingsboard.server.common.data.id.EntityIdFactory; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.dao.model.BaseEntity; |
|||
import org.thingsboard.server.dao.model.BaseSqlEntity; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_ID; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_TYPE; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TABLE_NAME; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TENANT_ID_COLUMN; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@Entity |
|||
@Table(name = CALCULATED_FIELD_LINK_TABLE_NAME) |
|||
public class CalculatedFieldLinkEntity extends BaseSqlEntity<CalculatedFieldLink> implements BaseEntity<CalculatedFieldLink> { |
|||
|
|||
@Column(name = CALCULATED_FIELD_LINK_TENANT_ID_COLUMN) |
|||
private UUID tenantId; |
|||
|
|||
@Column(name = CALCULATED_FIELD_LINK_ENTITY_TYPE) |
|||
private String entityType; |
|||
|
|||
@Column(name = CALCULATED_FIELD_LINK_ENTITY_ID) |
|||
private UUID entityId; |
|||
|
|||
@Column(name = CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID) |
|||
private UUID calculatedFieldId; |
|||
|
|||
public CalculatedFieldLinkEntity() { |
|||
super(); |
|||
} |
|||
|
|||
public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) { |
|||
super(calculatedFieldLink); |
|||
this.tenantId = calculatedFieldLink.getTenantId().getId(); |
|||
this.entityType = calculatedFieldLink.getEntityId().getEntityType().name(); |
|||
this.entityId = calculatedFieldLink.getEntityId().getId(); |
|||
this.calculatedFieldId = calculatedFieldLink.getCalculatedFieldId().getId(); |
|||
} |
|||
|
|||
@Override |
|||
public CalculatedFieldLink toData() { |
|||
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(new CalculatedFieldLinkId(id)); |
|||
calculatedFieldLink.setCreatedTime(createdTime); |
|||
calculatedFieldLink.setTenantId(TenantId.fromUUID(tenantId)); |
|||
calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); |
|||
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId)); |
|||
return calculatedFieldLink; |
|||
} |
|||
|
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.dao.service.validator; |
|||
|
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; |
|||
import org.thingsboard.server.dao.exception.DataValidationException; |
|||
import org.thingsboard.server.dao.service.DataValidator; |
|||
|
|||
@Component |
|||
public class CalculatedFieldLinkDataValidator extends DataValidator<CalculatedFieldLink> { |
|||
|
|||
@Autowired |
|||
private CalculatedFieldLinkDao calculatedFieldLinkDao; |
|||
|
|||
@Override |
|||
protected CalculatedFieldLink validateUpdate(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) { |
|||
CalculatedFieldLink old = calculatedFieldLinkDao.findById(calculatedFieldLink.getTenantId(), calculatedFieldLink.getId().getId()); |
|||
if (old == null) { |
|||
throw new DataValidationException("Can't update non existing calculated field link!"); |
|||
} |
|||
return old; |
|||
} |
|||
|
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.dao.sql.cf; |
|||
|
|||
import org.springframework.data.domain.Page; |
|||
import org.springframework.data.domain.Pageable; |
|||
import org.springframework.data.jpa.repository.JpaRepository; |
|||
import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
public interface CalculatedFieldLinkRepository extends JpaRepository<CalculatedFieldLinkEntity, UUID> { |
|||
|
|||
List<CalculatedFieldLinkEntity> findAllByTenantIdAndCalculatedFieldId(UUID tenantId, UUID calculatedFieldId); |
|||
|
|||
List<CalculatedFieldLinkEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); |
|||
|
|||
List<CalculatedFieldLinkEntity> findAllByTenantId(UUID tenantId); |
|||
|
|||
Page<CalculatedFieldLinkEntity> findAllByTenantId(UUID tenantId, Pageable pageable); |
|||
|
|||
} |
|||
@ -1,94 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.dao.sql.cf; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.data.jpa.repository.JpaRepository; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; |
|||
import org.thingsboard.server.common.data.id.CalculatedFieldId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.dao.DaoUtil; |
|||
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; |
|||
import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity; |
|||
import org.thingsboard.server.dao.sql.JpaAbstractDao; |
|||
import org.thingsboard.server.dao.util.SqlDao; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@AllArgsConstructor |
|||
@SqlDao |
|||
public class JpaCalculatedFieldLinkDao extends JpaAbstractDao<CalculatedFieldLinkEntity, CalculatedFieldLink> implements CalculatedFieldLinkDao { |
|||
|
|||
private final CalculatedFieldLinkRepository calculatedFieldLinkRepository; |
|||
private final NativeCalculatedFieldRepository nativeCalculatedFieldRepository; |
|||
|
|||
@Override |
|||
public List<CalculatedFieldLink> findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId) { |
|||
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndCalculatedFieldId(tenantId.getId(), calculatedFieldId.getId())); |
|||
} |
|||
|
|||
@Override |
|||
public List<CalculatedFieldLink> findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) { |
|||
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId())); |
|||
} |
|||
|
|||
@Override |
|||
public List<CalculatedFieldLink> findCalculatedFieldLinksByTenantId(TenantId tenantId) { |
|||
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId())); |
|||
} |
|||
|
|||
@Override |
|||
public List<CalculatedFieldLink> findAll() { |
|||
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAll()); |
|||
} |
|||
|
|||
@Override |
|||
public PageData<CalculatedFieldLink> findAll(PageLink pageLink) { |
|||
log.debug("Try to find calculated field links by pageLink [{}]", pageLink); |
|||
return nativeCalculatedFieldRepository.findCalculatedFieldLinks(DaoUtil.toPageable(pageLink)); |
|||
} |
|||
|
|||
@Override |
|||
public PageData<CalculatedFieldLink> findAllByTenantId(TenantId tenantId, PageLink pageLink) { |
|||
log.debug("Try to find calculated field links by tenantId [{}], pageLink [{}]", tenantId, pageLink); |
|||
return DaoUtil.toPageData(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); |
|||
} |
|||
|
|||
@Override |
|||
protected Class<CalculatedFieldLinkEntity> getEntityClass() { |
|||
return CalculatedFieldLinkEntity.class; |
|||
} |
|||
|
|||
@Override |
|||
protected JpaRepository<CalculatedFieldLinkEntity, UUID> getRepository() { |
|||
return calculatedFieldLinkRepository; |
|||
} |
|||
|
|||
@Override |
|||
public EntityType getEntityType() { |
|||
return EntityType.CALCULATED_FIELD_LINK; |
|||
} |
|||
|
|||
} |
|||
@ -1,57 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.dao.service.validator; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.boot.test.mock.mockito.SpyBean; |
|||
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; |
|||
import org.thingsboard.server.common.data.id.CalculatedFieldId; |
|||
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; |
|||
import org.thingsboard.server.dao.exception.DataValidationException; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|||
import static org.mockito.BDDMockito.given; |
|||
|
|||
@SpringBootTest(classes = CalculatedFieldLinkDataValidator.class) |
|||
public class CalculatedFieldLinkDataValidatorTest { |
|||
|
|||
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("2ba09d99-6143-43dc-b645-381fc0c43ebe")); |
|||
private final CalculatedFieldLinkId CALCULATED_FIELD_LINK_ID = new CalculatedFieldLinkId(UUID.fromString("a5609ef4-cb42-43ce-9b23-e090a4878d1c")); |
|||
|
|||
@MockBean |
|||
private CalculatedFieldLinkDao calculatedFieldLinkDao; |
|||
@SpyBean |
|||
private CalculatedFieldLinkDataValidator validator; |
|||
|
|||
@Test |
|||
public void testUpdateNonExistingCalculatedField() { |
|||
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(CALCULATED_FIELD_LINK_ID); |
|||
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(UUID.fromString("136477af-fd07-4498-b9c9-54fe50e82992"))); |
|||
|
|||
given(calculatedFieldLinkDao.findById(TENANT_ID, CALCULATED_FIELD_LINK_ID.getId())).willReturn(null); |
|||
|
|||
assertThatThrownBy(() -> validator.validateUpdate(TENANT_ID, calculatedFieldLink)) |
|||
.isInstanceOf(DataValidationException.class) |
|||
.hasMessage("Can't update non existing calculated field link!"); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,128 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.msa.connectivity; |
|||
|
|||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; |
|||
import org.apache.hc.client5.http.impl.classic.HttpClients; |
|||
import org.apache.hc.client5.http.io.HttpClientConnectionManager; |
|||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; |
|||
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; |
|||
import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy; |
|||
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; |
|||
import org.apache.hc.core5.ssl.SSLContexts; |
|||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; |
|||
import org.springframework.web.client.RestTemplate; |
|||
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; |
|||
import org.testng.annotations.AfterMethod; |
|||
import org.testng.annotations.BeforeClass; |
|||
import org.testng.annotations.BeforeMethod; |
|||
import org.testng.annotations.Test; |
|||
import org.thingsboard.rest.client.RestClient; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.alarm.Alarm; |
|||
import org.thingsboard.server.common.data.alarm.AlarmInfo; |
|||
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; |
|||
import org.thingsboard.server.common.data.alarm.AlarmSeverity; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.TimePageLink; |
|||
import org.thingsboard.server.msa.AbstractContainerTest; |
|||
import org.thingsboard.server.msa.TestProperties; |
|||
|
|||
import javax.net.ssl.SSLContext; |
|||
import java.util.List; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; |
|||
|
|||
public class JavaRestClientTest extends AbstractContainerTest { |
|||
|
|||
private RestClient restClient; |
|||
|
|||
@BeforeClass |
|||
public void beforeClass() throws Exception { |
|||
SSLContext ssl = SSLContexts.custom() |
|||
.loadTrustMaterial((chain, authType) -> true) |
|||
.build(); |
|||
|
|||
var tls = new DefaultClientTlsStrategy( |
|||
ssl, |
|||
HostnameVerificationPolicy.CLIENT, |
|||
NoopHostnameVerifier.INSTANCE |
|||
); |
|||
|
|||
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() |
|||
.setTlsSocketStrategy(tls) |
|||
.build(); |
|||
|
|||
CloseableHttpClient httpClient = HttpClients.custom() |
|||
.setConnectionManager(cm) |
|||
.build(); |
|||
|
|||
RestTemplate rt = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); |
|||
restClient = new RestClient(rt, TestProperties.getBaseUrl()); |
|||
} |
|||
|
|||
@BeforeMethod |
|||
public void setUp() throws Exception { |
|||
restClient.login("tenant@thingsboard.org", "tenant"); |
|||
} |
|||
|
|||
@AfterMethod |
|||
public void tearDown() { |
|||
} |
|||
|
|||
@Test |
|||
public void testGetAlarmsV2() { |
|||
Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5))); |
|||
assertThat(device).isNotNull(); |
|||
|
|||
String type = "High temp" + RandomStringUtils.randomAlphabetic(5); |
|||
Alarm alarm = Alarm.builder() |
|||
.originator(device.getId()) |
|||
.severity(AlarmSeverity.CRITICAL) |
|||
.type(type) |
|||
.build(); |
|||
restClient.saveAlarm(alarm); |
|||
|
|||
// get /api/v2/alarm
|
|||
PageData<AlarmInfo> alarmsV2 = restClient.getAlarmsV2(device.getId(), null, null, List.of(type), null, new TimePageLink(10, 0)); |
|||
assertThat(alarmsV2.getData()).hasSize(1); |
|||
|
|||
PageData<AlarmInfo> activeAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); |
|||
assertThat(activeAlarms.getData()).hasSize(1); |
|||
|
|||
PageData<AlarmInfo> cleared = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); |
|||
assertThat(cleared.getData()).hasSize(0); |
|||
|
|||
PageData<AlarmInfo> activeAndClearedAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACTIVE), null, null, null, new TimePageLink(10, 0)); |
|||
assertThat(activeAndClearedAlarms.getData()).hasSize(1); |
|||
|
|||
// get /api/v2/alarms
|
|||
PageData<AlarmInfo> allAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); |
|||
assertThat(allAlarmsV2.getData()).hasSize(1); |
|||
|
|||
PageData<AlarmInfo> allClearedAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); |
|||
assertThat(allClearedAlarmsV2.getData()).hasSize(0); |
|||
|
|||
// get /api/alarms
|
|||
PageData<AlarmInfo> allAlarms = restClient.getAllAlarms(AlarmSearchStatus.ACTIVE, null, new TimePageLink(10, 0), null); |
|||
assertThat(allAlarms.getData()).hasSize(1); |
|||
|
|||
PageData<AlarmInfo> allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null); |
|||
assertThat(allClearedAlarms.getData()).hasSize(0); |
|||
|
|||
} |
|||
} |
|||
File diff suppressed because it is too large
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue