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