602 changed files with 10838 additions and 2686 deletions
@ -0,0 +1,78 @@ |
|||
/** |
|||
* 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.config; |
|||
|
|||
import jakarta.servlet.FilterChain; |
|||
import jakarta.servlet.ServletException; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.AntPathMatcher; |
|||
import org.springframework.web.filter.OncePerRequestFilter; |
|||
import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; |
|||
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; |
|||
|
|||
import java.io.IOException; |
|||
import java.util.List; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class RequestSizeFilter extends OncePerRequestFilter { |
|||
|
|||
private final List<String> urls = List.of("/api/plugins/rpc/**", "/api/rpc/**"); |
|||
private final AntPathMatcher pathMatcher = new AntPathMatcher(); |
|||
private final ThingsboardErrorResponseHandler errorResponseHandler; |
|||
|
|||
@Value("${transport.http.max_payload_size:65536}") |
|||
private int maxPayloadSize; |
|||
|
|||
@Override |
|||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { |
|||
if (request.getContentLength() > maxPayloadSize) { |
|||
if (log.isDebugEnabled()) { |
|||
log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(), |
|||
request.getRemoteAddr(), request.getContentLength()); |
|||
} |
|||
errorResponseHandler.handle(new MaxPayloadSizeExceededException(), response); |
|||
return; |
|||
} |
|||
chain.doFilter(request, response); |
|||
} |
|||
|
|||
@Override |
|||
protected boolean shouldNotFilter(HttpServletRequest request) { |
|||
for (String url : urls) { |
|||
if (pathMatcher.match(url, request.getRequestURI())) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
protected boolean shouldNotFilterAsyncDispatch() { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
protected boolean shouldNotFilterErrorDispatch() { |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,237 @@ |
|||
/** |
|||
* 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.google.common.util.concurrent.FutureCallback; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import jakarta.annotation.Nullable; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.http.ResponseEntity; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestMethod; |
|||
import org.springframework.web.bind.annotation.ResponseBody; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.springframework.web.context.request.async.DeferredResult; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.audit.ActionType; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.EntityIdFactory; |
|||
import org.thingsboard.server.common.data.msg.TbMsgType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
import org.thingsboard.server.config.annotations.ApiOperation; |
|||
import org.thingsboard.server.exception.ToErrorResponseEntity; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.ruleengine.RuleEngineCallService; |
|||
import org.thingsboard.server.service.security.AccessValidator; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.permission.Operation; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.TimeoutException; |
|||
|
|||
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; |
|||
|
|||
@RestController |
|||
@TbCoreComponent |
|||
@RequestMapping(TbUrlConstants.RULE_ENGINE_URL_PREFIX) |
|||
@Slf4j |
|||
public class RuleEngineController extends BaseController { |
|||
public static final int DEFAULT_TIMEOUT = 10000; |
|||
private static final String MSG_DESCRIPTION_PREFIX = "Creates the Message with type 'REST_API_REQUEST' and payload taken from the request body. "; |
|||
private static final String MSG_DESCRIPTION = "This method allows you to extend the regular platform API with the power of Rule Engine. You may use default and custom rule nodes to handle the message. " + |
|||
"The generated message contains two important metadata fields:\n\n" + |
|||
" * **'serviceId'** to identify the platform server that received the request;\n" + |
|||
" * **'requestUUID'** to identify the request and route possible response from the Rule Engine;\n\n" + |
|||
"Use **'rest call reply'** rule node to push the reply from rule engine back as a REST API call response. "; |
|||
|
|||
@Autowired |
|||
private RuleEngineCallService ruleEngineCallService; |
|||
@Autowired |
|||
private AccessValidator accessValidator; |
|||
|
|||
@ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequest)", |
|||
notes = MSG_DESCRIPTION_PREFIX + |
|||
"Uses current User Id ( the one which credentials is used to perform the request) as the Rule Engine message originator. " + |
|||
MSG_DESCRIPTION + |
|||
"The default timeout of the request processing is 10 seconds." |
|||
+ "\n\n" + ControllerConstants.SECURITY_WRITE_CHECK) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@RequestMapping(value = "/", method = RequestMethod.POST) |
|||
@ResponseBody |
|||
public DeferredResult<ResponseEntity> handleRuleEngineRequest( |
|||
@Parameter(description = "A JSON value representing the message.", required = true) |
|||
@RequestBody String requestBody) throws ThingsboardException { |
|||
return handleRuleEngineRequest(null, null, null, DEFAULT_TIMEOUT, requestBody); |
|||
} |
|||
|
|||
@ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)", |
|||
notes = MSG_DESCRIPTION_PREFIX + |
|||
"Uses specified Entity Id as the Rule Engine message originator. " + |
|||
MSG_DESCRIPTION + |
|||
"The default timeout of the request processing is 10 seconds." |
|||
+ "\n\n" + ControllerConstants.SECURITY_WRITE_CHECK) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@RequestMapping(value = "/{entityType}/{entityId}", method = RequestMethod.POST) |
|||
@ResponseBody |
|||
public DeferredResult<ResponseEntity> handleRuleEngineRequest( |
|||
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable("entityType") String entityType, |
|||
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable("entityId") String entityIdStr, |
|||
@Parameter(description = "A JSON value representing the message.", required = true) |
|||
@RequestBody String requestBody) throws ThingsboardException { |
|||
return handleRuleEngineRequest(entityType, entityIdStr, null, DEFAULT_TIMEOUT, requestBody); |
|||
} |
|||
|
|||
@ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequest)", |
|||
notes = MSG_DESCRIPTION_PREFIX + |
|||
"Uses specified Entity Id as the Rule Engine message originator. " + |
|||
MSG_DESCRIPTION + |
|||
"The platform expects the timeout value in milliseconds." |
|||
+ "\n\n" + ControllerConstants.SECURITY_WRITE_CHECK) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@RequestMapping(value = "/{entityType}/{entityId}/{timeout}", method = RequestMethod.POST) |
|||
@ResponseBody |
|||
public DeferredResult<ResponseEntity> handleRuleEngineRequest( |
|||
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable("entityType") String entityType, |
|||
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable("entityId") String entityIdStr, |
|||
@Parameter(description = "Timeout to process the request in milliseconds", required = true) |
|||
@PathVariable("timeout") int timeout, |
|||
@Parameter(description = "A JSON value representing the message.", required = true) |
|||
@RequestBody String requestBody) throws ThingsboardException { |
|||
return handleRuleEngineRequest(entityType, entityIdStr, null, timeout, requestBody); |
|||
} |
|||
|
|||
@ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequest)", |
|||
notes = MSG_DESCRIPTION_PREFIX + |
|||
"Uses specified Entity Id as the Rule Engine message originator. " + |
|||
MSG_DESCRIPTION + |
|||
"If request sent for Device/Device Profile or Asset/Asset Profile entity, specified queue will be used instead of the queue selected in the device or asset profile. " + |
|||
"The platform expects the timeout value in milliseconds." |
|||
+ "\n\n" + ControllerConstants.SECURITY_WRITE_CHECK) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@RequestMapping(value = "/{entityType}/{entityId}/{queueName}/{timeout}", method = RequestMethod.POST) |
|||
@ResponseBody |
|||
public DeferredResult<ResponseEntity> handleRuleEngineRequest( |
|||
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable("entityType") String entityType, |
|||
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable("entityId") String entityIdStr, |
|||
@Parameter(description = "Queue name to process the request in the rule engine", required = true) |
|||
@PathVariable("queueName") String queueName, |
|||
@Parameter(description = "Timeout to process the request in milliseconds", required = true) |
|||
@PathVariable("timeout") int timeout, |
|||
@Parameter(description = "A JSON value representing the message.", required = true) |
|||
@RequestBody String requestBody) throws ThingsboardException { |
|||
try { |
|||
SecurityUser currentUser = getCurrentUser(); |
|||
EntityId entityId; |
|||
if (StringUtils.isEmpty(entityType) || StringUtils.isEmpty(entityIdStr)) { |
|||
entityId = currentUser.getId(); |
|||
} else { |
|||
entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); |
|||
} |
|||
//Check that this is a valid JSON
|
|||
JacksonUtil.toJsonNode(requestBody); |
|||
final DeferredResult<ResponseEntity> response = new DeferredResult<>(); |
|||
accessValidator.validate(currentUser, Operation.WRITE, entityId, new HttpValidationCallback(response, new FutureCallback<DeferredResult<ResponseEntity>>() { |
|||
@Override |
|||
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) { |
|||
long expTime = System.currentTimeMillis() + timeout; |
|||
HashMap<String, String> metaData = new HashMap<>(); |
|||
UUID requestId = UUID.randomUUID(); |
|||
metaData.put("serviceId", serviceInfoProvider.getServiceId()); |
|||
metaData.put("requestUUID", requestId.toString()); |
|||
metaData.put("expirationTime", Long.toString(expTime)); |
|||
TbMsg msg = TbMsg.newMsg(queueName, TbMsgType.REST_API_REQUEST, entityId, currentUser.getCustomerId(), new TbMsgMetaData(metaData), requestBody); |
|||
ruleEngineCallService.processRestApiCallToRuleEngine(currentUser.getTenantId(), requestId, msg, queueName != null, |
|||
reply -> reply(new LocalRequestMetaData(msg, currentUser, result), reply)); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable e) { |
|||
ResponseEntity entity; |
|||
if (e instanceof ToErrorResponseEntity) { |
|||
entity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); |
|||
} else { |
|||
entity = new ResponseEntity(HttpStatus.UNAUTHORIZED); |
|||
} |
|||
logRuleEngineCall(currentUser, entityId, requestBody, null, e); |
|||
response.setResult(entity); |
|||
} |
|||
})); |
|||
return response; |
|||
} catch (IllegalArgumentException iae) { |
|||
throw new ThingsboardException("Invalid request body", iae, ThingsboardErrorCode.BAD_REQUEST_PARAMS); |
|||
} |
|||
} |
|||
|
|||
private void reply(LocalRequestMetaData rpcRequest, TbMsg response) { |
|||
DeferredResult<ResponseEntity> responseWriter = rpcRequest.responseWriter(); |
|||
if (response == null) { |
|||
logRuleEngineCall(rpcRequest, null, new TimeoutException("Processing timeout detected!")); |
|||
responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT)); |
|||
} else { |
|||
String responseData = response.getData(); |
|||
if (!StringUtils.isEmpty(responseData)) { |
|||
try { |
|||
logRuleEngineCall(rpcRequest, response, null); |
|||
responseWriter.setResult(new ResponseEntity<>(JacksonUtil.toJsonNode(responseData), HttpStatus.OK)); |
|||
} catch (IllegalArgumentException e) { |
|||
log.debug("Failed to decode device response: {}", responseData, e); |
|||
logRuleEngineCall(rpcRequest, response, e); |
|||
responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE)); |
|||
} |
|||
} else { |
|||
logRuleEngineCall(rpcRequest, response, null); |
|||
responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void logRuleEngineCall(LocalRequestMetaData rpcRequest, TbMsg response, Throwable e) { |
|||
logRuleEngineCall(rpcRequest.user(), rpcRequest.request().getOriginator(), rpcRequest.request().getData(), response, e); |
|||
} |
|||
|
|||
private void logRuleEngineCall(SecurityUser user, EntityId entityId, String request, TbMsg response, Throwable e) { |
|||
auditLogService.logEntityAction( |
|||
user.getTenantId(), |
|||
user.getCustomerId(), |
|||
user.getId(), |
|||
user.getName(), |
|||
entityId, |
|||
null, |
|||
ActionType.REST_API_RULE_ENGINE_CALL, |
|||
BaseController.toException(e), |
|||
request, |
|||
response != null ? response.getData() : ""); |
|||
} |
|||
|
|||
private record LocalRequestMetaData(TbMsg request, SecurityUser user, DeferredResult<ResponseEntity> responseWriter) {} |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
/** |
|||
* 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.ruleengine; |
|||
|
|||
import jakarta.annotation.PostConstruct; |
|||
import jakarta.annotation.PreDestroy; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|||
import org.thingsboard.server.cluster.TbClusterService; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.queue.TbCallback; |
|||
import org.thingsboard.server.common.msg.queue.TbMsgCallback; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
|
|||
import java.util.UUID; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.concurrent.ScheduledExecutorService; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.function.Consumer; |
|||
|
|||
@Service |
|||
@Slf4j |
|||
public class DefaultRuleEngineCallService implements RuleEngineCallService { |
|||
|
|||
private final TbClusterService clusterService; |
|||
|
|||
private ScheduledExecutorService executor; |
|||
|
|||
private final ConcurrentMap<UUID, Consumer<TbMsg>> requests = new ConcurrentHashMap<>(); |
|||
|
|||
public DefaultRuleEngineCallService(TbClusterService clusterService) { |
|||
this.clusterService = clusterService; |
|||
} |
|||
|
|||
@PostConstruct |
|||
public void initExecutor() { |
|||
executor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("re-rest-callback")); |
|||
} |
|||
|
|||
@PreDestroy |
|||
public void shutdownExecutor() { |
|||
if (executor != null) { |
|||
executor.shutdownNow(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void processRestApiCallToRuleEngine(TenantId tenantId, UUID requestId, TbMsg request, boolean useQueueFromTbMsg, Consumer<TbMsg> responseConsumer) { |
|||
log.trace("[{}] Processing REST API call to rule engine: [{}] for entity: [{}]", tenantId, requestId, request.getOriginator()); |
|||
requests.put(requestId, responseConsumer); |
|||
sendRequestToRuleEngine(tenantId, request, useQueueFromTbMsg); |
|||
scheduleTimeout(request, requestId, requests); |
|||
} |
|||
|
|||
@Override |
|||
public void onQueueMsg(TransportProtos.RestApiCallResponseMsgProto restApiCallResponseMsg, TbCallback callback) { |
|||
UUID requestId = new UUID(restApiCallResponseMsg.getRequestIdMSB(), restApiCallResponseMsg.getRequestIdLSB()); |
|||
Consumer<TbMsg> consumer = requests.remove(requestId); |
|||
if (consumer != null) { |
|||
consumer.accept(TbMsg.fromBytes(null, restApiCallResponseMsg.getResponse().toByteArray(), TbMsgCallback.EMPTY)); |
|||
} else { |
|||
log.trace("[{}] Unknown or stale rest api call response received", requestId); |
|||
} |
|||
callback.onSuccess(); |
|||
} |
|||
|
|||
private void sendRequestToRuleEngine(TenantId tenantId, TbMsg msg, boolean useQueueFromTbMsg) { |
|||
clusterService.pushMsgToRuleEngine(tenantId, msg.getOriginator(), msg, useQueueFromTbMsg, null); |
|||
} |
|||
|
|||
private void scheduleTimeout(TbMsg request, UUID requestId, ConcurrentMap<UUID, Consumer<TbMsg>> requestsMap) { |
|||
long expirationTime = Long.parseLong(request.getMetaData().getValue("expirationTime")); |
|||
long timeout = Math.max(0, expirationTime - System.currentTimeMillis()); |
|||
log.trace("[{}] processing the request: [{}]", this.hashCode(), requestId); |
|||
executor.schedule(() -> { |
|||
Consumer<TbMsg> consumer = requestsMap.remove(requestId); |
|||
if (consumer != null) { |
|||
log.trace("[{}] request timeout detected: [{}]", this.hashCode(), requestId); |
|||
consumer.accept(null); |
|||
} |
|||
}, timeout, TimeUnit.MILLISECONDS); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* 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.ruleengine; |
|||
|
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.queue.TbCallback; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
|
|||
import java.util.UUID; |
|||
import java.util.function.Consumer; |
|||
|
|||
public interface RuleEngineCallService { |
|||
|
|||
void processRestApiCallToRuleEngine(TenantId tenantId, UUID requestId, TbMsg request, boolean useQueueFromTbMsg, Consumer<TbMsg> responseConsumer); |
|||
|
|||
void onQueueMsg(TransportProtos.RestApiCallResponseMsgProto restApiCallResponseMsg, TbCallback callback); |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue