/**
 * 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 org.apache.hadoop.hbase.client;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.apache.hadoop.hbase.CompareOperator;
import org.apache.hadoop.hbase.HBaseClassTestRule;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.filter.BinaryComparator;
import org.apache.hadoop.hbase.filter.FamilyFilter;
import org.apache.hadoop.hbase.filter.FilterList;
import org.apache.hadoop.hbase.filter.QualifierFilter;
import org.apache.hadoop.hbase.filter.SingleColumnValueFilter;
import org.apache.hadoop.hbase.filter.TimestampsFilter;
import org.apache.hadoop.hbase.io.TimeRange;
import org.apache.hadoop.hbase.regionserver.NoSuchColumnFamilyException;
import org.apache.hadoop.hbase.testclassification.MediumTests;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.TestName;

@Category(MediumTests.class)
public class TestCheckAndMutate {

  @ClassRule
  public static final HBaseClassTestRule CLASS_RULE =
      HBaseClassTestRule.forClass(TestCheckAndMutate.class);

  private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
  private static final byte[] ROWKEY = Bytes.toBytes("12345");
  private static final byte[] ROWKEY2 = Bytes.toBytes("67890");
  private static final byte[] ROWKEY3 = Bytes.toBytes("abcde");
  private static final byte[] ROWKEY4 = Bytes.toBytes("fghij");
  private static final byte[] FAMILY = Bytes.toBytes("cf");

  @Rule
  public TestName name = new TestName();

  @BeforeClass
  public static void setUpBeforeClass() throws Exception {
    TEST_UTIL.startMiniCluster();
  }

  @AfterClass
  public static void tearDownAfterClass() throws Exception {
    TEST_UTIL.shutdownMiniCluster();
  }

  private Table createTable()
  throws IOException, InterruptedException {
    final TableName tableName = TableName.valueOf(name.getMethodName());
    Table table = TEST_UTIL.createTable(tableName, FAMILY);
    TEST_UTIL.waitTableAvailable(tableName.getName(), 5000);
    return table;
  }

  private void putOneRow(Table table) throws IOException {
    Put put = new Put(ROWKEY);
    put.addColumn(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("a"));
    put.addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("b"));
    put.addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c"));
    table.put(put);
  }

  private void getOneRowAndAssertAllExist(final Table table) throws IOException {
    Get get = new Get(ROWKEY);
    Result result = table.get(get);
    assertTrue("Column A value should be a",
      Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("A"))).equals("a"));
    assertTrue("Column B value should be b",
      Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))).equals("b"));
    assertTrue("Column C value should be c",
      Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("C"))).equals("c"));
  }

  private void getOneRowAndAssertAllButCExist(final Table table) throws IOException {
    Get get = new Get(ROWKEY);
    Result result = table.get(get);
    assertTrue("Column A value should be a",
      Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("A"))).equals("a"));
    assertTrue("Column B value should be b",
      Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))).equals("b"));
    assertTrue("Column C should not exist",
    result.getValue(FAMILY, Bytes.toBytes("C")) == null);
  }

  private RowMutations makeRowMutationsWithColumnCDeleted() throws IOException {
    RowMutations rm = new RowMutations(ROWKEY, 2);
    Put put = new Put(ROWKEY);
    put.addColumn(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("a"));
    put.addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("b"));
    rm.add(put);
    Delete del = new Delete(ROWKEY);
    del.addColumn(FAMILY, Bytes.toBytes("C"));
    rm.add(del);
    return rm;
  }

  private RowMutations getBogusRowMutations() throws IOException {
    Put p = new Put(ROWKEY);
    byte[] value = new byte[0];
    p.addColumn(new byte[]{'b', 'o', 'g', 'u', 's'}, new byte[]{'A'}, value);
    RowMutations rm = new RowMutations(ROWKEY);
    rm.add(p);
    return rm;
  }

  // Tests for old checkAndMutate API

  @Test
  @Deprecated
  public void testCheckAndMutateForOldApi() throws Throwable {
    try (Table table = createTable()) {
      // put one row
      putOneRow(table);
      // get row back and assert the values
      getOneRowAndAssertAllExist(table);

      // put the same row again with C column deleted
      RowMutations rm = makeRowMutationsWithColumnCDeleted();
      boolean res = table.checkAndMutate(ROWKEY, FAMILY).qualifier(Bytes.toBytes("A"))
          .ifEquals(Bytes.toBytes("a")).thenMutate(rm);
      assertTrue(res);

      // get row back and assert the values
      getOneRowAndAssertAllButCExist(table);

      // Test that we get a region level exception
      try {
        rm = getBogusRowMutations();
        table.checkAndMutate(ROWKEY, FAMILY).qualifier(Bytes.toBytes("A"))
          .ifEquals(Bytes.toBytes("a")).thenMutate(rm);
        fail("Expected NoSuchColumnFamilyException");
      } catch (NoSuchColumnFamilyException e) {
        // expected
      } catch (RetriesExhaustedException e) {
        assertThat(e.getCause(), instanceOf(NoSuchColumnFamilyException.class));
      }
    }
  }

  @Test
  @Deprecated
  public void testCheckAndMutateWithSingleFilterForOldApi() throws Throwable {
    try (Table table = createTable()) {
      // put one row
      putOneRow(table);
      // get row back and assert the values
      getOneRowAndAssertAllExist(table);

      // Put with success
      boolean ok = table.checkAndMutate(ROWKEY, new SingleColumnValueFilter(FAMILY,
          Bytes.toBytes("A"), CompareOperator.EQUAL, Bytes.toBytes("a")))
        .thenPut(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d")));
      assertTrue(ok);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));

      // Put with failure
      ok = table.checkAndMutate(ROWKEY, new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"),
          CompareOperator.EQUAL, Bytes.toBytes("b")))
        .thenPut(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("E"), Bytes.toBytes("e")));
      assertFalse(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("E"))));

      // Delete with success
      ok = table.checkAndMutate(ROWKEY, new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"),
          CompareOperator.EQUAL, Bytes.toBytes("a")))
        .thenDelete(new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("D")));
      assertTrue(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"))));

      // Mutate with success
      ok = table.checkAndMutate(ROWKEY, new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"),
          CompareOperator.EQUAL, Bytes.toBytes("b")))
        .thenMutate(new RowMutations(ROWKEY)
          .add((Mutation) new Put(ROWKEY)
            .addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d")))
          .add((Mutation) new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("A"))));
      assertTrue(ok);

      result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"))));
    }
  }

  @Test
  @Deprecated
  public void testCheckAndMutateWithMultipleFiltersForOldApi() throws Throwable {
    try (Table table = createTable()) {
      // put one row
      putOneRow(table);
      // get row back and assert the values
      getOneRowAndAssertAllExist(table);

      // Put with success
      boolean ok = table.checkAndMutate(ROWKEY, new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))
        ))
        .thenPut(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d")));
      assertTrue(ok);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));

      // Put with failure
      ok = table.checkAndMutate(ROWKEY, new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("c"))
        ))
        .thenPut(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("E"), Bytes.toBytes("e")));
      assertFalse(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("E"))));

      // Delete with success
      ok = table.checkAndMutate(ROWKEY, new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))
        ))
        .thenDelete(new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("D")));
      assertTrue(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"))));

      // Mutate with success
      ok = table.checkAndMutate(ROWKEY, new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))
        ))
        .thenMutate(new RowMutations(ROWKEY)
          .add((Mutation) new Put(ROWKEY)
            .addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d")))
          .add((Mutation) new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("A"))));
      assertTrue(ok);

      result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"))));
    }
  }

  @Test
  @Deprecated
  public void testCheckAndMutateWithTimestampFilterForOldApi() throws Throwable {
    try (Table table = createTable()) {
      // Put with specifying the timestamp
      table.put(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), 100, Bytes.toBytes("a")));

      // Put with success
      boolean ok = table.checkAndMutate(ROWKEY, new FilterList(
          new FamilyFilter(CompareOperator.EQUAL, new BinaryComparator(FAMILY)),
          new QualifierFilter(CompareOperator.EQUAL, new BinaryComparator(Bytes.toBytes("A"))),
          new TimestampsFilter(Collections.singletonList(100L))
        ))
        .thenPut(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("b")));
      assertTrue(ok);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("B")));
      assertEquals("b", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))));

      // Put with failure
      ok = table.checkAndMutate(ROWKEY, new FilterList(
          new FamilyFilter(CompareOperator.EQUAL, new BinaryComparator(FAMILY)),
          new QualifierFilter(CompareOperator.EQUAL, new BinaryComparator(Bytes.toBytes("A"))),
          new TimestampsFilter(Collections.singletonList(101L))
        ))
        .thenPut(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c")));
      assertFalse(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"))));
    }
  }

  @Test
  @Deprecated
  public void testCheckAndMutateWithFilterAndTimeRangeForOldApi() throws Throwable {
    try (Table table = createTable()) {
      // Put with specifying the timestamp
      table.put(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), 100, Bytes.toBytes("a")));

      // Put with success
      boolean ok = table.checkAndMutate(ROWKEY, new SingleColumnValueFilter(FAMILY,
          Bytes.toBytes("A"), CompareOperator.EQUAL, Bytes.toBytes("a")))
        .timeRange(TimeRange.between(0, 101))
        .thenPut(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("b")));
      assertTrue(ok);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("B")));
      assertEquals("b", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))));

      // Put with failure
      ok = table.checkAndMutate(ROWKEY, new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"),
          CompareOperator.EQUAL, Bytes.toBytes("a")))
        .timeRange(TimeRange.between(0, 100))
        .thenPut(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c")));
      assertFalse(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"))));
    }
  }

  @Test(expected = NullPointerException.class)
  @Deprecated
  public void testCheckAndMutateWithoutConditionForOldApi() throws Throwable {
    try (Table table = createTable()) {
      table.checkAndMutate(ROWKEY, FAMILY)
        .thenPut(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d")));
    }
  }

  // Tests for new CheckAndMutate API

  @Test
  public void testCheckAndMutate() throws Throwable {
    try (Table table = createTable()) {
      // put one row
      putOneRow(table);
      // get row back and assert the values
      getOneRowAndAssertAllExist(table);

      // put the same row again with C column deleted
      RowMutations rm = makeRowMutationsWithColumnCDeleted();
      boolean res = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifEquals(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("a"))
        .build(rm));
      assertTrue(res);

      // get row back and assert the values
      getOneRowAndAssertAllButCExist(table);

      // Test that we get a region level exception
      try {
        rm = getBogusRowMutations();
        table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
          .ifEquals(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("a"))
          .build(rm));
        fail("Expected NoSuchColumnFamilyException");
      } catch (NoSuchColumnFamilyException e) {
        // expected
      } catch (RetriesExhaustedException e) {
        assertThat(e.getCause(), instanceOf(NoSuchColumnFamilyException.class));
      }
    }
  }

  @Test
  public void testCheckAndMutateWithSingleFilter() throws Throwable {
    try (Table table = createTable()) {
      // put one row
      putOneRow(table);
      // get row back and assert the values
      getOneRowAndAssertAllExist(table);

      // Put with success
      boolean ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new SingleColumnValueFilter(FAMILY,
          Bytes.toBytes("A"), CompareOperator.EQUAL, Bytes.toBytes("a")))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d"))));
      assertTrue(ok);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));

      // Put with failure
      ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"),
        CompareOperator.EQUAL, Bytes.toBytes("b")))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("E"), Bytes.toBytes("e"))));
      assertFalse(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("E"))));

      // Delete with success
      ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"),
          CompareOperator.EQUAL, Bytes.toBytes("a")))
        .build(new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("D"))));
      assertTrue(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"))));

      // Mutate with success
      ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"),
          CompareOperator.EQUAL, Bytes.toBytes("b")))
        .build(new RowMutations(ROWKEY)
          .add((Mutation) new Put(ROWKEY)
            .addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d")))
          .add((Mutation) new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("A")))));
      assertTrue(ok);

      result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"))));
    }
  }

  @Test
  public void testCheckAndMutateWithMultipleFilters() throws Throwable {
    try (Table table = createTable()) {
      // put one row
      putOneRow(table);
      // get row back and assert the values
      getOneRowAndAssertAllExist(table);

      // Put with success
      boolean ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d"))));
      assertTrue(ok);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));

      // Put with failure
      ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("c"))))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("E"), Bytes.toBytes("e"))));
      assertFalse(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("E"))));

      // Delete with success
      ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
            new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
              Bytes.toBytes("a")),
            new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
              Bytes.toBytes("b"))))
        .build(new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("D"))));
      assertTrue(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"))));

      // Mutate with success
      ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))))
        .build(new RowMutations(ROWKEY)
          .add((Mutation) new Put(ROWKEY)
            .addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d")))
          .add((Mutation) new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("A")))));
      assertTrue(ok);

      result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"))));
    }
  }

  @Test
  public void testCheckAndMutateWithTimestampFilter() throws Throwable {
    try (Table table = createTable()) {
      // Put with specifying the timestamp
      table.put(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), 100, Bytes.toBytes("a")));

      // Put with success
      boolean ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
          new FamilyFilter(CompareOperator.EQUAL, new BinaryComparator(FAMILY)),
          new QualifierFilter(CompareOperator.EQUAL, new BinaryComparator(Bytes.toBytes("A"))),
          new TimestampsFilter(Collections.singletonList(100L))))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("b"))));
      assertTrue(ok);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("B")));
      assertEquals("b", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))));

      // Put with failure
      ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
          new FamilyFilter(CompareOperator.EQUAL, new BinaryComparator(FAMILY)),
          new QualifierFilter(CompareOperator.EQUAL, new BinaryComparator(Bytes.toBytes("A"))),
          new TimestampsFilter(Collections.singletonList(101L))))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c"))));
      assertFalse(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"))));
    }
  }

  @Test
  public void testCheckAndMutateWithFilterAndTimeRange() throws Throwable {
    try (Table table = createTable()) {
      // Put with specifying the timestamp
      table.put(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), 100, Bytes.toBytes("a")));

      // Put with success
      boolean ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new SingleColumnValueFilter(FAMILY,
          Bytes.toBytes("A"), CompareOperator.EQUAL, Bytes.toBytes("a")))
        .timeRange(TimeRange.between(0, 101))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("b"))));
      assertTrue(ok);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("B")));
      assertEquals("b", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))));

      // Put with failure
      ok = table.checkAndMutate(CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"),
          CompareOperator.EQUAL, Bytes.toBytes("a")))
        .timeRange(TimeRange.between(0, 100))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c"))));
      assertFalse(ok);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"))));
    }
  }

  @Test(expected = IllegalStateException.class)
  public void testCheckAndMutateBuilderWithoutCondition() {
    CheckAndMutate.newBuilder(ROWKEY)
      .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d")));
  }

  // Tests for batch version of checkAndMutate

  @Test
  public void testCheckAndMutateBatch() throws Throwable {
    try (Table table = createTable()) {
      table.put(Arrays.asList(
        new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("a")),
        new Put(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("b")),
        new Put(ROWKEY3).addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c")),
        new Put(ROWKEY4).addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d"))));

      // Test for Put
      CheckAndMutate checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY)
        .ifEquals(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("a"))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("e")));

      CheckAndMutate checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY2)
        .ifEquals(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("a"))
        .build(new Put(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("f")));

      boolean[] results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A")));
      assertEquals("e", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("A"))));

      result = table.get(new Get(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("B")));
      assertEquals("b", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))));

      // Test for Delete
      checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY)
        .ifEquals(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("e"))
        .build(new Delete(ROWKEY));

      checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY2)
        .ifEquals(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("a"))
        .build(new Delete(ROWKEY2));

      results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"))));

      result = table.get(new Get(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("B")));
      assertEquals("b", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))));

      // Test for RowMutations
      checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY3)
        .ifEquals(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c"))
        .build(new RowMutations(ROWKEY3)
          .add((Mutation) new Put(ROWKEY3)
            .addColumn(FAMILY, Bytes.toBytes("F"), Bytes.toBytes("f")))
          .add((Mutation) new Delete(ROWKEY3).addColumns(FAMILY, Bytes.toBytes("C"))));

      checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY4)
        .ifEquals(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("f"))
        .build(new RowMutations(ROWKEY4)
          .add((Mutation) new Put(ROWKEY4)
            .addColumn(FAMILY, Bytes.toBytes("F"), Bytes.toBytes("f")))
          .add((Mutation) new Delete(ROWKEY4).addColumns(FAMILY, Bytes.toBytes("D"))));

      results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      result = table.get(new Get(ROWKEY3));
      assertEquals("f", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("F"))));
      assertNull(result.getValue(FAMILY, Bytes.toBytes("D")));

      result = table.get(new Get(ROWKEY4));
      assertNull(result.getValue(FAMILY, Bytes.toBytes("F")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));
    }
  }

  @Test
  public void testCheckAndMutateBatch2() throws Throwable {
    try (Table table = createTable()) {
      table.put(Arrays.asList(
        new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("a")),
        new Put(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("b")),
        new Put(ROWKEY3).addColumn(FAMILY, Bytes.toBytes("C"), 100, Bytes.toBytes("c")),
        new Put(ROWKEY4).addColumn(FAMILY, Bytes.toBytes("D"), 100, Bytes.toBytes("d"))));

      // Test for ifNotExists()
      CheckAndMutate checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY)
        .ifNotExists(FAMILY, Bytes.toBytes("B"))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("e")));

      CheckAndMutate checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY2)
        .ifNotExists(FAMILY, Bytes.toBytes("B"))
        .build(new Put(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("f")));

      boolean[] results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A")));
      assertEquals("e", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("A"))));

      result = table.get(new Get(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("B")));
      assertEquals("b", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))));

      // Test for ifMatches()
      checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(FAMILY, Bytes.toBytes("A"), CompareOperator.NOT_EQUAL, Bytes.toBytes("a"))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("a")));

      checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY2)
        .ifMatches(FAMILY, Bytes.toBytes("B"), CompareOperator.GREATER, Bytes.toBytes("b"))
        .build(new Put(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("f")));

      results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A")));
      assertEquals("a", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("A"))));

      result = table.get(new Get(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("B")));
      assertEquals("b", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("B"))));

      // Test for timeRange()
      checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY3)
        .ifEquals(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c"))
        .timeRange(TimeRange.between(0, 101))
        .build(new Put(ROWKEY3).addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("e")));

      checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY4)
        .ifEquals(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d"))
        .timeRange(TimeRange.between(0, 100))
        .build(new Put(ROWKEY4).addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("f")));

      results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      result = table.get(new Get(ROWKEY3).addColumn(FAMILY, Bytes.toBytes("C")));
      assertEquals("e", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("C"))));

      result = table.get(new Get(ROWKEY4).addColumn(FAMILY, Bytes.toBytes("D")));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));
    }
  }

  @Test
  public void testCheckAndMutateBatchWithFilter() throws Throwable {
    try (Table table = createTable()) {
      table.put(Arrays.asList(
        new Put(ROWKEY)
          .addColumn(FAMILY, Bytes.toBytes("A"), Bytes.toBytes("a"))
          .addColumn(FAMILY, Bytes.toBytes("B"), Bytes.toBytes("b"))
          .addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c")),
        new Put(ROWKEY2)
          .addColumn(FAMILY, Bytes.toBytes("D"), Bytes.toBytes("d"))
          .addColumn(FAMILY, Bytes.toBytes("E"), Bytes.toBytes("e"))
          .addColumn(FAMILY, Bytes.toBytes("F"), Bytes.toBytes("f"))));

      // Test for Put
      CheckAndMutate checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("g")));

      CheckAndMutate checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY2)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("D"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("E"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))))
        .build(new Put(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("F"), Bytes.toBytes("h")));

      boolean[] results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C")));
      assertEquals("g", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("C"))));

      result = table.get(new Get(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("F")));
      assertEquals("f", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("F"))));

      // Test for Delete
      checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))))
        .build(new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("C")));

      checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY2)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("D"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("E"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))))
        .build(new Delete(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("F")));

      results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      assertFalse(table.exists(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"))));

      result = table.get(new Get(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("F")));
      assertEquals("f", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("F"))));

      // Test for RowMutations
      checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))))
        .build(new RowMutations(ROWKEY)
          .add((Mutation) new Put(ROWKEY)
            .addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c")))
          .add((Mutation) new Delete(ROWKEY).addColumns(FAMILY, Bytes.toBytes("A"))));

      checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY2)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("D"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("E"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))))
        .build(new RowMutations(ROWKEY2)
          .add((Mutation) new Put(ROWKEY2)
            .addColumn(FAMILY, Bytes.toBytes("F"), Bytes.toBytes("g")))
          .add((Mutation) new Delete(ROWKEY2).addColumns(FAMILY, Bytes.toBytes("D"))));

      results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      result = table.get(new Get(ROWKEY));
      assertNull(result.getValue(FAMILY, Bytes.toBytes("A")));
      assertEquals("c", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("C"))));

      result = table.get(new Get(ROWKEY2));
      assertEquals("d", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("D"))));
      assertEquals("f", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("F"))));
    }
  }

  @Test
  public void testCheckAndMutateBatchWithFilterAndTimeRange() throws Throwable {
    try (Table table = createTable()) {
      table.put(Arrays.asList(
        new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("A"), 100, Bytes.toBytes("a"))
          .addColumn(FAMILY, Bytes.toBytes("B"), 100, Bytes.toBytes("b"))
          .addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("c")),
        new Put(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("D"), 100, Bytes.toBytes("d"))
          .addColumn(FAMILY, Bytes.toBytes("E"), 100, Bytes.toBytes("e"))
          .addColumn(FAMILY, Bytes.toBytes("F"), Bytes.toBytes("f"))));

      CheckAndMutate checkAndMutate1 = CheckAndMutate.newBuilder(ROWKEY)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("A"), CompareOperator.EQUAL,
            Bytes.toBytes("a")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("B"), CompareOperator.EQUAL,
            Bytes.toBytes("b"))))
        .timeRange(TimeRange.between(0, 101))
        .build(new Put(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C"), Bytes.toBytes("g")));

      CheckAndMutate checkAndMutate2 = CheckAndMutate.newBuilder(ROWKEY2)
        .ifMatches(new FilterList(
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("D"), CompareOperator.EQUAL,
            Bytes.toBytes("d")),
          new SingleColumnValueFilter(FAMILY, Bytes.toBytes("E"), CompareOperator.EQUAL,
            Bytes.toBytes("e"))))
        .timeRange(TimeRange.between(0, 100))
        .build(new Put(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("F"), Bytes.toBytes("h")));

      boolean[] results = table.checkAndMutate(Arrays.asList(checkAndMutate1, checkAndMutate2));

      assertTrue(results[0]);
      assertFalse(results[1]);

      Result result = table.get(new Get(ROWKEY).addColumn(FAMILY, Bytes.toBytes("C")));
      assertEquals("g", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("C"))));

      result = table.get(new Get(ROWKEY2).addColumn(FAMILY, Bytes.toBytes("F")));
      assertEquals("f", Bytes.toString(result.getValue(FAMILY, Bytes.toBytes("F"))));
    }
  }
}