All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.lab515.utils.Evaluator Maven / Gradle / Ivy

There is a newer version: 1.0.12
Show newest version
package io.github.lab515.utils;

import java.util.*;

/**
 * specifically for permission condition evaluation
 * supported operator: \(escape) !, *, **, &, |, and special keyword not, true, false, and, or
 * sample :
 * /sf* && **Service.abc(*,,**.ABC*)
 * function call can only return boolean evaluated results
 * no optimization (jmp, skip) since it's only for conditional check
 * no quote is allowed, any special chars need to use \ as escape
 *
 * permission namespace , same as java package,
 * permission.permtype.permstrig
 * feature.amdin tools and name
 */
public class Evaluator {
  static class Token{
    String val;
    int pri;
    Token _next;
    Token _last;
    Token operate(Token Op, Token Oper2) {
      if(Oper2 == null){
        return null;
      }
      if (Op._next != null) {
        return this;
      }
      Op._next = null;
      Op._last = this;

      (_next != null ? _last : this)._next = Oper2;
      _last = Oper2._next != null ? Oper2._last : Oper2;
      _last._next = Op;
      _last = Op;
      return this;
    }
    Token(String val, int pri){
      this.val = val;
      this.pri = pri;
    }
    boolean is(String val, int pri){
      return pri == this.pri && (val == null || val.equals(this.val));
    }
    boolean isStopper(){
      return is(",", NA) || is(":",TRI) || is(")",NA);
    }
  }


  static final int AND = 3, OR  = 2, NOT = 4, CALL = 5, PARA = 6, OPER = 0, NA = -1, CONST = -2, MATCH = -3, TRI = 1;
  static final int TOKEN = 0, COMMENT = 1, ESCAPE = 2;
  static final String OP_CHARS = "#&|!()?:,";
  static final int[]  OP_PRIORITIES = {NA, AND, OR, NA, NA, NA, TRI, TRI, NA};
  static final Token NULL_TOKEN = new Token("",NA);
  static final HashMap keywords = new HashMap<>();
  static {
    keywords.put("true",CONST);
    keywords.put("null",CONST);
    keywords.put("false",CONST);
    keywords.put("and",AND);
    keywords.put("or",OR);
    keywords.put("not",NA);
  }

  /**
   * tokenizer, it will parse string in greedy mode, stop at operators or others
   * ALWAYS case insensitive!
   * @param expr
   * @return
   */
  private void parse(String expr, boolean caseSensitive) {
    parseErr = null;
    expression = null;
    if(expr == null || expr.length() < 1) {
      parseErr = "empty expression";
      return;
    }
    LinkedList ret = new LinkedList<>();
    StringBuilder tk = new StringBuilder();
    StringBuilder tkSeg = new StringBuilder();
    int mode = TOKEN;
    int v = -1;
    String str = null;
    Integer pr = null;
    int op = OPER;
    for(char c : (caseSensitive ? expr : expr.toLowerCase()).toCharArray()){
      if(mode == COMMENT){
        if(c == '\n') {
          mode = TOKEN;
        }
      }else if(mode == ESCAPE){
        if (c == 'n') {
          tk.append('\n');
          tkSeg.append(' ');
        }else if(c == 't'){
          tk.append('\t');
          tkSeg.append(' ');
        }else if(Character.isLetter(c)){
          tk.append(c);
          tkSeg.append(' ');
        }else{
          tk.append(c); // doesn't care what it is
          tkSeg.append(' ');
          if(c == '*'){
            op = MATCH;
          }
        }
        mode = TOKEN;
      }else{
        if(c == '\\'){
          mode = ESCAPE;
        }else if((Character.isWhitespace(c) && (v = -1) == -1) ||  (v = OP_CHARS.indexOf(c)) >= 0) {
          // look back, to see if any "and","or","not"
          if(tk.length() > 0) {
            pr = keywords.get(tkSeg.toString());
            if(pr != null){
              // split it
              str = tk.substring(0,tk.length() - tkSeg.length()).trim();
              if(str.length() > 0){
                ret.add(new Token(str,op));
              }
              ret.add(new Token(pr == CONST && tkSeg.charAt(0) == 'n' ? "": tkSeg.toString(),pr));
              tk.setLength(0);
              op = OPER;
            }
          }
          if(v >= 0){
            if(tk.length() > 0){
              ret.add(new Token(tk.toString().trim(),op));
              tk.setLength(0);
              op = OPER;
            }
            if (c == '#') {
              mode = COMMENT;
            }else{
              ret.add(new Token(c+"", OP_PRIORITIES[v]));
            }
          }else if(tk.length() > 0){
            tk.append(c);
          }
          tkSeg.setLength(0);
        }else{
          tk.append(c);
          tkSeg.append(c);
          if(c == '*'){
            op = MATCH;
          }
        }
      }
    }
    if(tk.length()>0){
      pr = keywords.get(tkSeg.toString());
      if(pr != null){
        // split it
        str = tk.substring(0,tk.length() - tkSeg.length()).trim();
        if(str.length() > 0){
          ret.add(new Token(str,op));
        }
        ret.add(new Token(pr == CONST && tkSeg.charAt(0) == 'f' ? "": tkSeg.toString(),pr));
        tk.setLength(0);
      }else {
        ret.add(new Token(tk.toString().trim(), op));
      }
    }

    expression = parseExpr(ret, NULL_TOKEN);
    if(expression != null && !ret.isEmpty()){
      parseErr = "unexpected more tokens";
      expression = null;
    }

  }

  private static boolean isVariableName(String t){
    if(!(Character.isLetter(t.charAt(0)) || t.charAt(0) == '_')){
      return false;
    }
    for(char c : t.toCharArray()){
      if(!(Character.isLetter(c) || Character.isDigit(c) || c == '_' || c == '.')){
        return false;
      }
    }
    return true;
  }
  // function and not now is diferent
  // !a =>  a ! ""
  // a(b) => b call a
  private Token parseExpr(LinkedList tks, Token fromOp){
    // only support & | ! ? :
    Token t = tks.peek();
    if(t == null){
      parseErr = "expect more tokens";
      return null;
    }
    tks.pop();
    Token next = null;
    int paraC = 0;
    if(t.pri != OPER && t.pri != CONST && t.pri != MATCH){
      if(t.is("(", NA)){
        if((next = parseExpr(tks, NULL_TOKEN)) == null){
          return null;
        }
        if((t = tks.peek()) == null || !t.is(")",NA)) {
          parseErr = "not enclosed bracket";
          return null;
        }
        tks.pop();
        t = next;
      }else if(t.is("!",NA) || t.is("not",NA)){
        t.pri = NOT;
        tks.push(t);
        t = new Token("",NA); // nop is not necessary
      }else{
        parseErr = "invalid token, expect operand but " + t.val;
        return null;
      }
    }

    while(!tks.isEmpty()){
      // since no func call allowed
      next = tks.peek();
      if(next.isStopper()){ // we don't do match the enclose
        return t;
      }else if(next.pri <= 0) {
        if (next.is("(",NA) && t._last == null && isVariableName(t.val)) {
          tks.pop();
          next = null;
          paraC = 0;
          while (true) {
            if (tks.isEmpty()) {
              parseErr = "expect token as parameter";
              return null;
            }
            if (tks.peek().is(")",NA) && next == null) {
              break;
            }
            paraC++;
            if (next != null) {
              if(next.operate(new Token(",", PARA), parseExpr(tks, NULL_TOKEN)) == null){
                return null;
              }
            } else {
              if((next = parseExpr(tks, NULL_TOKEN)) == null){
                return null;
              }
            }
            if (tks.isEmpty() || !tks.peek().is(",",NA)) {
              break;
            }
            tks.pop();
          }
          if (tks.isEmpty() || !tks.peek().is(")",NA)) {
            parseErr = "exepct ) at end of func call";
            return null;
          }
          tks.pop();
          if (next == null) {
            next = new Token("", OPER);
          }
          t.pri = NA; // not a oper
          t = next.operate(new Token(paraC+"",CALL), t);
          //t.operate(new Token(paraC+"",CALL),next);
        } else {
          parseErr = "invalid function format, expect op but " + next.val;
          return null;
        }
      }else {
        if (fromOp != null && (fromOp.pri > next.pri || (fromOp.pri == next.pri && next.pri != NOT))) {
          break;
        }
        tks.pop();
        if (next.pri == TRI) { // no optimization jumps
          if(next.is("?",TRI)) {
            if(t.operate(next, parseExpr(tks, NULL_TOKEN))==null){
              return null;
            }
            if (tks.isEmpty() || !((next = tks.pop()).is(":", TRI))) {
              parseErr = "invalid ?: expr!";
              return null;
            }
          }else{
            parseErr = "invalid : expr";
            return null;
          }
        }
        //if(next.pri == NOT){
        //  if((t = parseExpr(tks, next).operate(next,t)) == null){
        //    return null;
        //  }
        //}else{
          if(t.operate(next, parseExpr(tks, next)) == null){
            return null;
          }
        //}

      }
    }
    return t;
  }

  private Token expression;
  private String exprStr;
  private String parseErr;
  private String defVarVal;
  private String defCallRet;

  public Evaluator(String expr, String defVarVal, String defCallRet, boolean caseSensitive){
    this.exprStr = expr;
    this.defVarVal = defVarVal;
    this.defCallRet = defCallRet;
    parse(exprStr, caseSensitive);
  }

  public Evaluator(String expr, String defVarVal, String defCallRet){
    this(expr,defVarVal,defCallRet,false);
  }
  public String getExpr(){
    return exprStr;
  }


  public boolean run(Map vars, Object reserved, EvalOptions ec){
    return evaluateBoolean(run2(vars,reserved, ec));
  }

  public String run2(Map vars, Object reserved, EvalOptions ec){
    if(ec != null){
      ec.beforeEvaluate(vars);;
    }
    String ret = evaluateExpr(vars,reserved, ec);
    if(ec != null){
      ret = ec.postEvaluate(ret);
    }
    return ret;
  }

  public String getParseError(){
    return parseErr;
  }

  public static boolean evaluateBoolean(String v){
    if(v == null || v.length() < 1){
      return false;
    }
    if(v.equals("false") || v.equals("0") || v.equals("null")){
      return false;
    }
    return true;
  }
  static class RunStack{
    RunStack next;
    String v;
    String t;
    RunStack(){

    }
    RunStack(String v, String t, RunStack next){
      this.v =v;
      this.t= t;
      this.next = next;
    }

    void push(String v,String t){
      next = new RunStack(v,t, next);
    }

    String pop(){
      v = next.v;
      t = next.t;
      next = next.next;
      return v;
    }

    String token(){
      return t;
    }
  }

  @SuppressWarnings("squid:S1643")
  private String evaluateExpr(Map vars, Object reserved, EvalOptions ec){
    RunStack runStack = new RunStack();
    String oper1, oper2, temp;
    String[] paras, paraNames;
    Token expr = expression;
    if(expr == null){
      if(ec != null){
        ec.raiseErr("invalid expression can not be executed", parseErr,true);
      }
      return null;
    }

    while(expr != null){
      if(expr.pri <= OPER){
        if(expr.pri != OPER && expr.pri != MATCH){
          oper1 = expr.val; // only function call, or const
        } else {
          oper1 = null;
          if(expr.pri == OPER){
            oper1 = vars != null ? vars.get(expr.val) : null;
          }
          // for all
          if (ec != null) {
            oper1 = ec.resolveVariable(expr.val, oper1, expr.pri == MATCH);
          }
          if(oper1 == null && ec == null && defVarVal != null){
            oper1 = defVarVal;
          }else if(oper1 == null) {
            return null;
          }
        }
        runStack.push(oper1,expr.val);
      }else{
        oper2 = runStack.pop();
        temp = runStack.token();
        oper1 = runStack.pop();
        switch (expr.pri){
          case TRI: //? :
            if(expr.val.equals("?")) {
              if (evaluateBoolean(oper1)) {
                oper2 = "?" + oper2; // reset
              }else{
                oper2 = "";
                temp = "";
              }
            }else{ // :
              if(oper1.startsWith("?")){
                oper2 = oper1.substring(1);
                temp = runStack.token();
              }
            }
            break;
          case OR:
            if(evaluateBoolean(oper1)){
              oper2 = oper1;
              oper2 = runStack.token();
            }
            break;
          case AND:
            if(!evaluateBoolean(oper1)){
              oper2 = "";
              temp = "";
            }
            break;
          case NOT:
            oper2 = evaluateBoolean(oper2) ? "" : "1";
            temp = "";
            break;
          case PARA:
            runStack.push(oper1, runStack.token());
            break;
          case CALL:
            paras = new String[Integer.parseInt(expr.val)];
            paraNames = new String[paras.length];
            if(paras.length > 0) {
              paras[paras.length-1] = oper1;
              paraNames[paras.length-1] = runStack.token();
              for (int i = paras.length - 2; i >= 0; i--) {
                paras[i] = runStack.pop();
                paraNames[i] = runStack.token();
              }
            }
            oper2 = ec != null ? ec.handleCall(oper2,paraNames, paras) : null;
            temp = "";
            if(oper2 == null && defCallRet == null){
              return null;
            }else if(oper2 == null){
              oper2 = defCallRet;
            }
            break;
          default:
            if(ec != null){
              ec.raiseErr("invalid op:" + expr.pri , expr.val,true);
            }
            return null;
        }
        runStack.push(oper2,temp);
      }
      expr = expr._next;
    }
    if(runStack.next == null || runStack.next.next != null){
      if(ec != null){
        ec.raiseErr("invalid runstack size:" + (runStack.next == null ? "0" : " more than 1"),"" ,true);
      }
      return null;
    }
    return runStack.pop();
  }

  protected  Set getAllOpers(int pri){
    HashSet ret = new HashSet<>();
    Token t = expression;
    while(t != null){
      if(t.pri == pri){
        ret.add(t.val);
      }
      t = t._next;
    }
    return ret;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy