Adding ctx as first argument in CF

This commit is contained in:
Andrii Shvaika 2025-03-07 11:44:49 +02:00
parent e2e49009a0
commit ee3d405ed8
10 changed files with 88 additions and 17 deletions

View File

@ -34,6 +34,8 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
@ -216,11 +218,14 @@ public class CalculatedFieldController extends BaseController {
@RequestBody JsonNode inputParams) { @RequestBody JsonNode inputParams) {
String expression = inputParams.get("expression").asText(); String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse( Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<Map<String, TbelCfArg>>() { JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {
}), }),
Collections.emptyMap() Collections.emptyMap()
); );
ArrayList<String> argNames = new ArrayList<>(arguments.keySet());
ArrayList<String> ctxAndArgNames = new ArrayList<>(arguments.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(arguments.keySet());
String output = ""; String output = "";
String errorText = ""; String errorText = "";
@ -234,12 +239,20 @@ public class CalculatedFieldController extends BaseController {
getTenantId(), getTenantId(),
tbelInvokeService, tbelInvokeService,
expression, expression,
argNames.toArray(String[]::new) ctxAndArgNames.toArray(String[]::new)
); );
Object[] args = argNames.stream()
.map(arguments::get) Object[] args = new Object[ctxAndArgNames.size()];
.toArray(); args[0] = new TbelCfCtx(arguments);
for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) {
args[i] = svArg.getValue();
} else {
args[i] = arg;
}
}
JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json); output = JacksonUtil.toString(json);
@ -260,7 +273,8 @@ public class CalculatedFieldController extends BaseController {
EntityType entityType = referencedEntityId.getEntityType(); EntityType entityType = referencedEntityId.getEntityType();
switch (entityType) { switch (entityType) {
case TENANT, CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); case TENANT, CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); default ->
throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
} }
} }

View File

@ -136,11 +136,14 @@ public class CalculatedFieldCtx {
throw new IllegalArgumentException("TBEL script engine is disabled!"); throw new IllegalArgumentException("TBEL script engine is disabled!");
} }
List<String> ctxAndArgNames = new ArrayList<>(argNames.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(argNames);
return new CalculatedFieldTbelScriptEngine( return new CalculatedFieldTbelScriptEngine(
tenantId, tenantId,
tbelInvokeService, tbelInvokeService,
expression, expression,
argNames.toArray(String[]::new) ctxAndArgNames.toArray(String[]::new)
); );
} }

View File

@ -23,11 +23,17 @@ import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
@Data @Data
@Slf4j @Slf4j
@ -49,11 +55,20 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
@Override @Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) { public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
Object[] args = ctx.getArgNames().stream() Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
.map(this::toTbelArgument) List<Object> args = new ArrayList<>(ctx.getArgNames().size() + 1);
.toArray(); args.add(new Object()); // first element is a ctx, but we will set it later;
for (String argName : ctx.getArgNames()) {
ListenableFuture<JsonNode> resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args); var arg = toTbelArgument(argName);
arguments.put(argName, arg);
if (arg instanceof TbelCfSingleValueArg svArg) {
args.add(svArg.getValue());
} else {
args.add(arg);
}
}
args.set(0, new TbelCfCtx(arguments));
ListenableFuture<JsonNode> resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray());
Output output = ctx.getOutput(); Output output = ctx.getOutput();
return Futures.transform(resultFuture, return Futures.transform(resultFuture,
result -> new CalculatedFieldResult(output.getType(), output.getScope(), result), result -> new CalculatedFieldResult(output.getType(), output.getScope(), result),

View File

@ -191,7 +191,7 @@ public class ScriptCalculatedFieldStateTest {
config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2)); config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2));
config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity.value}"); config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity}");
Output output = new Output(); Output output = new Output();
output.setType(OutputType.ATTRIBUTES); output.setType(OutputType.ATTRIBUTES);

View File

@ -140,7 +140,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxCalculatedFieldsPerEntity = 5; private long maxCalculatedFieldsPerEntity = 5;
@Schema(example = "10") @Schema(example = "10")
private long maxArgumentsPerCF = 10; private long maxArgumentsPerCF = 10;
@Min(value = 0, message = "must be at least 0") @Min(value = 1, message = "must be at least 1")
@Schema(example = "1000") @Schema(example = "1000")
private long maxDataPointsPerRollingArg = 1000; private long maxDataPointsPerRollingArg = 1000;
@Schema(example = "32") @Schema(example = "32")

View File

@ -139,6 +139,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize); parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize);
parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize); parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize);
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize); parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize);
parserConfig.registerDataType("TbelCfCtx", TbelCfCtx.class, TbelCfCtx::memorySize);
TbUtils.register(parserConfig); TbUtils.register(parserConfig);
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));

View File

@ -18,6 +18,8 @@ package org.thingsboard.script.api.tbel;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.mvel2.ConversionHandler;
import org.mvel2.DataConversion;
import org.mvel2.ExecutionContext; import org.mvel2.ExecutionContext;
import org.mvel2.ParserConfiguration; import org.mvel2.ParserConfiguration;
import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionArrayList;

View File

@ -0,0 +1,36 @@
/**
* 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.script.api.tbel;
import lombok.Getter;
import java.util.Collections;
import java.util.Map;
public class TbelCfCtx implements TbelCfObject {
@Getter
private final Map<String, TbelCfArg> args;
public TbelCfCtx(Map<String, TbelCfArg> args) {
this.args = Collections.unmodifiableMap(args);
}
@Override
public long memorySize() {
return OBJ_SIZE;
}
}

View File

@ -17,6 +17,8 @@ package org.thingsboard.script.api.tbel;
public interface TbelCfObject { public interface TbelCfObject {
long OBJ_SIZE = 32L; // Approximate calculation;
long memorySize(); long memorySize();
} }

View File

@ -22,8 +22,6 @@ import lombok.Data;
@Data @Data
public class TbelCfSingleValueArg implements TbelCfArg { public class TbelCfSingleValueArg implements TbelCfArg {
public static final long OBJ_SIZE = 32L; // Approximate calculation;
private final long ts; private final long ts;
private final Object value; private final Object value;