ArgrepParams.java

package redis.clients.jedis.params;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import redis.clients.jedis.CommandArguments;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.Protocol.Keyword;
import redis.clients.jedis.annots.Experimental;

/**
 * Arguments for the {@code ARGREP} command. Every instance carries the mandatory {@code start end}
 * bounds of the search range; instances are obtained through one of the static factories
 * ({@link #unbounded()}, {@link #range(long, long)}, {@link #from(long)}, {@link #to(long)}) and
 * may be flipped with {@link #reversed()} to traverse in descending index order. Predicates,
 * combinator ({@code AND}/{@code OR}), {@code LIMIT} and {@code NOCASE} are appended via fluent
 * setters in the order required by the wire protocol. The {@code WITHVALUES} flag is not exposed
 * here: use {@code argrepWithValues} to request index/value pairs.
 */
@Experimental
public class ArgrepParams implements IParams {

  private static final byte[] MIN = new byte[] { '-' };
  private static final byte[] MAX = new byte[] { '+' };

  private enum PredicateType {
    EXACT(Keyword.EXACT), MATCH(Keyword.MATCH), GLOB(Keyword.GLOB), RE(Keyword.RE);

    final Keyword keyword;

    PredicateType(Keyword keyword) {
      this.keyword = keyword;
    }
  }

  private static final class Predicate {
    final PredicateType type;
    final byte[] value;

    Predicate(PredicateType type, byte[] value) {
      this.type = type;
      this.value = value;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      Predicate that = (Predicate) o;
      return type == that.type && Arrays.equals(value, that.value);
    }

    @Override
    public int hashCode() {
      return 31 * Objects.hashCode(type) + Arrays.hashCode(value);
    }
  }

  private byte[] startRaw;
  private byte[] endRaw;
  private final List<Predicate> predicates = new ArrayList<>();
  private Keyword combinator;
  private Long limit;
  private boolean nocase;

  private ArgrepParams() {
  }

  /**
   * Search the entire array (wire bounds {@code - +}).
   * @return a new {@link ArgrepParams} with start={@code -}, end={@code +}
   */
  public static ArgrepParams unbounded() {
    ArgrepParams p = new ArgrepParams();
    p.startRaw = MIN;
    p.endRaw = MAX;
    return p;
  }

  /**
   * Search the inclusive index range {@code [start, end]}.
   * @param start the zero-based start index (inclusive)
   * @param end the zero-based end index (inclusive)
   * @return a new {@link ArgrepParams} with concrete numeric bounds
   */
  public static ArgrepParams range(long start, long end) {
    ArgrepParams p = new ArgrepParams();
    p.startRaw = Protocol.toByteArray(start);
    p.endRaw = Protocol.toByteArray(end);
    return p;
  }

  /**
   * Search from a concrete start index to the end of the array (wire end {@code +}).
   * @param start the zero-based start index (inclusive)
   * @return a new {@link ArgrepParams} with start=numeric, end={@code +}
   */
  public static ArgrepParams from(long start) {
    ArgrepParams p = new ArgrepParams();
    p.startRaw = Protocol.toByteArray(start);
    p.endRaw = MAX;
    return p;
  }

  /**
   * Search from the start of the array to a concrete end index (wire start {@code -}).
   * @param end the zero-based end index (inclusive)
   * @return a new {@link ArgrepParams} with start={@code -}, end=numeric
   */
  public static ArgrepParams to(long end) {
    ArgrepParams p = new ArgrepParams();
    p.startRaw = MIN;
    p.endRaw = Protocol.toByteArray(end);
    return p;
  }

  /**
   * Swap the start and end bounds. Because the server interprets {@code start > end} as a
   * descending traversal, this returns matches in reverse index order.
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams reversed() {
    byte[] tmp = this.startRaw;
    this.startRaw = this.endRaw;
    this.endRaw = tmp;
    return this;
  }

  /**
   * Add an {@code EXACT} predicate: matches elements whose value equals the given string exactly.
   * @param value the literal value to match against
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams exact(String value) {
    predicates.add(new Predicate(PredicateType.EXACT, encode(value)));
    return this;
  }

  /**
   * Add an {@code EXACT} predicate: matches elements whose value equals the given bytes exactly.
   * @param value the literal value to match against
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams exact(byte[] value) {
    predicates.add(new Predicate(PredicateType.EXACT, value));
    return this;
  }

  /**
   * Add a {@code MATCH} predicate: matches elements that contain the given string as a substring.
   * @param value the substring to search for
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams match(String value) {
    predicates.add(new Predicate(PredicateType.MATCH, encode(value)));
    return this;
  }

  /**
   * Add a {@code MATCH} predicate: matches elements that contain the given bytes as a substring.
   * @param value the substring to search for
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams match(byte[] value) {
    predicates.add(new Predicate(PredicateType.MATCH, value));
    return this;
  }

  /**
   * Add a {@code GLOB} predicate: matches elements against a glob-style pattern (supports
   * {@code *}, {@code ?} and {@code [...]}).
   * @param pattern the glob pattern
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams glob(String pattern) {
    predicates.add(new Predicate(PredicateType.GLOB, encode(pattern)));
    return this;
  }

  /**
   * Add a {@code GLOB} predicate: matches elements against a glob-style pattern (supports
   * {@code *}, {@code ?} and {@code [...]}).
   * @param pattern the glob pattern as raw bytes
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams glob(byte[] pattern) {
    predicates.add(new Predicate(PredicateType.GLOB, pattern));
    return this;
  }

  /**
   * Add a {@code RE} predicate: matches elements against a regular expression.
   * @param pattern the regular expression
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams re(String pattern) {
    predicates.add(new Predicate(PredicateType.RE, encode(pattern)));
    return this;
  }

  /**
   * Add a {@code RE} predicate: matches elements against a regular expression.
   * @param pattern the regular expression as raw bytes
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams re(byte[] pattern) {
    predicates.add(new Predicate(PredicateType.RE, pattern));
    return this;
  }

  /**
   * Combine multiple predicates with logical AND.
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams and() {
    this.combinator = Keyword.AND;
    return this;
  }

  /**
   * Combine multiple predicates with logical OR (the default if neither {@link #and()} nor
   * {@link #or()} is set).
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams or() {
    this.combinator = Keyword.OR;
    return this;
  }

  /**
   * Cap the number of matches returned.
   * @param limit the maximum number of matches to return
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams limit(long limit) {
    this.limit = limit;
    return this;
  }

  /**
   * Perform all predicate comparisons case-insensitively.
   * @return this {@link ArgrepParams}
   */
  public ArgrepParams nocase() {
    this.nocase = true;
    return this;
  }

  @Override
  public void addParams(CommandArguments args) {
    if (startRaw == null || endRaw == null) {
      throw new IllegalStateException(
          "ArgrepParams must be created via unbounded(), range(), from() or to()");
    }
    args.add(startRaw).add(endRaw);
    for (Predicate p : predicates) {
      args.add(p.type.keyword).add(p.value);
    }
    if (combinator != null) {
      args.add(combinator);
    }
    if (limit != null) {
      args.add(Keyword.LIMIT).add(limit);
    }
    if (nocase) {
      args.add(Keyword.NOCASE);
    }
  }

  private static byte[] encode(String s) {
    return redis.clients.jedis.util.SafeEncoder.encode(s);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    ArgrepParams that = (ArgrepParams) o;
    return nocase == that.nocase && Arrays.equals(startRaw, that.startRaw)
        && Arrays.equals(endRaw, that.endRaw) && Objects.equals(combinator, that.combinator)
        && Objects.equals(limit, that.limit) && Objects.equals(predicates, that.predicates);
  }

  @Override
  public int hashCode() {
    int result = Objects.hash(predicates, combinator, limit, nocase);
    result = 31 * result + Arrays.hashCode(startRaw);
    result = 31 * result + Arrays.hashCode(endRaw);
    return result;
  }
}