@ -33,11 +33,11 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy ;
import lombok.extern.slf4j.Slf4j ;
import org.springframework.beans.factory.annotation.Autowired ;
import org.springframework.beans.factory.annotation.Value ;
import org.springframework.http.HttpStatus ;
import org.springframework.http.ResponseEntity ;
import org.springframework.security.access.prepost.PreAuthorize ;
import org.springframework.web.bind.annotation.PathVariable ;
import org.springframework.web.bind.annotation.PostMapping ;
import org.springframework.web.bind.annotation.RequestBody ;
import org.springframework.web.bind.annotation.RequestMapping ;
import org.springframework.web.bind.annotation.RequestMethod ;
@ -65,25 +65,17 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased ;
import org.thingsboard.server.common.data.kv.Aggregation ;
import org.thingsboard.server.common.data.kv.AttributeKvEntry ;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry ;
import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery ;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry ;
import org.thingsboard.server.common.data.kv.BooleanDataEntry ;
import org.thingsboard.server.common.data.kv.DataType ;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery ;
import org.thingsboard.server.common.data.kv.DoubleDataEntry ;
import org.thingsboard.server.common.data.kv.IntervalType ;
import org.thingsboard.server.common.data.kv.JsonDataEntry ;
import org.thingsboard.server.common.data.kv.KvEntry ;
import org.thingsboard.server.common.data.kv.LongDataEntry ;
import org.thingsboard.server.common.data.kv.StringDataEntry ;
import org.thingsboard.server.common.data.kv.TsKvEntry ;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration ;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg ;
import org.thingsboard.server.config.annotations.ApiOperation ;
import org.thingsboard.server.dao.timeseries.TimeseriesService ;
import org.thingsboard.server.exception.InvalidParametersException ;
import org.thingsboard.server.exception.UncheckedApiException ;
import org.thingsboard.server.queue.util.TbCoreComponent ;
import org.thingsboard.server.service.security.AccessValidator ;
import org.thingsboard.server.service.security.model.SecurityUser ;
@ -156,9 +148,6 @@ public class TelemetryController extends BaseController {
@Autowired
private TbTelemetryService tbTelemetryService ;
@Value ( "${transport.json.max_string_value_length:0}" )
private int maxStringValueLength ;
private ExecutorService executor ;
@PostConstruct
@ -314,10 +303,10 @@ public class TelemetryController extends BaseController {
@Parameter ( description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types." )
@RequestParam ( name = "timeZone" , required = false ) String timeZone ,
@Parameter ( description = "An integer value that represents a max number of time series data points to fetch." +
" This parameter is used only in the case if 'agg' parameter is set to 'NONE'." , schema = @Schema ( defaultValue = "100" ) )
" This parameter is used only in the case if 'agg' parameter is set to 'NONE'." , schema = @Schema ( defaultValue = "100" ) )
@RequestParam ( name = "limit" , defaultValue = "100" ) Integer limit ,
@Parameter ( description = "A string value representing the aggregation function. " +
"If the interval is not specified, 'agg' parameter will use 'NONE' value." ,
"If the interval is not specified, 'agg' parameter will use 'NONE' value." ,
schema = @Schema ( allowableValues = { "MIN" , "MAX" , "AVG" , "SUM" , "COUNT" , "NONE" } ) )
@RequestParam ( name = "agg" , defaultValue = "NONE" ) String aggStr ,
@Parameter ( description = SORT_ORDER_DESCRIPTION , schema = @Schema ( allowableValues = { "ASC" , "DESC" } ) )
@ -337,20 +326,21 @@ public class TelemetryController extends BaseController {
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH )
@ApiResponses ( value = {
@ApiResponse ( responseCode = "200" , description = SAVE_ATTIRIBUTES_STATUS_OK +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'." ) ,
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'." ) ,
@ApiResponse ( responseCode = "400" , description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST ) ,
@ApiResponse ( responseCode = "401" , description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant." ) ,
@ApiResponse ( responseCode = "500" , description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace." ) ,
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace." ) ,
} )
@PreAuthorize ( "hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')" )
@RequestMapping ( value = "/{deviceId}/{scope}" , method = RequestMethod . POST )
@ResponseBody
public DeferredResult < ResponseEntity > saveDeviceAttributes (
@Parameter ( description = DEVICE_ID_PARAM_DESCRIPTION , required = true ) @PathVariable ( "deviceId" ) String deviceIdStr ,
@Parameter ( description = ATTRIBUTES_SCOPE_DESCRIPTION , schema = @Schema ( allowableValues = { "SERVER_SCOPE" , "SHARED_SCOPE" } , requiredMode = Schema . RequiredMode . REQUIRED ) ) @PathVariable ( "scope" ) AttributeScope scope ,
@io.swagger.v3.oas.annotations.parameters.RequestBody ( description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION , required = true ) @RequestBody JsonNode request ) throws ThingsboardException {
@PostMapping ( value = "/{deviceId}/{scope}" )
public DeferredResult < ResponseEntity > saveDeviceAttributes ( @Parameter ( description = DEVICE_ID_PARAM_DESCRIPTION , required = true )
@PathVariable ( "deviceId" ) String deviceIdStr ,
@Parameter ( description = ATTRIBUTES_SCOPE_DESCRIPTION , schema = @Schema ( allowableValues = { "SERVER_SCOPE" , "SHARED_SCOPE" } , requiredMode = Schema . RequiredMode . REQUIRED ) )
@PathVariable ( "scope" ) AttributeScope scope ,
@io.swagger.v3.oas.annotations.parameters.RequestBody ( description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION , required = true )
@RequestBody String request ) throws ThingsboardException {
EntityId entityId = EntityIdFactory . getByTypeAndUuid ( EntityType . DEVICE , deviceIdStr ) ;
return saveAttributes ( getTenantId ( ) , entityId , scope , request ) ;
}
@ -367,13 +357,15 @@ public class TelemetryController extends BaseController {
@ApiResponse ( responseCode = "500" , description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR ) ,
} )
@PreAuthorize ( "hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')" )
@RequestMapping ( value = "/{entityType}/{entityId}/{scope}" , method = RequestMethod . POST )
@ResponseBody
public DeferredResult < ResponseEntity > saveEntityAttributesV1 (
@Parameter ( description = ENTITY_TYPE_PARAM_DESCRIPTION , required = true , schema = @Schema ( defaultValue = "DEVICE" ) ) @PathVariable ( "entityType" ) String entityType ,
@Parameter ( description = ENTITY_ID_PARAM_DESCRIPTION , required = true ) @PathVariable ( "entityId" ) String entityIdStr ,
@Parameter ( description = ATTRIBUTES_SCOPE_DESCRIPTION , schema = @Schema ( allowableValues = { "SERVER_SCOPE" , "SHARED_SCOPE" } ) ) @PathVariable ( "scope" ) AttributeScope scope ,
@io.swagger.v3.oas.annotations.parameters.RequestBody ( description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION , required = true ) @RequestBody JsonNode request ) throws ThingsboardException {
@PostMapping ( value = "/{entityType}/{entityId}/{scope}" )
public DeferredResult < ResponseEntity > saveEntityAttributesV1 ( @Parameter ( description = ENTITY_TYPE_PARAM_DESCRIPTION , required = true , schema = @Schema ( defaultValue = "DEVICE" ) )
@PathVariable ( "entityType" ) String entityType ,
@Parameter ( description = ENTITY_ID_PARAM_DESCRIPTION , required = true )
@PathVariable ( "entityId" ) String entityIdStr ,
@Parameter ( description = ATTRIBUTES_SCOPE_DESCRIPTION , schema = @Schema ( allowableValues = { "SERVER_SCOPE" , "SHARED_SCOPE" } ) )
@PathVariable ( "scope" ) AttributeScope scope ,
@io.swagger.v3.oas.annotations.parameters.RequestBody ( description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION , required = true )
@RequestBody String request ) throws ThingsboardException {
EntityId entityId = EntityIdFactory . getByTypeAndId ( entityType , entityIdStr ) ;
return saveAttributes ( getTenantId ( ) , entityId , scope , request ) ;
}
@ -390,13 +382,15 @@ public class TelemetryController extends BaseController {
@ApiResponse ( responseCode = "500" , description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR ) ,
} )
@PreAuthorize ( "hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')" )
@RequestMapping ( value = "/{entityType}/{entityId}/attributes/{scope}" , method = RequestMethod . POST )
@ResponseBody
public DeferredResult < ResponseEntity > saveEntityAttributesV2 (
@Parameter ( description = ENTITY_TYPE_PARAM_DESCRIPTION , required = true , schema = @Schema ( defaultValue = "DEVICE" ) ) @PathVariable ( "entityType" ) String entityType ,
@Parameter ( description = ENTITY_ID_PARAM_DESCRIPTION , required = true ) @PathVariable ( "entityId" ) String entityIdStr ,
@Parameter ( description = ATTRIBUTES_SCOPE_DESCRIPTION , schema = @Schema ( allowableValues = { "SERVER_SCOPE" , "SHARED_SCOPE" } , requiredMode = Schema . RequiredMode . REQUIRED ) ) @PathVariable ( "scope" ) AttributeScope scope ,
@io.swagger.v3.oas.annotations.parameters.RequestBody ( description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION , required = true ) @RequestBody JsonNode request ) throws ThingsboardException {
@PostMapping ( value = "/{entityType}/{entityId}/attributes/{scope}" )
public DeferredResult < ResponseEntity > saveEntityAttributesV2 ( @Parameter ( description = ENTITY_TYPE_PARAM_DESCRIPTION , required = true , schema = @Schema ( defaultValue = "DEVICE" ) )
@PathVariable ( "entityType" ) String entityType ,
@Parameter ( description = ENTITY_ID_PARAM_DESCRIPTION , required = true )
@PathVariable ( "entityId" ) String entityIdStr ,
@Parameter ( description = ATTRIBUTES_SCOPE_DESCRIPTION , schema = @Schema ( allowableValues = { "SERVER_SCOPE" , "SHARED_SCOPE" } , requiredMode = Schema . RequiredMode . REQUIRED ) )
@PathVariable ( "scope" ) AttributeScope scope ,
@io.swagger.v3.oas.annotations.parameters.RequestBody ( description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION , required = true )
@RequestBody String request ) throws ThingsboardException {
EntityId entityId = EntityIdFactory . getByTypeAndId ( entityType , entityIdStr ) ;
return saveAttributes ( getTenantId ( ) , entityId , scope , request ) ;
}
@ -460,11 +454,11 @@ public class TelemetryController extends BaseController {
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH )
@ApiResponses ( value = {
@ApiResponse ( responseCode = "200" , description = "Time series for the selected keys in the request was removed. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'." ) ,
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'." ) ,
@ApiResponse ( responseCode = "400" , description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false." ) ,
@ApiResponse ( responseCode = "401" , description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant." ) ,
@ApiResponse ( responseCode = "500" , description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace." ) ,
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace." ) ,
} )
@PreAuthorize ( "hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')" )
@RequestMapping ( value = "/{entityType}/{entityId}/timeseries/delete" , method = RequestMethod . DELETE )
@ -541,11 +535,11 @@ public class TelemetryController extends BaseController {
"Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH )
@ApiResponses ( value = {
@ApiResponse ( responseCode = "200" , description = "Device attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'." ) ,
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'." ) ,
@ApiResponse ( responseCode = "400" , description = "Platform returns a bad request in case if keys or scope are not specified." ) ,
@ApiResponse ( responseCode = "401" , description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant." ) ,
@ApiResponse ( responseCode = "500" , description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace." ) ,
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace." ) ,
} )
@PreAuthorize ( "hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')" )
@RequestMapping ( value = "/{deviceId}/{scope}" , method = RequestMethod . DELETE )
@ -563,11 +557,11 @@ public class TelemetryController extends BaseController {
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH )
@ApiResponses ( value = {
@ApiResponse ( responseCode = "200" , description = "Entity attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'." ) ,
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'." ) ,
@ApiResponse ( responseCode = "400" , description = "Platform returns a bad request in case if keys or scope are not specified." ) ,
@ApiResponse ( responseCode = "401" , description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant." ) ,
@ApiResponse ( responseCode = "500" , description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace." ) ,
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace." ) ,
} )
@PreAuthorize ( "hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')" )
@RequestMapping ( value = "/{entityType}/{entityId}/{scope}" , method = RequestMethod . DELETE )
@ -616,18 +610,24 @@ public class TelemetryController extends BaseController {
} ) ;
}
private DeferredResult < ResponseEntity > saveAttributes ( TenantId srcTenantId , EntityId entityIdSrc , AttributeScope scope , JsonNode json ) throws ThingsboardException {
private DeferredResult < ResponseEntity > saveAttributes ( TenantId srcTenantId , EntityId entityIdSrc , AttributeScope scope , String jsonStr ) throws ThingsboardException {
if ( AttributeScope . SERVER_SCOPE ! = scope & & AttributeScope . SHARED_SCOPE ! = scope ) {
return getImmediateDeferredResult ( "Invalid scope: " + scope , HttpStatus . BAD_REQUEST ) ;
}
if ( json . isObject ( ) ) {
List < AttributeKvEntry > attributes = extractRequestAttributes ( json ) ;
JsonElement json ;
try {
json = JsonParser . parseString ( jsonStr ) ;
} catch ( Exception e ) {
return getImmediateDeferredResult ( "Invalid JSON" , HttpStatus . BAD_REQUEST ) ;
}
if ( json . isJsonObject ( ) ) {
List < AttributeKvEntry > attributes = JsonConverter . convertToAttributes ( json ) ;
if ( attributes . isEmpty ( ) ) {
return getImmediateDeferredResult ( "No attributes data found in request body!" , HttpStatus . BAD_REQUEST ) ;
}
for ( AttributeKvEntry attributeKvEntry : attributes ) {
if ( attributeKvEntry . getKey ( ) . isEmpty ( ) | | attributeKvEntry . getKey ( ) . trim ( ) . length ( ) = = 0 ) {
return getImmediateDeferredResult ( "Key cannot be empty or contains only spaces" , HttpStatus . BAD_REQUEST ) ;
if ( attributeKvEntry . getKey ( ) . isBlank ( ) ) {
return getImmediateDeferredResult ( "Key cannot be blank " , HttpStatus . BAD_REQUEST ) ;
}
}
SecurityUser user = getCurrentUser ( ) ;
@ -885,43 +885,6 @@ public class TelemetryController extends BaseController {
return result ;
}
private List < AttributeKvEntry > extractRequestAttributes ( JsonNode jsonNode ) {
long ts = System . currentTimeMillis ( ) ;
List < AttributeKvEntry > attributes = new ArrayList < > ( ) ;
jsonNode . fields ( ) . forEachRemaining ( entry - > {
String key = entry . getKey ( ) ;
JsonNode value = entry . getValue ( ) ;
if ( entry . getValue ( ) . isObject ( ) | | entry . getValue ( ) . isArray ( ) ) {
attributes . add ( new BaseAttributeKvEntry ( new JsonDataEntry ( key , toJsonStr ( value ) ) , ts ) ) ;
} else if ( entry . getValue ( ) . isTextual ( ) ) {
if ( maxStringValueLength > 0 & & entry . getValue ( ) . textValue ( ) . length ( ) > maxStringValueLength ) {
String message = String . format ( "String value length [%d] for key [%s] is greater than maximum allowed [%d]" , entry . getValue ( ) . textValue ( ) . length ( ) , key , maxStringValueLength ) ;
throw new UncheckedApiException ( new InvalidParametersException ( message ) ) ;
}
attributes . add ( new BaseAttributeKvEntry ( new StringDataEntry ( key , value . textValue ( ) ) , ts ) ) ;
} else if ( entry . getValue ( ) . isBoolean ( ) ) {
attributes . add ( new BaseAttributeKvEntry ( new BooleanDataEntry ( key , value . booleanValue ( ) ) , ts ) ) ;
} else if ( entry . getValue ( ) . isDouble ( ) ) {
attributes . add ( new BaseAttributeKvEntry ( new DoubleDataEntry ( key , value . doubleValue ( ) ) , ts ) ) ;
} else if ( entry . getValue ( ) . isNumber ( ) ) {
if ( entry . getValue ( ) . isBigInteger ( ) ) {
throw new UncheckedApiException ( new InvalidParametersException ( "Big integer values are not supported!" ) ) ;
} else {
attributes . add ( new BaseAttributeKvEntry ( new LongDataEntry ( key , value . longValue ( ) ) , ts ) ) ;
}
}
} ) ;
return attributes ;
}
private String toJsonStr ( JsonNode value ) {
try {
return JacksonUtil . toString ( value ) ;
} catch ( IllegalArgumentException e ) {
throw new JsonParseException ( "Can't parse jsonValue: " + value , e ) ;
}
}
private JsonNode toJsonNode ( String value ) {
try {
return JacksonUtil . toJsonNode ( value ) ;