blob: 1be38c36734a951b096e929cc955ac6e04e190d8 [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.metron.stellar.common.shell;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.SortedMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.collections4.trie.PatriciaTrie;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.metron.stellar.common.StellarProcessor;
import org.apache.metron.stellar.common.configuration.ConfigurationsUtils;
import org.apache.metron.stellar.common.utils.JSONUtils;
import org.apache.metron.stellar.dsl.Context;
import org.apache.metron.stellar.dsl.MapVariableResolver;
import org.apache.metron.stellar.dsl.StellarFunctionInfo;
import org.apache.metron.stellar.dsl.StellarFunctions;
import org.apache.metron.stellar.dsl.VariableResolver;
import org.apache.metron.stellar.dsl.functions.resolver.FunctionResolver;
import org.jboss.aesh.console.Console;
import static org.apache.metron.stellar.common.configuration.ConfigurationsUtils.readGlobalConfigBytesFromZookeeper;
import static org.apache.metron.stellar.common.shell.StellarExecutor.OperationType.DOC;
import static org.apache.metron.stellar.common.shell.StellarExecutor.OperationType.NORMAL;
import static org.apache.metron.stellar.dsl.Context.Capabilities.GLOBAL_CONFIG;
import static org.apache.metron.stellar.dsl.Context.Capabilities.STELLAR_CONFIG;
import static org.apache.metron.stellar.dsl.Context.Capabilities.ZOOKEEPER_CLIENT;
/**
* Executes Stellar expressions and maintains state across multiple invocations.
*/
public class StellarExecutor {
public static String SHELL_VARIABLES = "shellVariables";
public static String CONSOLE = "console";
private ReadWriteLock indexLock = new ReentrantReadWriteLock();
public static class VariableResult {
private String expression;
private Object result;
public VariableResult(String expression, Object result) {
this.expression = expression;
this.result = result;
}
public String getExpression() {
return expression;
}
public Object getResult() {
return result;
}
@Override
public String toString() {
String ret = "" + result;
if(expression != null) {
ret += " via " + expression;
}
return ret;
}
}
/**
* Prefix tree index of auto-completes.
*/
private PatriciaTrie<AutoCompleteType> autocompleteIndex;
/**
* The variables known by Stellar.
*/
private Map<String, VariableResult> variables;
/**
* The function resolver.
*/
private FunctionResolver functionResolver;
/**
* A Zookeeper client. Only defined if given a valid Zookeeper URL.
*/
private Optional<CuratorFramework> client;
/**
* The Stellar execution context.
*/
private Context context;
private Console console;
public enum OperationType {
DOC
, MAGIC
, NORMAL
}
public interface AutoCompleteTransformation {
String transform(OperationType type, String key);
}
public enum AutoCompleteType implements AutoCompleteTransformation{
FUNCTION((type, key) -> {
if(type == DOC) {
return StellarShell.DOC_PREFIX + key;
}
else if(type == NORMAL) {
return key + "(";
}
return key;
})
, VARIABLE((type, key) -> key )
, TOKEN((type, key) -> key)
;
AutoCompleteTransformation transform;
AutoCompleteType(AutoCompleteTransformation transform) {
this.transform = transform;
}
@Override
public String transform(OperationType type, String key) {
return transform.transform(type, key);
}
}
/**
* @param console The console used to drive the REPL.
* @param properties The Stellar properties.
* @throws Exception
*/
public StellarExecutor(Console console, Properties properties) throws Exception {
this(null, console, properties);
}
/**
* @param console The console used to drive the REPL.
* @param properties The Stellar properties.
* @throws Exception
*/
public StellarExecutor(String zookeeperUrl, Console console, Properties properties) throws Exception {
this.variables = new HashMap<>();
this.client = createClient(zookeeperUrl);
this.context = createContext(properties);
// initialize the default function resolver
StellarFunctions.initialize(this.context);
this.functionResolver = StellarFunctions.FUNCTION_RESOLVER();
this.autocompleteIndex = initializeIndex();
this.console = console;
// asynchronously update the index with function names found from a classpath scan.
new Thread( () -> {
Iterable<StellarFunctionInfo> functions = functionResolver.getFunctionInfo();
indexLock.writeLock().lock();
try {
for(StellarFunctionInfo info: functions) {
String functionName = info.getName();
autocompleteIndex.put(functionName, AutoCompleteType.FUNCTION);
}
} finally {
System.out.println("Functions loaded, you may refer to functions now...");
indexLock.writeLock().unlock();
}
}).start();
}
private PatriciaTrie<AutoCompleteType> initializeIndex() {
Map<String, AutoCompleteType> index = new HashMap<>();
index.put("==", AutoCompleteType.TOKEN);
index.put(">=", AutoCompleteType.TOKEN);
index.put("<=", AutoCompleteType.TOKEN);
index.put(":=", AutoCompleteType.TOKEN);
index.put("quit", AutoCompleteType.TOKEN);
index.put(StellarShell.MAGIC_FUNCTIONS, AutoCompleteType.FUNCTION);
index.put(StellarShell.MAGIC_VARS, AutoCompleteType.FUNCTION);
index.put(StellarShell.MAGIC_GLOBALS, AutoCompleteType.FUNCTION);
index.put(StellarShell.MAGIC_DEFINE, AutoCompleteType.FUNCTION);
index.put(StellarShell.MAGIC_UNDEFINE, AutoCompleteType.FUNCTION);
return new PatriciaTrie<>(index);
}
public Iterable<String> autoComplete(String buffer, final OperationType opType) {
indexLock.readLock().lock();
try {
SortedMap<String, AutoCompleteType> ret = autocompleteIndex.prefixMap(buffer);
if (ret.isEmpty()) {
return new ArrayList<>();
}
return Iterables.transform(ret.entrySet(), kv -> kv.getValue().transform(opType, kv.getKey()));
}
finally {
indexLock.readLock().unlock();
}
}
/**
* Creates a Zookeeper client.
* @param zookeeperUrl The Zookeeper URL.
*/
private Optional<CuratorFramework> createClient(String zookeeperUrl) {
// can only create client, if have valid zookeeper URL
if(StringUtils.isNotBlank(zookeeperUrl)) {
CuratorFramework client = ConfigurationsUtils.getClient(zookeeperUrl);
client.start();
return Optional.of(client);
} else {
return Optional.empty();
}
}
/**
* Creates a Context initialized with configuration stored in Zookeeper.
*/
private Context createContext(Properties properties) throws Exception {
Context.Builder contextBuilder = new Context.Builder()
.with(SHELL_VARIABLES, () -> variables)
.with(CONSOLE, () -> console)
.with(STELLAR_CONFIG, () -> properties);
// load global configuration from zookeeper
if (client.isPresent()) {
// fetch the global configuration
Map<String, Object> global = JSONUtils.INSTANCE.load(
new ByteArrayInputStream(readGlobalConfigBytesFromZookeeper(client.get())),
new TypeReference<Map<String, Object>>() {});
contextBuilder
.with(GLOBAL_CONFIG, () -> global)
.with(ZOOKEEPER_CLIENT, () -> client.get())
.with(STELLAR_CONFIG, () -> getStellarConfig(global, properties));
}
return contextBuilder.build();
}
private Map<String, Object> getStellarConfig(Map<String, Object> globalConfig, Properties props) {
Map<String, Object> ret = new HashMap<>();
ret.putAll(globalConfig);
if(props != null) {
for (Map.Entry<Object, Object> kv : props.entrySet()) {
ret.put(kv.getKey().toString(), kv.getValue());
}
}
return ret;
}
/**
* Executes the Stellar expression and returns the result.
* @param expression The Stellar expression to execute.
* @return The result of the expression.
*/
public Object execute(String expression) {
VariableResolver variableResolver = new MapVariableResolver(Maps.transformValues(variables, result -> result.getResult())
, Collections.emptyMap());
StellarProcessor processor = new StellarProcessor();
return processor.parse(expression, variableResolver, functionResolver, context);
}
/**
* Assigns a value to a variable.
* @param variable The name of the variable.
* @param value The value of the variable
*/
public void assign(String variable, String expression, Object value) {
this.variables.put(variable, new VariableResult(expression, value));
indexLock.writeLock().lock();
try {
if (value != null) {
this.autocompleteIndex.put(variable, AutoCompleteType.VARIABLE);
} else {
this.autocompleteIndex.remove(variable);
}
}
finally {
indexLock.writeLock().unlock();
}
}
public Map<String, VariableResult> getVariables() {
return this.variables;
}
public FunctionResolver getFunctionResolver() {
return functionResolver;
}
public Context getContext() {
return context;
}
}