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