001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.rest;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertNotNull;
023import static org.junit.Assert.assertTrue;
024
025import com.fasterxml.jackson.core.JsonFactory;
026import com.fasterxml.jackson.core.JsonParser;
027import com.fasterxml.jackson.core.JsonToken;
028import com.fasterxml.jackson.databind.ObjectMapper;
029import java.io.DataInputStream;
030import java.io.EOFException;
031import java.io.IOException;
032import java.io.InputStream;
033import java.io.Serializable;
034import java.net.URLEncoder;
035import java.nio.charset.StandardCharsets;
036import java.util.ArrayList;
037import java.util.Collections;
038import java.util.List;
039import javax.xml.bind.JAXBContext;
040import javax.xml.bind.JAXBException;
041import javax.xml.bind.Unmarshaller;
042import javax.xml.bind.annotation.XmlAccessType;
043import javax.xml.bind.annotation.XmlAccessorType;
044import javax.xml.bind.annotation.XmlElement;
045import javax.xml.bind.annotation.XmlRootElement;
046import javax.xml.parsers.SAXParserFactory;
047import org.apache.hadoop.conf.Configuration;
048import org.apache.hadoop.hbase.Cell;
049import org.apache.hadoop.hbase.HBaseClassTestRule;
050import org.apache.hadoop.hbase.HBaseTestingUtil;
051import org.apache.hadoop.hbase.TableName;
052import org.apache.hadoop.hbase.client.Admin;
053import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
054import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
055import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
056import org.apache.hadoop.hbase.filter.Filter;
057import org.apache.hadoop.hbase.filter.ParseFilter;
058import org.apache.hadoop.hbase.filter.PrefixFilter;
059import org.apache.hadoop.hbase.rest.client.Client;
060import org.apache.hadoop.hbase.rest.client.Cluster;
061import org.apache.hadoop.hbase.rest.client.Response;
062import org.apache.hadoop.hbase.rest.model.CellModel;
063import org.apache.hadoop.hbase.rest.model.CellSetModel;
064import org.apache.hadoop.hbase.rest.model.RowModel;
065import org.apache.hadoop.hbase.testclassification.MediumTests;
066import org.apache.hadoop.hbase.testclassification.RestTests;
067import org.apache.hadoop.hbase.util.Bytes;
068import org.junit.AfterClass;
069import org.junit.BeforeClass;
070import org.junit.ClassRule;
071import org.junit.Test;
072import org.junit.experimental.categories.Category;
073import org.xml.sax.InputSource;
074import org.xml.sax.XMLReader;
075
076import org.apache.hbase.thirdparty.com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
077import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType;
078
079@Category({RestTests.class, MediumTests.class})
080public class TestTableScan {
081  @ClassRule
082  public static final HBaseClassTestRule CLASS_RULE =
083      HBaseClassTestRule.forClass(TestTableScan.class);
084
085  private static final TableName TABLE = TableName.valueOf("TestScanResource");
086  private static final String CFA = "a";
087  private static final String CFB = "b";
088  private static final String COLUMN_1 = CFA + ":1";
089  private static final String COLUMN_2 = CFB + ":2";
090  private static final String COLUMN_EMPTY = CFA + ":";
091  private static Client client;
092  private static int expectedRows1;
093  private static int expectedRows2;
094  private static int expectedRows3;
095  private static Configuration conf;
096
097  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
098  private static final HBaseRESTTestingUtility REST_TEST_UTIL =
099    new HBaseRESTTestingUtility();
100
101  @BeforeClass
102  public static void setUpBeforeClass() throws Exception {
103    conf = TEST_UTIL.getConfiguration();
104    conf.set(Constants.CUSTOM_FILTERS, "CustomFilter:" + CustomFilter.class.getName());
105    TEST_UTIL.startMiniCluster();
106    REST_TEST_UTIL.startServletContainer(conf);
107    client = new Client(new Cluster().add("localhost",
108      REST_TEST_UTIL.getServletPort()));
109    Admin admin = TEST_UTIL.getAdmin();
110    if (!admin.tableExists(TABLE)) {
111      TableDescriptorBuilder tableDescriptorBuilder =
112        TableDescriptorBuilder.newBuilder(TABLE);
113      ColumnFamilyDescriptor columnFamilyDescriptor =
114        ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFA)).build();
115      tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
116      columnFamilyDescriptor =
117        ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFB)).build();
118      tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
119      admin.createTable(tableDescriptorBuilder.build());
120      expectedRows1 = TestScannerResource.insertData(conf, TABLE, COLUMN_1, 1.0);
121      expectedRows2 = TestScannerResource.insertData(conf, TABLE, COLUMN_2, 0.5);
122      expectedRows3 = TestScannerResource.insertData(conf, TABLE, COLUMN_EMPTY, 1.0);
123    }
124  }
125
126  @AfterClass
127  public static void tearDownAfterClass() throws Exception {
128    TEST_UTIL.getAdmin().disableTable(TABLE);
129    TEST_UTIL.getAdmin().deleteTable(TABLE);
130    REST_TEST_UTIL.shutdownServletContainer();
131    TEST_UTIL.shutdownMiniCluster();
132  }
133
134  @Test
135  public void testSimpleScannerXML() throws IOException, JAXBException {
136    // Test scanning particular columns
137    StringBuilder builder = new StringBuilder();
138    builder.append("/*");
139    builder.append("?");
140    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
141    builder.append("&");
142    builder.append(Constants.SCAN_LIMIT + "=10");
143    Response response = client.get("/" + TABLE + builder.toString(),
144      Constants.MIMETYPE_XML);
145    assertEquals(200, response.getCode());
146    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
147    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
148    Unmarshaller ush = ctx.createUnmarshaller();
149    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
150    int count = TestScannerResource.countCellSet(model);
151    assertEquals(10, count);
152    checkRowsNotNull(model);
153
154    //Test with no limit.
155    builder = new StringBuilder();
156    builder.append("/*");
157    builder.append("?");
158    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
159    response = client.get("/" + TABLE + builder.toString(),
160      Constants.MIMETYPE_XML);
161    assertEquals(200, response.getCode());
162    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
163    model = (CellSetModel) ush.unmarshal(response.getStream());
164    count = TestScannerResource.countCellSet(model);
165    assertEquals(expectedRows1, count);
166    checkRowsNotNull(model);
167
168    //Test with start and end row.
169    builder = new StringBuilder();
170    builder.append("/*");
171    builder.append("?");
172    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
173    builder.append("&");
174    builder.append(Constants.SCAN_START_ROW + "=aaa");
175    builder.append("&");
176    builder.append(Constants.SCAN_END_ROW + "=aay");
177    response = client.get("/" + TABLE + builder.toString(),
178      Constants.MIMETYPE_XML);
179    assertEquals(200, response.getCode());
180    model = (CellSetModel) ush.unmarshal(response.getStream());
181    count = TestScannerResource.countCellSet(model);
182    RowModel startRow = model.getRows().get(0);
183    assertEquals("aaa", Bytes.toString(startRow.getKey()));
184    RowModel endRow = model.getRows().get(model.getRows().size() - 1);
185    assertEquals("aax", Bytes.toString(endRow.getKey()));
186    assertEquals(24, count);
187    checkRowsNotNull(model);
188
189    //Test with start row and limit.
190    builder = new StringBuilder();
191    builder.append("/*");
192    builder.append("?");
193    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
194    builder.append("&");
195    builder.append(Constants.SCAN_START_ROW + "=aaa");
196    builder.append("&");
197    builder.append(Constants.SCAN_LIMIT + "=15");
198    response = client.get("/" + TABLE + builder.toString(),
199      Constants.MIMETYPE_XML);
200    assertEquals(200, response.getCode());
201    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
202    model = (CellSetModel) ush.unmarshal(response.getStream());
203    startRow = model.getRows().get(0);
204    assertEquals("aaa", Bytes.toString(startRow.getKey()));
205    count = TestScannerResource.countCellSet(model);
206    assertEquals(15, count);
207    checkRowsNotNull(model);
208  }
209
210  @Test
211  public void testSimpleScannerJson() throws IOException {
212    // Test scanning particular columns with limit.
213    StringBuilder builder = new StringBuilder();
214    builder.append("/*");
215    builder.append("?");
216    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
217    builder.append("&");
218    builder.append(Constants.SCAN_LIMIT + "=2");
219    Response response = client.get("/" + TABLE + builder.toString(),
220      Constants.MIMETYPE_JSON);
221    assertEquals(200, response.getCode());
222    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
223    ObjectMapper mapper = new JacksonJaxbJsonProvider()
224        .locateMapper(CellSetModel.class, MediaType.APPLICATION_JSON_TYPE);
225    CellSetModel model = mapper.readValue(response.getStream(), CellSetModel.class);
226    int count = TestScannerResource.countCellSet(model);
227    assertEquals(2, count);
228    checkRowsNotNull(model);
229
230    //Test scanning with no limit.
231    builder = new StringBuilder();
232    builder.append("/*");
233    builder.append("?");
234    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_2);
235    response = client.get("/" + TABLE + builder.toString(),
236      Constants.MIMETYPE_JSON);
237    assertEquals(200, response.getCode());
238    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
239    model = mapper.readValue(response.getStream(), CellSetModel.class);
240    count = TestScannerResource.countCellSet(model);
241    assertEquals(expectedRows2, count);
242    checkRowsNotNull(model);
243
244    //Test with start row and end row.
245    builder = new StringBuilder();
246    builder.append("/*");
247    builder.append("?");
248    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
249    builder.append("&");
250    builder.append(Constants.SCAN_START_ROW + "=aaa");
251    builder.append("&");
252    builder.append(Constants.SCAN_END_ROW + "=aay");
253    response = client.get("/" + TABLE + builder.toString(),
254      Constants.MIMETYPE_JSON);
255    assertEquals(200, response.getCode());
256    model = mapper.readValue(response.getStream(), CellSetModel.class);
257    RowModel startRow = model.getRows().get(0);
258    assertEquals("aaa", Bytes.toString(startRow.getKey()));
259    RowModel endRow = model.getRows().get(model.getRows().size() - 1);
260    assertEquals("aax", Bytes.toString(endRow.getKey()));
261    count = TestScannerResource.countCellSet(model);
262    assertEquals(24, count);
263    checkRowsNotNull(model);
264  }
265
266  /**
267   * An example to scan using listener in unmarshaller for XML.
268   * @throws Exception the exception
269   */
270  @Test
271  public void testScanUsingListenerUnmarshallerXML() throws Exception {
272    StringBuilder builder = new StringBuilder();
273    builder.append("/*");
274    builder.append("?");
275    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
276    builder.append("&");
277    builder.append(Constants.SCAN_LIMIT + "=10");
278    Response response = client.get("/" + TABLE + builder.toString(),
279      Constants.MIMETYPE_XML);
280    assertEquals(200, response.getCode());
281    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
282    JAXBContext context = JAXBContext.newInstance(ClientSideCellSetModel.class, RowModel.class,
283      CellModel.class);
284    Unmarshaller unmarshaller = context.createUnmarshaller();
285
286    final ClientSideCellSetModel.Listener listener = new ClientSideCellSetModel.Listener() {
287      @Override
288      public void handleRowModel(ClientSideCellSetModel helper, RowModel row) {
289        assertTrue(row.getKey() != null);
290        assertTrue(row.getCells().size() > 0);
291      }
292    };
293
294    // install the callback on all ClientSideCellSetModel instances
295    unmarshaller.setListener(new Unmarshaller.Listener() {
296        @Override
297        public void beforeUnmarshal(Object target, Object parent) {
298          if (target instanceof ClientSideCellSetModel) {
299            ((ClientSideCellSetModel) target).setCellSetModelListener(listener);
300          }
301        }
302
303        @Override
304        public void afterUnmarshal(Object target, Object parent) {
305          if (target instanceof ClientSideCellSetModel) {
306            ((ClientSideCellSetModel) target).setCellSetModelListener(null);
307          }
308        }
309    });
310
311    // create a new XML parser
312    SAXParserFactory factory = SAXParserFactory.newInstance();
313    factory.setNamespaceAware(true);
314    XMLReader reader = factory.newSAXParser().getXMLReader();
315    reader.setContentHandler(unmarshaller.getUnmarshallerHandler());
316    assertFalse(ClientSideCellSetModel.listenerInvoked);
317    reader.parse(new InputSource(response.getStream()));
318    assertTrue(ClientSideCellSetModel.listenerInvoked);
319
320  }
321
322  @Test
323  public void testStreamingJSON() throws Exception {
324    //Test with start row and end row.
325    StringBuilder builder = new StringBuilder();
326    builder.append("/*");
327    builder.append("?");
328    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
329    builder.append("&");
330    builder.append(Constants.SCAN_START_ROW + "=aaa");
331    builder.append("&");
332    builder.append(Constants.SCAN_END_ROW + "=aay");
333    Response response = client.get("/" + TABLE + builder.toString(),
334      Constants.MIMETYPE_JSON);
335    assertEquals(200, response.getCode());
336
337    int count = 0;
338    ObjectMapper mapper = new JacksonJaxbJsonProvider()
339        .locateMapper(CellSetModel.class, MediaType.APPLICATION_JSON_TYPE);
340    JsonFactory jfactory = new JsonFactory(mapper);
341    JsonParser jParser = jfactory.createJsonParser(response.getStream());
342    boolean found = false;
343    while (jParser.nextToken() != JsonToken.END_OBJECT) {
344      if(jParser.getCurrentToken() == JsonToken.START_OBJECT && found) {
345        RowModel row = jParser.readValueAs(RowModel.class);
346        assertNotNull(row.getKey());
347        for (int i = 0; i < row.getCells().size(); i++) {
348          if (count == 0) {
349            assertEquals("aaa", Bytes.toString(row.getKey()));
350          }
351          if (count == 23) {
352            assertEquals("aax", Bytes.toString(row.getKey()));
353          }
354          count++;
355        }
356        jParser.skipChildren();
357      } else {
358        found = jParser.getCurrentToken() == JsonToken.START_ARRAY;
359      }
360    }
361    assertEquals(24, count);
362  }
363
364  @Test
365  public void testSimpleScannerProtobuf() throws Exception {
366    StringBuilder builder = new StringBuilder();
367    builder.append("/*");
368    builder.append("?");
369    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
370    builder.append("&");
371    builder.append(Constants.SCAN_LIMIT + "=15");
372    Response response = client.get("/" + TABLE + builder.toString(),
373      Constants.MIMETYPE_PROTOBUF);
374    assertEquals(200, response.getCode());
375    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
376    int rowCount = readProtobufStream(response.getStream());
377    assertEquals(15, rowCount);
378
379    //Test with start row and end row.
380    builder = new StringBuilder();
381    builder.append("/*");
382    builder.append("?");
383    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
384    builder.append("&");
385    builder.append(Constants.SCAN_START_ROW + "=aaa");
386    builder.append("&");
387    builder.append(Constants.SCAN_END_ROW + "=aay");
388    response = client.get("/" + TABLE + builder.toString(),
389      Constants.MIMETYPE_PROTOBUF);
390    assertEquals(200, response.getCode());
391    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
392    rowCount = readProtobufStream(response.getStream());
393    assertEquals(24, rowCount);
394  }
395
396  private void checkRowsNotNull(CellSetModel model) {
397    for (RowModel row: model.getRows()) {
398      assertTrue(row.getKey() != null);
399      assertTrue(row.getCells().size() > 0);
400    }
401  }
402
403  /**
404   * Read protobuf stream.
405   * @param inputStream the input stream
406   * @return The number of rows in the cell set model.
407   * @throws IOException Signals that an I/O exception has occurred.
408   */
409  public int readProtobufStream(InputStream inputStream) throws IOException{
410    DataInputStream stream = new DataInputStream(inputStream);
411    CellSetModel model = null;
412    int rowCount = 0;
413    try {
414      while (true) {
415        byte[] lengthBytes = new byte[2];
416        int readBytes = stream.read(lengthBytes);
417        if (readBytes == -1) {
418          break;
419        }
420        assertEquals(2, readBytes);
421        int length = Bytes.toShort(lengthBytes);
422        byte[] cellset = new byte[length];
423        stream.read(cellset);
424        model = new CellSetModel();
425        model.getObjectFromMessage(cellset);
426        checkRowsNotNull(model);
427        rowCount = rowCount + TestScannerResource.countCellSet(model);
428      }
429    } catch (EOFException exp) {
430      exp.printStackTrace();
431    } finally {
432      stream.close();
433    }
434    return rowCount;
435  }
436
437  @Test
438  public void testScanningUnknownColumnJson() throws IOException {
439    // Test scanning particular columns with limit.
440    StringBuilder builder = new StringBuilder();
441    builder.append("/*");
442    builder.append("?");
443    builder.append(Constants.SCAN_COLUMN + "=a:test");
444    Response response = client.get("/" + TABLE  + builder.toString(),
445      Constants.MIMETYPE_JSON);
446    assertEquals(200, response.getCode());
447    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
448    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
449      MediaType.APPLICATION_JSON_TYPE);
450    CellSetModel model = mapper.readValue(response.getStream(), CellSetModel.class);
451    int count = TestScannerResource.countCellSet(model);
452    assertEquals(0, count);
453  }
454
455  @Test
456  public void testSimpleFilter() throws IOException, JAXBException {
457    StringBuilder builder = new StringBuilder();
458    builder.append("/*");
459    builder.append("?");
460    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
461    builder.append("&");
462    builder.append(Constants.SCAN_START_ROW + "=aaa");
463    builder.append("&");
464    builder.append(Constants.SCAN_END_ROW + "=aay");
465    builder.append("&");
466    builder.append(Constants.SCAN_FILTER + "=" + URLEncoder.encode("PrefixFilter('aab')", "UTF-8"));
467    Response response =
468        client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
469    assertEquals(200, response.getCode());
470    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
471    Unmarshaller ush = ctx.createUnmarshaller();
472    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
473    int count = TestScannerResource.countCellSet(model);
474    assertEquals(1, count);
475    assertEquals("aab",
476        new String(model.getRows().get(0).getCells().get(0).getValue(), StandardCharsets.UTF_8));
477  }
478
479  @Test
480  public void testQualifierAndPrefixFilters() throws IOException, JAXBException {
481    StringBuilder builder = new StringBuilder();
482    builder.append("/abc*");
483    builder.append("?");
484    builder.append(Constants.SCAN_FILTER + "="
485        + URLEncoder.encode("QualifierFilter(=,'binary:1')", "UTF-8"));
486    Response response =
487        client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
488    assertEquals(200, response.getCode());
489    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
490    Unmarshaller ush = ctx.createUnmarshaller();
491    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
492    int count = TestScannerResource.countCellSet(model);
493    assertEquals(1, count);
494    assertEquals("abc",
495        new String(model.getRows().get(0).getCells().get(0).getValue(), StandardCharsets.UTF_8));
496  }
497
498  @Test
499  public void testCompoundFilter() throws IOException, JAXBException {
500    StringBuilder builder = new StringBuilder();
501    builder.append("/*");
502    builder.append("?");
503    builder.append(Constants.SCAN_FILTER + "="
504        + URLEncoder.encode("PrefixFilter('abc') AND QualifierFilter(=,'binary:1')", "UTF-8"));
505    Response response =
506        client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
507    assertEquals(200, response.getCode());
508    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
509    Unmarshaller ush = ctx.createUnmarshaller();
510    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
511    int count = TestScannerResource.countCellSet(model);
512    assertEquals(1, count);
513    assertEquals("abc",
514        new String(model.getRows().get(0).getCells().get(0).getValue(), StandardCharsets.UTF_8));
515  }
516
517  @Test
518  public void testCustomFilter() throws IOException, JAXBException {
519    StringBuilder builder = new StringBuilder();
520    builder.append("/a*");
521    builder.append("?");
522    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
523    builder.append("&");
524    builder.append(Constants.SCAN_FILTER + "=" + URLEncoder.encode("CustomFilter('abc')", "UTF-8"));
525    Response response =
526        client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
527    assertEquals(200, response.getCode());
528    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
529    Unmarshaller ush = ctx.createUnmarshaller();
530    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
531    int count = TestScannerResource.countCellSet(model);
532    assertEquals(1, count);
533    assertEquals("abc",
534        new String(model.getRows().get(0).getCells().get(0).getValue(), StandardCharsets.UTF_8));
535  }
536
537  @Test
538  public void testNegativeCustomFilter() throws IOException, JAXBException {
539    StringBuilder builder = new StringBuilder();
540    builder.append("/b*");
541    builder.append("?");
542    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
543    builder.append("&");
544    builder.append(Constants.SCAN_FILTER + "=" + URLEncoder.encode("CustomFilter('abc')", "UTF-8"));
545    Response response =
546        client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
547    assertEquals(200, response.getCode());
548    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
549    Unmarshaller ush = ctx.createUnmarshaller();
550    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
551    int count = TestScannerResource.countCellSet(model);
552    // Should return no rows as the filters conflict
553    assertEquals(0, count);
554  }
555
556  @Test
557  public void testReversed() throws IOException, JAXBException {
558    StringBuilder builder = new StringBuilder();
559    builder.append("/*");
560    builder.append("?");
561    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
562    builder.append("&");
563    builder.append(Constants.SCAN_START_ROW + "=aaa");
564    builder.append("&");
565    builder.append(Constants.SCAN_END_ROW + "=aay");
566    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
567    assertEquals(200, response.getCode());
568    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
569    Unmarshaller ush = ctx.createUnmarshaller();
570    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
571    int count = TestScannerResource.countCellSet(model);
572    assertEquals(24, count);
573    List<RowModel> rowModels = model.getRows().subList(1, count);
574
575    //reversed
576    builder = new StringBuilder();
577    builder.append("/*");
578    builder.append("?");
579    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
580    builder.append("&");
581    builder.append(Constants.SCAN_START_ROW + "=aay");
582    builder.append("&");
583    builder.append(Constants.SCAN_END_ROW + "=aaa");
584    builder.append("&");
585    builder.append(Constants.SCAN_REVERSED + "=true");
586    response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
587    assertEquals(200, response.getCode());
588    model = (CellSetModel) ush.unmarshal(response.getStream());
589    count = TestScannerResource.countCellSet(model);
590    assertEquals(24, count);
591    List<RowModel> reversedRowModels = model.getRows().subList(1, count);
592
593    Collections.reverse(reversedRowModels);
594    assertEquals(rowModels.size(), reversedRowModels.size());
595    for (int i = 0; i < rowModels.size(); i++) {
596      RowModel rowModel = rowModels.get(i);
597      RowModel reversedRowModel = reversedRowModels.get(i);
598
599      assertEquals(new String(rowModel.getKey(), StandardCharsets.UTF_8),
600          new String(reversedRowModel.getKey(), StandardCharsets.UTF_8));
601      assertEquals(new String(rowModel.getCells().get(0).getValue(), StandardCharsets.UTF_8),
602          new String(reversedRowModel.getCells().get(0).getValue(), StandardCharsets.UTF_8));
603    }
604  }
605
606  @Test
607  public void testColumnWithEmptyQualifier() throws IOException {
608    // Test scanning with empty qualifier
609    StringBuilder builder = new StringBuilder();
610    builder.append("/*");
611    builder.append("?");
612    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_EMPTY);
613    Response response = client.get("/" + TABLE + builder.toString(),
614        Constants.MIMETYPE_JSON);
615    assertEquals(200, response.getCode());
616    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
617    ObjectMapper mapper = new JacksonJaxbJsonProvider()
618        .locateMapper(CellSetModel.class, MediaType.APPLICATION_JSON_TYPE);
619    CellSetModel model = mapper.readValue(response.getStream(), CellSetModel.class);
620    int count = TestScannerResource.countCellSet(model);
621    assertEquals(expectedRows3, count);
622    checkRowsNotNull(model);
623    RowModel startRow = model.getRows().get(0);
624    assertEquals("aaa", Bytes.toString(startRow.getKey()));
625    assertEquals(1, startRow.getCells().size());
626
627    // Test scanning with empty qualifier and normal qualifier
628    builder = new StringBuilder();
629    builder.append("/*");
630    builder.append("?");
631    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
632    builder.append("&");
633    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_EMPTY);
634    response = client.get("/" + TABLE + builder.toString(),
635        Constants.MIMETYPE_JSON);
636    assertEquals(200, response.getCode());
637    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
638    mapper = new JacksonJaxbJsonProvider()
639        .locateMapper(CellSetModel.class, MediaType.APPLICATION_JSON_TYPE);
640    model = mapper.readValue(response.getStream(), CellSetModel.class);
641    count = TestScannerResource.countCellSet(model);
642    assertEquals(expectedRows1 + expectedRows3, count);
643    checkRowsNotNull(model);
644  }
645
646  public static class CustomFilter extends PrefixFilter {
647    private byte[] key = null;
648
649    public CustomFilter(byte[] key) {
650      super(key);
651    }
652
653    @Override
654    public boolean filterRowKey(Cell cell) {
655      int cmp = Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(),
656              this.key, 0, this.key.length);
657      return cmp != 0;
658    }
659
660    public static Filter createFilterFromArguments(ArrayList<byte[]> filterArguments) {
661      byte[] prefix = ParseFilter.removeQuotesFromByteArray(filterArguments.get(0));
662      return new CustomFilter(prefix);
663    }
664  }
665
666  /**
667   * The Class ClientSideCellSetModel which mimics cell set model, and contains listener to perform
668   * user defined operations on the row model.
669   */
670  @XmlRootElement(name = "CellSet")
671  @XmlAccessorType(XmlAccessType.FIELD)
672  public static class ClientSideCellSetModel implements Serializable {
673    private static final long serialVersionUID = 1L;
674
675    /**
676     * This list is not a real list; instead it will notify a listener whenever JAXB has
677     * unmarshalled the next row.
678     */
679    @XmlElement(name="Row")
680    private List<RowModel> row;
681
682    static boolean listenerInvoked = false;
683
684    /**
685     * Install a listener for row model on this object. If l is null, the listener
686     * is removed again.
687     */
688    public void setCellSetModelListener(final Listener l) {
689      row = (l == null) ? null : new ArrayList<RowModel>() {
690        private static final long serialVersionUID = 1L;
691
692        @Override
693        public boolean add(RowModel o) {
694          l.handleRowModel(ClientSideCellSetModel.this, o);
695          listenerInvoked = true;
696          return false;
697        }
698      };
699    }
700
701    /**
702     * This listener is invoked every time a new row model is unmarshalled.
703     */
704    public interface Listener {
705      void handleRowModel(ClientSideCellSetModel helper, RowModel rowModel);
706    }
707  }
708}