Browse Source
# Conflicts: # application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java # application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java # application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java # common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.javapull/10728/head
265 changed files with 19422 additions and 1475 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,188 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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.controller; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.http.ResponseEntity; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.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.RequestHeader; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.mobile.AndroidConfig; |
|||
import org.thingsboard.server.common.data.mobile.IosConfig; |
|||
import org.thingsboard.server.common.data.mobile.MobileAppSettings; |
|||
import org.thingsboard.server.common.data.security.model.JwtPair; |
|||
import org.thingsboard.server.config.annotations.ApiOperation; |
|||
import org.thingsboard.server.dao.mobile.MobileAppSettingsService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.mobile.secret.MobileAppSecretService; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.permission.Operation; |
|||
import org.thingsboard.server.service.security.permission.Resource; |
|||
import org.thingsboard.server.service.security.system.SystemSecurityService; |
|||
|
|||
import java.net.URI; |
|||
import java.net.URISyntaxException; |
|||
|
|||
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; |
|||
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; |
|||
|
|||
@RequiredArgsConstructor |
|||
@RestController |
|||
@TbCoreComponent |
|||
public class MobileApplicationController extends BaseController { |
|||
|
|||
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}") |
|||
private int mobileSecretKeyTtl; |
|||
|
|||
public static final String ASSET_LINKS_PATTERN = "[{\n" + |
|||
" \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" + |
|||
" \"target\": {\n" + |
|||
" \"namespace\": \"android_app\",\n" + |
|||
" \"package_name\": \"%s\",\n" + |
|||
" \"sha256_cert_fingerprints\":\n" + |
|||
" [\"%s\"]\n" + |
|||
" }\n" + |
|||
"}]"; |
|||
|
|||
public static final String APPLE_APP_SITE_ASSOCIATION_PATTERN = "{\n" + |
|||
" \"applinks\": {\n" + |
|||
" \"apps\": [],\n" + |
|||
" \"details\": [\n" + |
|||
" {\n" + |
|||
" \"appID\": \"%s\",\n" + |
|||
" \"paths\": [ \"/api/noauth/qr\" ]\n" + |
|||
" }\n" + |
|||
" ]\n" + |
|||
" }\n" + |
|||
"}"; |
|||
|
|||
public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app"; |
|||
public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695"; |
|||
public static final String SECRET = "secret"; |
|||
public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-lived secret key"; |
|||
public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io"; |
|||
public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s&ttl=%s"; |
|||
|
|||
private final SystemSecurityService systemSecurityService; |
|||
private final MobileAppSecretService mobileAppSecretService; |
|||
private final MobileAppSettingsService mobileAppSettingsService; |
|||
|
|||
@ApiOperation(value = "Get associated android applications (getAssetLinks)") |
|||
@GetMapping(value = "/.well-known/assetlinks.json") |
|||
public ResponseEntity<JsonNode> getAssetLinks() { |
|||
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); |
|||
AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig(); |
|||
if (androidConfig != null && androidConfig.isEnabled() && !androidConfig.getAppPackage().isBlank() && !androidConfig.getSha256CertFingerprints().isBlank()) { |
|||
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints()))); |
|||
} else { |
|||
return ResponseEntity.notFound().build(); |
|||
} |
|||
} |
|||
|
|||
@ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)") |
|||
@GetMapping(value = "/.well-known/apple-app-site-association") |
|||
public ResponseEntity<JsonNode> getAppleAppSiteAssociation() { |
|||
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); |
|||
IosConfig iosConfig = mobileAppSettings.getIosConfig(); |
|||
if (iosConfig != null && iosConfig.isEnabled() && !iosConfig.getAppId().isBlank()) { |
|||
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId()))); |
|||
} else { |
|||
return ResponseEntity.notFound().build(); |
|||
} |
|||
} |
|||
|
|||
@ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)", |
|||
notes = "The request payload contains configuration for android/iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") |
|||
@PostMapping(value = "/api/mobile/app/settings") |
|||
public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") |
|||
@RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException { |
|||
SecurityUser currentUser = getCurrentUser(); |
|||
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE); |
|||
mobileAppSettings.setTenantId(getTenantId()); |
|||
return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)", |
|||
notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@GetMapping(value = "/api/mobile/app/settings") |
|||
public MobileAppSettings getMobileAppSettings() throws ThingsboardException { |
|||
SecurityUser currentUser = getCurrentUser(); |
|||
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); |
|||
return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)", |
|||
notes = "Fetch the url that takes user to linked mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@GetMapping(value = "/api/mobile/deepLink", produces = "text/plain") |
|||
public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { |
|||
String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser()); |
|||
String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request); |
|||
String platformDomain = new URI(baseUrl).getHost(); |
|||
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); |
|||
String appDomain; |
|||
if (!mobileAppSettings.isUseDefaultApp()) { |
|||
appDomain = platformDomain; |
|||
} else { |
|||
appDomain = DEFAULT_APP_DOMAIN; |
|||
} |
|||
String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl); |
|||
if (!appDomain.equals(platformDomain)) { |
|||
deepLink = deepLink + "&host=" + baseUrl; |
|||
} |
|||
return "\"" + deepLink + "\""; |
|||
} |
|||
|
|||
@ApiOperation(value = "Get User Token (getUserTokenByMobileSecret)", |
|||
notes = "Returns the token of the User based on the provided secret key.") |
|||
@GetMapping(value = "/api/noauth/qr/{secret}") |
|||
public JwtPair getUserTokenByMobileSecret(@Parameter(description = SECRET_PARAM_DESCRIPTION) |
|||
@PathVariable(SECRET) String secret) throws ThingsboardException { |
|||
checkParameter(SECRET, secret); |
|||
return mobileAppSecretService.getJwtPair(secret); |
|||
} |
|||
|
|||
@GetMapping(value = "/api/noauth/qr") |
|||
public ResponseEntity<?> getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) { |
|||
if (userAgent.contains("Android")) { |
|||
return ResponseEntity.status(HttpStatus.FOUND) |
|||
.header("Location", ANDROID_APPLICATION_STORE_LINK) |
|||
.build(); |
|||
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) { |
|||
return ResponseEntity.status(HttpStatus.FOUND) |
|||
.header("Location", APPLE_APPLICATION_STORE_LINK) |
|||
.build(); |
|||
} else { |
|||
return ResponseEntity.status(HttpStatus.NOT_FOUND) |
|||
.build(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.mail; |
|||
|
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.common.util.AbstractListeningExecutor; |
|||
|
|||
/** |
|||
* Executor have the sole purpose to send mails. It should be used only by Mail Service. |
|||
* For other purposes please use the MailExecutorService component |
|||
* */ |
|||
@Component |
|||
public class MailSenderInternalExecutorService extends AbstractListeningExecutor { |
|||
|
|||
@Value("${actors.rule.mail_thread_pool_size}") |
|||
private int mailExecutorThreadPoolSize; |
|||
|
|||
@Override |
|||
protected int getThreadPollSize() { |
|||
return mailExecutorThreadPoolSize; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.mobile.secret; |
|||
|
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.security.model.JwtPair; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
|
|||
public interface MobileAppSecretService { |
|||
|
|||
String generateMobileAppSecret(SecurityUser securityUser); |
|||
|
|||
JwtPair getJwtPair(String secret) throws ThingsboardException; |
|||
|
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.mobile.secret; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.event.TransactionalEventListener; |
|||
import org.thingsboard.server.cache.TbCacheValueWrapper; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.security.model.JwtPair; |
|||
import org.thingsboard.server.dao.entity.AbstractCachedService; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
|||
import org.thingsboard.server.service.security.system.SystemSecurityService; |
|||
|
|||
import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_MOBILE_SECRET_KEY_LENGTH; |
|||
|
|||
@Service |
|||
@Slf4j |
|||
@RequiredArgsConstructor |
|||
public class MobileAppSecretServiceImpl extends AbstractCachedService<String, JwtPair, MobileSecretEvictEvent> implements MobileAppSecretService { |
|||
|
|||
private final JwtTokenFactory tokenFactory; |
|||
private final SystemSecurityService systemSecurityService; |
|||
|
|||
@Override |
|||
public String generateMobileAppSecret(SecurityUser securityUser) { |
|||
log.trace("Executing generateSecret for user [{}]", securityUser.getId()); |
|||
Integer mobileSecretKeyLength = systemSecurityService.getSecuritySettings().getMobileSecretKeyLength(); |
|||
String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength); |
|||
cache.put(secret, tokenFactory.createTokenPair(securityUser)); |
|||
return secret; |
|||
} |
|||
|
|||
@Override |
|||
public JwtPair getJwtPair(String secret) throws ThingsboardException { |
|||
TbCacheValueWrapper<JwtPair> jwtPair = cache.get(secret); |
|||
if (jwtPair != null) { |
|||
return jwtPair.get(); |
|||
} else { |
|||
throw new ThingsboardException("Jwt token not found or expired!", ThingsboardErrorCode.JWT_TOKEN_EXPIRED); |
|||
} |
|||
} |
|||
|
|||
@TransactionalEventListener(classes = MobileSecretEvictEvent.class) |
|||
@Override |
|||
public void handleEvictEvent(MobileSecretEvictEvent event) { |
|||
cache.evict(event.getSecret()); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.mobile.secret; |
|||
|
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.cache.CacheManager; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.cache.CaffeineTbTransactionalCache; |
|||
import org.thingsboard.server.common.data.CacheConstants; |
|||
import org.thingsboard.server.common.data.security.model.JwtPair; |
|||
|
|||
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) |
|||
@Service("MobileSecretCache") |
|||
public class MobileSecretCaffeineCache extends CaffeineTbTransactionalCache<String, JwtPair> { |
|||
|
|||
public MobileSecretCaffeineCache(CacheManager cacheManager) { |
|||
super(cacheManager, CacheConstants.MOBILE_SECRET_KEY_CACHE); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.mobile.secret; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class MobileSecretEvictEvent { |
|||
|
|||
private final String secret; |
|||
|
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.mobile.secret; |
|||
|
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.cache.CacheSpecsMap; |
|||
import org.thingsboard.server.cache.RedisTbTransactionalCache; |
|||
import org.thingsboard.server.cache.TBRedisCacheConfiguration; |
|||
import org.thingsboard.server.cache.TbJsonRedisSerializer; |
|||
import org.thingsboard.server.common.data.CacheConstants; |
|||
import org.thingsboard.server.common.data.security.model.JwtPair; |
|||
|
|||
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") |
|||
@Service("MobileSecretCache") |
|||
public class MobileSecretRedisCache extends RedisTbTransactionalCache<String, JwtPair> { |
|||
|
|||
public MobileSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { |
|||
super(CacheConstants.MOBILE_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class)); |
|||
} |
|||
} |
|||
@ -0,0 +1,322 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.queue.consumer; |
|||
|
|||
import lombok.Builder; |
|||
import lombok.Getter; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|||
import org.thingsboard.server.common.data.queue.QueueConfig; |
|||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
|||
import org.thingsboard.server.queue.TbQueueConsumer; |
|||
import org.thingsboard.server.queue.TbQueueMsg; |
|||
import org.thingsboard.server.queue.discovery.QueueKey; |
|||
import org.thingsboard.server.service.queue.ruleengine.QueueEvent; |
|||
import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerManagerTask; |
|||
import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerTask; |
|||
|
|||
import java.util.Collection; |
|||
import java.util.Collections; |
|||
import java.util.HashMap; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ConcurrentLinkedQueue; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Future; |
|||
import java.util.concurrent.ScheduledExecutorService; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.locks.ReentrantLock; |
|||
import java.util.function.Function; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Slf4j |
|||
public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfig> { |
|||
|
|||
protected final QueueKey queueKey; |
|||
@Getter |
|||
protected C config; |
|||
protected final MsgPackProcessor<M, C> msgPackProcessor; |
|||
protected final Function<C, TbQueueConsumer<M>> consumerCreator; |
|||
protected final ExecutorService consumerExecutor; |
|||
protected final ScheduledExecutorService scheduler; |
|||
protected final ExecutorService taskExecutor; |
|||
|
|||
private final java.util.Queue<TbQueueConsumerManagerTask> tasks = new ConcurrentLinkedQueue<>(); |
|||
private final ReentrantLock lock = new ReentrantLock(); |
|||
|
|||
@Getter |
|||
private volatile Set<TopicPartitionInfo> partitions; |
|||
protected volatile ConsumerWrapper<M> consumerWrapper; |
|||
protected volatile boolean stopped; |
|||
|
|||
@Builder |
|||
public MainQueueConsumerManager(QueueKey queueKey, C config, |
|||
MsgPackProcessor<M, C> msgPackProcessor, |
|||
Function<C, TbQueueConsumer<M>> consumerCreator, |
|||
ExecutorService consumerExecutor, |
|||
ScheduledExecutorService scheduler, |
|||
ExecutorService taskExecutor) { |
|||
this.queueKey = queueKey; |
|||
this.config = config; |
|||
this.msgPackProcessor = msgPackProcessor; |
|||
this.consumerCreator = consumerCreator; |
|||
this.consumerExecutor = consumerExecutor; |
|||
this.scheduler = scheduler; |
|||
this.taskExecutor = taskExecutor; |
|||
if (config != null) { |
|||
init(config); |
|||
} |
|||
} |
|||
|
|||
public void init(C config) { |
|||
this.config = config; |
|||
if (config.isConsumerPerPartition()) { |
|||
this.consumerWrapper = new ConsumerPerPartitionWrapper(); |
|||
} else { |
|||
this.consumerWrapper = new SingleConsumerWrapper(); |
|||
} |
|||
log.debug("[{}] Initialized consumer for queue: {}", queueKey, config); |
|||
} |
|||
|
|||
public void update(C config) { |
|||
addTask(TbQueueConsumerManagerTask.configUpdate(config)); |
|||
} |
|||
|
|||
public void update(Set<TopicPartitionInfo> partitions) { |
|||
addTask(TbQueueConsumerManagerTask.partitionChange(partitions)); |
|||
} |
|||
|
|||
protected void addTask(TbQueueConsumerManagerTask todo) { |
|||
if (stopped) { |
|||
return; |
|||
} |
|||
tasks.add(todo); |
|||
log.trace("[{}] Added task: {}", queueKey, todo); |
|||
tryProcessTasks(); |
|||
} |
|||
|
|||
private void tryProcessTasks() { |
|||
taskExecutor.submit(() -> { |
|||
if (lock.tryLock()) { |
|||
try { |
|||
C newConfig = null; |
|||
Set<TopicPartitionInfo> newPartitions = null; |
|||
while (!stopped) { |
|||
TbQueueConsumerManagerTask task = tasks.poll(); |
|||
if (task == null) { |
|||
break; |
|||
} |
|||
log.trace("[{}] Processing task: {}", queueKey, task); |
|||
|
|||
if (task.getEvent() == QueueEvent.PARTITION_CHANGE) { |
|||
newPartitions = task.getPartitions(); |
|||
} else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) { |
|||
newConfig = (C) task.getConfig(); |
|||
} else { |
|||
processTask(task); |
|||
} |
|||
} |
|||
if (stopped) { |
|||
return; |
|||
} |
|||
if (newConfig != null) { |
|||
doUpdate(newConfig); |
|||
} |
|||
if (newPartitions != null) { |
|||
doUpdate(newPartitions); |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("[{}] Failed to process tasks", queueKey, e); |
|||
} finally { |
|||
lock.unlock(); |
|||
} |
|||
} else { |
|||
log.trace("[{}] Failed to acquire lock", queueKey); |
|||
scheduler.schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
protected void processTask(TbQueueConsumerManagerTask task) { |
|||
} |
|||
|
|||
private void doUpdate(C newConfig) { |
|||
log.info("[{}] Processing queue update: {}", queueKey, newConfig); |
|||
var oldConfig = this.config; |
|||
this.config = newConfig; |
|||
if (log.isTraceEnabled()) { |
|||
log.trace("[{}] Old queue configuration: {}", queueKey, oldConfig); |
|||
log.trace("[{}] New queue configuration: {}", queueKey, newConfig); |
|||
} |
|||
|
|||
if (oldConfig == null) { |
|||
init(config); |
|||
} else if (newConfig.isConsumerPerPartition() != oldConfig.isConsumerPerPartition()) { |
|||
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); |
|||
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); |
|||
|
|||
init(config); |
|||
if (partitions != null) { |
|||
doUpdate(partitions); // even if partitions number was changed, there can be no partition change event
|
|||
} |
|||
} else { |
|||
log.trace("[{}] Silently applied new config, because consumer-per-partition not changed", queueKey); |
|||
// do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent,
|
|||
// and changes to other config values will be picked up by consumer on the fly,
|
|||
// and queue topic and name are immutable
|
|||
} |
|||
} |
|||
|
|||
private void doUpdate(Set<TopicPartitionInfo> partitions) { |
|||
this.partitions = partitions; |
|||
consumerWrapper.updatePartitions(partitions); |
|||
} |
|||
|
|||
private void launchConsumer(TbQueueConsumerTask<M> consumerTask) { |
|||
log.info("[{}] Launching consumer", consumerTask.getKey()); |
|||
Future<?> consumerLoop = consumerExecutor.submit(() -> { |
|||
ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString()); |
|||
try { |
|||
consumerLoop(consumerTask.getConsumer()); |
|||
} catch (Throwable e) { |
|||
log.error("Failure in consumer loop", e); |
|||
} |
|||
log.info("[{}] Consumer stopped", consumerTask.getKey()); |
|||
}); |
|||
consumerTask.setTask(consumerLoop); |
|||
} |
|||
|
|||
private void consumerLoop(TbQueueConsumer<M> consumer) { |
|||
while (!stopped && !consumer.isStopped()) { |
|||
try { |
|||
List<M> msgs = consumer.poll(config.getPollInterval()); |
|||
if (msgs.isEmpty()) { |
|||
continue; |
|||
} |
|||
processMsgs(msgs, consumer, config); |
|||
} catch (Exception e) { |
|||
if (!consumer.isStopped()) { |
|||
log.warn("Failed to process messages from queue", e); |
|||
try { |
|||
Thread.sleep(config.getPollInterval()); |
|||
} catch (InterruptedException e2) { |
|||
log.trace("Failed to wait until the server has capacity to handle new requests", e2); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (consumer.isStopped()) { |
|||
consumer.unsubscribe(); |
|||
} |
|||
} |
|||
|
|||
protected void processMsgs(List<M> msgs, TbQueueConsumer<M> consumer, C config) throws Exception { |
|||
msgPackProcessor.process(msgs, consumer, config); |
|||
} |
|||
|
|||
public void stop() { |
|||
log.debug("[{}] Stopping consumers", queueKey); |
|||
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); |
|||
stopped = true; |
|||
} |
|||
|
|||
public void awaitStop() { |
|||
log.debug("[{}] Waiting for consumers to stop", queueKey); |
|||
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); |
|||
log.debug("[{}] Unsubscribed and stopped consumers", queueKey); |
|||
} |
|||
|
|||
private static String partitionsToString(Collection<TopicPartitionInfo> partitions) { |
|||
return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]")); |
|||
} |
|||
|
|||
public interface MsgPackProcessor<M extends TbQueueMsg, C extends QueueConfig> { |
|||
void process(List<M> msgs, TbQueueConsumer<M> consumer, C config) throws Exception; |
|||
} |
|||
|
|||
public interface ConsumerWrapper<M extends TbQueueMsg> { |
|||
|
|||
void updatePartitions(Set<TopicPartitionInfo> partitions); |
|||
|
|||
Collection<TbQueueConsumerTask<M>> getConsumers(); |
|||
|
|||
} |
|||
|
|||
class ConsumerPerPartitionWrapper implements ConsumerWrapper<M> { |
|||
private final Map<TopicPartitionInfo, TbQueueConsumerTask<M>> consumers = new HashMap<>(); |
|||
|
|||
@Override |
|||
public void updatePartitions(Set<TopicPartitionInfo> partitions) { |
|||
Set<TopicPartitionInfo> addedPartitions = new HashSet<>(partitions); |
|||
addedPartitions.removeAll(consumers.keySet()); |
|||
|
|||
Set<TopicPartitionInfo> removedPartitions = new HashSet<>(consumers.keySet()); |
|||
removedPartitions.removeAll(partitions); |
|||
log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); |
|||
|
|||
removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop()); |
|||
removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion()); |
|||
|
|||
addedPartitions.forEach((tpi) -> { |
|||
String key = queueKey + "-" + tpi.getPartition().orElse(-1); |
|||
TbQueueConsumerTask<M> consumer = new TbQueueConsumerTask<>(key, consumerCreator.apply(config)); |
|||
consumers.put(tpi, consumer); |
|||
consumer.subscribe(Set.of(tpi)); |
|||
launchConsumer(consumer); |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public Collection<TbQueueConsumerTask<M>> getConsumers() { |
|||
return consumers.values(); |
|||
} |
|||
} |
|||
|
|||
class SingleConsumerWrapper implements ConsumerWrapper<M> { |
|||
private TbQueueConsumerTask<M> consumer; |
|||
|
|||
@Override |
|||
public void updatePartitions(Set<TopicPartitionInfo> partitions) { |
|||
log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions)); |
|||
if (partitions.isEmpty()) { |
|||
if (consumer != null && consumer.isRunning()) { |
|||
consumer.initiateStop(); |
|||
consumer.awaitCompletion(); |
|||
} |
|||
consumer = null; |
|||
return; |
|||
} |
|||
|
|||
if (consumer == null) { |
|||
consumer = new TbQueueConsumerTask<>(queueKey, consumerCreator.apply(config)); |
|||
} |
|||
consumer.subscribe(partitions); |
|||
if (!consumer.isRunning()) { |
|||
launchConsumer(consumer); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Collection<TbQueueConsumerTask<M>> getConsumers() { |
|||
if (consumer == null) { |
|||
return Collections.emptyList(); |
|||
} |
|||
return List.of(consumer); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.subscription; |
|||
|
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
|
|||
/** |
|||
* The modification result of entity subscription |
|||
*/ |
|||
@Builder |
|||
@Data |
|||
public class SubscriptionModificationResult { |
|||
|
|||
private TenantId tenantId; |
|||
private EntityId entityId; |
|||
private TbSubscription<?> subscription; |
|||
private TbSubscription<?> missedUpdatesCandidate; |
|||
private TbEntitySubEvent event; |
|||
|
|||
public boolean hasEvent() { |
|||
return event != null; |
|||
} |
|||
} |
|||
@ -0,0 +1,253 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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.controller; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.thingsboard.server.common.data.mobile.AndroidConfig; |
|||
import org.thingsboard.server.common.data.mobile.IosConfig; |
|||
import org.thingsboard.server.common.data.mobile.MobileAppSettings; |
|||
import org.thingsboard.server.common.data.mobile.QRCodeConfig; |
|||
import org.thingsboard.server.common.data.security.model.JwtPair; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
import java.util.regex.Matcher; |
|||
import java.util.regex.Pattern; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.hamcrest.Matchers.containsString; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
|
|||
@Slf4j |
|||
@DaoSqlTest |
|||
public class MobileApplicationControllerTest extends AbstractControllerTest { |
|||
|
|||
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}") |
|||
private int mobileSecretKeyTtl; |
|||
private static final String ANDROID_PACKAGE_NAME = "testAppPackage"; |
|||
private static final String ANDROID_APP_SHA256 = "DF:28:32:66:8B:A7:D3:EC:7D:73:CF:CC"; |
|||
private static final String APPLE_APP_ID = "testId"; |
|||
private static final String TEST_LABEL = "Test label"; |
|||
|
|||
@Before |
|||
public void setUp() throws Exception { |
|||
loginSysAdmin(); |
|||
|
|||
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
|||
QRCodeConfig qrCodeConfig = new QRCodeConfig(); |
|||
qrCodeConfig.setQrCodeLabel(TEST_LABEL); |
|||
|
|||
mobileAppSettings.setUseDefaultApp(true); |
|||
AndroidConfig androidConfig = AndroidConfig.builder() |
|||
.appPackage(ANDROID_PACKAGE_NAME) |
|||
.sha256CertFingerprints(ANDROID_APP_SHA256) |
|||
.enabled(true) |
|||
.build(); |
|||
|
|||
IosConfig iosConfig = IosConfig.builder() |
|||
.appId(APPLE_APP_ID) |
|||
.enabled(true) |
|||
.build(); |
|||
mobileAppSettings.setAndroidConfig(androidConfig); |
|||
mobileAppSettings.setIosConfig(iosConfig); |
|||
mobileAppSettings.setQrCodeConfig(qrCodeConfig); |
|||
|
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testSaveMobileAppSettings() throws Exception { |
|||
loginSysAdmin(); |
|||
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
|||
assertThat(mobileAppSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL); |
|||
assertThat(mobileAppSettings.isUseDefaultApp()).isTrue(); |
|||
|
|||
mobileAppSettings.setUseDefaultApp(false); |
|||
|
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isOk()); |
|||
|
|||
MobileAppSettings updatedMobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
|||
assertThat(updatedMobileAppSettings.isUseDefaultApp()).isFalse(); |
|||
} |
|||
|
|||
@Test |
|||
public void testShouldNotSaveMobileAppSettingsWithoutRequiredConfig() throws Exception { |
|||
loginSysAdmin(); |
|||
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
|||
|
|||
mobileAppSettings.setUseDefaultApp(false); |
|||
mobileAppSettings.setAndroidConfig(null); |
|||
mobileAppSettings.setIosConfig(null); |
|||
mobileAppSettings.setQrCodeConfig(null); |
|||
|
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isBadRequest()) |
|||
.andExpect(statusReason(containsString("Android/ios settings are required to use custom application!"))); |
|||
|
|||
mobileAppSettings.setAndroidConfig(AndroidConfig.builder().enabled(false).build()); |
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isBadRequest()) |
|||
.andExpect(statusReason(containsString("Android/ios settings are required to use custom application!"))); |
|||
|
|||
mobileAppSettings.setIosConfig(IosConfig.builder().enabled(false).build()); |
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isBadRequest()) |
|||
.andExpect(statusReason(containsString("Qr code configuration is required!"))); |
|||
|
|||
mobileAppSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build()); |
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testShouldNotSaveMobileAppSettingsWithoutRequiredAndroidConf() throws Exception { |
|||
loginSysAdmin(); |
|||
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
|||
mobileAppSettings.setUseDefaultApp(false); |
|||
AndroidConfig androidConfig = AndroidConfig.builder() |
|||
.enabled(true) |
|||
.appPackage(null) |
|||
.sha256CertFingerprints(null) |
|||
.build(); |
|||
mobileAppSettings.setAndroidConfig(androidConfig); |
|||
|
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isBadRequest()) |
|||
.andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!"))); |
|||
|
|||
androidConfig.setAppPackage("test_app_package"); |
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isBadRequest()) |
|||
.andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!"))); |
|||
|
|||
androidConfig.setSha256CertFingerprints("test_sha_256"); |
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testShouldNotSaveMobileAppSettingsWithoutRequiredIosConf() throws Exception { |
|||
loginSysAdmin(); |
|||
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
|||
mobileAppSettings.setUseDefaultApp(false); |
|||
IosConfig iosConfig = IosConfig.builder() |
|||
.enabled(true) |
|||
.appId(null) |
|||
.build(); |
|||
mobileAppSettings.setIosConfig(iosConfig); |
|||
|
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isBadRequest()) |
|||
.andExpect(statusReason(containsString("Application id is required for custom ios application!"))); |
|||
|
|||
iosConfig.setAppId("test_app_id"); |
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testShouldSaveMobileAppSettingsForDefaultApp() throws Exception { |
|||
loginSysAdmin(); |
|||
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
|||
mobileAppSettings.setUseDefaultApp(true); |
|||
mobileAppSettings.setIosConfig(null); |
|||
mobileAppSettings.setAndroidConfig(null); |
|||
|
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetApplicationAssociations() throws Exception { |
|||
loginSysAdmin(); |
|||
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
|||
mobileAppSettings.setUseDefaultApp(false); |
|||
doPost("/api/mobile/app/settings", mobileAppSettings) |
|||
.andExpect(status().isOk()); |
|||
|
|||
JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class); |
|||
assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo(ANDROID_PACKAGE_NAME); |
|||
assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256); |
|||
|
|||
JsonNode appleAssociation = doGet("/.well-known/apple-app-site-association", JsonNode.class); |
|||
assertThat(appleAssociation.get("applinks").get("details").get(0).get("appID").asText()).isEqualTo(APPLE_APP_ID); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetMobileDeepLink() throws Exception { |
|||
loginSysAdmin(); |
|||
String deepLink = doGet("/api/mobile/deepLink", String.class); |
|||
|
|||
Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\""); |
|||
Matcher parsedDeepLink = expectedPattern.matcher(deepLink); |
|||
assertThat(parsedDeepLink.matches()).isTrue(); |
|||
String appHost = parsedDeepLink.group(1); |
|||
String secret = parsedDeepLink.group(2); |
|||
String ttl = parsedDeepLink.group(3); |
|||
assertThat(appHost).isEqualTo("demo.thingsboard.io"); |
|||
assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl)); |
|||
|
|||
JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class); |
|||
assertThat(jwtPair).isNotNull(); |
|||
|
|||
loginTenantAdmin(); |
|||
String tenantDeepLink = doGet("/api/mobile/deepLink", String.class); |
|||
Matcher tenantParsedDeepLink = expectedPattern.matcher(tenantDeepLink); |
|||
assertThat(tenantParsedDeepLink.matches()).isTrue(); |
|||
String tenantSecret = tenantParsedDeepLink.group(2); |
|||
|
|||
JwtPair tenantJwtPair = doGet("/api/noauth/qr/" + tenantSecret, JwtPair.class); |
|||
assertThat(tenantJwtPair).isNotNull(); |
|||
|
|||
loginCustomerUser(); |
|||
String customerDeepLink = doGet("/api/mobile/deepLink", String.class); |
|||
Matcher customerParsedDeepLink = expectedPattern.matcher(customerDeepLink); |
|||
assertThat(customerParsedDeepLink.matches()).isTrue(); |
|||
String customerSecret = customerParsedDeepLink.group(2); |
|||
|
|||
JwtPair customerJwtPair = doGet("/api/noauth/qr/" + customerSecret, JwtPair.class); |
|||
assertThat(customerJwtPair).isNotNull(); |
|||
|
|||
// update mobile setting to use custom one
|
|||
loginSysAdmin(); |
|||
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
|||
mobileAppSettings.setUseDefaultApp(false); |
|||
doPost("/api/mobile/app/settings", mobileAppSettings); |
|||
|
|||
String customAppDeepLink = doGet("/api/mobile/deepLink", String.class); |
|||
Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\""); |
|||
Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink); |
|||
assertThat(customAppParsedDeepLink.matches()).isTrue(); |
|||
assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost"); |
|||
|
|||
loginTenantAdmin(); |
|||
String tenantCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class); |
|||
Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink); |
|||
assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue(); |
|||
assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); |
|||
|
|||
loginCustomerUser(); |
|||
String customerCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class); |
|||
Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink); |
|||
assertThat(customerCustomAppParsedDeepLink.matches()).isTrue(); |
|||
assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); |
|||
} |
|||
} |
|||
@ -0,0 +1,171 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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.mqtt; |
|||
|
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.mockito.Mockito; |
|||
import org.springframework.boot.test.mock.mockito.SpyBean; |
|||
import org.springframework.test.context.TestPropertySource; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.TenantProfile; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; |
|||
import org.thingsboard.server.common.data.security.DeviceCredentials; |
|||
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
|||
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; |
|||
import org.thingsboard.server.controller.AbstractControllerTest; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; |
|||
|
|||
import java.util.function.Consumer; |
|||
|
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.junit.Assert.assertNotNull; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY; |
|||
|
|||
@DaoSqlTest |
|||
@TestPropertySource(properties = { |
|||
"service.integrations.supported=ALL", |
|||
"transport.mqtt.enabled=true", |
|||
}) |
|||
public class MqttGatewayRateLimitsTest extends AbstractControllerTest { |
|||
|
|||
private static final String TOPIC = "v1/gateway/telemetry"; |
|||
private static final String DEVICE_A = "DeviceA"; |
|||
private static final String DEVICE_B = "DeviceB"; |
|||
|
|||
private DeviceId gatewayId; |
|||
private String gatewayAccessToken; |
|||
|
|||
@SpyBean |
|||
private NotificationRuleProcessor notificationRuleProcessor; |
|||
|
|||
@Before |
|||
public void beforeTest() throws Exception { |
|||
loginSysAdmin(); |
|||
|
|||
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class); |
|||
Assert.assertNotNull(tenantProfile); |
|||
|
|||
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration(); |
|||
|
|||
profileConfiguration.setTransportGatewayMsgRateLimit(null); |
|||
profileConfiguration.setTransportGatewayTelemetryMsgRateLimit(null); |
|||
profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit(null); |
|||
|
|||
doPost("/api/tenantProfile", tenantProfile); |
|||
|
|||
loginTenantAdmin(); |
|||
createGateway(); |
|||
|
|||
Mockito.reset(notificationRuleProcessor); |
|||
} |
|||
|
|||
@Test |
|||
public void transportGatewayMsgRateLimitTest() throws Exception { |
|||
transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayMsgRateLimit("1:600")); |
|||
} |
|||
|
|||
@Test |
|||
public void transportGatewayTelemetryMsgRateLimitTest() throws Exception { |
|||
transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryMsgRateLimit("1:600")); |
|||
} |
|||
|
|||
@Test |
|||
public void transportGatewayTelemetryDataPointsRateLimitTest() throws Exception { |
|||
transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit("1:600")); |
|||
} |
|||
|
|||
private void transportGatewayRateLimitTest(Consumer<DefaultTenantProfileConfiguration> profileConfiguration) throws Exception { |
|||
loginSysAdmin(); |
|||
|
|||
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class); |
|||
Assert.assertNotNull(tenantProfile); |
|||
|
|||
profileConfiguration.accept((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration()); |
|||
|
|||
doPost("/api/tenantProfile", tenantProfile); |
|||
|
|||
MqttTestClient client = new MqttTestClient(); |
|||
client.connectAndWait(gatewayAccessToken); |
|||
client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_A)); |
|||
|
|||
loginTenantAdmin(); |
|||
|
|||
Device deviceA = getDeviceByName(DEVICE_A); |
|||
|
|||
var deviceATrigger = createRateLimitsTrigger(deviceA); |
|||
|
|||
Mockito.verify(notificationRuleProcessor, Mockito.never()).process(eq(deviceATrigger)); |
|||
|
|||
try { |
|||
client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_B)); |
|||
} catch (Exception t) { |
|||
} |
|||
|
|||
Device deviceB = getDeviceByName(DEVICE_B); |
|||
|
|||
var deviceBTrigger = createRateLimitsTrigger(deviceB); |
|||
|
|||
Mockito.verify(notificationRuleProcessor, Mockito.times(1)).process(deviceBTrigger); |
|||
|
|||
if (client.isConnected()) { |
|||
client.disconnect(); |
|||
} |
|||
} |
|||
|
|||
private void createGateway() throws Exception { |
|||
Device device = new Device(); |
|||
device.setName("gateway"); |
|||
ObjectNode additionalInfo = JacksonUtil.newObjectNode(); |
|||
additionalInfo.put("gateway", true); |
|||
device.setAdditionalInfo(additionalInfo); |
|||
device = doPost("/api/device", device, Device.class); |
|||
assertNotNull(device); |
|||
gatewayId = device.getId(); |
|||
assertNotNull(gatewayId); |
|||
|
|||
DeviceCredentials deviceCredentials = doGet("/api/device/" + gatewayId + "/credentials", DeviceCredentials.class); |
|||
assertNotNull(deviceCredentials); |
|||
assertEquals(gatewayId, deviceCredentials.getDeviceId()); |
|||
gatewayAccessToken = deviceCredentials.getCredentialsId(); |
|||
assertNotNull(gatewayAccessToken); |
|||
} |
|||
|
|||
private Device getDeviceByName(String deviceName) throws Exception { |
|||
Device device = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); |
|||
assertNotNull(device); |
|||
return device; |
|||
} |
|||
|
|||
private byte[] getGatewayPayload(String deviceName) { |
|||
return String.format("{\"%s\": [{\"values\": {\"temperature\": 42}}]}", deviceName).getBytes(); |
|||
} |
|||
|
|||
private RateLimitsTrigger createRateLimitsTrigger(Device device) { |
|||
return RateLimitsTrigger.builder() |
|||
.tenantId(tenantId) |
|||
.api(TRANSPORT_MESSAGES_PER_GATEWAY) |
|||
.limitLevel(device.getId()) |
|||
.limitLevelEntityName(device.getName()) |
|||
.build(); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.exception; |
|||
|
|||
import org.thingsboard.server.common.data.limit.LimitedApi; |
|||
|
|||
public class RateLimitExceededException extends AbstractRateLimitException { |
|||
|
|||
public RateLimitExceededException(String message) { |
|||
super(message); |
|||
} |
|||
|
|||
public RateLimitExceededException(LimitedApi api) { |
|||
super("Rate limit for " + api.getLabel() + " is exceeded"); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.id; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
@Schema |
|||
public class MobileAppSettingsId extends UUIDBased { |
|||
|
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
@JsonCreator |
|||
public MobileAppSettingsId(@JsonProperty("id") UUID id) { |
|||
super(id); |
|||
} |
|||
|
|||
public static MobileAppSettingsId fromString(String mobileAppSettingsId) { |
|||
return new MobileAppSettingsId(UUID.fromString(mobileAppSettingsId)); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.mobile; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.NoArgsConstructor; |
|||
import org.thingsboard.server.common.data.validation.NoXss; |
|||
|
|||
@Data |
|||
@Builder |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
@EqualsAndHashCode |
|||
public class AndroidConfig { |
|||
|
|||
private boolean enabled; |
|||
@NoXss |
|||
private String appPackage; |
|||
@NoXss |
|||
private String sha256CertFingerprints; |
|||
|
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.mobile; |
|||
|
|||
public enum BadgePosition { |
|||
|
|||
RIGHT, |
|||
LEFT; |
|||
|
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.mobile; |
|||
|
|||
|
|||
public enum BadgeStyle { |
|||
|
|||
ORIGINAL, |
|||
WHITE; |
|||
|
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.mobile; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.NoArgsConstructor; |
|||
import org.thingsboard.server.common.data.validation.NoXss; |
|||
|
|||
@Data |
|||
@Builder |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
@EqualsAndHashCode |
|||
public class IosConfig { |
|||
|
|||
private boolean enabled; |
|||
@NoXss |
|||
private String appId; |
|||
|
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.mobile; |
|||
|
|||
import jakarta.validation.Valid; |
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.BaseData; |
|||
import org.thingsboard.server.common.data.HasTenantId; |
|||
import org.thingsboard.server.common.data.id.MobileAppSettingsId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
|
|||
@Data |
|||
public class MobileAppSettings extends BaseData<MobileAppSettingsId> implements HasTenantId { |
|||
|
|||
private static final long serialVersionUID = 2628323657987010348L; |
|||
|
|||
private TenantId tenantId; |
|||
private boolean useDefaultApp; |
|||
@Valid |
|||
private AndroidConfig androidConfig; |
|||
@Valid |
|||
private IosConfig iosConfig; |
|||
@Valid |
|||
private QRCodeConfig qrCodeConfig; |
|||
|
|||
public MobileAppSettings() { |
|||
} |
|||
public MobileAppSettings(MobileAppSettingsId id) { |
|||
super(id); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.mobile; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.NoArgsConstructor; |
|||
import org.thingsboard.server.common.data.validation.NoXss; |
|||
|
|||
@Data |
|||
@Builder |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
@EqualsAndHashCode |
|||
public class QRCodeConfig { |
|||
|
|||
private boolean showOnHomePage; |
|||
private boolean badgeEnabled; |
|||
private boolean qrCodeLabelEnabled; |
|||
private BadgePosition badgePosition; |
|||
private BadgeStyle badgeStyle; |
|||
@NoXss |
|||
private String qrCodeLabel; |
|||
|
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.queue; |
|||
|
|||
public interface QueueConfig { |
|||
|
|||
boolean isConsumerPerPartition(); |
|||
|
|||
int getPollInterval(); |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue