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.thingsboard.common.util.JacksonUtil;
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.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
@ -216,11 +218,14 @@ public class CalculatedFieldController extends BaseController {
@RequestBody JsonNode inputParams) {
String expression = inputParams.get("expression").asText();
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()
);
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 errorText = "";
@ -234,12 +239,20 @@ public class CalculatedFieldController extends BaseController {
getTenantId(),
tbelInvokeService,
expression,
argNames.toArray(String[]::new)
ctxAndArgNames.toArray(String[]::new)
);
Object[] args = argNames.stream()
.map(arguments::get)
.toArray();
Object[] args = new Object[ctxAndArgNames.size()];
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);
output = JacksonUtil.toString(json);
@ -260,7 +273,8 @@ public class CalculatedFieldController extends BaseController {
EntityType entityType = referencedEntityId.getEntityType();
switch (entityType) {
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!");
}
List<String> ctxAndArgNames = new ArrayList<>(argNames.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(argNames);
return new CalculatedFieldTbelScriptEngine(
tenantId,
tbelInvokeService,
expression,
argNames.toArray(String[]::new)
ctxAndArgNames.toArray(String[]::new)
);
}

View File

@ -23,11 +23,17 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.configuration.Output;
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.Map;
@Data
@Slf4j
@ -49,11 +55,20 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
Object[] args = ctx.getArgNames().stream()
.map(this::toTbelArgument)
.toArray();
ListenableFuture<JsonNode> resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args);
Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
List<Object> args = new ArrayList<>(ctx.getArgNames().size() + 1);
args.add(new Object()); // first element is a ctx, but we will set it later;
for (String argName : ctx.getArgNames()) {
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();
return Futures.transform(resultFuture,
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.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity.value}");
config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity}");
Output output = new Output();
output.setType(OutputType.ATTRIBUTES);

View File

@ -140,7 +140,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxCalculatedFieldsPerEntity = 5;
@Schema(example = "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")
private long maxDataPointsPerRollingArg = 1000;
@Schema(example = "32")

View File

@ -139,6 +139,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize);
parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize);
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize);
parserConfig.registerDataType("TbelCfCtx", TbelCfCtx.class, TbelCfCtx::memorySize);
TbUtils.register(parserConfig);
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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.mvel2.ConversionHandler;
import org.mvel2.DataConversion;
import org.mvel2.ExecutionContext;
import org.mvel2.ParserConfiguration;
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 {
long OBJ_SIZE = 32L; // Approximate calculation;
long memorySize();
}

View File

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