mirror of
https://github.com/LuckPerms/LuckPerms.git
synced 2024-11-30 22:53:27 +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);
|
String filter = filters.isEmpty() ? "" : String.join(" ", filters);
|
||||||
|
|
||||||
VerboseFilter parsedFilter;
|
VerboseFilter compiledFilter;
|
||||||
try {
|
try {
|
||||||
parsedFilter = VerboseFilter.parse(filter);
|
compiledFilter = new VerboseFilter(filter);
|
||||||
} catch (InvalidFilterException e) {
|
} catch (InvalidFilterException e) {
|
||||||
Message.VERBOSE_INVALID_FILTER.send(sender, filter, e.getCause().getMessage());
|
Message.VERBOSE_INVALID_FILTER.send(sender, filter, e.getCause().getMessage());
|
||||||
return CommandResult.FAILURE;
|
return CommandResult.FAILURE;
|
||||||
@ -85,7 +85,7 @@ public class VerboseCommand extends SingleCommand {
|
|||||||
|
|
||||||
boolean notify = !mode.equals("record");
|
boolean notify = !mode.equals("record");
|
||||||
|
|
||||||
plugin.getVerboseHandler().registerListener(sender, parsedFilter, notify);
|
plugin.getVerboseHandler().registerListener(sender, compiledFilter, notify);
|
||||||
|
|
||||||
if (notify) {
|
if (notify) {
|
||||||
if (!filter.equals("")) {
|
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;
|
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 me.lucko.luckperms.common.verbose.event.VerboseEvent;
|
||||||
|
import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler;
|
||||||
import java.util.List;
|
import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler.AST;
|
||||||
import java.util.StringTokenizer;
|
import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler.LexerException;
|
||||||
import java.util.stream.Collectors;
|
import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler.ParserException;
|
||||||
|
|
||||||
import javax.script.ScriptEngine;
|
|
||||||
import javax.script.ScriptException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a verbose filter expression.
|
* 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>
|
* evaluations should be relatively fast.</p>
|
||||||
*/
|
*/
|
||||||
public final class VerboseFilter {
|
public final class VerboseFilter {
|
||||||
|
private final AST ast;
|
||||||
|
|
||||||
// the characters used in an expression which are part of the expression
|
public VerboseFilter(String filterExpression) throws InvalidFilterException {
|
||||||
// syntax - and not the filter itself.
|
if (filterExpression.isEmpty()) {
|
||||||
private static final String DELIMITERS = " |&()!";
|
this.ast = AST.ALWAYS_TRUE;
|
||||||
|
|
||||||
// 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();
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
this.expression = generateExpression(engine, filter);
|
this.ast = BooleanExpressionCompiler.compile(filterExpression);
|
||||||
} catch (Exception e) {
|
} catch (LexerException | ParserException e) {
|
||||||
throw new InvalidFilterException("Exception occurred whilst generating an expression for '" + filter + "'", 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
|
* Evaluates whether the check data passes the filter
|
||||||
*
|
*
|
||||||
@ -141,170 +59,20 @@ public final class VerboseFilter {
|
|||||||
* @return if the check data passes the filter
|
* @return if the check data passes the filter
|
||||||
*/
|
*/
|
||||||
public boolean evaluate(VerboseEvent data) {
|
public boolean evaluate(VerboseEvent data) {
|
||||||
if (this.expression.isEmpty()) {
|
if (isBlank()) {
|
||||||
return true;
|
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 {
|
try {
|
||||||
String result = this.engine.eval(expressionString).toString();
|
return this.ast.eval(data);
|
||||||
|
} catch (Exception e) {
|
||||||
// validate return value
|
e.printStackTrace();
|
||||||
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 false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBlank() {
|
public boolean isBlank() {
|
||||||
return this.expression.isEmpty();
|
return this.ast == AST.ALWAYS_TRUE;
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,14 @@ public class MetaCheckEvent extends VerboseEvent {
|
|||||||
.add("origin", this.origin.name().toLowerCase());
|
.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
|
* Represents the origin of a meta check
|
||||||
*/
|
*/
|
||||||
|
@ -86,6 +86,14 @@ public class PermissionCheckEvent extends VerboseEvent {
|
|||||||
object.add("origin", this.origin.name().toLowerCase());
|
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
|
* 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.StackTracePrinter;
|
||||||
import me.lucko.luckperms.common.util.gson.JArray;
|
import me.lucko.luckperms.common.util.gson.JArray;
|
||||||
import me.lucko.luckperms.common.util.gson.JObject;
|
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.context.Context;
|
||||||
import net.luckperms.api.query.QueryMode;
|
import net.luckperms.api.query.QueryMode;
|
||||||
@ -40,7 +41,7 @@ import java.util.Objects;
|
|||||||
/**
|
/**
|
||||||
* Represents a verbose event.
|
* Represents a verbose event.
|
||||||
*/
|
*/
|
||||||
public abstract class VerboseEvent {
|
public abstract class VerboseEvent implements VariableEvaluator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the entity which was checked
|
* 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