mirror of
https://github.com/LuckPerms/LuckPerms.git
synced 2024-11-24 11:38:40 +01:00
Implement a simple compiler (lexer + parser) for verbose filters
Removes the dependency on the Nashorn runtime.
This commit is contained in:
parent
81cbe02a9c
commit
7a37fcd792
@ -75,9 +75,9 @@ public class VerboseCommand extends SingleCommand {
|
||||
|
||||
String filter = filters.isEmpty() ? "" : String.join(" ", filters);
|
||||
|
||||
VerboseFilter parsedFilter;
|
||||
VerboseFilter compiledFilter;
|
||||
try {
|
||||
parsedFilter = VerboseFilter.parse(filter);
|
||||
compiledFilter = new VerboseFilter(filter);
|
||||
} catch (InvalidFilterException e) {
|
||||
Message.VERBOSE_INVALID_FILTER.send(sender, filter, e.getCause().getMessage());
|
||||
return CommandResult.FAILURE;
|
||||
@ -85,7 +85,7 @@ public class VerboseCommand extends SingleCommand {
|
||||
|
||||
boolean notify = !mode.equals("record");
|
||||
|
||||
plugin.getVerboseHandler().registerListener(sender, parsedFilter, notify);
|
||||
plugin.getVerboseHandler().registerListener(sender, compiledFilter, notify);
|
||||
|
||||
if (notify) {
|
||||
if (!filter.equals("")) {
|
||||
|
@ -1,47 +0,0 @@
|
||||
/*
|
||||
* This file is part of LuckPerms, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package me.lucko.luckperms.common.util;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
|
||||
/**
|
||||
* Provides a nashorn script engine (lazily)
|
||||
*/
|
||||
public final class Scripting {
|
||||
private Scripting() {}
|
||||
|
||||
private static ScriptEngine engine = null;
|
||||
|
||||
// Lazily load
|
||||
public static synchronized ScriptEngine getScriptEngine() {
|
||||
if (engine == null) {
|
||||
engine = new ScriptEngineManager(null).getEngineByName("nashorn");
|
||||
}
|
||||
return engine;
|
||||
}
|
||||
|
||||
}
|
@ -25,115 +25,33 @@
|
||||
|
||||
package me.lucko.luckperms.common.verbose;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import me.lucko.luckperms.common.util.Scripting;
|
||||
import me.lucko.luckperms.common.verbose.event.MetaCheckEvent;
|
||||
import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent;
|
||||
import me.lucko.luckperms.common.verbose.event.VerboseEvent;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptException;
|
||||
import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler;
|
||||
import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler.AST;
|
||||
import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler.LexerException;
|
||||
import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler.ParserException;
|
||||
|
||||
/**
|
||||
* Represents a verbose filter expression.
|
||||
*
|
||||
* <p>The filter is parsed when the instance is initialised - subsequent
|
||||
* <p>The expression is compiled when the instance is initialised - subsequent
|
||||
* evaluations should be relatively fast.</p>
|
||||
*/
|
||||
public final class VerboseFilter {
|
||||
private final AST ast;
|
||||
|
||||
// the characters used in an expression which are part of the expression
|
||||
// syntax - and not the filter itself.
|
||||
private static final String DELIMITERS = " |&()!";
|
||||
|
||||
// the script engine to use when evaluating the expression
|
||||
private final ScriptEngine engine;
|
||||
// the parsed expression
|
||||
private final List<Token> expression;
|
||||
|
||||
/**
|
||||
* Compiles a {@link VerboseFilter} instance for the given filter string
|
||||
*
|
||||
* @param filter the filter
|
||||
* @return a filter
|
||||
* @throws InvalidFilterException if the filter is invalid
|
||||
*/
|
||||
public static VerboseFilter parse(String filter) throws InvalidFilterException {
|
||||
ScriptEngine engine = Scripting.getScriptEngine();
|
||||
if (engine == null) {
|
||||
throw new RuntimeException("Script engine not present");
|
||||
}
|
||||
|
||||
return new VerboseFilter(engine, filter);
|
||||
}
|
||||
|
||||
private VerboseFilter(ScriptEngine engine, String filter) throws InvalidFilterException {
|
||||
this.engine = engine;
|
||||
|
||||
if (filter.isEmpty()) {
|
||||
this.expression = ImmutableList.of();
|
||||
public VerboseFilter(String filterExpression) throws InvalidFilterException {
|
||||
if (filterExpression.isEmpty()) {
|
||||
this.ast = AST.ALWAYS_TRUE;
|
||||
} else {
|
||||
try {
|
||||
this.expression = generateExpression(engine, filter);
|
||||
} catch (Exception e) {
|
||||
throw new InvalidFilterException("Exception occurred whilst generating an expression for '" + filter + "'", e);
|
||||
this.ast = BooleanExpressionCompiler.compile(filterExpression);
|
||||
} catch (LexerException | ParserException e) {
|
||||
throw new InvalidFilterException("Exception occurred whilst generating an expression for '" + filterExpression + "'", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a filter string into a list of 'tokens' forming the expression.
|
||||
*
|
||||
* Each token either represents part of the expressions syntax
|
||||
* (logical and, logical or, brackets, or space) or a value.
|
||||
*
|
||||
* @param engine the script engine to test the expression with
|
||||
* @param filter the filter string
|
||||
* @return a parsed list of expressions
|
||||
* @throws ScriptException if the engine throws an exception whilst evaluating the expression
|
||||
*/
|
||||
private static List<Token> generateExpression(ScriptEngine engine, String filter) throws ScriptException {
|
||||
// tokenize the filter using the filter characters as delimiters.
|
||||
StringTokenizer tokenizer = new StringTokenizer(filter, DELIMITERS, true);
|
||||
|
||||
// use the tokenizer to parse the string to a list of 'tokens'.
|
||||
ImmutableList.Builder<Token> expressionBuilder = ImmutableList.builder();
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
String token = tokenizer.nextToken();
|
||||
|
||||
if (isDelimiter(token)) {
|
||||
// if the token is a delimiter, just append it to the expression as a constant
|
||||
expressionBuilder.add(new ConstantToken(token));
|
||||
} else {
|
||||
// otherwise consider it to be a value
|
||||
expressionBuilder.add(new VariableToken(token));
|
||||
}
|
||||
}
|
||||
|
||||
// build & test the expression
|
||||
List<Token> expression = expressionBuilder.build();
|
||||
testExpression(expression, engine);
|
||||
return expression;
|
||||
}
|
||||
|
||||
private static void testExpression(List<Token> expression, ScriptEngine engine) throws ScriptException {
|
||||
// build a dummy version of the expression.
|
||||
// all values are simply replaced by "true"
|
||||
String dummyExpression = expression.stream().map(Token::forDummyExpression).collect(Collectors.joining());
|
||||
|
||||
// do a test run - if the engine returns a result without throwing an exception
|
||||
// and the result is a boolean, we can consider the expression to be valid
|
||||
String result = engine.eval(dummyExpression).toString();
|
||||
if (!result.equals("true") && !result.equals("false")) {
|
||||
throw new IllegalArgumentException("Expected true/false but got '" + result + "' instead.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates whether the check data passes the filter
|
||||
*
|
||||
@ -141,170 +59,20 @@ public final class VerboseFilter {
|
||||
* @return if the check data passes the filter
|
||||
*/
|
||||
public boolean evaluate(VerboseEvent data) {
|
||||
if (this.expression.isEmpty()) {
|
||||
if (isBlank()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// build an expression string for the passed check data.
|
||||
String expressionString = this.expression.stream().map(token -> token.forExpression(data)).collect(Collectors.joining());
|
||||
|
||||
// evaluate the expression using the script engine
|
||||
try {
|
||||
String result = this.engine.eval(expressionString).toString();
|
||||
|
||||
// validate return value
|
||||
if (!result.equals("true") && !result.equals("false")) {
|
||||
throw new IllegalArgumentException("Expected true/false but got '" + result + "' instead.");
|
||||
}
|
||||
|
||||
// return the result of the expression
|
||||
return Boolean.parseBoolean(result);
|
||||
|
||||
} catch (Throwable ex) {
|
||||
// print the error & return false
|
||||
ex.printStackTrace();
|
||||
return this.ast.eval(data);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBlank() {
|
||||
return this.expression.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.expression.stream().map(Token::toString).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the string is equal to one of the {@link #DELIMITERS}.
|
||||
*
|
||||
* @param string the string
|
||||
* @return true if delimiter, false otherwise
|
||||
*/
|
||||
private static boolean isDelimiter(String string) {
|
||||
switch (string.charAt(0)) {
|
||||
case ' ':
|
||||
case '|':
|
||||
case '&':
|
||||
case '(':
|
||||
case ')':
|
||||
case '!':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a part of an expression
|
||||
*/
|
||||
private interface Token {
|
||||
|
||||
/**
|
||||
* Returns the value of this token when part of an evaluated expression
|
||||
*
|
||||
* @param event the data which an expression is being formed for
|
||||
* @return the value to be used as part of the evaluated expression
|
||||
*/
|
||||
String forExpression(VerboseEvent event);
|
||||
|
||||
/**
|
||||
* Returns a 'dummy' value for this token in order to build a test
|
||||
* expression.
|
||||
*
|
||||
* @return the value to be used as part of the test expression
|
||||
*/
|
||||
String forDummyExpression();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a constant part of the expression - tokens will only ever
|
||||
* consist of the characters defined in the {@link #DELIMITERS} string.
|
||||
*/
|
||||
private static final class ConstantToken implements Token {
|
||||
private final String string;
|
||||
|
||||
private ConstantToken(String string) {
|
||||
// replace single '&' and '|' character with double values
|
||||
if (string.equals("&")) {
|
||||
string = "&&";
|
||||
} else if (string.equals("|")) {
|
||||
string = "||";
|
||||
}
|
||||
|
||||
this.string = string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String forExpression(VerboseEvent event) {
|
||||
return this.string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String forDummyExpression() {
|
||||
return this.string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a variable part of the token. When evaluated as an expression,
|
||||
* this token will be replaced with a boolean 'true' or 'false' - depending
|
||||
* on if the passed check data "matches" the value of this token.
|
||||
*
|
||||
* The check data will be deemed a "match" if:
|
||||
* - the target of the check is equal to the value of the token
|
||||
* - the permission/meta key being checked for starts with the value of the token
|
||||
* - the result of the check is equal to the value of the token
|
||||
*/
|
||||
private static final class VariableToken implements Token {
|
||||
private final String value;
|
||||
|
||||
private VariableToken(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String forExpression(VerboseEvent event) {
|
||||
if (event instanceof PermissionCheckEvent) {
|
||||
PermissionCheckEvent permissionEvent = (PermissionCheckEvent) event;
|
||||
return Boolean.toString(
|
||||
this.value.equals("permission") ||
|
||||
permissionEvent.getCheckTarget().equalsIgnoreCase(this.value) ||
|
||||
permissionEvent.getPermission().toLowerCase().startsWith(this.value.toLowerCase()) ||
|
||||
permissionEvent.getResult().result().name().equalsIgnoreCase(this.value)
|
||||
);
|
||||
}
|
||||
|
||||
if (event instanceof MetaCheckEvent) {
|
||||
MetaCheckEvent metaEvent = (MetaCheckEvent) event;
|
||||
return Boolean.toString(
|
||||
this.value.equals("meta") ||
|
||||
metaEvent.getCheckTarget().equalsIgnoreCase(this.value) ||
|
||||
metaEvent.getKey().toLowerCase().startsWith(this.value.toLowerCase()) ||
|
||||
metaEvent.getResult().equalsIgnoreCase(this.value)
|
||||
);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown event type: " + event);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String forDummyExpression() {
|
||||
return "true";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value;
|
||||
}
|
||||
return this.ast == AST.ALWAYS_TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -73,6 +73,14 @@ public class MetaCheckEvent extends VerboseEvent {
|
||||
.add("origin", this.origin.name().toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eval(String variable) {
|
||||
return variable.equals("meta") ||
|
||||
getCheckTarget().equalsIgnoreCase(variable) ||
|
||||
getKey().toLowerCase().startsWith(variable.toLowerCase()) ||
|
||||
getResult().equalsIgnoreCase(variable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the origin of a meta check
|
||||
*/
|
||||
|
@ -86,6 +86,14 @@ public class PermissionCheckEvent extends VerboseEvent {
|
||||
object.add("origin", this.origin.name().toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eval(String variable) {
|
||||
return variable.equals("permission") ||
|
||||
getCheckTarget().equalsIgnoreCase(variable) ||
|
||||
getPermission().toLowerCase().startsWith(variable.toLowerCase()) ||
|
||||
getResult().result().name().equalsIgnoreCase(variable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the origin of a permission check
|
||||
*/
|
||||
|
@ -30,6 +30,7 @@ import com.google.gson.JsonObject;
|
||||
import me.lucko.luckperms.common.util.StackTracePrinter;
|
||||
import me.lucko.luckperms.common.util.gson.JArray;
|
||||
import me.lucko.luckperms.common.util.gson.JObject;
|
||||
import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler.VariableEvaluator;
|
||||
|
||||
import net.luckperms.api.context.Context;
|
||||
import net.luckperms.api.query.QueryMode;
|
||||
@ -40,7 +41,7 @@ import java.util.Objects;
|
||||
/**
|
||||
* Represents a verbose event.
|
||||
*/
|
||||
public abstract class VerboseEvent {
|
||||
public abstract class VerboseEvent implements VariableEvaluator {
|
||||
|
||||
/**
|
||||
* The name of the entity which was checked
|
||||
|
@ -0,0 +1,297 @@
|
||||
/*
|
||||
* This file is part of LuckPerms, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package me.lucko.luckperms.common.verbose.expression;
|
||||
|
||||
import com.google.common.collect.AbstractIterator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StreamTokenizer;
|
||||
import java.io.StringReader;
|
||||
|
||||
/**
|
||||
* Compiler for boolean expressions with variables.
|
||||
*/
|
||||
public class BooleanExpressionCompiler {
|
||||
|
||||
/**
|
||||
* Compiles an {@link AST} for a given boolean expression.
|
||||
*
|
||||
* @param expression the expression string
|
||||
* @return the compiled AST
|
||||
* @throws LexerException if an error occurs when lexing the expression
|
||||
* @throws ParserException if an error occurs when parsing the expression
|
||||
*/
|
||||
public static AST compile(String expression) throws LexerException, ParserException {
|
||||
return new Parser(new Lexer(expression)).parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the value of variables within an expression.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface VariableEvaluator {
|
||||
|
||||
/**
|
||||
* Evaluates the value of a variable.
|
||||
*
|
||||
* @param variable the variable
|
||||
* @return the result
|
||||
*/
|
||||
boolean eval(String variable);
|
||||
}
|
||||
|
||||
/**
|
||||
* AST for a boolean expression.
|
||||
*/
|
||||
public interface AST {
|
||||
AST ALWAYS_TRUE = e -> true;
|
||||
|
||||
/**
|
||||
* Evaluates the AST.
|
||||
*
|
||||
* @param variableEvaluator the variable evaluator
|
||||
* @return the result
|
||||
*/
|
||||
boolean eval(VariableEvaluator variableEvaluator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a lexing error.
|
||||
*/
|
||||
public static final class LexerException extends RuntimeException {
|
||||
LexerException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
LexerException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a parsing error.
|
||||
*/
|
||||
public static final class ParserException extends RuntimeException {
|
||||
ParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of {@link Token}s into an {@link AST}.
|
||||
*/
|
||||
private static final class Parser {
|
||||
/*
|
||||
CFG accepted by this parser:
|
||||
|
||||
exp → term {OR term}
|
||||
term → factor {AND factor}
|
||||
factor → VARIABLE
|
||||
factor → NOT factor
|
||||
factor → OPEN_BRACKET exp CLOSE_BRACKET
|
||||
*/
|
||||
|
||||
/** The lexer generating the tokens. */
|
||||
private final Lexer lexer;
|
||||
/** The current token being parsed */
|
||||
private Token currentToken;
|
||||
/** The current root of the AST */
|
||||
private AST root;
|
||||
|
||||
Parser(Lexer lexer) {
|
||||
this.lexer = lexer;
|
||||
}
|
||||
|
||||
AST parse() {
|
||||
exp();
|
||||
return this.root;
|
||||
}
|
||||
|
||||
private void exp() {
|
||||
term();
|
||||
while (this.currentToken == ConstantToken.OR) {
|
||||
Or or = new Or();
|
||||
or.left = this.root;
|
||||
term();
|
||||
or.right = this.root;
|
||||
this.root = or;
|
||||
}
|
||||
}
|
||||
|
||||
private void term() {
|
||||
factor();
|
||||
while (this.currentToken == ConstantToken.AND) {
|
||||
And and = new And();
|
||||
and.left = this.root;
|
||||
factor();
|
||||
and.right = this.root;
|
||||
this.root = and;
|
||||
}
|
||||
}
|
||||
|
||||
private void factor() {
|
||||
this.currentToken = this.lexer.next();
|
||||
|
||||
if (this.currentToken instanceof VariableToken) {
|
||||
Variable variable = new Variable();
|
||||
variable.variable = ((VariableToken) this.currentToken).string;
|
||||
this.root = variable;
|
||||
this.currentToken = this.lexer.next();
|
||||
} else if (this.currentToken == ConstantToken.NOT) {
|
||||
Not not = new Not();
|
||||
factor();
|
||||
not.child = this.root;
|
||||
this.root = not;
|
||||
} else if (this.currentToken == ConstantToken.OPEN_BRACKET) {
|
||||
exp();
|
||||
if (this.currentToken != ConstantToken.CLOSE_BRACKET) {
|
||||
throw new ParserException("Brackets are not matched");
|
||||
}
|
||||
this.currentToken = this.lexer.next();
|
||||
} else {
|
||||
throw new ParserException("Malformed expression");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* AST implementations */
|
||||
|
||||
private static final class And implements AST {
|
||||
AST left;
|
||||
AST right;
|
||||
|
||||
@Override
|
||||
public boolean eval(VariableEvaluator variableEvaluator) {
|
||||
return this.left.eval(variableEvaluator) && this.right.eval(variableEvaluator);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Or implements AST {
|
||||
AST left;
|
||||
AST right;
|
||||
|
||||
@Override
|
||||
public boolean eval(VariableEvaluator variableEvaluator) {
|
||||
return this.left.eval(variableEvaluator) || this.right.eval(variableEvaluator);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Not implements AST {
|
||||
AST child;
|
||||
|
||||
@Override
|
||||
public boolean eval(VariableEvaluator variableEvaluator) {
|
||||
return !this.child.eval(variableEvaluator);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Variable implements AST {
|
||||
String variable;
|
||||
|
||||
@Override
|
||||
public boolean eval(VariableEvaluator variableEvaluator) {
|
||||
return variableEvaluator.eval(this.variable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexes a {@link String} into a list of {@link Token}s.
|
||||
*/
|
||||
private static final class Lexer extends AbstractIterator<Token> {
|
||||
private final StreamTokenizer tokenizer;
|
||||
private boolean end = false;
|
||||
|
||||
Lexer(String expression) {
|
||||
this.tokenizer = new StreamTokenizer(new StringReader(expression));
|
||||
this.tokenizer.resetSyntax();
|
||||
this.tokenizer.wordChars('!', '~'); // all ascii characters
|
||||
this.tokenizer.whitespaceChars('\u0000', ' ');
|
||||
"()&|!".chars().forEach(this.tokenizer::ordinaryChar);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Token computeNext() {
|
||||
if (this.end) {
|
||||
return endOfData();
|
||||
}
|
||||
try {
|
||||
int token = this.tokenizer.nextToken();
|
||||
switch (token) {
|
||||
case StreamTokenizer.TT_EOF:
|
||||
this.end = true;
|
||||
return ConstantToken.EOF;
|
||||
case StreamTokenizer.TT_WORD:
|
||||
return new VariableToken(this.tokenizer.sval);
|
||||
case '(':
|
||||
return ConstantToken.OPEN_BRACKET;
|
||||
case ')':
|
||||
return ConstantToken.CLOSE_BRACKET;
|
||||
case '&':
|
||||
return ConstantToken.AND;
|
||||
case '|':
|
||||
return ConstantToken.OR;
|
||||
case '!':
|
||||
return ConstantToken.NOT;
|
||||
default:
|
||||
throw new LexerException("Unknown token: " + ((char) token) + "(" + token + ")");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new LexerException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface Token { }
|
||||
|
||||
private enum ConstantToken implements Token {
|
||||
OPEN_BRACKET, CLOSE_BRACKET, AND, OR, NOT, EOF
|
||||
}
|
||||
|
||||
private static final class VariableToken implements Token {
|
||||
final String string;
|
||||
|
||||
VariableToken(String string) {
|
||||
this.string = string;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
private static void assertion(String expression, boolean expected) {
|
||||
if (compile(expression).eval(var -> var.equals("true")) != expected) {
|
||||
throw new AssertionError(expression + " is not " + expected);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
assertion("false & false | true", true);
|
||||
assertion("false & (false | true)", false);
|
||||
assertion("true | false & false", true);
|
||||
assertion("(true | false) & false", false);
|
||||
assertion("(true & ((true | false) & !(true & false)))", true);
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user