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 = 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      TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TABLE);
110      ColumnFamilyDescriptor columnFamilyDescriptor =
111        ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFA)).build();
112      tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
113      columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFB)).build();
114      tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
115      admin.createTable(tableDescriptorBuilder.build());
116      expectedRows1 = TestScannerResource.insertData(conf, TABLE, COLUMN_1, 1.0);
117      expectedRows2 = TestScannerResource.insertData(conf, TABLE, COLUMN_2, 0.5);
118      expectedRows3 = TestScannerResource.insertData(conf, TABLE, COLUMN_EMPTY, 1.0);
119    }
120  }
121
122  @AfterClass
123  public static void tearDownAfterClass() throws Exception {
124    TEST_UTIL.getAdmin().disableTable(TABLE);
125    TEST_UTIL.getAdmin().deleteTable(TABLE);
126    REST_TEST_UTIL.shutdownServletContainer();
127    TEST_UTIL.shutdownMiniCluster();
128  }
129
130  @Test
131  public void testSimpleScannerXML() throws IOException, JAXBException {
132    // Test scanning particular columns
133    StringBuilder builder = new StringBuilder();
134    builder.append("/*");
135    builder.append("?");
136    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
137    builder.append("&");
138    builder.append(Constants.SCAN_LIMIT + "=10");
139    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
140    assertEquals(200, response.getCode());
141    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
142    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
143    Unmarshaller ush = ctx.createUnmarshaller();
144    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
145    int count = TestScannerResource.countCellSet(model);
146    assertEquals(10, count);
147    checkRowsNotNull(model);
148
149    // Test with no limit.
150    builder = new StringBuilder();
151    builder.append("/*");
152    builder.append("?");
153    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
154    response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
155    assertEquals(200, response.getCode());
156    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
157    model = (CellSetModel) ush.unmarshal(response.getStream());
158    count = TestScannerResource.countCellSet(model);
159    assertEquals(expectedRows1, count);
160    checkRowsNotNull(model);
161
162    // Test with start and end row.
163    builder = new StringBuilder();
164    builder.append("/*");
165    builder.append("?");
166    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
167    builder.append("&");
168    builder.append(Constants.SCAN_START_ROW + "=aaa");
169    builder.append("&");
170    builder.append(Constants.SCAN_END_ROW + "=aay");
171    response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
172    assertEquals(200, response.getCode());
173    model = (CellSetModel) ush.unmarshal(response.getStream());
174    count = TestScannerResource.countCellSet(model);
175    RowModel startRow = model.getRows().get(0);
176    assertEquals("aaa", Bytes.toString(startRow.getKey()));
177    RowModel endRow = model.getRows().get(model.getRows().size() - 1);
178    assertEquals("aax", Bytes.toString(endRow.getKey()));
179    assertEquals(24, count);
180    checkRowsNotNull(model);
181
182    // Test with start row and limit.
183    builder = new StringBuilder();
184    builder.append("/*");
185    builder.append("?");
186    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
187    builder.append("&");
188    builder.append(Constants.SCAN_START_ROW + "=aaa");
189    builder.append("&");
190    builder.append(Constants.SCAN_LIMIT + "=15");
191    response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
192    assertEquals(200, response.getCode());
193    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
194    model = (CellSetModel) ush.unmarshal(response.getStream());
195    startRow = model.getRows().get(0);
196    assertEquals("aaa", Bytes.toString(startRow.getKey()));
197    count = TestScannerResource.countCellSet(model);
198    assertEquals(15, count);
199    checkRowsNotNull(model);
200  }
201
202  @Test
203  public void testSimpleScannerJson() throws IOException {
204    // Test scanning particular columns with limit.
205    StringBuilder builder = new StringBuilder();
206    builder.append("/*");
207    builder.append("?");
208    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
209    builder.append("&");
210    builder.append(Constants.SCAN_LIMIT + "=2");
211    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_JSON);
212    assertEquals(200, response.getCode());
213    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
214    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
215      MediaType.APPLICATION_JSON_TYPE);
216    CellSetModel model = mapper.readValue(response.getStream(), CellSetModel.class);
217    int count = TestScannerResource.countCellSet(model);
218    assertEquals(2, count);
219    checkRowsNotNull(model);
220
221    // Test scanning with no limit.
222    builder = new StringBuilder();
223    builder.append("/*");
224    builder.append("?");
225    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_2);
226    response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_JSON);
227    assertEquals(200, response.getCode());
228    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
229    model = mapper.readValue(response.getStream(), CellSetModel.class);
230    count = TestScannerResource.countCellSet(model);
231    assertEquals(expectedRows2, count);
232    checkRowsNotNull(model);
233
234    // Test with start row and end row.
235    builder = new StringBuilder();
236    builder.append("/*");
237    builder.append("?");
238    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
239    builder.append("&");
240    builder.append(Constants.SCAN_START_ROW + "=aaa");
241    builder.append("&");
242    builder.append(Constants.SCAN_END_ROW + "=aay");
243    response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_JSON);
244    assertEquals(200, response.getCode());
245    model = mapper.readValue(response.getStream(), CellSetModel.class);
246    RowModel startRow = model.getRows().get(0);
247    assertEquals("aaa", Bytes.toString(startRow.getKey()));
248    RowModel endRow = model.getRows().get(model.getRows().size() - 1);
249    assertEquals("aax", Bytes.toString(endRow.getKey()));
250    count = TestScannerResource.countCellSet(model);
251    assertEquals(24, count);
252    checkRowsNotNull(model);
253  }
254
255  /**
256   * An example to scan using listener in unmarshaller for XML.
257   * @throws Exception the exception
258   */
259  @Test
260  public void testScanUsingListenerUnmarshallerXML() throws Exception {
261    StringBuilder builder = new StringBuilder();
262    builder.append("/*");
263    builder.append("?");
264    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
265    builder.append("&");
266    builder.append(Constants.SCAN_LIMIT + "=10");
267    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
268    assertEquals(200, response.getCode());
269    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
270    JAXBContext context =
271      JAXBContext.newInstance(ClientSideCellSetModel.class, RowModel.class, CellModel.class);
272    Unmarshaller unmarshaller = context.createUnmarshaller();
273
274    final ClientSideCellSetModel.Listener listener = new ClientSideCellSetModel.Listener() {
275      @Override
276      public void handleRowModel(ClientSideCellSetModel helper, RowModel row) {
277        assertTrue(row.getKey() != null);
278        assertTrue(row.getCells().size() > 0);
279      }
280    };
281
282    // install the callback on all ClientSideCellSetModel instances
283    unmarshaller.setListener(new Unmarshaller.Listener() {
284      @Override
285      public void beforeUnmarshal(Object target, Object parent) {
286        if (target instanceof ClientSideCellSetModel) {
287          ((ClientSideCellSetModel) target).setCellSetModelListener(listener);
288        }
289      }
290
291      @Override
292      public void afterUnmarshal(Object target, Object parent) {
293        if (target instanceof ClientSideCellSetModel) {
294          ((ClientSideCellSetModel) target).setCellSetModelListener(null);
295        }
296      }
297    });
298
299    // create a new XML parser
300    SAXParserFactory factory = SAXParserFactory.newInstance();
301    factory.setNamespaceAware(true);
302    XMLReader reader = factory.newSAXParser().getXMLReader();
303    reader.setContentHandler(unmarshaller.getUnmarshallerHandler());
304    assertFalse(ClientSideCellSetModel.listenerInvoked);
305    reader.parse(new InputSource(response.getStream()));
306    assertTrue(ClientSideCellSetModel.listenerInvoked);
307
308  }
309
310  @Test
311  public void testStreamingJSON() throws Exception {
312    // Test with start row and end row.
313    StringBuilder builder = new StringBuilder();
314    builder.append("/*");
315    builder.append("?");
316    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
317    builder.append("&");
318    builder.append(Constants.SCAN_START_ROW + "=aaa");
319    builder.append("&");
320    builder.append(Constants.SCAN_END_ROW + "=aay");
321    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_JSON);
322    assertEquals(200, response.getCode());
323
324    int count = 0;
325    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
326      MediaType.APPLICATION_JSON_TYPE);
327    JsonFactory jfactory = new JsonFactory(mapper);
328    JsonParser jParser = jfactory.createJsonParser(response.getStream());
329    boolean found = false;
330    while (jParser.nextToken() != JsonToken.END_OBJECT) {
331      if (jParser.getCurrentToken() == JsonToken.START_OBJECT && found) {
332        RowModel row = jParser.readValueAs(RowModel.class);
333        assertNotNull(row.getKey());
334        for (int i = 0; i < row.getCells().size(); i++) {
335          if (count == 0) {
336            assertEquals("aaa", Bytes.toString(row.getKey()));
337          }
338          if (count == 23) {
339            assertEquals("aax", Bytes.toString(row.getKey()));
340          }
341          count++;
342        }
343        jParser.skipChildren();
344      } else {
345        found = jParser.getCurrentToken() == JsonToken.START_ARRAY;
346      }
347    }
348    assertEquals(24, count);
349  }
350
351  @Test
352  public void testSimpleScannerProtobuf() throws Exception {
353    StringBuilder builder = new StringBuilder();
354    builder.append("/*");
355    builder.append("?");
356    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
357    builder.append("&");
358    builder.append(Constants.SCAN_LIMIT + "=15");
359    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_PROTOBUF);
360    assertEquals(200, response.getCode());
361    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
362    int rowCount = readProtobufStream(response.getStream());
363    assertEquals(15, rowCount);
364
365    // Test with start row and end row.
366    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_START_ROW + "=aaa");
372    builder.append("&");
373    builder.append(Constants.SCAN_END_ROW + "=aay");
374    response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_PROTOBUF);
375    assertEquals(200, response.getCode());
376    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
377    rowCount = readProtobufStream(response.getStream());
378    assertEquals(24, rowCount);
379  }
380
381  private void checkRowsNotNull(CellSetModel model) {
382    for (RowModel row : model.getRows()) {
383      assertTrue(row.getKey() != null);
384      assertTrue(row.getCells().size() > 0);
385    }
386  }
387
388  /**
389   * Read protobuf stream.
390   * @param inputStream the input stream
391   * @return The number of rows in the cell set model.
392   * @throws IOException Signals that an I/O exception has occurred.
393   */
394  public int readProtobufStream(InputStream inputStream) throws IOException {
395    DataInputStream stream = new DataInputStream(inputStream);
396    CellSetModel model = null;
397    int rowCount = 0;
398    try {
399      while (true) {
400        byte[] lengthBytes = new byte[2];
401        int readBytes = stream.read(lengthBytes);
402        if (readBytes == -1) {
403          break;
404        }
405        assertEquals(2, readBytes);
406        int length = Bytes.toShort(lengthBytes);
407        byte[] cellset = new byte[length];
408        stream.read(cellset);
409        model = new CellSetModel();
410        model.getObjectFromMessage(cellset);
411        checkRowsNotNull(model);
412        rowCount = rowCount + TestScannerResource.countCellSet(model);
413      }
414    } catch (EOFException exp) {
415      exp.printStackTrace();
416    } finally {
417      stream.close();
418    }
419    return rowCount;
420  }
421
422  @Test
423  public void testScanningUnknownColumnJson() throws IOException {
424    // Test scanning particular columns with limit.
425    StringBuilder builder = new StringBuilder();
426    builder.append("/*");
427    builder.append("?");
428    builder.append(Constants.SCAN_COLUMN + "=a:test");
429    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_JSON);
430    assertEquals(200, response.getCode());
431    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
432    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
433      MediaType.APPLICATION_JSON_TYPE);
434    CellSetModel model = mapper.readValue(response.getStream(), CellSetModel.class);
435    int count = TestScannerResource.countCellSet(model);
436    assertEquals(0, count);
437  }
438
439  @Test
440  public void testSimpleFilter() throws IOException, JAXBException {
441    StringBuilder builder = new StringBuilder();
442    builder.append("/*");
443    builder.append("?");
444    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
445    builder.append("&");
446    builder.append(Constants.SCAN_START_ROW + "=aaa");
447    builder.append("&");
448    builder.append(Constants.SCAN_END_ROW + "=aay");
449    builder.append("&");
450    builder.append(Constants.SCAN_FILTER + "=" + URLEncoder.encode("PrefixFilter('aab')", "UTF-8"));
451    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
452    assertEquals(200, response.getCode());
453    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
454    Unmarshaller ush = ctx.createUnmarshaller();
455    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
456    int count = TestScannerResource.countCellSet(model);
457    assertEquals(1, count);
458    assertEquals("aab",
459      new String(model.getRows().get(0).getCells().get(0).getValue(), StandardCharsets.UTF_8));
460  }
461
462  @Test
463  public void testQualifierAndPrefixFilters() throws IOException, JAXBException {
464    StringBuilder builder = new StringBuilder();
465    builder.append("/abc*");
466    builder.append("?");
467    builder.append(
468      Constants.SCAN_FILTER + "=" + URLEncoder.encode("QualifierFilter(=,'binary:1')", "UTF-8"));
469    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
470    assertEquals(200, response.getCode());
471    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
472    Unmarshaller ush = ctx.createUnmarshaller();
473    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
474    int count = TestScannerResource.countCellSet(model);
475    assertEquals(1, count);
476    assertEquals("abc",
477      new String(model.getRows().get(0).getCells().get(0).getValue(), StandardCharsets.UTF_8));
478  }
479
480  @Test
481  public void testCompoundFilter() throws IOException, JAXBException {
482    StringBuilder builder = new StringBuilder();
483    builder.append("/*");
484    builder.append("?");
485    builder.append(Constants.SCAN_FILTER + "="
486      + URLEncoder.encode("PrefixFilter('abc') AND QualifierFilter(=,'binary:1')", "UTF-8"));
487    Response response = 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 testCustomFilter() throws IOException, JAXBException {
500    StringBuilder builder = new StringBuilder();
501    builder.append("/a*");
502    builder.append("?");
503    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
504    builder.append("&");
505    builder.append(Constants.SCAN_FILTER + "=" + URLEncoder.encode("CustomFilter('abc')", "UTF-8"));
506    Response response = 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 testNegativeCustomFilter() throws IOException, JAXBException {
519    StringBuilder builder = new StringBuilder();
520    builder.append("/b*");
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 = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
526    assertEquals(200, response.getCode());
527    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
528    Unmarshaller ush = ctx.createUnmarshaller();
529    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
530    int count = TestScannerResource.countCellSet(model);
531    // Should return no rows as the filters conflict
532    assertEquals(0, count);
533  }
534
535  @Test
536  public void testReversed() throws IOException, JAXBException {
537    StringBuilder builder = new StringBuilder();
538    builder.append("/*");
539    builder.append("?");
540    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
541    builder.append("&");
542    builder.append(Constants.SCAN_START_ROW + "=aaa");
543    builder.append("&");
544    builder.append(Constants.SCAN_END_ROW + "=aay");
545    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
546    assertEquals(200, response.getCode());
547    JAXBContext ctx = JAXBContext.newInstance(CellSetModel.class);
548    Unmarshaller ush = ctx.createUnmarshaller();
549    CellSetModel model = (CellSetModel) ush.unmarshal(response.getStream());
550    int count = TestScannerResource.countCellSet(model);
551    assertEquals(24, count);
552    List<RowModel> rowModels = model.getRows().subList(1, count);
553
554    // reversed
555    builder = new StringBuilder();
556    builder.append("/*");
557    builder.append("?");
558    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
559    builder.append("&");
560    builder.append(Constants.SCAN_START_ROW + "=aay");
561    builder.append("&");
562    builder.append(Constants.SCAN_END_ROW + "=aaa");
563    builder.append("&");
564    builder.append(Constants.SCAN_REVERSED + "=true");
565    response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_XML);
566    assertEquals(200, response.getCode());
567    model = (CellSetModel) ush.unmarshal(response.getStream());
568    count = TestScannerResource.countCellSet(model);
569    assertEquals(24, count);
570    List<RowModel> reversedRowModels = model.getRows().subList(1, count);
571
572    Collections.reverse(reversedRowModels);
573    assertEquals(rowModels.size(), reversedRowModels.size());
574    for (int i = 0; i < rowModels.size(); i++) {
575      RowModel rowModel = rowModels.get(i);
576      RowModel reversedRowModel = reversedRowModels.get(i);
577
578      assertEquals(new String(rowModel.getKey(), StandardCharsets.UTF_8),
579        new String(reversedRowModel.getKey(), StandardCharsets.UTF_8));
580      assertEquals(new String(rowModel.getCells().get(0).getValue(), StandardCharsets.UTF_8),
581        new String(reversedRowModel.getCells().get(0).getValue(), StandardCharsets.UTF_8));
582    }
583  }
584
585  @Test
586  public void testColumnWithEmptyQualifier() throws IOException {
587    // Test scanning with empty qualifier
588    StringBuilder builder = new StringBuilder();
589    builder.append("/*");
590    builder.append("?");
591    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_EMPTY);
592    Response response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_JSON);
593    assertEquals(200, response.getCode());
594    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
595    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
596      MediaType.APPLICATION_JSON_TYPE);
597    CellSetModel model = mapper.readValue(response.getStream(), CellSetModel.class);
598    int count = TestScannerResource.countCellSet(model);
599    assertEquals(expectedRows3, count);
600    checkRowsNotNull(model);
601    RowModel startRow = model.getRows().get(0);
602    assertEquals("aaa", Bytes.toString(startRow.getKey()));
603    assertEquals(1, startRow.getCells().size());
604
605    // Test scanning with empty qualifier and normal qualifier
606    builder = new StringBuilder();
607    builder.append("/*");
608    builder.append("?");
609    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_1);
610    builder.append("&");
611    builder.append(Constants.SCAN_COLUMN + "=" + COLUMN_EMPTY);
612    response = client.get("/" + TABLE + builder.toString(), Constants.MIMETYPE_JSON);
613    assertEquals(200, response.getCode());
614    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
615    mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
616      MediaType.APPLICATION_JSON_TYPE);
617    model = mapper.readValue(response.getStream(), CellSetModel.class);
618    count = TestScannerResource.countCellSet(model);
619    assertEquals(expectedRows1 + expectedRows3, count);
620    checkRowsNotNull(model);
621  }
622
623  public static class CustomFilter extends PrefixFilter {
624    private byte[] key = null;
625
626    public CustomFilter(byte[] key) {
627      super(key);
628    }
629
630    @Override
631    public boolean filterRowKey(Cell cell) {
632      int cmp = Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(),
633        this.key, 0, this.key.length);
634      return cmp != 0;
635    }
636
637    public static Filter createFilterFromArguments(ArrayList<byte[]> filterArguments) {
638      byte[] prefix = ParseFilter.removeQuotesFromByteArray(filterArguments.get(0));
639      return new CustomFilter(prefix);
640    }
641  }
642
643  /**
644   * The Class ClientSideCellSetModel which mimics cell set model, and contains listener to perform
645   * user defined operations on the row model.
646   */
647  @XmlRootElement(name = "CellSet")
648  @XmlAccessorType(XmlAccessType.FIELD)
649  public static class ClientSideCellSetModel implements Serializable {
650    private static final long serialVersionUID = 1L;
651
652    /**
653     * This list is not a real list; instead it will notify a listener whenever JAXB has
654     * unmarshalled the next row.
655     */
656    @XmlElement(name = "Row")
657    private List<RowModel> row;
658
659    static boolean listenerInvoked = false;
660
661    /**
662     * Install a listener for row model on this object. If l is null, the listener is removed again.
663     */
664    public void setCellSetModelListener(final Listener l) {
665      row = (l == null) ? null : new ArrayList<RowModel>() {
666        private static final long serialVersionUID = 1L;
667
668        @Override
669        public boolean add(RowModel o) {
670          l.handleRowModel(ClientSideCellSetModel.this, o);
671          listenerInvoked = true;
672          return false;
673        }
674      };
675    }
676
677    /**
678     * This listener is invoked every time a new row model is unmarshalled.
679     */
680    public interface Listener {
681      void handleRowModel(ClientSideCellSetModel helper, RowModel rowModel);
682    }
683  }
684}