29 changed files with 731 additions and 182 deletions
@ -0,0 +1,170 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script; |
|||
|
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.MoreExecutors; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.script.api.RuleNodeScriptFactory; |
|||
import org.thingsboard.script.api.mvel.MvelInvokeService; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
|
|||
import javax.script.ScriptException; |
|||
import java.util.ArrayList; |
|||
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.stream.Collectors; |
|||
|
|||
|
|||
@Slf4j |
|||
public class RuleNodeMvelScriptEngine extends RuleNodeScriptEngine<MvelInvokeService, Object> { |
|||
|
|||
public RuleNodeMvelScriptEngine(TenantId tenantId, MvelInvokeService scriptInvokeService, String script, String... argNames) { |
|||
super(tenantId, scriptInvokeService, script, argNames); |
|||
} |
|||
|
|||
@Override |
|||
protected ListenableFuture<Boolean> executeFilterTransform(Object result) { |
|||
if (result instanceof Boolean) { |
|||
return Futures.immediateFuture((Boolean) result); |
|||
} |
|||
return wrongResultType(result); |
|||
} |
|||
|
|||
@Override |
|||
protected ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, Object result) { |
|||
if (result instanceof Map) { |
|||
return Futures.immediateFuture(Collections.singletonList(unbindMsg((Map) result, msg))); |
|||
} else if (result instanceof Collection) { |
|||
List<TbMsg> res = new ArrayList<>(); |
|||
for (Object resObject : (Collection) result) { |
|||
if (resObject instanceof Map) { |
|||
res.add(unbindMsg((Map) result, msg)); |
|||
} else { |
|||
return wrongResultType(resObject); |
|||
} |
|||
} |
|||
return Futures.immediateFuture(res); |
|||
} |
|||
return wrongResultType(result); |
|||
} |
|||
|
|||
@Override |
|||
protected ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, Object result) { |
|||
if (result instanceof Map) { |
|||
return Futures.immediateFuture(unbindMsg((Map) result, prevMsg)); |
|||
} |
|||
return wrongResultType(result); |
|||
} |
|||
|
|||
@Override |
|||
protected ListenableFuture<String> executeToStringTransform(Object result) { |
|||
if (result instanceof String) { |
|||
return Futures.immediateFuture((String) result); |
|||
} else { |
|||
return Futures.immediateFuture(JacksonUtil.toString(result)); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected ListenableFuture<Set<String>> executeSwitchTransform(Object result) { |
|||
if (result instanceof String) { |
|||
return Futures.immediateFuture(Collections.singleton((String) result)); |
|||
} else if (result instanceof Collection) { |
|||
Set<String> res = new HashSet<>(); |
|||
for (Object resObject : (Collection) result) { |
|||
if (resObject instanceof String) { |
|||
res.add((String) resObject); |
|||
} else { |
|||
return wrongResultType(resObject); |
|||
} |
|||
} |
|||
return Futures.immediateFuture(res); |
|||
} |
|||
return wrongResultType(result); |
|||
} |
|||
|
|||
@Override |
|||
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) { |
|||
return Futures.transform(executeScriptAsync(msg), JacksonUtil::valueToTree, MoreExecutors.directExecutor()); |
|||
|
|||
} |
|||
|
|||
@Override |
|||
protected Object convertResult(Object result) { |
|||
return result; |
|||
} |
|||
|
|||
@Override |
|||
protected Object[] prepareArgs(TbMsg msg) { |
|||
Object[] args = new Object[3]; |
|||
if (msg.getData() != null) { |
|||
args[0] = JacksonUtil.fromString(msg.getData(), Map.class); |
|||
} else { |
|||
args[0] = new HashMap<>(); |
|||
} |
|||
args[1] = msg.getMetaData().getData(); |
|||
args[2] = msg.getType(); |
|||
return args; |
|||
} |
|||
|
|||
private static TbMsg unbindMsg(Map msgData, TbMsg msg) { |
|||
String data = null; |
|||
Map<String, String> metadata = null; |
|||
String messageType = null; |
|||
if (msgData.containsKey(RuleNodeScriptFactory.MSG)) { |
|||
data = JacksonUtil.toString(msgData.get(RuleNodeScriptFactory.MSG)); |
|||
} |
|||
if (msgData.containsKey(RuleNodeScriptFactory.METADATA)) { |
|||
Object msgMetadataObj = msgData.get(RuleNodeScriptFactory.METADATA); |
|||
if (msgMetadataObj instanceof Map) { |
|||
metadata = ((Map<?, ?>) msgMetadataObj).entrySet().stream().collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString())); |
|||
} else { |
|||
metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() { |
|||
}); |
|||
} |
|||
} |
|||
if (msgData.containsKey(RuleNodeScriptFactory.MSG_TYPE)) { |
|||
messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).toString(); |
|||
} |
|||
String newData = data != null ? data : msg.getData(); |
|||
TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy(); |
|||
String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType(); |
|||
return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData); |
|||
} |
|||
|
|||
private static <T> ListenableFuture<T> wrongResultType(Object result) { |
|||
String className = toClassName(result); |
|||
log.warn("Wrong result type: {}", className); |
|||
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + className)); |
|||
} |
|||
|
|||
private static String toClassName(Object result) { |
|||
return result != null ? result.getClass().getSimpleName() : "null"; |
|||
} |
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.MoreExecutors; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.rule.engine.api.ScriptEngine; |
|||
import org.thingsboard.script.api.ScriptInvokeService; |
|||
import org.thingsboard.script.api.ScriptType; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
|
|||
import javax.script.ScriptException; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ExecutionException; |
|||
|
|||
|
|||
@Slf4j |
|||
public abstract class RuleNodeScriptEngine<T extends ScriptInvokeService, R> implements ScriptEngine { |
|||
|
|||
private final T scriptInvokeService; |
|||
|
|||
private final UUID scriptId; |
|||
private final TenantId tenantId; |
|||
|
|||
public RuleNodeScriptEngine(TenantId tenantId, T scriptInvokeService, String script, String... argNames) { |
|||
this.tenantId = tenantId; |
|||
this.scriptInvokeService = scriptInvokeService; |
|||
try { |
|||
this.scriptId = this.scriptInvokeService.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, script, argNames).get(); |
|||
} catch (Exception e) { |
|||
Throwable t = e; |
|||
if (e instanceof ExecutionException) { |
|||
t = e.getCause(); |
|||
} |
|||
throw new IllegalArgumentException("Can't compile script: " + t.getMessage(), t); |
|||
} |
|||
} |
|||
|
|||
protected abstract Object[] prepareArgs(TbMsg msg); |
|||
|
|||
@Override |
|||
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) { |
|||
ListenableFuture<R> result = executeScriptAsync(msg); |
|||
return Futures.transformAsync(result, |
|||
json -> executeUpdateTransform(msg, json), |
|||
MoreExecutors.directExecutor()); |
|||
} |
|||
|
|||
protected abstract ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, R result); |
|||
|
|||
@Override |
|||
public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) { |
|||
return Futures.transformAsync(executeScriptAsync(prevMsg), |
|||
result -> executeGenerateTransform(prevMsg, result), |
|||
MoreExecutors.directExecutor()); |
|||
} |
|||
|
|||
protected abstract ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, R result); |
|||
|
|||
@Override |
|||
public ListenableFuture<String> executeToStringAsync(TbMsg msg) { |
|||
return Futures.transformAsync(executeScriptAsync(msg), this::executeToStringTransform, MoreExecutors.directExecutor()); |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) { |
|||
return Futures.transformAsync(executeScriptAsync(msg), |
|||
this::executeFilterTransform, |
|||
MoreExecutors.directExecutor()); |
|||
} |
|||
|
|||
protected abstract ListenableFuture<String> executeToStringTransform(R result); |
|||
|
|||
protected abstract ListenableFuture<Boolean> executeFilterTransform(R result); |
|||
|
|||
protected abstract ListenableFuture<Set<String>> executeSwitchTransform(R result); |
|||
|
|||
@Override |
|||
public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) { |
|||
return Futures.transformAsync(executeScriptAsync(msg), |
|||
this::executeSwitchTransform, |
|||
MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
|
|||
} |
|||
|
|||
ListenableFuture<R> executeScriptAsync(TbMsg msg) { |
|||
log.trace("execute script async, msg {}", msg); |
|||
Object[] inArgs = prepareArgs(msg); |
|||
return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]); |
|||
} |
|||
|
|||
ListenableFuture<R> executeScriptAsync(CustomerId customerId, Object... args) { |
|||
return Futures.transformAsync(scriptInvokeService.invokeScript(tenantId, customerId, this.scriptId, args), |
|||
o -> { |
|||
try { |
|||
return Futures.immediateFuture(convertResult(o)); |
|||
} catch (Exception e) { |
|||
if (e.getCause() instanceof ScriptException) { |
|||
return Futures.immediateFailedFuture(e.getCause()); |
|||
} else if (e.getCause() instanceof RuntimeException) { |
|||
return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage())); |
|||
} else { |
|||
return Futures.immediateFailedFuture(new ScriptException(e)); |
|||
} |
|||
} |
|||
}, MoreExecutors.directExecutor()); |
|||
} |
|||
|
|||
public void destroy() { |
|||
scriptInvokeService.release(this.scriptId); |
|||
} |
|||
|
|||
protected abstract R convertResult(Object result); |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script; |
|||
|
|||
public enum ScriptLanguage { |
|||
JS, MVEL |
|||
} |
|||
@ -1,6 +1,21 @@ |
|||
package org.thingsboard.script.api.mvel; |
|||
/** |
|||
* Copyright © 2016-2022 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.js; |
|||
|
|||
import org.thingsboard.script.api.ScriptInvokeService; |
|||
|
|||
public interface MvelInvokeService extends ScriptInvokeService { |
|||
public interface JsInvokeService extends ScriptInvokeService { |
|||
} |
|||
|
|||
@ -1,2 +1,21 @@ |
|||
package org.thingsboard.script.api.mvel;public interface MvelInvokeService { |
|||
/** |
|||
* Copyright © 2016-2022 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.mvel; |
|||
|
|||
import org.thingsboard.script.api.ScriptInvokeService; |
|||
|
|||
public interface MvelInvokeService extends ScriptInvokeService { |
|||
} |
|||
|
|||
@ -0,0 +1,65 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.mvel; |
|||
|
|||
import org.mvel2.compiler.Accessor; |
|||
import org.mvel2.compiler.ExecutableAccessor; |
|||
import org.mvel2.compiler.ExecutableStatement; |
|||
import org.mvel2.integration.VariableResolverFactory; |
|||
import org.mvel2.optimizers.impl.refl.collection.ExprValueAccessor; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
public class TbMapCreator implements Accessor { |
|||
private Accessor[] keys; |
|||
private Accessor[] vals; |
|||
private int size; |
|||
|
|||
public Object getValue(Object ctx, Object elCtx, VariableResolverFactory variableFactory) { |
|||
Map map = new HashMap<>(size * 2); |
|||
for (int i = size - 1; i != -1; i--) { |
|||
//noinspection unchecked
|
|||
map.put(getKey(i, ctx, elCtx, variableFactory), vals[i].getValue(ctx, elCtx, variableFactory)); |
|||
} |
|||
return map; |
|||
} |
|||
|
|||
private Object getKey(int index, Object ctx, Object elCtx, VariableResolverFactory variableFactory) { |
|||
Accessor keyAccessor = keys[index]; |
|||
if (keyAccessor instanceof ExprValueAccessor) { |
|||
ExecutableStatement executableStatement = ((ExprValueAccessor) keyAccessor).stmt; |
|||
if (executableStatement instanceof ExecutableAccessor) { |
|||
return ((ExecutableAccessor) executableStatement).getNode().getName(); |
|||
} |
|||
} |
|||
return keys[index].getValue(ctx, elCtx, variableFactory); |
|||
} |
|||
|
|||
public TbMapCreator(Accessor[] keys, Accessor[] vals) { |
|||
this.size = (this.keys = keys).length; |
|||
this.vals = vals; |
|||
} |
|||
|
|||
public Object setValue(Object ctx, Object elCtx, VariableResolverFactory variableFactory, Object value) { |
|||
// not implemented
|
|||
return null; |
|||
} |
|||
|
|||
public Class getKnownEgressType() { |
|||
return Map.class; |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.mvel; |
|||
|
|||
import org.mvel2.ParserContext; |
|||
import org.mvel2.compiler.Accessor; |
|||
import org.mvel2.integration.VariableResolverFactory; |
|||
import org.mvel2.optimizers.impl.refl.ReflectiveAccessorOptimizer; |
|||
import org.mvel2.optimizers.impl.refl.collection.ArrayCreator; |
|||
import org.mvel2.optimizers.impl.refl.collection.ExprValueAccessor; |
|||
import org.mvel2.optimizers.impl.refl.collection.ListCreator; |
|||
import org.mvel2.optimizers.impl.refl.nodes.Union; |
|||
|
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
import static org.mvel2.util.CompilerTools.expectType; |
|||
import static org.mvel2.util.ParseTools.findClass; |
|||
import static org.mvel2.util.ParseTools.getBaseComponentType; |
|||
import static org.mvel2.util.ParseTools.getSubComponentType; |
|||
import static org.mvel2.util.ParseTools.repeatChar; |
|||
|
|||
public class TbReflectiveAccessorOptimizer extends ReflectiveAccessorOptimizer { |
|||
private Object ctx; |
|||
private Class returnType; |
|||
private VariableResolverFactory variableFactory; |
|||
|
|||
@Override |
|||
public Accessor optimizeCollection(ParserContext pCtx, Object o, Class type, char[] property, int start, int offset, |
|||
Object ctx, Object thisRef, VariableResolverFactory factory) { |
|||
this.start = this.cursor = start; |
|||
this.length = start + offset; |
|||
this.returnType = type; |
|||
this.ctx = ctx; |
|||
this.variableFactory = factory; |
|||
this.pCtx = pCtx; |
|||
|
|||
Accessor root = _getAccessor(o, returnType); |
|||
|
|||
if (property != null && length > start) { |
|||
return new Union(pCtx, root, property, cursor, offset); |
|||
} else { |
|||
return root; |
|||
} |
|||
} |
|||
|
|||
private Accessor _getAccessor(Object o, Class type) { |
|||
if (o instanceof List) { |
|||
Accessor[] a = new Accessor[((List) o).size()]; |
|||
int i = 0; |
|||
|
|||
for (Object item : (List) o) { |
|||
a[i++] = _getAccessor(item, type); |
|||
} |
|||
|
|||
returnType = List.class; |
|||
|
|||
return new ListCreator(a); |
|||
} else if (o instanceof Map) { |
|||
Accessor[] k = new Accessor[((Map) o).size()]; |
|||
Accessor[] v = new Accessor[k.length]; |
|||
int i = 0; |
|||
|
|||
for (Object item : ((Map) o).keySet()) { |
|||
k[i] = _getAccessor(item, type); // key
|
|||
v[i++] = _getAccessor(((Map) o).get(item), type); // value
|
|||
} |
|||
|
|||
returnType = Map.class; |
|||
|
|||
return new TbMapCreator(k, v); |
|||
} else if (o instanceof Object[]) { |
|||
Accessor[] a = new Accessor[((Object[]) o).length]; |
|||
int i = 0; |
|||
int dim = 0; |
|||
|
|||
if (type != null) { |
|||
String nm = type.getName(); |
|||
while (nm.charAt(dim) == '[') dim++; |
|||
} else { |
|||
type = Object[].class; |
|||
dim = 1; |
|||
} |
|||
|
|||
try { |
|||
Class base = getBaseComponentType(type); |
|||
Class cls = dim > 1 ? findClass(null, repeatChar('[', dim - 1) + "L" + base.getName() + ";", pCtx) |
|||
: type; |
|||
|
|||
for (Object item : (Object[]) o) { |
|||
expectType(pCtx, a[i++] = _getAccessor(item, cls), base, true); |
|||
} |
|||
|
|||
return new ArrayCreator(a, getSubComponentType(type)); |
|||
} catch (ClassNotFoundException e) { |
|||
throw new RuntimeException("this error should never throw:" + getBaseComponentType(type).getName(), e); |
|||
} |
|||
} else { |
|||
if (returnType == null) returnType = Object.class; |
|||
if (type.isArray()) { |
|||
return new ExprValueAccessor((String) o, type, ctx, variableFactory, pCtx); |
|||
} else { |
|||
return new ExprValueAccessor((String) o, Object.class, ctx, variableFactory, pCtx); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue