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

com.igormaznitsa.jcp.expression.ExpressionParser Maven / Gradle / Ivy

/*
 * Copyright 2002-2019 Igor Maznitsa (http://www.igormaznitsa.com)
 *
 * 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 com.igormaznitsa.jcp.expression;

import com.igormaznitsa.jcp.context.PreprocessingState;
import com.igormaznitsa.jcp.context.PreprocessorContext;
import com.igormaznitsa.jcp.exceptions.FilePositionInfo;
import com.igormaznitsa.jcp.expression.functions.AbstractFunction;
import com.igormaznitsa.jcp.expression.functions.FunctionDefinedByUser;
import com.igormaznitsa.jcp.expression.operators.AbstractOperator;
import com.igormaznitsa.jcp.extension.PreprocessorExtension;
import com.igormaznitsa.jcp.utils.PreprocessorUtils;
import com.igormaznitsa.meta.annotation.MustNotContainNull;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.PushbackReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import static com.igormaznitsa.meta.common.utils.Assertions.assertNotNull;

/**
 * This class is a parser allows to parse an expression and make a tree as the output
 *
 * @author Igor Maznitsa ([email protected])
 */
public final class ExpressionParser {

  /**
   * It contains the instance for the parser, because the parser is a singleton
   */
  private static final ExpressionParser INSTANCE = new ExpressionParser();

  @Nonnull
  public static ExpressionParser getInstance() {
    return INSTANCE;
  }

  private static boolean isDelimiterOrOperatorChar(final char chr) {
    return isDelimiter(chr) || isOperatorChar(chr);
  }

  private static boolean isDelimiter(final char chr) {
    switch (chr) {
      case ',':
      case '(':
      case ')':
        return true;
      default:
        return false;
    }
  }

  private static boolean isOperatorChar(final char chr) {
    switch (chr) {
      case '-':
      case '+':
      case '%':
      case '*':
      case '/':
      case '&':
      case '|':
      case '!':
      case '^':
      case '=':
      case '<':
      case '>':
        return true;
      default:
        return false;
    }
  }

  /**
   * To parse an expression represented as a string and get a tree
   *
   * @param expressionStr the expression string to be parsed, must not be null
   * @param context       a preprocessor context to be used to get variable values
   * @return a tree containing parsed expression
   * @throws IOException it will be thrown if there is a problem to read the expression string
   */
  @Nonnull
  public ExpressionTree parse(@Nonnull final String expressionStr, @Nonnull final PreprocessorContext context) throws IOException {
    assertNotNull("Expression is null", expressionStr);

    final PushbackReader reader = new PushbackReader(new StringReader(expressionStr));

    final ExpressionTree result;
    final PreprocessingState state = context.getPreprocessingState();
    result = new ExpressionTree(state.makeIncludeStack(), state.getLastReadString());

    if (readExpression(reader, result, context, false, false) != null) {
      final String text = "Unexpected result during parsing [" + expressionStr + ']';
      throw context.makeException(text, null);
    }

    result.postProcess();

    return result;
  }

  /**
   * It reads an expression from a reader and fill a tree
   *
   * @param reader        the reader to be used as the character source, must not be null
   * @param tree          the result tree to be filled by read items, must not be null
   * @param context       a preprocessor context to be used for variables
   * @param insideBracket the flag shows that the expression can be ended by a bracket
   * @param argument      the flag shows that the expression can be ended by a comma
   * @return the last read expression item (a comma or a bracket for instance), it can be null
   * @throws IOException it will be thrown if there is a problem in reading from the reader
   */
  @Nullable
  public ExpressionItem readExpression(@Nonnull final PushbackReader reader, @Nonnull final ExpressionTree tree, @Nonnull final PreprocessorContext context, final boolean insideBracket, final boolean argument) throws IOException {
    boolean working = true;

    ExpressionItem result = null;

    final FilePositionInfo[] stack;
    final String sourceLine;

    final PreprocessingState state = context.getPreprocessingState();
    stack = state.makeIncludeStack();
    sourceLine = state.getLastReadString();

    ExpressionItem prev = null;

    while (working) {
      final ExpressionItem nextItem = nextItem(reader, context);
      if (nextItem == null) {
        working = false;
        result = null;
      } else if (nextItem.getExpressionItemType() == ExpressionItemType.SPECIAL) {
        if (nextItem == SpecialItem.BRACKET_CLOSING) {
          if (insideBracket) {
            working = false;
            result = nextItem;
          } else if (argument) {
            working = false;
            result = nextItem;
          } else {
            final String text = "Detected alone closing bracket";
            throw context.makeException("Detected alone closing bracket", null);
          }
        } else if (nextItem == SpecialItem.BRACKET_OPENING) {
          if (prev != null && prev.getExpressionItemType() == ExpressionItemType.VARIABLE) {
            final String text = "Unknown function detected [" + prev.toString() + ']';
            throw context.makeException(text, null);
          }

          final ExpressionTree subExpression;
          subExpression = new ExpressionTree(stack, sourceLine);
          if (SpecialItem.BRACKET_CLOSING != readExpression(reader, subExpression, context, true, false)) {
            final String text = "Detected unclosed bracket";
            throw context.makeException(text, null);
          }
          tree.addTree(subExpression);
        } else if (nextItem == SpecialItem.COMMA) {
          return nextItem;
        }
      } else if (nextItem.getExpressionItemType() == ExpressionItemType.FUNCTION) {
        final AbstractFunction function = (AbstractFunction) nextItem;
        ExpressionTree functionTree = readFunction(function, reader, context, stack, sourceLine);
        tree.addTree(functionTree);
      } else {
        tree.addItem(nextItem);
      }
      prev = nextItem;
    }
    return result;
  }

  /**
   * The auxiliary method allows to form a function and its arguments as a tree
   *
   * @param function     the function which arguments will be read from the stream, must not be null
   * @param reader       the reader to be used as the character source, must not be null
   * @param context      a preprocessor context, it will be used for a user functions and variables
   * @param includeStack the current file include stack, can be null
   * @param sources      the current source line, can be null
   * @return an expression tree containing parsed function arguments
   * @throws IOException it will be thrown if there is any problem to read chars
   */
  @Nonnull
  private ExpressionTree readFunction(@Nonnull final AbstractFunction function, @Nonnull final PushbackReader reader, @Nonnull final PreprocessorContext context, @Nullable @MustNotContainNull final FilePositionInfo[] includeStack, @Nullable final String sources) throws IOException {
    final ExpressionItem expectedBracket = nextItem(reader, context);
    if (expectedBracket == null) {
      throw context.makeException("Detected function without params [" + function.getName() + ']', null);
    }

    final int arity = function.getArity();

    ExpressionTree functionTree;

    if (arity == 0) {
      final ExpressionTree subExpression = new ExpressionTree(includeStack, sources);
      final ExpressionItem lastItem = readFunctionArgument(reader, subExpression, context, includeStack, sources);
      if (SpecialItem.BRACKET_CLOSING != lastItem) {
        throw context.makeException("There is not closing bracket for function [" + function.getName() + ']', null);
      } else if (!subExpression.getRoot().isEmptySlot()) {
        throw context.makeException("The function \'" + function.getName() + "\' doesn't need arguments", null);
      } else {
        functionTree = new ExpressionTree(includeStack, sources);
        functionTree.addItem(function);
      }
    } else {

      final List arguments = new ArrayList<>(arity);
      for (int i = 0; i < function.getArity(); i++) {
        final ExpressionTree subExpression = new ExpressionTree(includeStack, sources);
        final ExpressionItem lastItem = readFunctionArgument(reader, subExpression, context, includeStack, sources);

        if (SpecialItem.BRACKET_CLOSING == lastItem) {
          arguments.add(subExpression);
          break;
        } else if (SpecialItem.COMMA == lastItem) {
          arguments.add(subExpression);
        } else {
          throw context.makeException("Wrong argument for function [" + function.getName() + ']', null);
        }
      }

      functionTree = new ExpressionTree(includeStack, sources);
      functionTree.addItem(function);
      ExpressionTreeElement functionTreeElement = functionTree.getRoot();

      if (arguments.size() != functionTreeElement.getArity()) {
        throw context.makeException("Wrong argument number detected \'" + function.getName() + "\', must be " + function.getArity() + " argument(s)", null);
      }

      functionTreeElement.fillArguments(arguments);
    }
    return functionTree;
  }

  /**
   * The auxiliary method allows to read a function argument
   *
   * @param reader    a reader to be the character source, must not be null
   * @param tree      the result tree to be filled by read items, must not be null
   * @param context   a preprocessor context
   * @param callStack the current file call stack, can be null
   * @param source    the current source line, can be null
   * @return the last read expression item (a comma or a bracket)
   * @throws IOException it will be thrown if there is any error during char reading from the reader
   */
  @Nullable
  ExpressionItem readFunctionArgument(@Nonnull final PushbackReader reader, @Nonnull final ExpressionTree tree, @Nonnull final PreprocessorContext context, @Nullable @MustNotContainNull final FilePositionInfo[] callStack, @Nullable final String source) throws IOException {
    boolean working = true;
    ExpressionItem result = null;
    while (working) {
      final ExpressionItem nextItem = nextItem(reader, context);
      if (nextItem == null) {
        throw context.makeException("Non-closed function detected", null);
      } else if (SpecialItem.COMMA == nextItem) {
        result = nextItem;
        working = false;
      } else if (SpecialItem.BRACKET_OPENING == nextItem) {
        final ExpressionTree subExpression = new ExpressionTree(callStack, source);
        if (SpecialItem.BRACKET_CLOSING != readExpression(reader, subExpression, context, true, false)) {
          throw context.makeException("Non-closed bracket inside a function argument detected", null);
        }
        tree.addTree(subExpression);
      } else if (SpecialItem.BRACKET_CLOSING == nextItem) {
        result = nextItem;
        working = false;
      } else if (nextItem.getExpressionItemType() == ExpressionItemType.FUNCTION) {
        final AbstractFunction function = (AbstractFunction) nextItem;
        ExpressionTree functionTree = readFunction(function, reader, context, callStack, source);
        tree.addTree(functionTree);
      } else {
        tree.addItem(nextItem);
      }
    }
    return result;
  }

  private int hex2int(@Nonnull final PreprocessorContext context, final char chr) {
    final int result;
    if (Character.isDigit(chr)) {
      result = chr - '0';
    } else {
      result = 10 + (chr - Character.toLowerCase(chr) - 'a');
      if (result < 10 || result > 15) {
        throw context.makeException("Unexpected hex digit detected: " + chr, null);
      }
    }
    return result;
  }

  /**
   * Read the next item from the reader
   *
   * @param reader  a reader to be used as the char source, must not be null
   * @param context a preprocessor context
   * @return a read expression item, it can be null if the end is reached
   * @throws IOException it will be thrown if there is any error during a char reading
   */
  @Nullable
  ExpressionItem nextItem(@Nonnull final PushbackReader reader, @Nonnull final PreprocessorContext context) throws IOException {
    assertNotNull("Reader is null", reader);

    ParserState state = ParserState.WAIT;
    final StringBuilder builder = new StringBuilder(12);

    boolean found = false;
    char unicodeChar = 0;

    while (!found) {
      final int data = reader.read();

      if (data < 0) {
        if (state != ParserState.WAIT) {
          found = true;
        }
        break;
      }

      final char chr = (char) data;

      switch (state) {
        case WAIT: {
          if (Character.isWhitespace(chr)) {
            // do nothing
          } else if (chr == ',') {
            return SpecialItem.COMMA;
          } else if (chr == '(') {
            return SpecialItem.BRACKET_OPENING;
          } else if (chr == ')') {
            return SpecialItem.BRACKET_CLOSING;
          } else if (Character.isDigit(chr)) {
            builder.append(chr);
            if (chr == '0') {
              state = ParserState.HEX_NUMBER;
            } else {
              state = ParserState.NUMBER;
            }
          } else if (chr == '.') {
            builder.append('.');
            state = ParserState.FLOAT_NUMBER;
          } else if (Character.isLetter(chr) || chr == '$' || chr == '_') {
            builder.append(chr);
            state = ParserState.VALUE_OR_FUNCTION;
          } else if (chr == '\"') {
            state = ParserState.STRING;
          } else if (isOperatorChar(chr)) {
            builder.append(chr);
            state = ParserState.OPERATOR;
          } else {
            throw context.makeException("Unsupported token character detected \'" + chr + '\'', null);
          }
        }
        break;
        case OPERATOR: {
          if (!isOperatorChar(chr) || isDelimiter(chr)) {
            reader.unread(data);
            found = true;
          } else {
            builder.append(chr);
          }
        }
        break;
        case FLOAT_NUMBER: {
          if (Character.isDigit(chr)) {
            builder.append(chr);
          } else {
            found = true;
            reader.unread(data);
          }
        }
        break;
        case HEX_NUMBER: {
          if (builder.length() == 1) {
            if (chr == 'X' || chr == 'x') {
              builder.append(chr);
            } else if (chr == '.') {
              builder.append(chr);
              state = ParserState.FLOAT_NUMBER;
            } else if (Character.isDigit(chr)) {
              state = ParserState.NUMBER;
            } else {
              state = ParserState.NUMBER;
              found = true;
              reader.unread(data);
            }
          } else if (Character.isDigit(chr) || (chr >= 'a' && chr <= 'f') || (chr >= 'A' && chr <= 'F')) {
            builder.append(chr);
          } else {
            found = true;
            reader.unread(data);
          }
        }
        break;
        case UNICODE_DIGIT0:
          unicodeChar = (char) (hex2int(context, chr) << 12);
          state = ParserState.UNICODE_DIGIT1;
          break;
        case UNICODE_DIGIT1:
          unicodeChar = (char) (unicodeChar | (hex2int(context, chr) << 8));
          state = ParserState.UNICODE_DIGIT2;
          break;
        case UNICODE_DIGIT2:
          unicodeChar = (char) (unicodeChar | (hex2int(context, chr) << 4));
          state = ParserState.UNICODE_DIGIT3;
          break;
        case UNICODE_DIGIT3:
          unicodeChar = (char) (unicodeChar | hex2int(context, chr));
          state = ParserState.STRING;
          builder.append(unicodeChar);
          break;
        case NUMBER: {
          if (Character.isDigit(chr)) {
            builder.append(chr);
          } else if (chr == '.') {
            builder.append(chr);
            state = ParserState.FLOAT_NUMBER;
          } else {
            reader.unread(data);
            found = true;
          }
        }
        break;
        case VALUE_OR_FUNCTION: {
          if (Character.isWhitespace(chr) || isDelimiterOrOperatorChar(chr)) {
            reader.unread(data);
            found = true;
          } else {
            builder.append(chr);
          }
        }
        break;
        case SPECIAL_CHAR: {
          switch (chr) {
            case 'n':
              builder.append('\n');
              break;
            case 't':
              builder.append('\t');
              break;
            case 'b':
              builder.append('\b');
              break;
            case 'f':
              builder.append('\f');
              break;
            case 'r':
              builder.append('\r');
              break;
            case '\\':
              builder.append('\\');
              break;
            case '\"':
              builder.append('\"');
              break;
            case '\'':
              builder.append('\'');
              break;
            case 'u':
              state = ParserState.UNICODE_DIGIT0;
              break;
            default: {
              throw context.makeException("Unsupported special char detected \'\\" + chr + '\'', null);
            }
          }
          state = state == ParserState.SPECIAL_CHAR ? ParserState.STRING : state;
        }
        break;
        case STRING: {
          switch (chr) {
            case '\"': {
              found = true;
            }
            break;
            case '\\': {
              state = ParserState.SPECIAL_CHAR;
            }
            break;
            default: {
              builder.append(chr);
            }
            break;
          }
        }
        break;
        default:
          throw new Error("Unsupported parser state [" + state.name() + ']');
      }
    }

    if (!found) {
      switch (state) {
        case UNICODE_DIGIT0:
        case UNICODE_DIGIT1:
        case UNICODE_DIGIT2: {
          throw context.makeException("Non-completed unicode char has been detected", null);
        }
        case SPECIAL_CHAR:
        case STRING: {
          throw context.makeException("Non-closed string has been detected", null);
        }
        default:
          return null;
      }
    } else {
      ExpressionItem result = null;
      switch (state) {
        case FLOAT_NUMBER: {
          result = Value.valueOf(Float.parseFloat(builder.toString()));
        }
        break;
        case HEX_NUMBER: {
          final String text = builder.toString();
          if ("0".equals(text)) {
            result = Value.INT_ZERO;
          } else {
            final String str = PreprocessorUtils.extractTail("0x", text);
            result = Value.valueOf(Long.parseLong(str, 16));
          }
        }
        break;
        case NUMBER: {
          result = Value.valueOf(Long.parseLong(builder.toString()));
        }
        break;
        case OPERATOR: {
          final String operatorLC = builder.toString().toLowerCase(Locale.ENGLISH);
          for (final AbstractOperator operator : AbstractOperator.getAllOperators()) {
            if (operator.getKeyword().equals(operatorLC)) {
              result = operator;
              break;
            }
          }

          if (result == null) {
            throw context.makeException("Unknown operator detected \'" + operatorLC + '\'', null);
          }
        }
        break;
        case STRING: {
          result = Value.valueOf(builder.toString());
        }
        break;
        case VALUE_OR_FUNCTION: {
          final String str = builder.toString().toLowerCase();
          if (str.charAt(0) == '$') {

            assertNotNull("There is not a preprocessor context to define a user function [" + str + ']', context);

            final PreprocessorExtension extension = context.getPreprocessorExtension();
            if (extension == null) {
              throw context.makeException("There is not any defined preprocessor extension to get data about user functions [" + str + ']', null);
            }

            final String userFunctionName = PreprocessorUtils.extractTail("$", str);

            // user defined
            result = new FunctionDefinedByUser(userFunctionName, extension.getUserFunctionArity(userFunctionName), context);
          } else if ("true".equals(str)) {
            result = Value.BOOLEAN_TRUE;
          } else if ("false".equals(str)) {
            result = Value.BOOLEAN_FALSE;
          } else {
            final AbstractFunction function = AbstractFunction.findForName(str);
            if (function == null) {
              result = new Variable(str);
            } else {
              result = function;
            }
          }
        }
        break;
        default: {
          throw new Error("Unsupported final parser state detected [" + state.name() + ']');
        }
      }
      return result;
    }
  }

  /**
   * Internal parser states.
   */
  private enum ParserState {

    WAIT,
    NUMBER,
    HEX_NUMBER,
    FLOAT_NUMBER,
    STRING,
    SPECIAL_CHAR,
    UNICODE_DIGIT0,
    UNICODE_DIGIT1,
    UNICODE_DIGIT2,
    UNICODE_DIGIT3,
    VALUE_OR_FUNCTION,
    OPERATOR
  }

  /**
   * The enumeration describes some special items which can be met in the expression
   *
   * @author Igor Maznitsa ([email protected])
   */
  public enum SpecialItem implements ExpressionItem {

    BRACKET_OPENING,
    BRACKET_CLOSING,
    COMMA;

    SpecialItem() {
    }

    @Override
    @Nullable
    public ExpressionItemPriority getExpressionItemPriority() {
      return null;
    }

    @Override
    @Nullable
    public ExpressionItemType getExpressionItemType() {
      return ExpressionItemType.SPECIAL;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy