203 changed files with 4174 additions and 3718 deletions
File diff suppressed because one or more lines are too long
@ -1,37 +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.actors.calculatedField; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.id.CalculatedFieldId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.msg.MsgType; |
|||
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; |
|||
import org.thingsboard.server.common.msg.queue.TbCallback; |
|||
|
|||
@Data |
|||
public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg { |
|||
|
|||
private final TenantId tenantId; |
|||
private final CalculatedFieldId cfId; |
|||
private final TbCallback callback; |
|||
|
|||
@Override |
|||
public MsgType getMsgType() { |
|||
return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
/** |
|||
* 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.resource; |
|||
|
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.GeneralFileDescriptor; |
|||
import org.thingsboard.server.common.data.ResourceType; |
|||
import org.thingsboard.server.common.data.TbResource; |
|||
import org.thingsboard.server.common.data.TbResourceDataInfo; |
|||
import org.thingsboard.server.common.data.TbResourceInfo; |
|||
import org.thingsboard.server.controller.AbstractControllerTest; |
|||
import org.thingsboard.server.dao.resource.ResourceService; |
|||
import org.thingsboard.server.dao.resource.TbResourceDataCache; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.Mockito.clearInvocations; |
|||
import static org.mockito.Mockito.timeout; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.verifyNoMoreInteractions; |
|||
|
|||
@DaoSqlTest |
|||
public class DefaultResourceDataCacheTest extends AbstractControllerTest { |
|||
|
|||
@MockitoSpyBean |
|||
private ResourceService resourceService; |
|||
@Autowired |
|||
private TbResourceService tbResourceService; |
|||
@MockitoSpyBean |
|||
private TbResourceDataCache resourceDataCache; |
|||
|
|||
@Test |
|||
public void testGetCachedResourceData() throws Exception { |
|||
loginTenantAdmin(); |
|||
|
|||
TbResource resource = new TbResource(); |
|||
resource.setTenantId(tenantId); |
|||
resource.setTitle("File for AI request"); |
|||
resource.setResourceType(ResourceType.GENERAL); |
|||
resource.setFileName("myTestJson.json"); |
|||
GeneralFileDescriptor descriptor = new GeneralFileDescriptor("application/json"); |
|||
resource.setDescriptorValue(descriptor); |
|||
byte[] data = "This is a test prompt for AI request.".getBytes(); |
|||
resource.setData(data); |
|||
TbResourceInfo savedResource = tbResourceService.save(resource); |
|||
verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId()); |
|||
|
|||
TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); |
|||
assertThat(cachedData.getData()).isEqualTo(data); |
|||
assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor); |
|||
verify(resourceService).getResourceDataInfo(tenantId, savedResource.getId()); |
|||
|
|||
// retrieve resource data second time
|
|||
clearInvocations(resourceService); |
|||
TbResourceDataInfo cachedData2 = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); |
|||
assertThat(cachedData2.getData()).isEqualTo(data); |
|||
verifyNoMoreInteractions(resourceService); |
|||
|
|||
// delete resource, check cache
|
|||
TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId()); |
|||
tbResourceService.delete(resourceById, true, null); |
|||
verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId()); |
|||
TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); |
|||
assertThat(cachedDataAfterDeletion).isEqualTo(null); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* 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; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
public class GeneralFileDescriptor { |
|||
private String mediaType; |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* 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; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
public class TbResourceDataInfo { |
|||
|
|||
private byte[] data; |
|||
private JsonNode descriptor; |
|||
|
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
/** |
|||
* 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.ai.model.chat; |
|||
|
|||
import dev.langchain4j.model.chat.ChatModel; |
|||
import jakarta.validation.Valid; |
|||
import jakarta.validation.constraints.Max; |
|||
import jakarta.validation.constraints.NotBlank; |
|||
import jakarta.validation.constraints.NotNull; |
|||
import jakarta.validation.constraints.Positive; |
|||
import jakarta.validation.constraints.PositiveOrZero; |
|||
import lombok.Builder; |
|||
import lombok.With; |
|||
import org.thingsboard.server.common.data.ai.provider.AiProvider; |
|||
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; |
|||
|
|||
@Builder |
|||
public record OllamaChatModelConfig( |
|||
@NotNull @Valid OllamaProviderConfig providerConfig, |
|||
@NotBlank String modelId, |
|||
@PositiveOrZero Double temperature, |
|||
@Positive @Max(1) Double topP, |
|||
@PositiveOrZero Integer topK, |
|||
Integer contextLength, |
|||
Integer maxOutputTokens, |
|||
@With @Positive Integer timeoutSeconds, |
|||
@With @PositiveOrZero Integer maxRetries |
|||
) implements AiChatModelConfig<OllamaChatModelConfig> { |
|||
|
|||
@Override |
|||
public AiProvider provider() { |
|||
return AiProvider.OLLAMA; |
|||
} |
|||
|
|||
@Override |
|||
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { |
|||
return configurer.configureChatModel(this); |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsJsonMode() { |
|||
return true; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
/** |
|||
* 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.ai.provider; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonSubTypes; |
|||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
|||
import jakarta.validation.Valid; |
|||
import jakarta.validation.constraints.NotNull; |
|||
|
|||
public record OllamaProviderConfig( |
|||
@NotNull String baseUrl, |
|||
@NotNull @Valid OllamaAuth auth |
|||
) implements AiProviderConfig { |
|||
|
|||
@JsonTypeInfo( |
|||
use = JsonTypeInfo.Id.NAME, |
|||
include = JsonTypeInfo.As.PROPERTY, |
|||
property = "type" |
|||
) |
|||
@JsonSubTypes({ |
|||
@JsonSubTypes.Type(value = OllamaAuth.None.class, name = "NONE"), |
|||
@JsonSubTypes.Type(value = OllamaAuth.Basic.class, name = "BASIC"), |
|||
@JsonSubTypes.Type(value = OllamaAuth.Token.class, name = "TOKEN") |
|||
}) |
|||
public sealed interface OllamaAuth { |
|||
|
|||
record None() implements OllamaAuth {} |
|||
|
|||
record Basic(@NotNull String username, @NotNull String password) implements OllamaAuth {} |
|||
|
|||
record Token(@NotNull String token) implements OllamaAuth {} |
|||
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
/** |
|||
* 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.cf.configuration; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.relation.EntityRelation; |
|||
import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; |
|||
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
|||
import org.thingsboard.server.common.data.relation.RelationPathLevel; |
|||
import org.thingsboard.server.common.data.util.CollectionsUtil; |
|||
|
|||
import java.util.List; |
|||
import java.util.NoSuchElementException; |
|||
|
|||
@Data |
|||
public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { |
|||
|
|||
private List<RelationPathLevel> levels; |
|||
|
|||
@Override |
|||
public CFArgumentDynamicSourceType getType() { |
|||
return CFArgumentDynamicSourceType.RELATION_PATH_QUERY; |
|||
} |
|||
|
|||
@Override |
|||
public void validate() { |
|||
if (CollectionsUtil.isEmpty(levels)) { |
|||
throw new IllegalArgumentException("At least one relation level must be specified!"); |
|||
} |
|||
levels.forEach(RelationPathLevel::validate); |
|||
} |
|||
|
|||
public List<EntityId> resolveEntityIds(List<EntityRelation> relations) { |
|||
EntitySearchDirection lastLevelDirection = getLastLevel().direction(); |
|||
return switch (lastLevelDirection) { |
|||
case FROM -> relations.stream().map(EntityRelation::getTo).toList(); |
|||
case TO -> relations.stream().map(EntityRelation::getFrom).toList(); |
|||
}; |
|||
} |
|||
|
|||
public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { |
|||
if (levels.size() > maxAllowedRelationLevel) { |
|||
throw new IllegalArgumentException("Max relation level is greater than configured " + |
|||
"maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); |
|||
} |
|||
} |
|||
|
|||
public EntityRelationPathQuery toRelationPathQuery(EntityId entityId) { |
|||
return new EntityRelationPathQuery(entityId, levels); |
|||
} |
|||
|
|||
private RelationPathLevel getLastLevel() { |
|||
return levels.get(levels.size() - 1); |
|||
} |
|||
|
|||
} |
|||
@ -1,86 +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.cf.configuration; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.relation.EntityRelation; |
|||
import org.thingsboard.server.common.data.relation.EntityRelationsQuery; |
|||
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
|||
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; |
|||
import org.thingsboard.server.common.data.relation.RelationsSearchParameters; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
@Data |
|||
public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { |
|||
|
|||
private int maxLevel; |
|||
private boolean fetchLastLevelOnly; |
|||
private EntitySearchDirection direction; |
|||
private String relationType; |
|||
|
|||
@Override |
|||
public CFArgumentDynamicSourceType getType() { |
|||
return CFArgumentDynamicSourceType.RELATION_QUERY; |
|||
} |
|||
|
|||
@Override |
|||
public void validate() { |
|||
if (maxLevel < 1) { |
|||
throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be less than 1!"); |
|||
} |
|||
if (direction == null) { |
|||
throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!"); |
|||
} |
|||
if (StringUtils.isBlank(relationType)) { |
|||
throw new IllegalArgumentException("Relation query dynamic source configuration relation type must be specified!"); |
|||
} |
|||
} |
|||
|
|||
@JsonIgnore |
|||
public boolean isSimpleRelation() { |
|||
return maxLevel == 1; |
|||
} |
|||
|
|||
public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { |
|||
if (maxLevel > maxAllowedRelationLevel) { |
|||
throw new IllegalArgumentException("Max relation level is greater than configured " + |
|||
"maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); |
|||
} |
|||
} |
|||
|
|||
public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { |
|||
if (isSimpleRelation()) { |
|||
throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!"); |
|||
} |
|||
var entityRelationsQuery = new EntityRelationsQuery(); |
|||
entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, fetchLastLevelOnly)); |
|||
entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, Collections.emptyList()))); |
|||
return entityRelationsQuery; |
|||
} |
|||
|
|||
public List<EntityId> resolveEntityIds(List<EntityRelation> relations) { |
|||
return switch (direction) { |
|||
case FROM -> relations.stream().map(EntityRelation::getTo).toList(); |
|||
case TO -> relations.stream().map(EntityRelation::getFrom).toList(); |
|||
}; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* 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.relation; |
|||
|
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
|
|||
import java.util.List; |
|||
|
|||
public record EntityRelationPathQuery(EntityId rootEntityId, List<RelationPathLevel> levels) { |
|||
|
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
/** |
|||
* 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.relation; |
|||
|
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
|
|||
public record RelationPathLevel(EntitySearchDirection direction, String relationType) { |
|||
|
|||
public void validate() { |
|||
if (direction == null) { |
|||
throw new IllegalArgumentException("Direction must be specified!"); |
|||
} |
|||
if (StringUtils.isBlank(relationType)) { |
|||
throw new IllegalArgumentException("Relation type must be specified!"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
/** |
|||
* 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.cf.configuration; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.junit.jupiter.params.ParameterizedTest; |
|||
import org.junit.jupiter.params.provider.NullAndEmptySource; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.relation.EntityRelation; |
|||
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
|||
import org.thingsboard.server.common.data.relation.RelationPathLevel; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.assertj.core.api.Assertions.assertThatCode; |
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class RelationPathQueryDynamicSourceConfigurationTest { |
|||
|
|||
@Test |
|||
void typeShouldBeRelationQuery() { |
|||
var cfg = new RelationPathQueryDynamicSourceConfiguration(); |
|||
assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_PATH_QUERY); |
|||
} |
|||
|
|||
@ParameterizedTest |
|||
@NullAndEmptySource |
|||
void validateShouldThrowWhenLevelsIsNull(List<RelationPathLevel> levels) { |
|||
var cfg = new RelationPathQueryDynamicSourceConfiguration(); |
|||
cfg.setLevels(levels); |
|||
|
|||
assertThatThrownBy(cfg::validate) |
|||
.isInstanceOf(IllegalArgumentException.class) |
|||
.hasMessage("At least one relation level must be specified!"); |
|||
} |
|||
|
|||
@Test |
|||
void validateShouldCallValidateForPathLevels() { |
|||
List<RelationPathLevel> levels = new ArrayList<>(); |
|||
|
|||
RelationPathLevel lvl1 = mock(RelationPathLevel.class); |
|||
RelationPathLevel lvl2 = mock(RelationPathLevel.class); |
|||
levels.add(lvl1); |
|||
levels.add(lvl2); |
|||
|
|||
var cfg = new RelationPathQueryDynamicSourceConfiguration(); |
|||
cfg.setLevels(levels); |
|||
|
|||
assertThatCode(cfg::validate).doesNotThrowAnyException(); |
|||
|
|||
verify(lvl1).validate(); |
|||
verify(lvl2).validate(); |
|||
} |
|||
|
|||
@Test |
|||
void resolveEntityIds_whenDirectionFROM_thenReturnsToIds() { |
|||
List<RelationPathLevel> levels = new ArrayList<>(); |
|||
|
|||
RelationPathLevel lvl1 = mock(RelationPathLevel.class); |
|||
RelationPathLevel lvl2 = mock(RelationPathLevel.class); |
|||
levels.add(lvl1); |
|||
levels.add(lvl2); |
|||
|
|||
when(lvl2.direction()).thenReturn(EntitySearchDirection.FROM); |
|||
|
|||
EntityRelation rel1 = mock(EntityRelation.class); |
|||
EntityRelation rel2 = mock(EntityRelation.class); |
|||
|
|||
when(rel1.getTo()).thenReturn(mock(EntityId.class)); |
|||
when(rel2.getTo()).thenReturn(mock(EntityId.class)); |
|||
|
|||
var cfg = new RelationPathQueryDynamicSourceConfiguration(); |
|||
cfg.setLevels(levels); |
|||
|
|||
var out = cfg.resolveEntityIds(List.of(rel1, rel2)); |
|||
|
|||
assertThat(out).containsExactly(rel1.getTo(), rel2.getTo()); |
|||
} |
|||
|
|||
@Test |
|||
void resolveEntityIds_whenDirectionTO_thenReturnsFromIds() { |
|||
List<RelationPathLevel> levels = new ArrayList<>(); |
|||
|
|||
RelationPathLevel lvl1 = mock(RelationPathLevel.class); |
|||
RelationPathLevel lvl2 = mock(RelationPathLevel.class); |
|||
levels.add(lvl1); |
|||
levels.add(lvl2); |
|||
|
|||
when(lvl2.direction()).thenReturn(EntitySearchDirection.TO); |
|||
|
|||
EntityRelation rel1 = mock(EntityRelation.class); |
|||
EntityRelation rel2 = mock(EntityRelation.class); |
|||
|
|||
when(rel1.getFrom()).thenReturn(mock(EntityId.class)); |
|||
when(rel2.getFrom()).thenReturn(mock(EntityId.class)); |
|||
|
|||
var cfg = new RelationPathQueryDynamicSourceConfiguration(); |
|||
cfg.setLevels(levels); |
|||
|
|||
var out = cfg.resolveEntityIds(List.of(rel1, rel2)); |
|||
|
|||
assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom()); |
|||
} |
|||
|
|||
} |
|||
@ -1,216 +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.cf.configuration; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.junit.jupiter.params.ParameterizedTest; |
|||
import org.junit.jupiter.params.provider.NullAndEmptySource; |
|||
import org.junit.jupiter.params.provider.ValueSource; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.relation.EntityRelation; |
|||
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
|||
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; |
|||
import org.thingsboard.server.common.data.relation.RelationsSearchParameters; |
|||
|
|||
import java.util.List; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.assertj.core.api.Assertions.assertThatCode; |
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class RelationQueryDynamicSourceConfigurationTest { |
|||
|
|||
@Mock |
|||
EntityId rootEntityId; |
|||
|
|||
@Mock |
|||
EntityRelation rel1; |
|||
@Mock |
|||
EntityRelation rel2; |
|||
|
|||
@Test |
|||
void typeShouldBeRelationQuery() { |
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_QUERY); |
|||
} |
|||
|
|||
@Test |
|||
void validateShouldThrowWhenMaxLevelLessThanOne() { |
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(0); |
|||
cfg.setDirection(EntitySearchDirection.FROM); |
|||
cfg.setRelationType(EntityRelation.CONTAINS_TYPE); |
|||
|
|||
assertThatThrownBy(cfg::validate) |
|||
.isInstanceOf(IllegalArgumentException.class) |
|||
.hasMessage("Relation query dynamic source configuration max relation level can't be less than 1!"); |
|||
} |
|||
|
|||
@Test |
|||
void validateShouldThrowWhenMaxLevelGreaterThanMaxAllowedLevelFromTenantProfile() { |
|||
int maxAllowedRelationLevel = 2; |
|||
int argumentMaxRelationLevel = 3; |
|||
|
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(argumentMaxRelationLevel); |
|||
cfg.setDirection(EntitySearchDirection.FROM); |
|||
cfg.setRelationType(EntityRelation.CONTAINS_TYPE); |
|||
|
|||
String testRelationArgument = "testRelationArgument"; |
|||
assertThatThrownBy(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)) |
|||
.isInstanceOf(IllegalArgumentException.class) |
|||
.hasMessage("Max relation level is greater than configured " + |
|||
"maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + testRelationArgument); |
|||
} |
|||
|
|||
@Test |
|||
void validateShouldPassValidationWhenMaxLevelLessThanMaxAllowedLevelFromTenantProfile() { |
|||
int maxAllowedRelationLevel = 5; |
|||
int argumentMaxRelationLevel = 2; |
|||
|
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(argumentMaxRelationLevel); |
|||
cfg.setDirection(EntitySearchDirection.FROM); |
|||
cfg.setRelationType(EntityRelation.CONTAINS_TYPE); |
|||
|
|||
String testRelationArgument = "testRelationArgument"; |
|||
assertThatCode(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)).doesNotThrowAnyException(); |
|||
} |
|||
|
|||
@Test |
|||
void validateShouldThrowWhenDirectionIsNull() { |
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(1); |
|||
cfg.setDirection(null); |
|||
cfg.setRelationType(EntityRelation.CONTAINS_TYPE); |
|||
|
|||
assertThatThrownBy(cfg::validate) |
|||
.isInstanceOf(IllegalArgumentException.class) |
|||
.hasMessage("Relation query dynamic source configuration direction must be specified!"); |
|||
} |
|||
|
|||
@ParameterizedTest |
|||
@ValueSource(strings = {" "}) |
|||
@NullAndEmptySource |
|||
void validateShouldThrowWhenRelationTypeIsNull(String relationType) { |
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(1); |
|||
cfg.setDirection(EntitySearchDirection.TO); |
|||
cfg.setRelationType(relationType); |
|||
|
|||
assertThatThrownBy(cfg::validate) |
|||
.isInstanceOf(IllegalArgumentException.class) |
|||
.hasMessage("Relation query dynamic source configuration relation type must be specified!"); |
|||
} |
|||
|
|||
@ParameterizedTest |
|||
@NullAndEmptySource |
|||
void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull(List<EntityType> entityTypes) { |
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(1); |
|||
assertThat(cfg.isSimpleRelation()).isTrue(); |
|||
} |
|||
|
|||
@Test |
|||
void isSimpleRelationFalseWhenMaxLevelNotOne() { |
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(2); |
|||
assertThat(cfg.isSimpleRelation()).isFalse(); |
|||
} |
|||
|
|||
@ParameterizedTest |
|||
@NullAndEmptySource |
|||
void toEntityRelationsQueryShouldThrowForSimpleRelation(List<EntityType> entityTypes) { |
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(1); |
|||
cfg.setFetchLastLevelOnly(false); |
|||
cfg.setDirection(EntitySearchDirection.FROM); |
|||
cfg.setRelationType(EntityRelation.CONTAINS_TYPE); |
|||
|
|||
assertThatThrownBy(() -> cfg.toEntityRelationsQuery(rootEntityId)) |
|||
.isInstanceOf(IllegalArgumentException.class) |
|||
.hasMessage("Entity relations query can't be created for a simple relation!"); |
|||
} |
|||
|
|||
@Test |
|||
void toEntityRelationsQueryShouldBuildQueryForNonSimpleRelation() { |
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(2); |
|||
cfg.setFetchLastLevelOnly(true); |
|||
cfg.setDirection(EntitySearchDirection.TO); |
|||
cfg.setRelationType(EntityRelation.MANAGES_TYPE); |
|||
|
|||
var query = cfg.toEntityRelationsQuery(rootEntityId); |
|||
|
|||
assertThat(query).isNotNull(); |
|||
RelationsSearchParameters params = query.getParameters(); |
|||
assertThat(params).isNotNull(); |
|||
assertThat(params.getRootId()).isEqualTo(rootEntityId.getId()); |
|||
assertThat(params.getDirection()).isEqualTo(EntitySearchDirection.TO); |
|||
assertThat(params.getMaxLevel()).isEqualTo(2); |
|||
assertThat(params.isFetchLastLevelOnly()).isTrue(); |
|||
|
|||
assertThat(query.getFilters()).hasSize(1); |
|||
assertThat(query.getFilters().get(0)).isInstanceOf(RelationEntityTypeFilter.class); |
|||
RelationEntityTypeFilter filter = query.getFilters().get(0); |
|||
assertThat(filter.getRelationType()).isEqualTo(EntityRelation.MANAGES_TYPE); |
|||
} |
|||
|
|||
@Test |
|||
void resolveEntityIdsFromDirectionFROMReturnsToIds() { |
|||
when(rel1.getTo()).thenReturn(mock(EntityId.class)); |
|||
when(rel2.getTo()).thenReturn(mock(EntityId.class)); |
|||
|
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setDirection(EntitySearchDirection.FROM); |
|||
|
|||
var out = cfg.resolveEntityIds(List.of(rel1, rel2)); |
|||
|
|||
assertThat(out).containsExactly(rel1.getTo(), rel2.getTo()); |
|||
} |
|||
|
|||
@Test |
|||
void resolveEntityIdsFromDirectionTOReturnsFromIds() { |
|||
when(rel1.getFrom()).thenReturn(mock(EntityId.class)); |
|||
when(rel2.getFrom()).thenReturn(mock(EntityId.class)); |
|||
|
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setDirection(EntitySearchDirection.TO); |
|||
|
|||
var out = cfg.resolveEntityIds(List.of(rel1, rel2)); |
|||
|
|||
assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom()); |
|||
} |
|||
|
|||
@Test |
|||
void validateShouldPassForValidConfig() { |
|||
var cfg = new RelationQueryDynamicSourceConfiguration(); |
|||
cfg.setMaxLevel(2); |
|||
cfg.setFetchLastLevelOnly(false); |
|||
cfg.setDirection(EntitySearchDirection.FROM); |
|||
cfg.setRelationType(EntityRelation.CONTAINS_TYPE); |
|||
|
|||
assertThatCode(cfg::validate).doesNotThrowAnyException(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
/** |
|||
* 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.resource; |
|||
|
|||
import com.github.benmanes.caffeine.cache.AsyncLoadingCache; |
|||
import com.github.benmanes.caffeine.cache.Caffeine; |
|||
import com.google.common.util.concurrent.FluentFuture; |
|||
import jakarta.annotation.PostConstruct; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.common.util.DonAsynchron; |
|||
import org.thingsboard.server.common.data.TbResourceDataInfo; |
|||
import org.thingsboard.server.common.data.id.TbResourceId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.dao.sql.JpaExecutorService; |
|||
|
|||
import java.util.concurrent.CompletableFuture; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class DefaultTbResourceDataCache implements TbResourceDataCache { |
|||
|
|||
private final ResourceService resourceService; |
|||
private final JpaExecutorService executorService; |
|||
|
|||
@Value("${cache.tbResourceData.maxSize:100000}") |
|||
private int cacheMaxSize; |
|||
@Value("${cache.tbResourceData.timeToLiveInMinutes:44640}") |
|||
private int cacheValueTtl; |
|||
private AsyncLoadingCache<ResourceDataKey, TbResourceDataInfo> cache; |
|||
|
|||
@PostConstruct |
|||
private void init() { |
|||
cache = Caffeine.newBuilder() |
|||
.maximumSize(cacheMaxSize) |
|||
.expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES) |
|||
.executor(executorService) |
|||
.buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> resourceService.getResourceDataInfo(key.tenantId(), key.resourceId()), executor)); |
|||
} |
|||
|
|||
@Override |
|||
public FluentFuture<TbResourceDataInfo> getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId) { |
|||
log.trace("Retrieving resource data info by id [{}], tenant id [{}] from cache", resourceId, tenantId); |
|||
return DonAsynchron.toFluentFuture(cache.get(new ResourceDataKey(tenantId, resourceId))); |
|||
} |
|||
|
|||
@Override |
|||
public void evictResourceData(TenantId tenantId, TbResourceId resourceId) { |
|||
cache.asMap().remove(new ResourceDataKey(tenantId, resourceId)); |
|||
log.trace("Evicted resource data info with id [{}], tenant id [{}]", resourceId, tenantId); |
|||
} |
|||
|
|||
record ResourceDataKey (TenantId tenantId, TbResourceId resourceId) {} |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue