Browse Source

Merge branch 'master' into fix/3656-mqtt-templates-config

pull/11185/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
6f81a14867
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  2. 25
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 3
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  4. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java
  6. 57
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  7. 2
      application/src/main/resources/thingsboard.yml
  8. 31
      application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
  9. 9
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  10. 5
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  11. 516
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  12. 302
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  13. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  14. 15
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  15. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java
  16. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  17. 19
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java
  18. 10
      ui-ngx/src/app/core/utils.ts
  19. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts
  20. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts
  21. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts
  22. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts
  23. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts
  24. 2
      ui-ngx/src/app/shared/components/json-content.component.html
  25. 2
      ui-ngx/src/app/shared/models/device.models.ts
  26. 4
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  27. 4
      ui-ngx/src/app/shared/models/widget.models.ts

9
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -48,6 +48,7 @@ import org.springdoc.core.models.GroupedOpenApi;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.properties.SwaggerUiConfigProperties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@ -62,7 +63,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
import org.thingsboard.server.exception.ThingsboardErrorResponse;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.auth.rest.LoginResponse;
@ -79,7 +79,7 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Slf4j
@Configuration
@TbCoreComponent
@ConditionalOnExpression("('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && '${springdoc.api-docs.enabled:true}'=='true'")
@Profile("!test")
public class SwaggerConfiguration {
@ -113,7 +113,8 @@ public class SwaggerConfiguration {
private String version;
@Value("${app.version:unknown}")
private String appVersion;
@Value("${swagger.group_name:thingsboard}")
private String groupName;
@Bean
public OpenAPI thingsboardApi() {
@ -212,7 +213,7 @@ public class SwaggerConfiguration {
@Bean
public GroupedOpenApi groupedApi(SpringDocParameterNameDiscoverer localSpringDocParameterNameDiscoverer) {
return GroupedOpenApi.builder()
.group("thingsboard")
.group(groupName)
.pathsToMatch(apiPath)
.addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer))
.addOperationCustomizer(operationCustomizer())

25
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -22,6 +22,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolation;
import lombok.Getter;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -358,29 +359,33 @@ public abstract class BaseController {
private ThingsboardException handleException(Exception exception, boolean logException) {
if (logException && logControllerErrorStackTrace) {
log.error("Error [{}]", exception.getMessage(), exception);
}
String cause = "";
if (exception.getCause() != null) {
cause = exception.getCause().getClass().getCanonicalName();
try {
SecurityUser user = getCurrentUser();
log.error("[{}][{}] Error", user.getTenantId(), user.getId(), exception);
} catch (Exception e) {
log.error("Error", exception);
}
}
Throwable cause = exception.getCause();
if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause.contains("IncorrectParameterException")) {
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
} else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof DataAccessException) {
String errorType = exception.getClass().getSimpleName();
if (!logControllerErrorStackTrace) { // not to log the error twice
log.warn("Database error: {} - {}", errorType, ExceptionUtils.getRootCauseMessage(exception));
log.warn("Database error: {} - {}", exception.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(exception));
}
if (cause instanceof ConstraintViolationException) {
return new ThingsboardException(ExceptionUtils.getRootCause(exception).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
}
return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
}
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL);
}

3
application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java

@ -204,6 +204,9 @@ public class EdgeEventSourcingListener {
return false;
}
}
if (entity instanceof OAuth2Info oAuth2Info) {
return oAuth2Info.isEdgeEnabled();
}
// Default: If the entity doesn't match any of the conditions, consider it as valid.
return true;
}

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java

@ -45,8 +45,11 @@ public class OAuth2EdgeEventFetcher implements EdgeEventFetcher {
@Override
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
List<EdgeEvent> result = new ArrayList<>();
OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info();
if (!oAuth2Info.isEdgeEnabled()) {
return new PageData<>();
}
List<EdgeEvent> result = new ArrayList<>();
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.OAUTH2,
EdgeEventActionType.ADDED, null, JacksonUtil.valueToTree(oAuth2Info)));
// returns PageData object to be in sync with other fetchers

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java

@ -40,7 +40,7 @@ public class OAuth2EdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertOAuth2EventToDownlink(EdgeEvent edgeEvent) {
DownlinkMsg downlinkMsg = null;
OAuth2Info oAuth2Info = JacksonUtil.convertValue(edgeEvent.getBody(), OAuth2Info.class);
if (oAuth2Info != null) {
if (oAuth2Info != null && oAuth2Info.isEdgeEnabled()) {
OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2MsgConstructor.constructOAuth2UpdateMsg(oAuth2Info);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())

57
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -24,12 +24,14 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
@ -340,29 +342,33 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) {
TenantId tenantId = resource.getTenantId();
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setResourceType(resource.getResourceType().name())
.setResourceKey(resource.getResourceKey())
.build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
broadcast(transportMsg, callback);
if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
TenantId tenantId = resource.getTenantId();
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setResourceType(resource.getResourceType().name())
.setResourceKey(resource.getResourceKey())
.build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
}
}
@Override
public void onResourceDeleted(TbResourceInfo resource, TbQueueCallback callback) {
log.trace("[{}] Processing delete resource", resource);
TransportProtos.ResourceDeleteMsg resourceUpdateMsg = TransportProtos.ResourceDeleteMsg.newBuilder()
.setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits())
.setResourceType(resource.getResourceType().name())
.setResourceKey(resource.getResourceKey())
.build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceUpdateMsg).build();
broadcast(transportMsg, callback);
if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
log.trace("[{}][{}][{}] Processing delete resource", resource.getTenantId(), resource.getResourceType(), resource.getResourceKey());
TransportProtos.ResourceDeleteMsg resourceDeleteMsg = TransportProtos.ResourceDeleteMsg.newBuilder()
.setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits())
.setResourceType(resource.getResourceType().name())
.setResourceKey(resource.getResourceKey())
.build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
}
}
private <T> void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
@ -384,8 +390,19 @@ public class DefaultTbClusterService implements TbClusterService {
}
private void broadcast(ToTransportMsg transportMsg, TbQueueCallback callback) {
TbQueueProducer<TbProtoQueueMsg<ToTransportMsg>> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer();
Set<String> tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT);
broadcast(transportMsg, tbTransportServices, callback);
}
private void broadcast(ToTransportMsg transportMsg, String transportType, TbQueueCallback callback) {
Set<String> tbTransportServices = partitionService.getAllServices(ServiceType.TB_TRANSPORT).stream()
.filter(info -> info.getTransportsList().contains(transportType))
.map(TransportProtos.ServiceInfo::getServiceId).collect(Collectors.toSet());
broadcast(transportMsg, tbTransportServices, callback);
}
private void broadcast(ToTransportMsg transportMsg, Set<String> tbTransportServices, TbQueueCallback callback) {
TbQueueProducer<TbProtoQueueMsg<ToTransportMsg>> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer();
TbQueueCallback proxyCallback = callback != null ? new MultipleTbQueueCallbackWrapper(tbTransportServices.size(), callback) : null;
for (String transportServiceId : tbTransportServices) {
TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId);

2
application/src/main/resources/thingsboard.yml

@ -1385,6 +1385,8 @@ swagger:
url: "${SWAGGER_LICENSE_URL:https://github.com/thingsboard/thingsboard/blob/master/LICENSE}"
# The version of the API doc to display. Default to the package version.
version: "${SWAGGER_VERSION:}"
# The group name (definition) on the API doc UI page.
group_name: "${SWAGGER_GROUP_NAME:thingsboard}"
# Queue configuration parameters
queue:

31
application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java

@ -27,13 +27,16 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
@ -48,6 +51,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -547,6 +551,33 @@ public class DashboardControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(dashboardDao, savedTenant.getId(), dashboardId, "/api/dashboard/" + dashboardId);
}
@Test
public void whenDeletingDashboard_ifReferencedByDeviceProfile_thenReturnError() throws Exception {
Dashboard dashboard = createDashboard("test");
DeviceProfile deviceProfile = createDeviceProfile("test");
deviceProfile.setDefaultDashboardId(dashboard.getId());
doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
String response = doDelete("/api/dashboard/" + dashboard.getUuidId()).andExpect(status().isBadRequest())
.andReturn().getResponse().getContentAsString();
String errorMessage = JacksonUtil.toJsonNode(response).get("message").asText();
assertThat(errorMessage).containsIgnoringCase("referenced by a device profile");
}
@Test
public void whenDeletingDashboard_ifReferencedByAssetProfile_thenReturnError() throws Exception {
Dashboard dashboard = createDashboard("test");
AssetProfile assetProfile = createAssetProfile("test");
assetProfile.setDefaultDashboardId(dashboard.getId());
doPost("/api/assetProfile", assetProfile, AssetProfile.class);
String response = doDelete("/api/dashboard/" + dashboard.getUuidId()).andExpect(status().isBadRequest())
.andReturn().getResponse().getContentAsString();
String errorMessage = JacksonUtil.toJsonNode(response).get("message").asText();
assertThat(errorMessage).containsIgnoringCase("referenced by an asset profile");
}
private Dashboard createDashboard(String title) {
Dashboard dashboard = new Dashboard();
dashboard.setTitle(title);

9
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -616,6 +616,14 @@ public class TenantControllerTest extends AbstractControllerTest {
assertThat(usedTpi.getTopic()).isEqualTo(DataConstants.HP_QUEUE_TOPIC);
assertThat(usedTpi.getTenantId()).get().isEqualTo(TenantId.SYS_TENANT_ID);
});
assertThat(partitionService.resolve(ServiceType.TB_RULE_ENGINE, null, tenantId, tenantId)).satisfies(tpi -> {
assertThat(tpi.getTopic()).isEqualTo(MAIN_QUEUE_TOPIC);
assertThat(tpi.getTenantId()).get().isEqualTo(tenantId);
});
assertThat(partitionService.resolve(ServiceType.TB_RULE_ENGINE, "", tenantId, tenantId)).satisfies(tpi -> {
assertThat(tpi.getTopic()).isEqualTo(MAIN_QUEUE_TOPIC);
assertThat(tpi.getTenantId()).get().isEqualTo(tenantId);
});
loginSysAdmin();
tenantProfile.setIsolatedTbRuleEngine(true);
@ -850,4 +858,5 @@ public class TenantControllerTest extends AbstractControllerTest {
testBroadcastEntityStateChangeEventNever(createEntityId_NULL_UUID(new Tenant()));
Mockito.reset(tbClusterService);
}
}

5
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.queue.discovery;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -36,7 +37,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -319,7 +319,7 @@ public class HashPartitionService implements PartitionService {
private QueueKey getQueueKey(ServiceType serviceType, String queueName, TenantId tenantId) {
TenantId isolatedOrSystemTenantId = getIsolatedOrSystemTenantId(serviceType, tenantId);
if (queueName == null) {
if (queueName == null || queueName.isEmpty()) {
queueName = MAIN_QUEUE_NAME;
}
QueueKey queueKey = new QueueKey(serviceType, queueName, isolatedOrSystemTenantId);
@ -672,6 +672,7 @@ public class HashPartitionService implements PartitionService {
public QueueConfig(QueueRoutingInfo queueRoutingInfo) {
this.duplicateMsgToAllPartitions = queueRoutingInfo.isDuplicateMsgToAllPartitions();
}
}
}

516
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -46,11 +46,23 @@ import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import static java.lang.Character.MAX_RADIX;
import static java.lang.Character.MIN_RADIX;
@Slf4j
public class TbUtils {
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
private static final int ZERO_RADIX = 0;
private static final int OCTAL_RADIX = 8;
private static final int DEC_RADIX = 10;
private static final int HEX_RADIX = 16;
private static final int HEX_LEN_MIN = -1;
private static final int HEX_LEN_INT_MAX = 8;
private static final int HEX_LEN_LONG_MAX = 16;
private static final int BYTES_LEN_LONG_MAX = 8;
private static final LinkedHashMap<String, String> mdnEncodingReplacements = new LinkedHashMap<>();
static {
@ -103,6 +115,8 @@ public class TbUtils {
String.class, int.class)));
parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat",
String.class)));
parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat",
String.class, int.class)));
parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble",
String.class)));
parserConfig.addImport("parseLittleEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToInt",
@ -175,6 +189,40 @@ public class TbUtils {
float.class, int.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
ExecutionContext.class, String.class)));
parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex",
Integer.class)));
parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex",
Integer.class, boolean.class)));
parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex",
Integer.class, boolean.class, boolean.class)));
parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex",
Integer.class, boolean.class, boolean.class, int.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class, boolean.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class, boolean.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class, int.class, boolean.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class, int.class, boolean.class, boolean.class)));
parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex",
Float.class)));
parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex",
Float.class, boolean.class)));
parserConfig.addImport("doubleToHex", new MethodStub(TbUtils.class.getMethod("doubleToHex",
Double.class)));
parserConfig.addImport("doubleToHex", new MethodStub(TbUtils.class.getMethod("doubleToHex",
Double.class, boolean.class)));
parserConfig.addImport("printUnsignedBytes", new MethodStub(TbUtils.class.getMethod("printUnsignedBytes",
ExecutionContext.class, List.class)));
parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex",
String.class)));
parserConfig.addImport("base64ToBytes", new MethodStub(TbUtils.class.getMethod("base64ToBytes",
@ -197,6 +245,18 @@ public class TbUtils {
String.class)));
parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI",
String.class)));
parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError",
String.class, Object.class)));
parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError",
String.class)));
parserConfig.addImport("isBinary", new MethodStub(TbUtils.class.getMethod("isBinary",
String.class)));
parserConfig.addImport("isOctal", new MethodStub(TbUtils.class.getMethod("isOctal",
String.class)));
parserConfig.addImport("isDecimal", new MethodStub(TbUtils.class.getMethod("isDecimal",
String.class)));
parserConfig.addImport("isHexadecimal", new MethodStub(TbUtils.class.getMethod("isHexadecimal",
String.class)));
}
public static String btoa(String input) {
@ -210,6 +270,7 @@ public class TbUtils {
public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException {
return TbJson.parse(ctx, bytesToString(bytesList));
}
public static Object decodeToJson(ExecutionContext ctx, String jsonStr) throws IOException {
return TbJson.parse(ctx, jsonStr);
}
@ -269,88 +330,127 @@ public class TbUtils {
}
public static Integer parseInt(String value) {
int radix = getRadix(value);
return parseInt(value, radix);
return parseInt(value, ZERO_RADIX);
}
public static Integer parseInt(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
try {
String valueP = prepareNumberString(value);
isValidRadix(valueP, radix);
try {
return Integer.parseInt(valueP, radix);
} catch (NumberFormatException e) {
BigInteger bi = new BigInteger(valueP, radix);
if (bi.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0)
throw new NumberFormatException("Value \"" + value + "\" is greater than the maximum Integer value " + Integer.MAX_VALUE + " !");
if (bi.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0)
throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Integer value " + Integer.MIN_VALUE + " !");
Float f = parseFloat(valueP);
if (f != null) {
return f.intValue();
} else {
throw new NumberFormatException(e.getMessage());
}
}
} catch (NumberFormatException e) {
throw new NumberFormatException(e.getMessage());
String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue >= 25 && radixValue <= MAX_RADIX) {
return (Integer) compareIntLongValueMinMax(valueP, radixValue, Integer.MAX_VALUE, Integer.MIN_VALUE);
}
return switch (radixValue) {
case MIN_RADIX -> parseBinaryStringAsSignedInteger(valueP);
case OCTAL_RADIX, DEC_RADIX, HEX_RADIX -> Integer.parseInt(valueP, radixValue);
default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]");
};
}
return null;
}
public static Long parseLong(String value) {
int radix = getRadix(value);
return parseLong(value, radix);
return parseLong(value, ZERO_RADIX);
}
public static Long parseLong(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
try {
String valueP = prepareNumberString(value);
isValidRadix(valueP, radix);
try {
return Long.parseLong(valueP, radix);
} catch (NumberFormatException e) {
BigInteger bi = new BigInteger(valueP, radix);
if (bi.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0)
throw new NumberFormatException("Value \"" + value + "\"is greater than the maximum Long value " + Long.MAX_VALUE + " !");
if (bi.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Long value " + Long.MIN_VALUE + " !");
Double dd = parseDouble(valueP);
if (dd != null) {
return dd.longValue();
} else {
throw new NumberFormatException(e.getMessage());
}
}
} catch (NumberFormatException e) {
throw new NumberFormatException(e.getMessage());
String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue >= 25 && radixValue <= MAX_RADIX) {
return (Long) compareIntLongValueMinMax(valueP, radixValue, Long.MAX_VALUE, Long.MIN_VALUE);
}
return switch (radixValue) {
case MIN_RADIX -> parseBinaryStringAsSignedLong(valueP);
case OCTAL_RADIX, DEC_RADIX, HEX_RADIX -> Long.parseLong(valueP, radixValue);
default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]");
};
}
return null;
}
private static int getRadix(String value, int... radixS) {
return radixS.length > 0 ? radixS[0] : isHexadecimal(value) ? 16 : 10;
private static int parseBinaryStringAsSignedInteger(String binaryString) {
if (binaryString.length() != 32) {
// Pad the binary string to 64 bits if it is not already
binaryString = String.format("%32s", binaryString).replace(' ', '0');
}
// If the MSB is 1, the number is negative in two's complement
if (binaryString.charAt(0) == '1') {
// Calculate the two's complement
String invertedBinaryString = invertBinaryString(binaryString);
int positiveValue = Integer.parseInt(invertedBinaryString, MIN_RADIX) + 1;
return -positiveValue;
} else {
return Integer.parseInt(binaryString, MIN_RADIX);
}
}
private static long parseBinaryStringAsSignedLong(String binaryString) {
if (binaryString.length() != 64) {
// Pad the binary string to 64 bits if it is not already
binaryString = String.format("%64s", binaryString).replace(' ', '0');
}
// If the MSB is 1, the number is negative in two's complement
if (binaryString.charAt(0) == '1') {
// Calculate the two's complement
String invertedBinaryString = invertBinaryString(binaryString);
long positiveValue = Long.parseLong(invertedBinaryString, MIN_RADIX) + 1;
return -positiveValue;
} else {
return Long.parseLong(binaryString, MIN_RADIX);
}
}
private static String invertBinaryString(String binaryString) {
StringBuilder invertedString = new StringBuilder();
for (char bit : binaryString.toCharArray()) {
invertedString.append(bit == '0' ? '1' : '0');
}
return invertedString.toString();
}
private static int getRadix10_16(String value) {
int radix = isDecimal(value) > 0 ? DEC_RADIX : isHexadecimal(value);
if (radix > 0) {
return radix;
} else {
throw new NumberFormatException("Value: \"" + value + "\" is not numeric or hexDecimal format!");
}
}
public static Float parseFloat(String value) {
if (value != null) {
try {
return Float.parseFloat(prepareNumberString(value));
} catch (NumberFormatException e) {
return parseFloat(value, 0);
}
public static Float parseFloat(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue == DEC_RADIX) {
return Float.parseFloat(value);
} else {
int bits = Integer.parseUnsignedInt(valueP, HEX_RADIX);
return Float.intBitsToFloat(bits);
}
}
return null;
}
public static Double parseDouble(String value) {
int radix = getRadix10_16(value);
return parseDouble(value, radix);
}
public static Double parseDouble(String value, int radix) {
if (value != null) {
try {
String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue == DEC_RADIX) {
return Double.parseDouble(prepareNumberString(value));
} catch (NumberFormatException e) {
} else {
long bits = Long.parseUnsignedLong(valueP, HEX_RADIX);
return Double.longBitsToDouble(bits);
}
}
return null;
@ -368,9 +468,10 @@ public class TbUtils {
return parseHexToInt(hex, true);
}
public static int parseHexToInt(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 8);
return parseBytesToInt(data, 0, data.length, bigEndian);
public static Integer parseHexToInt(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? hexValue : reverseHexStringByOrder(hexValue);
return parseInt(hex, HEX_RADIX);
}
public static long parseLittleEndianHexToLong(String hex) {
@ -385,9 +486,10 @@ public class TbUtils {
return parseHexToLong(hex, true);
}
public static long parseHexToLong(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 16);
return parseBytesToLong(data, 0, data.length, bigEndian);
public static Long parseHexToLong(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseLong(hex, HEX_RADIX);
}
public static float parseLittleEndianHexToFloat(String hex) {
@ -402,9 +504,10 @@ public class TbUtils {
return parseHexToFloat(hex, true);
}
public static float parseHexToFloat(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 8);
return parseBytesToFloat(data, 0, bigEndian);
public static Float parseHexToFloat(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseFloat(hex, HEX_RADIX);
}
public static double parseLittleEndianHexToDouble(String hex) {
@ -419,39 +522,171 @@ public class TbUtils {
return parseHexToDouble(hex, true);
}
public static double parseHexToDouble(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 16);
return parseBytesToDouble(data, 0, bigEndian);
public static double parseHexToDouble(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseDouble(hex, HEX_RADIX);
}
private static byte[] prepareHexToBytesNumber(String hex, int len) {
int length = hex.length();
if (length > len) {
throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed.");
}
if (length % 2 > 0) {
public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String value) {
String hex = prepareNumberString(value);
int len = hex.length();
if (len % 2 > 0) {
throw new IllegalArgumentException("Hex string must be even-length.");
}
byte[] data = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16));
ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx);
for (int i = 0; i < hex.length(); i += 2) {
// Extract two characters from the hex string
String byteString = hex.substring(i, i + 2);
// Parse the hex string to a byte
byte byteValue = (byte) Integer.parseInt(byteString, HEX_RADIX);
// Add the byte to the ArrayList
data.add(byteValue);
}
return data;
}
public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String hex) {
int len = hex.length();
if (len % 2 > 0) {
throw new IllegalArgumentException("Hex string must be even-length.");
}
ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx);
for (int i = 0; i < len; i += 2) {
data.add((byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16)));
public static List<Integer> printUnsignedBytes(ExecutionContext ctx, List<Byte> byteArray) {
ExecutionArrayList<Integer> data = new ExecutionArrayList<>(ctx);
for (Byte b : byteArray) {
// Convert signed byte to unsigned integer
int unsignedByte = Byte.toUnsignedInt(b);
data.add(unsignedByte);
}
return data;
}
public static String intToHex(Integer i) {
return prepareNumberHexString(i.longValue(), true, false, HEX_LEN_MIN, HEX_LEN_INT_MAX);
}
public static String intToHex(Integer i, boolean bigEndian) {
return prepareNumberHexString(i.longValue(), bigEndian, false, HEX_LEN_MIN, HEX_LEN_INT_MAX);
}
public static String intToHex(Integer i, boolean bigEndian, boolean pref) {
return prepareNumberHexString(i.longValue(), bigEndian, pref, HEX_LEN_MIN, HEX_LEN_INT_MAX);
}
public static String intToHex(Integer i, boolean bigEndian, boolean pref, int len) {
return prepareNumberHexString(i.longValue(), bigEndian, pref, len, HEX_LEN_INT_MAX);
}
public static String longToHex(Long l) {
return prepareNumberHexString(l, true, false, HEX_LEN_MIN, HEX_LEN_LONG_MAX);
}
public static String longToHex(Long l, boolean bigEndian) {
return prepareNumberHexString(l, bigEndian, false, HEX_LEN_MIN, HEX_LEN_LONG_MAX);
}
public static String longToHex(Long l, boolean bigEndian, boolean pref) {
return prepareNumberHexString(l, bigEndian, pref, HEX_LEN_MIN, HEX_LEN_LONG_MAX);
}
public static String longToHex(Long l, boolean bigEndian, boolean pref, int len) {
return prepareNumberHexString(l, bigEndian, pref, len, HEX_LEN_LONG_MAX);
}
public static String intLongToString(Long number) {
return intLongToString(number, DEC_RADIX);
}
public static String intLongToString(Long number, int radix) {
return intLongToString(number, radix, true);
}
public static String intLongToString(Long number, int radix, boolean bigEndian) {
return intLongToString(number, radix, bigEndian, false);
}
public static String intLongToString(Long number, int radix, boolean bigEndian, boolean pref) {
if (radix >= 25 && radix <= MAX_RADIX) {
return Long.toString(number, radix);
}
return switch (radix) {
case MIN_RADIX -> Long.toBinaryString(number);
case OCTAL_RADIX -> Long.toOctalString(number);
case DEC_RADIX -> Long.toString(number);
case HEX_RADIX -> prepareNumberHexString(number, bigEndian, pref, -1, -1);
default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]");
};
}
private static Number compareIntLongValueMinMax(String valueP, int radix, Number maxValue, Number minValue) {
boolean isInteger = maxValue.getClass().getSimpleName().equals("Integer");
try {
if (isInteger) {
return Integer.parseInt(valueP, radix);
} else {
return Long.parseLong(valueP, radix);
}
} catch (NumberFormatException e) {
BigInteger bi = new BigInteger(valueP, radix);
long maxValueL = isInteger ? maxValue.longValue() : (long) maxValue;
if (bi.compareTo(BigInteger.valueOf(maxValueL)) > 0) {
throw new NumberFormatException("Value \"" + valueP + "\"is greater than the maximum " + maxValue.getClass().getSimpleName() + " value " + maxValue + " !");
}
long minValueL = isInteger ? minValue.longValue() : (long) minValue;
if (bi.compareTo(BigInteger.valueOf(minValueL)) < 0) {
throw new NumberFormatException("Value \"" + valueP + "\" is less than the minimum " + minValue.getClass().getSimpleName() + " value " + minValue + " !");
}
throw new NumberFormatException(e.getMessage());
}
}
private static String prepareNumberHexString(Long number, boolean bigEndian, boolean pref, int len, int hexLenMax) {
String hex = Long.toHexString(number).toUpperCase();
hexLenMax = hexLenMax < 0 ? hex.length() : hexLenMax;
String hexWithoutZeroFF = removeLeadingZero_FF(hex, number, hexLenMax);
hexWithoutZeroFF = bigEndian ? hexWithoutZeroFF : reverseHexStringByOrder(hexWithoutZeroFF);
len = len == HEX_LEN_MIN ? hexWithoutZeroFF.length() : len;
String result = hexWithoutZeroFF.substring(hexWithoutZeroFF.length() - len);
return pref ? "0x" + result : result;
}
private static String removeLeadingZero_FF(String hex, Long number, int hexLenMax) {
String hexWithoutZero = hex.replaceFirst("^0+(?!$)", ""); // Remove leading zeros except for the last one
hexWithoutZero = hexWithoutZero.length() % 2 > 0 ? "0" + hexWithoutZero : hexWithoutZero;
if (number >= 0) {
return hexWithoutZero;
} else {
String hexWithoutZeroFF = hexWithoutZero.replaceFirst("^F+(?!$)", "");
hexWithoutZeroFF = hexWithoutZeroFF.length() % 2 > 0 ? "F" + hexWithoutZeroFF : hexWithoutZeroFF;
if (hexWithoutZeroFF.length() > hexLenMax) {
return hexWithoutZeroFF.substring(hexWithoutZeroFF.length() - hexLenMax);
} else if (hexWithoutZeroFF.length() == hexLenMax) {
return hexWithoutZeroFF;
} else {
return "FF" + hexWithoutZeroFF;
}
}
}
public static String floatToHex(Float f) {
return floatToHex(f, true);
}
public static String floatToHex(Float f, boolean bigEndian) {
// Convert the float to its raw integer bits representation
int bits = Float.floatToRawIntBits(f);
// Format the integer bits as a hexadecimal string
String result = String.format("0x%08X", bits);
return bigEndian ? result : reverseHexStringByOrder(result);
}
public static String doubleToHex(Double d) {
return doubleToHex(d, true);
}
public static String doubleToHex(Double d, boolean bigEndian) {
long bits = Double.doubleToRawLongBits(d);
// Format the integer bits as a hexadecimal string
String result = String.format("0x%16X", bits);
return bigEndian ? result : reverseHexStringByOrder(result);
}
public static String base64ToHex(String base64) {
return bytesToHex(Base64.getDecoder().decode(base64));
}
@ -520,8 +755,8 @@ public class TbUtils {
if (offset > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!");
}
if (length > 8) {
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum 4 bytes is allowed!");
if (length > BYTES_LEN_LONG_MAX) {
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + BYTES_LEN_LONG_MAX + " bytes is allowed!");
}
if (offset + length > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!");
@ -567,7 +802,7 @@ public class TbUtils {
}
public static double parseBytesToDouble(byte[] data, int offset, boolean bigEndian) {
byte[] bytesToNumber = prepareBytesToNumber(data, offset, 8, bigEndian);
byte[] bytesToNumber = prepareBytesToNumber(data, offset, BYTES_LEN_LONG_MAX, bigEndian);
return ByteBuffer.wrap(bytesToNumber).getDouble();
}
@ -647,6 +882,15 @@ public class TbUtils {
return URLDecoder.decode(uri, StandardCharsets.UTF_8);
}
public static void raiseError(String message) {
raiseError(message, null);
}
public static void raiseError(String message, Object value) {
String msg = value == null ? message : message + " for value " + value;
throw new RuntimeException(msg);
}
private static void parseRecursive(Object json, Map<String, Object> map, List<String> excludeList, String path, boolean pathInKey) {
if (json instanceof Map.Entry) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) json;
@ -687,42 +931,104 @@ public class TbUtils {
}
}
private static boolean isHexadecimal(String value) {
return value != null && (value.contains("0x") || value.contains("0X"));
}
private static String prepareNumberString(String value) {
if (value != null) {
value = value.trim();
if (isHexadecimal(value)) {
value = value.replace("0x", "");
value = value.replace("0X", "");
}
value = value.replace("0x", "");
value = value.replace("0X", "");
value = value.replace(",", ".");
}
return value;
}
private static boolean isValidRadix(String value, int radix) {
for (int i = 0; i < value.length(); i++) {
if (i == 0 && value.charAt(i) == '-') {
if (value.length() == 1)
throw new NumberFormatException("Failed radix [" + radix + "] for value: \"" + value + "\"!");
else
continue;
private static int isValidStringAndRadix(String valueP, int radix, String value) {
int radixValue;
if (radix == 0) {
radixValue = getRadix10_16(valueP);
} else if (radix >= 25 && radix <= MAX_RADIX) {
return radix;
} else {
radixValue = switch (radix) {
case MIN_RADIX -> isBinary(valueP);
case OCTAL_RADIX -> isOctal(valueP);
case DEC_RADIX -> isDecimal(valueP);
case HEX_RADIX -> isHexadecimal(valueP);
default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]");
};
}
if (radixValue > 0) {
if (value.startsWith("0x")) radixValue = HEX_RADIX;
if (radixValue == HEX_RADIX) {
valueP = value.startsWith("-") ? value.substring(1) : value;
if (valueP.length() % 2 > 0) {
throw new NumberFormatException("The hexadecimal value: \"" + value + "\" must be of even length, or if the decimal value must be a number!");
}
}
return radixValue;
} else {
if (radix > 0) {
throw new NumberFormatException("Failed radix [" + radix + "] for value: \"" + value + "\", must be [" + radixValue + "] !");
} else {
throw new NumberFormatException("Invalid \"" + value + "\". It is not numeric or hexadecimal format!");
}
if (Character.digit(value.charAt(i), radix) < 0)
throw new NumberFormatException("Failed radix: [" + radix + "] for value: \"" + value + "\"!");
}
return true;
}
private static byte isValidIntegerToByte (Integer val) {
if (val > 255 || val.intValue() < -128) {
public static int isBinary(String str) {
if (str == null || str.isEmpty()) {
return -1;
}
return str.matches("[01]+") ? MIN_RADIX : -1;
}
public static int isOctal(String str) {
if (str == null || str.isEmpty()) {
return -1;
}
return str.matches("[0-7]+") ? OCTAL_RADIX : -1;
}
public static int isDecimal(String str) {
if (str == null || str.isEmpty()) {
return -1;
}
return str.matches("-?\\d+(\\.\\d+)?") ? DEC_RADIX : -1;
}
public static int isHexadecimal(String str) {
if (str == null || str.isEmpty()) {
return -1;
}
return str.matches("^-?(0[xX])?[0-9a-fA-F]+$") ? HEX_RADIX : -1;
}
private static byte isValidIntegerToByte(Integer val) {
if (val > 255 || val < -128) {
throw new NumberFormatException("The value '" + val + "' could not be correctly converted to a byte. " +
"Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!");
} else {
return val.byteValue();
}
}
private static String reverseHexStringByOrder(String value) {
if (value.startsWith("-")) {
throw new IllegalArgumentException("The hexadecimal string must be without a negative sign.");
}
boolean isHexPref = value.startsWith("0x");
String hex = isHexPref ? value.substring(2) : value;
if (hex.length() % 2 > 0) {
throw new IllegalArgumentException("The hexadecimal string must be even-length.");
}
// Split the hex string into bytes (2 characters each)
StringBuilder reversedHex = new StringBuilder(BYTES_LEN_LONG_MAX);
for (int i = hex.length() - 2; i >= 0; i -= 2) {
reversedHex.append(hex, i, i + 2);
}
String result = reversedHex.toString();
return isHexPref ? "0x" + result : result;
}
}

302
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -15,12 +15,14 @@
*/
package org.thingsboard.script.api.tbel;
import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mvel2.ExecutionContext;
import org.mvel2.ParserContext;
import org.mvel2.SandboxedParserConfiguration;
@ -31,28 +33,28 @@ import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import static java.lang.Character.MAX_RADIX;
import static java.lang.Character.MIN_RADIX;
@Slf4j
public class TbUtilsTest {
private ExecutionContext ctx;
private final String intValHex = "41EA62CC";
private final float floatVal = 29.29824f;
private final String floatValStr = "29.29824";
private final String floatValHexRev = "CC62EA41";
private final float floatValRev = -5.948442E7f;
private final long longVal = 0x409B04B10CB295EAL;
private final String longValHex = "409B04B10CB295EA";
private final long longValRev = 0xEA95B20CB1049B40L;
private final String longValHexRev = "EA95B20CB1049B40";
private final String doubleValStr = "1729.1729";
private final double doubleVal = 1729.1729;
private final double doubleValRev = -2.7208640774822924E205;
@ -86,10 +88,9 @@ public class TbUtilsTest {
Assertions.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false));
Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true));
Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("CCBBAA", false));
Assertions.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("AABBCCDD", true));
Assertions.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("DDCCBBAA", false));
Assertions.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("DDCCBBAA", true));
Assertions.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("AABBCCDD", false));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseHexToInt("AABBCCDD", true));
Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("11BBCC22", true));
Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("22CCBB11", false));
}
@Test
@ -208,15 +209,28 @@ public class TbUtilsTest {
Assertions.assertNull(TbUtils.parseInt(""));
Assertions.assertNull(TbUtils.parseInt(" "));
Assertions.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("0"));
Assertions.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("-0"));
Assertions.assertEquals((Integer) 0, TbUtils.parseInt("0"));
Assertions.assertEquals((Integer) 0, TbUtils.parseInt("-0"));
Assertions.assertEquals(java.util.Optional.of(473).get(), TbUtils.parseInt("473"));
Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-0xFF"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FF"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("-0xFF123"));
Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-FF"));
Assertions.assertEquals(java.util.Optional.of(255).get(), TbUtils.parseInt("FF"));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("FFF"));
Assertions.assertEquals(java.util.Optional.of(-2578).get(), TbUtils.parseInt("-0A12"));
Assertions.assertEquals(java.util.Optional.of(-2578).get(), TbUtils.parseHexToInt("-0A12"));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseHexToInt("A12", false));
Assertions.assertEquals(java.util.Optional.of(-14866).get(), TbUtils.parseBigEndianHexToInt("-3A12"));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseLittleEndianHexToInt("-A12"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("0xFG"));
Assertions.assertEquals(java.util.Optional.of(102).get(), TbUtils.parseInt("1100110", 2));
Assertions.assertEquals(java.util.Optional.of(-102).get(), TbUtils.parseInt("1111111111111111111111111111111111111111111111111111111110011010", 2));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("1100210", 2));
Assertions.assertEquals(java.util.Optional.of(13158).get(), TbUtils.parseInt("11001101100110", 2));
Assertions.assertEquals(java.util.Optional.of(-13158).get(), TbUtils.parseInt("1111111111111111111111111111111111111111111111111100110010011010", 2));
Assertions.assertEquals(java.util.Optional.of(63).get(), TbUtils.parseInt("77", 8));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("18", 8));
@ -224,19 +238,32 @@ public class TbUtilsTest {
Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-FF", 16));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FG", 16));
Assertions.assertEquals(java.util.Optional.of(Integer.MAX_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MAX_VALUE), 10));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.valueOf(1)).toString(10), 10));
Assertions.assertEquals(java.util.Optional.of(Integer.MIN_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MIN_VALUE), 10));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MIN_VALUE).subtract(BigInteger.valueOf(1)).toString(10), 10));
Assertions.assertEquals(java.util.Optional.of(506070563).get(), TbUtils.parseInt("KonaIn", 30));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("KonaIn", 10));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(".456", 10));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("4562.", 10));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("KonaIn", MAX_RADIX + 1));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("KonaIn", MIN_RADIX - 1));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("KonaIn", 12));
}
@Test
public void parseFloat() {
String floatValStr = "29.29824";
Assertions.assertEquals(java.util.Optional.of(floatVal).get(), TbUtils.parseFloat(floatValStr));
String floatValHex = "41EA62CC";
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseHexToFloat(floatValHex)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseHexToFloat(floatValHex, false)));
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBigEndianHexToFloat(floatValHex)));
String floatValHexRev = "CC62EA41";
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev)));
}
@Test
@ -246,21 +273,13 @@ public class TbUtilsTest {
Assertions.assertEquals(0, Float.compare(29.298f, actualF));
}
@Test
public void parseHexToFloat() {
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseHexToFloat(intValHex)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseHexToFloat(intValHex, false)));
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBigEndianHexToFloat(intValHex)));
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev)));
}
@Test
public void arseBytesToFloat() {
byte[] floatValByte = {65, -22, 98, -52};
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValByte, 0)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValByte, 0, false)));
List <Byte> floatVaList = Bytes.asList(floatValByte);
List<Byte> floatVaList = Bytes.asList(floatValByte);
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatVaList, 0)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatVaList, 0, false)));
}
@ -271,14 +290,15 @@ public class TbUtilsTest {
Assertions.assertNull(TbUtils.parseLong(""));
Assertions.assertNull(TbUtils.parseLong(" "));
Assertions.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("0"));
Assertions.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("-0"));
Assertions.assertEquals((Long) 0L, TbUtils.parseLong("0"));
Assertions.assertEquals((Long) 0L, TbUtils.parseLong("-0"));
Assertions.assertEquals(java.util.Optional.of(473L).get(), TbUtils.parseLong("473"));
Assertions.assertEquals(java.util.Optional.of(-65535L).get(), TbUtils.parseLong("-0xFFFF"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("FFFFFFFF"));
Assertions.assertEquals(java.util.Optional.of(4294967295L).get(), TbUtils.parseLong("FFFFFFFF"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("0xFGFFFFFF"));
Assertions.assertEquals(java.util.Optional.of(13158L).get(), TbUtils.parseLong("11001101100110", 2));
Assertions.assertEquals(java.util.Optional.of(-13158L).get(), TbUtils.parseLong("1111111111111111111111111111111111111111111111111100110010011010", 2));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("11001101100210", 2));
Assertions.assertEquals(java.util.Optional.of(9223372036854775807L).get(), TbUtils.parseLong("777777777777777777777", 8));
@ -295,10 +315,7 @@ public class TbUtilsTest {
Assertions.assertEquals(java.util.Optional.of(218840926543L).get(), TbUtils.parseLong("KonaLong", 27));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("KonaLong", 10));
}
@Test
public void parseHexToLong() {
Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHex));
Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHexRev, false));
Assertions.assertEquals(longVal, TbUtils.parseBigEndianHexToLong(longValHex));
@ -312,14 +329,20 @@ public class TbUtilsTest {
Bytes.reverse(longValByte);
Assertions.assertEquals(longVal, TbUtils.parseBytesToLong(longValByte, 0, 8, false));
List <Byte> longVaList = Bytes.asList(longValByte);
List<Byte> longVaList = Bytes.asList(longValByte);
Assertions.assertEquals(longVal, TbUtils.parseBytesToLong(longVaList, 0, 8, false));
long longValRev = 0xEA95B20CB1049B40L;
Assertions.assertEquals(longValRev, TbUtils.parseBytesToLong(longVaList, 0, 8));
}
@Test
public void parsDouble() {
String doubleValStr = "1729.1729";
Assertions.assertEquals(java.util.Optional.of(doubleVal).get(), TbUtils.parseDouble(doubleValStr));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false)));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex)));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev)));
}
@Test
@ -329,21 +352,13 @@ public class TbUtilsTest {
Assertions.assertEquals(0, Double.compare(1729.173, actualD));
}
@Test
public void parseHexToDouble() {
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false)));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex)));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev)));
}
@Test
public void parseBytesToDouble() {
byte[] doubleValByte = {64, -101, 4, -79, 12, -78, -107, -22};
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValByte, 0)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValByte, 0, false)));
List <Byte> doubleVaList = Bytes.asList(doubleValByte);
List<Byte> doubleVaList = Bytes.asList(doubleValByte);
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleVaList, 0)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false)));
}
@ -354,16 +369,17 @@ public class TbUtilsTest {
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world");
List<Byte> expectedBytes = TbUtils.stringToBytes(ctx, expectedStr);
Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes);
Assertions.assertEquals(expectedJson,actualJson);
Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes);
Assertions.assertEquals(expectedJson, actualJson);
}
@Test
public void parseStringDecodeToJson() throws IOException {
String expectedStr = "{\"hello\": \"world\"}";
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world");
Object actualJson = TbUtils.decodeToJson(ctx, expectedStr);
Assertions.assertEquals(expectedJson,actualJson);
Object actualJson = TbUtils.decodeToJson(ctx, expectedStr);
Assertions.assertEquals(expectedJson, actualJson);
}
@Test
@ -386,8 +402,8 @@ public class TbUtilsTest {
@Test
public void bytesFromList() {
byte[] arrayBytes = {(byte)0x00, (byte)0x08, (byte)0x10, (byte)0x1C, (byte)0xFF, (byte)0xFC, (byte)0xAD, (byte)0x88, (byte)0x75, (byte)0x74, (byte)0x8A, (byte)0x82};
Object[] arrayMix = { "0x00", 8, "16", "0x1C", 255, (byte)0xFC, 173, 136, 117, 116, -118, "-126"};
byte[] arrayBytes = {(byte) 0x00, (byte) 0x08, (byte) 0x10, (byte) 0x1C, (byte) 0xFF, (byte) 0xFC, (byte) 0xAD, (byte) 0x88, (byte) 0x75, (byte) 0x74, (byte) 0x8A, (byte) 0x82};
Object[] arrayMix = {"0x00", 8, "16", "0x1C", 255, (byte) 0xFC, 173, 136, 117, 116, -118, "-126"};
String expected = new String(arrayBytes);
ArrayList<Byte> listBytes = new ArrayList<>(arrayBytes.length);
@ -397,12 +413,36 @@ public class TbUtilsTest {
Assertions.assertEquals(expected, TbUtils.bytesToString(listBytes));
ArrayList<Object> listMix = new ArrayList<>(arrayMix.length);
for (Object element : arrayMix) {
listMix.add(element);
}
Collections.addAll(listMix, arrayMix);
Assertions.assertEquals(expected, TbUtils.bytesToString(listMix));
}
@Test
public void bytesFromList_SpecSymbol() {
List<String> listHex = new ArrayList<>(Arrays.asList("1D", "0x1D", "1F", "0x1F", "0x20", "0x20"));
byte[] expectedBytes = new byte[]{29, 29, 31, 31, 32, 32};
String actualStr = TbUtils.bytesToString(listHex);
byte[] actualBytes = actualStr.getBytes();
Assertions.assertArrayEquals(expectedBytes, actualBytes);
Assertions.assertTrue(actualStr.isBlank());
listHex = new ArrayList<>(Arrays.asList("0x21", "0x21"));
expectedBytes = new byte[]{33, 33};
actualStr = TbUtils.bytesToString(listHex);
actualBytes = actualStr.getBytes();
Assertions.assertArrayEquals(expectedBytes, actualBytes);
Assertions.assertFalse(actualStr.isBlank());
Assertions.assertEquals("!!", actualStr);
listHex = new ArrayList<>(Arrays.asList("21", "0x21"));
expectedBytes = new byte[]{21, 33};
actualStr = TbUtils.bytesToString(listHex);
actualBytes = actualStr.getBytes();
Assertions.assertArrayEquals(expectedBytes, actualBytes);
Assertions.assertFalse(actualStr.isBlank());
Assertions.assertEquals("!", actualStr.substring(1));
Assertions.assertEquals('\u0015', actualStr.charAt(0));
Assertions.assertEquals(21, actualStr.charAt(0));
}
@Test
public void bytesFromList_Error() {
List<String> listHex = new ArrayList<>();
@ -411,14 +451,7 @@ public class TbUtilsTest {
TbUtils.bytesToString(listHex);
Assertions.fail("Should throw NumberFormatException");
} catch (NumberFormatException e) {
Assertions.assertTrue(e.getMessage().contains("Failed radix: [16] for value: \"FG\"!"));
}
listHex.add(0, "1F");
try {
TbUtils.bytesToString(listHex);
Assertions.fail("Should throw NumberFormatException");
} catch (NumberFormatException e) {
Assertions.assertTrue(e.getMessage().contains("Failed radix: [10] for value: \"1F\"!"));
Assertions.assertTrue(e.getMessage().contains("Value: \"FG\" is not numeric or hexDecimal format!"));
}
List<String> listIntString = new ArrayList<>();
@ -482,6 +515,161 @@ public class TbUtilsTest {
String uriDecodeActual = TbUtils.decodeURI(uriEncodeActual);
Assertions.assertEquals(uriOriginal, uriDecodeActual);
}
@Test
public void intToHex_Test() {
Assertions.assertEquals("FFF5EE", TbUtils.intToHex(-2578));
Assertions.assertEquals("0xFFD8FFA6", TbUtils.intToHex(0xFFD8FFA6, true, true));
Assertions.assertEquals("0xA6FFD8FF", TbUtils.intToHex(0xFFD8FFA6, false, true));
Assertions.assertEquals("0x7FFFFFFF", TbUtils.intToHex(Integer.MAX_VALUE, true, true));
Assertions.assertEquals("0x80000000", TbUtils.intToHex(Integer.MIN_VALUE, true, true));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, true, true));
Assertions.assertEquals("0xABCD", TbUtils.intToHex(0xABCD, true, true));
Assertions.assertEquals("0xABCDEF", TbUtils.intToHex(0xABCDEF, true, true));
Assertions.assertEquals("0xCDAB", TbUtils.intToHex(0xABCDEF, false, true, 4));
Assertions.assertEquals("0xAB", TbUtils.intToHex(171, true, true));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, false, true));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, true, true, 2));
Assertions.assertEquals("AB", TbUtils.intToHex(0xAB, false, false, 2));
Assertions.assertEquals("AB", TbUtils.intToHex(171, true, false));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, true, true));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, false, true));
Assertions.assertEquals("AB", TbUtils.intToHex(0xAB, false, false));
Assertions.assertEquals("0xABCD", TbUtils.intToHex(0xABCD, true, true));
Assertions.assertEquals("0xCDAB", TbUtils.intToHex(0xABCD, false, true));
Assertions.assertEquals("0xCD", TbUtils.intToHex(0xABCD, true, true, 2));
Assertions.assertEquals("AB", TbUtils.intToHex(0xABCD, false, false, 2));
}
@Test
public void longToHex_Test() {
Assertions.assertEquals("0x7FFFFFFFFFFFFFFF", TbUtils.longToHex(Long.MAX_VALUE, true, true));
Assertions.assertEquals("0x8000000000000000", TbUtils.longToHex(Long.MIN_VALUE, true, true));
Assertions.assertEquals("0xFFD8FFA6FFD8FFA6", TbUtils.longToHex(0xFFD8FFA6FFD8FFA6L, true, true));
Assertions.assertEquals("0xA6FFD8FFA6FFCEFF", TbUtils.longToHex(0xFFCEFFA6FFD8FFA6L, false, true));
Assertions.assertEquals("0xAB", TbUtils.longToHex(0xABL, true, true));
Assertions.assertEquals("0xABCD", TbUtils.longToHex(0xABCDL, true, true));
Assertions.assertEquals("0xABCDEF", TbUtils.longToHex(0xABCDEFL, true, true));
Assertions.assertEquals("0xABEFCDAB", TbUtils.longToHex(0xABCDEFABCDEFL, false, true, 8));
Assertions.assertEquals("0xAB", TbUtils.longToHex(0xABL, true, true, 2));
Assertions.assertEquals("AB", TbUtils.longToHex(0xABL, false, false, 2));
Assertions.assertEquals("0xFFA6", TbUtils.longToHex(0xFFD8FFA6FFD8FFA6L, true, true, 4));
Assertions.assertEquals("D8FF", TbUtils.longToHex(0xFFD8FFA6FFD8FFA6L, false, false, 4));
}
@Test
public void numberToString_Test() {
Assertions.assertEquals("11001101100110", TbUtils.intLongToString(13158L, 2));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToString(-102L, 2));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToString(-13158L, 2));
Assertions.assertEquals("777777777777777777777", TbUtils.intLongToString(Long.MAX_VALUE, 8));
Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToString(Long.MIN_VALUE, 8));
Assertions.assertEquals("9223372036854775807", TbUtils.intLongToString(Long.MAX_VALUE));
Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToString(Long.MIN_VALUE));
Assertions.assertEquals("3366", TbUtils.intLongToString(13158L, 16));
Assertions.assertEquals("FFCC9A", TbUtils.intLongToString(-13158L, 16));
Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToString(-13158L, 16, true, true));
Assertions.assertEquals("0x0400", TbUtils.intLongToString(1024L, 16, true, true));
Assertions.assertNotEquals("400", TbUtils.intLongToString(1024L, 16));
Assertions.assertEquals("0xFFFC00", TbUtils.intLongToString(-1024L, 16, true, true));
Assertions.assertNotEquals("0xFC00", TbUtils.intLongToString(-1024L, 16, true, true));
Assertions.assertEquals("hazelnut", TbUtils.intLongToString(1356099454469L, MAX_RADIX));
}
@Test
public void intToHexWithPrintUnsignedBytes_Test() {
Integer value = -40;
String intToHexLe = TbUtils.intToHex(value, false, true);
String intToHexBe = TbUtils.intToHex(value, true, true);
List<Byte> hexTopByteLe = TbUtils.hexToBytes(ctx, intToHexLe);
List<Byte> hexTopByteBe = TbUtils.hexToBytes(ctx, intToHexBe);
byte[] arrayBytes = {-40, -1};
List<Byte> expectedHexTopByteLe = Bytes.asList(arrayBytes);
List<Byte> expectedHexTopByteBe = Lists.reverse(expectedHexTopByteLe);
Assertions.assertEquals(expectedHexTopByteLe, hexTopByteLe);
Assertions.assertEquals(expectedHexTopByteBe, hexTopByteBe);
List<Integer> actualLe = TbUtils.printUnsignedBytes(ctx, hexTopByteLe);
List<Integer> actualBe = TbUtils.printUnsignedBytes(ctx, hexTopByteBe);
List<Integer> expectedLe = Ints.asList(216, 255);
List<Integer> expectedBe = Lists.reverse(expectedLe);
Assertions.assertEquals(expectedLe, actualLe);
Assertions.assertEquals(expectedBe, actualBe);
}
@Test
public void floatToHex_Test() {
Float value = 20.89f;
String expectedHex = "0x41A71EB8";
String valueHexRev = "0xB81EA741";
String actual = TbUtils.floatToHex(value);
Assertions.assertEquals(expectedHex, actual);
Float valueActual = TbUtils.parseHexToFloat(actual);
Assertions.assertEquals(value, valueActual);
valueActual = TbUtils.parseHexToFloat(valueHexRev, false);
Assertions.assertEquals(value, valueActual);
}
@Test
public void doubleToHex_Test() {
String expectedHex = "0x409B04B10CB295EA";
String actual = TbUtils.doubleToHex(doubleVal);
Assertions.assertEquals(expectedHex, actual);
Double valueActual = TbUtils.parseHexToDouble(actual);
Assertions.assertEquals(doubleVal, valueActual);
actual = TbUtils.doubleToHex(doubleVal, false);
String expectedHexRev = "0xEA95B20CB1049B40";
Assertions.assertEquals(expectedHexRev, actual);
}
@Test
public void raiseError_Test() {
String message = "frequency_weighting_type must be 0, 1 or 2.";
Object value = 4;
try {
TbUtils.raiseError(message, value);
Assertions.fail("Should throw NumberFormatException");
} catch (RuntimeException e) {
Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. for value 4"));
}
try {
TbUtils.raiseError(message);
Assertions.fail("Should throw NumberFormatException");
} catch (RuntimeException e) {
Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2."));
}
}
@Test
public void isBinary_Test() {
Assertions.assertEquals(2, TbUtils.isBinary("1100110"));
Assertions.assertEquals(-1, TbUtils.isBinary("2100110"));
}
@Test
public void isOctal_Test() {
Assertions.assertEquals(8, TbUtils.isOctal("4567734"));
Assertions.assertEquals(-1, TbUtils.isOctal("8100110"));
}
@Test
public void isDecimal_Test() {
Assertions.assertEquals(10, TbUtils.isDecimal("4567039"));
Assertions.assertEquals(-1, TbUtils.isDecimal("C100110"));
}
@Test
public void isHexadecimal_Test() {
Assertions.assertEquals(16, TbUtils.isHexadecimal("F5D7039"));
Assertions.assertEquals(-1, TbUtils.isHexadecimal("K100110"));
}
private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length);
for (Byte b : data) {

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -948,7 +948,7 @@ public class DefaultTransportService extends TransportActivityManager implements
String resourceId = msg.getResourceKey();
transportResourceCache.evict(tenantId, resourceType, resourceId);
sessions.forEach((id, mdRez) -> {
log.warn("ResourceDelete - [{}] [{}]", id, mdRez);
log.trace("ResourceDelete - [{}] [{}]", id, mdRez);
transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(msg));
});
} else if (toSessionMsg.getQueueUpdateMsgsCount() > 0) {

15
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.dashboard;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@ -57,6 +56,7 @@ import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -235,13 +235,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
publishEvictEvent(new DashboardTitleEvictEvent(dashboardId));
countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(dashboardId).build());
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_dashboard_device_profile")) {
throw new DataValidationException("The dashboard referenced by the device profiles cannot be deleted!");
} else {
throw t;
}
} catch (Exception e) {
checkConstraintViolation(e, Map.of(
"fk_default_dashboard_device_profile", "The dashboard is referenced by a device profile",
"fk_default_dashboard_asset_profile", "The dashboard is referenced by an asset profile"
));
throw e;
}
}

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java

@ -181,8 +181,11 @@ public class CalculateDeltaNode implements TbNode {
private ListenableFuture<ValueWithTs> getLatestFromCacheOrFetchFromDb(TbContext ctx, TbMsg msg) {
EntityId originator = msg.getOriginator();
ValueWithTs valueWithTs = cache.get(msg.getOriginator());
return valueWithTs != null ? Futures.immediateFuture(valueWithTs) : fetchLatestValueAsync(ctx, originator);
if (config.isUseCache()) {
ValueWithTs valueWithTs = cache.get(msg.getOriginator());
return valueWithTs != null ? Futures.immediateFuture(valueWithTs) : fetchLatestValueAsync(ctx, originator);
}
return fetchLatestValueAsync(ctx, originator);
}
private record ValueWithTs(long ts, double value) {

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -53,6 +53,7 @@ import java.util.concurrent.TimeoutException;
type = ComponentType.EXTERNAL,
name = "mqtt",
configClazz = TbMqttNodeConfiguration.class,
version = 1,
clusteringMode = ComponentClusteringMode.USER_PREFERENCE,
nodeDescription = "Publish messages to the MQTT broker",
nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.",

19
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java

@ -109,7 +109,6 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
public void setUp() throws TbNodeException {
config = new CalculateDeltaNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
}
@Test
@ -166,8 +165,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() {
public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
var msgData = "{\"pulseCounter\": 42}";
var msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData);
@ -181,8 +181,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() {
public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY);
// WHEN
@ -196,8 +197,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
@Test
public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() {
public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
// WHEN
@ -451,8 +453,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenInvalidStringValue_whenOnMsg_thenException() {
public void givenInvalidStringValue_whenOnMsg_thenException() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high")));
var msgData = "{\"pulseCounter\":\"123\"}";
@ -475,8 +478,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenBooleanValue_whenOnMsg_thenException() {
public void givenBooleanValue_whenOnMsg_thenException() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false)));
var msgData = "{\"pulseCounter\":true}";
@ -499,8 +503,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenJsonValue_whenOnMsg_thenException() {
public void givenJsonValue_whenOnMsg_thenException() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}")));
var msgData = "{\"pulseCounter\":{\"isActive\":true}}";

10
ui-ngx/src/app/core/utils.ts

@ -364,6 +364,16 @@ export function mergeDeep<T>(target: T, ...sources: T[]): T {
return _.merge(target, ...sources);
}
function ignoreArrayMergeFunc(target: any, sources: any) {
if (_.isArray(target)) {
return sources;
}
}
export function mergeDeepIgnoreArray<T>(target: T, ...sources: T[]): T {
return _.mergeWith(target, ...sources, ignoreArrayMergeFunc);
}
export function guid(): string {
function s4(): string {
return Math.floor((1 + Math.random()) * 0x10000)

4
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts

@ -33,7 +33,7 @@ import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isUndefined, mergeDeep } from '@core/utils';
import { formatValue, isUndefined, mergeDeepIgnoreArray } from '@core/utils';
import {
cssSizeToStrSize,
DateFormatProcessor,
@ -117,7 +117,7 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent {
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: RangeChartWidgetSettings = mergeDeep<RangeChartWidgetSettings>({} as RangeChartWidgetSettings,
const settings: RangeChartWidgetSettings = mergeDeepIgnoreArray<RangeChartWidgetSettings>({} as RangeChartWidgetSettings,
rangeChartDefaultSettings, configData.config.settings as RangeChartWidgetSettings);
const iconSize = resolveCssSize(configData.config.iconSize);
this.rangeChartWidgetConfigForm = this.fb.group({

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts

@ -25,7 +25,7 @@ import {
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { formatValue, mergeDeep } from '@core/utils';
import { formatValue, mergeDeepIgnoreArray } from '@core/utils';
import {
rangeChartDefaultSettings,
RangeChartWidgetSettings
@ -99,7 +99,7 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent {
}
protected defaultSettings(): WidgetSettings {
return mergeDeep<RangeChartWidgetSettings>({} as RangeChartWidgetSettings, rangeChartDefaultSettings);
return mergeDeepIgnoreArray<RangeChartWidgetSettings>({} as RangeChartWidgetSettings, rangeChartDefaultSettings);
}
protected onSettingsSet(settings: WidgetSettings) {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts

@ -139,7 +139,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
} else {
rangeList = deepClone(value);
}
this.colorRangeListFormGroup.get('advancedMode').patchValue(rangeList.advancedMode, {emitEvent: false});
this.colorRangeListFormGroup.get('advancedMode').patchValue(rangeList.advancedMode || false, {emitEvent: false});
if (isDefinedAndNotNull(rangeList?.range)) {
rangeList.range.forEach((r) => this.rangeListFormArray.push(this.colorRangeControl(r), {emitEvent: false}));
}

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts

@ -16,7 +16,7 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { ColorRange } from '@shared/models/widget-settings.models';
import { ColorRange, ColorRangeSettings } from '@shared/models/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
@ -71,8 +71,8 @@ export class ColorRangePanelComponent extends PageComponent implements OnInit {
}
applyColorRangeSettings() {
const colorRangeSettings = this.colorRangeFormGroup.get('rangeList').value;
this.colorRangeApplied.emit(colorRangeSettings);
const colorRangeSettings: ColorRangeSettings = this.colorRangeFormGroup.get('rangeList').value;
this.colorRangeApplied.emit(colorRangeSettings.range);
}
}

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts

@ -25,7 +25,7 @@ import {
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ColorRange, ComponentStyle } from '@shared/models/widget-settings.models';
import { ColorRange, ColorRangeSettings, ComponentStyle } from '@shared/models/widget-settings.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { ColorRangePanelComponent } from '@home/components/widget/lib/settings/common/color-range-panel.component';
@ -108,8 +108,8 @@ export class ColorRangeSettingsComponent implements OnInit, ControlValueAccessor
this.updateColorStyle();
}
writeValue(value: Array<ColorRange>): void {
this.modelValue = value;
writeValue(value: Array<ColorRange> | ColorRangeSettings): void {
this.modelValue = Array.isArray(value) ? value : value.range;
this.updateColorStyle();
}
@ -131,7 +131,7 @@ export class ColorRangeSettingsComponent implements OnInit, ControlValueAccessor
{},
{}, {}, true);
colorRangeSettingsPanelPopover.tbComponentRef.instance.popover = colorRangeSettingsPanelPopover;
colorRangeSettingsPanelPopover.tbComponentRef.instance.colorRangeApplied.subscribe((colorRangeSettings) => {
colorRangeSettingsPanelPopover.tbComponentRef.instance.colorRangeApplied.subscribe((colorRangeSettings: Array<ColorRange>) => {
colorRangeSettingsPanelPopover.hide();
this.modelValue = colorRangeSettings;
this.updateColorStyle();

2
ui-ngx/src/app/shared/components/json-content.component.html

@ -18,7 +18,7 @@
<div class="tb-json-content" style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar" *ngIf="hideToolbar">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar" *ngIf="!hideToolbar">
<label class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (!contentValid || required && !contentBody), 'tb-required': !disabled && required}">{{ label }}</label>
<span fxFlex></span>
<button type="button"

2
ui-ngx/src/app/shared/models/device.models.ts

@ -655,7 +655,7 @@ export enum SnmpAuthenticationProtocol {
SHA_256 = 'SHA_256',
SHA_384 = 'SHA_384',
SHA_512 = 'SHA_512',
MD5 = 'MD%'
MD5 = 'MD5'
}
export const SnmpAuthenticationProtocolTranslationMap = new Map<SnmpAuthenticationProtocol, string>([

4
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -195,8 +195,8 @@ export const colorRangeIncludes = (range: ColorRange, toCheck: ColorRange): bool
}
};
export const filterIncludingColorRanges = (ranges: Array<ColorRange>): Array<ColorRange> => {
const result = [...ranges];
export const filterIncludingColorRanges = (ranges: Array<ColorRange> | ColorRangeSettings): Array<ColorRange> => {
const result = [...(Array.isArray(ranges) ? ranges : ranges.range)];
let includes = true;
while (includes) {
let index = -1;

4
ui-ngx/src/app/shared/models/widget.models.ts

@ -37,7 +37,7 @@ import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { isNotEmptyStr, mergeDeep } from '@core/utils';
import { isNotEmptyStr, mergeDeepIgnoreArray } from '@core/utils';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -878,7 +878,7 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
if (!value) {
this.settingsValue = this.defaultSettings();
} else {
this.settingsValue = mergeDeep(this.defaultSettings(), value);
this.settingsValue = mergeDeepIgnoreArray(this.defaultSettings(), value);
}
if (!this.settingsSet) {
this.settingsSet = true;

Loading…
Cancel
Save