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