Browse Source

Merge remote-tracking branch 'origin/lts-4.3' into rc

pull/15437/head
Viacheslav Klimov 2 months ago
parent
commit
e5ad6d021c
Failed to extract signature
  1. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java
  3. 126
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  4. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  5. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
  6. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  8. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  10. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  11. 35
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  12. 4
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java
  13. 47
      application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
  14. 517
      application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java
  15. 159
      application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java
  16. 160
      application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java
  17. 13
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
  18. 22
      dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java
  19. 20
      pom.xml
  20. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
  21. 2
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts
  22. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java

@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId;
@ -77,6 +78,7 @@ public class AlarmCommentController extends BaseController {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmInfoId(alarmId, Operation.WRITE);
alarmComment.setAlarmId(alarmId);
alarmComment.setType(AlarmCommentType.OTHER);
return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser());
}

2
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java

@ -61,7 +61,7 @@ public interface CalculatedFieldCache {
void addOwnerEntity(TenantId tenantId, EntityId entityId);
void evictEntity(EntityId entityId);
void evictOwnerEntity(EntityId entityId);
void evictOwner(EntityId owner);

126
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.server.actors.ActorSystemContext;
@ -37,6 +38,8 @@ import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.AfterStartUp;
@ -45,7 +48,9 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -53,6 +58,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
@ -268,18 +274,25 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
@Override
public void updateOwnerEntity(TenantId tenantId, EntityId entityId) {
evictEntity(entityId);
evictOwnerEntity(entityId);
addOwnerEntity(tenantId, entityId);
}
@Override
public void evictEntity(EntityId entityId) {
public void evictOwnerEntity(EntityId entityId) {
ownerEntities.values().forEach(entities -> entities.remove(entityId));
}
@Override
public void evictOwner(EntityId owner) {
ownerEntities.remove(owner);
Set<EntityId> removedEntities = ownerEntities.remove(owner);
if (removedEntities != null) {
Set<EntityId> removedCustomers = removedEntities
.stream()
.filter(entityId -> entityId.getEntityType() == EntityType.CUSTOMER)
.collect(Collectors.toSet());
removedCustomers.forEach(this::evictOwner);
}
}
private Set<EntityId> getOwnedEntities(TenantId tenantId, EntityId ownerId) {
@ -290,6 +303,113 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
});
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT_PROFILE:
if (event.getEvent() == ComponentLifecycleEvent.UPDATED) {
TenantProfileId tenantProfileId = new TenantProfileId(event.getEntityId().getId());
handleTenantProfileUpdate(tenantProfileId);
}
break;
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
evictTenantCfs(tenantId);
evictOwner(tenantId);
}
break;
case CUSTOMER:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) {
updateOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictOwner(event.getEntityId());
evictOwnerEntity(event.getEntityId());
}
break;
case DEVICE, ASSET:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) {
updateOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictOwnerEntity(event.getEntityId());
evictEntityCfs(event.getEntityId());
}
break;
case DEVICE_PROFILE, ASSET_PROFILE:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictEntityCfs(event.getEntityId());
}
break;
case CALCULATED_FIELD:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED) {
updateCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
} else {
evict((CalculatedFieldId) event.getEntityId());
}
break;
}
}
private void evictTenantCfs(TenantId tenantId) {
var removedCfEntityIds = new HashSet<EntityId>();
var removedLinkEntityIds = new HashSet<EntityId>();
var toRemove = calculatedFields.entrySet().stream()
.filter(e -> e.getValue().getTenantId().equals(tenantId))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
toRemove.forEach(cfId -> {
CalculatedField cf = calculatedFields.remove(cfId);
List<CalculatedFieldLink> links = calculatedFieldLinks.remove(cfId);
if (links != null) {
links.forEach(link -> removedLinkEntityIds.add(link.entityId()));
}
calculatedFieldsCtx.remove(cfId);
if (cf != null) {
removedCfEntityIds.add(cf.getEntityId());
}
});
removedCfEntityIds.forEach(entityId -> {
entityIdCalculatedFields.compute(entityId, (k, cfs) -> {
if (cfs != null) {
cfs.removeIf(cf -> toRemove.contains(cf.getId()));
return cfs.isEmpty() ? null : cfs;
}
return null;
});
});
removedLinkEntityIds.forEach(entityId -> {
entityIdCalculatedFieldLinks.compute(entityId, ((entityId1, links) -> {
if (links != null) {
links.removeIf(link -> toRemove.contains(link.calculatedFieldId()));
return links.isEmpty() ? null : links;
}
return null;
}));
});
}
private void evictEntityCfs(EntityId entityId) {
List<CalculatedField> cfs = entityIdCalculatedFields.remove(entityId);
if (cfs != null) {
var cfIds = new HashSet<CalculatedFieldId>();
cfs.forEach(cf -> {
calculatedFields.remove(cf.getId());
calculatedFieldLinks.remove(cf.getId());
calculatedFieldsCtx.remove(cf.getId());
cfIds.add(cf.getId());
log.debug("[{}] evict calculated field from cache on entity deletion: {}", cf.getId(), cf);
});
entityIdCalculatedFieldLinks.values().forEach(list -> list.removeIf(link -> cfIds.contains(link.calculatedFieldId())));
}
entityIdCalculatedFieldLinks.remove(entityId);
}
private Lock getFetchLock(CalculatedFieldId id) {
return calculatedFieldFetchLocks.computeIfAbsent(id, __ -> new ReentrantLock());
}

3
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -305,7 +305,8 @@ public class CalculatedFieldCtx implements Closeable {
public void setTenantProfileProperties() {
TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenantId);
if (tenantProfile == null) {
throw new IllegalStateException("Tenant Profile not found for tenant: " + tenantId);
log.warn("[{}][{}][{}] Tenant Profile not found for tenant: {}. CF limits and thresholds will not be updated.", tenantId, entityId, cfId, tenantId);
return;
}
tenantProfile.getProfileConfiguration().ifPresent(config -> {
this.maxStateSize = config.getMaxStateSizeInKBytes() * 1024L;

24
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
@ -23,15 +24,19 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Service
@Slf4j
@ -143,6 +148,25 @@ public class DefaultTbAssetProfileCache implements TbAssetProfileCache {
}
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
Set<AssetProfileId> toRemove = assetProfilesMap.values().stream()
.filter(assetProfile -> assetProfile.getTenantId().equals(tenantId))
.map(AssetProfile::getId)
.collect(Collectors.toSet());
assetProfilesMap.keySet().removeAll(toRemove);
assetsMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
profileListeners.remove(tenantId);
assetProfileListeners.remove(tenantId);
}
break;
}
}
private void notifyProfileListeners(AssetProfile profile) {
ConcurrentMap<EntityId, Consumer<AssetProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

24
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -23,15 +24,19 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Service
@Slf4j
@ -143,6 +148,25 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
}
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
Set<DeviceProfileId> toRemove = deviceProfilesMap.values().stream()
.filter(deviceProfile -> deviceProfile.getTenantId().equals(tenantId))
.map(DeviceProfile::getId)
.collect(Collectors.toSet());
deviceProfilesMap.keySet().removeAll(toRemove);
devicesMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
profileListeners.remove(tenantId);
deviceProfileListeners.remove(tenantId);
}
break;
}
}
private void notifyProfileListeners(DeviceProfile profile) {
ConcurrentMap<EntityId, Consumer<DeviceProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java

@ -51,7 +51,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
@ -91,9 +90,8 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache,
CalculatedFieldStateService stateService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.queueFactory = tbQueueFactory;
this.stateService = stateService;

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -86,7 +86,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.notification.NotificationSchedulerService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -179,9 +178,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TbImageService imageService,
TbResourceDataCache tbResourceDataCache,
RuleEngineCallService ruleEngineCallService,
CalculatedFieldCache calculatedFieldCache,
EdqsService edqsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService;

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java

@ -87,7 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
super(actorContext, null, null, null, null, null, null, null, null, null);
super(actorContext, null, null, null, null, null, null, null, null);
this.edgeCtx = edgeCtx;
this.stats = new EdgeConsumerStats(statsFactory);
this.queueFactory = tbCoreQueueFactory;

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -46,7 +46,6 @@ import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedConsumerService;
@ -84,9 +83,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService;

35
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -26,7 +26,6 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -47,7 +46,6 @@ import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.TbPackCallback;
@ -75,7 +73,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbDeviceProfileCache deviceProfileCache;
protected final TbAssetProfileCache assetProfileCache;
protected final TbResourceDataCache tbResourceDataCache;
protected final CalculatedFieldCache calculatedFieldCache;
protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService;
protected final ApplicationEventPublisher eventPublisher;
@ -166,7 +163,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
tenantProfileCache.evict(tenantProfileId);
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
calculatedFieldCache.handleTenantProfileUpdate(tenantProfileId);
}
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
@ -179,7 +175,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
apiUsageStateService.onTenantUpdate(tenantId);
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
apiUsageStateService.onTenantDelete(tenantId);
calculatedFieldCache.evictOwner(tenantId);
partitionService.removeTenant(tenantId);
}
}
@ -187,45 +182,17 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
deviceProfileCache.evict(tenantId, new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(tenantId, new DeviceId(componentLifecycleMsg.getEntityId().getId()));
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.ASSET_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(tenantId, new AssetProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.ASSET.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(tenantId, new AssetId(componentLifecycleMsg.getEntityId().getId()));
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.ENTITY_VIEW.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
actorContext.getTbEntityViewService().onComponentLifecycleMsg(componentLifecycleMsg);
} else if (EntityType.API_USAGE_STATE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
apiUsageStateService.onApiUsageStateUpdate(tenantId);
} else if (EntityType.CUSTOMER.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
apiUsageStateService.onCustomerDelete((CustomerId) componentLifecycleMsg.getEntityId());
calculatedFieldCache.evictOwner(componentLifecycleMsg.getEntityId());
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.CALCULATED_FIELD.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.CREATED) {
calculatedFieldCache.addCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.UPDATED) {
calculatedFieldCache.updateCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else {
calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId());
}
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tbResourceDataCache.evictResourceData(tenantId, new TbResourceId(componentLifecycleMsg.getEntityId().getId()));

4
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java

@ -24,7 +24,6 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
@ -45,12 +44,11 @@ public abstract class AbstractPartitionBasedConsumerService<N extends com.google
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
CalculatedFieldCache calculatedFieldCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
}
@PostConstruct

47
application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java

@ -44,7 +44,9 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -226,6 +228,18 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId())
.andExpect(status().isOk());
Optional<AlarmCommentInfo> systemCommentOpt = doGetTyped(
"/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=10", new TypeReference<PageData<AlarmCommentInfo>>() {
}
).getData().stream().filter(alarmCommentInfo -> alarmCommentInfo.getType().equals(AlarmCommentType.SYSTEM)).findFirst();
assertThat(systemCommentOpt).isPresent();
AlarmCommentInfo systemComment = systemCommentOpt.get();
assertThat(systemComment.getId()).isEqualTo(alarmComment.getId());
assertThat(systemComment.getType()).isEqualTo(AlarmCommentType.SYSTEM);
assertThat(systemComment.getComment().get("text").asText()).isEqualTo(String.format("User %s deleted his comment",
TENANT_ADMIN_EMAIL));
AlarmComment expectedAlarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
@ -361,6 +375,39 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
Assert.assertTrue("Created alarm doesn't match the found one!", equals);
}
@Test
public void testShouldNotCreateOrUpdateSystemAlarmComment() throws Exception {
loginTenantAdmin();
AlarmComment alarmComment = AlarmComment.builder()
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().set("text", new TextNode("Acknowledged by tenant admin")))
.build();
AlarmComment created = doPost("/api/alarm/" + alarm.getId() + "/comment", alarmComment, AlarmComment.class);
assertThat(created.getType()).isEqualTo(AlarmCommentType.OTHER);
// acknowledge alarm to create system comment
doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isOk());
Optional<AlarmCommentInfo> systemCommentOpt = doGetTyped(
"/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=10", new TypeReference<PageData<AlarmCommentInfo>>() {
}
).getData().stream().filter(alarmCommentInfo -> alarmCommentInfo.getType().equals(AlarmCommentType.SYSTEM)).findFirst();
assertThat(systemCommentOpt).isPresent();
AlarmCommentInfo systemComment = systemCommentOpt.get();
// system comment can't be updated with other type
systemComment.setType(AlarmCommentType.OTHER);
doPost("/api/alarm/" + alarm.getId() + "/comment", systemComment).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("System alarm comment can't be updated!")));
// system comment can't be updated with other text
systemComment.setType(AlarmCommentType.SYSTEM);
systemComment.setComment(JacksonUtil.newObjectNode().set("text", new TextNode("New system comment")));
doPost("/api/alarm/" + alarm.getId() + "/comment", systemComment).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("System alarm comment can't be updated!")));
}
private AlarmComment createAlarmComment(AlarmId alarmId, String text) {
AlarmComment alarmComment = AlarmComment.builder()
.comment(JacksonUtil.newObjectNode().set("text", new TextNode(text)))

517
application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java

@ -0,0 +1,517 @@
/**
* Copyright © 2016-2026 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.cf;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DefaultCalculatedFieldCacheTest {
@Mock
private CalculatedFieldService calculatedFieldService;
@Mock
private TbAssetProfileCache assetProfileCache;
@Mock
private TbDeviceProfileCache deviceProfileCache;
@Mock
private TbTenantProfileCache tenantProfileCache;
@Mock
private DeviceService deviceService;
@Mock
private AssetService assetService;
@Mock
private CustomerService customerService;
private DefaultCalculatedFieldCache cache;
@BeforeEach
public void setUp() {
// ActorSystemContext is only used in getCalculatedFieldCtx (not tested here), so null is safe
OwnerService ownerService = new OwnerService(deviceService, assetService, customerService);
cache = new DefaultCalculatedFieldCache(calculatedFieldService, assetProfileCache,
deviceProfileCache, tenantProfileCache, null, ownerService);
}
// --- Tenant deletion tests ---
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsAllTenantCfsFromAllMaps() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
DeviceId device1 = new DeviceId(UUID.randomUUID());
DeviceId device2 = new DeviceId(UUID.randomUUID());
CalculatedField cf1 = addCfToCache(tenant1, device1);
CalculatedField cf2 = addCfToCache(tenant2, device2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf1.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(device1)).isEmpty();
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
assertThat(cache.getCalculatedFieldsByEntityId(device2)).containsExactly(cf2);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsOwnerEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, tenant);
cache.addOwnerEntity(tenant, device);
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// After eviction, getDynamicEntities triggers a fresh load from ownerService (empty)
assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(device);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_removesLinksToLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId cfEntity = new DeviceId(UUID.randomUUID());
DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, cfEntity, linkedDevice);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
assertThat(cache.getCalculatedField(cf.getId())).isNull();
}
@Test
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
}
// --- Device/Asset deletion tests ---
@Test
public void onComponentLifecycleEvent_deviceDeleted_evictsCfsForThatDevice() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceDeleted_removesLinksForLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
addCfToCache(tenant, device, linkedDevice);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceDeleted_evictsDeviceFromOwnerEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
cache.addOwnerEntity(tenant, device);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
@Test
public void onComponentLifecycleEvent_assetDeleted_evictsCfsForThatAsset() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetId asset = new AssetId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, asset);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, asset, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(asset)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceCreated_addsDeviceToOwnerEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.CREATED));
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
}
// --- Customer deletion tests ---
@Test
public void onComponentLifecycleEvent_customerDeleted_evictsCustomerOwnerEntries() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
cache.addOwnerEntity(tenant, device);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED));
// The customer's owned-entities entry is evicted; fresh load returns empty
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
// --- DeviceProfile/AssetProfile deletion tests ---
@Test
public void onComponentLifecycleEvent_deviceProfileDeleted_evictsCfsForThatProfile() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceProfileDeleted_removesLinksForLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
addCfToCache(tenant, profileId, linkedDevice);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceProfileDeleted_doesNotEvictOtherProfilesCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profile1 = new DeviceProfileId(UUID.randomUUID());
DeviceProfileId profile2 = new DeviceProfileId(UUID.randomUUID());
CalculatedField cf1 = addCfToCache(tenant, profile1);
CalculatedField cf2 = addCfToCache(tenant, profile2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf1.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty();
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2);
}
@Test
public void onComponentLifecycleEvent_deviceProfileUpdated_doesNotEvictCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf);
}
@Test
public void onComponentLifecycleEvent_assetProfileDeleted_evictsCfsForThatProfile() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_assetProfileDeleted_removesLinksForLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
AssetId linkedAsset = new AssetId(UUID.randomUUID());
addCfToCache(tenant, profileId, linkedAsset);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedAsset)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_assetProfileDeleted_doesNotEvictOtherProfilesCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profile1 = new AssetProfileId(UUID.randomUUID());
AssetProfileId profile2 = new AssetProfileId(UUID.randomUUID());
CalculatedField cf1 = addCfToCache(tenant, profile1);
CalculatedField cf2 = addCfToCache(tenant, profile2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf1.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty();
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2);
}
@Test
public void onComponentLifecycleEvent_assetProfileUpdated_doesNotEvictCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf);
}
// --- CalculatedField lifecycle tests ---
@Test
public void onComponentLifecycleEvent_calculatedFieldCreated_addsCfToCache() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
CalculatedField cf = buildCalculatedField(cfId, tenant, device, simpleCfConfig());
when(calculatedFieldService.findById(tenant, cfId)).thenReturn(cf);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cfId, ComponentLifecycleEvent.CREATED));
assertThat(cache.getCalculatedField(cfId)).isEqualTo(cf);
assertThat(cache.getCalculatedFieldsByEntityId(device)).containsExactly(cf);
}
@Test
public void onComponentLifecycleEvent_calculatedFieldDeleted_evictsCfFromCache() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_calculatedFieldUpdated_refreshesCfInCache() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
CalculatedField updatedCf = buildCalculatedField(cf.getId(), tenant, device, simpleCfConfig());
updatedCf.setName("updated-name");
when(calculatedFieldService.findById(tenant, cf.getId())).thenReturn(updatedCf);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(updatedCf);
}
// --- evictOwner recursive traversal tests ---
@Test
public void evictOwner_customerDeleted_recursivelyEvictsDevicesOwnedByThatCustomer() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData());
// tenant owns customer (getOwner for CUSTOMER returns tenantId)
cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer}
cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device}
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
// deleting the customer evicts the customer key and recursively cleans its owned set
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED));
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
@Test
public void evictOwner_tenantDeleted_recursivelyEvictsCustomerAndItsOwnedDevices() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData());
cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer}
cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device}
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
// deleting the tenant: evictOwner(tenant) finds customer (CUSTOMER type) and recurses into it
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// both levels must be gone
assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(customer);
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
// --- TenantProfile lifecycle tests ---
@Test
public void onComponentLifecycleEvent_tenantProfileUpdated_callsHandleTenantProfileUpdate() {
TenantId tenant = new TenantId(UUID.randomUUID());
TenantProfileId profileId = new TenantProfileId(UUID.randomUUID());
DefaultCalculatedFieldCache spyCache = spy(cache);
spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
verify(spyCache).handleTenantProfileUpdate(profileId);
}
@Test
public void onComponentLifecycleEvent_tenantProfileDeleted_doesNotCallHandleTenantProfileUpdate() {
TenantId tenant = new TenantId(UUID.randomUUID());
TenantProfileId profileId = new TenantProfileId(UUID.randomUUID());
DefaultCalculatedFieldCache spyCache = spy(cache);
spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
verify(spyCache, never()).handleTenantProfileUpdate(any());
}
// --- Helpers ---
private void stubDeviceOwner(TenantId tenantId, DeviceId deviceId, EntityId ownerId) {
Device device = new Device();
device.setId(deviceId);
device.setTenantId(tenantId);
if (ownerId instanceof CustomerId customerId) {
device.setCustomerId(customerId);
}
// If ownerId is a TenantId, leaving customerId null means getOwnerId() returns tenantId
when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device);
// Stubs for getOwnedEntities iteration (empty pages — device is added explicitly)
when(deviceService.findDeviceInfosByFilter(any(), any())).thenReturn(PageData.emptyPageData());
when(assetService.findAssetsByTenantIdAndCustomerId(any(), any(), any())).thenReturn(PageData.emptyPageData());
if (ownerId instanceof TenantId) {
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData());
}
}
private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId) {
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, simpleCfConfig());
when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf);
cache.addCalculatedField(tenantId, cfId);
return cf;
}
private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId, EntityId linkedEntity) {
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
CalculatedFieldConfiguration config = linkedEntityCfConfig(tenantId, cfId, linkedEntity);
CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, config);
when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf);
cache.addCalculatedField(tenantId, cfId);
return cf;
}
private CalculatedField buildCalculatedField(CalculatedFieldId id, TenantId tenantId, EntityId entityId, CalculatedFieldConfiguration config) {
CalculatedField cf = new CalculatedField();
cf.setId(id);
cf.setTenantId(tenantId);
cf.setEntityId(entityId);
cf.setType(CalculatedFieldType.SIMPLE);
cf.setName("test-cf-" + id.getId());
cf.setConfiguration(config);
return cf;
}
private CalculatedFieldConfiguration simpleCfConfig() {
CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class);
when(config.getReferencedEntities()).thenReturn(Collections.emptySet());
when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(Collections.emptyList());
return config;
}
private CalculatedFieldConfiguration linkedEntityCfConfig(TenantId tenantId, CalculatedFieldId cfId, EntityId linkedEntity) {
CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class);
CalculatedFieldLink link = new CalculatedFieldLink(tenantId, linkedEntity, cfId);
when(config.getReferencedEntities()).thenReturn(Set.of(linkedEntity));
when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(List.of(link));
when(config.buildCalculatedFieldLink(any(), eq(linkedEntity), any())).thenReturn(link);
return config;
}
}

159
application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java

@ -0,0 +1,159 @@
/**
* Copyright © 2016-2026 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.profile;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DefaultTbAssetProfileCacheTest {
@Mock
private AssetProfileService assetProfileService;
@Mock
private AssetService assetService;
private DefaultTbAssetProfileCache cache;
@BeforeEach
public void setUp() {
cache = new DefaultTbAssetProfileCache(assetProfileService, assetService);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsAssetProfilesForThatTenant() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID());
AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
// After deletion tenant1 profile should be reloaded from service on next get
when(assetProfileService.findAssetProfileById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant1, profileId1)).isNull();
verify(assetProfileService, times(1)).findAssetProfileById(tenant2, profileId2);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsAssetMappingsForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
AssetId assetId = new AssetId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
loadAssetMappingIntoCache(tenant, assetId, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// After tenant deletion, asset-to-profile mapping should be gone; get() should try to reload
when(assetService.findAssetById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant, assetId)).isNull();
verify(assetService, times(2)).findAssetById(tenant, assetId); // once on load, once after eviction
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
EntityId listenerId = new AssetId(UUID.randomUUID());
AtomicInteger callCount = new AtomicInteger();
cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// Evicting a profile after tenant deletion should not trigger the removed listener
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.evict(tenant, profileId);
assertThat(callCount.get()).isZero();
}
@Test
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
// Profile should still be served from cache without hitting the service again
cache.get(tenant, profileId);
verify(assetProfileService, times(1)).findAssetProfileById(tenant, profileId);
}
@Test
public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID());
AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID());
AssetProfile profile1 = loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED));
assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1);
verify(assetProfileService, times(1)).findAssetProfileById(tenant1, profileId1);
}
// --- Helpers ---
private AssetProfile loadProfileIntoCache(TenantId tenantId, AssetProfileId profileId) {
AssetProfile profile = new AssetProfile();
profile.setId(profileId);
profile.setTenantId(tenantId);
when(assetProfileService.findAssetProfileById(tenantId, profileId)).thenReturn(profile);
cache.get(tenantId, profileId);
return profile;
}
private void loadAssetMappingIntoCache(TenantId tenantId, AssetId assetId, AssetProfileId profileId) {
Asset asset = new Asset();
asset.setId(assetId);
asset.setAssetProfileId(profileId);
when(assetService.findAssetById(tenantId, assetId)).thenReturn(asset);
cache.get(tenantId, assetId);
}
}

160
application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java

@ -0,0 +1,160 @@
/**
* Copyright © 2016-2026 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.profile;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DefaultTbDeviceProfileCacheTest {
@Mock
private DeviceProfileService deviceProfileService;
@Mock
private DeviceService deviceService;
private DefaultTbDeviceProfileCache cache;
@BeforeEach
public void setUp() {
cache = new DefaultTbDeviceProfileCache(deviceProfileService, deviceService);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceProfilesForThatTenant() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID());
DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
// After deletion tenant1 profile should be reloaded from service on next get
when(deviceProfileService.findDeviceProfileById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant1, profileId1)).isNull();
// tenant2 profile should still be served from cache (no extra service call)
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant2, profileId2);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceMappingsForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
DeviceId deviceId = new DeviceId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
loadDeviceMappingIntoCache(tenant, deviceId, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// After tenant deletion, device-to-profile mapping should be gone; get() should try to reload
when(deviceService.findDeviceById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant, deviceId)).isNull();
verify(deviceService, times(2)).findDeviceById(tenant, deviceId); // once on load, once after eviction
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
EntityId listenerId = new DeviceId(UUID.randomUUID());
AtomicInteger callCount = new AtomicInteger();
cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// Evicting a profile after tenant deletion should not trigger the removed listener
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.evict(tenant, profileId);
assertThat(callCount.get()).isZero();
}
@Test
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
// Profile should still be served from cache without hitting the service again
cache.get(tenant, profileId);
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant, profileId);
}
@Test
public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID());
DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID());
DeviceProfile profile1 = loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED));
assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1);
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant1, profileId1);
}
// --- Helpers ---
private DeviceProfile loadProfileIntoCache(TenantId tenantId, DeviceProfileId profileId) {
DeviceProfile profile = new DeviceProfile();
profile.setId(profileId);
profile.setTenantId(tenantId);
when(deviceProfileService.findDeviceProfileById(tenantId, profileId)).thenReturn(profile);
cache.get(tenantId, profileId);
return profile;
}
private void loadDeviceMappingIntoCache(TenantId tenantId, DeviceId deviceId, DeviceProfileId profileId) {
Device device = new Device();
device.setId(deviceId);
device.setDeviceProfileId(profileId);
when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device);
cache.get(tenantId, deviceId);
}
}

13
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java

@ -48,13 +48,13 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
@Override
public AlarmComment createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment) {
alarmCommentDataValidator.validate(alarmComment, c -> tenantId);
AlarmComment oldAlarmComment = alarmCommentDataValidator.validate(alarmComment, c -> tenantId);
boolean isCreated = alarmComment.getId() == null;
AlarmComment result;
if (isCreated) {
result = createAlarmComment(tenantId, alarmComment);
} else {
result = updateAlarmComment(tenantId, alarmComment);
result = updateAlarmComment(tenantId, alarmComment, oldAlarmComment);
}
if (result != null) {
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(result)
@ -101,18 +101,17 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
return alarmCommentDao.save(tenantId, alarmComment);
}
private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment) {
private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment, AlarmComment oldAlarmComment) {
log.debug("Update Alarm comment : {}", newAlarmComment);
AlarmComment existing = alarmCommentDao.findAlarmCommentById(tenantId, newAlarmComment.getId().getId());
if (existing != null) {
if (oldAlarmComment != null) {
if (newAlarmComment.getComment() != null) {
JsonNode comment = newAlarmComment.getComment();
((ObjectNode) comment).put("edited", "true");
((ObjectNode) comment).put("editedOn", System.currentTimeMillis());
existing.setComment(comment);
oldAlarmComment.setComment(comment);
}
return alarmCommentDao.save(tenantId, existing);
return alarmCommentDao.save(tenantId, oldAlarmComment);
}
return null;
}

22
dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java

@ -18,14 +18,18 @@ package org.thingsboard.server.dao.service.validator;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.alarm.AlarmCommentDao;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.exception.DataValidationException;
@Component
@AllArgsConstructor
public class AlarmCommentDataValidator extends DataValidator<AlarmComment> {
private final AlarmCommentDao alarmCommentDao;
@Override
protected void validateDataImpl(TenantId tenantId, AlarmComment alarmComment) {
if (alarmComment.getComment() == null) {
@ -35,4 +39,20 @@ public class AlarmCommentDataValidator extends DataValidator<AlarmComment> {
throw new DataValidationException("Alarm id should be specified!");
}
}
@Override
protected AlarmComment validateUpdate(TenantId tenantId, AlarmComment alarmComment) {
AlarmComment oldAlarmComment = null;
if (alarmComment.getId() != null) {
oldAlarmComment = alarmCommentDao.findAlarmCommentById(tenantId, alarmComment.getId().getId());
if (oldAlarmComment == null) {
throw new DataValidationException("Can't update non existing alarm comment!");
}
if (oldAlarmComment.getType() == AlarmCommentType.SYSTEM) {
throw new DataValidationException("System alarm comment can't be updated!");
}
}
return oldAlarmComment;
}
}

20
pom.xml

@ -70,6 +70,7 @@
<metrics.version>4.2.25</metrics.version>
<cassandra-all.version>5.0.4</cassandra-all.version> <!-- tools -->
<guava.version>33.1.0-jre</guava.version>
<tomcat.version>10.1.54</tomcat.version> <!-- to fix CVE-2026-34487, CVE-2026-34486, CVE-2026-34483. TODO: remove when fixed in spring-boot-dependencies -->
<commons-lang3.version>3.18.0</commons-lang3.version> <!-- to fix CVE-2025-48924. TODO: remove when fixed in spring-boot-dependencies -->
<commons-io.version>2.16.1</commons-io.version>
<commons-logging.version>1.3.1</commons-logging.version>
@ -991,6 +992,25 @@
<dependencyManagement>
<dependencies>
<!-- Temporary tomcat version override to fix CVE-2026-34487, CVE-2026-34486, CVE-2026-34483.
Must be declared before the spring-boot-dependencies BOM import to take precedence.
TODO: remove when fixed in spring-boot-dependencies -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<!-- End of tomcat version override -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>

2
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html

@ -42,7 +42,7 @@
<div class="user-display-name flex flex-col gap-0.5">
<span *ngIf="user.firstName || user.lastName"
[innerHTML]="getFullName(user) | highlight:searchText"></span>
<span [innerHTML]="user.email | highlight:searchText"></span>
<span [innerHTML]="user.email | highlight:searchText" tbTruncateWithTooltip></span>
</div>
</mat-option>
<mat-option *ngIf="!(filteredUsers | async)?.length" [value]="null" class="tb-not-found">

2
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts

@ -217,8 +217,6 @@ export class AddDeviceProfileDialogComponent extends
case 1:
return 'device-profile.transport-configuration';
case 2:
return 'device-profile.alarm-rules';
case 3:
return 'device-profile.device-provisioning';
}
}

4
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2402,7 +2402,7 @@
"search": "Search asset profiles",
"selected-asset-profiles": "{ count, plural, =1 {1 asset profile} other {# asset profiles} } selected",
"no-asset-profiles-matching": "No asset profile matching '{{entity}}' were found.",
"asset-profile-required": "Asset profile is required",
"asset-profile-required": "Asset profile is required.",
"idCopiedMessage": "Asset profile Id has been copied to clipboard",
"set-default": "Make asset profile default",
"delete": "Delete asset profile",
@ -2447,7 +2447,7 @@
"search": "Search device profiles",
"selected-device-profiles": "{ count, plural, =1 {1 device profile} other {# device profiles} } selected",
"no-device-profiles-matching": "No device profile matching '{{entity}}' were found.",
"device-profile-required": "Device profile is required",
"device-profile-required": "Device profile is required.",
"idCopiedMessage": "Device profile Id has been copied to clipboard",
"set-default": "Make device profile default",
"delete": "Delete device profile",

Loading…
Cancel
Save