/** Parse the query string. */
  public ZimbraQuery(OperationContext octxt, SoapProtocol proto, Mailbox mbox, SearchParams params)
      throws ServiceException {
    this.octxt = octxt;
    this.protocol = proto;
    this.params = params;
    this.mailbox = mbox;

    // Parse the text using the JavaCC parser.
    try {
      QueryParser parser = new QueryParser(mbox, mbox.index.getAnalyzer());
      parser.setDefaultField(params.getDefaultField());
      parser.setTypes(params.getTypes());
      parser.setTimeZone(params.getTimeZone());
      parser.setLocale(params.getLocale());
      parser.setQuick(params.isQuick());
      clauses = parser.parse(params.getQueryString());

      if (parser.getSortBy() != null) {
        SortBy sort = SortBy.of(parser.getSortBy());
        if (sort == null) {
          throw ServiceException.PARSE_ERROR("INVALID_SORTBY: " + sort, null);
        }
        params.setSortBy(sort);
      }
    } catch (Error e) {
      throw ServiceException.PARSE_ERROR("PARSER_ERROR", e);
    }

    ZimbraLog.search.debug("%s,types=%s,sort=%s", this, params.getTypes(), params.getSortBy());

    // Build a parse tree and push all the "NOT's" down to the bottom level.
    // This is because we cannot invert result sets.
    parseTree = ParseTree.build(clauses).simplify();
    parseTree.pushNotsDown();

    // Check sort compatibility.
    switch (params.getSortBy().getKey()) {
      case RCPT:
      case ATTACHMENT:
      case FLAG:
      case PRIORITY:
        // We don't store these in Lucene.
        if (hasTextOperation()) {
          throw ServiceException.INVALID_REQUEST(
              "Sort '" + params.getSortBy().name() + "' can't be used with text query.", null);
        }
        break;
      default:
        break;
    }

    SearchParams.Cursor cursor = params.getCursor();
    if (cursor != null) {
      // Check cursor compatibility
      if (params.getCursor().isIncludeOffset() && hasTextOperation()) {
        throw ServiceException.INVALID_REQUEST(
            "cursor.includeOffset can't be used with text query.", null);
      }
      // Supplement sortValue
      if (cursor.getSortValue() == null) {
        ZimbraLog.search.debug(
            "Supplementing sortValue sort=%s,id=%s", params.getSortBy(), cursor.getItemId());
        try {
          MailItem item =
              mailbox.getItemById(null, cursor.getItemId().getId(), MailItem.Type.UNKNOWN);
          switch (params.getSortBy().getKey()) {
            case NAME:
              cursor.setSortValue(item.getName());
              break;
            case RCPT:
              cursor.setSortValue(item.getSortRecipients());
              break;
            case SENDER:
              cursor.setSortValue(item.getSortSender());
              break;
            case SIZE:
              cursor.setSortValue(String.valueOf(item.getSize()));
              break;
            case SUBJECT:
              cursor.setSortValue(item.getSortSubject());
              break;
            case DATE:
            default:
              cursor.setSortValue(String.valueOf(item.getDate()));
              break;
          }
        } catch (NoSuchItemException e) {
          params.setCursor(null); // clear cursor
        }
      }
    }
  }