From f0575b66df8d5a1f888717135d935d6aebfe335d Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 24 Jun 2026 18:54:44 +0300 Subject: [PATCH] feat(attributes): per-scope fetch-all + scope-separated gateway response Actor getAttributesKvEntries: explicit per-scope all/specific/none with all-wins; both-absent keeps the 'fetch everything' backward-compat. Echo separateScopesResponse request->response. JsonConverter.getJsonObjectForGateway emits {client,shared} when separateScopesResponse, else legacy value/values. Unit test for the converter. --- .../device/DeviceActorMessageProcessor.java | 33 ++++++--- .../server/common/adaptor/JsonConverter.java | 24 ++++-- .../JsonConverterGatewayResponseTest.java | 74 +++++++++++++++++++ 3 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterGatewayResponseTest.java diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index d8710eaa2c..299992783d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -518,6 +518,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId); } + @SuppressWarnings("deprecation") // isMultipleAttributesRequest retained for the legacy gateway value/values response private void handleGetAttributesRequest(SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { int requestId = request.getRequestId(); if (request.getOnlyShared()) { @@ -548,6 +549,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso public void onSuccess(@Nullable List> result) { GetAttributeResponseMsg responseMsg = GetAttributeResponseMsg.newBuilder() .setRequestId(requestId) + .setSeparateScopesResponse(request.getSeparateScopesResponse()) .addAllClientAttributeList(KvProtoUtil.attrToTsKvProtos(result.get(0))) .addAllSharedAttributeList(KvProtoUtil.attrToTsKvProtos(result.get(1))) .setIsMultipleAttributesRequest( @@ -568,20 +570,33 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } private ListenableFuture>> getAttributesKvEntries(GetAttributeRequestMsg request) { + boolean clientAll = request.getAllClientAttributes(); + boolean sharedAll = request.getAllSharedAttributes(); + boolean clientSpecific = !CollectionUtils.isEmpty(request.getClientAttributeNamesList()); + boolean sharedSpecific = !CollectionUtils.isEmpty(request.getSharedAttributeNamesList()); + + boolean noClientSignal = !clientAll && !clientSpecific; + boolean noSharedSignal = !sharedAll && !sharedSpecific; + ListenableFuture> clientAttributesFuture; ListenableFuture> sharedAttributesFuture; - if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) { + + if (noClientSignal && noSharedSignal) { + // backward-compat: empty request => fetch everything from both scopes clientAttributesFuture = findAllAttributesByScope(AttributeScope.CLIENT_SCOPE); sharedAttributesFuture = findAllAttributesByScope(AttributeScope.SHARED_SCOPE); - } else if (!CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) { - clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), AttributeScope.CLIENT_SCOPE); - sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), AttributeScope.SHARED_SCOPE); - } else if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) { - clientAttributesFuture = Futures.immediateFuture(Collections.emptyList()); - sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), AttributeScope.SHARED_SCOPE); } else { - sharedAttributesFuture = Futures.immediateFuture(Collections.emptyList()); - clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), AttributeScope.CLIENT_SCOPE); + // "all " wins over a specific key list for the same scope + clientAttributesFuture = clientAll + ? findAllAttributesByScope(AttributeScope.CLIENT_SCOPE) + : (clientSpecific + ? findAttributesByScope(toSet(request.getClientAttributeNamesList()), AttributeScope.CLIENT_SCOPE) + : Futures.immediateFuture(Collections.emptyList())); + sharedAttributesFuture = sharedAll + ? findAllAttributesByScope(AttributeScope.SHARED_SCOPE) + : (sharedSpecific + ? findAttributesByScope(toSet(request.getSharedAttributeNamesList()), AttributeScope.SHARED_SCOPE) + : Futures.immediateFuture(Collections.emptyList())); } return Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture)); } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java index 8a40b1bc76..a7ccead805 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java @@ -354,6 +354,7 @@ public class JsonConverter { return result; } + @SuppressWarnings("deprecation") // isMultipleAttributesRequest retained for the legacy gateway value/values response public static JsonObject getJsonObjectForGateway( String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg @@ -361,11 +362,24 @@ public class JsonConverter { JsonObject result = new JsonObject(); result.addProperty("id", responseMsg.getRequestId()); result.addProperty(DEVICE_PROPERTY, deviceName); - if (responseMsg.getClientAttributeListCount() > 0) { - addValues(result, responseMsg.getClientAttributeListList(), responseMsg.getIsMultipleAttributesRequest()); - } - if (responseMsg.getSharedAttributeListCount() > 0) { - addValues(result, responseMsg.getSharedAttributeListList(), responseMsg.getIsMultipleAttributesRequest()); + if (responseMsg.getSeparateScopesResponse()) { + if (responseMsg.getClientAttributeListCount() > 0) { + JsonObject client = new JsonObject(); + responseMsg.getClientAttributeListList().forEach(addToObjectFromProto(client)); + result.add("client", client); + } + if (responseMsg.getSharedAttributeListCount() > 0) { + JsonObject shared = new JsonObject(); + responseMsg.getSharedAttributeListList().forEach(addToObjectFromProto(shared)); + result.add("shared", shared); + } + } else { + if (responseMsg.getClientAttributeListCount() > 0) { + addValues(result, responseMsg.getClientAttributeListList(), responseMsg.getIsMultipleAttributesRequest()); + } + if (responseMsg.getSharedAttributeListCount() > 0) { + addValues(result, responseMsg.getSharedAttributeListList(), responseMsg.getIsMultipleAttributesRequest()); + } } return result; } diff --git a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterGatewayResponseTest.java b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterGatewayResponseTest.java new file mode 100644 index 0000000000..5c9af2a679 --- /dev/null +++ b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterGatewayResponseTest.java @@ -0,0 +1,74 @@ +/** + * 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.adaptor; + +import com.google.gson.JsonObject; +import org.junit.Test; +import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; +import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; +import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JsonConverterGatewayResponseTest { + + private TsKvProto kv(String key, long v) { + return TsKvProto.newBuilder().setTs(1L).setKv( + KeyValueProto.newBuilder().setKey(key).setType(KeyValueType.LONG_V).setLongV(v).build()).build(); + } + + @Test + public void separateScopes_keepsScopeLabels_keyNames_andOverlap() { + GetAttributeResponseMsg msg = GetAttributeResponseMsg.newBuilder() + .setRequestId(1) + .setSeparateScopesResponse(true) + .addClientAttributeList(kv("test", 27)) + .addSharedAttributeList(kv("test", 99)) + .build(); + + JsonObject out = JsonConverter.getJsonObjectForGateway("DeviceA", msg); + + assertThat(out.get("id").getAsInt()).isEqualTo(1); + assertThat(out.get("device").getAsString()).isEqualTo("DeviceA"); + assertThat(out.getAsJsonObject("client").get("test").getAsLong()).isEqualTo(27); + assertThat(out.getAsJsonObject("shared").get("test").getAsLong()).isEqualTo(99); + assertThat(out.has("value")).isFalse(); + assertThat(out.has("values")).isFalse(); + } + + @Test + public void separateScopes_omitsEmptyScope() { + GetAttributeResponseMsg msg = GetAttributeResponseMsg.newBuilder() + .setRequestId(2).setSeparateScopesResponse(true) + .addSharedAttributeList(kv("s", 1)).build(); + JsonObject out = JsonConverter.getJsonObjectForGateway("DeviceA", msg); + assertThat(out.has("client")).isFalse(); + assertThat(out.getAsJsonObject("shared").get("s").getAsLong()).isEqualTo(1); + } + + @Test + @SuppressWarnings("deprecation") + public void legacy_singleValue_unchanged() { + GetAttributeResponseMsg msg = GetAttributeResponseMsg.newBuilder() + .setRequestId(3).setSeparateScopesResponse(false) + .setIsMultipleAttributesRequest(false) + .addSharedAttributeList(kv("s", 5)).build(); + JsonObject out = JsonConverter.getJsonObjectForGateway("DeviceA", msg); + assertThat(out.get("value").getAsLong()).isEqualTo(5); + assertThat(out.has("shared")).isFalse(); + } +}