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