private void parseExpression(String expr) throws ParseException {
    StringNode currStringNode = null;
    // We cheat a little and start an artificial
    // group right away. It makes finishing easier.
    pushOpp(null);
    ExpressionTokenizer et = new ExpressionTokenizer(expr);
    while (et.hasMoreTokens()) {
      int token = et.nextToken();
      if (token != ExpressionTokenizer.TOKEN_STRING) {
        currStringNode = null;
      }
      switch (token) {
        case ExpressionTokenizer.TOKEN_STRING -> {
          if (currStringNode == null) {
            currStringNode = new StringNode(et.getTokenValue());
            nodeStack.add(0, currStringNode);
          }
          else {
            // Add to the existing
            currStringNode.value.append(" ");
            currStringNode.value.append(et.getTokenValue());
          }
        }
        case ExpressionTokenizer.TOKEN_AND -> pushOpp(new AndNode());
        case ExpressionTokenizer.TOKEN_OR -> pushOpp(new OrNode());
        case ExpressionTokenizer.TOKEN_NOT -> pushOpp(new NotNode());
        case ExpressionTokenizer.TOKEN_EQ -> pushOpp(new EqualNode());
        case ExpressionTokenizer.TOKEN_NOT_EQ -> {
          pushOpp(new NotNode());
          // Sneak the regular node in. The NOT will
          // be resolved when the next opp comes along.
          oppStack.add(0, new EqualNode());
        }
        case ExpressionTokenizer.TOKEN_RBRACE ->
          // Closeout the current group
          resolveGroup();
        case ExpressionTokenizer.TOKEN_LBRACE ->
          // Push a group marker
          pushOpp(null);
        case ExpressionTokenizer.TOKEN_GE -> {
          pushOpp(new NotNode());
          // Similar strategy to NOT_EQ above, except this
          // is NOT less than
          oppStack.add(0, new LessThanNode());
        }
        case ExpressionTokenizer.TOKEN_LE -> {
          pushOpp(new NotNode());
          // Similar strategy to NOT_EQ above, except this
          // is NOT greater than
          oppStack.add(0, new GreaterThanNode());
        }
        case ExpressionTokenizer.TOKEN_GT -> pushOpp(new GreaterThanNode());
        case ExpressionTokenizer.TOKEN_LT -> pushOpp(new LessThanNode());
        case ExpressionTokenizer.TOKEN_END -> { }
      }
    }
    // Finish off the rest of the uopps
    resolveGroup();
    if (nodeStack.isEmpty()) {
      throw new ParseException("No nodes created.", et.getIndex());
    }
    if (nodeStack.size() > 1) {
      throw new ParseException("Extra nodes created.", et.getIndex());
    }
    if (!oppStack.isEmpty()) {
      throw new ParseException("Unused opp nodes exist.", et.getIndex());
    }
    root = nodeStack.get(0);
  }
private void parseExpression(String expr)