From 345c28c4826f63a3699f5fe4fd52e9f2275cb953 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 8 Jul 2025 11:40:18 +0300 Subject: [PATCH 01/17] Add admin settings entity type --- .../server/controller/AdminController.java | 51 ++--- .../service/mail/DefaultMailService.java | 183 ++++++----------- .../server/service/mail/TbMailSender.java | 22 +- .../service/security/permission/Resource.java | 2 +- .../service/sms/DefaultSmsSenderFactory.java | 16 +- .../dao/settings/AdminSettingsService.java | 5 +- .../server/common/data/EntityType.java | 6 +- .../common/data/id/AdminSettingsId.java | 18 +- .../server/common/data/id/EdgeId.java | 3 + .../server/common/data/id/EntityId.java | 4 - .../common/data/id/EntityIdFactory.java | 190 ++++++------------ .../server/common/data/id/JobId.java | 4 + .../server/common/data/id/TenantId.java | 2 + .../server/common/util/ProtoUtils.java | 12 +- common/proto/src/main/proto/queue.proto | 1 + .../HybridClientRegistrationRepository.java | 6 +- .../server/dao/settings/AdminSettingsDao.java | 14 +- .../settings/AdminSettingsServiceImpl.java | 22 +- .../sql/settings/AdminSettingsRepository.java | 3 - .../dao/sql/settings/JpaAdminSettingsDao.java | 34 ++-- .../server/dao/tenant/TenantServiceImpl.java | 8 +- .../rule/engine/util/TenantIdLoader.java | 1 + .../rule/engine/util/TenantIdLoaderTest.java | 8 +- 23 files changed, 252 insertions(+), 363 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 6b3132a1d2..154e349fdb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -37,14 +37,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; 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; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; @@ -125,8 +124,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings/{key}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/settings/{key}") public AdminSettings getAdminSettings( @Parameter(description = "A string value of the key (e.g. 'general' or 'mail').") @PathVariable("key") String key) throws ThingsboardException { @@ -144,8 +142,7 @@ public class AdminController extends BaseController { "The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " + "Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/settings") public AdminSettings saveAdminSettings( @Parameter(description = "A JSON value representing the Administration Settings.") @RequestBody AdminSettings adminSettings) throws ThingsboardException { @@ -165,8 +162,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Get the Security Settings object (getSecuritySettings)", notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/securitySettings", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/securitySettings") public SecuritySettings getSecuritySettings() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return checkNotNull(securitySettingsService.getSecuritySettings()); @@ -175,8 +171,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Update Security Settings (saveSecuritySettings)", notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/securitySettings", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/securitySettings") public SecuritySettings saveSecuritySettings( @Parameter(description = "A JSON value representing the Security Settings.") @RequestBody SecuritySettings securitySettings) throws ThingsboardException { @@ -188,8 +183,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Get the JWT Settings object (getJwtSettings)", notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/jwtSettings", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/jwtSettings") public JwtSettings getJwtSettings() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return checkNotNull(jwtSettingsService.getJwtSettings()); @@ -198,8 +192,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Update JWT Settings (saveJwtSettings)", notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/jwtSettings", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/jwtSettings") public JwtPair saveJwtSettings( @Parameter(description = "A JSON value representing the JWT Settings.") @RequestBody JwtSettings jwtSettings) throws ThingsboardException { @@ -213,7 +206,7 @@ public class AdminController extends BaseController { notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " + "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST) + @PostMapping(value = "/settings/testMail") public void sendTestMail( @Parameter(description = "A JSON value representing the Mail Settings.") @RequestBody AdminSettings adminSettings) throws ThingsboardException { @@ -251,7 +244,7 @@ public class AdminController extends BaseController { notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings/testSms", method = RequestMethod.POST) + @PostMapping(value = "/settings/testSms") public void sendTestSms( @Parameter(description = "A JSON value representing the Test SMS request.") @RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException { @@ -325,7 +318,7 @@ public class AdminController extends BaseController { notes = "Deletes the repository settings." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE) + @DeleteMapping(value = "/repositorySettings") @ResponseStatus(value = HttpStatus.OK) public DeferredResult deleteRepositorySettings() throws Exception { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); @@ -335,7 +328,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Check repository access (checkRepositoryAccess)", notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST) + @PostMapping(value = "/repositorySettings/checkAccess") public DeferredResult checkRepositoryAccess( @Parameter(description = "A JSON value representing the Repository Settings.") @RequestBody RepositorySettings settings) throws Exception { @@ -376,7 +369,7 @@ public class AdminController extends BaseController { notes = "Deletes the auto commit settings." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE) + @DeleteMapping(value = "/autoCommitSettings") @ResponseStatus(value = HttpStatus.OK) public void deleteAutoCommitSettings() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); @@ -387,9 +380,8 @@ public class AdminController extends BaseController { notes = "Check notifications about new platform releases. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/updates", method = RequestMethod.GET) - @ResponseBody - public UpdateMessage checkUpdates() throws ThingsboardException { + @GetMapping(value = "/updates") + public UpdateMessage checkUpdates() { return updateService.checkUpdates(); } @@ -397,9 +389,8 @@ public class AdminController extends BaseController { notes = "Get main information about system. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/systemInfo", method = RequestMethod.GET) - @ResponseBody - public SystemInfo getSystemInfo() throws ThingsboardException { + @GetMapping(value = "/systemInfo") + public SystemInfo getSystemInfo() { return systemInfoService.getSystemInfo(); } @@ -407,8 +398,7 @@ public class AdminController extends BaseController { notes = "Get information about enabled/disabled features. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/featuresInfo", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/featuresInfo") public FeaturesInfo getFeaturesInfo() { return systemInfoService.getFeaturesInfo(); } @@ -417,8 +407,7 @@ public class AdminController extends BaseController { "double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " + "further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/mail/oauth2/loginProcessingUrl", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/mail/oauth2/loginProcessingUrl") public String getMailProcessingUrl() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return "\"/api/admin/mail/oauth2/code\""; @@ -427,7 +416,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" + "provider sends authorization code to specified redirect uri.)") @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/mail/oauth2/authorize", method = RequestMethod.GET, produces = "application/text") + @GetMapping(value = "/mail/oauth2/authorize", produces = "application/text") public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { String state = StringUtils.generateSafeToken(); if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) { @@ -452,7 +441,7 @@ public class AdminController extends BaseController { .build() + "\""; } - @RequestMapping(value = "/mail/oauth2/code", params = {"code", "state"}, method = RequestMethod.GET) + @GetMapping(value = "/mail/oauth2/code", params = {"code", "state"}) public void codeProcessingUrl( @RequestParam(value = "code") String code, @RequestParam(value = "state") String state, HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 99faf1a1d0..c1cfe791da 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -22,9 +22,9 @@ import freemarker.template.Template; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Lazy; @@ -64,55 +64,37 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -@Service @Slf4j +@Service +@RequiredArgsConstructor public class DefaultMailService implements MailService { - public static final String TARGET_EMAIL = "targetEmail"; - public static final String UTF_8 = "UTF-8"; + private static final String TARGET_EMAIL = "targetEmail"; + private static final String UTF_8 = "UTF-8"; + private static final long DEFAULT_TIMEOUT = 10_000; + + private final ScheduledExecutorService timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog"); private final MessageSource messages; private final Configuration freemarkerConfig; private final AdminSettingsService adminSettingsService; private final TbApiUsageReportClient apiUsageClient; - - private static final long DEFAULT_TIMEOUT = 10_000; - @Lazy - @Autowired - private TbApiUsageStateService apiUsageStateService; - - @Autowired - private MailSenderInternalExecutorService mailExecutorService; - - @Autowired - private PasswordResetExecutorService passwordResetExecutorService; - - @Autowired - private TbMailContextComponent ctx; - - @Autowired - private RateLimitService rateLimitService; + private final TbApiUsageStateService apiUsageStateService; + private final MailSenderInternalExecutorService mailExecutorService; + private final PasswordResetExecutorService passwordResetExecutorService; + private final TbMailContextComponent ctx; + private final RateLimitService rateLimitService; @Value("${mail.per_tenant_rate_limits:}") private String perTenantRateLimitConfig; - private final ScheduledExecutorService timeoutScheduler; - private TbMailSender mailSender; private String mailFrom; private long timeout; - public DefaultMailService(MessageSource messages, Configuration freemarkerConfig, AdminSettingsService adminSettingsService, TbApiUsageReportClient apiUsageClient) { - this.messages = messages; - this.freemarkerConfig = freemarkerConfig; - this.adminSettingsService = adminSettingsService; - this.apiUsageClient = apiUsageClient; - this.timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog"); - } - @PostConstruct private void init() { updateMailConfiguration(); @@ -120,9 +102,7 @@ public class DefaultMailService implements MailService { @PreDestroy public void destroy() { - if (timeoutScheduler != null) { - timeoutScheduler.shutdownNow(); - } + timeoutScheduler.shutdownNow(); } @Override @@ -311,22 +291,21 @@ public class DefaultMailService implements MailService { model.put("apiFeature", apiFeature.getLabel()); model.put(TARGET_EMAIL, email); - String message = null; - - switch (stateValue) { - case ENABLED: + String message = switch (stateValue) { + case ENABLED -> { model.put("apiLabel", toEnabledValueLabel(apiFeature)); - message = mergeTemplateIntoString("state.enabled.ftl", model); - break; - case WARNING: + yield mergeTemplateIntoString("state.enabled.ftl", model); + } + case WARNING -> { model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(recordState)); - message = mergeTemplateIntoString("state.warning.ftl", model); - break; - case DISABLED: + yield mergeTemplateIntoString("state.warning.ftl", model); + } + case DISABLED -> { model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(recordState)); - message = mergeTemplateIntoString("state.disabled.ftl", model); - break; - } + yield mergeTemplateIntoString("state.disabled.ftl", model); + } + }; + sendMail(mailSender, mailFrom, email, subject, message, timeout); } @@ -341,89 +320,55 @@ public class DefaultMailService implements MailService { } private String toEnabledValueLabel(ApiFeature apiFeature) { - switch (apiFeature) { - case DB: - return "save"; - case TRANSPORT: - return "receive"; - case JS: - return "invoke"; - case RE: - return "process"; - case EMAIL: - case SMS: - return "send"; - case ALARM: - return "create"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (apiFeature) { + case DB -> "save"; + case TRANSPORT -> "receive"; + case JS -> "invoke"; + case RE -> "process"; + case EMAIL, SMS -> "send"; + case ALARM -> "create"; + default -> throw new RuntimeException("Not implemented!"); + }; } private String toDisabledValueLabel(ApiFeature apiFeature) { - switch (apiFeature) { - case DB: - return "saved"; - case TRANSPORT: - return "received"; - case JS: - return "invoked"; - case RE: - return "processed"; - case EMAIL: - case SMS: - return "sent"; - case ALARM: - return "created"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (apiFeature) { + case DB -> "saved"; + case TRANSPORT -> "received"; + case JS -> "invoked"; + case RE -> "processed"; + case EMAIL, SMS -> "sent"; + case ALARM -> "created"; + default -> throw new RuntimeException("Not implemented!"); + }; } private String toWarningValueLabel(ApiUsageRecordState recordState) { String valueInM = recordState.getValueAsString(); String thresholdInM = recordState.getThresholdAsString(); - switch (recordState.getKey()) { - case STORAGE_DP_COUNT: - case TRANSPORT_DP_COUNT: - return valueInM + " out of " + thresholdInM + " allowed data points"; - case TRANSPORT_MSG_COUNT: - return valueInM + " out of " + thresholdInM + " allowed messages"; - case JS_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed JavaScript functions"; - case TBEL_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed Tbel functions"; - case RE_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed Rule Engine messages"; - case EMAIL_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed Email messages"; - case SMS_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed SMS messages"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (recordState.getKey()) { + case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> valueInM + " out of " + thresholdInM + " allowed data points"; + case TRANSPORT_MSG_COUNT -> valueInM + " out of " + thresholdInM + " allowed messages"; + case JS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed JavaScript functions"; + case TBEL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Tbel functions"; + case RE_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Rule Engine messages"; + case EMAIL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Email messages"; + case SMS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed SMS messages"; + default -> throw new RuntimeException("Not implemented!"); + }; } private String toDisabledValueLabel(ApiUsageRecordState recordState) { - switch (recordState.getKey()) { - case STORAGE_DP_COUNT: - case TRANSPORT_DP_COUNT: - return recordState.getValueAsString() + " data points"; - case TRANSPORT_MSG_COUNT: - return recordState.getValueAsString() + " messages"; - case JS_EXEC_COUNT: - return "JavaScript functions " + recordState.getValueAsString() + " times"; - case TBEL_EXEC_COUNT: - return "TBEL functions " + recordState.getValueAsString() + " times"; - case RE_EXEC_COUNT: - return recordState.getValueAsString() + " Rule Engine messages"; - case EMAIL_EXEC_COUNT: - return recordState.getValueAsString() + " Email messages"; - case SMS_EXEC_COUNT: - return recordState.getValueAsString() + " SMS messages"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (recordState.getKey()) { + case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> recordState.getValueAsString() + " data points"; + case TRANSPORT_MSG_COUNT -> recordState.getValueAsString() + " messages"; + case JS_EXEC_COUNT -> "JavaScript functions " + recordState.getValueAsString() + " times"; + case TBEL_EXEC_COUNT -> "TBEL functions " + recordState.getValueAsString() + " times"; + case RE_EXEC_COUNT -> recordState.getValueAsString() + " Rule Engine messages"; + case EMAIL_EXEC_COUNT -> recordState.getValueAsString() + " Email messages"; + case SMS_EXEC_COUNT -> recordState.getValueAsString() + " SMS messages"; + default -> throw new RuntimeException("Not implemented!"); + }; } private void sendMail(JavaMailSenderImpl mailSender, String mailFrom, String email, diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java index 8d7356a507..914a53e8a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java @@ -25,6 +25,7 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.lang.Nullable; import org.springframework.mail.MailException; @@ -50,8 +51,10 @@ public class TbMailSender extends JavaMailSenderImpl { private final TbMailContextComponent ctx; private final Lock lock; + @Getter private final Boolean oauth2Enabled; private volatile String accessToken; + @Getter private volatile long tokenExpires; public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) { @@ -70,14 +73,6 @@ public class TbMailSender extends JavaMailSenderImpl { setJavaMailProperties(createJavaMailProperties(jsonConfig)); } - public Boolean getOauth2Enabled() { - return oauth2Enabled; - } - - public long getTokenExpires() { - return tokenExpires; - } - @Override protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException { updateOauth2PasswordIfExpired(); @@ -98,8 +93,8 @@ public class TbMailSender extends JavaMailSenderImpl { super.testConnection(); } - public void updateOauth2PasswordIfExpired() { - if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())){ + public void updateOauth2PasswordIfExpired() { + if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())) { refreshAccessToken(); setPassword(accessToken); } @@ -168,8 +163,8 @@ public class TbMailSender extends JavaMailSenderImpl { .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret)) .execute(); if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) { - ((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); - ((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); + ((ObjectNode) jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); + ((ObjectNode) jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings); } accessToken = tokenResponse.getAccessToken(); @@ -190,4 +185,5 @@ public class TbMailSender extends JavaMailSenderImpl { throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort)); } } -} \ No newline at end of file + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 2a92c040e3..22b75ea3f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -21,7 +21,7 @@ import java.util.Collections; import java.util.Set; public enum Resource { - ADMIN_SETTINGS(), + ADMIN_SETTINGS(EntityType.ADMIN_SETTINGS), ALARM(EntityType.ALARM), DEVICE(EntityType.DEVICE), ASSET(EntityType.ASSET), diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java index 5522047deb..475c9c2c75 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java @@ -31,16 +31,12 @@ public class DefaultSmsSenderFactory implements SmsSenderFactory { @Override public SmsSender createSmsSender(SmsProviderConfiguration config) { - switch (config.getType()) { - case AWS_SNS: - return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config); - case TWILIO: - return new TwilioSmsSender((TwilioSmsProviderConfiguration)config); - case SMPP: - return new SmppSmsSender((SmppSmsProviderConfiguration) config); - default: - throw new RuntimeException("Unknown SMS provider type " + config.getType()); - } + return switch (config.getType()) { + case AWS_SNS -> new AwsSmsSender((AwsSnsSmsProviderConfiguration) config); + case TWILIO -> new TwilioSmsSender((TwilioSmsProviderConfiguration) config); + case SMPP -> new SmppSmsSender((SmppSmsProviderConfiguration) config); + default -> throw new RuntimeException("Unknown SMS provider type " + config.getType()); + }; } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java index b803700582..5223a2a9c9 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java @@ -18,8 +18,9 @@ package org.thingsboard.server.dao.settings; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.AdminSettingsId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.entity.EntityDaoService; -public interface AdminSettingsService { +public interface AdminSettingsService extends EntityDaoService { AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId); @@ -31,6 +32,4 @@ public interface AdminSettingsService { boolean deleteAdminSettingsByTenantIdAndKey(TenantId tenantId, String key); - void deleteAdminSettingsByTenantId(TenantId tenantId); - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index af5fec1827..c2614ccec6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -22,9 +22,6 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.List; -/** - * @author Andrew Shvayka - */ public enum EntityType { TENANT(1), CUSTOMER(2), @@ -65,7 +62,8 @@ public enum EntityType { MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), CALCULATED_FIELD_LINK(40), - JOB(41); + JOB(41), + ADMIN_SETTINGS(42); @Getter private final int protoNumber; // Corresponds to EntityTypeProto diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java index 8f2104b5fd..05194b8df5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java @@ -17,14 +17,26 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; -public class AdminSettingsId extends UUIDBased { +public class AdminSettingsId extends UUIDBased implements EntityId { + + @Serial + private static final long serialVersionUID = -4208011957475806567L; @JsonCreator - public AdminSettingsId(@JsonProperty("id") UUID id){ + public AdminSettingsId(@JsonProperty("id") UUID id) { super(id); } - + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS") + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java index 9a20be4eb9..4ccabd39b8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -23,10 +23,12 @@ import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public class EdgeId extends UUIDBased implements EntityId { + @Serial private static final long serialVersionUID = 1L; @JsonIgnore @@ -51,4 +53,5 @@ public class EdgeId extends UUIDBased implements EntityId { public static EdgeId fromUUID(@JsonProperty("id") UUID id) { return edges.computeIfAbsent(id, EdgeId::new); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java index 24196e28b5..ddfcd85dc8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java @@ -24,10 +24,6 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serializable; import java.util.UUID; -/** - * @author Andrew Shvayka - */ - @JsonDeserialize(using = EntityIdDeserializer.class) @JsonSerialize(using = EntityIdSerializer.class) @Schema diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index d23e4c078d..fa4421e2c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -20,9 +20,6 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import java.util.UUID; -/** - * Created by ashvayka on 25.04.17. - */ public class EntityIdFactory { public static EntityId getByTypeAndUuid(int type, String uuid) { @@ -50,131 +47,74 @@ public class EntityIdFactory { } public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) { - switch (type) { - case TENANT: - return TenantId.fromUUID(uuid); - case CUSTOMER: - return new CustomerId(uuid); - case USER: - return new UserId(uuid); - case DASHBOARD: - return new DashboardId(uuid); - case DEVICE: - return new DeviceId(uuid); - case ASSET: - return new AssetId(uuid); - case ALARM: - return new AlarmId(uuid); - case RULE_CHAIN: - return new RuleChainId(uuid); - case RULE_NODE: - return new RuleNodeId(uuid); - case ENTITY_VIEW: - return new EntityViewId(uuid); - case WIDGETS_BUNDLE: - return new WidgetsBundleId(uuid); - case WIDGET_TYPE: - return new WidgetTypeId(uuid); - case DEVICE_PROFILE: - return new DeviceProfileId(uuid); - case ASSET_PROFILE: - return new AssetProfileId(uuid); - case TENANT_PROFILE: - return new TenantProfileId(uuid); - case API_USAGE_STATE: - return new ApiUsageStateId(uuid); - case TB_RESOURCE: - return new TbResourceId(uuid); - case OTA_PACKAGE: - return new OtaPackageId(uuid); - case EDGE: - return new EdgeId(uuid); - case RPC: - return new RpcId(uuid); - case QUEUE: - return new QueueId(uuid); - case NOTIFICATION_TARGET: - return new NotificationTargetId(uuid); - case NOTIFICATION_REQUEST: - return new NotificationRequestId(uuid); - case NOTIFICATION_RULE: - return new NotificationRuleId(uuid); - case NOTIFICATION_TEMPLATE: - return new NotificationTemplateId(uuid); - case NOTIFICATION: - return new NotificationId(uuid); - case QUEUE_STATS: - return new QueueStatsId(uuid); - case OAUTH2_CLIENT: - return new OAuth2ClientId(uuid); - case MOBILE_APP: - return new MobileAppId(uuid); - case DOMAIN: - return new DomainId(uuid); - case MOBILE_APP_BUNDLE: - return new MobileAppBundleId(uuid); - case CALCULATED_FIELD: - return new CalculatedFieldId(uuid); - case CALCULATED_FIELD_LINK: - return new CalculatedFieldLinkId(uuid); - case JOB: - return new JobId(uuid); - } - throw new IllegalArgumentException("EntityType " + type + " is not supported!"); + return switch (type) { + case TENANT -> TenantId.fromUUID(uuid); + case CUSTOMER -> new CustomerId(uuid); + case USER -> new UserId(uuid); + case DASHBOARD -> new DashboardId(uuid); + case DEVICE -> new DeviceId(uuid); + case ASSET -> new AssetId(uuid); + case ALARM -> new AlarmId(uuid); + case RULE_CHAIN -> new RuleChainId(uuid); + case RULE_NODE -> new RuleNodeId(uuid); + case ENTITY_VIEW -> new EntityViewId(uuid); + case WIDGETS_BUNDLE -> new WidgetsBundleId(uuid); + case WIDGET_TYPE -> new WidgetTypeId(uuid); + case DEVICE_PROFILE -> new DeviceProfileId(uuid); + case ASSET_PROFILE -> new AssetProfileId(uuid); + case TENANT_PROFILE -> new TenantProfileId(uuid); + case API_USAGE_STATE -> new ApiUsageStateId(uuid); + case TB_RESOURCE -> new TbResourceId(uuid); + case OTA_PACKAGE -> new OtaPackageId(uuid); + case EDGE -> EdgeId.fromUUID(uuid); + case RPC -> new RpcId(uuid); + case QUEUE -> new QueueId(uuid); + case NOTIFICATION_TARGET -> new NotificationTargetId(uuid); + case NOTIFICATION_REQUEST -> new NotificationRequestId(uuid); + case NOTIFICATION_RULE -> new NotificationRuleId(uuid); + case NOTIFICATION_TEMPLATE -> new NotificationTemplateId(uuid); + case NOTIFICATION -> new NotificationId(uuid); + case QUEUE_STATS -> new QueueStatsId(uuid); + case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); + case MOBILE_APP -> new MobileAppId(uuid); + case DOMAIN -> new DomainId(uuid); + case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid); + case CALCULATED_FIELD -> new CalculatedFieldId(uuid); + case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); + case JOB -> new JobId(uuid); + case ADMIN_SETTINGS -> new AdminSettingsId(uuid); + default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!"); + }; } public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) { - switch (edgeEventType) { - case TENANT: - return TenantId.fromUUID(uuid); - case CUSTOMER: - return new CustomerId(uuid); - case USER: - return new UserId(uuid); - case DASHBOARD: - return new DashboardId(uuid); - case DEVICE: - return new DeviceId(uuid); - case ASSET: - return new AssetId(uuid); - case ALARM: - return new AlarmId(uuid); - case RULE_CHAIN: - return new RuleChainId(uuid); - case ENTITY_VIEW: - return new EntityViewId(uuid); - case WIDGETS_BUNDLE: - return new WidgetsBundleId(uuid); - case WIDGET_TYPE: - return new WidgetTypeId(uuid); - case DEVICE_PROFILE: - return new DeviceProfileId(uuid); - case ASSET_PROFILE: - return new AssetProfileId(uuid); - case TENANT_PROFILE: - return new TenantProfileId(uuid); - case OTA_PACKAGE: - return new OtaPackageId(uuid); - case EDGE: - return new EdgeId(uuid); - case QUEUE: - return new QueueId(uuid); - case TB_RESOURCE: - return new TbResourceId(uuid); - case NOTIFICATION_RULE: - return new NotificationRuleId(uuid); - case NOTIFICATION_TARGET: - return new NotificationTargetId(uuid); - case NOTIFICATION_TEMPLATE: - return new NotificationTemplateId(uuid); - case OAUTH2_CLIENT: - return new OAuth2ClientId(uuid); - case DOMAIN: - return new DomainId(uuid); - case CALCULATED_FIELD: - return new CalculatedFieldId(uuid); - } - throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); + return switch (edgeEventType) { + case TENANT -> TenantId.fromUUID(uuid); + case CUSTOMER -> new CustomerId(uuid); + case USER -> new UserId(uuid); + case DASHBOARD -> new DashboardId(uuid); + case DEVICE -> new DeviceId(uuid); + case ASSET -> new AssetId(uuid); + case ALARM -> new AlarmId(uuid); + case RULE_CHAIN -> new RuleChainId(uuid); + case ENTITY_VIEW -> new EntityViewId(uuid); + case WIDGETS_BUNDLE -> new WidgetsBundleId(uuid); + case WIDGET_TYPE -> new WidgetTypeId(uuid); + case DEVICE_PROFILE -> new DeviceProfileId(uuid); + case ASSET_PROFILE -> new AssetProfileId(uuid); + case TENANT_PROFILE -> new TenantProfileId(uuid); + case OTA_PACKAGE -> new OtaPackageId(uuid); + case EDGE -> EdgeId.fromUUID(uuid); + case QUEUE -> new QueueId(uuid); + case TB_RESOURCE -> new TbResourceId(uuid); + case NOTIFICATION_RULE -> new NotificationRuleId(uuid); + case NOTIFICATION_TARGET -> new NotificationTargetId(uuid); + case NOTIFICATION_TEMPLATE -> new NotificationTemplateId(uuid); + case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); + case DOMAIN -> new DomainId(uuid); + case CALCULATED_FIELD -> new CalculatedFieldId(uuid); + default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); + }; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java index 76678b8b31..372452825f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java @@ -20,10 +20,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public class JobId extends UUIDBased implements EntityId { + @Serial + private static final long serialVersionUID = -2225072123132918395L; + @JsonCreator public JobId(@JsonProperty("id") UUID id) { super(id); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index 57561fb7ee..7cad1698d7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -23,6 +23,7 @@ import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public final class TenantId extends UUIDBased implements EntityId { @@ -33,6 +34,7 @@ public final class TenantId extends UUIDBased implements EntityId { @JsonIgnore public static final TenantId SYS_TENANT_ID = TenantId.fromUUID(EntityId.NULL_UUID); + @Serial private static final long serialVersionUID = 1L; @JsonCreator diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 3cded5a491..6d010a7a0d 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -252,7 +252,7 @@ public class ProtoUtils { public static EdgeEvent fromProto(TransportProtos.EdgeEventMsgProto proto) { EdgeEvent edgeEvent = new EdgeEvent(); - TenantId tenantId = new TenantId(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())); edgeEvent.setTenantId(tenantId); edgeEvent.setType(EdgeEventType.valueOf(proto.getEntityType())); edgeEvent.setAction(EdgeEventActionType.valueOf(proto.getAction())); @@ -845,7 +845,7 @@ public class ProtoUtils { public static Device fromProto(TransportProtos.DeviceProto proto) { Device device = new Device(getEntityId(proto.getDeviceIdMSB(), proto.getDeviceIdLSB(), DeviceId::new)); device.setCreatedTime(proto.getCreatedTime()); - device.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + device.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); device.setName(proto.getDeviceName()); device.setType(proto.getDeviceType()); device.setDeviceProfileId(getEntityId(proto.getDeviceProfileIdMSB(), proto.getDeviceProfileIdLSB(), DeviceProfileId::new)); @@ -937,7 +937,7 @@ public class ProtoUtils { public static DeviceProfile fromProto(TransportProtos.DeviceProfileProto proto) { DeviceProfile deviceProfile = new DeviceProfile(getEntityId(proto.getDeviceProfileIdMSB(), proto.getDeviceProfileIdLSB(), DeviceProfileId::new)); deviceProfile.setCreatedTime(proto.getCreatedTime()); - deviceProfile.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + deviceProfile.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); deviceProfile.setName(proto.getName()); deviceProfile.setDefault(proto.getIsDefault()); deviceProfile.setType(DeviceProfileType.valueOf(proto.getType())); @@ -1028,7 +1028,7 @@ public class ProtoUtils { } public static Tenant fromProto(TransportProtos.TenantProto proto) { - Tenant tenant = new Tenant(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + Tenant tenant = new Tenant(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); tenant.setCreatedTime(proto.getCreatedTime()); tenant.setTenantProfileId(getEntityId(proto.getTenantProfileIdMSB(), proto.getTenantProfileIdLSB(), TenantProfileId::new)); tenant.setTitle(proto.getTitle()); @@ -1142,7 +1142,7 @@ public class ProtoUtils { public static TbResource fromProto(TransportProtos.TbResourceProto proto) { TbResource resource = new TbResource(getEntityId(proto.getResourceIdMSB(), proto.getResourceIdLSB(), TbResourceId::new)); - resource.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + resource.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); resource.setCreatedTime(proto.getCreatedTime()); resource.setTitle(proto.getTitle()); resource.setResourceType(ResourceType.valueOf(proto.getResourceType())); @@ -1198,7 +1198,7 @@ public class ProtoUtils { public static ApiUsageState fromProto(TransportProtos.ApiUsageStateProto proto) { ApiUsageState apiUsageState = new ApiUsageState(getEntityId(proto.getApiUsageStateIdMSB(), proto.getApiUsageStateIdLSB(), ApiUsageStateId::new)); - apiUsageState.setTenantId(getEntityId(proto.getTenantProfileIdMSB(), proto.getTenantProfileIdLSB(), TenantId::new)); + apiUsageState.setTenantId(getEntityId(proto.getTenantProfileIdMSB(), proto.getTenantProfileIdLSB(), TenantId::fromUUID)); apiUsageState.setCreatedTime(proto.getCreatedTime()); apiUsageState.setEntityId(EntityIdFactory.getByTypeAndUuid(fromProto(proto.getEntityType()), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()))); apiUsageState.setTransportState(ApiUsageStateValue.valueOf(proto.getTransportState())); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 2667838b60..2709a2bdc5 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -64,6 +64,7 @@ enum EntityTypeProto { CALCULATED_FIELD = 39; CALCULATED_FIELD_LINK = 40; JOB = 41; + ADMIN_SETTINGS = 42; } enum ApiUsageRecordKeyProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index bc84a58cd9..84dc22b36b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -37,8 +37,10 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep @Override public ClientRegistration findByRegistrationId(String registrationId) { OAuth2Client oAuth2Client = oAuth2ClientService.findOAuth2ClientById(TenantId.SYS_TENANT_ID, new OAuth2ClientId(UUID.fromString(registrationId))); - return oAuth2Client == null ? - null : toSpringClientRegistration(oAuth2Client); + if (oAuth2Client == null) { + return null; + } + return toSpringClientRegistration(oAuth2Client); } private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client){ diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java index b330ce8ee3..8c184c8c35 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java @@ -23,20 +23,8 @@ import java.util.UUID; public interface AdminSettingsDao extends Dao { - /** - * Save or update admin settings object - * - * @param adminSettings the admin settings object - * @return saved admin settings object - */ AdminSettings save(TenantId tenantId, AdminSettings adminSettings); - - /** - * Find admin settings by key. - * - * @param key the key - * @return the admin settings object - */ + AdminSettings findByTenantIdAndKey(UUID tenantId, String key); boolean removeByTenantIdAndKey(UUID tenantId, String key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index b1b3b25a19..604a37f4bb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -21,11 +21,16 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AdminSettingsId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; +import java.util.Optional; + @Service @Slf4j public class AdminSettingsServiceImpl implements AdminSettingsService { @@ -87,10 +92,25 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { } @Override - public void deleteAdminSettingsByTenantId(TenantId tenantId) { + public void deleteByTenantId(TenantId tenantId) { adminSettingsDao.removeByTenantId(tenantId.getId()); } + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + adminSettingsDao.removeById(tenantId, id.getId()); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(adminSettingsDao.findById(tenantId, entityId.getId())); + } + + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + private void dropTokenIfProviderInfoChanged(JsonNode newJsonValue, JsonNode oldJsonValue) { if (newJsonValue.has("enableOauth2") && newJsonValue.get("enableOauth2").asBoolean()) { if (!newJsonValue.get("providerId").equals(oldJsonValue.get("providerId")) || diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java index c27602421d..889de7cb74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java @@ -22,9 +22,6 @@ import org.thingsboard.server.dao.model.sql.AdminSettingsEntity; import java.util.UUID; -/** - * Created by Valerii Sosliuk on 5/6/2017. - */ public interface AdminSettingsRepository extends JpaRepository { AdminSettingsEntity findByTenantIdAndKey(UUID tenantId, String key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java index 68ce5e9d22..3dafa79c3f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.dao.sql.settings; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -35,21 +35,10 @@ import java.util.UUID; @Component @SqlDao -@Slf4j +@RequiredArgsConstructor public class JpaAdminSettingsDao extends JpaAbstractDao implements AdminSettingsDao, TenantEntityDao { - @Autowired - private AdminSettingsRepository adminSettingsRepository; - - @Override - protected Class getEntityClass() { - return AdminSettingsEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return adminSettingsRepository; - } + private final AdminSettingsRepository adminSettingsRepository; @Override public AdminSettings findByTenantIdAndKey(UUID tenantId, String key) { @@ -77,4 +66,19 @@ public class JpaAdminSettingsDao extends JpaAbstractDao getEntityClass() { + return AdminSettingsEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return adminSettingsRepository; + } + + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index b9d09e55ba..45cb832fef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -43,7 +43,6 @@ import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.TenantDataValidator; -import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.trendz.TrendzSettingsService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; @@ -76,8 +75,6 @@ public class TenantServiceImpl extends AbstractCachedEntityService tenantDao.existsById(tenantId, tenantId.getId()), false); } - private PaginatedRemover tenantsRemover = new PaginatedRemover<>() { + private final PaginatedRemover tenantsRemover = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 93fad4c0e7..f4a80937d7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -146,6 +146,7 @@ public class TenantIdLoader { tenantEntity = ctx.getNotificationRequestService().findNotificationRequestById(ctxTenantId, new NotificationRequestId(id)); break; case NOTIFICATION: + case ADMIN_SETTINGS: return ctxTenantId; case NOTIFICATION_RULE: tenantEntity = ctx.getNotificationRuleService().findNotificationRuleById(ctxTenantId, new NotificationRuleId(id)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 4cbc091bdc..60d2bd500a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -29,6 +29,7 @@ import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -167,7 +168,6 @@ public class TenantIdLoaderTest { private TenantId tenantId; private TenantProfileId tenantProfileId; - private NotificationId notificationId; private AbstractListeningExecutor dbExecutor; @BeforeEach @@ -179,9 +179,8 @@ public class TenantIdLoaderTest { } }; dbExecutor.init(); - this.tenantId = new TenantId(UUID.randomUUID()); + this.tenantId = TenantId.fromUUID(UUID.randomUUID()); this.tenantProfileId = new TenantProfileId(UUID.randomUUID()); - this.notificationId = new NotificationId(UUID.randomUUID()); when(ctx.getTenantId()).thenReturn(tenantId); @@ -199,6 +198,7 @@ public class TenantIdLoaderTest { switch (entityType) { case TENANT: case NOTIFICATION: + case ADMIN_SETTINGS: break; case CUSTOMER: Customer customer = new Customer(); @@ -465,7 +465,7 @@ public class TenantIdLoaderTest { @Test public void test_findEntityIdAsync_other_tenant() { - checkTenant(new TenantId(UUID.randomUUID()), false); + checkTenant(TenantId.fromUUID(UUID.randomUUID()), false); } } From c6497281c36a1f07c96c8eaf2010bea2e7a4e46a Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 8 Jul 2025 13:24:27 +0300 Subject: [PATCH 02/17] Fix upgrade for ota_package --- application/src/main/data/upgrade/basic/schema_update.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index c959cfd6c1..160217fc4f 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -19,7 +19,7 @@ ALTER TABLE ota_package ADD COLUMN IF NOT EXISTS external_id uuid; ALTER TABLE ota_package - ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); + ADD CONSTRAINT IF NOT EXISTS ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); -- UPDATE OTA PACKAGE EXTERNAL ID END From a691ff126236d2bc3110f235d39b2e85701552e8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 8 Jul 2025 14:28:42 +0300 Subject: [PATCH 03/17] Fix upgrade for ota_package --- .../src/main/data/upgrade/basic/schema_update.sql | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 160217fc4f..2c6f6a9ca0 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -18,8 +18,16 @@ ALTER TABLE ota_package ADD COLUMN IF NOT EXISTS external_id uuid; -ALTER TABLE ota_package - ADD CONSTRAINT IF NOT EXISTS ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); + +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN + ALTER TABLE ota_package + ADD CONSTRAINT ota_package_external_id_unq_key (tenant_id, external_id); + END IF; + END; +$$; -- UPDATE OTA PACKAGE EXTERNAL ID END From 349e7402a47c78ead512aa59f1c6fcd37f528460 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 8 Jul 2025 14:33:25 +0300 Subject: [PATCH 04/17] Small refactoring --- application/src/main/data/upgrade/basic/schema_update.sql | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 2c6f6a9ca0..22cac14829 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -23,8 +23,7 @@ DO $$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN - ALTER TABLE ota_package - ADD CONSTRAINT ota_package_external_id_unq_key (tenant_id, external_id); + ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); END IF; END; $$; From edba3517da4bfb91716abb3bdb7052876d23fce4 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 11 Jul 2025 14:37:02 +0300 Subject: [PATCH 05/17] tbel: add ExecutionArrayList with methods: add, addAll, remove, clear, sort, toSorted, toList --- .../service/script/TbelInvokeDocsIoTest.java | 258 ++++++++++++++++++ .../thingsboard/script/api/tbel/TbUtils.java | 21 ++ .../script/api/tbel/TbUtilsTest.java | 68 ++++- pom.xml | 2 +- 4 files changed, 344 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index c283a791fd..7900ca9b25 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -26,8 +26,10 @@ import java.util.Base64; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicReference; @@ -750,6 +752,262 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expected, actual); } + + // Sets + @Test + public void setsCreateNewSetFromMap_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["B", "A", "C", "A"]} + """; + decoderStr = """ + var originalMap = {}; + var set1 = originalMap.entrySet(); // create new Set from map, Empty + var set2 = set1.clone(); // clone new Set, Empty + var result1 = set1.addAll(msg.list); // addAll list, no sort, size = 3 ("A" - duplicate) + return {set1: set1, + set2: set2, + result1: result1 + } + """; + Set expectedSet1 = new LinkedHashSet(List.of("B", "A", "C", "A")); + Set expectedSet2 = new LinkedHashSet(); + Map expected = new LinkedHashMap<>(); + expected.put("set1", expectedSet1); + expected.put("set2", expectedSet2); + expected.put("result1", true); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected.toString(), actual.toString()); + } + + @Test + public void setsCreateNewSetFromCreateSetTbMethod_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["B", "A", "C", "A"]} + """; + decoderStr = """ + var set1 = createSetTb(msg.list); // create new Set from createSetTb() with list, no sort, size = 3 ("A" - duplicate) + var set2 = createSetTb(); // create new Set from createSetTb(), Empty + return {set1: set1, + set2: set2 + } + """; + Set expectedSet1 = new LinkedHashSet(List.of("B", "A", "C", "A")); + Set expectedSet2 = new LinkedHashSet(); + Map expected = new LinkedHashMap<>(); + expected.put("set1", expectedSet1); + expected.put("set2", expectedSet2); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected.toString(), actual.toString()); + } + + @Test + public void setsForeachForLoop_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["A", "B", "C"]} + """; + decoderStr = """ + var set2 = createSetTb(msg.list); // create new from list, size = 3 + var set2_0 = set2.toArray()[0]; // return "A", value with index = 0 from Set + var set2Size = set2.size(); // return size = 3 + var smthForeach = ""; + foreach (item : set2) { // foreach for Set + smthForeach += item; // return "ABC" + } + var smthForLoop= ""; + var set2Array = set2.toArray(); // for loop for Set (Set to array)) + for (var i =0; i < set2.size; i++) { + smthForLoop += set2Array[i]; // return "ABC" + } + return { + set2: set2, + set2_0: set2_0, + set2Size: set2Size, + smthForeach: smthForeach, + smthForLoop: smthForLoop + } + """; + Set expectedSet2 = new LinkedHashSet(List.of("A", "B", "C")); + Map expected = new LinkedHashMap<>(); + expected.put("set2", expectedSet2); + expected.put("set2_0", expectedSet2.toArray()[0]); + expected.put("set2Size", expectedSet2.size()); + AtomicReference smth = new AtomicReference<>(""); + expectedSet2.forEach(s -> smth.updateAndGet(v -> v + s)); + expected.put("smthForeach", smth.get()); + expected.put("smthForLoop", smth.get()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected.toString(), actual.toString()); + } + + /** + * add + * delete/remove + * setCreate, setCreatList + */ + @Test + public void setsAddRemove_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["B", "C", "A", "B", "C", "hello", 34567]} + """; + decoderStr = """ + var msgRez = {}; + // add + var setAdd = createSetTb(["thigsboard", 4, 67]); // create new, size = 3 + var setAdd1_value = setAdd.clone(); // clone setAdd, size = 3 + var setAdd2_result = setAdd.add(35); // add value = 35, result = true + var setAdd2_value = setAdd.clone(); // clone setAdd (fixing the result add = 35), size = 4 + var setAddList1 = createSetTb(msg.list); // create new from list without duplicate value ("B" and "C" - only one), size = 5 + var setAdd3_result = setAdd.addAll(setAddList1); // add all without duplicate values, result = true + var setAdd3_value = setAdd.clone(); // clone setAdd (with addAll), size = 9 + var setAdd4_result = setAdd.add(35); // add duplicate value = 35, result = false + var setAdd4_value = setAdd.clone(); // clone setAdd (after add duplicate value = 35), size = 9 + var setAddList2 = createSetTb(msg.list); // create new from list without duplicate value ("B" and "C" - only one), start: size = 5, finish: size = 7 + var setAdd5_result1 = setAddList2.add(72); // add is not duplicate value = 72, result = true + var setAdd5_result2 = setAddList2.add(72); // add duplicate value = 72, result = false + var setAdd5_result3 = setAddList2.add("hello25"); // add is not duplicate value = "hello25", result = true + var setAdd5_value = setAddList2.clone(); // clone setAddList2, size = 7 + var setAdd6_result = setAdd.addAll(setAddList2); // add all with duplicate values, result = true + var setAdd6_value = setAdd.clone(); // clone setAdd (after addAll setAddList2), before size = 9, after size = 11, added only is not duplicate values {"hello25", 72} + + // remove + var setAdd7_value = setAdd6_value.clone(); // clone setAdd6_value, before size = 11, after remove value = 4 size = 10 + var setAdd7_result = setAdd7_value.remove(4); // remove value = 4, result = true + var setAdd8_value = setAdd7_value.clone(); // clone setAdd7_value, before size = 10, after clear size = 0 + setAdd8_value.clear(); // setAdd8_value clear, result size = 0 + return { + "setAdd1_value": setAdd1_value, + "setAdd2_result": setAdd2_result, + "setAdd2_value": setAdd2_value, + "setAddList1": setAddList1, + "setAdd3_result": setAdd3_result, + "setAdd3_value": setAdd3_value, + "setAdd4_result": setAdd4_result, + "setAdd4_value": setAdd4_value, + "setAdd5_result1": setAdd5_result1, + "setAdd5_result2": setAdd5_result2, + "setAdd5_result3": setAdd5_result3, + "setAddList2": setAddList2, + "setAdd5_value": setAdd5_value, + "setAdd6_result": setAdd6_result, + "setAdd6_value": setAdd6_value, + "setAdd7_result": setAdd7_result, + "setAdd7_value": setAdd7_value, + "setAdd8_value": setAdd8_value + }; + """; + ArrayList list = new ArrayList<>(List.of("B", "C", "A", "B", "C", "hello", 34567)); + ArrayList listAdd = new ArrayList<>(List.of("thigsboard", 4, 67)); + Set setAdd = new LinkedHashSet<>(listAdd); + Set setAdd1_value = new LinkedHashSet<>(setAdd); + boolean setAdd2_result = setAdd.add(35); + Set setAdd2_value = new LinkedHashSet<>(setAdd); + Set setAddList1 = new LinkedHashSet<>(list); + boolean setAdd3_result = setAdd.addAll(setAddList1); + Set setAdd3_value = new LinkedHashSet<>(setAdd); + boolean setAdd4_result = setAdd.add(35); + Set setAdd4_value = new LinkedHashSet<>(setAdd); + Set setAddList2 = new LinkedHashSet<>(list); + boolean setAdd5_result1 = setAddList2.add(72); + boolean setAdd5_result2 = setAddList2.add(72); + boolean setAdd5_result3 = setAddList2.add("hello25"); + Set setAdd5_value = new LinkedHashSet<>(setAddList2); + boolean setAdd6_result = setAdd.addAll(setAddList2); + Set setAdd6_value = new LinkedHashSet<>(setAdd); + // remove + Set setAdd7_value = new LinkedHashSet<>(setAdd6_value); + boolean setAdd7_result = setAdd7_value.remove(4); + Set setAdd8_value = new LinkedHashSet<>(setAdd7_value); + setAdd8_value.clear(); + + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("setAdd1_value", setAdd1_value); + expected.put("setAdd2_result", setAdd2_result); + expected.put("setAdd2_value", setAdd2_value); + expected.put("setAddList1", setAddList1); + expected.put("setAdd3_result", setAdd3_result); + expected.put("setAdd3_value", setAdd3_value); + expected.put("setAdd4_result", setAdd4_result); + expected.put("setAdd4_value", setAdd4_value); + expected.put("setAdd5_result1", setAdd5_result1); + expected.put("setAdd5_result2", setAdd5_result2); + expected.put("setAdd5_result3", setAdd5_result3); + expected.put("setAddList2", setAddList2); + expected.put("setAdd5_value", setAdd5_value); + expected.put("setAdd6_result", setAdd6_result); + expected.put("setAdd6_value", setAdd6_value); + expected.put("setAdd7_result", setAdd7_result); + expected.put("setAdd7_value", setAdd7_value); + expected.put("setAdd8_value", setAdd8_value); + + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected.toString(), actual.toString()); + } + + @Test + public void setsSort_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} + """; + decoderStr = """ + var msgRez = {}; + var set1 = msgRez.entrySet(); // create Set from map, size = 0 + set1.addAll(msg.list); // addAll to set1 from list no sort (length = 8), but set`s size = 6 ("A" and "C" is duplicated) + var set2 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) + var set1_asc = set1.clone(); // clone set1, size = 6 + var set1_desc = set1.clone(); // clone set1, size = 6 + var set2_asc = set2.clone(); // clone set2, size = 6 + var set2_desc = set2.clone(); // clone set2, size = 6 + set1.sort(); // sort set1 -> asc + set1_asc.sort(true); // sort set1_asc -> asc + set1_desc.sort(false); // sort set1_desc -> desc + set2.sort(); // sort set2 -> asc + set2_asc.sort(true); // sort set2_asc -> asc + set2_desc.sort(false); // sort set2_desc -> desc + return { + "set1": set1, + "set1_asc": set1_asc, + "set1_desc": set1_desc, + "set2": set2, + "set2_asc": set2_asc, + "set2_desc": set2_desc, + } + """; + ArrayList listSortAsc = new ArrayList<>(List.of(34, 34567, "A", "B", "C", "hello")); + Set expectedAsc = new LinkedHashSet<>(listSortAsc); + ArrayList listSortDesc = new ArrayList<>(List.of("hello", "C", "B", "A", 34567, 34)); + Set expectedDesc = new LinkedHashSet<>(listSortDesc); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set1").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set1_asc").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set2").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set2_asc").toString()); + assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set1_desc").toString()); + assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set2_desc").toString()); + } + + @Test + public void setsToList_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} + """; + decoderStr = """ + var set1 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) + var tolist = set1.toList(); // create new List from Set, size = 6 + return { + "list": msg.list, + "set1": set1, + "tolist": tolist + } + """; + List listOrigin = new ArrayList<>(List.of("C", "B", "A", 34567, "B", "C", "hello", 34)); + Set expectedSet = new LinkedHashSet<>(listOrigin); + List expectedToList = new ArrayList<>(expectedSet); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(listOrigin.toString(), ((LinkedHashMap)actual).get("list").toString()); + assertEquals(expectedSet.toString(), ((LinkedHashMap)actual).get("set1").toString()); + assertEquals(expectedToList.toString(), ((LinkedHashMap)actual).get("tolist").toString()); + } + @Test public void arraysWillCauseArrayIndexOutOfBoundsException_Test() throws ExecutionException, InterruptedException { msgStr = """ diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index b782040a99..339cafd217 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -23,6 +23,7 @@ import org.mvel2.ExecutionContext; import org.mvel2.ParserConfiguration; import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionHashMap; +import org.mvel2.execution.ExecutionLinkedHashSet; import org.mvel2.util.MethodStub; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; @@ -46,6 +47,7 @@ import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -386,6 +388,12 @@ public class TbUtils { Object.class))); parserConfig.addImport("isArray", new MethodStub(TbUtils.class.getMethod("isArray", Object.class))); + parserConfig.addImport("createSetTb", new MethodStub(TbUtils.class.getMethod("createSetTb", + ExecutionContext.class))); + parserConfig.addImport("createSetTb", new MethodStub(TbUtils.class.getMethod("createSetTb", + List.class, ExecutionContext.class))); + parserConfig.addImport("isSet", new MethodStub(TbUtils.class.getMethod("isSet", + Object.class))); } public static String btoa(String input) { @@ -1481,6 +1489,19 @@ public class TbUtils { return obj != null && obj.getClass().isArray(); } + public static Set createSetTb(ExecutionContext ctx) { + return new ExecutionLinkedHashSet<>(ctx); + } + + public static Set createSetTb(List list, ExecutionContext ctx) { + Set newSet = new LinkedHashSet<>(list); + return new ExecutionLinkedHashSet<>(newSet, ctx); + } + + public static boolean isSet(Object obj) { + return obj instanceof Set; + } + 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. " + diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 6d793f2c8d..1567fdeed8 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -28,6 +28,7 @@ import org.mvel2.ParserContext; import org.mvel2.SandboxedParserConfiguration; import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionHashMap; +import org.mvel2.execution.ExecutionLinkedHashSet; import java.io.IOException; import java.math.BigDecimal; @@ -39,14 +40,18 @@ import java.util.Base64; import java.util.Calendar; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.ExecutionException; import static java.lang.Character.MAX_RADIX; import static java.lang.Character.MIN_RADIX; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @Slf4j @@ -1184,10 +1189,11 @@ public class TbUtilsTest { @Test public void isList() throws ExecutionException, InterruptedException { - List liat = List.of(0x35); - assertTrue(TbUtils.isList(liat)); - assertFalse(TbUtils.isMap(liat)); - assertFalse(TbUtils.isArray(liat)); + List list = List.of(0x35); + assertTrue(TbUtils.isList(list)); + assertFalse(TbUtils.isMap(list)); + assertFalse(TbUtils.isArray(list)); + assertFalse(TbUtils.isSet(list)); } @Test @@ -1195,6 +1201,52 @@ public class TbUtilsTest { byte [] array = new byte[]{1, 2, 3}; assertTrue(TbUtils.isArray(array)); assertFalse(TbUtils.isList(array)); + assertFalse(TbUtils.isSet(array)); + } + + @Test + public void isSet() throws ExecutionException, InterruptedException { + Set set = toSet(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xBB, (byte) 0xAA}); + assertTrue(TbUtils.isSet(set)); + assertFalse(TbUtils.isList(set)); + assertFalse(TbUtils.isArray(set)); + } + @Test + public void setTest() throws ExecutionException, InterruptedException { + Set actual = TbUtils.createSetTb(ctx); + Set expected = toSet(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xCC}); + actual.add((byte) 0xDD); + actual.add((byte) 0xCC); + actual.add((byte) 0xCC); + assertTrue(expected.containsAll(actual)); + List list = toList(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xBB, (byte) 0xAA}); + actual.addAll(list); + assertEquals(4, actual.size()); + assertTrue(actual.containsAll(expected)); + actual = TbUtils.createSetTb(list, ctx); + expected = toSet(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xDA}); + actual.add((byte) 0xDA); + actual.remove((byte) 0xBB); + actual.remove((byte) 0xAA); + assertTrue(expected.containsAll(actual)); + assertEquals(actual.size(), 3); + actual.clear(); + assertTrue(actual.isEmpty()); + actual = TbUtils.createSetTb(list, ctx); + Set actualClone = TbUtils.createSetTb(list, ctx); + Set actualClone_asc = TbUtils.createSetTb(list, ctx); + Set actualClone_desc = TbUtils.createSetTb(list, ctx); + ((ExecutionLinkedHashSet)actualClone).sort(); + ((ExecutionLinkedHashSet)actualClone_asc).sort(true); + ((ExecutionLinkedHashSet)actualClone_desc).sort(false); + assertEquals(list.toString(), actual.toString()); + assertNotEquals(list.toString(), actualClone.toString()); + Collections.sort(list); + assertEquals(list.toString(), actualClone.toString()); + assertEquals(list.toString(), actualClone_asc.toString()); + Collections.sort(list, Collections.reverseOrder()); + assertNotEquals(list.toString(), actualClone_asc.toString()); + assertEquals(list.toString(), actualClone_desc.toString()); } private static List toList(byte[] data) { @@ -1204,5 +1256,13 @@ public class TbUtilsTest { } return result; } + + private static Set toSet(byte[] data) { + Set result = new LinkedHashSet<>(); + for (Byte b : data) { + result.add(b); + } + return result; + } } diff --git a/pom.xml b/pom.xml index 64cdd63eaf..f26581507a 100755 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ 3.9.3 3.25.5 1.63.0 - 1.2.6 + 1.2.7 1.18.32 1.2.5 1.2.5 From 03c135e6ca0be1da64eaf8a9572f00db52ea9e03 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 11 Jul 2025 15:18:46 +0300 Subject: [PATCH 06/17] tbel: add to tests isSet --- .../service/script/TbelInvokeDocsIoTest.java | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 7900ca9b25..d49e091a5e 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -2657,25 +2657,21 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { list.add(0x35); return isList(list); """); + + } + + + @Test + public void isSet_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} + """; + decoderStr = """ + return isSet(createSetTb(msg.list)); // return true + """; Object actual = invokeScript(evalScript(decoderStr), msgStr); assertInstanceOf(Boolean.class, actual); assertTrue((Boolean) actual); - decoderStr = String.format(""" - var list = []; - list.add(0x35); - return isMap(list); - """); - actual = invokeScript(evalScript(decoderStr), msgStr); - assertInstanceOf(Boolean.class, actual); - assertFalse((Boolean) actual); - decoderStr = String.format(""" - var list = []; - list.add(0x35); - return isArray(list); - """); - actual = invokeScript(evalScript(decoderStr), msgStr); - assertInstanceOf(Boolean.class, actual); - assertFalse((Boolean) actual); } @Test @@ -2693,16 +2689,6 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { Object actual = invokeScript(evalScript(decoderStr), msgStr); assertInstanceOf(Boolean.class, actual); assertTrue((Boolean) actual); - decoderStr = """ - var array = new int[3]; - array[0] = 1; - array[1] = 2; - array[2] = 3; - return isList(array); - """; - actual = invokeScript(evalScript(decoderStr), msgStr); - assertInstanceOf(Boolean.class, actual); - assertFalse((Boolean) actual); } @Test From 62c8c1dbc0e4d8faa19e86d699b498b491727d9a Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 11 Jul 2025 15:19:37 +0300 Subject: [PATCH 07/17] tbel: add to tests isSet refactoring --- .../thingsboard/server/service/script/TbelInvokeDocsIoTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index d49e091a5e..b318fee08f 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -2657,10 +2657,8 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { list.add(0x35); return isList(list); """); - } - @Test public void isSet_Test() throws ExecutionException, InterruptedException { msgStr = """ From 63dd915e1b8b7c4e609a1b56c43f257122a85f56 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 11 Jul 2025 16:02:43 +0300 Subject: [PATCH 08/17] Renaming --- .../thingsboard/server/controller/AdminController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 154e349fdb..371ca4d3e4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -130,7 +130,7 @@ public class AdminController extends BaseController { @PathVariable("key") String key) throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key); - if (adminSettings.getKey().equals("mail")) { + if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) { ((ObjectNode) adminSettings.getJsonValue()).remove("password"); ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); } @@ -149,7 +149,7 @@ public class AdminController extends BaseController { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); adminSettings.setTenantId(getTenantId()); adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); - if (adminSettings.getKey().equals("mail")) { + if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) { mailService.updateMailConfiguration(); ((ObjectNode) adminSettings.getJsonValue()).remove("password"); ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); @@ -212,9 +212,9 @@ public class AdminController extends BaseController { @RequestBody AdminSettings adminSettings) throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); adminSettings = checkNotNull(adminSettings); - if (adminSettings.getKey().equals("mail")) { + if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) { if (adminSettings.getJsonValue().has("enableOauth2") && adminSettings.getJsonValue().get("enableOauth2").asBoolean()) { - AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); + AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY)); JsonNode refreshToken = mailSettings.getJsonValue().get("refreshToken"); if (refreshToken == null) { throw new ThingsboardException("Refresh token was not generated. Please, generate refresh token.", ThingsboardErrorCode.GENERAL); @@ -223,7 +223,7 @@ public class AdminController extends BaseController { settings.put("refreshToken", refreshToken.asText()); } else { if (!adminSettings.getJsonValue().has("password")) { - AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); + AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY)); ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); } } From baf68bbbfcedbd33e2cc987b421dad1654af177f Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 11 Jul 2025 16:39:03 +0300 Subject: [PATCH 09/17] tbel: refactoring --- .../java/org/thingsboard/script/api/tbel/TbUtils.java | 4 ++-- .../org/thingsboard/script/api/tbel/TbUtilsTest.java | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 339cafd217..e48d236dba 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -391,7 +391,7 @@ public class TbUtils { parserConfig.addImport("createSetTb", new MethodStub(TbUtils.class.getMethod("createSetTb", ExecutionContext.class))); parserConfig.addImport("createSetTb", new MethodStub(TbUtils.class.getMethod("createSetTb", - List.class, ExecutionContext.class))); + ExecutionContext.class, List.class))); parserConfig.addImport("isSet", new MethodStub(TbUtils.class.getMethod("isSet", Object.class))); } @@ -1493,7 +1493,7 @@ public class TbUtils { return new ExecutionLinkedHashSet<>(ctx); } - public static Set createSetTb(List list, ExecutionContext ctx) { + public static Set createSetTb(ExecutionContext ctx, List list) { Set newSet = new LinkedHashSet<>(list); return new ExecutionLinkedHashSet<>(newSet, ctx); } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 1567fdeed8..b7a3f4c07d 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1223,7 +1223,7 @@ public class TbUtilsTest { actual.addAll(list); assertEquals(4, actual.size()); assertTrue(actual.containsAll(expected)); - actual = TbUtils.createSetTb(list, ctx); + actual = TbUtils.createSetTb(ctx, list); expected = toSet(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xDA}); actual.add((byte) 0xDA); actual.remove((byte) 0xBB); @@ -1232,10 +1232,10 @@ public class TbUtilsTest { assertEquals(actual.size(), 3); actual.clear(); assertTrue(actual.isEmpty()); - actual = TbUtils.createSetTb(list, ctx); - Set actualClone = TbUtils.createSetTb(list, ctx); - Set actualClone_asc = TbUtils.createSetTb(list, ctx); - Set actualClone_desc = TbUtils.createSetTb(list, ctx); + actual = TbUtils.createSetTb(ctx, list); + Set actualClone = TbUtils.createSetTb(ctx, list); + Set actualClone_asc = TbUtils.createSetTb(ctx, list); + Set actualClone_desc = TbUtils.createSetTb(ctx, list); ((ExecutionLinkedHashSet)actualClone).sort(); ((ExecutionLinkedHashSet)actualClone_asc).sort(true); ((ExecutionLinkedHashSet)actualClone_desc).sort(false); From 017d523cc661c67d197714e7e9d7ffd8dfdd4c40 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 11 Jul 2025 18:24:51 +0300 Subject: [PATCH 10/17] tbel: test for docs: add Contains --- .../service/script/TbelInvokeDocsIoTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index b318fee08f..ead78a349c 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -985,6 +985,29 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set2_desc").toString()); } + @Test + public void setsContains_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} + """; + decoderStr = """ + var set1 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) + var result1 = set1.contains("A"); // return true + var result2 = set1.contains("H"); // return false + return { + "set1": set1, + "result1": result1, + "result2": result2 + } + """; + List listOrigin = new ArrayList<>(List.of("C", "B", "A", 34567, "B", "C", "hello", 34)); + Set expectedSet = new LinkedHashSet<>(listOrigin); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expectedSet.toString(), ((LinkedHashMap)actual).get("set1").toString()); + assertEquals(true, ((LinkedHashMap)actual).get("result1")); + assertEquals(false, ((LinkedHashMap)actual).get("result2")); + } + @Test public void setsToList_Test() throws ExecutionException, InterruptedException { msgStr = """ From 3d5d84eb1ac0f00c508f5c94368dc6629a891f70 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 11 Jul 2025 20:21:39 +0300 Subject: [PATCH 11/17] tbel: test for docs: add toSorted --- .../service/script/TbelInvokeDocsIoTest.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index ead78a349c..4aeb0d8c95 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -850,7 +850,6 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { {"list": ["B", "C", "A", "B", "C", "hello", 34567]} """; decoderStr = """ - var msgRez = {}; // add var setAdd = createSetTb(["thigsboard", 4, 67]); // create new, size = 3 var setAdd1_value = setAdd.clone(); // clone setAdd, size = 3 @@ -949,29 +948,28 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} """; decoderStr = """ - var msgRez = {}; - var set1 = msgRez.entrySet(); // create Set from map, size = 0 - set1.addAll(msg.list); // addAll to set1 from list no sort (length = 8), but set`s size = 6 ("A" and "C" is duplicated) - var set2 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) + var set1 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) + var set2 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) var set1_asc = set1.clone(); // clone set1, size = 6 var set1_desc = set1.clone(); // clone set1, size = 6 - var set2_asc = set2.clone(); // clone set2, size = 6 - var set2_desc = set2.clone(); // clone set2, size = 6 set1.sort(); // sort set1 -> asc set1_asc.sort(true); // sort set1_asc -> asc set1_desc.sort(false); // sort set1_desc -> desc - set2.sort(); // sort set2 -> asc - set2_asc.sort(true); // sort set2_asc -> asc - set2_desc.sort(false); // sort set2_desc -> desc + var set3 = set2.toSorted(); // toSorted set3 -> asc + var set3_asc = set2.toSorted(true); // toSorted set3 -> asc + var set3_desc = set2.toSorted(false); // toSorted set3 -> desc return { "set1": set1, "set1_asc": set1_asc, "set1_desc": set1_desc, "set2": set2, - "set2_asc": set2_asc, - "set2_desc": set2_desc, + "set3": set3, + "set3_asc": set3_asc, + "set3_desc": set3_desc, } """; + ArrayList list = new ArrayList<>(List.of("C", "B", "A", 34567, "hello", 34)); + Set expected = new LinkedHashSet<>(list); ArrayList listSortAsc = new ArrayList<>(List.of(34, 34567, "A", "B", "C", "hello")); Set expectedAsc = new LinkedHashSet<>(listSortAsc); ArrayList listSortDesc = new ArrayList<>(List.of("hello", "C", "B", "A", 34567, 34)); @@ -979,10 +977,11 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { Object actual = invokeScript(evalScript(decoderStr), msgStr); assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set1").toString()); assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set1_asc").toString()); - assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set2").toString()); - assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set2_asc").toString()); assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set1_desc").toString()); - assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set2_desc").toString()); + assertEquals(expected.toString(), ((LinkedHashMap)actual).get("set2").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set3").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set3_asc").toString()); + assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set3_desc").toString()); } @Test From 2eb043f29166d5aa83587d9a3c8ce8460b4da650 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 14 Jul 2025 17:54:34 +0300 Subject: [PATCH 12/17] Refactor DefaultSmsService --- .../oauth2/Oauth2AuthenticationSuccessHandler.java | 3 ++- .../server/service/sms/DefaultSmsService.java | 12 ++++-------- .../oauth2/HybridClientRegistrationRepository.java | 4 +++- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 9a34fa08df..111234500f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -121,7 +121,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS errorPrefix = "/login?loginError="; } getRedirectStrategy().sendRedirect(request, response, baseUrl + errorPrefix + - URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString())); + URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8)); } } @@ -138,4 +138,5 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS } return baseUrl + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java index 779a3adfa6..a313dcb9a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.sms; import com.fasterxml.jackson.databind.JsonNode; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.NestedRuntimeException; import org.springframework.stereotype.Service; @@ -37,8 +38,9 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -@Service @Slf4j +@Service +@RequiredArgsConstructor public class DefaultSmsService implements SmsService { private final SmsSenderFactory smsSenderFactory; @@ -48,13 +50,6 @@ public class DefaultSmsService implements SmsService { private SmsSender smsSender; - public DefaultSmsService(SmsSenderFactory smsSenderFactory, AdminSettingsService adminSettingsService, TbApiUsageStateService apiUsageStateService, TbApiUsageReportClient apiUsageClient) { - this.smsSenderFactory = smsSenderFactory; - this.adminSettingsService = adminSettingsService; - this.apiUsageStateService = apiUsageStateService; - this.apiUsageClient = apiUsageClient; - } - @PostConstruct private void init() { updateSmsConfiguration(); @@ -148,4 +143,5 @@ public class DefaultSmsService implements SmsService { return new ThingsboardException(String.format("Unable to send SMS: %s", message), ThingsboardErrorCode.GENERAL); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index 84dc22b36b..70112fa72f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -29,6 +29,7 @@ import java.util.UUID; @Component public class HybridClientRegistrationRepository implements ClientRegistrationRepository { + private static final String defaultRedirectUriTemplate = "{baseUrl}/login/oauth2/code/{registrationId}"; @Autowired @@ -43,7 +44,7 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep return toSpringClientRegistration(oAuth2Client); } - private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client){ + private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client) { String registrationId = oAuth2Client.getUuidId().toString(); // NONE is used if we need pkce-based code challenge @@ -69,4 +70,5 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep .redirectUri(defaultRedirectUriTemplate) .build(); } + } From 3e223ed8bfb8fd64b3f3cd294ab4b6f0b735f741 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 15 Jul 2025 10:31:54 +0300 Subject: [PATCH 13/17] Max client id for different mqtt protocol version --- .../server/common/data/DataConstants.java | 3 - .../mqtt/ChannelClosedException.java | 7 +- .../thingsboard/mqtt/MqttClientCallback.java | 4 +- .../thingsboard/mqtt/MqttClientConfig.java | 124 +++++------------- .../org/thingsboard/mqtt/MqttClientImpl.java | 77 +++-------- .../thingsboard/mqtt/MqttConnectResult.java | 16 +-- .../thingsboard/mqtt/MqttSubscription.java | 30 ++--- .../rule/engine/mqtt/TbMqttNode.java | 47 ++++--- .../rule/engine/mqtt/TbMqttNodeTest.java | 50 +++---- 9 files changed, 120 insertions(+), 238 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index b2d9d59cca..8a72b26a28 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.common.data; -/** - * @author Andrew Shvayka - */ public class DataConstants { public static final String TENANT = "TENANT"; diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java index 0b50b1883a..a1987cfe98 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java @@ -15,11 +15,11 @@ */ package org.thingsboard.mqtt; -/** - * Created by Valerii Sosliuk on 12/26/2017. - */ +import java.io.Serial; + public class ChannelClosedException extends RuntimeException { + @Serial private static final long serialVersionUID = 6266638352424706909L; public ChannelClosedException() { @@ -40,4 +40,5 @@ public class ChannelClosedException extends RuntimeException { public ChannelClosedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } + } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java index 85b4499e36..ae5168e466 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java @@ -21,9 +21,6 @@ import io.netty.handler.codec.mqtt.MqttPubAckMessage; import io.netty.handler.codec.mqtt.MqttSubAckMessage; import io.netty.handler.codec.mqtt.MqttUnsubAckMessage; -/** - * Created by Valerii Sosliuk on 12/30/2017. - */ public interface MqttClientCallback { /** @@ -53,4 +50,5 @@ public interface MqttClientCallback { default void onDisconnect(MqttMessage mqttDisconnectMessage) { } + } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java index 24feb3e58e..e2b6967f06 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java @@ -28,23 +28,46 @@ import java.util.Random; @SuppressWarnings({"WeakerAccess", "unused"}) public final class MqttClientConfig { + + @Getter private final SslContext sslContext; private final String randomClientId; @Getter @Setter private String ownerId; // [TenantId][IntegrationId] or [TenantId][RuleNodeId] for exceptions logging purposes + @Nonnull + @Getter private String clientId; + @Getter private int timeoutSeconds = 60; + @Getter private MqttVersion protocolVersion = MqttVersion.MQTT_3_1; - @Nullable private String username = null; - @Nullable private String password = null; + @Nullable + @Getter + @Setter + private String username = null; + @Nullable + @Getter + @Setter + private String password = null; + @Getter + @Setter private boolean cleanSession = true; - @Nullable private MqttLastWill lastWill; + @Nullable + @Getter + @Setter + private MqttLastWill lastWill; + @Setter + @Getter private Class channelClass = NioSocketChannel.class; + @Getter + @Setter private boolean reconnect = true; + @Getter private long reconnectDelay = 1L; + @Getter private int maxBytesInMessage = 8092; @Getter @@ -74,109 +97,37 @@ public final class MqttClientConfig { public MqttClientConfig(SslContext sslContext) { this.sslContext = sslContext; Random random = new Random(); - String id = "netty-mqtt/"; + StringBuilder id = new StringBuilder("netty-mqtt/"); String[] options = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(""); - for(int i = 0; i < 8; i++){ - id += options[random.nextInt(options.length)]; + for (int i = 0; i < 8; i++) { + id.append(options[random.nextInt(options.length)]); } - this.clientId = id; - this.randomClientId = id; - } - - @Nonnull - public String getClientId() { - return clientId; + this.clientId = id.toString(); + this.randomClientId = id.toString(); } public void setClientId(@Nullable String clientId) { - if(clientId == null){ + if (clientId == null) { this.clientId = randomClientId; - }else{ + } else { this.clientId = clientId; } } - public int getTimeoutSeconds() { - return timeoutSeconds; - } - public void setTimeoutSeconds(int timeoutSeconds) { - if(timeoutSeconds != -1 && timeoutSeconds <= 0){ + if (timeoutSeconds != -1 && timeoutSeconds <= 0) { throw new IllegalArgumentException("timeoutSeconds must be > 0 or -1"); } this.timeoutSeconds = timeoutSeconds; } - public MqttVersion getProtocolVersion() { - return protocolVersion; - } - public void setProtocolVersion(MqttVersion protocolVersion) { - if(protocolVersion == null){ + if (protocolVersion == null) { throw new NullPointerException("protocolVersion"); } this.protocolVersion = protocolVersion; } - @Nullable - public String getUsername() { - return username; - } - - public void setUsername(@Nullable String username) { - this.username = username; - } - - @Nullable - public String getPassword() { - return password; - } - - public void setPassword(@Nullable String password) { - this.password = password; - } - - public boolean isCleanSession() { - return cleanSession; - } - - public void setCleanSession(boolean cleanSession) { - this.cleanSession = cleanSession; - } - - @Nullable - public MqttLastWill getLastWill() { - return lastWill; - } - - public void setLastWill(@Nullable MqttLastWill lastWill) { - this.lastWill = lastWill; - } - - public Class getChannelClass() { - return channelClass; - } - - public void setChannelClass(Class channelClass) { - this.channelClass = channelClass; - } - - public SslContext getSslContext() { - return sslContext; - } - - public boolean isReconnect() { - return reconnect; - } - - public void setReconnect(boolean reconnect) { - this.reconnect = reconnect; - } - - public long getReconnectDelay() { - return reconnectDelay; - } - /** * Sets the reconnect delay in seconds. Defaults to 1 second. * @param reconnectDelay @@ -189,10 +140,6 @@ public final class MqttClientConfig { this.reconnectDelay = reconnectDelay; } - public int getMaxBytesInMessage() { - return maxBytesInMessage; - } - /** * Sets the maximum number of bytes in the message for the {@link io.netty.handler.codec.mqtt.MqttDecoder}. * Default value is 8092 as specified by Netty. The absolute maximum size is 256MB as set by the MQTT spec. @@ -206,4 +153,5 @@ public final class MqttClientConfig { } this.maxBytesInMessage = maxBytesInMessage; } + } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index 801470284b..aec63224dd 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -46,7 +46,9 @@ import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.concurrent.DefaultPromise; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; +import lombok.AccessLevel; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ListeningExecutor; @@ -67,18 +69,28 @@ import java.util.concurrent.atomic.AtomicInteger; @Slf4j final class MqttClientImpl implements MqttClient { + @Getter(AccessLevel.PACKAGE) private final Set serverSubscriptions = new HashSet<>(); + @Getter(AccessLevel.PACKAGE) private final ConcurrentMap pendingServerUnsubscribes = new ConcurrentHashMap<>(); + @Getter(AccessLevel.PACKAGE) private final ConcurrentMap qos2PendingIncomingPublishes = new ConcurrentHashMap<>(); + @Getter(AccessLevel.PACKAGE) private final ConcurrentMap pendingPublishes = new ConcurrentHashMap<>(); + @Getter(AccessLevel.PACKAGE) private final HashMultimap subscriptions = HashMultimap.create(); + @Getter(AccessLevel.PACKAGE) private final ConcurrentMap pendingSubscriptions = new ConcurrentHashMap<>(); + @Getter(AccessLevel.PACKAGE) private final Set pendingSubscribeTopics = new HashSet<>(); + @Getter(AccessLevel.PACKAGE) private final HashMultimap handlerToSubscription = HashMultimap.create(); private final AtomicInteger nextMessageId = new AtomicInteger(1); + @Getter private final MqttClientConfig clientConfig; + @Getter(AccessLevel.PACKAGE) private final MqttHandler defaultHandler; private final ReconnectStrategy reconnectStrategy; @@ -88,12 +100,15 @@ final class MqttClientImpl implements MqttClient { private volatile Channel channel; private volatile boolean disconnected = false; + @Getter private volatile boolean reconnect = false; private String host; private int port; @Getter + @Setter private MqttClientCallback callback; + @Getter private final ListeningExecutor handlerExecutor; private final static int DISCONNECT_FALLBACK_DELAY_SECS = 1; @@ -240,11 +255,6 @@ final class MqttClientImpl implements MqttClient { this.eventLoop = eventLoop; } - @Override - public ListeningExecutor getHandlerExecutor() { - return this.handlerExecutor; - } - /** * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * @@ -446,16 +456,6 @@ final class MqttClientImpl implements MqttClient { return future; } - /** - * Retrieve the MqttClient configuration - * - * @return The {@link MqttClientConfig} instance we use - */ - @Override - public MqttClientConfig getClientConfig() { - return clientConfig; - } - @Override public void disconnect() { if (disconnected) { @@ -480,25 +480,15 @@ final class MqttClientImpl implements MqttClient { } } - @Override - public void setCallback(MqttClientCallback callback) { - this.callback = callback; - } - ///////////////////////////////////////////// PRIVATE API ///////////////////////////////////////////// - public boolean isReconnect() { - return reconnect; - } - public void onSuccessfulReconnect() { if (callback != null) { callback.onSuccessfulReconnect(); } } - ChannelFuture sendAndFlushPacket(Object message) { if (this.channel == null) { return null; @@ -576,7 +566,7 @@ final class MqttClientImpl implements MqttClient { } private void checkSubscriptions(String topic, Promise promise) { - if (!(this.subscriptions.containsKey(topic) && this.subscriptions.get(topic).size() != 0) && this.serverSubscriptions.contains(topic)) { + if (!(this.subscriptions.containsKey(topic) && !this.subscriptions.get(topic).isEmpty()) && this.serverSubscriptions.contains(topic)) { MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); MqttMessageIdVariableHeader variableHeader = getNewMessageId(); MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Collections.singletonList(topic)); @@ -614,38 +604,6 @@ final class MqttClientImpl implements MqttClient { } } - ConcurrentMap getPendingSubscriptions() { - return pendingSubscriptions; - } - - HashMultimap getSubscriptions() { - return subscriptions; - } - - Set getPendingSubscribeTopics() { - return pendingSubscribeTopics; - } - - HashMultimap getHandlerToSubscription() { - return handlerToSubscription; - } - - Set getServerSubscriptions() { - return serverSubscriptions; - } - - ConcurrentMap getPendingServerUnsubscribes() { - return pendingServerUnsubscribes; - } - - ConcurrentMap getPendingPublishes() { - return pendingPublishes; - } - - ConcurrentMap getQos2PendingIncomingPublishes() { - return qos2PendingIncomingPublishes; - } - private class MqttChannelInitializer extends ChannelInitializer { private final Promise connectFuture; @@ -673,10 +631,7 @@ final class MqttClientImpl implements MqttClient { ch.pipeline().addLast("mqttPingHandler", new MqttPingHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds())); ch.pipeline().addLast("mqttHandler", new MqttChannelHandler(MqttClientImpl.this, connectFuture)); } - } - MqttHandler getDefaultHandler() { - return defaultHandler; } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java index 67757d2a7a..909955d062 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java @@ -17,14 +17,18 @@ package org.thingsboard.mqtt; import io.netty.channel.ChannelFuture; import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import lombok.Getter; import lombok.ToString; @ToString @SuppressWarnings({"WeakerAccess", "unused"}) public final class MqttConnectResult { + @Getter private final boolean success; + @Getter private final MqttConnectReturnCode returnCode; + @Getter private final ChannelFuture closeFuture; MqttConnectResult(boolean success, MqttConnectReturnCode returnCode, ChannelFuture closeFuture) { @@ -33,16 +37,4 @@ public final class MqttConnectResult { this.closeFuture = closeFuture; } - public boolean isSuccess() { - return success; - } - - public MqttConnectReturnCode getReturnCode() { - return returnCode; - } - - public ChannelFuture getCloseFuture() { - return closeFuture; - } - } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java index 7ad93462ab..d5125757da 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java @@ -15,16 +15,23 @@ */ package org.thingsboard.mqtt; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + import java.util.regex.Pattern; final class MqttSubscription { + @Getter(AccessLevel.PACKAGE) private final String topic; private final Pattern topicRegex; + @Getter private final MqttHandler handler; - + @Getter(AccessLevel.PACKAGE) private final boolean once; - + @Getter(AccessLevel.PACKAGE) + @Setter(AccessLevel.PACKAGE) private volatile boolean called; MqttSubscription(String topic, MqttHandler handler, boolean once) { @@ -40,22 +47,6 @@ final class MqttSubscription { this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$"); } - String getTopic() { - return topic; - } - - public MqttHandler getHandler() { - return handler; - } - - boolean isOnce() { - return once; - } - - boolean isCalled() { - return called; - } - boolean matches(String topic) { return this.topicRegex.matcher(topic).matches(); } @@ -78,7 +69,4 @@ final class MqttSubscription { return result; } - void setCalled(boolean called) { - this.called = called; - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 694fed1bf8..87643ae46d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -45,7 +45,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.net.ssl.SSLException; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -64,12 +63,10 @@ import java.util.concurrent.TimeoutException; ) public class TbMqttNode extends TbAbstractExternalNode { - private static final Charset UTF8 = StandardCharsets.UTF_8; - - private static final String ERROR = "error"; + private static final int MQTT_3_MAX_CLIENT_ID_LENGTH = 23; + private static final int MQTT_5_MAX_CLIENT_ID_LENGTH = 256; protected TbMqttNodeConfiguration mqttNodeConfiguration; - protected MqttClient mqttClient; @Override @@ -87,9 +84,9 @@ public class TbMqttNode extends TbAbstractExternalNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg); + String topic = TbNodeUtils.processPattern(mqttNodeConfiguration.getTopicPattern(), msg); var tbMsg = ackIfNeeded(ctx, msg); - this.mqttClient.publish(topic, Unpooled.wrappedBuffer(getData(tbMsg, mqttNodeConfiguration.isParseToPlainText()).getBytes(UTF8)), + this.mqttClient.publish(topic, Unpooled.wrappedBuffer(getData(tbMsg, mqttNodeConfiguration.isParseToPlainText()).getBytes(StandardCharsets.UTF_8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage()) .addListener(future -> { if (future.isSuccess()) { @@ -103,7 +100,7 @@ public class TbMqttNode extends TbAbstractExternalNode { private TbMsg processException(TbMsg origMsg, Throwable e) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); - metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); + metaData.putValue("error", e.getClass() + ": " + e.getMessage()); return origMsg.transform() .metaData(metaData) .build(); @@ -111,8 +108,8 @@ public class TbMqttNode extends TbAbstractExternalNode { @Override public void destroy() { - if (this.mqttClient != null) { - this.mqttClient.disconnect(); + if (mqttClient != null) { + mqttClient.disconnect(); } } @@ -123,11 +120,11 @@ public class TbMqttNode extends TbAbstractExternalNode { protected MqttClient initClient(TbContext ctx) throws Exception { MqttClientConfig config = new MqttClientConfig(getSslContext()); config.setOwnerId(getOwnerId(ctx)); - if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { + if (!StringUtils.isEmpty(mqttNodeConfiguration.getClientId())) { config.setClientId(getClientId(ctx)); } - config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); - config.setProtocolVersion(this.mqttNodeConfiguration.getProtocolVersion()); + config.setCleanSession(mqttNodeConfiguration.isCleanSession()); + config.setProtocolVersion(mqttNodeConfiguration.getProtocolVersion()); MqttClientSettings mqttClientSettings = ctx.getMqttClientSettings(); config.setRetransmissionConfig(new MqttClientConfig.RetransmissionConfig( @@ -139,32 +136,32 @@ public class TbMqttNode extends TbAbstractExternalNode { prepareMqttClientConfig(config); MqttClient client = getMqttClient(ctx, config); client.setEventLoop(ctx.getSharedEventLoop()); - Promise connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort()); + Promise connectFuture = client.connect(mqttNodeConfiguration.getHost(), mqttNodeConfiguration.getPort()); MqttConnectResult result; try { - result = connectFuture.get(this.mqttNodeConfiguration.getConnectTimeoutSec(), TimeUnit.SECONDS); + result = connectFuture.get(mqttNodeConfiguration.getConnectTimeoutSec(), TimeUnit.SECONDS); } catch (TimeoutException ex) { connectFuture.cancel(true); client.disconnect(); - String hostPort = this.mqttNodeConfiguration.getHost() + ":" + this.mqttNodeConfiguration.getPort(); + String hostPort = mqttNodeConfiguration.getHost() + ":" + mqttNodeConfiguration.getPort(); throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s.", hostPort)); } if (!result.isSuccess()) { connectFuture.cancel(true); client.disconnect(); - String hostPort = this.mqttNodeConfiguration.getHost() + ":" + this.mqttNodeConfiguration.getPort(); + String hostPort = mqttNodeConfiguration.getHost() + ":" + mqttNodeConfiguration.getPort(); throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s. Result code is: %s", hostPort, result.getReturnCode())); } return client; } private String getClientId(TbContext ctx) throws TbNodeException { - String clientId = this.mqttNodeConfiguration.isAppendClientIdSuffix() ? - this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : - this.mqttNodeConfiguration.getClientId(); - if (clientId.length() > 23) { - throw new TbNodeException("Client ID is too long '" + clientId + "'. " + - "The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true); + String clientId = mqttNodeConfiguration.isAppendClientIdSuffix() ? + mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : + mqttNodeConfiguration.getClientId(); + int maxLength = mqttNodeConfiguration.getProtocolVersion() == MqttVersion.MQTT_3_1 ? MQTT_3_MAX_CLIENT_ID_LENGTH : MQTT_5_MAX_CLIENT_ID_LENGTH; + if (clientId.length() > maxLength) { + throw new TbNodeException("The length of Client ID cannot be longer than " + maxLength + ", but current length is " + clientId.length() + ".", true); } return clientId; } @@ -174,7 +171,7 @@ public class TbMqttNode extends TbAbstractExternalNode { } protected void prepareMqttClientConfig(MqttClientConfig config) { - ClientCredentials credentials = this.mqttNodeConfiguration.getCredentials(); + ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); if (credentials.getType() == CredentialsType.BASIC) { BasicCredentials basicCredentials = (BasicCredentials) credentials; config.setUsername(basicCredentials.getUsername()); @@ -183,7 +180,7 @@ public class TbMqttNode extends TbAbstractExternalNode { } private SslContext getSslContext() throws SSLException { - return this.mqttNodeConfiguration.isSsl() ? this.mqttNodeConfiguration.getCredentials().initSslContext() : null; + return mqttNodeConfiguration.isSsl() ? mqttNodeConfiguration.getCredentials().initSslContext() : null; } private String getData(TbMsg tbMsg, boolean parseToPlainText) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index 1e877b80dc..3725dafa5e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -212,40 +212,46 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { assertThatNoException().isThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))); } - @Test - public void givenClientIdIsTooLong_whenInit_thenThrowsException() { - String invalidClientId = "vhfrbeb38ygwfwrgfwefgterhytjytj"; - mqttNodeConfig.setClientId(invalidClientId); + @ParameterizedTest + @MethodSource("provideInvalidClientIdScenarios") + public void givenInvalidClientId_whenInit_thenThrowsException(MqttVersion version, int maxLength, int repeat, String serviceId, boolean appendSuffix) { + String baseClientId = "x".repeat(repeat); + mqttNodeConfig.setClientId(baseClientId); + mqttNodeConfig.setAppendClientIdSuffix(appendSuffix); + mqttNodeConfig.setProtocolVersion(version); given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); + String clientId = appendSuffix ? baseClientId + "_" + serviceId : baseClientId; + if (appendSuffix) { + given(ctxMock.getServiceId()).willReturn(serviceId); + } + + String expectedMessage = "Client ID is too long '" + clientId + "'. " + + "The length of Client ID cannot be longer than " + maxLength + ", but current length is " + clientId.length() + "."; + assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID is too long '" + invalidClientId + "'. " + - "The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".") + .hasMessage(expectedMessage) .extracting(e -> ((TbNodeException) e).isUnrecoverable()) .isEqualTo(true); } - @Test - public void givenClientIdIsOkAndAppendClientIdSuffixIsTrue_whenInit_thenClientIdBecomesInvalidAndThrowsException() { - String validClientId = "fertjnhnjj4ge"; - mqttNodeConfig.setClientId("fertjnhnjj4ge"); - mqttNodeConfig.setAppendClientIdSuffix(true); + private static Stream provideInvalidClientIdScenarios() { + return Stream.of( + // MQTT_5, too long clientId + Arguments.of(MqttVersion.MQTT_5, 256, 257, null, false), - given(ctxMock.getTenantId()).willReturn(TENANT_ID); - given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); - String serviceId = "test-service"; - given(ctxMock.getServiceId()).willReturn(serviceId); + // MQTT_5, base + suffix exceeds + Arguments.of(MqttVersion.MQTT_5, 256, 250, "test-service", true), - String resultedClientId = validClientId + "_" + serviceId; - assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) - .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID is too long '" + resultedClientId + "'. " + - "The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + // MQTT_3_1, too long clientId + Arguments.of(MqttVersion.MQTT_3_1, 23, 24, null, false), + + // MQTT_3_1, base + suffix exceeds + Arguments.of(MqttVersion.MQTT_3_1, 23, 5, "verylongservicename", true) + ); } @Test From 233d883d4cf4592075ac95db0059891d2b41e508 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 15 Jul 2025 10:38:09 +0300 Subject: [PATCH 14/17] Test: givenInvalidClientId_whenInit_thenThrowsException --- .../java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index 3725dafa5e..5e93f6910f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -228,8 +228,7 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { given(ctxMock.getServiceId()).willReturn(serviceId); } - String expectedMessage = "Client ID is too long '" + clientId + "'. " + - "The length of Client ID cannot be longer than " + maxLength + ", but current length is " + clientId.length() + "."; + String expectedMessage = "The length of Client ID cannot be longer than " + maxLength + ", but current length is " + clientId.length() + "."; assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) .isInstanceOf(TbNodeException.class) From 8b86fdc1dc7b737c10c4d76942fcab06c344b28e Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 15 Jul 2025 18:22:40 +0300 Subject: [PATCH 15/17] lwm2m: fix bug in onUpdateValueWithSendRequest convertObjectIdToVersionedId --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 97 +++++++++++++++++-- .../lwm2m/client/LwM2MTestClient.java | 4 +- .../lwm2m/client/SimpleLwM2MDevice.java | 26 +++-- ...IntegrationDataReceivedFromClientTest.java | 16 +++ .../uplink/DefaultLwM2mUplinkMsgHandler.java | 2 +- 5 files changed, 128 insertions(+), 17 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 587f9ade1c..df253ece7c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -21,8 +21,14 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; +import org.eclipse.leshan.client.LeshanClient; import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.response.ErrorCallback; +import org.eclipse.leshan.core.response.ResponseCallback; +import org.eclipse.leshan.core.response.SendResponse; import org.eclipse.leshan.server.registration.Registration; import org.junit.After; import org.junit.Assert; @@ -73,6 +79,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; +import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult; import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; @@ -82,6 +89,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -93,6 +101,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -351,7 +360,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte getWsClient().waitForReply(); getWsClient().registerWaitForUpdate(); - this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString()); + this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString(), null); awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr()); String msg = getWsClient().waitForUpdate(); @@ -422,7 +431,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte getWsClient().waitForReply(); getWsClient().registerWaitForUpdate(); - this.createNewClient(security, null, false, endpoint, null, true, device.getId().getId().toString()); + this.createNewClient(security, null, false, endpoint, null, true, device.getId().getId().toString(), null); awaitObserveReadAll(cntObserve, lwM2MTestClient.getDeviceIdStr()); String msg = getWsClient().waitForUpdate(); @@ -543,16 +552,17 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public void createNewClient(Security security, Security securityBs, boolean isRpc, String endpoint, String deviceIdStr) throws Exception { - this.createNewClient(security, securityBs, isRpc, endpoint, null, false, deviceIdStr); + this.createNewClient(security, securityBs, isRpc, endpoint, null, false, deviceIdStr, null); } public void createNewClient(Security security, Security securityBs, boolean isRpc, String endpoint, Integer clientDtlsCidLength, String deviceIdStr) throws Exception { - this.createNewClient(security, securityBs, isRpc, endpoint, clientDtlsCidLength, false, deviceIdStr); + this.createNewClient(security, securityBs, isRpc, endpoint, clientDtlsCidLength, false, deviceIdStr, null); } public void createNewClient(Security security, Security securityBs, boolean isRpc, - String endpoint, Integer clientDtlsCidLength, boolean queueMode, String deviceIdStr) throws Exception { + String endpoint, Integer clientDtlsCidLength, boolean queueMode, + String deviceIdStr, Integer value3_0_9) throws Exception { this.clientDestroy(false); lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources); @@ -560,11 +570,86 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte int clientPort = socket.getLocalPort(); lwM2MTestClient.init(security, securityBs, clientPort, isRpc, this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, - clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR); + clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR, value3_0_9); } lwM2MTestClient.setDeviceIdStr(deviceIdStr); } + /** + * Test: "/3/0/9" value = 44 (constant); count = 10; send from client to telemetry without observe + * @param security + * @param deviceCredentials + * @param endpoint + * @param queueMode + * @throws Exception + */ + public void testConnectionWithoutObserveWithDataReceivedSingleTelemetry(Security security, + LwM2MDeviceCredentials deviceCredentials, + String endpoint, + boolean queueMode) throws Exception { + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration); + Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); + + + + SingleEntityFilter sef = new SingleEntityFilter(); + sef.setSingleEntity(device.getId()); + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel"))); + EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null); + getWsClient().send(cmd); + getWsClient().waitForReply(); + + getWsClient().registerWaitForUpdate(); + + this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString(), 44); + awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr()); + + LeshanClient leshanClient = lwM2MTestClient.getLeshanClient(); + Map registeredServers = leshanClient.getRegisteredServers(); + List paths = List.of("/3/0/9"); + int cntUpdate = 10; + int cntLast = cntUpdate; + for (final LwM2mServer server : registeredServers.values()) { + log.info("Sending Data to {} using {}.", server, ContentFormat.SENML_CBOR); + ResponseCallback responseCallback = (response) -> { + if (response.isSuccess()) + log.warn("Data sent successfully to {} [{}].", server, response.getCode()); + else + log.warn("Send data to {} failed [{}] : {}.", server, response.getCode(), + response.getErrorMessage() == null ? "" : response.getErrorMessage()); + }; + ErrorCallback errorCallback = (e) -> log.warn("Unable to send data to {}.", server, e); + while(cntLast > 0) { + leshanClient.getSendService().sendData(server, ContentFormat.SENML_CBOR, paths, + 2000, responseCallback, errorCallback); + cntLast-- ; + } + } + + + verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate)) + .updateAttrTelemetry(Mockito.any(ResourceUpdateResult.class), eq(null)); + + String msg = getWsClient().waitForUpdate(); + EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel"); + assertThat(Long.parseLong(tsValue.getValue()), instanceOf(Long.class)); + int expected = 44; + assertEquals(expected, Long.parseLong(tsValue.getValue())); + } + + private void clientDestroy(boolean isAfter) { try { if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 06e41fe29f..2ae1432cac 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -144,7 +144,7 @@ public class LwM2MTestClient { public void init(Security security, Security securityBs, int port, boolean isRpc, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode, - boolean supportFormatOnly_SenMLJSON_SenMLCBOR) throws InvalidDDFFileException, IOException { + boolean supportFormatOnly_SenMLJSON_SenMLCBOR, Integer value3_0_9) throws InvalidDDFFileException, IOException { Assert.assertNull("client already initialized", leshanClient); this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.clientContext = clientContext; @@ -197,7 +197,7 @@ public class LwM2MTestClient { initializer.setInstancesForObject(SERVER, lwm2mServer); } - initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor)); + initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor, value3_0_9)); initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice()); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index 447cc051fc..5157d5597d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -85,18 +85,22 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl */ private static Map errorCode = Map.of(0, 0L); // 0-32 + private Integer value3_0_9; public SimpleLwM2MDevice() { } - public SimpleLwM2MDevice(ScheduledExecutorService executorService) { + public SimpleLwM2MDevice(ScheduledExecutorService executorService, Integer value3_0_9) { + this.value3_0_9 = value3_0_9; try { - executorService.scheduleWithFixedDelay(() -> { - fireResourceChange(9); - fireResourceChange(20); - } - , 1, 1, TimeUnit.SECONDS); // 2 sec + if ( this.value3_0_9 == null) { + executorService.scheduleWithFixedDelay(() -> { + fireResourceChange(9); + fireResourceChange(20); + } + , 1, 1, TimeUnit.SECONDS); // 2 sec // , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN + } } catch (Throwable e) { log.error("[{}]Throwable", e.toString()); e.printStackTrace(); @@ -211,8 +215,14 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl } private int getBatteryLevel() { - int valBattery = randomIterator.nextInt(); - log.trace("Send from client [3/0/9] val: [{}]", valBattery); + int valBattery; + if (this.value3_0_9 == null) { + valBattery = randomIterator.nextInt(); + log.trace("Send from client [3/0/9] val: [{}]", valBattery); + } else { + valBattery = this.value3_0_9; + log.warn("Send from client [3/0/9] constant value: [{}]", valBattery); + } return valBattery; } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java new file mode 100644 index 0000000000..4710b98028 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java @@ -0,0 +1,16 @@ +package org.thingsboard.server.transport.lwm2m.rpc.sql; + +import org.junit.Test; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; + +public class RpcLwm2mIntegrationDataReceivedFromClientTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessAndObserveTelemetry() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC; + LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); + super.testConnectionWithoutObserveWithDataReceivedSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false); + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index 37f7829c12..431b623da9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -383,7 +383,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl LwM2mPath path = instant.getKey(); LwM2mNode node = instant.getValue(); LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); - ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path.toString(), modelProvider); + ObjectModel objectModelVersion = lwM2MClient.getObjectModel(convertObjectIdToVersionedId(path.toString(), lwM2MClient), modelProvider); if (objectModelVersion != null) { ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); if (node instanceof LwM2mObject) { From 5f6cc76d11d4bcc3cb300cf2cb7acc9fb9899a09 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 16 Jul 2025 17:31:59 +0300 Subject: [PATCH 16/17] tbel: reName createSetTb to newSet and to toSet --- .../service/script/TbelInvokeDocsIoTest.java | 22 +++++++++---------- .../thingsboard/script/api/tbel/TbUtils.java | 8 +++---- .../script/api/tbel/TbUtilsTest.java | 12 +++++----- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 4aeb0d8c95..a7affe6dff 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -785,8 +785,8 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { {"list": ["B", "A", "C", "A"]} """; decoderStr = """ - var set1 = createSetTb(msg.list); // create new Set from createSetTb() with list, no sort, size = 3 ("A" - duplicate) - var set2 = createSetTb(); // create new Set from createSetTb(), Empty + var set1 = toSet(msg.list); // create new Set from toSet() with list, no sort, size = 3 ("A" - duplicate) + var set2 = newSet(); // create new Set from newSet(), Empty return {set1: set1, set2: set2 } @@ -806,7 +806,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { {"list": ["A", "B", "C"]} """; decoderStr = """ - var set2 = createSetTb(msg.list); // create new from list, size = 3 + var set2 = toSet(msg.list); // create new from list, size = 3 var set2_0 = set2.toArray()[0]; // return "A", value with index = 0 from Set var set2Size = set2.size(); // return size = 3 var smthForeach = ""; @@ -851,16 +851,16 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { """; decoderStr = """ // add - var setAdd = createSetTb(["thigsboard", 4, 67]); // create new, size = 3 + var setAdd = toSet(["thigsboard", 4, 67]); // create new, size = 3 var setAdd1_value = setAdd.clone(); // clone setAdd, size = 3 var setAdd2_result = setAdd.add(35); // add value = 35, result = true var setAdd2_value = setAdd.clone(); // clone setAdd (fixing the result add = 35), size = 4 - var setAddList1 = createSetTb(msg.list); // create new from list without duplicate value ("B" and "C" - only one), size = 5 + var setAddList1 = toSet(msg.list); // create new from list without duplicate value ("B" and "C" - only one), size = 5 var setAdd3_result = setAdd.addAll(setAddList1); // add all without duplicate values, result = true var setAdd3_value = setAdd.clone(); // clone setAdd (with addAll), size = 9 var setAdd4_result = setAdd.add(35); // add duplicate value = 35, result = false var setAdd4_value = setAdd.clone(); // clone setAdd (after add duplicate value = 35), size = 9 - var setAddList2 = createSetTb(msg.list); // create new from list without duplicate value ("B" and "C" - only one), start: size = 5, finish: size = 7 + var setAddList2 = toSet(msg.list); // create new from list without duplicate value ("B" and "C" - only one), start: size = 5, finish: size = 7 var setAdd5_result1 = setAddList2.add(72); // add is not duplicate value = 72, result = true var setAdd5_result2 = setAddList2.add(72); // add duplicate value = 72, result = false var setAdd5_result3 = setAddList2.add("hello25"); // add is not duplicate value = "hello25", result = true @@ -948,8 +948,8 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} """; decoderStr = """ - var set1 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) - var set2 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) + var set1 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated) + var set2 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated) var set1_asc = set1.clone(); // clone set1, size = 6 var set1_desc = set1.clone(); // clone set1, size = 6 set1.sort(); // sort set1 -> asc @@ -990,7 +990,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} """; decoderStr = """ - var set1 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) + var set1 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated) var result1 = set1.contains("A"); // return true var result2 = set1.contains("H"); // return false return { @@ -1013,7 +1013,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} """; decoderStr = """ - var set1 = createSetTb(msg.list); // create new from method createSetTb(List list) no sort, size = 6 ("A" and "C" is duplicated) + var set1 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated) var tolist = set1.toList(); // create new List from Set, size = 6 return { "list": msg.list, @@ -2687,7 +2687,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} """; decoderStr = """ - return isSet(createSetTb(msg.list)); // return true + return isSet(toSet(msg.list)); // return true """; Object actual = invokeScript(evalScript(decoderStr), msgStr); assertInstanceOf(Boolean.class, actual); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index e48d236dba..072a17835d 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -388,9 +388,9 @@ public class TbUtils { Object.class))); parserConfig.addImport("isArray", new MethodStub(TbUtils.class.getMethod("isArray", Object.class))); - parserConfig.addImport("createSetTb", new MethodStub(TbUtils.class.getMethod("createSetTb", + parserConfig.addImport("newSet", new MethodStub(TbUtils.class.getMethod("newSet", ExecutionContext.class))); - parserConfig.addImport("createSetTb", new MethodStub(TbUtils.class.getMethod("createSetTb", + parserConfig.addImport("toSet", new MethodStub(TbUtils.class.getMethod("toSet", ExecutionContext.class, List.class))); parserConfig.addImport("isSet", new MethodStub(TbUtils.class.getMethod("isSet", Object.class))); @@ -1489,11 +1489,11 @@ public class TbUtils { return obj != null && obj.getClass().isArray(); } - public static Set createSetTb(ExecutionContext ctx) { + public static Set newSet(ExecutionContext ctx) { return new ExecutionLinkedHashSet<>(ctx); } - public static Set createSetTb(ExecutionContext ctx, List list) { + public static Set toSet(ExecutionContext ctx, List list) { Set newSet = new LinkedHashSet<>(list); return new ExecutionLinkedHashSet<>(newSet, ctx); } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index b7a3f4c07d..4dcbd2d69c 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1213,7 +1213,7 @@ public class TbUtilsTest { } @Test public void setTest() throws ExecutionException, InterruptedException { - Set actual = TbUtils.createSetTb(ctx); + Set actual = TbUtils.newSet(ctx); Set expected = toSet(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xCC}); actual.add((byte) 0xDD); actual.add((byte) 0xCC); @@ -1223,7 +1223,7 @@ public class TbUtilsTest { actual.addAll(list); assertEquals(4, actual.size()); assertTrue(actual.containsAll(expected)); - actual = TbUtils.createSetTb(ctx, list); + actual = TbUtils.toSet(ctx, list); expected = toSet(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xDA}); actual.add((byte) 0xDA); actual.remove((byte) 0xBB); @@ -1232,10 +1232,10 @@ public class TbUtilsTest { assertEquals(actual.size(), 3); actual.clear(); assertTrue(actual.isEmpty()); - actual = TbUtils.createSetTb(ctx, list); - Set actualClone = TbUtils.createSetTb(ctx, list); - Set actualClone_asc = TbUtils.createSetTb(ctx, list); - Set actualClone_desc = TbUtils.createSetTb(ctx, list); + actual = TbUtils.toSet(ctx, list); + Set actualClone = TbUtils.toSet(ctx, list); + Set actualClone_asc = TbUtils.toSet(ctx, list); + Set actualClone_desc = TbUtils.toSet(ctx, list); ((ExecutionLinkedHashSet)actualClone).sort(); ((ExecutionLinkedHashSet)actualClone_asc).sort(true); ((ExecutionLinkedHashSet)actualClone_desc).sort(false); From 42f7fd32e56d579486bb950ec9c5ef3579dec7b8 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Wed, 16 Jul 2025 18:37:54 +0300 Subject: [PATCH 17/17] Add missing license header --- ...wm2mIntegrationDataReceivedFromClientTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java index 4710b98028..8cb5e06248 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.thingsboard.server.transport.lwm2m.rpc.sql; import org.junit.Test;