Package httl.spi.parsers

Source Code of httl.spi.parsers.ExpressionParser

/*
* Copyright 2011-2013 HTTL Team.
* Licensed 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 httl.spi.parsers;

import httl.Node;
import httl.ast.BinaryOperator;
import httl.ast.Constant;
import httl.ast.Expression;
import httl.ast.Operator;
import httl.ast.UnaryOperator;
import httl.ast.Variable;
import httl.spi.Filter;
import httl.spi.Parser;
import httl.util.ClassUtils;
import httl.util.DfaScanner;
import httl.util.LinkedStack;
import httl.util.StringUtils;
import httl.util.Token;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* ExpressionParser. (SPI, Singleton, ThreadSafe)
*
* @see httl.spi.parsers.TemplateParser#setExpressionParser(Parser)
*
* @author Liang Fei (liangfei0201 AT gmail DOT com)
*/
public class ExpressionParser implements Parser {

  //单字母命名, 保证状态机图简洁
 
  // BREAK,结束片段,包含当前字符
  private static final int B = DfaScanner.BREAK;

  // BACK_ONE,结束片段,退还当前字符
  private static final int B1 = DfaScanner.BREAK - 1;

  // BACK_TWO,结束片段,退还两个字符
  private static final int B2 = DfaScanner.BREAK - 2;

  // ERROR,解析出错
  private static final int E = DfaScanner.ERROR;

  // 表达式语法状态机图
  // 行表示状态
  // 行列交点表示, 在该状态时, 遇到某类型的字符时, 切换到的下一状态(数组行号)
  // E/B/T表示接收前面经过的字符为一个片断, R表示错误状态(这些状态均为负数)
  static final int[][] states = {
          // 0.空格, 1.字母, 2.数字, 3.点号, 4.双引号, 5.单引号, 6.反单引号, 7.反斜线, 8.括号, 9.其它
    /* 0.起始  */ { 0, 1, 2, 5, 7, 9, 11, 4, 6, 4}, // 初始状态或上一片断刚接收完成状态
    /* 1.变量  */{ B1, 1, 1, B1, E, E, E, B1, B1, B1}, // 变量名识别
    /* 2.数字  */{ B1, 2, 2, 13, E, E, E, B1, B1, B1}, // 数字识别
    /* 3.小数  */{ B1, 3, 3, B1, E, E, E, B1, B1, B1}, // 小数点号识别
    /* 4.操作*/{ B1, B1, B1, 4, B1, B1, B1, 4, B1, 4}, // 操作符识别
    /* 5.点号  */{ B1, 1, 3, B, B1, B1, B1, B1, B1, 4}, // 属性点号
    /* 6.括号  */{ B1, B1, B1, B1, B1, B1, B1, B1, B1, B1}, // 括号
    /* 7.字符*/{ 7, 7, 7, 7, B, 7, 7, 8, 7, 7}, // 双引号字符串识别
    /* 8.转义  */{ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7}, // 双引号字符串转义
    /* 9.字符*/{ 9, 9, 9, 9, 9, B, 9, 10, 9, 9}, // 单引号字符串识别
    /*10.转义  */{ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9}, // 单引号字符串转义
    /*11.字符  */{ 11, 11, 11, 11, 11, 11, B, 12, 11, 11}, // 反单引号字符串识别
    /*12.转义  */{ 11, 11, 11, 11, 11, 11, 11, 11, 11, 11}, // 反单引号字符串转义
    /*13.数点  */{ B2, B2, 3, B2, B2, B2, B2, B2, B2, B2}, // 数字属性点号识别, 区分于小数点(如: 123.toString 或 11..15)
  };

  static int getCharType(char ch) {
    switch (ch) {
      case ' ': case '\t': case '\n': case '\r': case '\f': case '\b':
        return 0;
      case '_' :
      case 'a' : case 'b' : case 'c' : case 'd' : case 'e' : case 'f' : case 'g' :
      case 'h' : case 'i' : case 'j' : case 'k' : case 'l' : case 'm' : case 'n' :
      case 'o' : case 'p' : case 'q' : case 'r' : case 's' : case 't' :
      case 'u' : case 'v' : case 'w' : case 'x' : case 'y' : case 'z' :
      case 'A' : case 'B' : case 'C' : case 'D' : case 'E' : case 'F' : case 'G' :
      case 'H' : case 'I' : case 'J' : case 'K' : case 'L' : case 'M' : case 'N' :
      case 'O' : case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' :
      case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' : case 'Z' :
        return 1;
      case '0' : case '1' : case '2' : case '3' : case '4' :
      case '5' : case '6' : case '7' : case '8' : case '9' :
        return 2;
      case '.' :
        return 3;
      case '\"' :
        return 4;
      case '\'' :
        return 5;
      case '`' :
        return 6;
      case '\\' :
        return 7;
      case '(' : case ')' : case '[' : case ']' :
        return 8;
      default:
        return 9;
    }
  }

  private static DfaScanner scanner = new DfaScanner() {
    @Override
    public int next(int state, char ch) {
      return states[state][getCharType(ch)];
    }
   
  };
 
  private static final Set<String> BINARY_OPERATORS = new HashSet<String>(Arrays.asList(new String[]{"+", "-", "*", "/", "%", "==", "!=", ">", ">=", "<", "<=", "gt", "ge", "lt", "le", "&&", "||", "&", "|", "^", ">>", "<<", ">>>", ",", "?", ":", "instanceof", "is", "[", ".."}));
 
  private static final Set<String> UNARY_OPERATORS = new HashSet<String>(Arrays.asList(new String[]{"+", "-", "!", "~", "new", "[", "{"}));

  private static final Pattern BLANK_PATTERN = Pattern.compile("^(\\s+)");
 
  private Filter expressionFilter;

  private List<String> forbidEqualsMethods;

  private List<String> forbidStartsMethods;

  private List<String> forbidEndsMethods;

  private String[] importPackages;

  public void setImportPackages(String[] importPackages) {
    this.importPackages = importPackages;
  }

  /**
   * httl.properties: expression.filters=httl.spi.filters.UnescapeXmlFilter
   */
  public void setExpressionFilter(Filter expressionFilter) {
    this.expressionFilter = expressionFilter;
  }

  /**
   * httl.properties: import.getters=forbid.methods=add,put,save,insert,modify,update,delete,remove,clear
   */
  public void setForbidMethods(String[] forbidMethods) {
    for (String method : forbidMethods) {
      if (method.startsWith("*")) {
        if (forbidEndsMethods == null) {
          forbidEndsMethods = new ArrayList<String>();
        }
        forbidEndsMethods.add(method.substring(1));
      } else if (method.endsWith("*")) {
        if (forbidStartsMethods == null) {
          forbidStartsMethods = new ArrayList<String>();
        }
        forbidStartsMethods.add(method.substring(0, method.length() - 1));
      } else {
        if (forbidEqualsMethods == null) {
          forbidEqualsMethods = new ArrayList<String>();
        }
        forbidEqualsMethods.add(method);
      }
    }
  }

  private int getTokenOffset(Token token) {
    int offset = token.getOffset();
    String msg = token.getMessage();
    Matcher matcher = BLANK_PATTERN.matcher(msg);
    if (matcher.find()) {
      return offset + matcher.group(1).length();
    }
    if (offset < 0) {
      offset = 0;
    }
    return offset;
  }

  private static final Map<String, String> UNARY_OPERATOR_NAMES;

  private static final Map<String, String> BINARY_OPERATOR_NAMES;
 
  static {
    Map<String, String> unary = new HashMap<String, String>();
    unary.put("+", "pos");
    unary.put("-", "neg");
    unary.put("!", "not");
    unary.put("~", "bitnot");
    unary.put("[", "list");
    unary.put("{", "map");
    unary.put("new ", "new");
    unary.put("(", "cast");
    UNARY_OPERATOR_NAMES = Collections.unmodifiableMap(unary);
   
    Map<String, String> binary = new HashMap<String, String>();
    binary.put("+", "add");
    binary.put("-", "sub");
    binary.put("*", "mul");
    binary.put("/", "div");
    binary.put("%", "mod");
    binary.put("==", "eq");
    binary.put("!=", "ne");
    binary.put(">", "gt");
    binary.put(">=", "ge");
    binary.put("<", "lt");
    binary.put("<=", "le");
    binary.put("&&", "and");
    binary.put("||", "or");
    binary.put("&", "bitand");
    binary.put("|", "bitor");
    binary.put("^", "xor");
    binary.put(">>", "rs");
    binary.put("<<", "ls");
    binary.put(">>>", "us");
    binary.put(",", "array");
    binary.put("?", "select");
    binary.put(":", "kv");
    binary.put("instanceof", "is");
    binary.put("[", "get");
    binary.put("..", "seq");
    BINARY_OPERATOR_NAMES = Collections.unmodifiableMap(binary);
  }

  private UnaryOperator createUnaryOperator(String operator, int priority, int offset) {
    String name = UNARY_OPERATOR_NAMES.get(operator);
    if (StringUtils.isNotEmpty(name)) {
      return new UnaryOperator(name, priority, offset);
    } else if (StringUtils.isFunction(operator)) {
      return new UnaryOperator(operator.substring(1), priority, offset);
    } else {
      throw new UnsupportedOperationException("Unsupported unary operator " + operator);
    }
  }

  private BinaryOperator createBinaryOperator(String operator, int priority, int offset) {
    String name = BINARY_OPERATOR_NAMES.get(operator);
    if (StringUtils.isNotEmpty(name)) {
      return new BinaryOperator(name, priority, offset);
    } else if (StringUtils.isFunction(operator)) {
      return new BinaryOperator(operator.substring(1), priority, offset);
    } else {
      throw new UnsupportedOperationException("Unsupported binary operator " + operator);
    }
  }

  private int getPriority(String operator, boolean unary) {
    int priority = 1000;
    if (unary && operator.startsWith("new ")) {
      return priority;
    }
    priority --;
    if (StringUtils.isFunction(operator) || operator.equals("[")) {
      return priority;
    }
    priority --;
    if (unary) {
      return priority;
    }
    priority --;
    if ("*".equals(operator)
        || "/".equals(operator)
        || "%".equals(operator)) {
      return priority;
    }
    priority --;
    if ("+".equals(operator)
        || "-".equals(operator)) {
      return priority;
    }
    priority --;
    if (">>".equals(operator)
        || "<<".equals(operator)
        || ">>>".equals(operator)) {
      return priority;
    }
    priority --;
    if ("..".equals(operator)) {
      return priority;
    }
    priority --;
    if (">".equals(operator)
        || "<".equals(operator)
        || ">=".equals(operator)
        || "<=".equals(operator)
        || "instanceof".equals(operator)) {
      return priority;
    }
    priority --;
    if ("==".equals(operator)
        || "!=".equals(operator)) {
      return priority;
    }
    priority --;
    if ("&".equals(operator)) {
      return priority;
    }
    priority --;
    if ("^".equals(operator)) {
      return priority;
    }
    priority --;
    if ("|".equals(operator)) {
      return priority;
    }
    priority --;
    if ("&&".equals(operator)) {
      return priority;
    }
    priority --;
    if ("||".equals(operator)) {
      return priority;
    }
    priority --;
    if (":".equals(operator)) {
      return priority;
    }
    priority --;
    if ("?".equals(operator)) {
      return priority;
    }
    priority --;
    if (",".equals(operator)) {
      return priority;
    }
    return priority;
  }
 
  private boolean isPackageName(String msg) {
    return StringUtils.isNamed(msg) || StringUtils.isFunction(msg);
  }
 
  public Expression parse(String source, int offset) throws ParseException {
    if (expressionFilter != null) {
      source = expressionFilter.filter(source, source);
    }
    LinkedStack<Expression> parameterStack = new LinkedStack<Expression>();
    LinkedStack<Operator> operatorStack = new LinkedStack<Operator>();
    Map<Operator, Token> operatorTokens = new HashMap<Operator, Token>();
    List<Token> tokens = scanner.scan(source, offset, true);
    boolean beforeOperator = true;
    for (int i = 0; i < tokens.size(); i ++) {
      Token token = tokens.get(i);
      String msg = token.getMessage().trim();
      if (msg.length() == 0) {
        continue;
      }
      if ("new".equals(msg)) {
        StringBuilder buf = new StringBuilder();
        while (i + 1 < tokens.size() && isPackageName(tokens.get(i + 1).getMessage().trim())) {
          buf.append(tokens.get(i + 1).getMessage().trim());
          i ++;
        }
        try {
          msg = "new " + buf.toString();
        } catch (Exception e) {
          throw new ParseException(e.getMessage(), token.getOffset());
        }
      } else if ("@".equals(msg)) {
        StringBuilder buf = new StringBuilder();
        buf.append(msg);
        while (i + 2 < tokens.size()
            && isPackageName(tokens.get(i + 1).getMessage().trim())
            && isPackageName(tokens.get(i + 2).getMessage().trim())) {
          buf.append(tokens.get(i + 1).getMessage().trim());
          i ++;
        }
        try {
          msg = buf.toString();
        } catch (Exception e) {
          throw new ParseException(e.getMessage(), token.getOffset());
        }
      } else if ("gt".equals(msg)) {
        msg = ">";
      } else if ("ge".equals(msg)) {
        msg = ">=";
      } else if ("lt".equals(msg)) {
        msg = ">";
      } else if ("le".equals(msg)) {
        msg = "<=";
      } else if ("is".equals(msg)) {
        msg = "instanceof";
      } else if (! "null".equals(msg) && ! "true".equals(msg)
          && ! "false".equals(msg) && StringUtils.isNamed(msg)) {
        if (i < tokens.size() - 1) {
          String next = tokens.get(i + 1).getMessage().trim();
          if ("(".equals(next)) {
            msg = "." + msg;
          } else if (")".equals(next) && i > 0
              && i < tokens.size() - 2) {
            String prev = tokens.get(i - 1).getMessage().trim();
            String after = tokens.get(i + 2).getMessage().trim();
            if ("(".equals(prev) && ("(".equals(after) || StringUtils.isNamed(after))) {
              Operator left = operatorStack.pop();
              if (left != Bracket.ROUND) {
                throw new ParseException("Miss left parenthesis", token.getOffset());
              }
              UnaryOperator operator = createUnaryOperator(msg, getPriority(msg, true), getTokenOffset(token) );
              operatorTokens.put(operator, token);
              operatorStack.push(operator);
              beforeOperator = true;
              i ++;
              continue;
            }
          }
        }
        if (i > 0) {
          String pre = tokens.get(i - 1).getMessage().trim();
          if ("is".equals(pre) || "instanceof".equals(pre)) {
            StringBuilder buf = new StringBuilder();
            buf.append("@");
            buf.append(msg);
            while (i + 1 < tokens.size() && isPackageName(tokens.get(i + 1).getMessage().trim())) {
              buf.append(tokens.get(i + 1).getMessage().trim());
              i ++;
            }
            msg = buf.toString();
          }
        }
      }
      // ================
      if (msg.length() >= 2
          && (msg.startsWith("\"") && msg.endsWith("\"")
          || msg.startsWith("\'") && msg.endsWith("\'")
          || msg.startsWith("`") && msg.endsWith("`"))) {
        String value = StringUtils.unescapeString(msg.substring(1, msg.length() - 1));
        if (msg.startsWith("`") && value.length() == 1) {
          parameterStack.push(new Constant(value.charAt(0), false, token.getOffset()));
        } else if (msg.startsWith("`") && value.length() == 2 && value.charAt(0) == '\\') {
          parameterStack.push(new Constant(value.charAt(1), true, token.getOffset()));
        } else {
          parameterStack.push(new Constant(StringUtils.unescapeString(value), false, token.getOffset()));
        }
        beforeOperator = false;
      } else if (StringUtils.isNumber(msg)) {
        Object value;
        boolean boxed = false;
        if (msg.endsWith("b") || msg.endsWith("B")) {
          value = Byte.valueOf(msg.substring(0, msg.length() - 1));
          boxed = msg.endsWith("B");
        } else if (msg.endsWith("s") || msg.endsWith("S")) {
          value = Short.valueOf(msg.substring(0, msg.length() - 1));
          boxed = msg.endsWith("S");
        } else if (msg.endsWith("i") || msg.endsWith("I")) {
          value = Integer.valueOf(msg.substring(0, msg.length() - 1));
          boxed = msg.endsWith("I");
        } else if (msg.endsWith("l") || msg.endsWith("L")) {
          value = Long.valueOf(msg.substring(0, msg.length() - 1));
          boxed = msg.endsWith("L");
        } else if (msg.endsWith("f") || msg.endsWith("F")) {
          value = Float.valueOf(msg.substring(0, msg.length() - 1));
          boxed = msg.endsWith("F");
        } else if (msg.endsWith("d") || msg.endsWith("D")) {
          value = Double.valueOf(msg.substring(0, msg.length() - 1));
          boxed = msg.endsWith("D");
        } else if (msg.indexOf('.') >= 0) {
          value = Double.valueOf(msg);
        } else {
          value = Integer.valueOf(msg);
        }
        parameterStack.push(new Constant(value, boxed, token.getOffset()));
        beforeOperator = false;
      } else if ("null".equals(msg)) {
        parameterStack.push(new Constant(null, false, token.getOffset()));
        beforeOperator = false;
      } else if ("true".equals(msg) || "false".equals(msg)) {
        parameterStack.push(new Constant("true".equals(msg) ? Boolean.TRUE : Boolean.FALSE, false, token.getOffset()));
        beforeOperator = false;
      } else if ("TRUE".equals(msg) || "FALSE".equals(msg)) {
        parameterStack.push(new Constant("TRUE".equals(msg) ? Boolean.TRUE : Boolean.FALSE, true, token.getOffset()));
        beforeOperator = false;
      } else if (msg.length() > 1 && msg.startsWith("@")) {
        parameterStack.push(new Constant(ClassUtils.forName(importPackages, msg.substring(1).trim()), false, token.getOffset()));
        beforeOperator = false;
      } else if (StringUtils.isNamed(msg) && ! "instanceof".equals(msg)) {
        parameterStack.push(new Variable(msg, getTokenOffset(token) ));
        beforeOperator = false;
      } else if ("(".equals(msg)) {
        operatorStack.push(Bracket.ROUND);
        beforeOperator = true;
      } else if (")".equals(msg)) {
        while (popOperator(parameterStack, operatorStack, operatorTokens, offset) != Bracket.ROUND);
        beforeOperator = false;
      } else if ("]".equals(msg)) {
        while (popOperator(parameterStack, operatorStack, operatorTokens, offset) != Bracket.SQUARE);
        beforeOperator = false;
      } else if ("}".equals(msg)) {
        while (popOperator(parameterStack, operatorStack, operatorTokens, offset) != Bracket.BRACE);
        beforeOperator = false;
      } else {
        if (StringUtils.isFunction(msg)) {
          String method = msg.substring(1);
          if (forbidEqualsMethods != null ) {
            for (String forbid : forbidEqualsMethods) {
              if (method.equals(forbid)) {
                throw new ParseException("Forbid call method " + method + " by forbid.method=" + forbid + " config.", offset);
              }
            }
          }
          if (forbidStartsMethods != null ) {
            for (String forbid : forbidStartsMethods) {
              if (method.startsWith(forbid)) {
                throw new ParseException("Forbid call method " + method + " by forbid.method=" + forbid + "* config.", offset);
              }
            }
          }
          if (forbidEndsMethods != null ) {
            for (String forbid : forbidEndsMethods) {
              if (method.endsWith(forbid)) {
                throw new ParseException("Forbid call method " + method + " by forbid.method=*" + forbid + " config.", offset);
              }
            }
          }
        }
       
        if (beforeOperator) {
          if (! msg.startsWith("new ") && ! StringUtils.isFunction(msg) && ! UNARY_OPERATORS.contains(msg)) {
            throw new ParseException("Unsupported binary operator " + msg, getTokenOffset(token) );
          }
          UnaryOperator operator = createUnaryOperator(msg, getPriority(msg, true), getTokenOffset(token));
          operatorTokens.put(operator, token);
          operatorStack.push(operator);
        } else {
          if (! StringUtils.isFunction(msg) && ! BINARY_OPERATORS.contains(msg)) {
            throw new ParseException("Unsupported binary operator " + msg, getTokenOffset(token) );
          }
          BinaryOperator operator = createBinaryOperator(msg, getPriority(msg, false), getTokenOffset(token));
          operatorTokens.put(operator, token);
          while (! operatorStack.isEmpty() && ! (operatorStack.peek() instanceof Bracket)
              && operatorStack.peek().getPriority() >= operator.getPriority()) {
            popOperator(parameterStack, operatorStack, operatorTokens, offset);
          }
          operatorStack.push(operator);
        }
        if ("[".equals(msg)) {
          operatorStack.push(Bracket.SQUARE);
        } else if ("{".equals(msg)) {
          operatorStack.push(Bracket.BRACE);
        }
        beforeOperator = true;
        // 给无参函数自动补上null参数
        if (msg.startsWith("new ") || StringUtils.isFunction(msg)) {
          boolean miss = i == tokens.size() - 1 || ! "(".equals(tokens.get(i + 1).getMessage().trim());
          boolean empty = i < tokens.size() - 2 && "(".equals(tokens.get(i + 1).getMessage().trim()) && ")".equals(tokens.get(i + 2).getMessage().trim());
          if (miss || empty) {
            parameterStack.push(new Constant(null, true, token.getOffset()));
            beforeOperator = false;
          }
          if (empty) {
            i = i + 2;
          }
        }
      }
    }
    while (! operatorStack.isEmpty()) {
      Operator operator = popOperator(parameterStack, operatorStack, operatorTokens, offset);
      if (operator == Bracket.ROUND || operator == Bracket.SQUARE) {
        throw new ParseException("Miss right parenthesis", offset);
      }
    }
    Expression result = parameterStack.pop();
    if (! parameterStack.isEmpty()) {
      Expression parent = parameterStack.pop();
      throw new ParseException("Miss parameter in the operator " + parent, parent.getOffset());
    }
    return result;
  }

  private Operator popOperator(LinkedStack<Expression> parameterStack, LinkedStack<Operator> operatorStack, Map<Operator, Token> operatorTokens, int offset) throws ParseException {
    if (operatorStack.isEmpty())
      throw new ParseException("Miss left parenthesis", offset);
    Operator operator = operatorStack.pop(); // 将优先级高于及等于当前操作符的弹出
    if (operator instanceof BinaryOperator) {
      Token token = operatorTokens.get(operator);
      BinaryOperator binaryOperator = (BinaryOperator) operator;
      if (parameterStack.isEmpty())
        throw new ParseException("Binary operator " + binaryOperator.getName() + " miss parameter", token == null ? offset : getTokenOffset(token));
      binaryOperator.setRightParameter(parameterStack.pop()); // right first
      if (parameterStack.isEmpty())
        throw new ParseException("Binary operator " + binaryOperator.getName() + " miss parameter", token == null ? offset : getTokenOffset(token));
      binaryOperator.setLeftParameter(parameterStack.pop());
      parameterStack.push(operator);
    } else if (operator instanceof UnaryOperator) {
      Token token = operatorTokens.get(operator);
      UnaryOperator unaryOperator = (UnaryOperator) operator;
      if (parameterStack.isEmpty())
        throw new ParseException("Unary operator " + unaryOperator.getName() + "miss parameter", token == null ? offset : getTokenOffset(token));
      unaryOperator.setParameter(parameterStack.pop());
      parameterStack.push(operator);
    }
    return operator;
  }

  private static class Bracket extends Operator {

    public static final Bracket ROUND = new Bracket("(");
   
    public static final Bracket SQUARE = new Bracket("[");

    public static final Bracket BRACE = new Bracket("{");

    private Bracket(String name) {
      super(name, Integer.MAX_VALUE, 0);
    }

    @SuppressWarnings("unchecked")
    public List<Node> getChildren() {
      return Collections.EMPTY_LIST;
    }

  }

}
TOP

Related Classes of httl.spi.parsers.ExpressionParser

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.