Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Andre601/ExpressionParser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ExpressionParser

Important

This repository has been archived and therefore won't receive any updates in the future. To still receive updates (And for a better way of using this library with Maven/Gradle) check the new Codeberg Repository.

ExpressionParser is a copy of the original expression parser made by @CodeCrafter47 for the TabOverlayCommon project used by his plugins BungeeTabListPlus and AdvancedTabOverlay and shared with his permission.

It allows you to parse simple to complex expressions using Tokenization, while also allowing you to further extend and customize it.

License

This project is licensed under GNU General Public License v3.0 and as such is freely available for everyone.
Original credit and copyright goes to CodeCrafter47 for the original Expression parser system.

How it works

To parse a String into a ExpressionTemplate one has to create new instances of the ExpressionTokenizer and ExpressionTemplateParser, and call their respective parse method.
The ExpressionTokenizer takes a Iterable of TokenReaders while the ExpressionTemplateParser takes a ImmutableMap of Token keys with Operator values.

For the sake of demonstration are we using DefaultExpressionParserEngine, which already creates the necessary instances for us through the provided Lists and Map.
When calling the compile(String, ParseWarnCollector) method will the DefaultExpressionParserEngine call the parse(String, ParseWarnCollector) method of ExpressionTokenizer and parse(List<Token>, ParseWarnCollector) of the ExpressionTemplateParser.

The ExpressionTokenizer will iterate through the String one character at a time, skipping whitespaces in the process.
For each Iteration is it going through a list of TokenReaders to see if any returns a non-null Token. Should one be found will it be added to the List of Tokens the ExpressionTokenizer returns. The TokenReader returning a valid token also updates the position in the String for the next iteration.

The List of Tokens is being given to the ExpressionTemplateParser which first will try to turn as many of the tokens into a single ExpressionTemplate.
It does so by iterating through a list of ValueReaders, giving each the list of Tokens to try and convert. Should no valid ValueReader be found will an exception be thrown, which is caught by ExpressionTemplateParser, added to the ParseWarnCollector before returning null to cancel the parsing.
Should, however, a valid ExpressionTemplate be found, will it be added to a list before moving on to finding Operators.

To find Operators, the ExpressionTemplateParser first checks if the list of Tokens still has entries left. Should this be the case will the first entry be removed from the list, which also gives the Token entry that was removed.
This Token is then used as a key for the ImmutableMap containing Token keys and Operator values. Should no entry be found will a warning be added to the ParseWarnCollector before null is returned to stop the parsing.
Should a Operator be found will it be added to a list of Operators before continuing with parsing the remaining tokens the same way like in the start. Should the list at this point be empty is a warning added to the ParseWarnCollector before null is returned to stop the parsing.
In the next step is the list of Operators iterated through, prioritizing Operators with a higher priority. The Operator is used to create a new ExpressionTemplate using the two ExpressionTemplates that exist before and after the operator in the String. In the case of a ListOperator are the different ExpressionTemplates created by the Operators AND-ed together.

As a final step is the List of ExpressionTemplates updated before returning the very first entry of the list.

Getting the library

Note

Replace {VERSION} with the latest available release tag in this repository

GitHub Tag

Maven (pom.xml)

<repositories>
  <repository>
    <id>jitpack</id>
    <url>https://jitpack.io/</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>ch.andre601</groupId>
    <artifactId>ExpressionParser</artifactId>
    <version>{VERSION}</version>
    <scope>compile</scope> <!-- Includes the project -->
  </dependency>
</dependencies>

Gradle (build.gradle)

repositories {
  maven { url = "https://jitpack.io/" }
}

dependencies {
  implementation "ch.andre601:ExpressionParser:{VERSION}"
}

Adding own Tokens

This library allows you to create your own Tokens which will be used when an expression is parsed.
For our example will we try to implement the placeholder syntax ${placeholder} which will be replaced using certain values depending on the placeholder.

To get started, we first have to create a new class extending the Token class and add the necessary constructor:

import ch.andre601.expressionparser.tokens.Token;

public class PlaceholderToken extends Token{
    
    // This will later hold our parsed placeholder value.
    private final Placeholder value;
    
    public PlaceholderToken(Placeholder value){
        super("PLACEHOLDER");
        this.value = value;
    }
    
    public Placeholder getValue(){
        return value;
    }
}

Make sure that the String you provide to the super is unique, as it is used as the Token's ID in errors to identify it.

Next step is to create a TokenReader for the PlaceholderToken, so that the ExpressionTokenizer can actually recognize our placeholder and return the right token for it.
Simply create a new class that extends the TokenReader and add the necessary constructor and method override. In our case would it look like this:

import ch.andre601.expressionparser.ParseWarnCollector;
import ch.andre601.expressionparser.tokens.Token;
import ch.andre601.expressionparser.tokens.readers.TokenReader;

import java.text.ParsePosition;

public class PlaceholderTokenReader extends TokenReader{
    
    public PlaceholderTokenReader(int priority){
        super(priority);
    }
    
    @Override
    public Token read(String text, ParsePosition position, ParseWarnCollector collector){
        if(position.getIndex() + 1 < text.length() && text.charAt(position.getIndex()) == '$' && text.charAt(position.getIndex() + 1) == '{'){
            position.setIndex(position.getIndex() + 2);
            return new PlaceholderToken(PlaceholderParser.parse(text, position, collector));
        }
        
        return null;
    }
}

In the read method are we doing a few things:

  1. We check if the current position in the text + 1 is less than the text's total length.
  2. We check if the character at the current position in the text equals $.
  3. We check if the character at the next position in the text equals {.
  4. In case of all of the above being true are we increasing the text position by 2 and return a new PlaceholderToken instance.

In our example do we use a separate class - the PlaceholderParser - to parse the String into a Placeholder instance.

import ch.andre601.expressionparser.ParseWarnCollector;

import java.text.ParsePosition;

public class PlaceholderParser{
    
    public static Placeholder parse(String text, ParsePosition position, ParseWarnCollector collector){
        int start = position.getIndex();
        int index = position.getIndex();
        
        StringBuilder values = new StringBuilder();
        
        boolean invalid = true;
        boolean hadSpace = false;
        
        // We only need the text that has been identified as start of a placeholder.
        char[] chars = text.substring(index).toCharArray();
        
        // This loop just goes through the text, considerin a placeholder valid if it finds a } before any space
        for(final char c : chars){
            index++;
            
            if(c == ' '){
                hadSpace = true;
                break;
            }
            
            if(c == '}'){
                invalid = false;
                break;
            }
            
            values.append(c);
        }
        
        String valueStr = values.toString();
        
        // Reset StringBuilder
        values.setLength(0);
        
        // Set to the last position in the for loop + 1 to skip the last found character (Space or })
        position.setIndex(index + 1);
        
        if(invalid){
            values.append("${")
                .append(valueStr);
            
            if(hadSpace){
                collector.appendWarning(start, "Placeholder contained space character.");
                values.append(' ');
            }else{
                collector.appendWarning(start, "Placeholder does not have any closing bracket.");
            }
            
            return values.toString();
        }
        
        return switch(valueStr){
            case "placeholder1" -> new Placeholder("1");
            case "placeholder2" -> new Placeholder("2");
            case "placeholder3" -> new Placeholder("3");
            default -> null;
        };
    }
}

Now the question: What is Placeholder actually?
It's a class that implements the ExpressionTemplate interface to return ToBooleanExpression, ToDoubleExpression and [ToStringExpressions] for the ExpressionEngine to use.

Here is how it looks:

import ch.andre601.expressionparser.expressions.ToBooleanExpression;
import ch.andre601.expressionparser.expressions.ToDoubleExpression;
import ch.andre601.expressionparser.expressions.ToStringExpression;
import ch.andre601.expressionparser.templates.ExpressionTemplate;

public class Placeholder implements ExpressionTemplate{
    
    private final ToBooleanExpression toBooleanExpression;
    private final ToDoubleExpression toDoubleExpression;
    private final ToStringExpression toStringExpression;
    
    public Placeholder(String value){
        double doubleValue;
        try{
            doubleValue = Double.parseDouble(value);
        }catch(NumberFormatException ex){
            doubleValue = value.length();
        }
        
        this.toBooleanExpression = ToBooleanExpression.literal(Boolean.parseBoolean(value));
        this.toDoubleExpression = ToDoubleExpression.literal(doubleValue);
        this.toStringExpression = ToStringExpression.literal(value);
    }
    
    @Override
    public ToBooleanExpression returnBooleanExpression(){
        return toBooleanExpression;
    }
    
    @Override
    public ToDoubleExpression returnDoubleExpression(){
        return toDoubleExpression;
    }
    
    @Override
    public ToStringExpression returnStringExpression(){
        return toStringExpression;
    }
}

What we did here is create a class that accepts a String in its constructor and creates a ToBooleanExpression, ToDoubleExpression and ToStringExpression instance to return when used.
For the ToDoubleExpression do we try to parse the String as a double and should it fail, use the length of the String itself.

It's worth pointing out that the library offers a ConstantExpressionTemplate which you could use instead of making a Placeholder class, as it already has the same functionality available, allowing you to create instances using available static methods for boolean, double and String.

The next step to take now is to create a class extending the abstract ValueReader class and override the read method:

import ch.andre601.expressionparser.ParseWarnCollector;
import ch.andre601.expressionparser.parsers.ExpressionTemplateParser;
import ch.andre601.expressionparser.parsers.ValueReader;
import ch.andre601.expressionparser.templates.ExpressionTemplate;
import ch.andre601.expressionparser.tokens.Token;

import java.util.List;

public class PlaceholderReader extends ValueReader{
    
    @Override
    public ExpressionTemplate read(ExpressionTemplateParser parser, List<Token> tokens, ParseWarnCollector collector){
        if(tokens.get(0) instanceof PlaceholderToken){
            PlaceholderToken token = (PlaceholderToken)tokens.remove(0);
            return token.getValue();
        }
        
        return null;
    }
}

This would now check if the first token in the list is a instance of PlaceholderToken and if true, gets it from the list while also removing it before returning its value, which would be our Placeholder class.
Should it not be such a token will null be returned instead.

Now as a final step do we need to add the Token, TokenReader and ValueReader into the ExpressionTemplateParser or ExpressionTokenizer, depending on what it is.
We will use the DefaultExpressionParserEngine as it offers a Builder class to more easily add the necessary instances to use. Here is an example again:

import ch.andre601.expressionparser.DefaultExpressionParserEngine;
import ch.andre601.expressionparser.ParseWarnCollector;
import ch.andre601.expressionparser.templates.ExpressionTemplate;

public class ConditionParser{
    
    private final DefaultExpressionParserEngine engine;
    
    public ConditionParser(){
        engine = new DefaultExpressionParserEngine.DefaultBuilder().createDefault()
            .addTokenReader(new PlaceholderTokenReader(-20))
            .addValueReader(new PlaceholderReader())
            .build();
    }
    
    public boolean parse(String text){
        ParseWarnCollector collector = new ParseWarnCollector(text);
        ExpressionTemplate result = engine.compile(text, collector);
        
        if(collector.hasWarnings()){
            System.out.println("Encountered issues while parsing expression '" + text + "':");
            
            for(ParseWarnCollector.Context context : collector.getWarnings()){
                if(context.position() == -1){
                    System.out.println("  - " + context.message());
                }else{
                    System.out.println("  - At position " + context.position() + ": " + context.message());
                }
            }
        }
        
        if(result == null)
            return false;
        
        return result.returnBooleanExpression().evaluate();
    }
}

In this final example are we doing a lot of things.
First of all are we creating a new DefaultExpressionParserEngine using the Builder's createDefault() static method. This gives us an instance of the Builder with default TokenReaders, Operators and ValueReaders already applied.
When then add our own TokenReader and ValueReader to the Builder before building it, creating the DefaultExpressionParserEngine.

Finally, are we using this engine instance in a parse method, creating a ParseWarnCollector too.
We simply call the engine's compile method to retrieve a ExpressionTemplate instance. At this point will we check the collector for if it has received any warnings and should this be the case, print them in our console.
Finally are we checking for the template to not be null. If it is, return false, else get the ToBooleanExpression instance it holds and call evaluate() to return the boolean value stored.

You now have a working Placeholder token parser!

Adding own operators

Operators are used to perform operations, as the name may suggest.
As an example in the expression 1 + 2 is + the operator.

An operator is a Token, just like normal tokens are, with the difference that a Operator instance is also created for it.

In this example will we recreate the "contains" (<_) operator to use.
Unlike custom tokens can we just use the Token class itself for our Operator. In our example would this be a new Token("CONTAINS").

First, we have to create a new method that would be used when our operator is called. What you do in this method is completely up to you. Only thing to keep in mind is to have the method return an ExpressionTemplate.
In our example are we creating a nested class that would be our ExpressionTemplate and a method that creates a new instance of this class:

import ch.andre601.expressionparser.expressions.ToBooleanExpression;
import ch.andre601.expressionparser.expressions.ToStringExpression;
import ch.andre601.expressionparser.expressions.abstracted.AbstractBinaryToBooleanExpression;
import ch.andre601.expressionparser.templates.ExpressionTemplate;
import ch.andre601.expressionparser.templates.abstracted.AbstractBooleanExpressionTemplate;

public class ExampleExpressions{
    
    public static ExpressionTemplate contains(ExpressionTemplate a, ExpressionTemplate b){
        return new Contains(a, b);
    }
    
    private static class Contains extends AbstractBooleanExpressionTemplate{
        
        private final ExpressionTemplate a;
        private final ExpressionTemplate b;
        
        public Contains(ExpressionTemplate a, ExpressionTemplate b){
            this.a = a;
            this.b = b;
        }
        
        @Override
        public ToBooleanExpression returnBooleanExpression(){
            ToStringExpression expressionA = a.returnStringExpression();
            ToStringExpression expressionB = b.returnStringExpression();
            
            return new AbstractBinaryToBooleanExpression<>(expressionA, expressionB){
                @Override
                public boolean evaluate(){
                    return expressionA.evaluate().contains(expressionB.evaluate());
                }
            };
        }
    }
}

This is a lot to take in, so lets go over it step by step.

The Contains class is what is important here. It extends the [AbstractBooleanExpressionTemplate], which implements the ExpressionTemplate interface and overrides the returnDoubleExpression() and returnStringExpression() to return whatever value toBooleanExpression() returns.
We created a constructor that accepts two ExpressionTemplates, a and b, which would be the content to the left and right of our operator characters respectively.
The returnBooleanExpression() method is overriden here due to it not being overriden in the AbstractBooleanExpressionTemplate. In it do we first get the ToStringExpression instances of each ExpressionTemplate.
We then create a new AbstractBinaryToBooleanExpression instance. This abstract class accepts two Objects implementing the Expression interface. We give it our two expressions as constructor values and then call their evaluate() method to return their String outputs and use a simple contains call here to see if String a contains String b.

Our next step is to add our operator as such to the Parser, so that it will be used and recognized.
Assuming the DefaultExpressionParserEngine is used would it look something similar to this:

import ch.andre601.expressionparser.DefaultExpressionParserEngine;
import ch.andre601.expressionparser.operator.Operator;
import ch.andre601.expressionparser.tokens.Token;
import ch.andre601.expressionparser.tokens.readers.PatternTokenReader;

public class ConditionParser{
    
    private final Token contains = new Token("CONTAINS");
    private final DefaultExpressionParserEngine engine;
    
    public ConditionParser(){
        engine = new DefaultExpressionParserEngine.DefaultBuilder().createDefault()
            .addTokenReader(new PatternTokenReader(contains, "<_"))
            .addOperator(contains, Operator.of(25, ExampleExpressions::contains))
            .build();
    }
}

We add our contains token using the [PatternTokenReader] to associate <_ with our token while also using the token to associate with a new Operator instance.
The Operator.of method accepts a priority and a function with two ExpressionTemplates. Since our contains method only needs two can we use a reference call here.

With this, you have successfully added a contains operator!

About

The Expression parsing system from AdvancedTabOverlay and BungeeTabListPlus, separated into its own project to use! REPOSITORY ARCHIVED! CHECK LINK FOR NEW LOCATION!

Resources

License

Stars

Watchers

Forks

Languages